mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 17:58:27 +02:00
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:
@@ -81,13 +81,13 @@ function EventDetails({
|
|||||||
const rawText = formatEventRaw(event.event);
|
const rawText = formatEventRaw(event.event);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 h-full">
|
<div className="flex flex-col gap-2 h-full">
|
||||||
<DetailHeader title={label} timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
|
<DetailHeader
|
||||||
<Editor
|
title={label}
|
||||||
language="text"
|
timestamp={timestamp}
|
||||||
defaultValue={rawText}
|
showRaw={showRaw}
|
||||||
readOnly
|
setShowRaw={setShowRaw}
|
||||||
stateKey={null}
|
|
||||||
/>
|
/>
|
||||||
|
<Editor language="text" defaultValue={rawText} readOnly stateKey={null} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,12 @@ function EventDetails({
|
|||||||
if (e.type === 'send_url') {
|
if (e.type === 'send_url') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<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>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Method">
|
<KeyValueRow label="Method">
|
||||||
<HttpMethodTagRaw forceColor method={e.method} />
|
<HttpMethodTagRaw forceColor method={e.method} />
|
||||||
@@ -129,7 +134,12 @@ function EventDetails({
|
|||||||
if (e.type === 'receive_url') {
|
if (e.type === 'receive_url') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<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>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="HTTP Version">{e.version}</KeyValueRow>
|
<KeyValueRow label="HTTP Version">{e.version}</KeyValueRow>
|
||||||
<KeyValueRow label="Status">
|
<KeyValueRow label="Status">
|
||||||
@@ -144,7 +154,12 @@ function EventDetails({
|
|||||||
if (e.type === 'redirect') {
|
if (e.type === 'redirect') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<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>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Status">
|
<KeyValueRow label="Status">
|
||||||
<HttpStatusTagRaw status={e.status} />
|
<HttpStatusTagRaw status={e.status} />
|
||||||
@@ -162,7 +177,12 @@ function EventDetails({
|
|||||||
if (e.type === 'setting') {
|
if (e.type === 'setting') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<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>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
|
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
|
||||||
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
||||||
@@ -176,7 +196,12 @@ function EventDetails({
|
|||||||
const direction = e.type === 'chunk_sent' ? 'Sent' : 'Received';
|
const direction = e.type === 'chunk_sent' ? 'Sent' : 'Received';
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<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 className="font-mono text-editor">{formatBytes(e.bytes)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WebsocketResponsePane({ activeRequest }: Props) {
|
export function WebsocketResponsePane({ activeRequest }: Props) {
|
||||||
const [activeEventIndex, setActiveEventIndex] = useState<number | null>(null);
|
|
||||||
const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]);
|
const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]);
|
||||||
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
||||||
const [hexDumps, setHexDumps] = useState<Record<number, boolean>>({});
|
const [hexDumps, setHexDumps] = useState<Record<number, boolean>>({});
|
||||||
@@ -40,26 +39,6 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
|
|||||||
const connections = useAtomValue(activeWebsocketConnectionsAtom);
|
const connections = useAtomValue(activeWebsocketConnectionsAtom);
|
||||||
const events = useWebsocketEvents(activeConnection?.id ?? null);
|
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) {
|
if (activeConnection == null) {
|
||||||
return (
|
return (
|
||||||
<HotkeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} />
|
<HotkeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} />
|
||||||
@@ -101,12 +80,8 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
|
|||||||
renderDetail={({ event, index }) => (
|
renderDetail={({ event, index }) => (
|
||||||
<WebsocketEventDetail
|
<WebsocketEventDetail
|
||||||
event={event}
|
event={event}
|
||||||
index={index}
|
hexDump={hexDumps[index] ?? event.messageType === 'binary'}
|
||||||
hexDump={hexDump}
|
|
||||||
setHexDump={(v) => setHexDumps({ ...hexDumps, [index]: v })}
|
setHexDump={(v) => setHexDumps({ ...hexDumps, [index]: v })}
|
||||||
message={message}
|
|
||||||
formattedMessage={formattedMessage}
|
|
||||||
language={language}
|
|
||||||
showLarge={showLarge}
|
showLarge={showLarge}
|
||||||
showingLarge={showingLarge}
|
showingLarge={showingLarge}
|
||||||
setShowLarge={setShowLarge}
|
setShowLarge={setShowLarge}
|
||||||
@@ -133,11 +108,7 @@ function WebsocketEventRow({
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
const iconColor =
|
const iconColor =
|
||||||
messageType === 'close' || messageType === 'open'
|
messageType === 'close' || messageType === 'open' ? 'secondary' : isServer ? 'info' : 'primary';
|
||||||
? 'secondary'
|
|
||||||
: isServer
|
|
||||||
? 'info'
|
|
||||||
: 'primary';
|
|
||||||
|
|
||||||
const icon =
|
const icon =
|
||||||
messageType === 'close' || messageType === 'open'
|
messageType === 'close' || messageType === 'open'
|
||||||
@@ -170,29 +141,31 @@ function WebsocketEventRow({
|
|||||||
|
|
||||||
function WebsocketEventDetail({
|
function WebsocketEventDetail({
|
||||||
event,
|
event,
|
||||||
index,
|
|
||||||
hexDump,
|
hexDump,
|
||||||
setHexDump,
|
setHexDump,
|
||||||
message,
|
|
||||||
formattedMessage,
|
|
||||||
language,
|
|
||||||
showLarge,
|
showLarge,
|
||||||
showingLarge,
|
showingLarge,
|
||||||
setShowLarge,
|
setShowLarge,
|
||||||
setShowingLarge,
|
setShowingLarge,
|
||||||
}: {
|
}: {
|
||||||
event: WebsocketEvent;
|
event: WebsocketEvent;
|
||||||
index: number;
|
|
||||||
hexDump: boolean;
|
hexDump: boolean;
|
||||||
setHexDump: (v: boolean) => void;
|
setHexDump: (v: boolean) => void;
|
||||||
message: string;
|
|
||||||
formattedMessage: string | null;
|
|
||||||
language: string;
|
|
||||||
showLarge: boolean;
|
showLarge: boolean;
|
||||||
showingLarge: boolean;
|
showingLarge: boolean;
|
||||||
setShowLarge: (v: boolean) => void;
|
setShowLarge: (v: boolean) => void;
|
||||||
setShowingLarge: (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 =
|
const title =
|
||||||
event.messageType === 'close'
|
event.messageType === 'close'
|
||||||
? 'Connection Closed'
|
? 'Connection Closed'
|
||||||
|
|||||||
@@ -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 type { ReactElement, ReactNode, UIEvent } from 'react';
|
||||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ interface EventViewerProps<T> {
|
|||||||
|
|
||||||
/** Message to show when no events */
|
/** Message to show when no events */
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
|
|
||||||
|
/** Callback when active index changes (for controlled state in parent) */
|
||||||
|
onActiveIndexChange?: (index: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventViewer<T>({
|
export function EventViewer<T>({
|
||||||
@@ -63,8 +66,22 @@ export function EventViewer<T>({
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
loadingMessage = 'Loading events...',
|
loadingMessage = 'Loading events...',
|
||||||
emptyMessage = 'No events recorded',
|
emptyMessage = 'No events recorded',
|
||||||
|
onActiveIndexChange,
|
||||||
}: EventViewerProps<T>) {
|
}: 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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
const virtualizerRef = useRef<Virtualizer<HTMLDivElement, Element> | null>(null);
|
const virtualizerRef = useRef<Virtualizer<HTMLDivElement, Element> | null>(null);
|
||||||
|
|
||||||
@@ -101,7 +118,7 @@ export function EventViewer<T>({
|
|||||||
(index: number) => {
|
(index: number) => {
|
||||||
setActiveIndex((prev) => (prev === index ? null : index));
|
setActiveIndex((prev) => (prev === index ? null : index));
|
||||||
},
|
},
|
||||||
[],
|
[setActiveIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -154,7 +171,7 @@ export function EventViewer<T>({
|
|||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-2 overflow-y-auto">
|
<div className="mx-2 overflow-y-auto">
|
||||||
{renderDetail({ event: activeEvent, index: activeIndex! })}
|
{renderDetail({ event: activeEvent, index: activeIndex ?? 0 })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user