import type { GrpcEvent, GrpcRequest } from '@yaakapp-internal/models'; import { useAtomValue, useSetAtom } from 'jotai'; import type { CSSProperties } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { activeGrpcConnectionAtom, activeGrpcConnections, pinnedGrpcConnectionIdAtom, useGrpcEvents, } from '../hooks/usePinnedGrpcConnection'; import { useStateWithDeps } from '../hooks/useStateWithDeps'; import { Button } from './core/Button'; import { Editor } from './core/Editor/LazyEditor'; import { EventDetailHeader, EventViewer } from './core/EventViewer'; import { EventViewerRow } from './core/EventViewerRow'; import { HotkeyList } from './core/HotkeyList'; import { Icon, type IconProps } from './core/Icon'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { LoadingIcon } from './core/LoadingIcon'; import { HStack, VStack } from './core/Stacks'; import { EmptyStateText } from './EmptyStateText'; import { ErrorBoundary } from './ErrorBoundary'; import { RecentGrpcConnectionsDropdown } from './RecentGrpcConnectionsDropdown'; interface Props { style?: CSSProperties; className?: string; activeRequest: GrpcRequest; methodType: | 'unary' | 'client_streaming' | 'server_streaming' | 'streaming' | 'no-schema' | 'no-method'; } export function GrpcResponsePane({ style, methodType, activeRequest }: Props) { const [activeEventIndex, setActiveEventIndex] = useState(null); const [showLarge, setShowLarge] = useStateWithDeps(false, [activeRequest.id]); const [showingLarge, setShowingLarge] = useState(false); const connections = useAtomValue(activeGrpcConnections); const activeConnection = useAtomValue(activeGrpcConnectionAtom); const events = useGrpcEvents(activeConnection?.id ?? null); const setPinnedGrpcConnectionId = useSetAtom(pinnedGrpcConnectionIdAtom); const activeEvent = useMemo( () => (activeEventIndex != null ? events[activeEventIndex] : null), [activeEventIndex, events], ); // Set the active message to the first message received if unary // biome-ignore lint/correctness/useExhaustiveDependencies: none useEffect(() => { if (events.length === 0 || activeEvent != null || methodType !== 'unary') { return; } const firstServerMessageIndex = events.findIndex((m) => m.eventType === 'server_message'); if (firstServerMessageIndex !== -1) { setActiveEventIndex(firstServerMessageIndex); } }, [events.length]); if (activeConnection == null) { return ( ); } const header = ( {events.length} Messages {activeConnection.state !== 'closed' && ( )}
); return (
event.id} error={activeConnection.error} header={header} splitLayoutName="grpc_events" defaultRatio={0.4} renderRow={({ event, isActive, onClick }) => ( )} renderDetail={({ event }) => ( )} />
); } function GrpcEventRow({ event, isActive, onClick, }: { event: GrpcEvent; isActive: boolean; onClick: () => void; }) { const { eventType, status, content, error } = event; const display = getEventDisplay(eventType, status); return ( } content={ {content.slice(0, 1000)} {error && ({error})} } timestamp={event.createdAt} /> ); } function GrpcEventDetail({ event, showLarge, showingLarge, setShowLarge, setShowingLarge, }: { event: GrpcEvent; showLarge: boolean; showingLarge: boolean; setShowLarge: (v: boolean) => void; setShowingLarge: (v: boolean) => void; }) { if (event.eventType === 'client_message' || event.eventType === 'server_message') { const title = `Message ${event.eventType === 'client_message' ? 'Sent' : 'Received'}`; return (
{!showLarge && event.content.length > 1000 * 1000 ? ( Message previews larger than 1MB are hidden
) : ( )}
); } // Error or connection_end - show metadata/trailers return (
{event.error && (
{event.error}
)}
{Object.keys(event.metadata).length === 0 ? ( No {event.eventType === 'connection_end' ? 'trailers' : 'metadata'} ) : ( {Object.entries(event.metadata).map(([key, value]) => ( {value} ))} )}
); } function getEventDisplay( eventType: GrpcEvent['eventType'], status: GrpcEvent['status'], ): { icon: IconProps['icon']; color: IconProps['color']; title: string } { if (eventType === 'server_message') { return { icon: 'arrow_big_down_dash', color: 'info', title: 'Server message' }; } if (eventType === 'client_message') { return { icon: 'arrow_big_up_dash', color: 'primary', title: 'Client message' }; } if (eventType === 'error' || (status != null && status > 0)) { return { icon: 'alert_triangle', color: 'danger', title: 'Error' }; } if (eventType === 'connection_end') { return { icon: 'check', color: 'success', title: 'Connection response' }; } return { icon: 'info', color: undefined, title: 'Event' }; }