@@ -3,18 +3,18 @@ import type {
33 HttpResponseEvent ,
44 HttpResponseEventData ,
55} from '@yaakapp-internal/models' ;
6- import classNames from 'classnames' ;
76import { format } from 'date-fns' ;
8- import { type ReactNode , useMemo , useState } from 'react' ;
7+ import { type ReactNode , useState } from 'react' ;
98import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents' ;
10- import { AutoScroller } from './core/AutoScroller' ;
11- import { Banner } from './core/Banner' ;
9+ import { Button } from './core/Button' ;
10+ import { Editor } from './core/Editor/LazyEditor' ;
11+ import { EventViewer } from './core/EventViewer' ;
12+ import { EventViewerRow } from './core/EventViewerRow' ;
1213import { HttpMethodTagRaw } from './core/HttpMethodTag' ;
1314import { HttpStatusTagRaw } from './core/HttpStatusTag' ;
1415import { Icon , type IconProps } from './core/Icon' ;
1516import { KeyValueRow , KeyValueRows } from './core/KeyValueRow' ;
16- import { Separator } from './core/Separator' ;
17- import { SplitLayout } from './core/SplitLayout' ;
17+ import { HStack } from './core/Stacks' ;
1818
1919interface Props {
2020 response : HttpResponse ;
@@ -25,120 +25,82 @@ export function HttpResponseTimeline({ response }: Props) {
2525}
2626
2727function Inner ( { response } : Props ) {
28- const [ activeEventIndex , setActiveEventIndex ] = useState < number | null > ( null ) ;
28+ const [ showRaw , setShowRaw ] = useState ( false ) ;
2929 const { data : events , error, isLoading } = useHttpResponseEvents ( response ) ;
3030
31- const activeEvent = useMemo (
32- ( ) => ( activeEventIndex == null ? null : events ?. [ activeEventIndex ] ) ,
33- [ activeEventIndex , events ] ,
34- ) ;
35-
36- if ( isLoading ) {
37- return < div className = "p-3 text-text-subtlest italic" > Loading events...</ div > ;
38- }
39-
40- if ( error ) {
41- return (
42- < Banner color = "danger" className = "m-3" >
43- { String ( error ) }
44- </ Banner >
45- ) ;
46- }
47-
48- if ( ! events || events . length === 0 ) {
49- return < div className = "p-3 text-text-subtlest italic" > No events recorded</ div > ;
50- }
51-
5231 return (
53- < SplitLayout
54- layout = "vertical"
55- name = "http_response_events"
32+ < EventViewer
33+ events = { events ?? [ ] }
34+ getEventKey = { ( event ) => event . id }
35+ error = { error ? String ( error ) : null }
36+ isLoading = { isLoading }
37+ loadingMessage = "Loading events..."
38+ emptyMessage = "No events recorded"
39+ splitLayoutName = "http_response_events"
5640 defaultRatio = { 0.25 }
57- minHeightPx = { 10 }
58- firstSlot = { ( ) => (
59- < AutoScroller
60- data = { events }
61- render = { ( event , i ) => (
62- < EventRow
63- key = { event . id }
64- event = { event }
65- isActive = { i === activeEventIndex }
66- onClick = { ( ) => {
67- if ( i === activeEventIndex ) setActiveEventIndex ( null ) ;
68- else setActiveEventIndex ( i ) ;
69- } }
70- />
71- ) }
72- />
41+ renderRow = { ( { event, isActive, onClick } ) => {
42+ const display = getEventDisplay ( event . event ) ;
43+ return (
44+ < EventViewerRow
45+ isActive = { isActive }
46+ onClick = { onClick }
47+ icon = { < Icon color = { display . color } icon = { display . icon } size = "sm" /> }
48+ content = { display . summary }
49+ timestamp = { event . createdAt }
50+ />
51+ ) ;
52+ } }
53+ renderDetail = { ( { event } ) => (
54+ < EventDetails event = { event } showRaw = { showRaw } setShowRaw = { setShowRaw } />
7355 ) }
74- secondSlot = {
75- activeEvent
76- ? ( ) => (
77- < div className = "grid grid-rows-[auto_minmax(0,1fr)]" >
78- < div className = "pb-3 px-2" >
79- < Separator />
80- </ div >
81- < div className = "mx-2 overflow-y-auto" >
82- < EventDetails event = { activeEvent } />
83- </ div >
84- </ div >
85- )
86- : null
87- }
8856 />
8957 ) ;
9058}
9159
92- function EventRow ( {
93- onClick,
94- isActive,
95- event,
96- } : {
97- onClick : ( ) => void ;
98- isActive : boolean ;
99- event : HttpResponseEvent ;
100- } ) {
101- const display = getEventDisplay ( event . event ) ;
102- const { icon, color, summary } = display ;
103-
104- return (
105- < div className = "px-1" >
106- < button
107- type = "button"
108- onClick = { onClick }
109- className = { classNames (
110- 'w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left' ,
111- 'px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded' ,
112- isActive && '!bg-surface-active !text-text' ,
113- 'text-text-subtle hover:text' ,
114- ) }
115- >
116- < Icon color = { color } icon = { icon } size = "sm" />
117- < div className = "w-full truncate" > { summary } </ div >
118- < div className = "opacity-50" > { format ( `${ event . createdAt } Z` , 'HH:mm:ss.SSS' ) } </ div >
119- </ button >
120- </ div >
121- ) ;
122- }
123-
12460function formatBytes ( bytes : number ) : string {
12561 if ( bytes < 1024 ) return `${ bytes } B` ;
12662 if ( bytes < 1024 * 1024 ) return `${ ( bytes / 1024 ) . toFixed ( 1 ) } KB` ;
12763 return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 1 ) } MB` ;
12864}
12965
130- function EventDetails ( { event } : { event : HttpResponseEvent } ) {
66+ function EventDetails ( {
67+ event,
68+ showRaw,
69+ setShowRaw,
70+ } : {
71+ event : HttpResponseEvent ;
72+ showRaw : boolean ;
73+ setShowRaw : ( v : boolean ) => void ;
74+ } ) {
13175 const { label } = getEventDisplay ( event . event ) ;
13276 const timestamp = format ( new Date ( `${ event . createdAt } Z` ) , 'HH:mm:ss.SSS' ) ;
13377 const e = event . event ;
13478
79+ // Raw view - show plaintext representation
80+ if ( showRaw ) {
81+ const rawText = formatEventRaw ( event . event ) ;
82+ return (
83+ < div className = "flex flex-col gap-2 h-full" >
84+ < DetailHeader
85+ title = { label }
86+ timestamp = { timestamp }
87+ showRaw = { showRaw }
88+ setShowRaw = { setShowRaw }
89+ />
90+ < Editor language = "text" defaultValue = { rawText } readOnly stateKey = { null } />
91+ </ div >
92+ ) ;
93+ }
94+
13595 // Headers - show name and value with Editor for JSON
13696 if ( e . type === 'header_up' || e . type === 'header_down' ) {
13797 return (
13898 < div className = "flex flex-col gap-2 h-full" >
13999 < DetailHeader
140100 title = { e . type === 'header_down' ? 'Header Received' : 'Header Sent' }
141101 timestamp = { timestamp }
102+ showRaw = { showRaw }
103+ setShowRaw = { setShowRaw }
142104 />
143105 < KeyValueRows >
144106 < KeyValueRow label = "Header" > { e . name } </ KeyValueRow >
@@ -152,7 +114,12 @@ function EventDetails({ event }: { event: HttpResponseEvent }) {
152114 if ( e . type === 'send_url' ) {
153115 return (
154116 < div className = "flex flex-col gap-2" >
155- < DetailHeader title = "Request" timestamp = { timestamp } />
117+ < DetailHeader
118+ title = "Request"
119+ timestamp = { timestamp }
120+ showRaw = { showRaw }
121+ setShowRaw = { setShowRaw }
122+ />
156123 < KeyValueRows >
157124 < KeyValueRow label = "Method" >
158125 < HttpMethodTagRaw forceColor method = { e . method } />
@@ -167,7 +134,12 @@ function EventDetails({ event }: { event: HttpResponseEvent }) {
167134 if ( e . type === 'receive_url' ) {
168135 return (
169136 < div className = "flex flex-col gap-2" >
170- < DetailHeader title = "Response" timestamp = { timestamp } />
137+ < DetailHeader
138+ title = "Response"
139+ timestamp = { timestamp }
140+ showRaw = { showRaw }
141+ setShowRaw = { setShowRaw }
142+ />
171143 < KeyValueRows >
172144 < KeyValueRow label = "HTTP Version" > { e . version } </ KeyValueRow >
173145 < KeyValueRow label = "Status" >
@@ -182,7 +154,12 @@ function EventDetails({ event }: { event: HttpResponseEvent }) {
182154 if ( e . type === 'redirect' ) {
183155 return (
184156 < div className = "flex flex-col gap-2" >
185- < DetailHeader title = "Redirect" timestamp = { timestamp } />
157+ < DetailHeader
158+ title = "Redirect"
159+ timestamp = { timestamp }
160+ showRaw = { showRaw }
161+ setShowRaw = { setShowRaw }
162+ />
186163 < KeyValueRows >
187164 < KeyValueRow label = "Status" >
188165 < HttpStatusTagRaw status = { e . status } />
@@ -200,7 +177,12 @@ function EventDetails({ event }: { event: HttpResponseEvent }) {
200177 if ( e . type === 'setting' ) {
201178 return (
202179 < div className = "flex flex-col gap-2" >
203- < DetailHeader title = "Apply Setting" timestamp = { timestamp } />
180+ < DetailHeader
181+ title = "Apply Setting"
182+ timestamp = { timestamp }
183+ showRaw = { showRaw }
184+ setShowRaw = { setShowRaw }
185+ />
204186 < KeyValueRows >
205187 < KeyValueRow label = "Setting" > { e . name } </ KeyValueRow >
206188 < KeyValueRow label = "Value" > { e . value } </ KeyValueRow >
@@ -214,7 +196,12 @@ function EventDetails({ event }: { event: HttpResponseEvent }) {
214196 const direction = e . type === 'chunk_sent' ? 'Sent' : 'Received' ;
215197 return (
216198 < div className = "flex flex-col gap-2" >
217- < DetailHeader title = { `Data ${ direction } ` } timestamp = { timestamp } />
199+ < DetailHeader
200+ title = { `Data ${ direction } ` }
201+ timestamp = { timestamp }
202+ showRaw = { showRaw }
203+ setShowRaw = { setShowRaw }
204+ />
218205 < div className = "font-mono text-editor" > { formatBytes ( e . bytes ) } </ div >
219206 </ div >
220207 ) ;
@@ -224,21 +211,62 @@ function EventDetails({ event }: { event: HttpResponseEvent }) {
224211 const { summary } = getEventDisplay ( event . event ) ;
225212 return (
226213 < div className = "flex flex-col gap-1" >
227- < DetailHeader title = { label } timestamp = { timestamp } />
214+ < DetailHeader title = { label } timestamp = { timestamp } showRaw = { showRaw } setShowRaw = { setShowRaw } />
228215 < div className = "font-mono text-editor" > { summary } </ div >
229216 </ div >
230217 ) ;
231218}
232219
233- function DetailHeader ( { title, timestamp } : { title : string ; timestamp : string } ) {
220+ function DetailHeader ( {
221+ title,
222+ timestamp,
223+ showRaw,
224+ setShowRaw,
225+ } : {
226+ title : string ;
227+ timestamp : string ;
228+ showRaw : boolean ;
229+ setShowRaw : ( v : boolean ) => void ;
230+ } ) {
234231 return (
235232 < div className = "flex items-center justify-between gap-2" >
236- < h3 className = "font-semibold select-auto cursor-auto" > { title } </ h3 >
233+ < HStack space = { 2 } className = "items-center" >
234+ < h3 className = "font-semibold select-auto cursor-auto" > { title } </ h3 >
235+ < Button variant = "border" size = "xs" onClick = { ( ) => setShowRaw ( ! showRaw ) } >
236+ { showRaw ? 'Formatted' : 'Raw' }
237+ </ Button >
238+ </ HStack >
237239 < span className = "text-text-subtlest font-mono text-editor" > { timestamp } </ span >
238240 </ div >
239241 ) ;
240242}
241243
244+ /** Format event as raw plaintext for debugging */
245+ function formatEventRaw ( event : HttpResponseEventData ) : string {
246+ switch ( event . type ) {
247+ case 'send_url' :
248+ return `> ${ event . method } ${ event . path } ` ;
249+ case 'receive_url' :
250+ return `< ${ event . version } ${ event . status } ` ;
251+ case 'header_up' :
252+ return `> ${ event . name } : ${ event . value } ` ;
253+ case 'header_down' :
254+ return `< ${ event . name } : ${ event . value } ` ;
255+ case 'redirect' :
256+ return `< ${ event . status } Redirect: ${ event . url } ` ;
257+ case 'setting' :
258+ return `[setting] ${ event . name } = ${ event . value } ` ;
259+ case 'info' :
260+ return `[info] ${ event . message } ` ;
261+ case 'chunk_sent' :
262+ return `> [${ formatBytes ( event . bytes ) } sent]` ;
263+ case 'chunk_received' :
264+ return `< [${ formatBytes ( event . bytes ) } received]` ;
265+ default :
266+ return '[unknown event]' ;
267+ }
268+ }
269+
242270type EventDisplay = {
243271 icon : IconProps [ 'icon' ] ;
244272 color : IconProps [ 'color' ] ;
0 commit comments