feat(ui/alerts): NotificationBell with Page Visibility poll pause
Adds a header bell component linking to /alerts/inbox with an unread-count badge for the selected environment. Polling pauses when the tab is hidden via TanStack Query's refetchIntervalInBackground:false (already set on useUnreadCount); the new usePageVisible hook gives components a re-renders-on-visibility-change signal for future defense-in-depth. Plan-prose deviation: the plan assumed UnreadCountResponse carries a bySeverity map for per-severity badge coloring, but the backend DTO only exposes a scalar `count`. The bell reads `data?.count` and renders a single var(--error) tint; a TODO references spec §13 for future per-severity work that would require expanding the DTO. Tests: usePageVisible toggles on visibilitychange events; NotificationBell renders the bell with no badge at count=0 and shows "3" at count=3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
ui/src/components/NotificationBell.tsx
Normal file
51
ui/src/components/NotificationBell.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Link } from 'react-router';
|
||||
import { Bell } from 'lucide-react';
|
||||
import { useUnreadCount } from '../api/queries/alerts';
|
||||
import { useSelectedEnv } from '../api/queries/alertMeta';
|
||||
import { usePageVisible } from '../hooks/usePageVisible';
|
||||
import css from './NotificationBell.module.css';
|
||||
|
||||
/**
|
||||
* Global notification bell shown in the layout header. Links to the alerts
|
||||
* inbox and renders a badge with the unread-alert count for the currently
|
||||
* selected environment.
|
||||
*
|
||||
* Polling is driven by `useUnreadCount` (30s interval, paused in background
|
||||
* via TanStack Query's `refetchIntervalInBackground: false`). The
|
||||
* `usePageVisible` hook is retained as a defense-in-depth signal so future
|
||||
* UI behavior (e.g. animations, live-region updates) can key off visibility
|
||||
* without re-wiring the polling logic.
|
||||
*
|
||||
* TODO (spec §13): per-severity badge coloring — the backend
|
||||
* `UnreadCountResponse` currently exposes only a scalar `count` field. To
|
||||
* colour the badge by max unread severity (CRITICAL → error, WARNING →
|
||||
* amber, INFO → muted) the DTO must grow a `bySeverity` map; deferred to a
|
||||
* future task. Until then the badge uses a single `var(--error)` tint.
|
||||
*/
|
||||
export function NotificationBell() {
|
||||
const env = useSelectedEnv();
|
||||
// Subscribe to visibility so the component re-renders on tab focus, even
|
||||
// though the polling pause itself is handled inside useUnreadCount.
|
||||
usePageVisible();
|
||||
const { data } = useUnreadCount();
|
||||
|
||||
const count = data?.count ?? 0;
|
||||
|
||||
if (!env) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/alerts/inbox"
|
||||
role="button"
|
||||
aria-label={`Notifications (${count} unread)`}
|
||||
className={css.bell}
|
||||
>
|
||||
<Bell size={16} />
|
||||
{count > 0 && (
|
||||
<span className={css.badge} aria-hidden>
|
||||
{count > 99 ? '99+' : count}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user