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 type { GrpcMessage } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { Editor } from './core/Editor'; 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 [activeMessage, setActiveMessage] = useState(null); const [resp, setResp] = useState(''); const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.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 handleCancel = useCallback(() => { grpc.cancel.mutateAsync().catch(console.error); }, [grpc.cancel]); 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.bidiStreaming.mutateAsync(activeRequest); } else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) { await grpc.serverStreaming.mutateAsync(activeRequest); } else { setResp(await grpc.unary.mutateAsync(activeRequest)); } }, [activeMethod, activeRequest, alert, grpc.bidiStreaming, grpc.serverStreaming, 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); }); if (activeRequest == null) { return; } return ( (
({ label: o.label, value: o.value, type: 'default', shortLabel: o.label, }))} onChange={handleChangeService} > {activeMethod?.clientStreaming && grpc.isStreaming && ( grpc.send.mutateAsync({ message: activeRequest.message ?? '' })} icon="sendHorizontal" /> )}
)} rightSlot={() => !grpc.unary.isLoading && (
{grpc.unary.error ? ( {grpc.unary.error} ) : (grpc.messages.value ?? []).length > 0 ? ( (
{...(grpc.messages.value ?? []).map((m, i) => ( { if (m === activeMessage) setActiveMessage(null); else setActiveMessage(m); }} alignItems="center" className={classNames( 'px-2 py-1 font-mono', m === activeMessage && 'bg-highlight', )} >
{m.message}
{format(m.timestamp, 'HH:mm:ss')}
))}
)} rightSlot={ !activeMessage ? null : () => (
) } /> ) : resp ? ( ) : ( )}
) } /> ); }