diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index ba9f2849..1c5213f8 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -1,47 +1,177 @@ -import { useMemo } from 'react'; -import { Button, SectionHeader, useToast } from '@cameleer/design-system'; +import { useMemo, useState } from 'react'; +import { Link } from 'react-router'; +import { Inbox } from 'lucide-react'; +import { + Button, SectionHeader, DataTable, EmptyState, useToast, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; -import { useAlerts, useBulkReadAlerts } from '../../api/queries/alerts'; -import { AlertRow } from './AlertRow'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { AlertStateChip } from '../../components/AlertStateChip'; +import { + useAlerts, useAckAlert, useBulkReadAlerts, useMarkAlertRead, + type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; export default function InboxPage() { const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 100 }); const bulkRead = useBulkReadAlerts(); + const markRead = useMarkAlertRead(); + const ack = useAckAlert(); const { toast } = useToast(); - const unreadIds = useMemo( - () => (data ?? []).filter((a) => a.state === 'FIRING').map((a) => a.id), - [data], - ); - - if (isLoading) return ; - if (error) return
Failed to load alerts: {String(error)}
; - + const [selected, setSelected] = useState>(new Set()); const rows = data ?? []; - const onMarkAllRead = async () => { - if (unreadIds.length === 0) return; + const unreadIds = useMemo( + () => rows.filter((a) => a.state === 'FIRING').map((a) => a.id), + [rows], + ); + + const toggleSelected = (id: string) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + const onAck = async (id: string, title?: string) => { try { - await bulkRead.mutateAsync(unreadIds); - toast({ title: `Marked ${unreadIds.length} as read`, variant: 'success' }); + await ack.mutateAsync(id); + toast({ title: 'Acknowledged', description: title, variant: 'success' }); + } catch (e) { + toast({ title: 'Ack failed', description: String(e), variant: 'error' }); + } + }; + + const onBulkRead = async (ids: string[]) => { + if (ids.length === 0) return; + try { + await bulkRead.mutateAsync(ids); + setSelected(new Set()); + toast({ title: `Marked ${ids.length} as read`, variant: 'success' }); } catch (e) { toast({ title: 'Bulk read failed', description: String(e), variant: 'error' }); } }; + const columns: Column[] = [ + { + key: 'select', header: '', width: '40px', + render: (_, row) => ( + toggleSelected(row.id)} + aria-label={`Select ${row.title ?? row.id}`} + onClick={(e) => e.stopPropagation()} + /> + ), + }, + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => + row.severity ? : null, + }, + { + key: 'state', header: 'State', width: '140px', + render: (_, row) => + row.state ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => { + const unread = row.state === 'FIRING'; + return ( +
+ markRead.mutate(row.id)}> + {row.title ?? '(untitled)'} + + {row.message && {row.message}} +
+ ); + }, + }, + { + key: 'age', header: 'Age', width: '100px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + { + key: 'ack', header: '', width: '70px', + render: (_, row) => + row.state === 'FIRING' ? ( + + ) : null, + }, + ]; + + if (isLoading) return ; + if (error) return
Failed to load alerts: {String(error)}
; + + const selectedIds = Array.from(selected); + return (
Inbox -
+ +
+ + {selectedIds.length > 0 + ? `${selectedIds.length} selected` + : `${unreadIds.length} unread`} + +
+ + +
+
+ {rows.length === 0 ? ( -
No open alerts for you in this environment.
+ } + title="All clear" + description="No open alerts for you in this environment." + /> ) : ( - rows.map((a) => ) +
+ + columns={columns as Column[]} + data={rows as Array} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
)}
);