import classNames from 'classnames'; import { format, addMilliseconds } from 'date-fns'; import type { CSSProperties, ReactNode } from 'react'; import React, { useEffect, useMemo, useState } from 'react'; import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useGrpcEvents } from '../hooks/useGrpcEvents'; import type { GrpcEvent, GrpcRequest } from '../lib/models'; import { Icon } from './core/Icon'; import { JsonAttributeTree } from './core/JsonAttributeTree'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; import { HStack } from './core/Stacks'; import { EmptyStateText } from './EmptyStateText'; import { RecentConnectionsDropdown } from './RecentConnectionsDropdown'; interface Props { style?: CSSProperties; className?: string; activeRequest: GrpcRequest; methodType: | 'unary' | 'client_streaming' | 'server_streaming' | 'streaming' | 'no-schema' | 'no-method'; } const CONNECTION_RESPONSE_EVENT_ID = 'connection_response'; export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) { const [activeEventId, setActiveEventId] = useState(null); const connections = useGrpcConnections(activeRequest.id ?? null); const activeConnection = connections[0] ?? null; const ogEvents = useGrpcEvents(activeConnection?.id ?? null); const events = useMemo(() => { const createdAt = activeConnection != null && addMilliseconds(activeConnection.createdAt, activeConnection.elapsed) .toISOString() .replace('Z', ''); if (activeConnection == null || activeConnection.elapsed === 0) { return ogEvents; } else if (activeConnection.error != null) { return [ ...ogEvents, { id: CONNECTION_RESPONSE_EVENT_ID, eventType: 'error', content: activeConnection.error, metadata: activeConnection.trailers, createdAt, updatedAt: createdAt, } as GrpcEvent, ]; } else { return [ ...ogEvents, { id: CONNECTION_RESPONSE_EVENT_ID, eventType: activeConnection.status === 0 ? 'connection_response' : 'error', content: `Connection ${GRPC_CODES[activeConnection.status] ?? 'closed'}`, metadata: activeConnection.trailers, createdAt, updatedAt: createdAt, } as GrpcEvent, ]; } }, [activeConnection, ogEvents]); 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 && (
{events.length} messages {activeConnection.elapsed === 0 && ( )} { // todo }} />
{...events.map((m) => ( { if (m.id === activeEventId) setActiveEventId(null); else setActiveEventId(m.id); }} > {m.content} ))}
) } secondSlot={ activeEvent && (() => (
{activeEvent.eventType === 'client_message' || activeEvent.eventType === 'server_message' ? ( <>
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
) : (
{activeEvent.content}
{Object.keys(activeEvent.metadata).length === 0 ? ( No {activeEvent.eventType === 'connection_response' ? 'trailers' : 'metadata'} ) : ( {Object.entries(activeEvent.metadata).map(([key, value]) => ( ))} )}
)}
)) } /> ); } function MessageRow({ onClick, isActive, eventType, children, timestamp, }: { onClick?: () => void; isActive?: boolean; eventType: GrpcEvent['eventType']; children: ReactNode; timestamp: string; }) { return ( ); } const GRPC_CODES: Record = { 0: 'Ok', 1: 'Cancelled', 2: 'Unknown', 3: 'Invalid argument', 4: 'Deadline exceeded', 5: 'Not found', 6: 'Already exists', 7: 'Permission denied', 8: 'Resource exhausted', 9: 'Failed precondition', 10: 'Aborted', 11: 'Out of range', 12: 'Unimplemented', 13: 'Internal', 14: 'Unavailable', 15: 'Data loss', 16: 'Unauthenticated', };