mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-14 12:59:42 +02:00
Better reflect failure UI
This commit is contained in:
@@ -29,10 +29,11 @@ pub struct MethodDefinition {
|
|||||||
pub server_streaming: bool,
|
pub server_streaming: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reflect(uri: &Uri) -> Vec<ServiceDefinition> {
|
pub async fn reflect(uri: &Uri) -> Result<Vec<ServiceDefinition>, String> {
|
||||||
let (pool, _) = fill_pool(uri).await;
|
let (pool, _) = fill_pool(uri).await?;
|
||||||
|
|
||||||
pool.services()
|
Ok(pool
|
||||||
|
.services()
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
let mut def = ServiceDefinition {
|
let mut def = ServiceDefinition {
|
||||||
name: s.full_name().to_string(),
|
name: s.full_name().to_string(),
|
||||||
@@ -53,5 +54,5 @@ pub async fn reflect(uri: &Uri) -> Vec<ServiceDefinition> {
|
|||||||
}
|
}
|
||||||
def
|
def
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ impl GrpcManager {
|
|||||||
message: &str,
|
message: &str,
|
||||||
) -> Result<Streaming<DynamicMessage>> {
|
) -> Result<Streaming<DynamicMessage>> {
|
||||||
self.connect(id, uri)
|
self.connect(id, uri)
|
||||||
.await
|
.await?
|
||||||
.server_streaming(service, method, message)
|
.server_streaming(service, method, message)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ impl GrpcManager {
|
|||||||
stream: ReceiverStream<String>,
|
stream: ReceiverStream<String>,
|
||||||
) -> Result<DynamicMessage> {
|
) -> Result<DynamicMessage> {
|
||||||
self.connect(id, uri)
|
self.connect(id, uri)
|
||||||
.await
|
.await?
|
||||||
.client_streaming(service, method, stream)
|
.client_streaming(service, method, stream)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -201,15 +201,15 @@ impl GrpcManager {
|
|||||||
stream: ReceiverStream<String>,
|
stream: ReceiverStream<String>,
|
||||||
) -> Result<Streaming<DynamicMessage>> {
|
) -> Result<Streaming<DynamicMessage>> {
|
||||||
self.connect(id, uri)
|
self.connect(id, uri)
|
||||||
.await
|
.await?
|
||||||
.streaming(service, method, stream)
|
.streaming(service, method, stream)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect(&mut self, id: &str, uri: Uri) -> GrpcConnection {
|
pub async fn connect(&mut self, id: &str, uri: Uri) -> Result<GrpcConnection> {
|
||||||
let (pool, conn) = fill_pool(&uri).await;
|
let (pool, conn) = fill_pool(&uri).await?;
|
||||||
let connection = GrpcConnection { pool, conn, uri };
|
let connection = GrpcConnection { pool, conn, uri };
|
||||||
self.connections.insert(id.to_string(), connection.clone());
|
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::HttpConnector;
|
||||||
use hyper::Client;
|
use hyper::Client;
|
||||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||||
|
use log::warn;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
||||||
use prost_types::FileDescriptorProto;
|
use prost_types::FileDescriptorProto;
|
||||||
@@ -12,6 +13,7 @@ use tokio_stream::StreamExt;
|
|||||||
use tonic::body::BoxBody;
|
use tonic::body::BoxBody;
|
||||||
use tonic::codegen::http::uri::PathAndQuery;
|
use tonic::codegen::http::uri::PathAndQuery;
|
||||||
use tonic::transport::Uri;
|
use tonic::transport::Uri;
|
||||||
|
use tonic::Code::Unimplemented;
|
||||||
use tonic::Request;
|
use tonic::Request;
|
||||||
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
|
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
|
||||||
use tonic_reflection::pb::server_reflection_request::MessageRequest;
|
use tonic_reflection::pb::server_reflection_request::MessageRequest;
|
||||||
@@ -20,10 +22,13 @@ use tonic_reflection::pb::ServerReflectionRequest;
|
|||||||
|
|
||||||
pub async fn fill_pool(
|
pub async fn fill_pool(
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
) -> (
|
) -> Result<
|
||||||
DescriptorPool,
|
(
|
||||||
Client<HttpsConnector<HttpConnector>, BoxBody>,
|
DescriptorPool,
|
||||||
) {
|
Client<HttpsConnector<HttpConnector>, BoxBody>,
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
> {
|
||||||
let mut pool = DescriptorPool::new();
|
let mut pool = DescriptorPool::new();
|
||||||
let connector = HttpsConnectorBuilder::new().with_native_roots();
|
let connector = HttpsConnectorBuilder::new().with_native_roots();
|
||||||
let connector = connector.https_or_http().enable_http2().wrap_connector({
|
let connector = connector.https_or_http().enable_http2().wrap_connector({
|
||||||
@@ -37,34 +42,33 @@ pub async fn fill_pool(
|
|||||||
.build(connector);
|
.build(connector);
|
||||||
|
|
||||||
let mut client = ServerReflectionClient::with_origin(transport.clone(), uri.clone());
|
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" {
|
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
|
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
(pool, transport)
|
Ok((pool, transport))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_services(
|
async fn list_services(
|
||||||
reflect_client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
reflect_client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
||||||
) -> Vec<String> {
|
) -> Result<Vec<String>, String> {
|
||||||
let response =
|
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 {
|
let list_services_response = match response {
|
||||||
MessageResponse::ListServicesResponse(resp) => resp,
|
MessageResponse::ListServicesResponse(resp) => resp,
|
||||||
_ => panic!("Expected a ListServicesResponse variant"),
|
_ => panic!("Expected a ListServicesResponse variant"),
|
||||||
};
|
};
|
||||||
|
|
||||||
list_services_response
|
Ok(list_services_response
|
||||||
.service
|
.service
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.name.clone())
|
.map(|s| s.name.clone())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn file_descriptor_set_from_service_name(
|
async fn file_descriptor_set_from_service_name(
|
||||||
@@ -72,11 +76,21 @@ async fn file_descriptor_set_from_service_name(
|
|||||||
pool: &mut DescriptorPool,
|
pool: &mut DescriptorPool,
|
||||||
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
||||||
) {
|
) {
|
||||||
let response = send_reflection_request(
|
let response = match send_reflection_request(
|
||||||
client,
|
client,
|
||||||
MessageRequest::FileContainingSymbol(service_name.into()),
|
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 {
|
let file_descriptor_response = match response {
|
||||||
MessageResponse::FileDescriptorResponse(resp) => resp,
|
MessageResponse::FileDescriptorResponse(resp) => resp,
|
||||||
@@ -109,8 +123,14 @@ async fn file_descriptor_set_by_filename(
|
|||||||
let response =
|
let response =
|
||||||
send_reflection_request(client, MessageRequest::FileByFilename(filename.into())).await;
|
send_reflection_request(client, MessageRequest::FileByFilename(filename.into())).await;
|
||||||
let file_descriptor_response = match response {
|
let file_descriptor_response = match response {
|
||||||
MessageResponse::FileDescriptorResponse(resp) => resp,
|
Ok(MessageResponse::FileDescriptorResponse(resp)) => resp,
|
||||||
_ => panic!("Expected a FileDescriptorResponse variant"),
|
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 {
|
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(
|
async fn send_reflection_request(
|
||||||
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
||||||
message: MessageRequest,
|
message: MessageRequest,
|
||||||
) -> MessageResponse {
|
) -> Result<MessageResponse, String> {
|
||||||
let reflection_request = ServerReflectionRequest {
|
let reflection_request = ServerReflectionRequest {
|
||||||
host: "".into(), // Doesn't matter
|
host: "".into(), // Doesn't matter
|
||||||
message_request: Some(message),
|
message_request: Some(message),
|
||||||
@@ -134,14 +154,17 @@ async fn send_reflection_request(
|
|||||||
client
|
client
|
||||||
.server_reflection_info(request)
|
.server_reflection_info(request)
|
||||||
.await
|
.await
|
||||||
.expect("server reflection failed")
|
.map_err(|e| match e.code() {
|
||||||
|
Unimplemented => "Reflection not implemented for server".to_string(),
|
||||||
|
_ => e.to_string(),
|
||||||
|
})?
|
||||||
.into_inner()
|
.into_inner()
|
||||||
.next()
|
.next()
|
||||||
.await
|
.await
|
||||||
.expect("steamed response")
|
.expect("steamed response")
|
||||||
.expect("successful response")
|
.map_err(|e| e.to_string())?
|
||||||
.message_response
|
.message_response
|
||||||
.expect("some MessageResponse")
|
.ok_or("No reflection response".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
|
pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ async fn cmd_grpc_reflect(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let uri = safe_uri(&req.url).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]
|
#[tauri::command]
|
||||||
@@ -150,7 +150,7 @@ async fn cmd_grpc_call_unary(
|
|||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.connect(&conn.clone().id, uri)
|
.connect(&conn.clone().id, uri)
|
||||||
.await
|
.await?
|
||||||
.unary(
|
.unary(
|
||||||
&req.service.unwrap_or_default(),
|
&req.service.unwrap_or_default(),
|
||||||
&req.method.unwrap_or_default(),
|
&req.method.unwrap_or_default(),
|
||||||
|
|||||||
@@ -86,10 +86,8 @@ export function GlobalHooks() {
|
|||||||
queryClient.setQueryData<Model[]>(queryKey, (values = []) => {
|
queryClient.setQueryData<Model[]>(queryKey, (values = []) => {
|
||||||
const index = values.findIndex((v) => modelsEq(v, payload)) ?? -1;
|
const index = values.findIndex((v) => modelsEq(v, payload)) ?? -1;
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
console.log('UPDATED MODEL', payload);
|
|
||||||
return [...values.slice(0, index), payload, ...values.slice(index + 1)];
|
return [...values.slice(0, index), payload, ...values.slice(index + 1)];
|
||||||
} else {
|
} else {
|
||||||
console.log('INSERTED MODEL', payload);
|
|
||||||
return pushToFront ? [payload, ...(values ?? [])] : [...(values ?? []), payload];
|
return pushToFront ? [payload, ...(values ?? [])] : [...(values ?? []), payload];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import useResizeObserver from '@react-hook/resize-observer';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { format } from 'date-fns';
|
import type { CSSProperties } from 'react';
|
||||||
import type { CSSProperties, FormEvent } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useAlert } from '../hooks/useAlert';
|
|
||||||
import { useGrpc } from '../hooks/useGrpc';
|
import { useGrpc } from '../hooks/useGrpc';
|
||||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||||
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
||||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||||
import { Banner } from './core/Banner';
|
import { Banner } from './core/Banner';
|
||||||
import { Button } from './core/Button';
|
|
||||||
import { HotKeyList } from './core/HotKeyList';
|
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 { SplitLayout } from './core/SplitLayout';
|
||||||
import { HStack, VStack } from './core/Stacks';
|
import { GrpcConnectionMessagesPane } from './GrpcConnectionMessagesPane';
|
||||||
import { GrpcEditor } from './GrpcEditor';
|
import { GrpcConnectionSetupPane } from './GrpcConnectionSetupPane';
|
||||||
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
|
||||||
import { UrlBar } from './UrlBar';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
@@ -30,62 +19,19 @@ interface Props {
|
|||||||
export function GrpcConnectionLayout({ style }: Props) {
|
export function GrpcConnectionLayout({ style }: Props) {
|
||||||
const activeRequest = useActiveRequest('grpc_request');
|
const activeRequest = useActiveRequest('grpc_request');
|
||||||
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
|
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
|
||||||
const alert = useAlert();
|
|
||||||
const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
|
|
||||||
const connections = useGrpcConnections(activeRequest?.id ?? null);
|
const connections = useGrpcConnections(activeRequest?.id ?? null);
|
||||||
const activeConnection = connections[0] ?? null;
|
const activeConnection = connections[0] ?? null;
|
||||||
const messages = useGrpcMessages(activeConnection?.id ?? null);
|
const messages = useGrpcMessages(activeConnection?.id ?? null);
|
||||||
const grpc = useGrpc(activeRequest, activeConnection);
|
const grpc = useGrpc(activeRequest, activeConnection);
|
||||||
|
|
||||||
const activeMethod = useMemo(() => {
|
const services = grpc.reflect.data ?? null;
|
||||||
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,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (grpc.services == null || activeRequest == null) return;
|
if (services == null || activeRequest == null) return;
|
||||||
const s = grpc.services.find((s) => s.name === activeRequest.service);
|
const s = services.find((s) => s.name === activeRequest.service);
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
updateRequest.mutate({
|
updateRequest.mutate({
|
||||||
service: grpc.services[0]?.name ?? null,
|
service: services[0]?.name ?? null,
|
||||||
method: grpc.services[0]?.methods[0]?.name ?? null,
|
method: services[0]?.methods[0]?.name ?? null,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,61 +41,30 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
updateRequest.mutate({ method: s.methods[0]?.name ?? null });
|
updateRequest.mutate({ method: s.methods[0]?.name ?? null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [activeRequest, grpc.services, updateRequest]);
|
}, [activeRequest, services, updateRequest]);
|
||||||
|
|
||||||
const handleChangeService = useCallback(
|
const activeMethod = useMemo(() => {
|
||||||
async (v: string) => {
|
if (services == null || activeRequest == null) return null;
|
||||||
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 handleChangeUrl = useCallback(
|
const s = services.find((s) => s.name === activeRequest.service);
|
||||||
(url: string) => updateRequest.mutateAsync({ url }),
|
if (s == null) return null;
|
||||||
[updateRequest],
|
return s.methods.find((m) => m.name === activeRequest.method);
|
||||||
);
|
}, [activeRequest, services]);
|
||||||
|
|
||||||
const handleChangeMessage = useCallback(
|
const methodType:
|
||||||
(message: string) => updateRequest.mutateAsync({ message }),
|
| 'unary'
|
||||||
[updateRequest],
|
| 'server_streaming'
|
||||||
);
|
| 'client_streaming'
|
||||||
|
| 'streaming'
|
||||||
const select = useMemo(() => {
|
| 'no-schema'
|
||||||
const options =
|
| 'no-method' = useMemo(() => {
|
||||||
grpc.services?.flatMap((s) =>
|
if (services == null) return 'no-schema';
|
||||||
s.methods.map((m) => ({
|
if (activeMethod == null) return 'no-method';
|
||||||
label: `${s.name.split('.', 2).pop() ?? s.name}/${m.name}`,
|
if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming';
|
||||||
value: `${s.name}/${m.name}`,
|
if (activeMethod.clientStreaming) return 'client_streaming';
|
||||||
})),
|
if (activeMethod.serverStreaming) return 'server_streaming';
|
||||||
) ?? [];
|
return 'unary';
|
||||||
const value = `${activeRequest?.service ?? ''}/${activeRequest?.method ?? ''}`;
|
}, [activeMethod, services]);
|
||||||
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]);
|
|
||||||
|
|
||||||
if (activeRequest == null) {
|
if (activeRequest == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -160,110 +75,28 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
name="grpc_layout"
|
name="grpc_layout"
|
||||||
className="p-3 gap-1.5"
|
className="p-3 gap-1.5"
|
||||||
style={style}
|
style={style}
|
||||||
firstSlot={() => (
|
firstSlot={({ style }) => (
|
||||||
<VStack space={2}>
|
<GrpcConnectionSetupPane
|
||||||
<div
|
style={style}
|
||||||
ref={urlContainerEl}
|
activeRequest={activeRequest}
|
||||||
className={classNames(
|
methodType={methodType}
|
||||||
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5',
|
onUnary={grpc.unary.mutate}
|
||||||
paneSize < 400 && '!grid-cols-1',
|
onServerStreaming={grpc.serverStreaming.mutate}
|
||||||
)}
|
onClientStreaming={grpc.clientStreaming.mutate}
|
||||||
>
|
onStreaming={grpc.streaming.mutate}
|
||||||
<UrlBar
|
onCommit={grpc.commit.mutate}
|
||||||
url={activeRequest.url ?? ''}
|
onCancel={grpc.cancel.mutate}
|
||||||
method={null}
|
onSend={grpc.send.mutate}
|
||||||
submitIcon={null}
|
onReflectRefetch={grpc.reflect.refetch}
|
||||||
forceUpdateKey={activeRequest?.id ?? ''}
|
services={services ?? null}
|
||||||
placeholder="localhost:50051"
|
reflectionError={grpc.reflect.error as string | undefined}
|
||||||
onSubmit={handleConnect}
|
reflectionLoading={grpc.reflect.isLoading}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
secondSlot={() =>
|
secondSlot={({ style }) =>
|
||||||
!grpc.unary.isLoading && (
|
!grpc.unary.isLoading && (
|
||||||
<div
|
<div
|
||||||
|
style={style}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
|
'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',
|
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
|
||||||
@@ -275,102 +108,8 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
{grpc.unary.error}
|
{grpc.unary.error}
|
||||||
</Banner>
|
</Banner>
|
||||||
) : messages.length >= 0 ? (
|
) : messages.length >= 0 ? (
|
||||||
<SplitLayout
|
<GrpcConnectionMessagesPane activeRequest={activeRequest} methodType={methodType} />
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
// ) : ? (
|
|
||||||
// <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']} />
|
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 type { EditorView } from 'codemirror';
|
||||||
import { updateSchema } from 'codemirror-json-schema';
|
import { updateSchema } from 'codemirror-json-schema';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
|
||||||
import { useAlert } from '../hooks/useAlert';
|
import { useAlert } from '../hooks/useAlert';
|
||||||
import { useGrpc } from '../hooks/useGrpc';
|
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
|
import { Button } from './core/Button';
|
||||||
import type { EditorProps } from './core/Editor';
|
import type { EditorProps } from './core/Editor';
|
||||||
import { Editor } from './core/Editor';
|
import { Editor } from './core/Editor';
|
||||||
import { FormattedError } from './core/FormattedError';
|
import { FormattedError } from './core/FormattedError';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import { VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
|
import { useDialog } from './DialogContext';
|
||||||
|
|
||||||
type Props = Pick<
|
type Props = Pick<
|
||||||
EditorProps,
|
EditorProps,
|
||||||
@@ -18,17 +19,30 @@ type Props = Pick<
|
|||||||
url: string;
|
url: string;
|
||||||
service: string | null;
|
service: string | null;
|
||||||
method: 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 editorViewRef = useRef<EditorView>(null);
|
||||||
const activeRequestId = useActiveRequestId();
|
|
||||||
const grpc = useGrpc(url, activeRequestId);
|
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorViewRef.current == null || grpc.services == null) return;
|
if (editorViewRef.current == null || services === null) return;
|
||||||
const s = grpc.services?.find((s) => s.name === service);
|
|
||||||
|
const s = services?.find((s) => s.name === service);
|
||||||
if (service != null && s == null) {
|
if (service != null && s == null) {
|
||||||
alert({
|
alert({
|
||||||
id: 'grpc-find-service-error',
|
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);
|
console.log('Failed to parse method schema', method, schema);
|
||||||
}
|
}
|
||||||
}, [alert, grpc.services, method, service]);
|
}, [alert, services, method, service]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
<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"
|
heightMode="auto"
|
||||||
placeholder="..."
|
placeholder="..."
|
||||||
ref={editorViewRef}
|
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}
|
{...extraEditorProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -755,12 +755,14 @@ const SidebarItem = forwardRef(function SidebarItem(
|
|||||||
)}
|
)}
|
||||||
{latestGrpcConnection ? (
|
{latestGrpcConnection ? (
|
||||||
<div className="ml-auto">
|
<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>
|
</div>
|
||||||
) : latestHttpResponse ? (
|
) : latestHttpResponse ? (
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
{isResponseLoading(latestHttpResponse) ? (
|
{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} />
|
<StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Icon } from './Icon';
|
|||||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
|
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
|
||||||
innerClassName?: string;
|
innerClassName?: string;
|
||||||
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
|
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
|
||||||
|
variant?: 'border' | 'solid';
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
size?: 'sm' | 'md' | 'xs';
|
size?: 'sm' | 'md' | 'xs';
|
||||||
justify?: 'start' | 'center';
|
justify?: 'start' | 'center';
|
||||||
@@ -27,10 +28,11 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
innerClassName,
|
innerClassName,
|
||||||
children,
|
children,
|
||||||
forDropdown,
|
forDropdown,
|
||||||
color,
|
color = 'default',
|
||||||
type = 'button',
|
type = 'button',
|
||||||
justify = 'center',
|
justify = 'center',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
|
variant = 'solid',
|
||||||
leftSlot,
|
leftSlot,
|
||||||
rightSlot,
|
rightSlot,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -53,24 +55,53 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
'flex-shrink-0 flex items-center',
|
'flex-shrink-0 flex items-center',
|
||||||
'focus-visible-or-class:ring rounded-md',
|
'focus-visible-or-class:ring rounded-md',
|
||||||
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
|
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 === 'start' && 'justify-start',
|
||||||
justify === 'center' && 'justify-center',
|
justify === 'center' && 'justify-center',
|
||||||
size === 'md' && 'h-md px-3',
|
size === 'md' && 'h-md px-3',
|
||||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||||
size === 'xs' && 'h-xs px-2 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);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: string;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormattedError({ children }: Props) {
|
export function FormattedError({ children }: Props) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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 { emit } from '@tauri-apps/api/event';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
|
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
|
||||||
|
|
||||||
interface ReflectResponseService {
|
export interface ReflectResponseService {
|
||||||
name: string;
|
name: string;
|
||||||
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
|
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>({
|
const unary = useMutation<GrpcMessage, string>({
|
||||||
mutationKey: ['grpc_unary', conn?.id ?? 'n/a'],
|
mutationKey: ['grpc_unary', conn?.id ?? 'n/a'],
|
||||||
mutationFn: async () => {
|
mutationFn: async () =>
|
||||||
const message = (await invoke('cmd_grpc_call_unary', {
|
(await invoke('cmd_grpc_call_unary', {
|
||||||
requestId,
|
requestId,
|
||||||
})) as GrpcMessage;
|
})) as GrpcMessage,
|
||||||
return message;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientStreaming = useMutation<void, string>({
|
const clientStreaming = useMutation<void, string>({
|
||||||
mutationKey: ['grpc_client_streaming', conn?.id ?? 'n/a'],
|
mutationKey: ['grpc_client_streaming', conn?.id ?? 'n/a'],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => await invoke('cmd_grpc_client_streaming', { requestId }),
|
||||||
await invoke('cmd_grpc_client_streaming', { requestId });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverStreaming = useMutation<void, string>({
|
const serverStreaming = useMutation<void, string>({
|
||||||
mutationKey: ['grpc_server_streaming', conn?.id ?? 'n/a'],
|
mutationKey: ['grpc_server_streaming', conn?.id ?? 'n/a'],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => await invoke('cmd_grpc_server_streaming', { requestId }),
|
||||||
await invoke('cmd_grpc_server_streaming', { requestId });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const streaming = useMutation<void, string>({
|
const streaming = useMutation<void, string>({
|
||||||
mutationKey: ['grpc_streaming', conn?.id ?? 'n/a'],
|
mutationKey: ['grpc_streaming', conn?.id ?? 'n/a'],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => await invoke('cmd_grpc_streaming', { requestId }),
|
||||||
await invoke('cmd_grpc_streaming', { requestId });
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const send = useMutation({
|
const send = useMutation({
|
||||||
mutationFn: async ({ message }: { message: string }) => {
|
mutationFn: async ({ message }: { message: string }) =>
|
||||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message });
|
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message }),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancel = useMutation({
|
const cancel = useMutation({
|
||||||
mutationKey: ['grpc_cancel', conn?.id ?? 'n/a'],
|
mutationKey: ['grpc_cancel', conn?.id ?? 'n/a'],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel'),
|
||||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const commit = useMutation({
|
const commit = useMutation({
|
||||||
mutationKey: ['grpc_commit', conn?.id ?? 'n/a'],
|
mutationKey: ['grpc_commit', conn?.id ?? 'n/a'],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
|
||||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const reflect = useQuery<ReflectResponseService[]>({
|
const reflect = useQuery<ReflectResponseService[]>({
|
||||||
queryKey: ['grpc_reflect', conn?.id ?? 'n/a'],
|
queryKey: ['grpc_reflect', req?.url ?? 'n/a'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
return (await invoke('cmd_grpc_reflect', { requestId })) as ReflectResponseService[];
|
return (await invoke('cmd_grpc_reflect', { requestId })) as ReflectResponseService[];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -74,7 +63,7 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
|
|||||||
clientStreaming,
|
clientStreaming,
|
||||||
serverStreaming,
|
serverStreaming,
|
||||||
streaming,
|
streaming,
|
||||||
services: reflect.data,
|
reflect,
|
||||||
cancel,
|
cancel,
|
||||||
commit,
|
commit,
|
||||||
isStreaming: conn?.elapsed === 0,
|
isStreaming: conn?.elapsed === 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user