import { useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { query } from '../api/client'; import { useQuery } from '../lib/hooks'; import { ago, num, when } from '../lib/format'; import { Banner, Button, Card, EmptyRow, Field, Loading, PageHead, Segments, TableWrap, Tag, Tiles, } from '../components/ui'; import { Glyph } from '../components/Icon'; import { eventIcon, eventTone, eventTypeLabel, useNotifications, type AdminEvent, type EventTypeCount, } from '../lib/notifications'; /* The activity feed in full: the bell's dropdown with filters and no twenty-row cap. * * It reads its own window from the server rather than the provider's cached list, because * the provider holds only the most recent fifty — enough for a badge and a dropdown, not * enough to answer "what happened on Tuesday". The badge and the "mark all read" button * still come from the provider, so this page and the bell can never disagree about how * much is unread. */ interface FeedResponse { events: AdminEvent[]; total: number; unread: number; types: EventTypeCount[]; subscribers: number; } const WINDOWS = [ { value: 1, label: 'Today' }, { value: 7, label: '7 days' }, { value: 30, label: '30 days' }, ]; export function ActivityPage() { const { unread, connected, markAllRead, reload: reloadBell } = useNotifications(); const [days, setDays] = useState(7); const [type, setType] = useState(''); const [severity, setSeverity] = useState(''); const [unreadOnly, setUnreadOnly] = useState(false); const [page, setPage] = useState(0); const limit = 100; const path = useMemo( () => `/admin/api/notifications${query({ days, type, severity, unread: unreadOnly, limit, offset: page * limit, })}`, [days, type, severity, unreadOnly, page], ); const { data, error, loading, reload } = useQuery(path); const markAll = async () => { await markAllRead(); await reload(); }; return ( <> 0 ? ( ) : undefined } /> 0 ? 'warn' : undefined }, { label: 'Kinds seen', value: num(data?.types.length ?? 0), icon: 'list', tone: 'note' }, { label: 'Live feed', value: connected ? 'connected' : 'reconnecting', small: true, icon: 'pulse', tone: connected ? 'ok' : 'warn', }, ]} />
({ value: w.value, label: w.label }))} onChange={(next) => { setDays(next); setPage(0); }} /> {/* Built from what has actually been published, so the filter can neither offer a kind that matches nothing nor miss one a service added after this shipped. */}
{loading ? ( ) : ( limit ? ( <> ) : undefined } > {(data?.events.length ?? 0) === 0 ? ( Nothing has happened in this window. ) : ( data?.events.map((event) => ( )) )}
When Kind What happened Who What
{ago(event.occurredAt)} {eventTypeLabel(event.type)} {event.title} {event.summary ?
{event.summary}
: null}
{event.actor || '—'} {event.target || '—'} {!event.readAt ? new : null} {event.link ? ( Open ) : null}
)} ); }