mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-17 14:37:05 +01:00
232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
|
import type { ServerSentEvent } from '@yaakapp-internal/sse';
|
|
import classNames from 'classnames';
|
|
import { motion } from 'framer-motion';
|
|
import React, { Fragment, useMemo, useRef, useState } from 'react';
|
|
import { useFormatText } from '../../hooks/useFormatText';
|
|
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
|
|
import { isJSON } from '../../lib/contentType';
|
|
import { Button } from '../core/Button';
|
|
import type { EditorProps } from '../core/Editor/Editor';
|
|
import { Editor } from '../core/Editor/Editor';
|
|
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 {
|
|
response: HttpResponse;
|
|
}
|
|
|
|
export function EventStreamViewer({ response }: Props) {
|
|
return (
|
|
<Fragment
|
|
key={response.id} // force a refresh when the response changes
|
|
>
|
|
<ActualEventStreamViewer response={response} />
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
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"
|
|
defaultRatio={0.4}
|
|
minHeightPx={20}
|
|
firstSlot={() => (
|
|
<EventStreamEventsVirtual
|
|
events={events.data ?? []}
|
|
activeEventIndex={activeEventIndex}
|
|
setActiveEventIndex={setActiveEventIndex}
|
|
/>
|
|
)}
|
|
secondSlot={
|
|
activeEvent
|
|
? () => (
|
|
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
|
<div className="pb-3 px-2">
|
|
<Separator />
|
|
</div>
|
|
<div className="pl-2 overflow-y-auto">
|
|
<HStack space={1.5} className="mb-2 select-text cursor-text 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 FormattedEditor({ text, language }: { text: string; language: EditorProps['language'] }) {
|
|
const formatted = useFormatText({ text, language, pretty: true });
|
|
if (formatted.data == null) return null;
|
|
return <Editor readOnly defaultValue={formatted.data} language={language} stateKey={null} />;
|
|
}
|
|
|
|
function EventStreamEventsVirtual({
|
|
events,
|
|
activeEventIndex,
|
|
setActiveEventIndex,
|
|
}: {
|
|
events: ServerSentEvent[];
|
|
activeEventIndex: number | null;
|
|
setActiveEventIndex: (eventId: number | null) => void;
|
|
}) {
|
|
// The scrollable element for your list
|
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
|
|
// The virtualizer
|
|
const rowVirtualizer = useVirtualizer({
|
|
count: events.length,
|
|
getScrollElement: () => parentRef.current,
|
|
estimateSize: () => 28, // react-virtual requires a height, so we'll give it one
|
|
});
|
|
|
|
return (
|
|
<div ref={parentRef} className="overflow-y-auto">
|
|
<div
|
|
style={{
|
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
width: '100%',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
|
|
const event = events[virtualItem.index]!;
|
|
return (
|
|
<div
|
|
key={virtualItem.key}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: `${virtualItem.size}px`,
|
|
transform: `translateY(${virtualItem.start}px)`,
|
|
}}
|
|
>
|
|
<EventStreamEvent
|
|
event={event}
|
|
isActive={virtualItem.index === activeEventIndex}
|
|
index={virtualItem.index}
|
|
onClick={() => {
|
|
if (virtualItem.index === activeEventIndex) setActiveEventIndex(null);
|
|
else setActiveEventIndex(virtualItem.index);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EventStreamEvent({
|
|
onClick,
|
|
isActive,
|
|
event,
|
|
className,
|
|
index,
|
|
}: {
|
|
onClick: () => void;
|
|
isActive: boolean;
|
|
event: ServerSentEvent;
|
|
className?: string;
|
|
index: number;
|
|
}) {
|
|
return (
|
|
<motion.button
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
onClick={onClick}
|
|
className={classNames(
|
|
className,
|
|
'w-full grid grid-cols-[auto_auto_minmax(0,3fr)] gap-2 items-center text-left',
|
|
'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded',
|
|
isActive && '!bg-surface-highlight !text-text',
|
|
'text-text-subtle hover:text',
|
|
)}
|
|
>
|
|
<Icon className={classNames('text-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>
|
|
</motion.button>
|
|
);
|
|
}
|
|
|
|
function EventLabels({
|
|
className,
|
|
event,
|
|
index,
|
|
isActive,
|
|
}: {
|
|
event: ServerSentEvent;
|
|
index: number;
|
|
className: string;
|
|
isActive?: boolean;
|
|
}) {
|
|
return (
|
|
<HStack space={1.5} alignItems="center" className={className}>
|
|
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
|
|
{event.id ?? index}
|
|
</InlineCode>
|
|
{event.eventType && (
|
|
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
|
|
{event.eventType}
|
|
</InlineCode>
|
|
)}
|
|
</HStack>
|
|
);
|
|
}
|