mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-25 10:18:31 +02:00
Improve EventViewer UX
- Separate selected item from panel open state (closing panel keeps selection) - Scroll selected item into view when detail panel opens - Enter/Space opens detail panel, Escape closes it - Remove browser focus outline on scroll container - Add prefix prop to EventDetailHeader for labels - Make timestamp optional in EventViewerRow - Add close button to EventDetailHeader - Fix title truncation with min-w-0 - Consolidate HttpResponseTimeline title generation - Add ID/event labels to SSE detail header - Remove fake timestamp from SSE events Closes https://feedback.yaak.app/p/feedback-on-sse-viewer-ux-in-yaak
This commit is contained in:
@@ -47,8 +47,8 @@ function Inner({ response }: Props) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
renderDetail={({ event }) => (
|
renderDetail={({ event, onClose }) => (
|
||||||
<EventDetails event={event} showRaw={showRaw} setShowRaw={setShowRaw} />
|
<EventDetails event={event} showRaw={showRaw} setShowRaw={setShowRaw} onClose={onClose} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -64,10 +64,12 @@ function EventDetails({
|
|||||||
event,
|
event,
|
||||||
showRaw,
|
showRaw,
|
||||||
setShowRaw,
|
setShowRaw,
|
||||||
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
event: HttpResponseEvent;
|
event: HttpResponseEvent;
|
||||||
showRaw: boolean;
|
showRaw: boolean;
|
||||||
setShowRaw: (v: boolean) => void;
|
setShowRaw: (v: boolean) => void;
|
||||||
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { label } = getEventDisplay(event.event);
|
const { label } = getEventDisplay(event.event);
|
||||||
const e = event.event;
|
const e = event.event;
|
||||||
@@ -81,72 +83,76 @@ function EventDetails({
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Determine the title based on event type
|
// Determine the title based on event type
|
||||||
const title =
|
const title = (() => {
|
||||||
e.type === 'header_up'
|
switch (e.type) {
|
||||||
? 'Header Sent'
|
case 'header_up':
|
||||||
: e.type === 'header_down'
|
return 'Header Sent';
|
||||||
? 'Header Received'
|
case 'header_down':
|
||||||
: label;
|
return 'Header Received';
|
||||||
|
case 'send_url':
|
||||||
|
return 'Request';
|
||||||
|
case 'receive_url':
|
||||||
|
return 'Response';
|
||||||
|
case 'redirect':
|
||||||
|
return 'Redirect';
|
||||||
|
case 'setting':
|
||||||
|
return 'Apply Setting';
|
||||||
|
case 'chunk_sent':
|
||||||
|
return 'Data Sent';
|
||||||
|
case 'chunk_received':
|
||||||
|
return 'Data Received';
|
||||||
|
case 'dns_resolved':
|
||||||
|
return e.overridden ? 'DNS Override' : 'DNS Resolution';
|
||||||
|
default:
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Raw view - show plaintext representation
|
// Render content based on view mode and event type
|
||||||
if (showRaw) {
|
const renderContent = () => {
|
||||||
const rawText = formatEventRaw(event.event);
|
// Raw view - show plaintext representation
|
||||||
return (
|
if (showRaw) {
|
||||||
<div className="flex flex-col gap-2 h-full">
|
const rawText = formatEventRaw(event.event);
|
||||||
<EventDetailHeader title={title} timestamp={event.createdAt} actions={actions} />
|
return <Editor language="text" defaultValue={rawText} readOnly stateKey={null} />;
|
||||||
<Editor language="text" defaultValue={rawText} readOnly stateKey={null} />
|
}
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers - show name and value with Editor for JSON
|
// Headers - show name and value
|
||||||
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">
|
|
||||||
<EventDetailHeader title={title} timestamp={event.createdAt} actions={actions} />
|
|
||||||
<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>
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Request URL - show method and path separately
|
// Request URL - show method and path separately
|
||||||
if (e.type === 'send_url') {
|
if (e.type === 'send_url') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<EventDetailHeader title="Request" timestamp={event.createdAt} actions={actions} />
|
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Method">
|
<KeyValueRow label="Method">
|
||||||
<HttpMethodTagRaw forceColor method={e.method} />
|
<HttpMethodTagRaw forceColor method={e.method} />
|
||||||
</KeyValueRow>
|
</KeyValueRow>
|
||||||
<KeyValueRow label="Path">{e.path}</KeyValueRow>
|
<KeyValueRow label="Path">{e.path}</KeyValueRow>
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Response status - show version and status separately
|
// Response status - show version and status separately
|
||||||
if (e.type === 'receive_url') {
|
if (e.type === 'receive_url') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<EventDetailHeader title="Response" timestamp={event.createdAt} actions={actions} />
|
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="HTTP Version">{e.version}</KeyValueRow>
|
<KeyValueRow label="HTTP Version">{e.version}</KeyValueRow>
|
||||||
<KeyValueRow label="Status">
|
<KeyValueRow label="Status">
|
||||||
<HttpStatusTagRaw status={e.status} />
|
<HttpStatusTagRaw status={e.status} />
|
||||||
</KeyValueRow>
|
</KeyValueRow>
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect - show status, URL, and behavior
|
// Redirect - show status, URL, and behavior
|
||||||
if (e.type === 'redirect') {
|
if (e.type === 'redirect') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<EventDetailHeader title="Redirect" timestamp={event.createdAt} actions={actions} />
|
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Status">
|
<KeyValueRow label="Status">
|
||||||
<HttpStatusTagRaw status={e.status} />
|
<HttpStatusTagRaw status={e.status} />
|
||||||
@@ -156,47 +162,27 @@ function EventDetails({
|
|||||||
{e.behavior === 'drop_body' ? 'Drop body, change to GET' : 'Preserve method and body'}
|
{e.behavior === 'drop_body' ? 'Drop body, change to GET' : 'Preserve method and body'}
|
||||||
</KeyValueRow>
|
</KeyValueRow>
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Settings - show as key/value
|
// Settings - show as key/value
|
||||||
if (e.type === 'setting') {
|
if (e.type === 'setting') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<EventDetailHeader title="Apply Setting" timestamp={event.createdAt} actions={actions} />
|
|
||||||
<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>
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Chunks - show formatted bytes
|
// Chunks - show formatted bytes
|
||||||
if (e.type === 'chunk_sent' || e.type === 'chunk_received') {
|
if (e.type === 'chunk_sent' || e.type === 'chunk_received') {
|
||||||
const direction = e.type === 'chunk_sent' ? 'Sent' : 'Received';
|
return <div className="font-mono text-editor">{formatBytes(e.bytes)}</div>;
|
||||||
return (
|
}
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<EventDetailHeader
|
|
||||||
title={`Data ${direction}`}
|
|
||||||
timestamp={event.createdAt}
|
|
||||||
actions={actions}
|
|
||||||
/>
|
|
||||||
<div className="font-mono text-editor">{formatBytes(e.bytes)}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS Resolution - show hostname, addresses, and timing
|
// DNS Resolution - show hostname, addresses, and timing
|
||||||
if (e.type === 'dns_resolved') {
|
if (e.type === 'dns_resolved') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<EventDetailHeader
|
|
||||||
title={e.overridden ? 'DNS Override' : 'DNS Resolution'}
|
|
||||||
timestamp={event.createdAt}
|
|
||||||
actions={actions}
|
|
||||||
/>
|
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Hostname">{e.hostname}</KeyValueRow>
|
<KeyValueRow label="Hostname">{e.hostname}</KeyValueRow>
|
||||||
<KeyValueRow label="Addresses">{e.addresses.join(', ')}</KeyValueRow>
|
<KeyValueRow label="Addresses">{e.addresses.join(', ')}</KeyValueRow>
|
||||||
@@ -207,22 +193,19 @@ function EventDetails({
|
|||||||
`${String(e.duration)}ms`
|
`${String(e.duration)}ms`
|
||||||
)}
|
)}
|
||||||
</KeyValueRow>
|
</KeyValueRow>
|
||||||
|
{e.overridden && <KeyValueRow label="Source">Workspace Override</KeyValueRow>}
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
{e.overridden && (
|
);
|
||||||
<KeyValueRows>
|
}
|
||||||
<KeyValueRow label="Source">Workspace Override</KeyValueRow>
|
|
||||||
</KeyValueRows>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default - use summary
|
// Default - use summary
|
||||||
const { summary } = getEventDisplay(event.event);
|
const { summary } = getEventDisplay(event.event);
|
||||||
|
return <div className="font-mono text-editor">{summary}</div>;
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-2 h-full">
|
||||||
<EventDetailHeader title={label} timestamp={event.createdAt} actions={actions} />
|
<EventDetailHeader title={title} timestamp={event.createdAt} actions={actions} onClose={onClose} />
|
||||||
<div className="font-mono text-editor">{summary}</div>
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -284,7 +267,7 @@ function getEventDisplay(event: HttpResponseEventData): EventDisplay {
|
|||||||
case 'redirect':
|
case 'redirect':
|
||||||
return {
|
return {
|
||||||
icon: 'arrow_big_right_dash',
|
icon: 'arrow_big_right_dash',
|
||||||
color: 'warning',
|
color: 'success',
|
||||||
label: 'Redirect',
|
label: 'Redirect',
|
||||||
summary: `Redirecting ${event.status} ${event.url}${event.behavior === 'drop_body' ? ' (drop body)' : ''}`,
|
summary: `Redirecting ${event.status} ${event.url}${event.behavior === 'drop_body' ? ' (drop body)' : ''}`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function AutoScroller<T>({
|
|||||||
{header ?? <span aria-hidden />}
|
{header ?? <span aria-hidden />}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="h-full w-full overflow-y-auto"
|
className="h-full w-full overflow-y-auto focus:outline-none"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
tabIndex={focusable ? 0 : undefined}
|
tabIndex={focusable ? 0 : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { Button } from './Button';
|
|||||||
import { Separator } from './Separator';
|
import { Separator } from './Separator';
|
||||||
import { SplitLayout } from './SplitLayout';
|
import { SplitLayout } from './SplitLayout';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface EventViewerProps<T> {
|
interface EventViewerProps<T> {
|
||||||
/** Array of events to display */
|
/** Array of events to display */
|
||||||
@@ -27,7 +29,7 @@ interface EventViewerProps<T> {
|
|||||||
}) => ReactNode;
|
}) => ReactNode;
|
||||||
|
|
||||||
/** Render the detail pane for the selected event */
|
/** Render the detail pane for the selected event */
|
||||||
renderDetail?: (props: { event: T; index: number }) => ReactNode;
|
renderDetail?: (props: { event: T; index: number; onClose: () => void }) => ReactNode;
|
||||||
|
|
||||||
/** Optional header above the event list (e.g., connection status) */
|
/** Optional header above the event list (e.g., connection status) */
|
||||||
header?: ReactNode;
|
header?: ReactNode;
|
||||||
@@ -73,6 +75,7 @@ export function EventViewer<T>({
|
|||||||
onActiveIndexChange,
|
onActiveIndexChange,
|
||||||
}: EventViewerProps<T>) {
|
}: EventViewerProps<T>) {
|
||||||
const [activeIndex, setActiveIndexInternal] = useState<number | null>(null);
|
const [activeIndex, setActiveIndexInternal] = useState<number | null>(null);
|
||||||
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
|
|
||||||
// Wrap setActiveIndex to notify parent
|
// Wrap setActiveIndex to notify parent
|
||||||
const setActiveIndex = useCallback(
|
const setActiveIndex = useCallback(
|
||||||
@@ -107,6 +110,8 @@ export function EventViewer<T>({
|
|||||||
virtualizer: virtualizerRef.current,
|
virtualizer: virtualizerRef.current,
|
||||||
isContainerFocused,
|
isContainerFocused,
|
||||||
enabled: enableKeyboardNav,
|
enabled: enableKeyboardNav,
|
||||||
|
closePanel: () => setIsPanelOpen(false),
|
||||||
|
openPanel: () => setIsPanelOpen(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle virtualizer ready callback
|
// Handle virtualizer ready callback
|
||||||
@@ -117,14 +122,23 @@ export function EventViewer<T>({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toggle selection on click
|
// Handle row click - select and open panel, scroll into view
|
||||||
const handleRowClick = useCallback(
|
const handleRowClick = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setActiveIndex((prev) => (prev === index ? null : index));
|
setActiveIndex(index);
|
||||||
|
setIsPanelOpen(true);
|
||||||
|
// Scroll to ensure selected item is visible after panel opens
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
virtualizerRef.current?.scrollToIndex(index, { align: 'auto' });
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[setActiveIndex],
|
[setActiveIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setIsPanelOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="p-3 text-text-subtlest italic">{loadingMessage}</div>;
|
return <div className="p-3 text-text-subtlest italic">{loadingMessage}</div>;
|
||||||
}
|
}
|
||||||
@@ -168,14 +182,14 @@ export function EventViewer<T>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
secondSlot={
|
secondSlot={
|
||||||
activeEvent != null && renderDetail
|
activeEvent != null && renderDetail && isPanelOpen
|
||||||
? ({ style }) => (
|
? ({ style }) => (
|
||||||
<div style={style} className="grid grid-rows-[auto_minmax(0,1fr)] bg-surface">
|
<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>
|
||||||
<div className="mx-2 overflow-y-auto">
|
<div className="mx-2 overflow-y-auto">
|
||||||
{renderDetail({ event: activeEvent, index: activeIndex ?? 0 })}
|
{renderDetail({ event: activeEvent, index: activeIndex ?? 0, onClose: handleClose })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -198,28 +212,30 @@ export interface EventDetailAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EventDetailHeaderProps {
|
interface EventDetailHeaderProps {
|
||||||
/** Title/label for the event */
|
|
||||||
title: string;
|
title: string;
|
||||||
/** Timestamp string (ISO format) - will be formatted as HH:mm:ss.SSS */
|
prefix?: ReactNode;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
/** Optional action buttons to show before timestamp */
|
|
||||||
actions?: EventDetailAction[];
|
actions?: EventDetailAction[];
|
||||||
/** Text to copy when copy button is clicked - renders a copy icon button after actions */
|
|
||||||
copyText?: string;
|
copyText?: string;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Standardized header for event detail panes */
|
|
||||||
export function EventDetailHeader({
|
export function EventDetailHeader({
|
||||||
title,
|
title,
|
||||||
|
prefix,
|
||||||
timestamp,
|
timestamp,
|
||||||
actions,
|
actions,
|
||||||
copyText,
|
copyText,
|
||||||
|
onClose,
|
||||||
}: EventDetailHeaderProps) {
|
}: EventDetailHeaderProps) {
|
||||||
const formattedTime = timestamp ? format(new Date(`${timestamp}Z`), 'HH:mm:ss.SSS') : null;
|
const formattedTime = timestamp ? format(new Date(`${timestamp}Z`), 'HH:mm:ss.SSS') : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-2 mb-2 h-xs">
|
<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 min-w-0">
|
||||||
|
{prefix}
|
||||||
|
<h3 className="font-semibold select-auto cursor-auto truncate">{title}</h3>
|
||||||
|
</HStack>
|
||||||
<HStack space={2} className="items-center">
|
<HStack space={2} className="items-center">
|
||||||
{actions?.map((action) => (
|
{actions?.map((action) => (
|
||||||
<Button key={action.key} variant="border" size="xs" onClick={action.onClick}>
|
<Button key={action.key} variant="border" size="xs" onClick={action.onClick}>
|
||||||
@@ -231,8 +247,11 @@ export function EventDetailHeader({
|
|||||||
<CopyIconButton text={copyText} size="xs" title="Copy" variant="border" iconSize="sm" />
|
<CopyIconButton text={copyText} size="xs" title="Copy" variant="border" iconSize="sm" />
|
||||||
)}
|
)}
|
||||||
{formattedTime && (
|
{formattedTime && (
|
||||||
<span className="text-text-subtlest font-mono text-editor">{formattedTime}</span>
|
<span className="text-text-subtlest font-mono text-editor ml-2">{formattedTime}</span>
|
||||||
)}
|
)}
|
||||||
|
<div className={classNames(copyText != null || formattedTime || (actions ?? []).length > 0 && "border-l border-l-surface-highlight ml-2 pl-3")}>
|
||||||
|
<IconButton color="custom" className="text-text-subtle -mr-3" size="xs" icon="x" title="Close event panel" onClick={onClose} />
|
||||||
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface EventViewerRowProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
timestamp: string;
|
timestamp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventViewerRow({
|
export function EventViewerRow({
|
||||||
@@ -25,13 +25,13 @@ export function EventViewerRow({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left',
|
'w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left',
|
||||||
'px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded',
|
'px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded',
|
||||||
isActive && '!bg-surface-active !text-text',
|
isActive && 'bg-surface-active !text-text',
|
||||||
'text-text-subtle hover:text',
|
'text-text-subtle hover:text',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<div className="w-full truncate">{content}</div>
|
<div className="w-full truncate">{content}</div>
|
||||||
<div className="opacity-50">{format(`${timestamp}Z`, 'HH:mm:ss.SSS')}</div>
|
{timestamp && <div className="opacity-50">{format(`${timestamp}Z`, 'HH:mm:ss.SSS')}</div>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,12 +51,13 @@ function ActualEventStreamViewer({ response }: Props) {
|
|||||||
<span className="truncate text-xs">{event.data.slice(0, 1000)}</span>
|
<span className="truncate text-xs">{event.data.slice(0, 1000)}</span>
|
||||||
</HStack>
|
</HStack>
|
||||||
}
|
}
|
||||||
timestamp={new Date().toISOString().slice(0, -1)} // SSE events don't have timestamps
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderDetail={({ event }) => (
|
renderDetail={({ event, index }) => (
|
||||||
<EventDetail
|
<EventDetail
|
||||||
event={event}
|
event={event}
|
||||||
|
index={index}
|
||||||
showLarge={showLarge}
|
showLarge={showLarge}
|
||||||
showingLarge={showingLarge}
|
showingLarge={showingLarge}
|
||||||
setShowLarge={setShowLarge}
|
setShowLarge={setShowLarge}
|
||||||
@@ -69,12 +70,14 @@ 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;
|
||||||
@@ -87,7 +90,7 @@ function EventDetail({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<EventDetailHeader title="Message Received" />
|
<EventDetailHeader title="Message Received" prefix={<EventLabels event={event} index={index} />} />
|
||||||
{!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
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ interface UseEventViewerKeyboardProps {
|
|||||||
virtualizer?: Virtualizer<HTMLDivElement, Element> | null;
|
virtualizer?: Virtualizer<HTMLDivElement, Element> | null;
|
||||||
isContainerFocused: () => boolean;
|
isContainerFocused: () => boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
closePanel?: () => void;
|
||||||
|
openPanel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEventViewerKeyboard({
|
export function useEventViewerKeyboard({
|
||||||
@@ -18,6 +20,8 @@ export function useEventViewerKeyboard({
|
|||||||
virtualizer,
|
virtualizer,
|
||||||
isContainerFocused,
|
isContainerFocused,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
|
closePanel,
|
||||||
|
openPanel,
|
||||||
}: UseEventViewerKeyboardProps) {
|
}: UseEventViewerKeyboardProps) {
|
||||||
const selectPrev = useCallback(() => {
|
const selectPrev = useCallback(() => {
|
||||||
if (totalCount === 0) return;
|
if (totalCount === 0) return;
|
||||||
@@ -62,9 +66,20 @@ export function useEventViewerKeyboard({
|
|||||||
(e) => {
|
(e) => {
|
||||||
if (!enabled || !isContainerFocused()) return;
|
if (!enabled || !isContainerFocused()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setActiveIndex(null);
|
closePanel?.();
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[enabled, isContainerFocused, setActiveIndex],
|
[enabled, isContainerFocused, closePanel],
|
||||||
|
);
|
||||||
|
|
||||||
|
useKey(
|
||||||
|
(e) => e.key === 'Enter' || e.key === ' ',
|
||||||
|
(e) => {
|
||||||
|
if (!enabled || !isContainerFocused() || activeIndex == null) return;
|
||||||
|
e.preventDefault();
|
||||||
|
openPanel?.();
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[enabled, isContainerFocused, activeIndex, openPanel],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user