import useResizeObserver from '@react-hook/resize-observer'; import classNames from 'classnames'; import { format } from 'date-fns'; import type { CSSProperties, FormEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useAlert } from '../hooks/useAlert'; import { useGrpc } from '../hooks/useGrpc'; import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useGrpcMessages } from '../hooks/useGrpcMessages'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { HotKeyList } from './core/HotKeyList'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { JsonAttributeTree } from './core/JsonAttributeTree'; import { RadioDropdown } from './core/RadioDropdown'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; import { HStack, VStack } from './core/Stacks'; import { GrpcEditor } from './GrpcEditor'; import { UrlBar } from './UrlBar'; interface Props { style: CSSProperties; } export function GrpcConnectionLayout({ style }: Props) { const activeRequest = useActiveRequest('grpc_request'); const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); const alert = useAlert(); const [activeMessageId, setActiveMessageId] = useState(null); const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.id ?? null); const connections = useGrpcConnections(activeRequest?.id ?? null); const activeConnection = connections[0] ?? null; const messages = useGrpcMessages(activeConnection?.id ?? null); const activeMethod = useMemo(() => { if (grpc.services == null || activeRequest == null) return null; const s = grpc.services.find((s) => s.name === activeRequest.service); if (s == null) return null; return s.methods.find((m) => m.name === activeRequest.method); }, [activeRequest, grpc.services]); const handleConnect = useCallback( async (e: FormEvent) => { e.preventDefault(); if (activeMethod == null || activeRequest == null) return; if (activeRequest.service == null || activeRequest.method == null) { alert({ id: 'grpc-invalid-service-method', title: 'Error', body: 'Service or method not selected', }); } if (activeMethod.clientStreaming && activeMethod.serverStreaming) { await grpc.streaming.mutateAsync(activeRequest.id); } else if (!activeMethod.clientStreaming && activeMethod.serverStreaming) { await grpc.serverStreaming.mutateAsync(activeRequest.id); } else if (activeMethod.clientStreaming && !activeMethod.serverStreaming) { await grpc.clientStreaming.mutateAsync(activeRequest.id); } else { const msg = await grpc.unary.mutateAsync(activeRequest.id); setActiveMessageId(msg.id); } }, [ activeMethod, activeRequest, alert, grpc.streaming, grpc.serverStreaming, grpc.clientStreaming, grpc.unary, ], ); useEffect(() => { if (grpc.services == null || activeRequest == null) return; const s = grpc.services.find((s) => s.name === activeRequest.service); if (s == null) { updateRequest.mutate({ service: grpc.services[0]?.name ?? null, method: grpc.services[0]?.methods[0]?.name ?? null, }); return; } const m = s.methods.find((m) => m.name === activeRequest.method); if (m == null) { updateRequest.mutate({ method: s.methods[0]?.name ?? null }); return; } }, [activeRequest, grpc.services, updateRequest]); const handleChangeService = useCallback( async (v: string) => { const [serviceName, methodName] = v.split('/', 2); if (serviceName == null || methodName == null) throw new Error('Should never happen'); await updateRequest.mutateAsync({ service: serviceName, method: methodName, }); }, [updateRequest], ); const handleChangeUrl = useCallback( (url: string) => updateRequest.mutateAsync({ url }), [updateRequest], ); const handleChangeMessage = useCallback( (message: string) => updateRequest.mutateAsync({ message }), [updateRequest], ); const select = useMemo(() => { const options = grpc.services?.flatMap((s) => s.methods.map((m) => ({ label: `${s.name.split('.', 2).pop() ?? s.name}/${m.name}`, value: `${s.name}/${m.name}`, })), ) ?? []; const value = `${activeRequest?.service ?? ''}/${activeRequest?.method ?? ''}`; return { value, options }; }, [activeRequest?.method, activeRequest?.service, grpc.services]); const [paneSize, setPaneSize] = useState(99999); const urlContainerEl = useRef(null); useResizeObserver(urlContainerEl.current, (entry) => { setPaneSize(entry.contentRect.width); }); const activeMessage = useMemo( () => messages.find((m) => m.id === activeMessageId) ?? null, [activeMessageId, messages], ); const messageType: 'unary' | 'server_streaming' | 'client_streaming' | 'streaming' = useMemo(() => { if (activeMethod == null) return 'unary'; // Good enough if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming'; if (activeMethod.clientStreaming) return 'client_streaming'; if (activeMethod.serverStreaming) return 'server_streaming'; return 'unary'; }, [activeMethod]); if (activeRequest == null) { return null; } return ( (
({ label: o.label, value: o.value, type: 'default', shortLabel: o.label, }))} onChange={handleChangeService} > grpc.cancel.mutateAsync() : handleConnect} disabled={grpc.isStreaming} spin={grpc.isStreaming || grpc.unary.isLoading} icon={ grpc.isStreaming ? 'refresh' : messageType === 'unary' ? 'sendHorizontal' : 'arrowUpDown' } /> {grpc.isStreaming && ( grpc.cancel.mutateAsync()} icon="x" disabled={!grpc.isStreaming} /> )} {activeMethod?.clientStreaming && !activeMethod.serverStreaming && grpc.isStreaming && ( grpc.commit.mutateAsync()} icon="check" /> )} {activeMethod?.clientStreaming && grpc.isStreaming && ( grpc.send.mutateAsync({ message: activeRequest.message ?? '' })} icon="sendHorizontal" /> )}
)} secondSlot={() => !grpc.unary.isLoading && (
{grpc.unary.error ? ( {grpc.unary.error} ) : messages.length >= 0 ? ( (
{...messages.map((m) => ( { if (m.id === activeMessageId) setActiveMessageId(null); else setActiveMessageId(m.id); }} alignItems="center" className={classNames( 'px-2 py-1 font-mono', m === activeMessage && 'bg-highlight', )} >
{m.message}
{format(m.createdAt, 'HH:mm:ss')}
))}
)} secondSlot={ !activeMessage ? null : () => (
{activeMessage.isInfo ? ( {activeMessage.message} ) : ( )}
) } /> ) : ( // ) : ? ( // )}
) } /> ); }