mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-27 11:07:03 +02:00
Add EventDetailHeader component and fix EventViewer overflow
- Create standardized EventDetailHeader with title, timestamp, actions, and copyText props - Fix EventViewer firstSlot overflow/scrolling issue - Update GrpcResponsePane, WebsocketResponsePane, HttpResponseTimeline, and EventStreamViewer to use EventDetailHeader - Fix Timeline title consistency when toggling Raw/Formatted views
This commit is contained in:
@@ -9,14 +9,12 @@ import {
|
|||||||
useGrpcEvents,
|
useGrpcEvents,
|
||||||
} from '../hooks/usePinnedGrpcConnection';
|
} from '../hooks/usePinnedGrpcConnection';
|
||||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||||
import { copyToClipboard } from '../lib/copy';
|
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { Editor } from './core/Editor/LazyEditor';
|
import { Editor } from './core/Editor/LazyEditor';
|
||||||
import { EventViewer } from './core/EventViewer';
|
import { EventDetailHeader, EventViewer } from './core/EventViewer';
|
||||||
import { EventViewerRow } from './core/EventViewerRow';
|
import { EventViewerRow } from './core/EventViewerRow';
|
||||||
import { HotkeyList } from './core/HotkeyList';
|
import { HotkeyList } from './core/HotkeyList';
|
||||||
import { Icon, type IconProps } from './core/Icon';
|
import { Icon, type IconProps } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
|
||||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||||
import { LoadingIcon } from './core/LoadingIcon';
|
import { LoadingIcon } from './core/LoadingIcon';
|
||||||
import { HStack, VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
@@ -157,19 +155,11 @@ function GrpcEventDetail({
|
|||||||
setShowingLarge: (v: boolean) => void;
|
setShowingLarge: (v: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
if (event.eventType === 'client_message' || event.eventType === 'server_message') {
|
if (event.eventType === 'client_message' || event.eventType === 'server_message') {
|
||||||
|
const title = `Message ${event.eventType === 'client_message' ? 'Sent' : 'Received'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
<div className="mb-2 select-text cursor-text grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
<EventDetailHeader title={title} timestamp={event.createdAt} copyText={event.content} />
|
||||||
<div className="font-semibold">
|
|
||||||
Message {event.eventType === 'client_message' ? 'Sent' : 'Received'}
|
|
||||||
</div>
|
|
||||||
<IconButton
|
|
||||||
title="Copy message"
|
|
||||||
icon="copy"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => copyToClipboard(event.content)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!showLarge && event.content.length > 1000 * 1000 ? (
|
{!showLarge && event.content.length > 1000 * 1000 ? (
|
||||||
<VStack space={2} className="italic text-text-subtlest">
|
<VStack space={2} className="italic text-text-subtlest">
|
||||||
Message previews larger than 1MB are hidden
|
Message previews larger than 1MB are hidden
|
||||||
@@ -207,14 +197,12 @@ function GrpcEventDetail({
|
|||||||
// Error or connection_end - show metadata/trailers
|
// Error or connection_end - show metadata/trailers
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
<div>
|
<EventDetailHeader title={event.content} timestamp={event.createdAt} />
|
||||||
<div className="select-text cursor-text font-semibold">{event.content}</div>
|
{event.error && (
|
||||||
{event.error && (
|
<div className="select-text cursor-text text-sm font-mono py-1 text-warning">
|
||||||
<div className="select-text cursor-text text-sm font-mono py-1 text-warning">
|
{event.error}
|
||||||
{event.error}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="py-2 h-full">
|
<div className="py-2 h-full">
|
||||||
{Object.keys(event.metadata).length === 0 ? (
|
{Object.keys(event.metadata).length === 0 ? (
|
||||||
<EmptyStateText>
|
<EmptyStateText>
|
||||||
|
|||||||
@@ -3,18 +3,15 @@ import type {
|
|||||||
HttpResponseEvent,
|
HttpResponseEvent,
|
||||||
HttpResponseEventData,
|
HttpResponseEventData,
|
||||||
} from '@yaakapp-internal/models';
|
} from '@yaakapp-internal/models';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { type ReactNode, useState } from 'react';
|
import { type ReactNode, useState } from 'react';
|
||||||
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents';
|
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents';
|
||||||
import { Button } from './core/Button';
|
|
||||||
import { Editor } from './core/Editor/LazyEditor';
|
import { Editor } from './core/Editor/LazyEditor';
|
||||||
import { EventViewer } from './core/EventViewer';
|
import { EventDetailHeader, EventViewer, type EventDetailAction } from './core/EventViewer';
|
||||||
import { EventViewerRow } from './core/EventViewerRow';
|
import { EventViewerRow } from './core/EventViewerRow';
|
||||||
import { HttpMethodTagRaw } from './core/HttpMethodTag';
|
import { HttpMethodTagRaw } from './core/HttpMethodTag';
|
||||||
import { HttpStatusTagRaw } from './core/HttpStatusTag';
|
import { HttpStatusTagRaw } from './core/HttpStatusTag';
|
||||||
import { Icon, type IconProps } from './core/Icon';
|
import { Icon, type IconProps } from './core/Icon';
|
||||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||||
import { HStack } from './core/Stacks';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
@@ -73,20 +70,30 @@ function EventDetails({
|
|||||||
setShowRaw: (v: boolean) => void;
|
setShowRaw: (v: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { label } = getEventDisplay(event.event);
|
const { label } = getEventDisplay(event.event);
|
||||||
const timestamp = format(new Date(`${event.createdAt}Z`), 'HH:mm:ss.SSS');
|
|
||||||
const e = event.event;
|
const e = event.event;
|
||||||
|
|
||||||
|
const actions: EventDetailAction[] = [
|
||||||
|
{
|
||||||
|
key: 'toggle-raw',
|
||||||
|
label: showRaw ? 'Formatted' : 'Text',
|
||||||
|
onClick: () => setShowRaw(!showRaw),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Determine the title based on event type
|
||||||
|
const title =
|
||||||
|
e.type === 'header_up'
|
||||||
|
? 'Header Sent'
|
||||||
|
: e.type === 'header_down'
|
||||||
|
? 'Header Received'
|
||||||
|
: label;
|
||||||
|
|
||||||
// Raw view - show plaintext representation
|
// Raw view - show plaintext representation
|
||||||
if (showRaw) {
|
if (showRaw) {
|
||||||
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
|
<EventDetailHeader title={title} timestamp={event.createdAt} actions={actions} />
|
||||||
title={label}
|
|
||||||
timestamp={timestamp}
|
|
||||||
showRaw={showRaw}
|
|
||||||
setShowRaw={setShowRaw}
|
|
||||||
/>
|
|
||||||
<Editor language="text" defaultValue={rawText} readOnly stateKey={null} />
|
<Editor language="text" defaultValue={rawText} readOnly stateKey={null} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -96,12 +103,7 @@ function EventDetails({
|
|||||||
if (e.type === 'header_up' || e.type === 'header_down') {
|
if (e.type === 'header_up' || e.type === 'header_down') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 h-full">
|
<div className="flex flex-col gap-2 h-full">
|
||||||
<DetailHeader
|
<EventDetailHeader title={title} timestamp={event.createdAt} actions={actions} />
|
||||||
title={e.type === 'header_down' ? 'Header Received' : 'Header Sent'}
|
|
||||||
timestamp={timestamp}
|
|
||||||
showRaw={showRaw}
|
|
||||||
setShowRaw={setShowRaw}
|
|
||||||
/>
|
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Header">{e.name}</KeyValueRow>
|
<KeyValueRow label="Header">{e.name}</KeyValueRow>
|
||||||
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
||||||
@@ -114,12 +116,7 @@ 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
|
<EventDetailHeader title="Request" timestamp={event.createdAt} actions={actions} />
|
||||||
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} />
|
||||||
@@ -134,12 +131,7 @@ 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
|
<EventDetailHeader title="Response" timestamp={event.createdAt} actions={actions} />
|
||||||
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">
|
||||||
@@ -154,12 +146,7 @@ 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
|
<EventDetailHeader title="Redirect" timestamp={event.createdAt} actions={actions} />
|
||||||
title="Redirect"
|
|
||||||
timestamp={timestamp}
|
|
||||||
showRaw={showRaw}
|
|
||||||
setShowRaw={setShowRaw}
|
|
||||||
/>
|
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Status">
|
<KeyValueRow label="Status">
|
||||||
<HttpStatusTagRaw status={e.status} />
|
<HttpStatusTagRaw status={e.status} />
|
||||||
@@ -177,12 +164,7 @@ 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
|
<EventDetailHeader title="Apply Setting" timestamp={event.createdAt} actions={actions} />
|
||||||
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>
|
||||||
@@ -196,11 +178,10 @@ 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
|
<EventDetailHeader
|
||||||
title={`Data ${direction}`}
|
title={`Data ${direction}`}
|
||||||
timestamp={timestamp}
|
timestamp={event.createdAt}
|
||||||
showRaw={showRaw}
|
actions={actions}
|
||||||
setShowRaw={setShowRaw}
|
|
||||||
/>
|
/>
|
||||||
<div className="font-mono text-editor">{formatBytes(e.bytes)}</div>
|
<div className="font-mono text-editor">{formatBytes(e.bytes)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,57 +192,33 @@ function EventDetails({
|
|||||||
const { summary } = getEventDisplay(event.event);
|
const { summary } = getEventDisplay(event.event);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<DetailHeader title={label} timestamp={timestamp} showRaw={showRaw} setShowRaw={setShowRaw} />
|
<EventDetailHeader title={label} timestamp={event.createdAt} actions={actions} />
|
||||||
<div className="font-mono text-editor">{summary}</div>
|
<div className="font-mono text-editor">{summary}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailHeader({
|
|
||||||
title,
|
|
||||||
timestamp,
|
|
||||||
showRaw,
|
|
||||||
setShowRaw,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
timestamp: string;
|
|
||||||
showRaw: boolean;
|
|
||||||
setShowRaw: (v: boolean) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<HStack space={2} className="items-center">
|
|
||||||
<h3 className="font-semibold select-auto cursor-auto">{title}</h3>
|
|
||||||
<Button variant="border" size="xs" onClick={() => setShowRaw(!showRaw)}>
|
|
||||||
{showRaw ? 'Formatted' : 'Raw'}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
<span className="text-text-subtlest font-mono text-editor">{timestamp}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format event as raw plaintext for debugging */
|
/** Format event as raw plaintext for debugging */
|
||||||
function formatEventRaw(event: HttpResponseEventData): string {
|
function formatEventRaw(event: HttpResponseEventData): string {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'send_url':
|
case 'send_url':
|
||||||
return `> ${event.method} ${event.path}`;
|
return `${event.method} ${event.path}`;
|
||||||
case 'receive_url':
|
case 'receive_url':
|
||||||
return `< ${event.version} ${event.status}`;
|
return `${event.version} ${event.status}`;
|
||||||
case 'header_up':
|
case 'header_up':
|
||||||
return `> ${event.name}: ${event.value}`;
|
return `${event.name}: ${event.value}`;
|
||||||
case 'header_down':
|
case 'header_down':
|
||||||
return `< ${event.name}: ${event.value}`;
|
return `${event.name}: ${event.value}`;
|
||||||
case 'redirect':
|
case 'redirect':
|
||||||
return `< ${event.status} Redirect: ${event.url}`;
|
return `${event.status} Redirect: ${event.url}`;
|
||||||
case 'setting':
|
case 'setting':
|
||||||
return `[setting] ${event.name} = ${event.value}`;
|
return `${event.name} = ${event.value}`;
|
||||||
case 'info':
|
case 'info':
|
||||||
return `[info] ${event.message}`;
|
return `${event.message}`;
|
||||||
case 'chunk_sent':
|
case 'chunk_sent':
|
||||||
return `> [${formatBytes(event.bytes)} sent]`;
|
return `[${formatBytes(event.bytes)} sent]`;
|
||||||
case 'chunk_received':
|
case 'chunk_received':
|
||||||
return `< [${formatBytes(event.bytes)} received]`;
|
return `[${formatBytes(event.bytes)} received]`;
|
||||||
default:
|
default:
|
||||||
return '[unknown event]';
|
return '[unknown event]';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ import {
|
|||||||
} from '../hooks/usePinnedWebsocketConnection';
|
} from '../hooks/usePinnedWebsocketConnection';
|
||||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||||
import { languageFromContentType } from '../lib/contentType';
|
import { languageFromContentType } from '../lib/contentType';
|
||||||
import { copyToClipboard } from '../lib/copy';
|
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { Editor } from './core/Editor/LazyEditor';
|
import { Editor } from './core/Editor/LazyEditor';
|
||||||
import { EventViewer } from './core/EventViewer';
|
import { EventDetailHeader, EventViewer, type EventDetailAction } from './core/EventViewer';
|
||||||
import { EventViewerRow } from './core/EventViewerRow';
|
import { EventViewerRow } from './core/EventViewerRow';
|
||||||
import { HotkeyList } from './core/HotkeyList';
|
import { HotkeyList } from './core/HotkeyList';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
|
||||||
import { LoadingIcon } from './core/LoadingIcon';
|
import { LoadingIcon } from './core/LoadingIcon';
|
||||||
import { HStack, VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { WebsocketStatusTag } from './core/WebsocketStatusTag';
|
import { WebsocketStatusTag } from './core/WebsocketStatusTag';
|
||||||
@@ -173,24 +171,25 @@ function WebsocketEventDetail({
|
|||||||
? 'Connection Open'
|
? 'Connection Open'
|
||||||
: `Message ${event.isServer ? 'Received' : 'Sent'}`;
|
: `Message ${event.isServer ? 'Received' : 'Sent'}`;
|
||||||
|
|
||||||
|
const actions: EventDetailAction[] =
|
||||||
|
message !== ''
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: 'toggle-hexdump',
|
||||||
|
label: hexDump ? 'Show Message' : 'Show Hexdump',
|
||||||
|
onClick: () => setHexDump(!hexDump),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
<div className="h-xs mb-2 grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
<EventDetailHeader
|
||||||
<div className="font-semibold">{title}</div>
|
title={title}
|
||||||
{message !== '' && (
|
timestamp={event.createdAt}
|
||||||
<HStack space={1}>
|
actions={actions}
|
||||||
<Button variant="border" size="xs" onClick={() => setHexDump(!hexDump)}>
|
copyText={formattedMessage || undefined}
|
||||||
{hexDump ? 'Show Message' : 'Show Hexdump'}
|
/>
|
||||||
</Button>
|
|
||||||
<IconButton
|
|
||||||
title="Copy message"
|
|
||||||
icon="copy"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => copyToClipboard(formattedMessage ?? '')}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!showLarge && event.message.length > 1000 * 1000 ? (
|
{!showLarge && event.message.length > 1000 * 1000 ? (
|
||||||
<VStack space={2} className="italic text-text-subtlest">
|
<VStack space={2} className="italic text-text-subtlest">
|
||||||
Message previews larger than 1MB are hidden
|
Message previews larger than 1MB are hidden
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function AutoScroller<T>({
|
|||||||
}, [autoScroll, data.length]);
|
}, [autoScroll, data.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative grid grid-rows-[minmax(0,auto)_minmax(0,1fr)]">
|
<div className="h-full w-full relative grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
{!autoScroll && (
|
{!autoScroll && (
|
||||||
<div className="absolute bottom-0 right-0 m-2">
|
<div className="absolute bottom-0 right-0 m-2">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import type { Virtualizer } from '@tanstack/react-virtual';
|
import type { Virtualizer } from '@tanstack/react-virtual';
|
||||||
|
import { format } from 'date-fns';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useEventViewerKeyboard } from '../../hooks/useEventViewerKeyboard';
|
import { useEventViewerKeyboard } from '../../hooks/useEventViewerKeyboard';
|
||||||
|
import { CopyIconButton } from '../CopyIconButton';
|
||||||
import { AutoScroller } from './AutoScroller';
|
import { AutoScroller } from './AutoScroller';
|
||||||
import { Banner } from './Banner';
|
import { Banner } from './Banner';
|
||||||
|
import { Button } from './Button';
|
||||||
import { Separator } from './Separator';
|
import { Separator } from './Separator';
|
||||||
import { SplitLayout } from './SplitLayout';
|
import { SplitLayout } from './SplitLayout';
|
||||||
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
interface EventViewerProps<T> {
|
interface EventViewerProps<T> {
|
||||||
/** Array of events to display */
|
/** Array of events to display */
|
||||||
@@ -137,8 +141,8 @@ export function EventViewer<T>({
|
|||||||
defaultRatio={defaultRatio}
|
defaultRatio={defaultRatio}
|
||||||
minHeightPx={10}
|
minHeightPx={10}
|
||||||
firstSlot={({ style }) => (
|
firstSlot={({ style }) => (
|
||||||
<div style={style} className="w-full grid grid-rows-[auto_minmax(0,1fr)]">
|
<div style={style} className="w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
{header}
|
{header ?? <span aria-hidden />}
|
||||||
<AutoScroller
|
<AutoScroller
|
||||||
data={events}
|
data={events}
|
||||||
focusable={enableKeyboardNav}
|
focusable={enableKeyboardNav}
|
||||||
@@ -166,7 +170,7 @@ export function EventViewer<T>({
|
|||||||
secondSlot={
|
secondSlot={
|
||||||
activeEvent != null && renderDetail
|
activeEvent != null && renderDetail
|
||||||
? ({ style }) => (
|
? ({ style }) => (
|
||||||
<div style={style} className="grid grid-rows-[auto_minmax(0,1fr)]">
|
<div style={style} className="grid grid-rows-[auto_minmax(0,1fr)] bg-surface">
|
||||||
<div className="pb-3 px-2">
|
<div className="pb-3 px-2">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
@@ -181,3 +185,55 @@ export function EventViewer<T>({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EventDetailAction {
|
||||||
|
/** Unique key for React */
|
||||||
|
key: string;
|
||||||
|
/** Button label */
|
||||||
|
label: string;
|
||||||
|
/** Optional icon */
|
||||||
|
icon?: ReactNode;
|
||||||
|
/** Click handler */
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventDetailHeaderProps {
|
||||||
|
/** Title/label for the event */
|
||||||
|
title: string;
|
||||||
|
/** Timestamp string (ISO format) - will be formatted as HH:mm:ss.SSS */
|
||||||
|
timestamp?: string;
|
||||||
|
/** Optional action buttons to show before timestamp */
|
||||||
|
actions?: EventDetailAction[];
|
||||||
|
/** Text to copy when copy button is clicked - renders a copy icon button after actions */
|
||||||
|
copyText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Standardized header for event detail panes */
|
||||||
|
export function EventDetailHeader({
|
||||||
|
title,
|
||||||
|
timestamp,
|
||||||
|
actions,
|
||||||
|
copyText,
|
||||||
|
}: EventDetailHeaderProps) {
|
||||||
|
const formattedTime = timestamp ? format(new Date(`${timestamp}Z`), 'HH:mm:ss.SSS') : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2 mb-2 h-xs">
|
||||||
|
<h3 className="font-semibold select-auto cursor-auto">{title}</h3>
|
||||||
|
<HStack space={2} className="items-center">
|
||||||
|
{actions?.map((action) => (
|
||||||
|
<Button key={action.key} variant="border" size="xs" onClick={action.onClick}>
|
||||||
|
{action.icon}
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
{copyText != null && (
|
||||||
|
<CopyIconButton text={copyText} size="xs" title="Copy" variant="border" iconSize="sm" />
|
||||||
|
)}
|
||||||
|
{formattedTime && (
|
||||||
|
<span className="text-text-subtlest font-mono text-editor">{formattedTime}</span>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { isJSON } from '../../lib/contentType';
|
|||||||
import { Button } from '../core/Button';
|
import { Button } from '../core/Button';
|
||||||
import type { EditorProps } from '../core/Editor/Editor';
|
import type { EditorProps } from '../core/Editor/Editor';
|
||||||
import { Editor } from '../core/Editor/LazyEditor';
|
import { Editor } from '../core/Editor/LazyEditor';
|
||||||
import { EventViewer } from '../core/EventViewer';
|
import { EventDetailHeader, EventViewer } from '../core/EventViewer';
|
||||||
import { EventViewerRow } from '../core/EventViewerRow';
|
import { EventViewerRow } from '../core/EventViewerRow';
|
||||||
import { Icon } from '../core/Icon';
|
import { Icon } from '../core/Icon';
|
||||||
import { InlineCode } from '../core/InlineCode';
|
import { InlineCode } from '../core/InlineCode';
|
||||||
@@ -54,10 +54,9 @@ function ActualEventStreamViewer({ response }: Props) {
|
|||||||
timestamp={new Date().toISOString().slice(0, -1)} // SSE events don't have timestamps
|
timestamp={new Date().toISOString().slice(0, -1)} // SSE events don't have timestamps
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderDetail={({ event, index }) => (
|
renderDetail={({ event }) => (
|
||||||
<EventDetail
|
<EventDetail
|
||||||
event={event}
|
event={event}
|
||||||
index={index}
|
|
||||||
showLarge={showLarge}
|
showLarge={showLarge}
|
||||||
showingLarge={showingLarge}
|
showingLarge={showingLarge}
|
||||||
setShowLarge={setShowLarge}
|
setShowLarge={setShowLarge}
|
||||||
@@ -70,14 +69,12 @@ function ActualEventStreamViewer({ response }: Props) {
|
|||||||
|
|
||||||
function EventDetail({
|
function EventDetail({
|
||||||
event,
|
event,
|
||||||
index,
|
|
||||||
showLarge,
|
showLarge,
|
||||||
showingLarge,
|
showingLarge,
|
||||||
setShowLarge,
|
setShowLarge,
|
||||||
setShowingLarge,
|
setShowingLarge,
|
||||||
}: {
|
}: {
|
||||||
event: ServerSentEvent;
|
event: ServerSentEvent;
|
||||||
index: number;
|
|
||||||
showLarge: boolean;
|
showLarge: boolean;
|
||||||
showingLarge: boolean;
|
showingLarge: boolean;
|
||||||
setShowLarge: (v: boolean) => void;
|
setShowLarge: (v: boolean) => void;
|
||||||
@@ -90,10 +87,7 @@ function EventDetail({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<HStack space={1.5} className="mb-2 font-semibold">
|
<EventDetailHeader title="Message Received" />
|
||||||
<EventLabels className="text-sm" event={event} index={index} />
|
|
||||||
Message Received
|
|
||||||
</HStack>
|
|
||||||
{!showLarge && event.data.length > 1000 * 1000 ? (
|
{!showLarge && event.data.length > 1000 * 1000 ? (
|
||||||
<VStack space={2} className="italic text-text-subtlest">
|
<VStack space={2} className="italic text-text-subtlest">
|
||||||
Message previews larger than 1MB are hidden
|
Message previews larger than 1MB are hidden
|
||||||
|
|||||||
Reference in New Issue
Block a user