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}
+ />
+
)}
);