mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-16 22:16:49 +01:00
Better reflect failure UI
This commit is contained in:
@@ -29,10 +29,11 @@ pub struct MethodDefinition {
|
||||
pub server_streaming: bool,
|
||||
}
|
||||
|
||||
pub async fn reflect(uri: &Uri) -> Vec<ServiceDefinition> {
|
||||
let (pool, _) = fill_pool(uri).await;
|
||||
pub async fn reflect(uri: &Uri) -> Result<Vec<ServiceDefinition>, String> {
|
||||
let (pool, _) = fill_pool(uri).await?;
|
||||
|
||||
pool.services()
|
||||
Ok(pool
|
||||
.services()
|
||||
.map(|s| {
|
||||
let mut def = ServiceDefinition {
|
||||
name: s.full_name().to_string(),
|
||||
@@ -53,5 +54,5 @@ pub async fn reflect(uri: &Uri) -> Vec<ServiceDefinition> {
|
||||
}
|
||||
def
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ impl GrpcManager {
|
||||
message: &str,
|
||||
) -> Result<Streaming<DynamicMessage>> {
|
||||
self.connect(id, uri)
|
||||
.await
|
||||
.await?
|
||||
.server_streaming(service, method, message)
|
||||
.await
|
||||
}
|
||||
@@ -187,7 +187,7 @@ impl GrpcManager {
|
||||
stream: ReceiverStream<String>,
|
||||
) -> Result<DynamicMessage> {
|
||||
self.connect(id, uri)
|
||||
.await
|
||||
.await?
|
||||
.client_streaming(service, method, stream)
|
||||
.await
|
||||
}
|
||||
@@ -201,15 +201,15 @@ impl GrpcManager {
|
||||
stream: ReceiverStream<String>,
|
||||
) -> Result<Streaming<DynamicMessage>> {
|
||||
self.connect(id, uri)
|
||||
.await
|
||||
.await?
|
||||
.streaming(service, method, stream)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self, id: &str, uri: Uri) -> GrpcConnection {
|
||||
let (pool, conn) = fill_pool(&uri).await;
|
||||
pub async fn connect(&mut self, id: &str, uri: Uri) -> Result<GrpcConnection> {
|
||||
let (pool, conn) = fill_pool(&uri).await?;
|
||||
let connection = GrpcConnection { pool, conn, uri };
|
||||
self.connections.insert(id.to_string(), connection.clone());
|
||||
connection
|
||||
Ok(connection)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use anyhow::anyhow;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::Client;
|
||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||
use log::warn;
|
||||
use prost::Message;
|
||||
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
||||
use prost_types::FileDescriptorProto;
|
||||
@@ -12,6 +13,7 @@ use tokio_stream::StreamExt;
|
||||
use tonic::body::BoxBody;
|
||||
use tonic::codegen::http::uri::PathAndQuery;
|
||||
use tonic::transport::Uri;
|
||||
use tonic::Code::Unimplemented;
|
||||
use tonic::Request;
|
||||
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
|
||||
use tonic_reflection::pb::server_reflection_request::MessageRequest;
|
||||
@@ -20,10 +22,13 @@ use tonic_reflection::pb::ServerReflectionRequest;
|
||||
|
||||
pub async fn fill_pool(
|
||||
uri: &Uri,
|
||||
) -> (
|
||||
DescriptorPool,
|
||||
Client<HttpsConnector<HttpConnector>, BoxBody>,
|
||||
) {
|
||||
) -> Result<
|
||||
(
|
||||
DescriptorPool,
|
||||
Client<HttpsConnector<HttpConnector>, BoxBody>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
let connector = HttpsConnectorBuilder::new().with_native_roots();
|
||||
let connector = connector.https_or_http().enable_http2().wrap_connector({
|
||||
@@ -37,34 +42,33 @@ pub async fn fill_pool(
|
||||
.build(connector);
|
||||
|
||||
let mut client = ServerReflectionClient::with_origin(transport.clone(), uri.clone());
|
||||
let services = list_services(&mut client).await;
|
||||
|
||||
for service in services {
|
||||
for service in list_services(&mut client).await? {
|
||||
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
||||
continue;
|
||||
}
|
||||
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
|
||||
}
|
||||
|
||||
(pool, transport)
|
||||
Ok((pool, transport))
|
||||
}
|
||||
|
||||
async fn list_services(
|
||||
reflect_client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
||||
) -> Vec<String> {
|
||||
) -> Result<Vec<String>, String> {
|
||||
let response =
|
||||
send_reflection_request(reflect_client, MessageRequest::ListServices("".into())).await;
|
||||
send_reflection_request(reflect_client, MessageRequest::ListServices("".into())).await?;
|
||||
|
||||
let list_services_response = match response {
|
||||
MessageResponse::ListServicesResponse(resp) => resp,
|
||||
_ => panic!("Expected a ListServicesResponse variant"),
|
||||
};
|
||||
|
||||
list_services_response
|
||||
Ok(list_services_response
|
||||
.service
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
async fn file_descriptor_set_from_service_name(
|
||||
@@ -72,11 +76,21 @@ async fn file_descriptor_set_from_service_name(
|
||||
pool: &mut DescriptorPool,
|
||||
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
||||
) {
|
||||
let response = send_reflection_request(
|
||||
let response = match send_reflection_request(
|
||||
client,
|
||||
MessageRequest::FileContainingSymbol(service_name.into()),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Error fetching file descriptor for service {}: {}",
|
||||
service_name, e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let file_descriptor_response = match response {
|
||||
MessageResponse::FileDescriptorResponse(resp) => resp,
|
||||
@@ -109,8 +123,14 @@ async fn file_descriptor_set_by_filename(
|
||||
let response =
|
||||
send_reflection_request(client, MessageRequest::FileByFilename(filename.into())).await;
|
||||
let file_descriptor_response = match response {
|
||||
MessageResponse::FileDescriptorResponse(resp) => resp,
|
||||
_ => panic!("Expected a FileDescriptorResponse variant"),
|
||||
Ok(MessageResponse::FileDescriptorResponse(resp)) => resp,
|
||||
Ok(_) => {
|
||||
panic!("Expected a FileDescriptorResponse variant")
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error fetching file descriptor for {}: {}", filename, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for fd in file_descriptor_response.file_descriptor_proto {
|
||||
@@ -123,7 +143,7 @@ async fn file_descriptor_set_by_filename(
|
||||
async fn send_reflection_request(
|
||||
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
||||
message: MessageRequest,
|
||||
) -> MessageResponse {
|
||||
) -> Result<MessageResponse, String> {
|
||||
let reflection_request = ServerReflectionRequest {
|
||||
host: "".into(), // Doesn't matter
|
||||
message_request: Some(message),
|
||||
@@ -134,14 +154,17 @@ async fn send_reflection_request(
|
||||
client
|
||||
.server_reflection_info(request)
|
||||
.await
|
||||
.expect("server reflection failed")
|
||||
.map_err(|e| match e.code() {
|
||||
Unimplemented => "Reflection not implemented for server".to_string(),
|
||||
_ => e.to_string(),
|
||||
})?
|
||||
.into_inner()
|
||||
.next()
|
||||
.await
|
||||
.expect("steamed response")
|
||||
.expect("successful response")
|
||||
.map_err(|e| e.to_string())?
|
||||
.message_response
|
||||
.expect("some MessageResponse")
|
||||
.ok_or("No reflection response".to_string())
|
||||
}
|
||||
|
||||
pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn cmd_grpc_reflect(
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
|
||||
Ok(grpc::reflect(&uri).await)
|
||||
grpc::reflect(&uri).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -150,7 +150,7 @@ async fn cmd_grpc_call_unary(
|
||||
.lock()
|
||||
.await
|
||||
.connect(&conn.clone().id, uri)
|
||||
.await
|
||||
.await?
|
||||
.unary(
|
||||
&req.service.unwrap_or_default(),
|
||||
&req.method.unwrap_or_default(),
|
||||
|
||||
@@ -86,10 +86,8 @@ export function GlobalHooks() {
|
||||
queryClient.setQueryData<Model[]>(queryKey, (values = []) => {
|
||||
const index = values.findIndex((v) => modelsEq(v, payload)) ?? -1;
|
||||
if (index >= 0) {
|
||||
console.log('UPDATED MODEL', payload);
|
||||
return [...values.slice(0, index), payload, ...values.slice(index + 1)];
|
||||
} else {
|
||||
console.log('INSERTED MODEL', payload);
|
||||
return pushToFront ? [payload, ...(values ?? [])] : [...(values ?? []), payload];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
109
src-web/components/GrpcConnectionMessagesPane.tsx
Normal file
109
src-web/components/GrpcConnectionMessagesPane.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import classNames from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
||||
import type { GrpcRequest } from '../lib/models';
|
||||
import { Icon } from './core/Icon';
|
||||
import { JsonAttributeTree } from './core/JsonAttributeTree';
|
||||
import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
activeRequest: GrpcRequest;
|
||||
methodType:
|
||||
| 'unary'
|
||||
| 'client_streaming'
|
||||
| 'server_streaming'
|
||||
| 'streaming'
|
||||
| 'no-schema'
|
||||
| 'no-method';
|
||||
}
|
||||
|
||||
export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) {
|
||||
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 activeMessage = useMemo(
|
||||
() => messages.find((m) => m.id === activeMessageId) ?? null,
|
||||
[activeMessageId, messages],
|
||||
);
|
||||
|
||||
return (
|
||||
<SplitLayout
|
||||
forceVertical
|
||||
style={style}
|
||||
name={methodType === 'unary' ? 'grpc_messages_unary' : 'grpc_messages_streaming'}
|
||||
defaultRatio={methodType === 'unary' ? 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>
|
||||
{activeConnection?.elapsed === 0 && (
|
||||
<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 &&
|
||||
(() => (
|
||||
<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>
|
||||
))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
242
src-web/components/GrpcConnectionSetupPane.tsx
Normal file
242
src-web/components/GrpcConnectionSetupPane.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import useResizeObserver from '@react-hook/resize-observer';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, FormEvent } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||
import type { GrpcRequest } from '../lib/models';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { RadioDropdown } from './core/RadioDropdown';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { GrpcEditor } from './GrpcEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
activeRequest: GrpcRequest;
|
||||
reflectionError?: string;
|
||||
reflectionLoading?: boolean;
|
||||
methodType:
|
||||
| 'unary'
|
||||
| 'client_streaming'
|
||||
| 'server_streaming'
|
||||
| 'streaming'
|
||||
| 'no-schema'
|
||||
| 'no-method';
|
||||
onUnary: () => void;
|
||||
onCommit: () => void;
|
||||
onCancel: () => void;
|
||||
onSend: (v: { message: string }) => void;
|
||||
onClientStreaming: () => void;
|
||||
onServerStreaming: () => void;
|
||||
onStreaming: () => void;
|
||||
onReflectRefetch: () => void;
|
||||
services: ReflectResponseService[] | null;
|
||||
}
|
||||
|
||||
export function GrpcConnectionSetupPane({
|
||||
style,
|
||||
services,
|
||||
methodType,
|
||||
activeRequest,
|
||||
reflectionError,
|
||||
reflectionLoading,
|
||||
onReflectRefetch,
|
||||
onStreaming,
|
||||
onClientStreaming,
|
||||
onServerStreaming,
|
||||
onCommit,
|
||||
onCancel,
|
||||
onSend,
|
||||
onUnary,
|
||||
}: Props) {
|
||||
const connections = useGrpcConnections(activeRequest.id ?? null);
|
||||
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
|
||||
const activeConnection = connections[0] ?? null;
|
||||
const isStreaming = activeConnection?.elapsed === 0;
|
||||
|
||||
const [paneSize, setPaneSize] = useState(99999);
|
||||
const urlContainerEl = useRef<HTMLDivElement>(null);
|
||||
useResizeObserver<HTMLDivElement>(urlContainerEl.current, (entry) => {
|
||||
setPaneSize(entry.contentRect.width);
|
||||
});
|
||||
|
||||
const handleChangeUrl = useCallback(
|
||||
(url: string) => updateRequest.mutateAsync({ url }),
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const handleChangeMessage = useCallback(
|
||||
(message: string) => updateRequest.mutateAsync({ message }),
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const select = useMemo(() => {
|
||||
const options =
|
||||
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, services]);
|
||||
|
||||
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 handleConnect = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (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 (methodType === 'streaming') {
|
||||
onStreaming();
|
||||
} else if (methodType === 'server_streaming') {
|
||||
onServerStreaming();
|
||||
} else if (methodType === 'client_streaming') {
|
||||
onClientStreaming();
|
||||
} else {
|
||||
onUnary();
|
||||
}
|
||||
},
|
||||
[activeRequest, methodType, onStreaming, onServerStreaming, onClientStreaming, onUnary],
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack space={2} style={style}>
|
||||
<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}
|
||||
onUrlChange={handleChangeUrl}
|
||||
isLoading={false}
|
||||
/>
|
||||
<HStack space={1.5}>
|
||||
<RadioDropdown
|
||||
value={select.value}
|
||||
onChange={handleChangeService}
|
||||
items={select.options.map((o) => ({
|
||||
label: o.label,
|
||||
value: o.value,
|
||||
type: 'default',
|
||||
shortLabel: o.label,
|
||||
}))}
|
||||
extraItems={[
|
||||
{
|
||||
label: 'Custom',
|
||||
type: 'default',
|
||||
key: 'custom',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
rightSlot={<Icon className="text-gray-600" size="sm" icon="chevronDown" />}
|
||||
disabled={isStreaming || services == null}
|
||||
className={classNames(
|
||||
'font-mono text-xs min-w-[5rem] !ring-0',
|
||||
paneSize < 400 && 'flex-1',
|
||||
)}
|
||||
>
|
||||
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
{!isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title={methodType === 'unary' ? 'Send' : 'Connect'}
|
||||
hotkeyAction={isStreaming ? undefined : 'http_request.send'}
|
||||
onClick={handleConnect}
|
||||
disabled={methodType === 'no-schema' || methodType === 'no-method'}
|
||||
icon={
|
||||
isStreaming
|
||||
? 'refresh'
|
||||
: methodType.includes('streaming')
|
||||
? 'arrowUpDown'
|
||||
: 'sendHorizontal'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="Cancel"
|
||||
onClick={onCancel}
|
||||
icon="x"
|
||||
disabled={!isStreaming}
|
||||
/>
|
||||
)}
|
||||
{methodType === 'client_streaming' && isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
onClick={onCommit}
|
||||
icon="check"
|
||||
/>
|
||||
)}
|
||||
{(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={() => onSend({ message: activeRequest.message ?? '' })}
|
||||
icon="sendHorizontal"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
<GrpcEditor
|
||||
forceUpdateKey={activeRequest?.id ?? ''}
|
||||
url={activeRequest.url ?? ''}
|
||||
defaultValue={activeRequest.message}
|
||||
onChange={handleChangeMessage}
|
||||
service={activeRequest.service}
|
||||
services={services}
|
||||
method={activeRequest.method}
|
||||
className="bg-gray-50"
|
||||
reflectionError={reflectionError}
|
||||
reflectionLoading={reflectionLoading}
|
||||
onReflect={onReflectRefetch}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { EditorView } from 'codemirror';
|
||||
import { updateSchema } from 'codemirror-json-schema';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useAlert } from '../hooks/useAlert';
|
||||
import { useGrpc } from '../hooks/useGrpc';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import { Button } from './core/Button';
|
||||
import type { EditorProps } from './core/Editor';
|
||||
import { Editor } from './core/Editor';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
|
||||
type Props = Pick<
|
||||
EditorProps,
|
||||
@@ -18,17 +19,30 @@ type Props = Pick<
|
||||
url: string;
|
||||
service: string | null;
|
||||
method: string | null;
|
||||
services: ReflectResponseService[] | null;
|
||||
reflectionError?: string;
|
||||
reflectionLoading?: boolean;
|
||||
onReflect: () => void;
|
||||
};
|
||||
|
||||
export function GrpcEditor({ url, service, method, defaultValue, ...extraEditorProps }: Props) {
|
||||
export function GrpcEditor({
|
||||
service,
|
||||
method,
|
||||
services,
|
||||
defaultValue,
|
||||
reflectionError,
|
||||
reflectionLoading,
|
||||
onReflect,
|
||||
...extraEditorProps
|
||||
}: Props) {
|
||||
const editorViewRef = useRef<EditorView>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const grpc = useGrpc(url, activeRequestId);
|
||||
const alert = useAlert();
|
||||
const dialog = useDialog();
|
||||
|
||||
useEffect(() => {
|
||||
if (editorViewRef.current == null || grpc.services == null) return;
|
||||
const s = grpc.services?.find((s) => s.name === service);
|
||||
if (editorViewRef.current == null || services === null) return;
|
||||
|
||||
const s = services?.find((s) => s.name === service);
|
||||
if (service != null && s == null) {
|
||||
alert({
|
||||
id: 'grpc-find-service-error',
|
||||
@@ -79,7 +93,7 @@ export function GrpcEditor({ url, service, method, defaultValue, ...extraEditorP
|
||||
});
|
||||
console.log('Failed to parse method schema', method, schema);
|
||||
}
|
||||
}, [alert, grpc.services, method, service]);
|
||||
}, [alert, services, method, service]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||
@@ -90,6 +104,46 @@ export function GrpcEditor({ url, service, method, defaultValue, ...extraEditorP
|
||||
heightMode="auto"
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
actions={
|
||||
reflectionError || reflectionLoading
|
||||
? [
|
||||
<div key="introspection" className="!opacity-100">
|
||||
<Button
|
||||
key="introspection"
|
||||
size="xs"
|
||||
color={reflectionError ? 'danger' : 'gray'}
|
||||
isLoading={reflectionLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<>
|
||||
<FormattedError>{reflectionError ?? 'unknown'}</FormattedError>
|
||||
<HStack className="w-full my-4" space={2} justifyContent="end">
|
||||
<Button color="gray">Select .proto</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
onReflect();
|
||||
}}
|
||||
color="secondary"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</HStack>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{reflectionError ? 'Reflection Failed' : 'Reflecting'}
|
||||
</Button>
|
||||
</div>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
{...extraEditorProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -755,12 +755,14 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
)}
|
||||
{latestGrpcConnection ? (
|
||||
<div className="ml-auto">
|
||||
{latestGrpcConnection.elapsed === 0 && <Icon spin size="sm" icon="update" />}
|
||||
{latestGrpcConnection.elapsed === 0 && (
|
||||
<Icon spin size="sm" icon="update" className="text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
) : latestHttpResponse ? (
|
||||
<div className="ml-auto">
|
||||
{isResponseLoading(latestHttpResponse) ? (
|
||||
<Icon spin size="sm" icon="update" />
|
||||
<Icon spin size="sm" icon="update" className="text-gray-400" />
|
||||
) : (
|
||||
<StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} />
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Icon } from './Icon';
|
||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
|
||||
innerClassName?: string;
|
||||
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
|
||||
variant?: 'border' | 'solid';
|
||||
isLoading?: boolean;
|
||||
size?: 'sm' | 'md' | 'xs';
|
||||
justify?: 'start' | 'center';
|
||||
@@ -27,10 +28,11 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
innerClassName,
|
||||
children,
|
||||
forDropdown,
|
||||
color,
|
||||
color = 'default',
|
||||
type = 'button',
|
||||
justify = 'center',
|
||||
size = 'md',
|
||||
variant = 'solid',
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
disabled,
|
||||
@@ -53,24 +55,53 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
'flex-shrink-0 flex items-center',
|
||||
'focus-visible-or-class:ring rounded-md',
|
||||
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
|
||||
color === 'custom' && 'ring-blue-500/50',
|
||||
color === 'default' &&
|
||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
color === 'gray' &&
|
||||
'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
color === 'primary' && 'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
|
||||
color === 'secondary' &&
|
||||
'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
|
||||
color === 'warning' &&
|
||||
'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
|
||||
color === 'danger' && 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
|
||||
justify === 'start' && 'justify-start',
|
||||
justify === 'center' && 'justify-center',
|
||||
size === 'md' && 'h-md px-3',
|
||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||
size === 'xs' && 'h-xs px-2 text-sm',
|
||||
variant === 'border' && 'border',
|
||||
// Solids
|
||||
variant === 'solid' && color === 'custom' && 'ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'default' &&
|
||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'gray' &&
|
||||
'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'primary' &&
|
||||
'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'secondary' &&
|
||||
'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'warning' &&
|
||||
'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
|
||||
variant === 'solid' &&
|
||||
color === 'danger' &&
|
||||
'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
|
||||
// Borders
|
||||
variant === 'border' &&
|
||||
color === 'default' &&
|
||||
'border-highlight text-gray-700 enabled:hocus:border-focus enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'gray' &&
|
||||
'border-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'primary' &&
|
||||
'border-blue-500/70 text-blue-700 enabled:hocus:border-blue-500 ring-blue-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'secondary' &&
|
||||
'border-violet-500/70 text-violet-700 enabled:hocus:border-violet-500 ring-violet-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'warning' &&
|
||||
'border-orange-500/70 text-orange-700 enabled:hocus:border-orange-500 ring-orange-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'danger' &&
|
||||
'border-red-500/70 text-red-700 enabled:hocus:border-red-500 ring-red-500/50',
|
||||
),
|
||||
[className, disabled, color, justify, size],
|
||||
[className, disabled, justify, size, variant, color],
|
||||
);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function FormattedError({ children }: Props) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { useCallback } from 'react';
|
||||
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
|
||||
|
||||
interface ReflectResponseService {
|
||||
export interface ReflectResponseService {
|
||||
name: string;
|
||||
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
|
||||
}
|
||||
@@ -13,58 +14,46 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
|
||||
|
||||
const unary = useMutation<GrpcMessage, string>({
|
||||
mutationKey: ['grpc_unary', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => {
|
||||
const message = (await invoke('cmd_grpc_call_unary', {
|
||||
mutationFn: async () =>
|
||||
(await invoke('cmd_grpc_call_unary', {
|
||||
requestId,
|
||||
})) as GrpcMessage;
|
||||
return message;
|
||||
},
|
||||
})) as GrpcMessage,
|
||||
});
|
||||
|
||||
const clientStreaming = useMutation<void, string>({
|
||||
mutationKey: ['grpc_client_streaming', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => {
|
||||
await invoke('cmd_grpc_client_streaming', { requestId });
|
||||
},
|
||||
mutationFn: async () => await invoke('cmd_grpc_client_streaming', { requestId }),
|
||||
});
|
||||
|
||||
const serverStreaming = useMutation<void, string>({
|
||||
mutationKey: ['grpc_server_streaming', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => {
|
||||
await invoke('cmd_grpc_server_streaming', { requestId });
|
||||
},
|
||||
mutationFn: async () => await invoke('cmd_grpc_server_streaming', { requestId }),
|
||||
});
|
||||
|
||||
const streaming = useMutation<void, string>({
|
||||
mutationKey: ['grpc_streaming', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => {
|
||||
await invoke('cmd_grpc_streaming', { requestId });
|
||||
},
|
||||
mutationFn: async () => await invoke('cmd_grpc_streaming', { requestId }),
|
||||
});
|
||||
|
||||
const send = useMutation({
|
||||
mutationFn: async ({ message }: { message: string }) => {
|
||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message });
|
||||
},
|
||||
mutationFn: async ({ message }: { message: string }) =>
|
||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message }),
|
||||
});
|
||||
|
||||
const cancel = useMutation({
|
||||
mutationKey: ['grpc_cancel', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => {
|
||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel');
|
||||
},
|
||||
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel'),
|
||||
});
|
||||
|
||||
const commit = useMutation({
|
||||
mutationKey: ['grpc_commit', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => {
|
||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit');
|
||||
},
|
||||
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
|
||||
});
|
||||
|
||||
const reflect = useQuery<ReflectResponseService[]>({
|
||||
queryKey: ['grpc_reflect', conn?.id ?? 'n/a'],
|
||||
queryKey: ['grpc_reflect', req?.url ?? 'n/a'],
|
||||
queryFn: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return (await invoke('cmd_grpc_reflect', { requestId })) as ReflectResponseService[];
|
||||
},
|
||||
});
|
||||
@@ -74,7 +63,7 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
|
||||
clientStreaming,
|
||||
serverStreaming,
|
||||
streaming,
|
||||
services: reflect.data,
|
||||
reflect,
|
||||
cancel,
|
||||
commit,
|
||||
isStreaming: conn?.elapsed === 0,
|
||||
|
||||
Reference in New Issue
Block a user