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 { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useAlert } from '../hooks/useAlert'; import type { GrpcMessage } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc'; import { useKeyValue } from '../hooks/useKeyValue'; 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 activeRequestId = useActiveRequestId(); const url = useKeyValue({ namespace: 'debug', key: ['grpc_url', activeRequestId ?? ''], defaultValue: '', }); const alert = useAlert(); const service = useKeyValue({ namespace: 'debug', key: ['grpc_service', activeRequestId ?? ''], defaultValue: null, }); const method = useKeyValue({ namespace: 'debug', key: ['grpc_method', activeRequestId ?? ''], defaultValue: null, }); const message = useKeyValue({ namespace: 'debug', key: ['grpc_message', activeRequestId ?? ''], defaultValue: '', }); const [activeMessage, setActiveMessage] = useState(null); const [resp, setResp] = useState(''); const grpc = useGrpc(url.value ?? null, activeRequestId); const activeMethod = useMemo(() => { if (grpc.schema == null) return null; const s = grpc.schema.find((s) => s.name === service.value); if (s == null) return null; return s.methods.find((m) => m.name === method.value); }, [grpc.schema, method.value, service.value]); const handleCancel = useCallback(() => { grpc.cancel.mutateAsync().catch(console.error); }, [grpc.cancel]); const handleConnect = useCallback( async (e: FormEvent) => { e.preventDefault(); if (activeMethod == null) return; if (service.value == null || method.value == null) { alert({ id: 'grpc-invalid-service-method', title: 'Error', body: 'Service or method not selected', }); } if (activeMethod.clientStreaming && activeMethod.serverStreaming) { await grpc.bidiStreaming.mutateAsync({ service: service.value ?? 'n/a', method: method.value ?? 'n/a', message: message.value ?? '', }); } else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) { await grpc.serverStreaming.mutateAsync({ service: service.value ?? 'n/a', method: method.value ?? 'n/a', message: message.value ?? '', }); } else { setResp( await grpc.unary.mutateAsync({ service: service.value ?? 'n/a', method: method.value ?? 'n/a', message: message.value ?? '', }), ); } }, [ activeMethod, alert, grpc.bidiStreaming, grpc.serverStreaming, grpc.unary, message.value, method.value, service.value, ], ); useEffect(() => { console.log('GrpcConnectionLayout'); if (grpc.schema == null) return; const s = grpc.schema.find((s) => s.name === service.value); if (s == null) { service.set(grpc.schema[0]?.name ?? null); method.set(grpc.schema[0]?.methods[0]?.name ?? null); return; } const m = s.methods.find((m) => m.name === method.value); if (m == null) { method.set(s.methods[0]?.name ?? null); return; } }, [grpc.schema, method, service]); const handleChangeService = useCallback( (v: string) => { const [serviceName, methodName] = v.split('/', 2); if (serviceName == null || methodName == null) throw new Error('Should never happen'); method.set(methodName); service.set(serviceName); }, [method, service], ); const select = useMemo(() => { const options = grpc.schema?.flatMap((s) => s.methods.map((m) => ({ label: `${s.name.split('.', 2).pop() ?? s.name}/${m.name}`, value: `${s.name}/${m.name}`, })), ) ?? []; const value = `${service.value ?? ''}/${method.value ?? ''}`; return { value, options }; }, [grpc.schema, method.value, service.value]); const [paneSize, setPaneSize] = useState(99999); const urlContainerEl = useRef(null); useResizeObserver(urlContainerEl.current, (entry) => { setPaneSize(entry.contentRect.width); }); if (url.isLoading || url.value == null) { return null; } return ( (
({ label: o.label, value: o.value, type: 'default', shortLabel: o.label, }))} onChange={handleChangeService} > {activeMethod?.clientStreaming && grpc.isStreaming && ( grpc.send.mutateAsync({ message: message.value ?? '' })} icon="sendHorizontal" /> )}
{!service.isLoading && !method.isLoading && ( )}
)} 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 ? ( ) : ( )}
) } /> ); }