import classNames from 'classnames'; import { format } from 'date-fns'; import { m } from 'framer-motion'; import type { CSSProperties, FormEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useAlert } from '../hooks/useAlert'; import type { GrpcMessage } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc'; import { useKeyValue } from '../hooks/useKeyValue'; import { tryFormatJson } from '../lib/formatters'; import { Banner } from './core/Banner'; import { Editor } from './core/Editor'; import { HotKeyList } from './core/HotKeyList'; import { Icon } from './core/Icon'; import { JsonAttributeTree } from './core/JsonAttributeTree'; import { Select } from './core/Select'; 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 url = useKeyValue({ namespace: 'debug', key: 'grpc_url', defaultValue: '' }); const alert = useAlert(); const service = useKeyValue({ namespace: 'debug', key: 'grpc_service', defaultValue: null, }); const method = useKeyValue({ namespace: 'debug', key: 'grpc_method', defaultValue: null, }); const message = useKeyValue({ namespace: 'debug', key: 'grpc_message', defaultValue: '', }); const [activeMessage, setActiveMessage] = useState(null); const [resp, setResp] = useState(''); const grpc = useGrpc(url.value ?? null); 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 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.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.serverStreaming, grpc.unary, message.value, method.value, service.value, ], ); useEffect(() => { 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]); if (url.isLoading || url.value == null) { return null; } return ( (