import type { GrpcEvent, GrpcRequest } from '@yaakapp-internal/models'; import classNames from 'classnames'; import { format } from 'date-fns'; import { useAtomValue, useSetAtom } from 'jotai'; import type { CSSProperties } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { activeGrpcConnectionAtom, activeGrpcConnections, pinnedGrpcConnectionIdAtom, useGrpcEvents, } from '../hooks/usePinnedGrpcConnection'; import { useStateWithDeps } from '../hooks/useStateWithDeps'; import { copyToClipboard } from '../lib/copy'; import { AutoScroller } from './core/AutoScroller'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { Editor } from './core/Editor/LazyEditor'; import { HotKeyList } from './core/HotKeyList'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { LoadingIcon } from './core/LoadingIcon'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; 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 [activeEventId, setActiveEventId] = 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( () => events.find((m) => m.id === activeEventId) ?? null, [activeEventId, events], ); // Set active message to the first message received if unary useEffect(() => { if (events.length === 0 || activeEvent != null || methodType !== 'unary') { return; } setActiveEventId(events.find((m) => m.eventType === 'server_message')?.id ?? null); // eslint-disable-next-line react-hooks/exhaustive-deps }, [events.length]); return ( activeConnection == null ? ( ) : (
{events.length} Messages {activeConnection.state !== 'closed' && ( )}
{activeConnection.error} ) } render={(event) => ( { if (event.id === activeEventId) setActiveEventId(null); else setActiveEventId(event.id); }} /> )} />
) } secondSlot={ activeEvent != null && activeConnection != null ? () => (
{activeEvent.eventType === 'client_message' || activeEvent.eventType === 'server_message' ? ( <>
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
copyToClipboard(activeEvent.content)} />
{!showLarge && activeEvent.content.length > 1000 * 1000 ? ( Message previews larger than 1MB are hidden
) : ( )} ) : (
{activeEvent.content}
{activeEvent.error && (
{activeEvent.error}
)}
{Object.keys(activeEvent.metadata).length === 0 ? ( No{' '} {activeEvent.eventType === 'connection_end' ? 'trailers' : 'metadata'} ) : ( {Object.entries(activeEvent.metadata).map(([key, value]) => ( {value} ))} )}
)}
) : null } /> ); } function EventRow({ onClick, isActive, event, }: { onClick?: () => void; isActive?: boolean; event: GrpcEvent; }) { const { eventType, status, createdAt, content, error } = event; const ref = useRef(null); return (
); }