Fix lint issues and improve EventViewer component

- Fix biome lint formatting issues
- Add onActiveIndexChange callback for controlled state
- Simplify WebsocketEventDetail to compute message internally
- Fix exhaustive deps warning in handleRowClick

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-01-09 15:49:11 -08:00
parent 928099c6fd
commit 904d0063d6
4 changed files with 69 additions and 54 deletions

View File

@@ -81,13 +81,13 @@ function EventDetails({
const rawText = formatEventRaw(event.event);
return (
<div className="flex flex-col gap-2 h-full">
<DetailHeader title={label} timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
<Editor
language="text"
defaultValue={rawText}
readOnly
stateKey={null}
<DetailHeader
title={label}
timestamp={timestamp}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
<Editor language="text" defaultValue={rawText} readOnly stateKey={null} />
</div>
);
}
@@ -114,7 +114,12 @@ function EventDetails({
if (e.type === 'send_url') {
return (
<div className="flex flex-col gap-2">
<DetailHeader title="Request" timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
<DetailHeader
title="Request"
timestamp={timestamp}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
<KeyValueRows>
<KeyValueRow label="Method">
<HttpMethodTagRaw forceColor method={e.method} />
@@ -129,7 +134,12 @@ function EventDetails({
if (e.type === 'receive_url') {
return (
<div className="flex flex-col gap-2">
<DetailHeader title="Response" timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
<DetailHeader
title="Response"
timestamp={timestamp}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
<KeyValueRows>
<KeyValueRow label="HTTP Version">{e.version}</KeyValueRow>
<KeyValueRow label="Status">
@@ -144,7 +154,12 @@ function EventDetails({
if (e.type === 'redirect') {
return (
<div className="flex flex-col gap-2">
<DetailHeader title="Redirect" timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
<DetailHeader
title="Redirect"
timestamp={timestamp}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
<KeyValueRows>
<KeyValueRow label="Status">
<HttpStatusTagRaw status={e.status} />
@@ -162,7 +177,12 @@ function EventDetails({
if (e.type === 'setting') {
return (
<div className="flex flex-col gap-2">
<DetailHeader title="Apply Setting" timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
<DetailHeader
title="Apply Setting"
timestamp={timestamp}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
<KeyValueRows>
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
<KeyValueRow label="Value">{e.value}</KeyValueRow>
@@ -176,7 +196,12 @@ function EventDetails({
const direction = e.type === 'chunk_sent' ? 'Sent' : 'Received';
return (
<div className="flex flex-col gap-2">
<DetailHeader title={`Data ${direction}`} timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
<DetailHeader
title={`Data ${direction}`}
timestamp={timestamp}
showRaw={showRaw}
setShowRaw={setShowRaw}
/>
<div className="font-mono text-editor">{formatBytes(e.bytes)}</div>
</div>
);

View File

@@ -31,7 +31,6 @@ interface Props {
}
export function WebsocketResponsePane({ activeRequest }: Props) {
const [activeEventIndex, setActiveEventIndex] = useState<number | null>(null);
const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]);
const [showingLarge, setShowingLarge] = useState<boolean>(false);
const [hexDumps, setHexDumps] = useState<Record<number, boolean>>({});
@@ -40,26 +39,6 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
const connections = useAtomValue(activeWebsocketConnectionsAtom);
const events = useWebsocketEvents(activeConnection?.id ?? null);
const activeEvent = useMemo(
() => (activeEventIndex != null ? events[activeEventIndex] : null),
[activeEventIndex, events],
);
const hexDump =
hexDumps[activeEventIndex ?? -1] ?? activeEvent?.messageType === 'binary';
const message = useMemo(() => {
if (hexDump) {
return activeEvent?.message ? hexy(activeEvent?.message) : '';
}
return activeEvent?.message
? new TextDecoder('utf-8').decode(Uint8Array.from(activeEvent.message))
: '';
}, [activeEvent?.message, hexDump]);
const language = languageFromContentType(null, message);
const formattedMessage = useFormatText({ language, text: message, pretty: true });
if (activeConnection == null) {
return (
<HotkeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} />
@@ -101,12 +80,8 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
renderDetail={({ event, index }) => (
<WebsocketEventDetail
event={event}
index={index}
hexDump={hexDump}
hexDump={hexDumps[index] ?? event.messageType === 'binary'}
setHexDump={(v) => setHexDumps({ ...hexDumps, [index]: v })}
message={message}
formattedMessage={formattedMessage}
language={language}
showLarge={showLarge}
showingLarge={showingLarge}
setShowLarge={setShowLarge}
@@ -133,11 +108,7 @@ function WebsocketEventRow({
: '';
const iconColor =
messageType === 'close' || messageType === 'open'
? 'secondary'
: isServer
? 'info'
: 'primary';
messageType === 'close' || messageType === 'open' ? 'secondary' : isServer ? 'info' : 'primary';
const icon =
messageType === 'close' || messageType === 'open'
@@ -170,29 +141,31 @@ function WebsocketEventRow({
function WebsocketEventDetail({
event,
index,
hexDump,
setHexDump,
message,
formattedMessage,
language,
showLarge,
showingLarge,
setShowLarge,
setShowingLarge,
}: {
event: WebsocketEvent;
index: number;
hexDump: boolean;
setHexDump: (v: boolean) => void;
message: string;
formattedMessage: string | null;
language: string;
showLarge: boolean;
showingLarge: boolean;
setShowLarge: (v: boolean) => void;
setShowingLarge: (v: boolean) => void;
}) {
const message = useMemo(() => {
if (hexDump) {
return event.message ? hexy(event.message) : '';
}
return event.message ? new TextDecoder('utf-8').decode(Uint8Array.from(event.message)) : '';
}, [event.message, hexDump]);
const language = languageFromContentType(null, message);
const formattedMessage = useFormatText({ language, text: message, pretty: true });
const title =
event.messageType === 'close'
? 'Connection Closed'

View File

@@ -1,4 +1,4 @@
import { type Virtualizer, useVirtualizer } from '@tanstack/react-virtual';
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
import type { ReactElement, ReactNode, UIEvent } from 'react';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { IconButton } from './IconButton';

View File

@@ -48,6 +48,9 @@ interface EventViewerProps<T> {
/** Message to show when no events */
emptyMessage?: string;
/** Callback when active index changes (for controlled state in parent) */
onActiveIndexChange?: (index: number | null) => void;
}
export function EventViewer<T>({
@@ -63,8 +66,22 @@ export function EventViewer<T>({
isLoading = false,
loadingMessage = 'Loading events...',
emptyMessage = 'No events recorded',
onActiveIndexChange,
}: EventViewerProps<T>) {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const [activeIndex, setActiveIndexInternal] = useState<number | null>(null);
// Wrap setActiveIndex to notify parent
const setActiveIndex = useCallback(
(indexOrUpdater: number | null | ((prev: number | null) => number | null)) => {
setActiveIndexInternal((prev) => {
const newIndex =
typeof indexOrUpdater === 'function' ? indexOrUpdater(prev) : indexOrUpdater;
onActiveIndexChange?.(newIndex);
return newIndex;
});
},
[onActiveIndexChange],
);
const containerRef = useRef<HTMLDivElement>(null);
const virtualizerRef = useRef<Virtualizer<HTMLDivElement, Element> | null>(null);
@@ -101,7 +118,7 @@ export function EventViewer<T>({
(index: number) => {
setActiveIndex((prev) => (prev === index ? null : index));
},
[],
[setActiveIndex],
);
if (isLoading) {
@@ -154,7 +171,7 @@ export function EventViewer<T>({
<Separator />
</div>
<div className="mx-2 overflow-y-auto">
{renderDetail({ event: activeEvent, index: activeIndex! })}
{renderDetail({ event: activeEvent, index: activeIndex ?? 0 })}
</div>
</div>
)