import type { HttpResponse, HttpResponseEvent, HttpResponseEventData, } from '@yaakapp-internal/models'; import { type ReactNode, useState } from 'react'; import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents'; import { Editor } from './core/Editor/LazyEditor'; import { EventDetailHeader, EventViewer, type EventDetailAction } from './core/EventViewer'; import { EventViewerRow } from './core/EventViewerRow'; import { HttpMethodTagRaw } from './core/HttpMethodTag'; import { HttpStatusTagRaw } from './core/HttpStatusTag'; import { Icon, type IconProps } from './core/Icon'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; interface Props { response: HttpResponse; } export function HttpResponseTimeline({ response }: Props) { return ; } function Inner({ response }: Props) { const [showRaw, setShowRaw] = useState(false); const { data: events, error, isLoading } = useHttpResponseEvents(response); return ( event.id} error={error ? String(error) : null} isLoading={isLoading} loadingMessage="Loading events..." emptyMessage="No events recorded" splitLayoutName="http_response_events" defaultRatio={0.25} renderRow={({ event, isActive, onClick }) => { const display = getEventDisplay(event.event); return ( } content={display.summary} timestamp={event.createdAt} /> ); }} renderDetail={({ event }) => ( )} /> ); } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function EventDetails({ event, showRaw, setShowRaw, }: { event: HttpResponseEvent; showRaw: boolean; setShowRaw: (v: boolean) => void; }) { const { label } = getEventDisplay(event.event); const e = event.event; const actions: EventDetailAction[] = [ { key: 'toggle-raw', label: showRaw ? 'Formatted' : 'Text', onClick: () => setShowRaw(!showRaw), }, ]; // Determine the title based on event type const title = e.type === 'header_up' ? 'Header Sent' : e.type === 'header_down' ? 'Header Received' : label; // Raw view - show plaintext representation if (showRaw) { const rawText = formatEventRaw(event.event); return (
); } // Headers - show name and value with Editor for JSON if (e.type === 'header_up' || e.type === 'header_down') { return (
{e.name} {e.value}
); } // Request URL - show method and path separately if (e.type === 'send_url') { return (
{e.path}
); } // Response status - show version and status separately if (e.type === 'receive_url') { return (
{e.version}
); } // Redirect - show status, URL, and behavior if (e.type === 'redirect') { return (
{e.url} {e.behavior === 'drop_body' ? 'Drop body, change to GET' : 'Preserve method and body'}
); } // Settings - show as key/value if (e.type === 'setting') { return (
{e.name} {e.value}
); } // Chunks - show formatted bytes if (e.type === 'chunk_sent' || e.type === 'chunk_received') { const direction = e.type === 'chunk_sent' ? 'Sent' : 'Received'; return (
{formatBytes(e.bytes)}
); } // Default - use summary const { summary } = getEventDisplay(event.event); return (
{summary}
); } /** Format event as raw plaintext for debugging */ function formatEventRaw(event: HttpResponseEventData): string { switch (event.type) { case 'send_url': return `${event.method} ${event.path}`; case 'receive_url': return `${event.version} ${event.status}`; case 'header_up': return `${event.name}: ${event.value}`; case 'header_down': return `${event.name}: ${event.value}`; case 'redirect': return `${event.status} Redirect: ${event.url}`; case 'setting': return `${event.name} = ${event.value}`; case 'info': return `${event.message}`; case 'chunk_sent': return `[${formatBytes(event.bytes)} sent]`; case 'chunk_received': return `[${formatBytes(event.bytes)} received]`; default: return '[unknown event]'; } } type EventDisplay = { icon: IconProps['icon']; color: IconProps['color']; label: string; summary: ReactNode; }; function getEventDisplay(event: HttpResponseEventData): EventDisplay { switch (event.type) { case 'setting': return { icon: 'settings', color: 'secondary', label: 'Setting', summary: `${event.name} = ${event.value}`, }; case 'info': return { icon: 'info', color: 'secondary', label: 'Info', summary: event.message, }; case 'redirect': return { icon: 'arrow_big_right_dash', color: 'warning', label: 'Redirect', summary: `Redirecting ${event.status} ${event.url}${event.behavior === 'drop_body' ? ' (drop body)' : ''}`, }; case 'send_url': return { icon: 'arrow_big_up_dash', color: 'primary', label: 'Request', summary: `${event.method} ${event.path}`, }; case 'receive_url': return { icon: 'arrow_big_down_dash', color: 'info', label: 'Response', summary: `${event.version} ${event.status}`, }; case 'header_up': return { icon: 'arrow_big_up_dash', color: 'primary', label: 'Header', summary: `${event.name}: ${event.value}`, }; case 'header_down': return { icon: 'arrow_big_down_dash', color: 'info', label: 'Header', summary: `${event.name}: ${event.value}`, }; case 'chunk_sent': return { icon: 'info', color: 'secondary', label: 'Chunk', summary: `${formatBytes(event.bytes)} chunk sent`, }; case 'chunk_received': return { icon: 'info', color: 'secondary', label: 'Chunk', summary: `${formatBytes(event.bytes)} chunk received`, }; default: return { icon: 'info', color: 'secondary', label: 'Unknown', summary: 'Unknown event', }; } }