mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 14:31:12 -05:00
Consolidate Admin Logs Hub
- Implement new unified monitoring hub by adding EdgeFunctionLogs, DatabaseLogs, CorrelatedLogsView, and UnifiedLogSearch components - Integrate new tabs (edge-functions, database, traces) into ErrorMonitoring and expose TraceViewer route - Update admin sidebar link to reflect Monitoring hub and extend error modals with log-correlation actions - Wire up app to include trace viewer route and adjust related components for unified log correlation
This commit is contained in:
203
src/components/admin/UnifiedLogSearch.tsx
Normal file
203
src/components/admin/UnifiedLogSearch.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Search, Loader2, ExternalLink } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
|
||||
interface SearchResult {
|
||||
type: 'error' | 'approval' | 'edge' | 'database';
|
||||
id: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
severity?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface UnifiedLogSearchProps {
|
||||
onNavigate: (tab: string, filters: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
export function UnifiedLogSearch({ onNavigate }: UnifiedLogSearchProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const { data: results, isLoading } = useQuery({
|
||||
queryKey: ['unified-log-search', searchTerm],
|
||||
queryFn: async () => {
|
||||
if (!searchTerm) return [];
|
||||
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
// Search application errors
|
||||
const { data: errors } = await supabase
|
||||
.from('request_metadata')
|
||||
.select('request_id, created_at, error_type, error_message')
|
||||
.or(`request_id.ilike.%${searchTerm}%,error_message.ilike.%${searchTerm}%`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (errors) {
|
||||
results.push(...errors.map(e => ({
|
||||
type: 'error' as const,
|
||||
id: e.request_id,
|
||||
timestamp: e.created_at,
|
||||
message: e.error_message || 'Unknown error',
|
||||
severity: e.error_type || undefined,
|
||||
})));
|
||||
}
|
||||
|
||||
// Search approval failures
|
||||
const { data: approvals } = await supabase
|
||||
.from('approval_transaction_metrics')
|
||||
.select('id, created_at, error_message, request_id')
|
||||
.eq('success', false)
|
||||
.or(`request_id.ilike.%${searchTerm}%,error_message.ilike.%${searchTerm}%`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (approvals) {
|
||||
results.push(...approvals
|
||||
.filter(a => a.created_at)
|
||||
.map(a => ({
|
||||
type: 'approval' as const,
|
||||
id: a.id,
|
||||
timestamp: a.created_at!,
|
||||
message: a.error_message || 'Approval failed',
|
||||
metadata: { request_id: a.request_id },
|
||||
})));
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
return results;
|
||||
},
|
||||
enabled: !!searchTerm,
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
setSearchTerm(searchQuery);
|
||||
};
|
||||
|
||||
const getTypeColor = (type: string): "default" | "destructive" | "outline" | "secondary" => {
|
||||
switch (type) {
|
||||
case 'error': return 'destructive';
|
||||
case 'approval': return 'destructive';
|
||||
case 'edge': return 'default';
|
||||
case 'database': return 'secondary';
|
||||
default: return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'error': return 'Application Error';
|
||||
case 'approval': return 'Approval Failure';
|
||||
case 'edge': return 'Edge Function';
|
||||
case 'database': return 'Database Log';
|
||||
default: return type;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResultClick = (result: SearchResult) => {
|
||||
switch (result.type) {
|
||||
case 'error':
|
||||
onNavigate('errors', { requestId: result.id });
|
||||
break;
|
||||
case 'approval':
|
||||
onNavigate('approvals', { failureId: result.id });
|
||||
break;
|
||||
case 'edge':
|
||||
onNavigate('edge-functions', { search: result.message });
|
||||
break;
|
||||
case 'database':
|
||||
onNavigate('database', { search: result.message });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Unified Log Search</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search across all logs (request ID, error message, trace ID...)"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} disabled={!searchQuery || isLoading}>
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{searchTerm && (
|
||||
<div className="space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : results && results.length > 0 ? (
|
||||
<>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Found {results.length} results
|
||||
</div>
|
||||
{results.map((result) => (
|
||||
<Card
|
||||
key={`${result.type}-${result.id}`}
|
||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => handleResultClick(result)}
|
||||
>
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getTypeColor(result.type)}>
|
||||
{getTypeLabel(result.type)}
|
||||
</Badge>
|
||||
{result.severity && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{result.severity}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{format(new Date(result.timestamp), 'PPp')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm line-clamp-2">{result.message}</p>
|
||||
<code className="text-xs text-muted-foreground">
|
||||
{result.id.slice(0, 16)}...
|
||||
</code>
|
||||
</div>
|
||||
<ExternalLink className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
No results found for "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user