Skip to content

Commit ff084a2

Browse files
authored
Consolidate event viewer interfaces (#355)
1 parent bbcae34 commit ff084a2

File tree

8 files changed

+876
-632
lines changed

8 files changed

+876
-632
lines changed

src-web/components/GrpcResponsePane.tsx

Lines changed: 184 additions & 213 deletions
Large diffs are not rendered by default.

src-web/components/HttpResponseTimeline.tsx

Lines changed: 130 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import type {
33
HttpResponseEvent,
44
HttpResponseEventData,
55
} from '@yaakapp-internal/models';
6-
import classNames from 'classnames';
76
import { format } from 'date-fns';
8-
import { type ReactNode, useMemo, useState } from 'react';
7+
import { type ReactNode, useState } from 'react';
98
import { 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';
1213
import { HttpMethodTagRaw } from './core/HttpMethodTag';
1314
import { HttpStatusTagRaw } from './core/HttpStatusTag';
1415
import { Icon, type IconProps } from './core/Icon';
1516
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
16-
import { Separator } from './core/Separator';
17-
import { SplitLayout } from './core/SplitLayout';
17+
import { HStack } from './core/Stacks';
1818

1919
interface Props {
2020
response: HttpResponse;
@@ -25,120 +25,82 @@ export function HttpResponseTimeline({ response }: Props) {
2525
}
2626

2727
function 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-
12460
function 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+
242270
type EventDisplay = {
243271
icon: IconProps['icon'];
244272
color: IconProps['color'];

0 commit comments

Comments
 (0)