mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-22 09:29:42 +01:00
Better reflect failure UI
This commit is contained in:
@@ -1,27 +1,16 @@
|
||||
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 type { CSSProperties } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useAlert } from '../hooks/useAlert';
|
||||
import { useGrpc } from '../hooks/useGrpc';
|
||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
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 { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { GrpcConnectionMessagesPane } from './GrpcConnectionMessagesPane';
|
||||
import { GrpcConnectionSetupPane } from './GrpcConnectionSetupPane';
|
||||
|
||||
interface Props {
|
||||
style: CSSProperties;
|
||||
@@ -30,62 +19,19 @@ interface Props {
|
||||
export function GrpcConnectionLayout({ style }: Props) {
|
||||
const activeRequest = useActiveRequest('grpc_request');
|
||||
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
|
||||
const alert = useAlert();
|
||||
const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
|
||||
const connections = useGrpcConnections(activeRequest?.id ?? null);
|
||||
const activeConnection = connections[0] ?? null;
|
||||
const messages = useGrpcMessages(activeConnection?.id ?? null);
|
||||
const grpc = useGrpc(activeRequest, activeConnection);
|
||||
|
||||
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 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.streaming.mutateAsync();
|
||||
} else if (!activeMethod.clientStreaming && activeMethod.serverStreaming) {
|
||||
await grpc.serverStreaming.mutateAsync();
|
||||
} else if (activeMethod.clientStreaming && !activeMethod.serverStreaming) {
|
||||
await grpc.clientStreaming.mutateAsync();
|
||||
} else {
|
||||
const msg = await grpc.unary.mutateAsync();
|
||||
setActiveMessageId(msg.id);
|
||||
}
|
||||
},
|
||||
[
|
||||
activeMethod,
|
||||
activeRequest,
|
||||
alert,
|
||||
grpc.streaming,
|
||||
grpc.serverStreaming,
|
||||
grpc.clientStreaming,
|
||||
grpc.unary,
|
||||
],
|
||||
);
|
||||
|
||||
const services = grpc.reflect.data ?? null;
|
||||
useEffect(() => {
|
||||
if (grpc.services == null || activeRequest == null) return;
|
||||
const s = grpc.services.find((s) => s.name === activeRequest.service);
|
||||
if (services == null || activeRequest == null) return;
|
||||
const s = 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,
|
||||
service: services[0]?.name ?? null,
|
||||
method: services[0]?.methods[0]?.name ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -95,61 +41,30 @@ export function GrpcConnectionLayout({ style }: Props) {
|
||||
updateRequest.mutate({ method: s.methods[0]?.name ?? null });
|
||||
return;
|
||||
}
|
||||
}, [activeRequest, grpc.services, updateRequest]);
|
||||
}, [activeRequest, 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 activeMethod = useMemo(() => {
|
||||
if (services == null || activeRequest == null) return null;
|
||||
|
||||
const handleChangeUrl = useCallback(
|
||||
(url: string) => updateRequest.mutateAsync({ url }),
|
||||
[updateRequest],
|
||||
);
|
||||
const s = services.find((s) => s.name === activeRequest.service);
|
||||
if (s == null) return null;
|
||||
return s.methods.find((m) => m.name === activeRequest.method);
|
||||
}, [activeRequest, services]);
|
||||
|
||||
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<HTMLDivElement>(null);
|
||||
useResizeObserver<HTMLDivElement>(urlContainerEl.current, (entry) => {
|
||||
setPaneSize(entry.contentRect.width);
|
||||
});
|
||||
|
||||
const activeMessage = useMemo(
|
||||
() => messages.find((m) => m.id === activeMessageId) ?? null,
|
||||
[activeMessageId, messages],
|
||||
);
|
||||
|
||||
const messageType: 'unary' | 'server_streaming' | 'client_streaming' | 'streaming' =
|
||||
useMemo(() => {
|
||||
if (activeMethod == null) return 'unary'; // Good enough
|
||||
if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming';
|
||||
if (activeMethod.clientStreaming) return 'client_streaming';
|
||||
if (activeMethod.serverStreaming) return 'server_streaming';
|
||||
return 'unary';
|
||||
}, [activeMethod]);
|
||||
const methodType:
|
||||
| 'unary'
|
||||
| 'server_streaming'
|
||||
| 'client_streaming'
|
||||
| 'streaming'
|
||||
| 'no-schema'
|
||||
| 'no-method' = useMemo(() => {
|
||||
if (services == null) return 'no-schema';
|
||||
if (activeMethod == null) return 'no-method';
|
||||
if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming';
|
||||
if (activeMethod.clientStreaming) return 'client_streaming';
|
||||
if (activeMethod.serverStreaming) return 'server_streaming';
|
||||
return 'unary';
|
||||
}, [activeMethod, services]);
|
||||
|
||||
if (activeRequest == null) {
|
||||
return null;
|
||||
@@ -160,110 +75,28 @@ export function GrpcConnectionLayout({ style }: Props) {
|
||||
name="grpc_layout"
|
||||
className="p-3 gap-1.5"
|
||||
style={style}
|
||||
firstSlot={() => (
|
||||
<VStack space={2}>
|
||||
<div
|
||||
ref={urlContainerEl}
|
||||
className={classNames(
|
||||
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5',
|
||||
paneSize < 400 && '!grid-cols-1',
|
||||
)}
|
||||
>
|
||||
<UrlBar
|
||||
url={activeRequest.url ?? ''}
|
||||
method={null}
|
||||
submitIcon={null}
|
||||
forceUpdateKey={activeRequest?.id ?? ''}
|
||||
placeholder="localhost:50051"
|
||||
onSubmit={handleConnect}
|
||||
isLoading={grpc.unary.isLoading}
|
||||
onUrlChange={handleChangeUrl}
|
||||
/>
|
||||
<HStack space={1.5}>
|
||||
<RadioDropdown
|
||||
value={select.value}
|
||||
items={select.options.map((o) => ({
|
||||
label: o.label,
|
||||
value: o.value,
|
||||
type: 'default',
|
||||
shortLabel: o.label,
|
||||
}))}
|
||||
onChange={handleChangeService}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
className={classNames(
|
||||
'border border-highlight font-mono text-xs text-gray-800',
|
||||
paneSize < 400 && 'flex-1',
|
||||
)}
|
||||
rightSlot={<Icon className="text-gray-600" size="sm" icon="chevronDown" />}
|
||||
>
|
||||
{select.options.find((o) => o.value === select.value)?.label}
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
{!grpc.isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title={messageType === 'unary' ? 'Send' : 'Connect'}
|
||||
hotkeyAction={grpc.isStreaming ? undefined : 'http_request.send'}
|
||||
onClick={handleConnect}
|
||||
icon={
|
||||
grpc.isStreaming
|
||||
? 'refresh'
|
||||
: messageType === 'unary'
|
||||
? 'sendHorizontal'
|
||||
: 'arrowUpDown'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{grpc.isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="Cancel"
|
||||
onClick={() => grpc.cancel.mutateAsync()}
|
||||
icon="x"
|
||||
disabled={!grpc.isStreaming}
|
||||
/>
|
||||
)}
|
||||
{activeMethod?.clientStreaming &&
|
||||
!activeMethod.serverStreaming &&
|
||||
grpc.isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
onClick={() => grpc.commit.mutateAsync()}
|
||||
icon="check"
|
||||
/>
|
||||
)}
|
||||
{activeMethod?.clientStreaming && grpc.isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={() => grpc.send.mutateAsync({ message: activeRequest.message ?? '' })}
|
||||
icon="sendHorizontal"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
<GrpcEditor
|
||||
forceUpdateKey={activeRequest?.id ?? ''}
|
||||
url={activeRequest.url ?? ''}
|
||||
defaultValue={activeRequest.message}
|
||||
onChange={handleChangeMessage}
|
||||
service={activeRequest.service}
|
||||
method={activeRequest.method}
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</VStack>
|
||||
firstSlot={({ style }) => (
|
||||
<GrpcConnectionSetupPane
|
||||
style={style}
|
||||
activeRequest={activeRequest}
|
||||
methodType={methodType}
|
||||
onUnary={grpc.unary.mutate}
|
||||
onServerStreaming={grpc.serverStreaming.mutate}
|
||||
onClientStreaming={grpc.clientStreaming.mutate}
|
||||
onStreaming={grpc.streaming.mutate}
|
||||
onCommit={grpc.commit.mutate}
|
||||
onCancel={grpc.cancel.mutate}
|
||||
onSend={grpc.send.mutate}
|
||||
onReflectRefetch={grpc.reflect.refetch}
|
||||
services={services ?? null}
|
||||
reflectionError={grpc.reflect.error as string | undefined}
|
||||
reflectionLoading={grpc.reflect.isLoading}
|
||||
/>
|
||||
)}
|
||||
secondSlot={() =>
|
||||
secondSlot={({ style }) =>
|
||||
!grpc.unary.isLoading && (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
|
||||
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
|
||||
@@ -275,102 +108,8 @@ export function GrpcConnectionLayout({ style }: Props) {
|
||||
{grpc.unary.error}
|
||||
</Banner>
|
||||
) : messages.length >= 0 ? (
|
||||
<SplitLayout
|
||||
forceVertical
|
||||
name={
|
||||
!activeMethod?.clientStreaming && !activeMethod?.serverStreaming
|
||||
? 'grpc_messages_unary'
|
||||
: 'grpc_messages_streaming'
|
||||
}
|
||||
defaultRatio={
|
||||
!activeMethod?.clientStreaming && !activeMethod?.serverStreaming ? 0.75 : 0.3
|
||||
}
|
||||
minHeightPx={20}
|
||||
firstSlot={() => (
|
||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
|
||||
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
|
||||
<HStack alignItems="center" space={2}>
|
||||
<span>{messages.filter((m) => !m.isInfo).length} messages</span>
|
||||
{grpc.isStreaming && (
|
||||
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
|
||||
)}
|
||||
</HStack>
|
||||
{activeConnection && (
|
||||
<RecentConnectionsDropdown
|
||||
connections={connections}
|
||||
activeConnection={activeConnection}
|
||||
onPinned={() => {
|
||||
// todo
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<div className="overflow-y-auto h-full">
|
||||
{...messages.map((m) => (
|
||||
<HStack
|
||||
key={m.id}
|
||||
space={2}
|
||||
onClick={() => {
|
||||
if (m.id === activeMessageId) setActiveMessageId(null);
|
||||
else setActiveMessageId(m.id);
|
||||
}}
|
||||
alignItems="center"
|
||||
className={classNames(
|
||||
'px-2 py-1 font-mono',
|
||||
m === activeMessage && 'bg-highlight',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={
|
||||
m.isInfo
|
||||
? 'text-gray-600'
|
||||
: m.isServer
|
||||
? 'text-blue-600'
|
||||
: 'text-green-600'
|
||||
}
|
||||
icon={
|
||||
m.isInfo ? 'info' : m.isServer ? 'arrowBigDownDash' : 'arrowBigUpDash'
|
||||
}
|
||||
/>
|
||||
<div className="w-full truncate text-gray-800 text-2xs">{m.message}</div>
|
||||
<div className="text-gray-600 text-2xs">
|
||||
{format(m.createdAt, 'HH:mm:ss')}
|
||||
</div>
|
||||
</HStack>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
secondSlot={
|
||||
!activeMessage
|
||||
? null
|
||||
: () => (
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="pb-3 px-2">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="pl-2 overflow-y-auto">
|
||||
{activeMessage.isInfo ? (
|
||||
<span>{activeMessage.message}</span>
|
||||
) : (
|
||||
<JsonAttributeTree
|
||||
attrValue={JSON.parse(activeMessage?.message ?? '{}')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<GrpcConnectionMessagesPane activeRequest={activeRequest} methodType={methodType} />
|
||||
) : (
|
||||
// ) : ? (
|
||||
// <Editor
|
||||
// readOnly
|
||||
// className="bg-gray-50 dark:bg-gray-100"
|
||||
// contentType="application/json"
|
||||
// defaultValue={resp.message}
|
||||
// forceUpdateKey={resp.id}
|
||||
// />
|
||||
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user