A bit better handling of responses

This commit is contained in:
Gregory Schier
2024-02-02 13:32:06 -08:00
parent d8948bb061
commit 6c69fff27d
7 changed files with 168 additions and 120 deletions

View File

@@ -189,25 +189,29 @@ async fn cmd_grpc_bidi_streaming(
} }
} }
}; };
let event_handler = app_handle.listen_global("grpc_message_in", cb); let event_handler =
app_handle.listen_global(format!("grpc_client_msg_{}", conn_id).as_str(), cb);
let app_handle2 = app_handle.clone(); let grpc_listen = {
let grpc_listen = async move { let app_handle = app_handle.clone();
loop { let conn_id = conn_id.clone();
match stream.next().await { async move {
Some(Ok(item)) => { loop {
let item = serde_json::to_string_pretty(&item).unwrap(); match stream.next().await {
app_handle2 Some(Ok(item)) => {
.emit_all("grpc_message", item) let item = serde_json::to_string_pretty(&item).unwrap();
.expect("Failed to emit"); app_handle
} .emit_all(format!("grpc_server_msg_{}", &conn_id).as_str(), item)
Some(Err(e)) => { .expect("Failed to emit");
error!("gRPC stream error: {:?}", e); }
// TODO: Handle error Some(Err(e)) => {
} error!("gRPC stream error: {:?}", e);
None => { // TODO: Handle error
info!("gRPC stream closed by sender"); }
break; None => {
info!("gRPC stream closed by sender");
break;
}
} }
} }
} }
@@ -283,25 +287,29 @@ async fn cmd_grpc_server_streaming(
} }
} }
}; };
let event_handler = app_handle.listen_global("grpc_message_in", cb); let event_handler =
app_handle.listen_global(format!("grpc_client_msg_{}", conn_id).as_str(), cb);
let app_handle2 = app_handle.clone(); let grpc_listen = {
let grpc_listen = async move { let app_handle = app_handle.clone();
loop { let conn_id = conn_id.clone();
match stream.next().await { async move {
Some(Ok(item)) => { loop {
let item = serde_json::to_string_pretty(&item).unwrap(); match stream.next().await {
app_handle2 Some(Ok(item)) => {
.emit_all("grpc_message", item) let item = serde_json::to_string_pretty(&item).unwrap();
.expect("Failed to emit"); app_handle
} .emit_all(format!("grpc_server_msg_{}", &conn_id).as_str(), item)
Some(Err(e)) => { .expect("Failed to emit");
error!("gRPC stream error: {:?}", e); }
// TODO: Handle error Some(Err(e)) => {
} error!("gRPC stream error: {:?}", e);
None => { // TODO: Handle error
info!("gRPC stream closed by sender"); }
break; None => {
info!("gRPC stream closed by sender");
break;
}
} }
} }
} }

View File

@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { format } from 'date-fns'; import { format } from 'date-fns';
import type { CSSProperties, FormEvent } from 'react'; import type { CSSProperties, FormEvent } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useAlert } from '../hooks/useAlert'; import { useAlert } from '../hooks/useAlert';
import type { GrpcMessage } from '../hooks/useGrpc'; import type { GrpcMessage } from '../hooks/useGrpc';
import { useGrpc } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc';
@@ -26,26 +27,31 @@ interface Props {
} }
export function GrpcConnectionLayout({ style }: Props) { export function GrpcConnectionLayout({ style }: Props) {
const url = useKeyValue<string>({ namespace: 'debug', key: 'grpc_url', defaultValue: '' }); const activeRequestId = useActiveRequestId();
const url = useKeyValue<string>({
namespace: 'debug',
key: ['grpc_url', activeRequestId ?? ''],
defaultValue: '',
});
const alert = useAlert(); const alert = useAlert();
const service = useKeyValue<string | null>({ const service = useKeyValue<string | null>({
namespace: 'debug', namespace: 'debug',
key: 'grpc_service', key: ['grpc_service', activeRequestId ?? ''],
defaultValue: null, defaultValue: null,
}); });
const method = useKeyValue<string | null>({ const method = useKeyValue<string | null>({
namespace: 'debug', namespace: 'debug',
key: 'grpc_method', key: ['grpc_method', activeRequestId ?? ''],
defaultValue: null, defaultValue: null,
}); });
const message = useKeyValue<string>({ const message = useKeyValue<string>({
namespace: 'debug', namespace: 'debug',
key: 'grpc_message', key: ['grpc_message', activeRequestId ?? ''],
defaultValue: '', defaultValue: '',
}); });
const [activeMessage, setActiveMessage] = useState<GrpcMessage | null>(null); const [activeMessage, setActiveMessage] = useState<GrpcMessage | null>(null);
const [resp, setResp] = useState<string>(''); const [resp, setResp] = useState<string>('');
const grpc = useGrpc(url.value ?? null); const grpc = useGrpc(url.value ?? null, activeRequestId);
const activeMethod = useMemo(() => { const activeMethod = useMemo(() => {
if (grpc.schema == null) return null; if (grpc.schema == null) return null;
@@ -105,6 +111,7 @@ export function GrpcConnectionLayout({ style }: Props) {
); );
useEffect(() => { useEffect(() => {
console.log('GrpcConnectionLayout');
if (grpc.schema == null) return; if (grpc.schema == null) return;
const s = grpc.schema.find((s) => s.name === service.value); const s = grpc.schema.find((s) => s.name === service.value);
if (s == null) { if (s == null) {
@@ -167,11 +174,10 @@ export function GrpcConnectionLayout({ style }: Props) {
)} )}
> >
<UrlBar <UrlBar
id="foo"
url={url.value ?? ''} url={url.value ?? ''}
method={null} method={null}
submitIcon={null} submitIcon={null}
forceUpdateKey="to-do" forceUpdateKey={activeRequestId ?? ''}
placeholder="localhost:50051" placeholder="localhost:50051"
onSubmit={handleConnect} onSubmit={handleConnect}
isLoading={grpc.unary.isLoading} isLoading={grpc.unary.isLoading}
@@ -231,7 +237,7 @@ export function GrpcConnectionLayout({ style }: Props) {
</div> </div>
{!service.isLoading && !method.isLoading && ( {!service.isLoading && !method.isLoading && (
<GrpcEditor <GrpcEditor
forceUpdateKey={[service, method].join('::')} forceUpdateKey={activeRequestId ?? ''}
url={url.value ?? ''} url={url.value ?? ''}
defaultValue={message.value} defaultValue={message.value}
onChange={message.set} onChange={message.set}
@@ -255,16 +261,16 @@ export function GrpcConnectionLayout({ style }: Props) {
<Banner color="danger" className="m-2"> <Banner color="danger" className="m-2">
{grpc.unary.error} {grpc.unary.error}
</Banner> </Banner>
) : grpc.messages.length > 0 ? ( ) : (grpc.messages.value ?? []).length > 0 ? (
<SplitLayout <SplitLayout
name="grpc_messages2" name="grpc_messages2"
minHeightPx={20} minHeightPx={20}
defaultRatio={0.25} defaultRatio={0.25}
leftSlot={() => ( leftSlot={() => (
<div className="overflow-y-auto"> <div className="overflow-y-auto">
{...grpc.messages.map((m, i) => ( {...(grpc.messages.value ?? []).map((m, i) => (
<HStack <HStack
key={`${m.time.getTime()}::${m.message}::${i}`} key={`${m.timestamp}::${m.message}::${i}`}
space={2} space={2}
onClick={() => { onClick={() => {
if (m === activeMessage) setActiveMessage(null); if (m === activeMessage) setActiveMessage(null);
@@ -292,25 +298,29 @@ export function GrpcConnectionLayout({ style }: Props) {
: 'info' : 'info'
} }
/> />
<div className="w-full truncate text-gray-800 text-xs">{m.message}</div> <div className="w-full truncate text-gray-800 text-2xs">{m.message}</div>
<div className="text-gray-600 text-2xs" title={m.time.toISOString()}> <div className="text-gray-600 text-2xs">
{format(m.time, 'HH:mm:ss')} {format(m.timestamp, 'HH:mm:ss')}
</div> </div>
</HStack> </HStack>
))} ))}
</div> </div>
)} )}
rightSlot={() => rightSlot={
activeMessage && ( !activeMessage
<div className="grid grid-rows-[auto_minmax(0,1fr)]"> ? null
<div className="pb-3 px-2"> : () => (
<Separator /> <div className="grid grid-rows-[auto_minmax(0,1fr)]">
</div> <div className="pb-3 px-2">
<div className="pl-2 overflow-y-auto"> <Separator />
<JsonAttributeTree attrValue={JSON.parse(activeMessage?.message ?? '{}')} /> </div>
</div> <div className="pl-2 overflow-y-auto">
</div> <JsonAttributeTree
) attrValue={JSON.parse(activeMessage?.message ?? '{}')}
/>
</div>
</div>
)
} }
/> />
) : resp ? ( ) : resp ? (

View File

@@ -209,8 +209,6 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
{activeRequest && ( {activeRequest && (
<> <>
<UrlBar <UrlBar
key={activeRequest.id} // Force-reset the url bar when the active request changes
id={activeRequest.id}
url={activeRequest.url} url={activeRequest.url}
method={activeRequest.method} method={activeRequest.method}
placeholder="https://example.com" placeholder="https://example.com"

View File

@@ -8,7 +8,7 @@ import { IconButton } from './core/IconButton';
import { Input } from './core/Input'; import { Input } from './core/Input';
import { RequestMethodDropdown } from './RequestMethodDropdown'; import { RequestMethodDropdown } from './RequestMethodDropdown';
type Props = Pick<HttpRequest, 'id' | 'url'> & { type Props = Pick<HttpRequest, 'url'> & {
className?: string; className?: string;
method: HttpRequest['method'] | null; method: HttpRequest['method'] | null;
placeholder: string; placeholder: string;

View File

@@ -17,7 +17,7 @@ interface SlotProps {
interface Props { interface Props {
name: string; name: string;
leftSlot: (props: SlotProps) => ReactNode; leftSlot: (props: SlotProps) => ReactNode;
rightSlot: (props: SlotProps) => ReactNode; rightSlot: null | ((props: SlotProps) => ReactNode);
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
defaultRatio?: number; defaultRatio?: number;
@@ -48,33 +48,37 @@ export function SplitLayout({
`${name}_height::${useActiveWorkspaceId()}`, `${name}_height::${useActiveWorkspaceId()}`,
); );
const width = widthRaw ?? defaultRatio; const width = widthRaw ?? defaultRatio;
const height = heightRaw ?? defaultRatio; let height = heightRaw ?? defaultRatio;
const [isResizing, setIsResizing] = useState<boolean>(false); const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null, null,
); );
if (!rightSlot) {
height = 0;
minHeightPx = 0;
}
useResizeObserver(containerRef.current, ({ contentRect }) => { useResizeObserver(containerRef.current, ({ contentRect }) => {
setVertical(contentRect.width < STACK_VERTICAL_WIDTH); setVertical(contentRect.width < STACK_VERTICAL_WIDTH);
}); });
const styles = useMemo<CSSProperties>( const styles = useMemo<CSSProperties>(() => {
() => ({ return {
...style, ...style,
gridTemplate: vertical gridTemplate: vertical
? ` ? `
' ${areaL.gridArea}' minmax(0,${1 - height}fr) ' ${areaL.gridArea}' minmax(0,${1 - height}fr)
' ${areaD.gridArea}' 0 ' ${areaD.gridArea}' 0
' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr) ' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr)
/ 1fr / 1fr
` `
: ` : `
' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr) ' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr)
/ ${1 - width}fr 0 ${width}fr / ${1 - width}fr 0 ${width}fr
`, `,
}), };
[vertical, width, height, style], }, [style, vertical, height, minHeightPx, width]);
);
const unsub = () => { const unsub = () => {
if (moveState.current !== null) { if (moveState.current !== null) {
@@ -142,17 +146,21 @@ export function SplitLayout({
return ( return (
<div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}> <div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}>
{leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} {leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
<ResizeHandle {rightSlot && (
style={areaD} <>
isResizing={isResizing} <ResizeHandle
barClassName={'bg-red-300'} style={areaD}
className={classNames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')} isResizing={isResizing}
onResizeStart={handleResizeStart} barClassName={'bg-red-300'}
onReset={handleReset} className={classNames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')}
side={vertical ? 'top' : 'left'} onResizeStart={handleResizeStart}
justify="center" onReset={handleReset}
/> side={vertical ? 'top' : 'left'}
{rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })} justify="center"
/>
{rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
</>
)}
</div> </div>
); );
} }

View File

@@ -1,8 +1,9 @@
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { emit } from '@tauri-apps/api/event'; import type { UnlistenFn } from '@tauri-apps/api/event';
import { useState } from 'react'; import { emit, listen } from '@tauri-apps/api/event';
import { useListenToTauriEvent } from './useListenToTauriEvent'; import { useEffect, useRef, useState } from 'react';
import { useKeyValue } from './useKeyValue';
interface ReflectResponseService { interface ReflectResponseService {
name: string; name: string;
@@ -11,24 +12,23 @@ interface ReflectResponseService {
export interface GrpcMessage { export interface GrpcMessage {
message: string; message: string;
time: Date; timestamp: string;
type: 'server' | 'client' | 'info'; type: 'server' | 'client' | 'info';
} }
export function useGrpc(url: string | null) { export function useGrpc(url: string | null, requestId: string | null) {
const [messages, setMessages] = useState<GrpcMessage[]>([]); const messages = useKeyValue<GrpcMessage[]>({
namespace: 'debug',
key: ['grpc_msgs', requestId ?? 'n/a'],
defaultValue: [],
});
const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null); const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null);
const unlisten = useRef<UnlistenFn | null>(null);
useListenToTauriEvent<string>( useEffect(() => {
'grpc_message', setActiveConnectionId(null);
(event) => { unlisten.current?.();
setMessages((prev) => [ }, [requestId]);
...prev,
{ message: event.payload, time: new Date(), type: 'server' },
]);
},
[setMessages],
);
const unary = useMutation<string, string, { service: string; method: string; message: string }>({ const unary = useMutation<string, string, { service: string; method: string; message: string }>({
mutationKey: ['grpc_unary', url], mutationKey: ['grpc_unary', url],
@@ -51,8 +51,12 @@ export function useGrpc(url: string | null) {
mutationKey: ['grpc_server_streaming', url], mutationKey: ['grpc_server_streaming', url],
mutationFn: async ({ service, method, message }) => { mutationFn: async ({ service, method, message }) => {
if (url === null) throw new Error('No URL provided'); if (url === null) throw new Error('No URL provided');
setMessages([ await messages.set([
{ type: 'client', message: JSON.stringify(JSON.parse(message)), time: new Date() }, {
type: 'client',
message: JSON.stringify(JSON.parse(message)),
timestamp: new Date().toISOString(),
},
]); ]);
const id: string = await invoke('cmd_grpc_server_streaming', { const id: string = await invoke('cmd_grpc_server_streaming', {
endpoint: url, endpoint: url,
@@ -60,6 +64,12 @@ export function useGrpc(url: string | null) {
method, method,
message, message,
}); });
unlisten.current = await listen(`grpc_server_msg_${id}`, async (event) => {
await messages.set((prev) => [
...prev,
{ message: event.payload as string, timestamp: new Date().toISOString(), type: 'server' },
]);
});
setActiveConnectionId(id); setActiveConnectionId(id);
}, },
}); });
@@ -78,16 +88,27 @@ export function useGrpc(url: string | null) {
method, method,
message, message,
}); });
setMessages([{ type: 'info', message: `Started connection ${id}`, time: new Date() }]); messages.set([
{ type: 'info', message: `Started connection ${id}`, timestamp: new Date().toISOString() },
]);
setActiveConnectionId(id); setActiveConnectionId(id);
unlisten.current = await listen(`grpc_server_msg_${id}`, (event) => {
messages.set((prev) => [
...prev,
{ message: event.payload as string, timestamp: new Date().toISOString(), type: 'server' },
]);
});
}, },
}); });
const send = useMutation({ const send = useMutation({
mutationKey: ['grpc_send', url], mutationKey: ['grpc_send', url],
mutationFn: async ({ message }: { message: string }) => { mutationFn: async ({ message }: { message: string }) => {
await emit('grpc_message_in', { Message: message }); if (activeConnectionId == null) throw new Error('No active connection');
setMessages((m) => [...m, { type: 'client', message, time: new Date() }]); await messages.set((m) => {
return [...m, { type: 'client', message, timestamp: new Date().toISOString() }];
});
await emit(`grpc_client_msg_${activeConnectionId}`, { Message: message });
}, },
}); });
@@ -95,10 +116,11 @@ export function useGrpc(url: string | null) {
mutationKey: ['grpc_cancel', url], mutationKey: ['grpc_cancel', url],
mutationFn: async () => { mutationFn: async () => {
setActiveConnectionId(null); setActiveConnectionId(null);
unlisten.current?.();
await emit('grpc_message_in', 'Cancel'); await emit('grpc_message_in', 'Cancel');
setMessages((m) => [ await messages.set((m) => [
...m, ...m,
{ type: 'info', message: 'Cancelled by client', time: new Date() }, { type: 'info', message: 'Cancelled by client', timestamp: new Date().toISOString() },
]); ]);
}, },
}); });

View File

@@ -37,19 +37,21 @@ export function useKeyValue<T extends Object | null>({
}); });
const set = useCallback( const set = useCallback(
(value: ((v: T) => T) | T) => { async (value: ((v: T) => T) | T) => {
if (typeof value === 'function') { if (typeof value === 'function') {
getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => { await getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => {
mutate.mutate(value(kv)); const newV = value(kv);
if (newV === kv) return;
return mutate.mutateAsync(newV);
}); });
} else { } else if (value !== query.data) {
mutate.mutate(value); await mutate.mutateAsync(value);
} }
}, },
[defaultValue, key, mutate, namespace], [defaultValue, key, mutate, namespace, query.data],
); );
const reset = useCallback(() => mutate.mutate(defaultValue), [mutate, defaultValue]); const reset = useCallback(async () => mutate.mutateAsync(defaultValue), [mutate, defaultValue]);
return useMemo( return useMemo(
() => ({ () => ({