import { useEffect, useRef, useState } from 'react'; import { assertActivityJournalPage, type ActivityJournalEvent } from '../../../shared/activityJournal.js'; import { compactActivityJournalEvents, activityJournalEventCopy, refreshStreakCopy, type ActivityJournalDisplayItem } from './activityJournalModel.js'; import { Drawer } from '../../ui/Drawer.js'; import { RailAction } from '../../ui/RailAction.js'; export function useActivityJournalFeature() { const [isOpen, setIsOpen] = useState(false); const panelRef = useRef(null); const toggleRef = useRef(null); const closeRef = useRef(null); useEffect(() => { if (!isOpen) return undefined; const frame = requestAnimationFrame(() => closeRef.current?.focus()); const close = (event: PointerEvent | KeyboardEvent) => { if (event.type === 'keydown' && (event as KeyboardEvent).key !== 'Escape') return; if (event.type !== 'keydown' && ( panelRef.current?.contains(event.target as Node) || toggleRef.current?.contains(event.target as Node) )) return; setIsOpen(false); }; document.addEventListener('pointerdown', close); document.addEventListener('keydown', close); return () => { cancelAnimationFrame(frame); document.removeEventListener('pointerdown', close); document.removeEventListener('keydown', close); requestAnimationFrame(() => { if (panelRef.current?.contains(document.activeElement)) toggleRef.current?.focus(); }); }; }, [isOpen]); return { isOpen, panelRef, toggleRef, closeRef, close: () => setIsOpen(false), toggle: () => setIsOpen((value) => !value) }; } export type ActivityJournalFeature = ReturnType; export function ActivityJournalToggle({ feature, open, onToggle }: { feature: ActivityJournalFeature; open: boolean; onToggle: () => void; }) { return ; } function eventTargetRole(event: ActivityJournalEvent) { if (event.type === 'failover.switched') return event.data.toRole; if ( event.type === 'failover.reserve_unavailable' || event.type === 'failover.reserve_recovered' || (event.type === 'failover.recovered' && event.data.role === 'reserve') ) return 'reserve'; if ( event.type === 'failover.primary_unavailable' || event.type === 'failover.primary_recovered' || (event.type === 'failover.recovered' && event.data.role === 'primary') ) return 'primary'; return undefined; } function dayLabel(value: string) { const date = new Date(value); const today = new Date(); const startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const start = startDate.getTime(); const yesterday = new Date(startDate); yesterday.setDate(yesterday.getDate() - 1); const day = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); if (day === start) return 'Сегодня'; if (day === yesterday.getTime()) return 'Вчера'; return new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long' }).format(date); } export function ActivityJournalPanel({ feature, loadPage }: { feature: ActivityJournalFeature; loadPage: (cursor?: string | null) => Promise; }) { const [events, setEvents] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'refreshing' | 'older' | 'error'>('idle'); const [announcement, setAnnouncement] = useState(''); async function load(cursor: string | null = null, refresh = false) { setStatus(cursor ? 'older' : refresh ? 'refreshing' : 'loading'); try { const page = assertActivityJournalPage(await loadPage(cursor)); if (page.storage.status === 'error') throw new Error('journal unavailable'); if (refresh) { const known = new Set(events.map(({ id }) => id)); setAnnouncement(`Журнал обновлён, новых событий: ${page.events.filter(({ id }) => !known.has(id)).length}`); } setEvents((current) => cursor ? [...current, ...page.events] : page.events); setNextCursor(page.nextCursor); setStatus('ready'); } catch { setStatus('error'); } } useEffect(() => { if (feature.isOpen) void load(null, status !== 'idle'); }, [feature.isOpen]); const groups = compactActivityJournalEvents(events).reduce>((result, item) => { const label = dayLabel(item.event.occurredAt); const group = result.at(-1); if (group?.label === label) group.items.push(item); else result.push({ label, items: [item] }); return result; }, []); return
Важные события хранятся 30 дней

Журнал

{announcement} {status === 'error' &&
Журнал временно недоступен
} {status === 'loading' && !events.length ?
{[0, 1, 2, 3].map((value) => )}
: !events.length && status === 'ready' ?

За последние 30 дней важных событий пока нет

:
{groups.map((group) =>

{group.label}

    {group.items.map((item) => { const { event } = item; const [title, details] = refreshStreakCopy(item) || activityJournalEventCopy(event); return
  1. {event.source}
    {title}{details && {details}}{event.severity !== 'info' && {event.severity === 'error' ? 'Ошибка' : 'Внимание'}}
  2. ; })}
)}
}
; }