import type { HttpResponse, HttpResponseEvent, HttpResponseEventData, } from '@yaakapp-internal/models'; import classNames from 'classnames'; import { format } from 'date-fns'; import { type ReactNode, useMemo, useState } from 'react'; import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents'; import { AutoScroller } from './core/AutoScroller'; import { Banner } from './core/Banner'; import { HttpMethodTagRaw } from './core/HttpMethodTag'; import { HttpStatusTagRaw } from './core/HttpStatusTag'; import { Icon, type IconProps } from './core/Icon'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; interface Props { response: HttpResponse; } export function HttpResponseTimeline({ response }: Props) { return ; } function Inner({ response }: Props) { const [activeEventIndex, setActiveEventIndex] = useState(null); const { data: events, error, isLoading } = useHttpResponseEvents(response); const activeEvent = useMemo( () => (activeEventIndex == null ? null : events?.[activeEventIndex]), [activeEventIndex, events], ); if (isLoading) { return
Loading events...
; } if (error) { return ( {String(error)} ); } if (!events || events.length === 0) { return
No events recorded
; } return ( ( ( { if (i === activeEventIndex) setActiveEventIndex(null); else setActiveEventIndex(i); }} /> )} /> )} secondSlot={ activeEvent ? () => (
) : null } /> ); } function EventRow({ onClick, isActive, event, }: { onClick: () => void; isActive: boolean; event: HttpResponseEvent; }) { const display = getEventDisplay(event.event); const { icon, color, summary } = display; return (
); } 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 }: { event: HttpResponseEvent }) { const { label } = getEventDisplay(event.event); const timestamp = format(new Date(`${event.createdAt}Z`), 'HH:mm:ss.SSS'); const e = event.event; // 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}
); } function DetailHeader({ title, timestamp }: { title: string; timestamp: string }) { return (

{title}

{timestamp}
); } 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', }; } }