feat(alerts): always-visible dismiss/ack actions + Dismiss all (service, endpoint, UI)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 17:53:12 +02:00
parent 13efe177a5
commit 6c4490f653
7 changed files with 180 additions and 10 deletions

View File

@@ -62,7 +62,7 @@ export function AlertCard({ alert, readOnly = false }: AlertCardProps) {
</div>
</div>
{!readOnly ? (
<div className="flex shrink-0 items-start gap-1 opacity-0 transition-opacity duration-base ease-spring group-hover:opacity-100 focus-within:opacity-100">
<div className="flex shrink-0 items-start gap-1 text-muted-foreground">
{!acknowledged ? (
<Button
variant="ghost"

View File

@@ -4,10 +4,11 @@ import { useState } from 'react';
import { ShieldAlert } from 'lucide-react';
import { PageHeader } from '@/components/shared/page-header';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Skeleton } from '@/components/ui/skeleton';
import { AlertCard, AlertCardEmpty } from './alert-card';
import { useAlertCount, useAlertList, useAlertRealtime } from './use-alerts';
import { useAlertCount, useAlertList, useAlertRealtime, useDismissAll } from './use-alerts';
import type { AlertStatus } from './types';
/**
@@ -30,6 +31,7 @@ export function AlertsPageShell({ embedded = false }: AlertsPageShellProps = {})
const total = count?.total ?? 0;
const alerts = data?.data ?? [];
const dismissAll = useDismissAll();
return (
<div className={embedded ? 'space-y-3' : 'space-y-6'}>
@@ -62,6 +64,18 @@ export function AlertsPageShell({ embedded = false }: AlertsPageShellProps = {})
</TabsList>
<TabsContent value={tab} className="mt-4 space-y-2">
{tab === 'open' && alerts.length > 0 ? (
<div className="flex justify-end">
<Button
variant="ghost"
size="sm"
onClick={() => dismissAll.mutate({})}
disabled={dismissAll.isPending}
>
Dismiss all
</Button>
</div>
) : null}
{isLoading ? (
<div className="space-y-2">
<Skeleton className="h-20 w-full" />

View File

@@ -41,6 +41,15 @@ export function useAlertActions() {
return { acknowledge, dismiss };
}
export function useDismissAll() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (filter: { ruleId?: string; severity?: string } = {}) =>
apiFetch('/api/v1/alerts/dismiss-all', { method: 'POST', body: filter }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['alerts'] }),
});
}
export function useAlertRealtime() {
useRealtimeInvalidation({
'alert:created': [['alerts']],

View File

@@ -37,7 +37,12 @@ import { cn } from '@/lib/utils';
import { useNotifications } from '@/hooks/use-notifications';
import { NotificationItem } from '@/components/notifications/notification-item';
import { AlertCard, AlertCardEmpty } from '@/components/alerts/alert-card';
import { useAlertCount, useAlertList, useAlertRealtime } from '@/components/alerts/use-alerts';
import {
useAlertCount,
useAlertList,
useAlertRealtime,
useDismissAll,
} from '@/components/alerts/use-alerts';
interface NotificationListResponse {
data: Array<{
@@ -66,6 +71,7 @@ export function Inbox() {
const systemCritical = alertCount?.bySeverity.critical ?? 0;
const systemAlerts = alertList?.data ?? [];
const systemTop = systemAlerts.slice(0, 8);
const dismissAll = useDismissAll();
// ── Personal (notifications) ──
const { unreadCount: personalUnread } = useNotifications();
@@ -230,13 +236,25 @@ export function Inbox() {
<h4 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Active alerts
</h4>
<Link
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
href={portSlug ? (`/${portSlug}/alerts` as any) : ('/alerts' as any)}
className="text-xs text-muted-foreground hover:text-foreground"
>
View all
</Link>
<div className="flex items-center gap-3">
{systemAlerts.length > 0 ? (
<button
type="button"
onClick={() => dismissAll.mutate({})}
disabled={dismissAll.isPending}
className="text-xs text-muted-foreground hover:text-foreground disabled:opacity-50"
>
Dismiss all
</button>
) : null}
<Link
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
href={portSlug ? (`/${portSlug}/alerts` as any) : ('/alerts' as any)}
className="text-xs text-muted-foreground hover:text-foreground"
>
View all
</Link>
</div>
</div>
<Separator />
<ScrollArea className="max-h-[400px]">