import type { GrpcEvent, GrpcRequest } from "@yaakapp-internal/models"; import { useAtomValue, useSetAtom } from "jotai"; import type { CSSProperties } from "react"; import { useEffect, useMemo, useState } from "react"; import { activeGrpcConnectionAtom, activeGrpcConnections, pinnedGrpcConnectionIdAtom, useGrpcEvents, } from "../hooks/usePinnedGrpcConnection"; import { useStateWithDeps } from "../hooks/useStateWithDeps"; import { Button } from "./core/Button"; import { Editor } from "./core/Editor/LazyEditor"; import { EventDetailHeader, EventViewer } from "./core/EventViewer"; import { EventViewerRow } from "./core/EventViewerRow"; import { HotkeyList } from "./core/HotkeyList"; import { Icon, type IconProps } from "./core/Icon"; import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow"; import { LoadingIcon } from "./core/LoadingIcon"; import { HStack, VStack } from "./core/Stacks"; import { EmptyStateText } from "./EmptyStateText"; import { ErrorBoundary } from "./ErrorBoundary"; import { RecentGrpcConnectionsDropdown } from "./RecentGrpcConnectionsDropdown"; interface Props { style?: CSSProperties; className?: string; activeRequest: GrpcRequest; methodType: | "unary" | "client_streaming" | "server_streaming" | "streaming" | "no-schema" | "no-method"; } export function GrpcResponsePane({ style, methodType, activeRequest }: Props) { const [activeEventIndex, setActiveEventIndex] = useState(null); const [showLarge, setShowLarge] = useStateWithDeps(false, [activeRequest.id]); const [showingLarge, setShowingLarge] = useState(false); const connections = useAtomValue(activeGrpcConnections); const activeConnection = useAtomValue(activeGrpcConnectionAtom); const events = useGrpcEvents(activeConnection?.id ?? null); const setPinnedGrpcConnectionId = useSetAtom(pinnedGrpcConnectionIdAtom); const activeEvent = useMemo( () => (activeEventIndex != null ? events[activeEventIndex] : null), [activeEventIndex, events], ); // Set the active message to the first message received if unary // oxlint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (events.length === 0 || activeEvent != null || methodType !== "unary") { return; } const firstServerMessageIndex = events.findIndex((m) => m.eventType === "server_message"); if (firstServerMessageIndex !== -1) { setActiveEventIndex(firstServerMessageIndex); } }, [events.length]); if (activeConnection == null) { return ( ); } const header = ( {events.length} Messages {activeConnection.state !== "closed" && ( )}
); return (
event.id} error={activeConnection.error} header={header} splitLayoutName="grpc_events" defaultRatio={0.4} renderRow={({ event, isActive, onClick }) => ( )} renderDetail={({ event, onClose }) => ( )} />
); } function GrpcEventRow({ event, isActive, onClick, }: { event: GrpcEvent; isActive: boolean; onClick: () => void; }) { const { eventType, status, content, error } = event; const display = getEventDisplay(eventType, status); return ( } content={ {content.slice(0, 1000)} {error && ({error})} } timestamp={event.createdAt} /> ); } function GrpcEventDetail({ event, showLarge, showingLarge, setShowLarge, setShowingLarge, onClose, }: { event: GrpcEvent; showLarge: boolean; showingLarge: boolean; setShowLarge: (v: boolean) => void; setShowingLarge: (v: boolean) => void; onClose: () => void; }) { if (event.eventType === "client_message" || event.eventType === "server_message") { const title = `Message ${event.eventType === "client_message" ? "Sent" : "Received"}`; return (
{!showLarge && event.content.length > 1000 * 1000 ? ( Message previews larger than 1MB are hidden
) : ( )}
); } // Error or connection_end - show metadata/trailers return (
{event.error && (
{event.error}
)}
{Object.keys(event.metadata).length === 0 ? ( No {event.eventType === "connection_end" ? "trailers" : "metadata"} ) : ( {Object.entries(event.metadata).map(([key, value]) => ( {value} ))} )}
); } function getEventDisplay( eventType: GrpcEvent["eventType"], status: GrpcEvent["status"], ): { icon: IconProps["icon"]; color: IconProps["color"]; title: string } { if (eventType === "server_message") { return { icon: "arrow_big_down_dash", color: "info", title: "Server message" }; } if (eventType === "client_message") { return { icon: "arrow_big_up_dash", color: "primary", title: "Client message" }; } if (eventType === "error" || (status != null && status > 0)) { return { icon: "alert_triangle", color: "danger", title: "Error" }; } if (eventType === "connection_end") { return { icon: "check", color: "success", title: "Connection response" }; } return { icon: "info", color: undefined, title: "Event" }; }