mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Add comprehensive monitoring dashboard scaffolding: - Extend queryKeys with monitoring keys - Create hooks: useCombinedAlerts, useRecentActivity, useDatabaseHealth, useModerationHealth - Build UI components: SystemHealthStatus, CriticalAlertsPanel, MonitoringQuickStats, RecentActivityTimeline, MonitoringNavCards - Create MonitoringOverview page and integrate routing (MonitoringOverview route) - Wire AdminSidebar to include Monitoring navigation - Introduce supporting routing and admin layout hooks/utilities - Align with TanStack Query patterns and plan for auto-refresh and optimistic updates
171 lines
6.0 KiB
TypeScript
171 lines
6.0 KiB
TypeScript
import { AlertTriangle, CheckCircle2, Clock, ShieldAlert, XCircle } from 'lucide-react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { toast } from 'sonner';
|
|
import { Link } from 'react-router-dom';
|
|
import type { CombinedAlert } from '@/hooks/admin/useCombinedAlerts';
|
|
|
|
interface CriticalAlertsPanelProps {
|
|
alerts?: CombinedAlert[];
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const SEVERITY_CONFIG = {
|
|
critical: { color: 'destructive' as const, icon: XCircle, label: 'Critical' },
|
|
high: { color: 'destructive' as const, icon: AlertTriangle, label: 'High' },
|
|
medium: { color: 'secondary' as const, icon: Clock, label: 'Medium' },
|
|
low: { color: 'secondary' as const, icon: Clock, label: 'Low' },
|
|
};
|
|
|
|
export function CriticalAlertsPanel({ alerts, isLoading }: CriticalAlertsPanelProps) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const resolveSystemAlert = useMutation({
|
|
mutationFn: async (alertId: string) => {
|
|
const { error } = await supabase
|
|
.from('system_alerts')
|
|
.update({ resolved_at: new Date().toISOString() })
|
|
.eq('id', alertId);
|
|
if (error) throw error;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['system-alerts'] });
|
|
queryClient.invalidateQueries({ queryKey: ['monitoring'] });
|
|
toast.success('Alert resolved');
|
|
},
|
|
onError: () => {
|
|
toast.error('Failed to resolve alert');
|
|
},
|
|
});
|
|
|
|
const resolveRateLimitAlert = useMutation({
|
|
mutationFn: async (alertId: string) => {
|
|
const { error } = await supabase
|
|
.from('rate_limit_alerts')
|
|
.update({ resolved_at: new Date().toISOString() })
|
|
.eq('id', alertId);
|
|
if (error) throw error;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['rate-limit-alerts'] });
|
|
queryClient.invalidateQueries({ queryKey: ['monitoring'] });
|
|
toast.success('Alert resolved');
|
|
},
|
|
onError: () => {
|
|
toast.error('Failed to resolve alert');
|
|
},
|
|
});
|
|
|
|
const handleResolve = (alert: CombinedAlert) => {
|
|
if (alert.source === 'system') {
|
|
resolveSystemAlert.mutate(alert.id);
|
|
} else {
|
|
resolveRateLimitAlert.mutate(alert.id);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<ShieldAlert className="w-5 h-5" />
|
|
Critical Alerts
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-center text-muted-foreground py-8">Loading alerts...</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!alerts || alerts.length === 0) {
|
|
return (
|
|
<Card className="border-green-500/20">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<ShieldAlert className="w-5 h-5" />
|
|
Critical Alerts
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10">
|
|
<CheckCircle2 className="w-8 h-8 text-green-500" />
|
|
<div>
|
|
<div className="font-semibold">All Systems Operational</div>
|
|
<div className="text-sm text-muted-foreground">No active alerts detected</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<ShieldAlert className="w-5 h-5" />
|
|
Critical Alerts
|
|
<Badge variant="destructive">{alerts.length}</Badge>
|
|
</CardTitle>
|
|
<div className="flex gap-2">
|
|
<Button asChild size="sm" variant="ghost">
|
|
<Link to="/admin/error-monitoring">View All</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{alerts.map((alert) => {
|
|
const config = SEVERITY_CONFIG[alert.severity];
|
|
const SeverityIcon = config.icon;
|
|
|
|
return (
|
|
<div
|
|
key={alert.id}
|
|
className="flex items-start gap-3 p-3 rounded-lg border border-border hover:bg-accent/50 transition-colors"
|
|
>
|
|
<SeverityIcon className={`w-5 h-5 mt-0.5 flex-shrink-0 ${alert.severity === 'critical' || alert.severity === 'high' ? 'text-destructive' : 'text-muted-foreground'}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start gap-2 flex-wrap">
|
|
<Badge variant={config.color} className="flex-shrink-0">
|
|
{config.label}
|
|
</Badge>
|
|
<Badge variant="outline" className="flex-shrink-0">
|
|
{alert.source === 'system' ? 'System' : 'Rate Limit'}
|
|
</Badge>
|
|
{alert.alert_type && (
|
|
<span className="text-xs text-muted-foreground">
|
|
{alert.alert_type.replace(/_/g, ' ')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm mt-1 break-words">{alert.message}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{formatDistanceToNow(new Date(alert.created_at), { addSuffix: true })}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handleResolve(alert)}
|
|
loading={resolveSystemAlert.isPending || resolveRateLimitAlert.isPending}
|
|
className="flex-shrink-0"
|
|
>
|
|
Resolve
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|