mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:31:12 -05:00
Investigate and address why the Resolve action isn’t triggering despite superuser role. Add diagnostic logging and ensure the resolve mutation is invoked with correct data, plus prep for validating frontend event wiring and mutation calls.
250 lines
10 KiB
TypeScript
250 lines
10 KiB
TypeScript
import { useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { AlertCircle, AlertTriangle, Info, ChevronDown, ChevronUp, Clock, Zap, RefreshCw, Loader2 } from 'lucide-react';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import type { GroupedAlert } from '@/hooks/admin/useGroupedAlerts';
|
|
import { useResolveAlertGroup, useSnoozeAlertGroup } from '@/hooks/admin/useAlertGroupActions';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
|
|
interface GroupedAlertsPanelProps {
|
|
alerts?: GroupedAlert[];
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const SEVERITY_CONFIG = {
|
|
critical: { color: 'text-destructive', icon: AlertCircle, label: 'Critical', badge: 'bg-destructive/10 text-destructive' },
|
|
high: { color: 'text-orange-500', icon: AlertTriangle, label: 'High', badge: 'bg-orange-500/10 text-orange-500' },
|
|
medium: { color: 'text-yellow-500', icon: AlertTriangle, label: 'Medium', badge: 'bg-yellow-500/10 text-yellow-500' },
|
|
low: { color: 'text-blue-500', icon: Info, label: 'Low', badge: 'bg-blue-500/10 text-blue-500' },
|
|
};
|
|
|
|
export function GroupedAlertsPanel({ alerts, isLoading }: GroupedAlertsPanelProps) {
|
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
|
const resolveGroup = useResolveAlertGroup();
|
|
const snoozeGroup = useSnoozeAlertGroup();
|
|
|
|
// Filter out snoozed alerts
|
|
const snoozedAlerts = JSON.parse(localStorage.getItem('snoozed_alerts') || '{}');
|
|
const visibleAlerts = alerts?.filter(alert => {
|
|
const snoozeUntil = snoozedAlerts[alert.group_key];
|
|
return !snoozeUntil || Date.now() > snoozeUntil;
|
|
});
|
|
|
|
const handleResolveGroup = (alert: GroupedAlert) => {
|
|
console.log('🔴 Resolve button clicked', {
|
|
alertIds: alert.alert_ids,
|
|
source: alert.source,
|
|
alert,
|
|
});
|
|
resolveGroup.mutate({
|
|
alertIds: alert.alert_ids,
|
|
source: alert.source,
|
|
});
|
|
};
|
|
|
|
const handleSnooze = (alert: GroupedAlert, durationMs: number) => {
|
|
snoozeGroup.mutate({
|
|
groupKey: alert.group_key,
|
|
duration: durationMs,
|
|
});
|
|
};
|
|
|
|
const toggleExpanded = (groupKey: string) => {
|
|
setExpandedGroups(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(groupKey)) {
|
|
next.delete(groupKey);
|
|
} else {
|
|
next.add(groupKey);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Critical Alerts</CardTitle>
|
|
<CardDescription>Loading alerts...</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!visibleAlerts || visibleAlerts.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Critical Alerts</CardTitle>
|
|
<CardDescription>All systems operational</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
|
|
<AlertCircle className="h-12 w-12 mb-2 opacity-50" />
|
|
<p>No active alerts</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const totalAlerts = visibleAlerts.reduce((sum, alert) => sum + alert.unresolved_count, 0);
|
|
const recurringCount = visibleAlerts.filter(a => a.is_recurring).length;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span>Critical Alerts</span>
|
|
<span className="text-sm font-normal text-muted-foreground">
|
|
{visibleAlerts.length} {visibleAlerts.length === 1 ? 'group' : 'groups'} • {totalAlerts} total alerts
|
|
{recurringCount > 0 && ` • ${recurringCount} recurring`}
|
|
</span>
|
|
</CardTitle>
|
|
<CardDescription>Grouped by type to reduce alert fatigue</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{visibleAlerts.map(alert => {
|
|
const config = SEVERITY_CONFIG[alert.severity];
|
|
const Icon = config.icon;
|
|
const isExpanded = expandedGroups.has(alert.group_key);
|
|
|
|
return (
|
|
<div
|
|
key={alert.group_key}
|
|
className="border rounded-lg p-4 space-y-2 bg-card hover:bg-accent/5 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex items-start gap-3 flex-1">
|
|
<Icon className={`h-5 w-5 mt-0.5 ${config.color}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap mb-1">
|
|
<span className={`text-xs font-medium px-2 py-0.5 rounded ${config.badge}`}>
|
|
{config.label}
|
|
</span>
|
|
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground">
|
|
{alert.source === 'system' ? 'System' : 'Rate Limit'}
|
|
</span>
|
|
{alert.is_active && (
|
|
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded bg-green-500/10 text-green-600">
|
|
<Zap className="h-3 w-3" />
|
|
Active
|
|
</span>
|
|
)}
|
|
{alert.is_recurring && (
|
|
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded bg-amber-500/10 text-amber-600">
|
|
<RefreshCw className="h-3 w-3" />
|
|
Recurring
|
|
</span>
|
|
)}
|
|
<span className="text-xs font-semibold px-2 py-0.5 rounded bg-primary/10 text-primary">
|
|
{alert.unresolved_count} {alert.unresolved_count === 1 ? 'alert' : 'alerts'}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm font-medium">
|
|
{alert.alert_type || alert.metric_type || 'Alert'}
|
|
{alert.function_name && <span className="text-muted-foreground"> • {alert.function_name}</span>}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
{alert.messages[0]}
|
|
</p>
|
|
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
First: {formatDistanceToNow(new Date(alert.first_seen), { addSuffix: true })}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
Last: {formatDistanceToNow(new Date(alert.last_seen), { addSuffix: true })}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{alert.alert_count > 1 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => toggleExpanded(alert.group_key)}
|
|
>
|
|
{isExpanded ? (
|
|
<>
|
|
<ChevronUp className="h-4 w-4 mr-1" />
|
|
Hide
|
|
</>
|
|
) : (
|
|
<>
|
|
<ChevronDown className="h-4 w-4 mr-1" />
|
|
Show all {alert.alert_count}
|
|
</>
|
|
)}
|
|
</Button>
|
|
)}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm">
|
|
Snooze
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleSnooze(alert, 3600000)}>
|
|
1 hour
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleSnooze(alert, 14400000)}>
|
|
4 hours
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleSnooze(alert, 86400000)}>
|
|
24 hours
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onClick={() => handleResolveGroup(alert)}
|
|
disabled={resolveGroup.isPending}
|
|
>
|
|
{resolveGroup.isPending ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Resolving...
|
|
</>
|
|
) : (
|
|
'Resolve All'
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && alert.messages.length > 1 && (
|
|
<div className="mt-3 pt-3 border-t space-y-2">
|
|
<p className="text-xs font-medium text-muted-foreground">All messages in this group:</p>
|
|
<div className="space-y-1 max-h-64 overflow-y-auto">
|
|
{alert.messages.map((message, idx) => (
|
|
<div key={idx} className="text-xs p-2 rounded bg-muted/50">
|
|
{message}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|