From 8d8bae4e183cde6a86ed24fef48ee38931fbcb74 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:49:23 +0200 Subject: [PATCH] feat(ui/alerts): InboxPage with ack + bulk-read actions AlertRow is reused by AllAlertsPage and HistoryPage. Marking a row as read happens when its link is followed (the detail sub-route will be added in phase 10 polish). FIRING rows get an amber left border. --- ui/src/pages/Alerts/AlertRow.tsx | 48 ++++++++++++++++++++++ ui/src/pages/Alerts/InboxPage.tsx | 47 ++++++++++++++++++++- ui/src/pages/Alerts/alerts-page.module.css | 18 ++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 ui/src/pages/Alerts/AlertRow.tsx create mode 100644 ui/src/pages/Alerts/alerts-page.module.css diff --git a/ui/src/pages/Alerts/AlertRow.tsx b/ui/src/pages/Alerts/AlertRow.tsx new file mode 100644 index 00000000..9f23c9bd --- /dev/null +++ b/ui/src/pages/Alerts/AlertRow.tsx @@ -0,0 +1,48 @@ +import { Link } from 'react-router'; +import { Button, useToast } from '@cameleer/design-system'; +import { AlertStateChip } from '../../components/AlertStateChip'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import type { AlertDto } from '../../api/queries/alerts'; +import { useAckAlert, useMarkAlertRead } from '../../api/queries/alerts'; +import css from './alerts-page.module.css'; + +export function AlertRow({ alert, unread }: { alert: AlertDto; unread: boolean }) { + const ack = useAckAlert(); + const markRead = useMarkAlertRead(); + const { toast } = useToast(); + + const onAck = async () => { + try { + await ack.mutateAsync(alert.id); + toast({ title: 'Acknowledged', description: alert.title, variant: 'success' }); + } catch (e) { + toast({ title: 'Ack failed', description: String(e), variant: 'error' }); + } + }; + + return ( +
+ +
+ markRead.mutate(alert.id)}> + {alert.title} + +
+ + {alert.firedAt} +
+

{alert.message}

+
+
+ {alert.state === 'FIRING' && ( + + )} +
+
+ ); +} diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 10f561b6..ba9f2849 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -1,3 +1,48 @@ +import { useMemo } from 'react'; +import { Button, SectionHeader, useToast } from '@cameleer/design-system'; +import { PageLoader } from '../../components/PageLoader'; +import { useAlerts, useBulkReadAlerts } from '../../api/queries/alerts'; +import { AlertRow } from './AlertRow'; +import css from './alerts-page.module.css'; + export default function InboxPage() { - return
InboxPage — coming soon
; + const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 100 }); + const bulkRead = useBulkReadAlerts(); + 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 rows = data ?? []; + + const onMarkAllRead = async () => { + if (unreadIds.length === 0) return; + try { + await bulkRead.mutateAsync(unreadIds); + toast({ title: `Marked ${unreadIds.length} as read`, variant: 'success' }); + } catch (e) { + toast({ title: 'Bulk read failed', description: String(e), variant: 'error' }); + } + }; + + return ( +
+
+ Inbox + +
+ {rows.length === 0 ? ( +
No open alerts for you in this environment.
+ ) : ( + rows.map((a) => ) + )} +
+ ); } diff --git a/ui/src/pages/Alerts/alerts-page.module.css b/ui/src/pages/Alerts/alerts-page.module.css new file mode 100644 index 00000000..71047bfd --- /dev/null +++ b/ui/src/pages/Alerts/alerts-page.module.css @@ -0,0 +1,18 @@ +.page { padding: 16px; display: flex; flex-direction: column; gap: 12px; } +.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; } +.row { + display: grid; + grid-template-columns: 72px 1fr auto; + gap: 12px; + padding: 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); +} +.rowUnread { border-left: 3px solid var(--accent); } +.body { display: flex; flex-direction: column; gap: 4px; min-width: 0; } +.meta { display: flex; gap: 8px; font-size: 12px; color: var(--muted); } +.time { font-variant-numeric: tabular-nums; } +.message { margin: 0; font-size: 13px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.actions { display: flex; align-items: center; } +.empty { padding: 48px; text-align: center; color: var(--muted); }