mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:31:13 -05:00
- 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
204 lines
7.1 KiB
TypeScript
204 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|