mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-01 15:03:11 +02:00
Refactor: Consolidate event viewers into unified EventViewer component
Migrate EventStreamViewer, HttpResponseTimeline, GrpcResponsePane, and WebsocketResponsePane to use a shared EventViewer component with generic event type support, render props for rows and details, and keyboard navigation (↑/↓/j/k/Escape). This reduces duplication and provides a consistent event viewing experience across all response types.
This commit is contained in:
@@ -5,15 +5,13 @@ import { Fragment, useMemo, useState } from 'react';
|
||||
import { useFormatText } from '../../hooks/useFormatText';
|
||||
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
|
||||
import { isJSON } from '../../lib/contentType';
|
||||
import { AutoScroller } from '../core/AutoScroller';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { Button } from '../core/Button';
|
||||
import type { EditorProps } from '../core/Editor/Editor';
|
||||
import { Editor } from '../core/Editor/LazyEditor';
|
||||
import { EventViewer } from '../core/EventViewer';
|
||||
import { EventViewerRow } from '../core/EventViewerRow';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { Separator } from '../core/Separator';
|
||||
import { SplitLayout } from '../core/SplitLayout';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
|
||||
interface Props {
|
||||
@@ -33,134 +31,103 @@ export function EventStreamViewer({ response }: Props) {
|
||||
function ActualEventStreamViewer({ response }: Props) {
|
||||
const [showLarge, setShowLarge] = useState<boolean>(false);
|
||||
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
||||
const [activeEventIndex, setActiveEventIndex] = useState<number | null>(null);
|
||||
const events = useResponseBodyEventSource(response);
|
||||
const activeEvent = useMemo(
|
||||
() => (activeEventIndex == null ? null : events.data?.[activeEventIndex]),
|
||||
[activeEventIndex, events],
|
||||
);
|
||||
|
||||
const language = useMemo<'text' | 'json'>(() => {
|
||||
if (!activeEvent?.data) return 'text';
|
||||
return isJSON(activeEvent?.data) ? 'json' : 'text';
|
||||
}, [activeEvent?.data]);
|
||||
|
||||
return (
|
||||
<SplitLayout
|
||||
layout="vertical"
|
||||
name="grpc_events"
|
||||
<EventViewer
|
||||
events={events.data ?? []}
|
||||
getEventKey={(_, index) => String(index)}
|
||||
error={events.error ? String(events.error) : null}
|
||||
splitLayoutName="sse_events"
|
||||
defaultRatio={0.4}
|
||||
minHeightPx={20}
|
||||
firstSlot={() => (
|
||||
<AutoScroller
|
||||
data={events.data ?? []}
|
||||
header={
|
||||
events.error && (
|
||||
<Banner color="danger" className="m-3">
|
||||
{String(events.error)}
|
||||
</Banner>
|
||||
)
|
||||
renderRow={({ event, index, isActive, onClick }) => (
|
||||
<EventViewerRow
|
||||
isActive={isActive}
|
||||
onClick={onClick}
|
||||
icon={<Icon color="info" title="Server Message" icon="arrow_big_down_dash" />}
|
||||
content={
|
||||
<HStack space={2} className="items-center">
|
||||
<EventLabels event={event} index={index} isActive={isActive} />
|
||||
<span className="truncate text-xs">{event.data.slice(0, 1000)}</span>
|
||||
</HStack>
|
||||
}
|
||||
render={(event, i) => (
|
||||
<EventRow
|
||||
event={event}
|
||||
isActive={i === activeEventIndex}
|
||||
index={i}
|
||||
onClick={() => {
|
||||
if (i === activeEventIndex) setActiveEventIndex(null);
|
||||
else setActiveEventIndex(i);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
timestamp={new Date().toISOString().slice(0, -1)} // SSE events don't have timestamps
|
||||
/>
|
||||
)}
|
||||
renderDetail={({ event, index }) => (
|
||||
<EventDetail
|
||||
event={event}
|
||||
index={index}
|
||||
showLarge={showLarge}
|
||||
showingLarge={showingLarge}
|
||||
setShowLarge={setShowLarge}
|
||||
setShowingLarge={setShowingLarge}
|
||||
/>
|
||||
)}
|
||||
secondSlot={
|
||||
activeEvent
|
||||
? () => (
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="pb-3 px-2">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="flex flex-col pl-2">
|
||||
<HStack space={1.5} className="mb-2 font-semibold">
|
||||
<EventLabels
|
||||
className="text-sm"
|
||||
event={activeEvent}
|
||||
index={activeEventIndex ?? 0}
|
||||
/>
|
||||
Message Received
|
||||
</HStack>
|
||||
{!showLarge && activeEvent.data.length > 1000 * 1000 ? (
|
||||
<VStack space={2} className="italic text-text-subtlest">
|
||||
Message previews larger than 1MB are hidden
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowingLarge(true);
|
||||
setTimeout(() => {
|
||||
setShowLarge(true);
|
||||
setShowingLarge(false);
|
||||
}, 500);
|
||||
}}
|
||||
isLoading={showingLarge}
|
||||
color="secondary"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Try Showing
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
) : (
|
||||
<FormattedEditor language={language} text={activeEvent.data} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EventDetail({
|
||||
event,
|
||||
index,
|
||||
showLarge,
|
||||
showingLarge,
|
||||
setShowLarge,
|
||||
setShowingLarge,
|
||||
}: {
|
||||
event: ServerSentEvent;
|
||||
index: number;
|
||||
showLarge: boolean;
|
||||
showingLarge: boolean;
|
||||
setShowLarge: (v: boolean) => void;
|
||||
setShowingLarge: (v: boolean) => void;
|
||||
}) {
|
||||
const language = useMemo<'text' | 'json'>(() => {
|
||||
if (!event?.data) return 'text';
|
||||
return isJSON(event?.data) ? 'json' : 'text';
|
||||
}, [event?.data]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<HStack space={1.5} className="mb-2 font-semibold">
|
||||
<EventLabels className="text-sm" event={event} index={index} />
|
||||
Message Received
|
||||
</HStack>
|
||||
{!showLarge && event.data.length > 1000 * 1000 ? (
|
||||
<VStack space={2} className="italic text-text-subtlest">
|
||||
Message previews larger than 1MB are hidden
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowingLarge(true);
|
||||
setTimeout(() => {
|
||||
setShowLarge(true);
|
||||
setShowingLarge(false);
|
||||
}, 500);
|
||||
}}
|
||||
isLoading={showingLarge}
|
||||
color="secondary"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Try Showing
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
) : (
|
||||
<FormattedEditor language={language} text={event.data} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormattedEditor({ text, language }: { text: string; language: EditorProps['language'] }) {
|
||||
const formatted = useFormatText({ text, language, pretty: true });
|
||||
if (formatted == null) return null;
|
||||
return <Editor readOnly defaultValue={formatted} language={language} stateKey={null} />;
|
||||
}
|
||||
|
||||
function EventRow({
|
||||
onClick,
|
||||
isActive,
|
||||
event,
|
||||
className,
|
||||
index,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
isActive: boolean;
|
||||
event: ServerSentEvent;
|
||||
className?: string;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
className,
|
||||
'w-full grid grid-cols-[auto_auto_minmax(0,3fr)] gap-2 items-center text-left',
|
||||
'-mx-1.5 px-1.5 h-xs font-mono group focus:outline-none focus:text-text rounded',
|
||||
isActive && '!bg-surface-active !text-text',
|
||||
'text-text-subtle hover:text',
|
||||
)}
|
||||
>
|
||||
<Icon color="info" title="Server Message" icon="arrow_big_down_dash" />
|
||||
<EventLabels className="text-sm" event={event} isActive={isActive} index={index} />
|
||||
<div className={classNames('w-full truncate text-xs')}>{event.data.slice(0, 1000)}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function EventLabels({
|
||||
className,
|
||||
event,
|
||||
@@ -169,7 +136,7 @@ function EventLabels({
|
||||
}: {
|
||||
event: ServerSentEvent;
|
||||
index: number;
|
||||
className: string;
|
||||
className?: string;
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user