Files
ophion/dashboard/src/app/traces/page.tsx
2026-02-06 14:26:15 -03:00

185 lines
7.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Search, Clock, AlertCircle, ExternalLink } from 'lucide-react';
import { api } from '@/lib/api';
import { TraceTimeline } from '@/components/traces/TraceTimeline';
import { formatDuration, formatTime } from '@/lib/utils';
interface Trace {
trace_id: string;
services: string[];
start_time: string;
duration_ns: number;
span_count: number;
has_error: boolean;
root_span?: {
operation: string;
service: string;
};
}
export default function TracesPage() {
const [service, setService] = useState('');
const [operation, setOperation] = useState('');
const [minDuration, setMinDuration] = useState('');
const [onlyErrors, setOnlyErrors] = useState(false);
const [selectedTrace, setSelectedTrace] = useState<string | null>(null);
const { data: traces, isLoading, refetch } = useQuery({
queryKey: ['traces', service, operation, minDuration, onlyErrors],
queryFn: () => api.get('/api/v1/traces', {
service,
operation,
min_duration_ms: minDuration,
error: onlyErrors ? 'true' : '',
from: new Date(Date.now() - 3600000).toISOString(),
}),
});
const { data: traceDetail } = useQuery({
queryKey: ['trace', selectedTrace],
queryFn: () => api.get(`/api/v1/traces/${selectedTrace}`),
enabled: !!selectedTrace,
});
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-white">Traces</h1>
<button
onClick={() => refetch()}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
>
Refresh
</button>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 p-4 bg-gray-900/50 rounded-lg border border-gray-800">
<div className="flex-1 min-w-[200px]">
<label className="block text-sm text-gray-400 mb-1">Service</label>
<input
type="text"
value={service}
onChange={(e) => setService(e.target.value)}
placeholder="All services"
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
/>
</div>
<div className="flex-1 min-w-[200px]">
<label className="block text-sm text-gray-400 mb-1">Operation</label>
<input
type="text"
value={operation}
onChange={(e) => setOperation(e.target.value)}
placeholder="All operations"
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
/>
</div>
<div className="w-32">
<label className="block text-sm text-gray-400 mb-1">Min Duration</label>
<input
type="text"
value={minDuration}
onChange={(e) => setMinDuration(e.target.value)}
placeholder="ms"
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500"
/>
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 px-3 py-2 cursor-pointer">
<input
type="checkbox"
checked={onlyErrors}
onChange={(e) => setOnlyErrors(e.target.checked)}
className="rounded border-gray-600 bg-gray-800 text-indigo-500 focus:ring-indigo-500"
/>
<span className="text-sm text-gray-300">Errors only</span>
</label>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Trace List */}
<div className="space-y-2">
<h2 className="text-lg font-semibold text-white">
{isLoading ? 'Loading...' : `${traces?.traces?.length ?? 0} traces`}
</h2>
<div className="space-y-2 max-h-[600px] overflow-auto">
{traces?.traces?.map((trace: Trace) => (
<div
key={trace.trace_id}
onClick={() => setSelectedTrace(trace.trace_id)}
className={`p-4 bg-gray-900/50 rounded-lg border cursor-pointer transition-colors ${
selectedTrace === trace.trace_id
? 'border-indigo-500'
: 'border-gray-800 hover:border-gray-700'
}`}
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2">
{trace.has_error && (
<AlertCircle className="h-4 w-4 text-red-400" />
)}
<span className="font-medium text-white">
{trace.root_span?.operation || trace.trace_id.slice(0, 16)}
</span>
</div>
<div className="flex flex-wrap gap-1 mt-1">
{trace.services?.slice(0, 3).map((svc) => (
<span
key={svc}
className="px-2 py-0.5 text-xs bg-gray-800 text-gray-300 rounded"
>
{svc}
</span>
))}
{(trace.services?.length ?? 0) > 3 && (
<span className="text-xs text-gray-500">
+{trace.services.length - 3} more
</span>
)}
</div>
</div>
<div className="text-right text-sm">
<div className="flex items-center gap-1 text-gray-400">
<Clock className="h-3 w-3" />
{formatDuration(trace.duration_ns)}
</div>
<div className="text-gray-500">
{trace.span_count} spans
</div>
</div>
</div>
<div className="mt-2 text-xs text-gray-500">
{formatTime(trace.start_time)}
</div>
</div>
))}
{!isLoading && (!traces?.traces || traces.traces.length === 0) && (
<div className="text-center py-8 text-gray-500">
No traces found
</div>
)}
</div>
</div>
{/* Trace Detail */}
<div className="space-y-2">
<h2 className="text-lg font-semibold text-white">Trace Timeline</h2>
{selectedTrace && traceDetail ? (
<TraceTimeline trace={traceDetail} />
) : (
<div className="flex items-center justify-center h-64 bg-gray-900/50 rounded-lg border border-gray-800 text-gray-500">
Select a trace to view timeline
</div>
)}
</div>
</div>
</div>
);
}