diff --git a/docs/features/alerts-notifications.mdx b/docs/features/alerts-notifications.mdx index 29e33be3..418bdf38 100644 --- a/docs/features/alerts-notifications.mdx +++ b/docs/features/alerts-notifications.mdx @@ -101,19 +101,21 @@ All dispatched notifications appear in the **notification bell** in the top-righ Notification popover showing recent alert entries with level badges and timestamps -Click the bell to open the notification popover, which shows: +Click the bell to open the notification popover. Each entry shows: -- **Level badge** for each entry (ERROR in red, WARNING in amber, INFO in default) +- **Level badge** (ERROR in red, WARNING in amber, INFO in default) - **Node name** badge for notifications from remote nodes - **Timestamp** of when the alert was triggered - **Alert message** describing what was breached -The popover header includes: +A toolbar above the list provides filtering and bulk actions: -| Action | What it does | -|--------|--------------| -| **Mark all as read** | Marks all notifications as read (removes the red dot) | -| **Clear all** | Deletes all notifications from the list | +| Control | What it does | +|---------|--------------| +| **All / Unread / Alerts** | Switches the list between every notification, unread only, or alert-level only | +| **Filter toggle** | Reveals dropdowns to filter by node and by notification type; an accent dot on the icon indicates an active filter | +| **Mark all as read** | Marks every notification as read (removes the red dot) | +| **Clear all** | Deletes every notification from the list | You can also dismiss individual notifications by hovering over them and clicking the dismiss button. diff --git a/docs/images/alerts-notifications/notification-popover.png b/docs/images/alerts-notifications/notification-popover.png index edee97c2..c31b9d35 100644 Binary files a/docs/images/alerts-notifications/notification-popover.png and b/docs/images/alerts-notifications/notification-popover.png differ diff --git a/frontend/src/components/NotificationPanel.tsx b/frontend/src/components/NotificationPanel.tsx index a7ce0d66..efe2c726 100644 --- a/frontend/src/components/NotificationPanel.tsx +++ b/frontend/src/components/NotificationPanel.tsx @@ -7,6 +7,8 @@ import { AlertOctagon, X, Trash2, + SlidersHorizontal, + CheckCheck, } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -30,6 +32,9 @@ type NotifFilter = 'all' | 'unread' | 'alerts'; type NodeFilter = typeof NODE_FILTER_ALL | number; type CategoryFilter = typeof CATEGORY_FILTER_ALL | NotificationCategory; +const FILTER_LABEL_CLASS = 'font-mono text-[10px] uppercase tracking-[0.14em]'; +const FILTER_TRIGGER_CLASS = `h-7 w-[140px] border-card-border bg-card px-2 text-stat-subtitle shadow-none focus:ring-0 ${FILTER_LABEL_CLASS}`; + type LevelConfig = { icon: LucideIcon; iconClass: string; @@ -136,6 +141,10 @@ export function NotificationPanel({ const [nodeFilter, setNodeFilter] = useState(NODE_FILTER_ALL); const [categoryFilter, setCategoryFilter] = useState(CATEGORY_FILTER_ALL); const [open, setOpen] = useState(false); + const [showFilters, setShowFilters] = useState(false); + + const hasActiveFilters = + nodeFilter !== NODE_FILTER_ALL || categoryFilter !== CATEGORY_FILTER_ALL; const unreadCount = useMemo( () => notifications.filter((n) => !n.is_read).length, @@ -215,25 +224,59 @@ export function NotificationPanel({
-
- - Notifications + + Notifications + + {unreadCount > 0 ? ( + + {unreadCount} unread - {unreadCount > 0 ? ( - - {unreadCount} unread - + ) : null} +
+
+ + {/* Filter segment */} +
+
+ +
+ {notifications.length > 0 ? ( + ) : null} -
-
{unreadCount > 0 ? ( ) : null} {notifications.length > 0 ? ( @@ -243,68 +286,55 @@ export function NotificationPanel({ onClick={onClearAll} className="h-7 w-7 text-stat-subtitle hover:text-destructive" title="Clear all" + aria-label="Clear all notifications" > ) : null}
-
- - {/* Filter segment */} -
- {showNodeFilter ? ( - setNodeFilter(v === NODE_FILTER_ALL ? NODE_FILTER_ALL : Number(v))} + > + + + + + + All nodes + + {nodes.map((n) => ( + + {n.name} + + ))} + + + ) : null} + + {(Object.keys(CATEGORY_LABELS) as NotificationCategory[]).map((cat) => ( + + {CATEGORY_LABELS[cat]} + + ))} + + +
) : null} - -
- -
{/* Stream */}