mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:38:26 +02:00
Refactor into grpc events
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE connection_id = ?\n ",
|
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, content,\n event_type AS \"event_type!: GrpcEventType\",\n metadata AS \"metadata!: sqlx::types::Json<HashMap<String, String>>\"\n FROM grpc_events\n WHERE id = ?\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -34,19 +34,19 @@
|
|||||||
"type_info": "Datetime"
|
"type_info": "Datetime"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "message",
|
"name": "content",
|
||||||
"ordinal": 6,
|
"ordinal": 6,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "is_server",
|
"name": "event_type!: GrpcEventType",
|
||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"type_info": "Bool"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "is_info",
|
"name": "metadata!: sqlx::types::Json<HashMap<String, String>>",
|
||||||
"ordinal": 8,
|
"ordinal": 8,
|
||||||
"type_info": "Bool"
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -64,5 +64,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59"
|
"hash": "20d6b878bb8d16bde3e78e22cf801b5b191905d867091bb54a210256a0145a17"
|
||||||
}
|
}
|
||||||
12
src-tauri/.sqlx/query-3dce053aef78e831db2369f3c49e891cb8a9e1ba6e7a60fe9e24292a3f97dca3.json
generated
Normal file
12
src-tauri/.sqlx/query-3dce053aef78e831db2369f3c49e891cb8a9e1ba6e7a60fe9e24292a3f97dca3.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO grpc_events (\n id, workspace_id, request_id, connection_id, content, event_type, metadata\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n content = excluded.content,\n event_type = excluded.event_type,\n metadata = excluded.metadata\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 7
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "3dce053aef78e831db2369f3c49e891cb8a9e1ba6e7a60fe9e24292a3f97dca3"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ",
|
"query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed, status, error, url,\n trailers AS \"trailers!: sqlx::types::Json<HashMap<String, String>>\"\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -47,6 +47,26 @@
|
|||||||
"name": "elapsed",
|
"name": "elapsed",
|
||||||
"ordinal": 8,
|
"ordinal": 8,
|
||||||
"type_info": "Int64"
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "error",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trailers!: sqlx::types::Json<HashMap<String, String>>",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -61,8 +81,12 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407"
|
"hash": "3e8651cca7feecc208a676dfd24c7d8775040d5287c16890056dcb474674edfb"
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message, is_server, is_info\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message,\n is_server = excluded.is_server,\n is_info = excluded.is_info\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 7
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460"
|
|
||||||
}
|
|
||||||
12
src-tauri/.sqlx/query-66deed028199c78ed15ea2f837907887c2a2cb564d1d076dd4ebf0ecbc82e098.json
generated
Normal file
12
src-tauri/.sqlx/query-66deed028199c78ed15ea2f837907887c2a2cb564d1d076dd4ebf0ecbc82e098.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO grpc_connections (\n id, workspace_id, request_id, service, method, elapsed,\n status, error, trailers, url\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n service = excluded.service,\n method = excluded.method,\n elapsed = excluded.elapsed,\n status = excluded.status,\n error = excluded.error,\n trailers = excluded.trailers,\n url = excluded.url\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 10
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "66deed028199c78ed15ea2f837907887c2a2cb564d1d076dd4ebf0ecbc82e098"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE id = ?\n ",
|
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, content,\n event_type AS \"event_type!: GrpcEventType\",\n metadata AS \"metadata!: sqlx::types::Json<HashMap<String, String>>\"\n FROM grpc_events\n WHERE connection_id = ?\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -34,19 +34,19 @@
|
|||||||
"type_info": "Datetime"
|
"type_info": "Datetime"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "message",
|
"name": "content",
|
||||||
"ordinal": 6,
|
"ordinal": 6,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "is_server",
|
"name": "event_type!: GrpcEventType",
|
||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"type_info": "Bool"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "is_info",
|
"name": "metadata!: sqlx::types::Json<HashMap<String, String>>",
|
||||||
"ordinal": 8,
|
"ordinal": 8,
|
||||||
"type_info": "Bool"
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -64,5 +64,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c"
|
"hash": "737045ddd5f8ba3454425e82b9d3943f93649742d8f78613e01d322745e47ebd"
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT INTO grpc_connections (\n id, workspace_id, request_id, service, method, elapsed\n )\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n service = excluded.service,\n method = excluded.method,\n elapsed = excluded.elapsed\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 6
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed\n FROM grpc_connections\n WHERE id = ?\n ",
|
"query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed, status, error, url,\n trailers AS \"trailers!: sqlx::types::Json<HashMap<String, String>>\"\n FROM grpc_connections\n WHERE id = ?\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -47,6 +47,26 @@
|
|||||||
"name": "elapsed",
|
"name": "elapsed",
|
||||||
"ordinal": 8,
|
"ordinal": 8,
|
||||||
"type_info": "Int64"
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "error",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trailers!: sqlx::types::Json<HashMap<String, String>>",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -61,8 +81,12 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8"
|
"hash": "d4b64c466624eb75e0f5bd201ebfb6a73d25eb7c9e09cb9690afdb7fef5fca8b"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
use prost_reflect::{DynamicMessage, SerializeOptions};
|
use prost_reflect::{DynamicMessage, MethodDescriptor, SerializeOptions};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Deserializer;
|
||||||
|
|
||||||
mod codec;
|
mod codec;
|
||||||
mod json_schema;
|
mod json_schema;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
mod proto;
|
mod proto;
|
||||||
|
|
||||||
|
pub use tonic::metadata::*;
|
||||||
|
pub use tonic::Code;
|
||||||
|
|
||||||
pub fn serialize_options() -> SerializeOptions {
|
pub fn serialize_options() -> SerializeOptions {
|
||||||
SerializeOptions::new().skip_default_fields(false)
|
SerializeOptions::new().skip_default_fields(false)
|
||||||
}
|
}
|
||||||
@@ -38,3 +42,11 @@ pub fn serialize_message(msg: &DynamicMessage) -> Result<String, String> {
|
|||||||
let s = String::from_utf8(buf).expect("serde_json to emit valid utf8");
|
let s = String::from_utf8(buf).expect("serde_json to emit valid utf8");
|
||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deserialize_message(msg: &str, method: MethodDescriptor) -> Result<DynamicMessage, String> {
|
||||||
|
let mut deserializer = Deserializer::from_str(&msg);
|
||||||
|
let req_message = DynamicMessage::deserialize(method.input(), &mut deserializer)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
deserializer.end().map_err(|e| e.to_string())?;
|
||||||
|
Ok(req_message)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ pub use prost_reflect::DynamicMessage;
|
|||||||
use prost_reflect::{DescriptorPool, MethodDescriptor, ServiceDescriptor};
|
use prost_reflect::{DescriptorPool, MethodDescriptor, ServiceDescriptor};
|
||||||
use serde_json::Deserializer;
|
use serde_json::Deserializer;
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tokio_stream::StreamExt;
|
|
||||||
use tonic::body::BoxBody;
|
use tonic::body::BoxBody;
|
||||||
use tonic::metadata::{MetadataKey, MetadataValue};
|
use tonic::metadata::{MetadataKey, MetadataValue};
|
||||||
use tonic::transport::Uri;
|
use tonic::transport::Uri;
|
||||||
@@ -50,7 +49,7 @@ impl GrpcConnection {
|
|||||||
method: &str,
|
method: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
metadata: HashMap<String, String>,
|
metadata: HashMap<String, String>,
|
||||||
) -> Result<DynamicMessage, String> {
|
) -> Result<Response<DynamicMessage>, String> {
|
||||||
let method = &self.method(&service, &method)?;
|
let method = &self.method(&service, &method)?;
|
||||||
let input_message = method.input();
|
let input_message = method.input();
|
||||||
|
|
||||||
@@ -68,34 +67,23 @@ impl GrpcConnection {
|
|||||||
let codec = DynamicCodec::new(method.clone());
|
let codec = DynamicCodec::new(method.clone());
|
||||||
client.ready().await.unwrap();
|
client.ready().await.unwrap();
|
||||||
|
|
||||||
Ok(client
|
client
|
||||||
.unary(req, path, codec)
|
.unary(req, path, codec)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())
|
||||||
.into_inner())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn streaming(
|
pub async fn streaming(
|
||||||
&self,
|
&self,
|
||||||
service: &str,
|
service: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
stream: ReceiverStream<String>,
|
stream: ReceiverStream<DynamicMessage>,
|
||||||
metadata: HashMap<String, String>,
|
metadata: HashMap<String, String>,
|
||||||
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
|
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
|
||||||
let method = &self.method(&service, &method)?;
|
let method = &self.method(&service, &method)?;
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||||
|
|
||||||
let method2 = method.clone();
|
let mut req = stream.into_streaming_request();
|
||||||
let mut req = stream
|
|
||||||
.map(move |s| {
|
|
||||||
let mut deserializer = Deserializer::from_str(&s);
|
|
||||||
let req_message = DynamicMessage::deserialize(method2.input(), &mut deserializer)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
.unwrap();
|
|
||||||
deserializer.end().unwrap();
|
|
||||||
req_message
|
|
||||||
})
|
|
||||||
.into_streaming_request();
|
|
||||||
|
|
||||||
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@@ -109,37 +97,21 @@ impl GrpcConnection {
|
|||||||
&self,
|
&self,
|
||||||
service: &str,
|
service: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
stream: ReceiverStream<String>,
|
stream: ReceiverStream<DynamicMessage>,
|
||||||
metadata: HashMap<String, String>,
|
metadata: HashMap<String, String>,
|
||||||
) -> Result<DynamicMessage, String> {
|
) -> Result<Response<DynamicMessage>, String> {
|
||||||
let method = &self.method(&service, &method)?;
|
let method = &self.method(&service, &method)?;
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||||
|
let mut req = stream.into_streaming_request();
|
||||||
let mut req = {
|
|
||||||
let method = method.clone();
|
|
||||||
stream
|
|
||||||
.map(move |s| {
|
|
||||||
let mut deserializer = Deserializer::from_str(&s);
|
|
||||||
let req_message =
|
|
||||||
DynamicMessage::deserialize(method.input(), &mut deserializer)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
.unwrap();
|
|
||||||
deserializer.end().unwrap();
|
|
||||||
req_message
|
|
||||||
})
|
|
||||||
.into_streaming_request()
|
|
||||||
};
|
|
||||||
|
|
||||||
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let path = method_desc_to_path(method);
|
let path = method_desc_to_path(method);
|
||||||
let codec = DynamicCodec::new(method.clone());
|
let codec = DynamicCodec::new(method.clone());
|
||||||
client.ready().await.unwrap();
|
client.ready().await.unwrap();
|
||||||
Ok(client
|
client
|
||||||
.client_streaming(req, path, codec)
|
.client_streaming(req, path, codec)
|
||||||
.await
|
.await
|
||||||
.map_err(|s| s.to_string())?
|
.map_err(|s| s.to_string())
|
||||||
.into_inner())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn server_streaming(
|
pub async fn server_streaming(
|
||||||
@@ -230,54 +202,6 @@ impl GrpcHandle {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn server_streaming(
|
|
||||||
&mut self,
|
|
||||||
id: &str,
|
|
||||||
uri: Uri,
|
|
||||||
proto_files: Vec<PathBuf>,
|
|
||||||
service: &str,
|
|
||||||
method: &str,
|
|
||||||
message: &str,
|
|
||||||
metadata: HashMap<String, String>,
|
|
||||||
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
|
|
||||||
self.connect(id, uri, proto_files)
|
|
||||||
.await?
|
|
||||||
.server_streaming(service, method, message, metadata)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn client_streaming(
|
|
||||||
&mut self,
|
|
||||||
id: &str,
|
|
||||||
uri: Uri,
|
|
||||||
proto_files: Vec<PathBuf>,
|
|
||||||
service: &str,
|
|
||||||
method: &str,
|
|
||||||
stream: ReceiverStream<String>,
|
|
||||||
metadata: HashMap<String, String>,
|
|
||||||
) -> Result<DynamicMessage, String> {
|
|
||||||
self.connect(id, uri, proto_files)
|
|
||||||
.await?
|
|
||||||
.client_streaming(service, method, stream, metadata)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn streaming(
|
|
||||||
&mut self,
|
|
||||||
id: &str,
|
|
||||||
uri: Uri,
|
|
||||||
proto_files: Vec<PathBuf>,
|
|
||||||
service: &str,
|
|
||||||
method: &str,
|
|
||||||
stream: ReceiverStream<String>,
|
|
||||||
metadata: HashMap<String, String>,
|
|
||||||
) -> Result<Result<Response<Streaming<DynamicMessage>>, Status>, String> {
|
|
||||||
self.connect(id, uri, proto_files)
|
|
||||||
.await?
|
|
||||||
.streaming(service, method, stream, metadata)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect(
|
pub async fn connect(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
|||||||
@@ -1,63 +1,67 @@
|
|||||||
CREATE TABLE grpc_requests
|
CREATE TABLE grpc_requests
|
||||||
(
|
(
|
||||||
id TEXT NOT NULL
|
id TEXT NOT NULL
|
||||||
PRIMARY KEY,
|
PRIMARY KEY,
|
||||||
model TEXT DEFAULT 'grpc_request' NOT NULL,
|
model TEXT DEFAULT 'grpc_request' NOT NULL,
|
||||||
workspace_id TEXT NOT NULL
|
workspace_id TEXT NOT NULL
|
||||||
REFERENCES workspaces
|
REFERENCES workspaces
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
folder_id TEXT NULL
|
folder_id TEXT NULL
|
||||||
REFERENCES folders
|
REFERENCES folders
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
created_at DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
updated_at DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
sort_priority REAL NOT NULL,
|
sort_priority REAL NOT NULL,
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
service TEXT NULL,
|
service TEXT NULL,
|
||||||
method TEXT NULL,
|
method TEXT NULL,
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
proto_files TEXT DEFAULT '[]' NOT NULL,
|
proto_files TEXT DEFAULT '[]' NOT NULL,
|
||||||
authentication TEXT DEFAULT '{}' NOT NULL,
|
authentication TEXT DEFAULT '{}' NOT NULL,
|
||||||
authentication_type TEXT NULL,
|
authentication_type TEXT NULL,
|
||||||
metadata TEXT DEFAULT '[]' NOT NULL
|
metadata TEXT DEFAULT '[]' NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE grpc_connections
|
CREATE TABLE grpc_connections
|
||||||
(
|
(
|
||||||
id TEXT NOT NULL
|
id TEXT NOT NULL
|
||||||
PRIMARY KEY,
|
PRIMARY KEY,
|
||||||
model TEXT DEFAULT 'grpc_connection' NOT NULL,
|
model TEXT DEFAULT 'grpc_connection' NOT NULL,
|
||||||
workspace_id TEXT NOT NULL
|
workspace_id TEXT NOT NULL
|
||||||
REFERENCES workspaces
|
REFERENCES workspaces
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
request_id TEXT NOT NULL
|
request_id TEXT NOT NULL
|
||||||
REFERENCES grpc_requests
|
REFERENCES grpc_requests
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
created_at DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
updated_at DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||||
service TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
method TEXT NOT NULL,
|
service TEXT NOT NULL,
|
||||||
elapsed INTEGER NOT NULL
|
method TEXT NOT NULL,
|
||||||
|
status INTEGER DEFAULT -1 NOT NULL,
|
||||||
|
error TEXT NULL,
|
||||||
|
elapsed INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
trailers TEXT DEFAULT '{}' NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE grpc_messages
|
CREATE TABLE grpc_events
|
||||||
(
|
(
|
||||||
id TEXT NOT NULL
|
id TEXT NOT NULL
|
||||||
PRIMARY KEY,
|
PRIMARY KEY,
|
||||||
model TEXT DEFAULT 'grpc_message' NOT NULL,
|
model TEXT DEFAULT 'grpc_event' NOT NULL,
|
||||||
workspace_id TEXT NOT NULL
|
workspace_id TEXT NOT NULL
|
||||||
REFERENCES workspaces
|
REFERENCES workspaces
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
request_id TEXT NOT NULL
|
request_id TEXT NOT NULL
|
||||||
REFERENCES grpc_requests
|
REFERENCES grpc_requests
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
connection_id TEXT NOT NULL
|
connection_id TEXT NOT NULL
|
||||||
REFERENCES grpc_connections
|
REFERENCES grpc_connections
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
created_at DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
updated_at DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
|
||||||
is_server BOOLEAN NOT NULL,
|
metadata TEXT DEFAULT '{}' NOT NULL,
|
||||||
is_info BOOLEAN NOT NULL,
|
event_type TEXT NOT NULL,
|
||||||
message TEXT NOT NULL
|
content TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub enum AnalyticsResource {
|
|||||||
Environment,
|
Environment,
|
||||||
Folder,
|
Folder,
|
||||||
GrpcConnection,
|
GrpcConnection,
|
||||||
GrpcMessage,
|
GrpcEvent,
|
||||||
GrpcRequest,
|
GrpcRequest,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
@@ -33,7 +33,7 @@ impl AnalyticsResource {
|
|||||||
"Environment" => Some(AnalyticsResource::Environment),
|
"Environment" => Some(AnalyticsResource::Environment),
|
||||||
"Folder" => Some(AnalyticsResource::Folder),
|
"Folder" => Some(AnalyticsResource::Folder),
|
||||||
"GrpcConnection" => Some(AnalyticsResource::GrpcConnection),
|
"GrpcConnection" => Some(AnalyticsResource::GrpcConnection),
|
||||||
"GrpcMessage" => Some(AnalyticsResource::GrpcMessage),
|
"GrpcEvent" => Some(AnalyticsResource::GrpcEvent),
|
||||||
"GrpcRequest" => Some(AnalyticsResource::GrpcRequest),
|
"GrpcRequest" => Some(AnalyticsResource::GrpcRequest),
|
||||||
"HttpRequest" => Some(AnalyticsResource::HttpRequest),
|
"HttpRequest" => Some(AnalyticsResource::HttpRequest),
|
||||||
"HttpResponse" => Some(AnalyticsResource::HttpResponse),
|
"HttpResponse" => Some(AnalyticsResource::HttpResponse),
|
||||||
@@ -96,7 +96,7 @@ fn resource_name(resource: AnalyticsResource) -> &'static str {
|
|||||||
AnalyticsResource::Folder => "folder",
|
AnalyticsResource::Folder => "folder",
|
||||||
AnalyticsResource::GrpcRequest => "grpc_request",
|
AnalyticsResource::GrpcRequest => "grpc_request",
|
||||||
AnalyticsResource::GrpcConnection => "grpc_connection",
|
AnalyticsResource::GrpcConnection => "grpc_connection",
|
||||||
AnalyticsResource::GrpcMessage => "grpc_message",
|
AnalyticsResource::GrpcEvent => "grpc_event",
|
||||||
AnalyticsResource::HttpRequest => "http_request",
|
AnalyticsResource::HttpRequest => "http_request",
|
||||||
AnalyticsResource::HttpResponse => "http_response",
|
AnalyticsResource::HttpResponse => "http_response",
|
||||||
AnalyticsResource::KeyValue => "key_value",
|
AnalyticsResource::KeyValue => "key_value",
|
||||||
|
|||||||
16
src-tauri/src/grpc.rs
Normal file
16
src-tauri/src/grpc.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use KeyAndValueRef::{Ascii, Binary};
|
||||||
|
|
||||||
|
use grpc::{KeyAndValueRef, MetadataMap};
|
||||||
|
|
||||||
|
pub fn metadata_to_map(metadata: MetadataMap) -> HashMap<String, String> {
|
||||||
|
let mut entries = HashMap::new();
|
||||||
|
for r in metadata.iter() {
|
||||||
|
match r {
|
||||||
|
Ascii(k, v) => entries.insert(k.to_string(), v.to_str().unwrap().to_string()),
|
||||||
|
Binary(k, v) => entries.insert(k.to_string(), format!("{:?}", v)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
}
|
||||||
@@ -35,31 +35,33 @@ use tokio::sync::Mutex;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use window_shadows::set_shadow;
|
use window_shadows::set_shadow;
|
||||||
|
|
||||||
use grpc::manager::GrpcHandle;
|
use ::grpc::manager::{DynamicMessage, GrpcHandle};
|
||||||
use grpc::{serialize_message, ServiceDefinition};
|
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
|
||||||
use window_ext::TrafficLightWindowExt;
|
use window_ext::TrafficLightWindowExt;
|
||||||
|
|
||||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||||
|
use crate::grpc::metadata_to_map;
|
||||||
use crate::http::send_http_request;
|
use crate::http::send_http_request;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
|
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
|
||||||
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
|
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
|
||||||
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
|
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
|
||||||
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
|
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
|
||||||
get_cookie_jar, get_environment, get_folder, get_grpc_request, get_http_request,
|
get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request,
|
||||||
get_http_response, get_key_value_raw, get_or_create_settings, get_workspace,
|
get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace,
|
||||||
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
|
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
|
||||||
list_grpc_connections, list_grpc_messages, list_grpc_requests, list_requests, list_responses,
|
list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses,
|
||||||
list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
|
list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
|
||||||
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_message,
|
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
|
||||||
upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment,
|
upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment,
|
||||||
EnvironmentVariable, Folder, GrpcConnection, GrpcMessage, GrpcRequest, HttpRequest,
|
EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest,
|
||||||
HttpResponse, KeyValue, Settings, Workspace,
|
HttpRequest, HttpResponse, KeyValue, Settings, Workspace,
|
||||||
};
|
};
|
||||||
use crate::plugin::{ImportResources, ImportResult};
|
use crate::plugin::{ImportResources, ImportResult};
|
||||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||||
|
|
||||||
mod analytics;
|
mod analytics;
|
||||||
|
mod grpc;
|
||||||
mod http;
|
mod http;
|
||||||
mod models;
|
mod models;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
@@ -143,137 +145,6 @@ async fn cmd_grpc_go(
|
|||||||
let workspace = get_workspace(&w, &req.workspace_id)
|
let workspace = get_workspace(&w, &req.workspace_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let conn = {
|
|
||||||
let req = req.clone();
|
|
||||||
upsert_grpc_connection(
|
|
||||||
&w,
|
|
||||||
&GrpcConnection {
|
|
||||||
workspace_id: req.workspace_id,
|
|
||||||
request_id: req.id,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
};
|
|
||||||
|
|
||||||
let base_msg = GrpcMessage {
|
|
||||||
workspace_id: req.clone().workspace_id,
|
|
||||||
request_id: req.clone().id,
|
|
||||||
connection_id: conn.clone().id,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
|
||||||
&GrpcMessage {
|
|
||||||
message: "Initiating connection".to_string(),
|
|
||||||
is_info: true,
|
|
||||||
..base_msg.clone()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to upsert message");
|
|
||||||
|
|
||||||
let (in_msg_tx, in_msg_rx) = tauri::async_runtime::channel::<String>(16);
|
|
||||||
let maybe_in_msg_tx = std::sync::Mutex::new(Some(in_msg_tx.clone()));
|
|
||||||
let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false);
|
|
||||||
|
|
||||||
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let in_msg_stream = tokio_stream::wrappers::ReceiverStream::new(in_msg_rx);
|
|
||||||
|
|
||||||
let (service, method) = {
|
|
||||||
let req = req.clone();
|
|
||||||
match (req.service, req.method) {
|
|
||||||
(Some(service), Some(method)) => (service, method),
|
|
||||||
_ => return Err("Service and method are required".to_string()),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let connection = grpc_handle
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.connect(
|
|
||||||
&req.clone().id,
|
|
||||||
uri,
|
|
||||||
req.proto_files
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.map(|p| PathBuf::from_str(p).unwrap())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let method_desc = connection
|
|
||||||
.method(&service, &method)
|
|
||||||
.expect("Service not found");
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
enum IncomingMsg {
|
|
||||||
Message(String),
|
|
||||||
Cancel,
|
|
||||||
Commit,
|
|
||||||
}
|
|
||||||
|
|
||||||
let cb = {
|
|
||||||
let cancelled_rx = cancelled_rx.clone();
|
|
||||||
let environment = environment.clone();
|
|
||||||
let workspace = workspace.clone();
|
|
||||||
let w = w.clone();
|
|
||||||
let base_msg = base_msg.clone();
|
|
||||||
|
|
||||||
move |ev: tauri::Event| {
|
|
||||||
if *cancelled_rx.borrow() {
|
|
||||||
// Stream is cancelled
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut maybe_in_msg_tx = maybe_in_msg_tx
|
|
||||||
.lock()
|
|
||||||
.expect("previous holder not to panic");
|
|
||||||
let in_msg_tx = if let Some(in_msg_tx) = maybe_in_msg_tx.as_ref() {
|
|
||||||
in_msg_tx
|
|
||||||
} else {
|
|
||||||
// This would mean that the stream is already committed because
|
|
||||||
// we have already dropped the sending half
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match serde_json::from_str::<IncomingMsg>(ev.payload().unwrap()) {
|
|
||||||
Ok(IncomingMsg::Message(raw_msg)) => {
|
|
||||||
in_msg_tx.try_send(raw_msg.clone()).unwrap();
|
|
||||||
let w = w.clone();
|
|
||||||
let base_msg = base_msg.clone();
|
|
||||||
let environment_ref = environment.as_ref();
|
|
||||||
let msg = render::render(raw_msg.as_str(), &workspace, environment_ref);
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
|
||||||
&GrpcMessage {
|
|
||||||
message: msg,
|
|
||||||
..base_msg.clone()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(IncomingMsg::Commit) => {
|
|
||||||
maybe_in_msg_tx.take();
|
|
||||||
}
|
|
||||||
Ok(IncomingMsg::Cancel) => {
|
|
||||||
cancelled_tx.send_replace(true);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to parse gRPC message: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let event_handler = w.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
|
|
||||||
let mut metadata = HashMap::new();
|
let mut metadata = HashMap::new();
|
||||||
|
|
||||||
// Add rest of metadata
|
// Add rest of metadata
|
||||||
@@ -322,7 +193,150 @@ async fn cmd_grpc_go(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("METADATA: {:?}", metadata);
|
let conn = {
|
||||||
|
let req = req.clone();
|
||||||
|
upsert_grpc_connection(
|
||||||
|
&w,
|
||||||
|
&GrpcConnection {
|
||||||
|
workspace_id: req.workspace_id,
|
||||||
|
request_id: req.id,
|
||||||
|
status: -1,
|
||||||
|
url: req.url.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
|
let conn_id = conn.id.clone();
|
||||||
|
|
||||||
|
let base_msg = GrpcEvent {
|
||||||
|
workspace_id: req.clone().workspace_id,
|
||||||
|
request_id: req.clone().id,
|
||||||
|
connection_id: conn.clone().id,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (in_msg_tx, in_msg_rx) = tauri::async_runtime::channel::<DynamicMessage>(16);
|
||||||
|
let maybe_in_msg_tx = std::sync::Mutex::new(Some(in_msg_tx.clone()));
|
||||||
|
let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false);
|
||||||
|
|
||||||
|
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let in_msg_stream = tokio_stream::wrappers::ReceiverStream::new(in_msg_rx);
|
||||||
|
|
||||||
|
let (service, method) = {
|
||||||
|
let req = req.clone();
|
||||||
|
match (req.service, req.method) {
|
||||||
|
(Some(service), Some(method)) => (service, method),
|
||||||
|
_ => return Err("Service and method are required".to_string()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let connection = grpc_handle
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.connect(
|
||||||
|
&req.clone().id,
|
||||||
|
uri,
|
||||||
|
req.proto_files
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|p| PathBuf::from_str(p).unwrap())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let method_desc = connection
|
||||||
|
.method(&service, &method)
|
||||||
|
.expect("Service not found");
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
enum IncomingMsg {
|
||||||
|
Message(String),
|
||||||
|
Cancel,
|
||||||
|
Commit,
|
||||||
|
}
|
||||||
|
|
||||||
|
let cb = {
|
||||||
|
let cancelled_rx = cancelled_rx.clone();
|
||||||
|
let environment = environment.clone();
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
let w = w.clone();
|
||||||
|
let base_msg = base_msg.clone();
|
||||||
|
let method_desc = method_desc.clone();
|
||||||
|
|
||||||
|
move |ev: tauri::Event| {
|
||||||
|
if *cancelled_rx.borrow() {
|
||||||
|
// Stream is cancelled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut maybe_in_msg_tx = maybe_in_msg_tx
|
||||||
|
.lock()
|
||||||
|
.expect("previous holder not to panic");
|
||||||
|
let in_msg_tx = if let Some(in_msg_tx) = maybe_in_msg_tx.as_ref() {
|
||||||
|
in_msg_tx
|
||||||
|
} else {
|
||||||
|
// This would mean that the stream is already committed because
|
||||||
|
// we have already dropped the sending half
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match serde_json::from_str::<IncomingMsg>(ev.payload().unwrap()) {
|
||||||
|
Ok(IncomingMsg::Message(raw_msg)) => {
|
||||||
|
let w = w.clone();
|
||||||
|
let base_msg = base_msg.clone();
|
||||||
|
let environment_ref = environment.as_ref();
|
||||||
|
let method_desc = method_desc.clone();
|
||||||
|
let msg = render::render(raw_msg.as_str(), &workspace, environment_ref);
|
||||||
|
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
|
||||||
|
{
|
||||||
|
Ok(d_msg) => d_msg,
|
||||||
|
Err(e) => {
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
upsert_grpc_event(
|
||||||
|
&w,
|
||||||
|
&GrpcEvent {
|
||||||
|
event_type: GrpcEventType::Error,
|
||||||
|
content: e.to_string(),
|
||||||
|
..base_msg.clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
in_msg_tx.try_send(d_msg).unwrap();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
upsert_grpc_event(
|
||||||
|
&w,
|
||||||
|
&GrpcEvent {
|
||||||
|
content: msg,
|
||||||
|
event_type: GrpcEventType::ClientMessage,
|
||||||
|
..base_msg.clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(IncomingMsg::Commit) => {
|
||||||
|
maybe_in_msg_tx.take();
|
||||||
|
}
|
||||||
|
Ok(IncomingMsg::Cancel) => {
|
||||||
|
cancelled_tx.send_replace(true);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to parse gRPC message: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let event_handler = w.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
|
||||||
|
|
||||||
let grpc_listen = {
|
let grpc_listen = {
|
||||||
let w = w.clone();
|
let w = w.clone();
|
||||||
@@ -330,7 +344,26 @@ async fn cmd_grpc_go(
|
|||||||
let req = req.clone();
|
let req = req.clone();
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
let environment = environment.clone();
|
let environment = environment.clone();
|
||||||
let msg = render::render(&req.message, &workspace, environment.as_ref());
|
let raw_msg = if req.message.is_empty() {
|
||||||
|
"{}".to_string()
|
||||||
|
} else {
|
||||||
|
req.message
|
||||||
|
};
|
||||||
|
let msg = render::render(&raw_msg, &workspace, environment.as_ref());
|
||||||
|
let conn_id = conn_id.clone();
|
||||||
|
|
||||||
|
upsert_grpc_event(
|
||||||
|
&w,
|
||||||
|
&GrpcEvent {
|
||||||
|
content: format!("Connecting to {}", req.url),
|
||||||
|
event_type: GrpcEventType::Info,
|
||||||
|
metadata: Json(metadata.clone()),
|
||||||
|
..base_msg.clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let (maybe_stream, maybe_msg) = match (
|
let (maybe_stream, maybe_msg) = match (
|
||||||
method_desc.is_client_streaming(),
|
method_desc.is_client_streaming(),
|
||||||
@@ -366,54 +399,104 @@ async fn cmd_grpc_go(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !method_desc.is_client_streaming() {
|
||||||
|
upsert_grpc_event(
|
||||||
|
&w,
|
||||||
|
&GrpcEvent {
|
||||||
|
event_type: GrpcEventType::ClientMessage,
|
||||||
|
content: msg,
|
||||||
|
..base_msg.clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
match maybe_msg {
|
match maybe_msg {
|
||||||
Some(Ok(msg)) => {
|
Some(Ok(msg)) => {
|
||||||
println!("Message: {:?}", msg);
|
upsert_grpc_event(
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
&w,
|
||||||
&GrpcMessage {
|
&GrpcEvent {
|
||||||
message: serialize_message(&msg).unwrap(),
|
metadata: Json(metadata_to_map(msg.metadata().clone())),
|
||||||
is_server: true,
|
content: if msg.metadata().len() == 0 {
|
||||||
|
"Connection established"
|
||||||
|
} else {
|
||||||
|
"Received metadata"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
event_type: GrpcEventType::Info,
|
||||||
..base_msg.clone()
|
..base_msg.clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
upsert_grpc_event(
|
||||||
|
&w,
|
||||||
|
&GrpcEvent {
|
||||||
|
content: serialize_message(&msg.into_inner()).unwrap(),
|
||||||
|
event_type: GrpcEventType::ServerMessage,
|
||||||
|
..base_msg.clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
upsert_grpc_connection(
|
||||||
|
&w,
|
||||||
|
&GrpcConnection {
|
||||||
|
elapsed: start.elapsed().as_millis() as i64,
|
||||||
|
status: Code::Ok as i64,
|
||||||
|
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
Some(Err(e)) => {
|
Some(Err(e)) => {
|
||||||
// TODO: Make into error
|
upsert_grpc_connection(
|
||||||
println!("Error connecting: {:?}", e);
|
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
&w,
|
||||||
&GrpcMessage {
|
&GrpcConnection {
|
||||||
message: e.to_string(),
|
error: Some(e.to_string()),
|
||||||
is_server: true,
|
elapsed: start.elapsed().as_millis() as i64,
|
||||||
is_info: true,
|
status: Code::Unknown as i64,
|
||||||
..base_msg.clone()
|
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
None => {}
|
None => {
|
||||||
|
// Server streaming doesn't return initial message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stream = match maybe_stream {
|
let mut stream = match maybe_stream {
|
||||||
Some(Ok(Ok(s))) => {
|
Some(Ok(Ok(s))) => {
|
||||||
// TODO: Store metadata on... connection? Or in a message
|
upsert_grpc_event(
|
||||||
println!("METADATA: {:?}", s.metadata());
|
&w,
|
||||||
|
&GrpcEvent {
|
||||||
|
metadata: Json(metadata_to_map(s.metadata().clone())),
|
||||||
|
content: if s.metadata().len() == 0 {
|
||||||
|
"Connection established"
|
||||||
|
} else {
|
||||||
|
"Received metadata"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
event_type: GrpcEventType::Info,
|
||||||
|
..base_msg.clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
s.into_inner()
|
s.into_inner()
|
||||||
}
|
}
|
||||||
Some(Ok(Err(e))) => {
|
Some(Ok(Err(e))) => {
|
||||||
// TODO: Make into error, and use status
|
upsert_grpc_connection(
|
||||||
println!("Connection status error: {:?}", e);
|
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
&w,
|
||||||
&GrpcMessage {
|
&GrpcConnection {
|
||||||
message: e.message().to_string(),
|
error: Some(e.message().to_string()),
|
||||||
is_server: true,
|
status: e.code() as i64,
|
||||||
is_info: true,
|
elapsed: start.elapsed().as_millis() as i64,
|
||||||
..base_msg.clone()
|
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -421,15 +504,13 @@ async fn cmd_grpc_go(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Some(Err(e)) => {
|
Some(Err(e)) => {
|
||||||
// TODO: Make into error
|
upsert_grpc_connection(
|
||||||
println!("Generic error: {:?}", e);
|
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
&w,
|
||||||
&GrpcMessage {
|
&GrpcConnection {
|
||||||
message: e.to_string(),
|
error: Some(e),
|
||||||
is_server: true,
|
status: Code::Unknown as i64,
|
||||||
is_info: true,
|
elapsed: start.elapsed().as_millis() as i64,
|
||||||
..base_msg.clone()
|
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -443,11 +524,11 @@ async fn cmd_grpc_go(
|
|||||||
match stream.message().await {
|
match stream.message().await {
|
||||||
Ok(Some(msg)) => {
|
Ok(Some(msg)) => {
|
||||||
let message = serialize_message(&msg).unwrap();
|
let message = serialize_message(&msg).unwrap();
|
||||||
upsert_grpc_message(
|
upsert_grpc_event(
|
||||||
&w,
|
&w,
|
||||||
&GrpcMessage {
|
&GrpcEvent {
|
||||||
message,
|
content: message,
|
||||||
is_server: true,
|
event_type: GrpcEventType::ServerMessage,
|
||||||
..base_msg.clone()
|
..base_msg.clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -455,16 +536,18 @@ async fn cmd_grpc_go(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// TODO: Store trailers on connection
|
let trailers = stream
|
||||||
let trailers = stream.trailers().await.unwrap_or_default();
|
.trailers()
|
||||||
info!("gRPC stream closed by sender {:?}", trailers,);
|
.await
|
||||||
// TODO: Mark this on connection instead
|
.unwrap_or_default()
|
||||||
upsert_grpc_message(
|
.unwrap_or_default();
|
||||||
|
upsert_grpc_connection(
|
||||||
&w,
|
&w,
|
||||||
&GrpcMessage {
|
&GrpcConnection {
|
||||||
message: "Connection closed".to_string(),
|
elapsed: start.elapsed().as_millis() as i64,
|
||||||
is_info: true,
|
status: Code::Unavailable as i64,
|
||||||
..base_msg.clone()
|
trailers: Json(metadata_to_map(trailers)),
|
||||||
|
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -472,15 +555,13 @@ async fn cmd_grpc_go(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(status) => {
|
Err(status) => {
|
||||||
// TODO: Make into error
|
upsert_grpc_connection(
|
||||||
println!("Error status: {:?}", status);
|
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
&w,
|
||||||
&GrpcMessage {
|
&GrpcConnection {
|
||||||
message: status.message().to_string(),
|
elapsed: start.elapsed().as_millis() as i64,
|
||||||
is_server: true,
|
status: Code::Unavailable as i64,
|
||||||
is_info: true,
|
trailers: Json(metadata_to_map(status.metadata().clone())),
|
||||||
..base_msg.clone()
|
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -492,36 +573,31 @@ async fn cmd_grpc_go(
|
|||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let conn = conn.clone();
|
let conn_id = conn_id.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let w = w.clone();
|
let w = w.clone();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = grpc_listen => {
|
_ = grpc_listen => {
|
||||||
upsert_grpc_connection(
|
// upsert_grpc_connection(
|
||||||
&w,
|
// &w,
|
||||||
&GrpcConnection{
|
// &GrpcConnection{
|
||||||
elapsed: start.elapsed().as_millis() as i64,
|
// elapsed: start.elapsed().as_millis() as i64,
|
||||||
..conn
|
// status: Code::Ok as i64,
|
||||||
},
|
// ..conn
|
||||||
).await.unwrap();
|
// },
|
||||||
|
// ).await.unwrap();
|
||||||
},
|
},
|
||||||
_ = cancelled_rx.changed() => {
|
_ = cancelled_rx.changed() => {
|
||||||
upsert_grpc_message(
|
|
||||||
&w,
|
|
||||||
&GrpcMessage {
|
|
||||||
message: "Connection cancelled".to_string(),
|
|
||||||
is_info: true,
|
|
||||||
..base_msg.clone()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await.unwrap();
|
|
||||||
upsert_grpc_connection(
|
upsert_grpc_connection(
|
||||||
&w,
|
&w,
|
||||||
&GrpcConnection{
|
&GrpcConnection {
|
||||||
elapsed: start.elapsed().as_millis() as i64,
|
elapsed: start.elapsed().as_millis() as i64,
|
||||||
..conn
|
status: Code::Cancelled as i64,
|
||||||
|
..get_grpc_connection(&w, &conn_id).await.unwrap().clone()
|
||||||
},
|
},
|
||||||
).await.unwrap();
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
w.unlisten(event_handler);
|
w.unlisten(event_handler);
|
||||||
@@ -1037,11 +1113,8 @@ async fn cmd_list_grpc_connections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_list_grpc_messages(
|
async fn cmd_list_grpc_events(connection_id: &str, w: Window) -> Result<Vec<GrpcEvent>, String> {
|
||||||
connection_id: &str,
|
list_grpc_events(&w, connection_id)
|
||||||
w: Window,
|
|
||||||
) -> Result<Vec<GrpcMessage>, String> {
|
|
||||||
list_grpc_messages(&w, connection_id)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
@@ -1334,7 +1407,7 @@ fn main() {
|
|||||||
cmd_list_http_requests,
|
cmd_list_http_requests,
|
||||||
cmd_list_grpc_requests,
|
cmd_list_grpc_requests,
|
||||||
cmd_list_grpc_connections,
|
cmd_list_grpc_connections,
|
||||||
cmd_list_grpc_messages,
|
cmd_list_grpc_events,
|
||||||
cmd_list_http_responses,
|
cmd_list_http_responses,
|
||||||
cmd_list_workspaces,
|
cmd_list_workspaces,
|
||||||
cmd_new_window,
|
cmd_new_window,
|
||||||
|
|||||||
@@ -231,33 +231,41 @@ pub struct GrpcConnection {
|
|||||||
pub service: String,
|
pub service: String,
|
||||||
pub method: String,
|
pub method: String,
|
||||||
pub elapsed: i64,
|
pub elapsed: i64,
|
||||||
|
pub status: i64,
|
||||||
|
pub url: String,
|
||||||
|
pub error: Option<String>,
|
||||||
|
pub trailers: Json<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::Type, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[sqlx(rename_all = "snake_case")]
|
||||||
|
pub enum GrpcEventType {
|
||||||
|
Info,
|
||||||
|
Error,
|
||||||
|
ClientMessage,
|
||||||
|
ServerMessage,
|
||||||
|
ConnectionResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GrpcEventType {
|
||||||
|
fn default() -> Self {
|
||||||
|
GrpcEventType::Info
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct GrpcMessage {
|
pub struct GrpcEvent {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub request_id: String,
|
pub request_id: String,
|
||||||
pub connection_id: String,
|
pub connection_id: String,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub message: String,
|
pub content: String,
|
||||||
pub is_server: bool,
|
pub event_type: GrpcEventType,
|
||||||
pub is_info: bool,
|
pub metadata: Json<HashMap<String, String>>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
#[serde(default, rename_all = "camelCase")]
|
|
||||||
pub struct GrpcResponse {
|
|
||||||
pub id: String,
|
|
||||||
pub model: String,
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub grpc_endpoint_id: String,
|
|
||||||
pub grpc_connection_id: String,
|
|
||||||
pub request_id: String,
|
|
||||||
pub created_at: NaiveDateTime,
|
|
||||||
pub updated_at: NaiveDateTime,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
@@ -612,14 +620,19 @@ pub async fn upsert_grpc_connection(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO grpc_connections (
|
INSERT INTO grpc_connections (
|
||||||
id, workspace_id, request_id, service, method, elapsed
|
id, workspace_id, request_id, service, method, elapsed,
|
||||||
|
status, error, trailers, url
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
updated_at = CURRENT_TIMESTAMP,
|
updated_at = CURRENT_TIMESTAMP,
|
||||||
service = excluded.service,
|
service = excluded.service,
|
||||||
method = excluded.method,
|
method = excluded.method,
|
||||||
elapsed = excluded.elapsed
|
elapsed = excluded.elapsed,
|
||||||
|
status = excluded.status,
|
||||||
|
error = excluded.error,
|
||||||
|
trailers = excluded.trailers,
|
||||||
|
url = excluded.url
|
||||||
"#,
|
"#,
|
||||||
id,
|
id,
|
||||||
connection.workspace_id,
|
connection.workspace_id,
|
||||||
@@ -627,6 +640,10 @@ pub async fn upsert_grpc_connection(
|
|||||||
connection.service,
|
connection.service,
|
||||||
connection.method,
|
connection.method,
|
||||||
connection.elapsed,
|
connection.elapsed,
|
||||||
|
connection.status,
|
||||||
|
connection.error,
|
||||||
|
connection.trailers,
|
||||||
|
connection.url,
|
||||||
)
|
)
|
||||||
.execute(&db)
|
.execute(&db)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -647,7 +664,8 @@ pub async fn get_grpc_connection(
|
|||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
id, model, workspace_id, request_id, created_at, updated_at, service,
|
id, model, workspace_id, request_id, created_at, updated_at, service,
|
||||||
method, elapsed
|
method, elapsed, status, error, url,
|
||||||
|
trailers AS "trailers!: sqlx::types::Json<HashMap<String, String>>"
|
||||||
FROM grpc_connections
|
FROM grpc_connections
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
"#,
|
"#,
|
||||||
@@ -667,7 +685,8 @@ pub async fn list_grpc_connections(
|
|||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
id, model, workspace_id, request_id, created_at, updated_at, service,
|
id, model, workspace_id, request_id, created_at, updated_at, service,
|
||||||
method, elapsed
|
method, elapsed, status, error, url,
|
||||||
|
trailers AS "trailers!: sqlx::types::Json<HashMap<String, String>>"
|
||||||
FROM grpc_connections
|
FROM grpc_connections
|
||||||
WHERE request_id = ?
|
WHERE request_id = ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@@ -678,56 +697,57 @@ pub async fn list_grpc_connections(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upsert_grpc_message(
|
pub async fn upsert_grpc_event(
|
||||||
mgr: &impl Manager<Wry>,
|
mgr: &impl Manager<Wry>,
|
||||||
message: &GrpcMessage,
|
message: &GrpcEvent,
|
||||||
) -> Result<GrpcMessage, sqlx::Error> {
|
) -> Result<GrpcEvent, sqlx::Error> {
|
||||||
let db = get_db(mgr).await;
|
let db = get_db(mgr).await;
|
||||||
let id = match message.id.as_str() {
|
let id = match message.id.as_str() {
|
||||||
"" => generate_id(Some("gm")),
|
"" => generate_id(Some("ge")),
|
||||||
_ => message.id.to_string(),
|
_ => message.id.to_string(),
|
||||||
};
|
};
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO grpc_messages (
|
INSERT INTO grpc_events (
|
||||||
id, workspace_id, request_id, connection_id, message, is_server, is_info
|
id, workspace_id, request_id, connection_id, content, event_type, metadata
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
updated_at = CURRENT_TIMESTAMP,
|
updated_at = CURRENT_TIMESTAMP,
|
||||||
message = excluded.message,
|
content = excluded.content,
|
||||||
is_server = excluded.is_server,
|
event_type = excluded.event_type,
|
||||||
is_info = excluded.is_info
|
metadata = excluded.metadata
|
||||||
"#,
|
"#,
|
||||||
id,
|
id,
|
||||||
message.workspace_id,
|
message.workspace_id,
|
||||||
message.request_id,
|
message.request_id,
|
||||||
message.connection_id,
|
message.connection_id,
|
||||||
message.message,
|
message.content,
|
||||||
message.is_server,
|
message.event_type,
|
||||||
message.is_info,
|
message.metadata,
|
||||||
)
|
)
|
||||||
.execute(&db)
|
.execute(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match get_grpc_message(mgr, &id).await {
|
match get_grpc_event(mgr, &id).await {
|
||||||
Ok(m) => Ok(emit_upserted_model(mgr, m)),
|
Ok(m) => Ok(emit_upserted_model(mgr, m)),
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_grpc_message(
|
pub async fn get_grpc_event(
|
||||||
mgr: &impl Manager<Wry>,
|
mgr: &impl Manager<Wry>,
|
||||||
id: &str,
|
id: &str,
|
||||||
) -> Result<GrpcMessage, sqlx::Error> {
|
) -> Result<GrpcEvent, sqlx::Error> {
|
||||||
let db = get_db(mgr).await;
|
let db = get_db(mgr).await;
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
GrpcMessage,
|
GrpcEvent,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
id, model, workspace_id, request_id, connection_id, created_at, message,
|
id, model, workspace_id, request_id, connection_id, created_at, content,
|
||||||
is_server, is_info
|
event_type AS "event_type!: GrpcEventType",
|
||||||
FROM grpc_messages
|
metadata AS "metadata!: sqlx::types::Json<HashMap<String, String>>"
|
||||||
|
FROM grpc_events
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
"#,
|
"#,
|
||||||
id,
|
id,
|
||||||
@@ -736,18 +756,19 @@ pub async fn get_grpc_message(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_grpc_messages(
|
pub async fn list_grpc_events(
|
||||||
mgr: &impl Manager<Wry>,
|
mgr: &impl Manager<Wry>,
|
||||||
connection_id: &str,
|
connection_id: &str,
|
||||||
) -> Result<Vec<GrpcMessage>, sqlx::Error> {
|
) -> Result<Vec<GrpcEvent>, sqlx::Error> {
|
||||||
let db = get_db(mgr).await;
|
let db = get_db(mgr).await;
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
GrpcMessage,
|
GrpcEvent,
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
id, model, workspace_id, request_id, connection_id, created_at, message,
|
id, model, workspace_id, request_id, connection_id, created_at, content,
|
||||||
is_server, is_info
|
event_type AS "event_type!: GrpcEventType",
|
||||||
FROM grpc_messages
|
metadata AS "metadata!: sqlx::types::Json<HashMap<String, String>>"
|
||||||
|
FROM grpc_events
|
||||||
WHERE connection_id = ?
|
WHERE connection_id = ?
|
||||||
"#,
|
"#,
|
||||||
connection_id,
|
connection_id,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ interface Props {
|
|||||||
|
|
||||||
export function EmptyStateText({ children }: Props) {
|
export function EmptyStateText({ children }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-dashed border-highlight h-full text-gray-400 flex items-center justify-center">
|
<div className="rounded-lg border border-dashed border-highlight h-full py-2 text-gray-400 flex items-center justify-center">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||||
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
||||||
import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages';
|
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
|
||||||
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
||||||
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
|
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
|
||||||
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
|
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
|
||||||
@@ -53,8 +53,8 @@ export function GlobalHooks() {
|
|||||||
? httpResponsesQueryKey(payload)
|
? httpResponsesQueryKey(payload)
|
||||||
: payload.model === 'grpc_connection'
|
: payload.model === 'grpc_connection'
|
||||||
? grpcConnectionsQueryKey(payload)
|
? grpcConnectionsQueryKey(payload)
|
||||||
: payload.model === 'grpc_message'
|
: payload.model === 'grpc_event'
|
||||||
? grpcMessagesQueryKey(payload)
|
? grpcEventsQueryKey(payload)
|
||||||
: payload.model === 'grpc_request'
|
: payload.model === 'grpc_request'
|
||||||
? grpcRequestsQueryKey(payload)
|
? grpcRequestsQueryKey(payload)
|
||||||
: payload.model === 'workspace'
|
: payload.model === 'workspace'
|
||||||
@@ -107,8 +107,8 @@ export function GlobalHooks() {
|
|||||||
queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload));
|
queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload));
|
||||||
} else if (payload.model === 'grpc_connection') {
|
} else if (payload.model === 'grpc_connection') {
|
||||||
queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload));
|
queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload));
|
||||||
} else if (payload.model === 'grpc_message') {
|
} else if (payload.model === 'grpc_event') {
|
||||||
queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload));
|
queryClient.setQueryData(grpcEventsQueryKey(payload), removeById(payload));
|
||||||
} else if (payload.model === 'key_value') {
|
} else if (payload.model === 'key_value') {
|
||||||
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
||||||
} else if (payload.model === 'cookie_jar') {
|
} else if (payload.model === 'cookie_jar') {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useEffect, useMemo } from 'react';
|
|||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
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 { useGrpcEvents } from '../hooks/useGrpcEvents';
|
||||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||||
import { Banner } from './core/Banner';
|
import { Banner } from './core/Banner';
|
||||||
import { HotKeyList } from './core/HotKeyList';
|
import { HotKeyList } from './core/HotKeyList';
|
||||||
@@ -21,7 +21,7 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
|
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? 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 = useGrpcEvents(activeConnection?.id ?? null);
|
||||||
const grpc = useGrpc(activeRequest, activeConnection);
|
const grpc = useGrpc(activeRequest, activeConnection);
|
||||||
|
|
||||||
const services = grpc.reflect.data ?? null;
|
const services = grpc.reflect.data ?? null;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { format } from 'date-fns';
|
import { format, addMilliseconds } from 'date-fns';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||||
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
import { useGrpcEvents } from '../hooks/useGrpcEvents';
|
||||||
import type { GrpcRequest } from '../lib/models';
|
import type { GrpcEvent, GrpcRequest } from '../lib/models';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { JsonAttributeTree } from './core/JsonAttributeTree';
|
import { JsonAttributeTree } from './core/JsonAttributeTree';
|
||||||
|
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||||
import { Separator } from './core/Separator';
|
import { Separator } from './core/Separator';
|
||||||
import { SplitLayout } from './core/SplitLayout';
|
import { SplitLayout } from './core/SplitLayout';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
|
import { EmptyStateText } from './EmptyStateText';
|
||||||
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -25,34 +27,80 @@ interface Props {
|
|||||||
| 'no-method';
|
| 'no-method';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONNECTION_RESPONSE_EVENT_ID = 'connection_response';
|
||||||
|
|
||||||
export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) {
|
export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) {
|
||||||
const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
|
const [activeEventId, setActiveEventId] = 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 ogEvents = useGrpcEvents(activeConnection?.id ?? null);
|
||||||
|
|
||||||
const activeMessage = useMemo(
|
const events = useMemo(() => {
|
||||||
() => messages.find((m) => m.id === activeMessageId) ?? null,
|
const createdAt =
|
||||||
[activeMessageId, messages],
|
activeConnection != null &&
|
||||||
|
addMilliseconds(activeConnection.createdAt, activeConnection.elapsed)
|
||||||
|
.toISOString()
|
||||||
|
.replace('Z', '');
|
||||||
|
if (activeConnection == null || activeConnection.elapsed === 0) {
|
||||||
|
return ogEvents;
|
||||||
|
} else if (activeConnection.error != null) {
|
||||||
|
return [
|
||||||
|
...ogEvents,
|
||||||
|
{
|
||||||
|
id: CONNECTION_RESPONSE_EVENT_ID,
|
||||||
|
eventType: 'error',
|
||||||
|
content: activeConnection.error,
|
||||||
|
metadata: activeConnection.trailers,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt,
|
||||||
|
} as GrpcEvent,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
...ogEvents,
|
||||||
|
{
|
||||||
|
id: CONNECTION_RESPONSE_EVENT_ID,
|
||||||
|
eventType: activeConnection.status === 0 ? 'connection_response' : 'error',
|
||||||
|
content: `Connection ${GRPC_CODES[activeConnection.status] ?? 'closed'}`,
|
||||||
|
metadata: activeConnection.trailers,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt,
|
||||||
|
} as GrpcEvent,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}, [activeConnection, ogEvents]);
|
||||||
|
|
||||||
|
const activeEvent = useMemo(
|
||||||
|
() => events.find((m) => m.id === activeEventId) ?? null,
|
||||||
|
[activeEventId, events],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set active message to the first message received if unary
|
||||||
|
useEffect(() => {
|
||||||
|
if (events.length === 0 || activeEvent != null || methodType !== 'unary') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActiveEventId(events.find((m) => m.eventType === 'server_message')?.id ?? null);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [events.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitLayout
|
<SplitLayout
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
style={style}
|
style={style}
|
||||||
name={methodType === 'unary' ? 'grpc_messages_unary' : 'grpc_messages_streaming'}
|
name="grpc_events"
|
||||||
defaultRatio={methodType === 'unary' ? 0.75 : 0.3}
|
defaultRatio={0.4}
|
||||||
minHeightPx={20}
|
minHeightPx={20}
|
||||||
firstSlot={() => (
|
firstSlot={() =>
|
||||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
|
activeConnection && (
|
||||||
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
|
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
|
||||||
<HStack alignItems="center" space={2}>
|
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
|
||||||
<span>{messages.filter((m) => !m.isInfo).length} messages</span>
|
<HStack alignItems="center" space={2}>
|
||||||
{activeConnection?.elapsed === 0 && (
|
<span>{events.length} messages</span>
|
||||||
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
|
{activeConnection.elapsed === 0 && (
|
||||||
)}
|
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
|
||||||
</HStack>
|
)}
|
||||||
{activeConnection && (
|
</HStack>
|
||||||
<RecentConnectionsDropdown
|
<RecentConnectionsDropdown
|
||||||
connections={connections}
|
connections={connections}
|
||||||
activeConnection={activeConnection}
|
activeConnection={activeConnection}
|
||||||
@@ -60,63 +108,59 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
|||||||
// todo
|
// todo
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</HStack>
|
||||||
</HStack>
|
<div className="overflow-y-auto h-full">
|
||||||
<div className="overflow-y-auto h-full">
|
{...events.map((m) => (
|
||||||
{...messages.map((m) => (
|
<MessageRow
|
||||||
<HStack
|
key={m.id}
|
||||||
role="button"
|
isActive={m.id === activeEventId}
|
||||||
key={m.id}
|
eventType={m.eventType}
|
||||||
space={2}
|
timestamp={m.createdAt}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (m.id === activeMessageId) setActiveMessageId(null);
|
if (m.id === activeEventId) setActiveEventId(null);
|
||||||
else setActiveMessageId(m.id);
|
else setActiveEventId(m.id);
|
||||||
}}
|
}}
|
||||||
alignItems="center"
|
|
||||||
className={classNames(
|
|
||||||
'px-2 py-1 font-mono cursor-default group',
|
|
||||||
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={classNames(
|
|
||||||
'w-full truncate text-gray-800 text-2xs group-hover:text-gray-900',
|
|
||||||
m.id === activeMessageId && 'text-gray-900',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{m.message}
|
{m.content}
|
||||||
</div>
|
</MessageRow>
|
||||||
<div
|
))}
|
||||||
className={classNames(
|
</div>
|
||||||
'text-gray-600 text-2xs group-hover:text-gray-700',
|
|
||||||
m.id === activeMessageId && 'text-gray-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{format(m.createdAt, 'HH:mm:ss')}
|
|
||||||
</div>
|
|
||||||
</HStack>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
secondSlot={
|
secondSlot={
|
||||||
activeMessage &&
|
activeEvent &&
|
||||||
(() => (
|
(() => (
|
||||||
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
<div className="pb-3 px-2">
|
<div className="pb-3 px-2">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-2 overflow-y-auto">
|
<div className="pl-2 overflow-y-auto">
|
||||||
{activeMessage.isInfo ? (
|
{activeEvent.eventType === 'client_message' ||
|
||||||
<span>{activeMessage.message}</span>
|
activeEvent.eventType === 'server_message' ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-2 select-text cursor-text font-semibold">
|
||||||
|
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
|
||||||
|
</div>
|
||||||
|
<JsonAttributeTree attrValue={JSON.parse(activeEvent?.content ?? '{}')} />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<JsonAttributeTree attrValue={JSON.parse(activeMessage?.message ?? '{}')} />
|
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
|
<div className="mb-2 select-text cursor-text font-semibold">
|
||||||
|
{activeEvent.content}
|
||||||
|
</div>
|
||||||
|
{Object.keys(activeEvent.metadata).length === 0 ? (
|
||||||
|
<EmptyStateText>
|
||||||
|
No {activeEvent.eventType === 'connection_response' ? 'trailers' : 'metadata'}
|
||||||
|
</EmptyStateText>
|
||||||
|
) : (
|
||||||
|
<KeyValueRows>
|
||||||
|
{Object.entries(activeEvent.metadata).map(([key, value]) => (
|
||||||
|
<KeyValueRow key={key} label={key} value={value} />
|
||||||
|
))}
|
||||||
|
</KeyValueRows>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,3 +169,89 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MessageRow({
|
||||||
|
onClick,
|
||||||
|
isActive,
|
||||||
|
eventType,
|
||||||
|
children,
|
||||||
|
timestamp,
|
||||||
|
}: {
|
||||||
|
onClick?: () => void;
|
||||||
|
isActive?: boolean;
|
||||||
|
eventType: GrpcEvent['eventType'];
|
||||||
|
children: ReactNode;
|
||||||
|
timestamp: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames(
|
||||||
|
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
|
||||||
|
'px-1 py-1 font-mono cursor-default group focus:outline-none',
|
||||||
|
isActive && '!bg-highlight text-gray-900',
|
||||||
|
'text-gray-800 hover:text-gray-900',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={
|
||||||
|
eventType === 'server_message'
|
||||||
|
? 'text-blue-600'
|
||||||
|
: eventType === 'client_message'
|
||||||
|
? 'text-violet-600'
|
||||||
|
: eventType === 'error'
|
||||||
|
? 'text-orange-600'
|
||||||
|
: eventType === 'connection_response'
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-gray-700'
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
eventType === 'server_message'
|
||||||
|
? 'Server message'
|
||||||
|
: eventType === 'client_message'
|
||||||
|
? 'Client message'
|
||||||
|
: eventType === 'error'
|
||||||
|
? 'Error'
|
||||||
|
: eventType === 'connection_response'
|
||||||
|
? 'Connection response'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
eventType === 'server_message'
|
||||||
|
? 'arrowBigDownDash'
|
||||||
|
: eventType === 'client_message'
|
||||||
|
? 'arrowBigUpDash'
|
||||||
|
: eventType === 'error'
|
||||||
|
? 'alert'
|
||||||
|
: eventType === 'connection_response'
|
||||||
|
? 'check'
|
||||||
|
: 'info'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className={classNames('w-full truncate text-2xs')}>{children}</div>
|
||||||
|
<div className={classNames('opacity-50 text-2xs')}>
|
||||||
|
{format(timestamp + 'Z', 'HH:mm:ss.SSS')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GRPC_CODES: Record<number, string> = {
|
||||||
|
0: 'Ok',
|
||||||
|
1: 'Cancelled',
|
||||||
|
2: 'Unknown',
|
||||||
|
3: 'Invalid argument',
|
||||||
|
4: 'Deadline exceeded',
|
||||||
|
5: 'Not found',
|
||||||
|
6: 'Already exists',
|
||||||
|
7: 'Permission denied',
|
||||||
|
8: 'Resource exhausted',
|
||||||
|
9: 'Failed precondition',
|
||||||
|
10: 'Aborted',
|
||||||
|
11: 'Out of range',
|
||||||
|
12: 'Unimplemented',
|
||||||
|
13: 'Internal',
|
||||||
|
14: 'Unavailable',
|
||||||
|
15: 'Data loss',
|
||||||
|
16: 'Unauthenticated',
|
||||||
|
};
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export function GrpcConnectionSetupPane({
|
|||||||
className="border border-highlight"
|
className="border border-highlight"
|
||||||
size="sm"
|
size="sm"
|
||||||
title={methodType === 'unary' ? 'Send' : 'Connect'}
|
title={methodType === 'unary' ? 'Send' : 'Connect'}
|
||||||
hotkeyAction={isStreaming ? undefined : 'http_request.send'}
|
hotkeyAction="grpc_request.send"
|
||||||
onClick={handleConnect}
|
onClick={handleConnect}
|
||||||
disabled={methodType === 'no-schema' || methodType === 'no-method'}
|
disabled={methodType === 'no-schema' || methodType === 'no-method'}
|
||||||
icon={
|
icon={
|
||||||
@@ -240,24 +240,24 @@ export function GrpcConnectionSetupPane({
|
|||||||
disabled={!isStreaming}
|
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 && (
|
{(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && (
|
||||||
<IconButton
|
<>
|
||||||
className="border border-highlight"
|
<IconButton
|
||||||
size="sm"
|
className="border border-highlight"
|
||||||
title="to-do"
|
size="sm"
|
||||||
hotkeyAction="grpc_request.send"
|
title="to-do"
|
||||||
onClick={() => onSend({ message: activeRequest.message ?? '' })}
|
onClick={onCommit}
|
||||||
icon="sendHorizontal"
|
icon="check"
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
className="border border-highlight"
|
||||||
|
size="sm"
|
||||||
|
title="to-do"
|
||||||
|
hotkeyAction="grpc_request.send"
|
||||||
|
onClick={() => onSend({ message: activeRequest.message ?? '' })}
|
||||||
|
icon="sendHorizontal"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { shell } from '@tauri-apps/api';
|
import { shell } from '@tauri-apps/api';
|
||||||
import classNames from 'classnames';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import type { HttpResponse } from '../lib/models';
|
import type { HttpResponse } from '../lib/models';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
|
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||||
import { Separator } from './core/Separator';
|
import { Separator } from './core/Separator';
|
||||||
import { HStack } from './core/Stacks';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
@@ -13,16 +11,16 @@ interface Props {
|
|||||||
export function ResponseHeaders({ response }: Props) {
|
export function ResponseHeaders({ response }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto h-full pb-4">
|
<div className="overflow-auto h-full pb-4">
|
||||||
<dl className="text-xs w-full font-mono flex flex-col">
|
<KeyValueRows>
|
||||||
{response.headers.map((h, i) => (
|
{response.headers.map((h, i) => (
|
||||||
<Row key={i} label={h.name} value={h.value} labelClassName="!text-violet-600" />
|
<KeyValueRow key={i} label={h.name} value={h.value} labelClassName="!text-violet-600" />
|
||||||
))}
|
))}
|
||||||
</dl>
|
</KeyValueRows>
|
||||||
<Separator className="my-4">Other Info</Separator>
|
<Separator className="my-4">Other Info</Separator>
|
||||||
<dl className="text-xs w-full font-mono divide-highlightSecondary">
|
<KeyValueRows>
|
||||||
<Row label="Version" value={response.version} />
|
<KeyValueRow label="Version" value={response.version} />
|
||||||
<Row label="Remote Address" value={response.remoteAddr} />
|
<KeyValueRow label="Remote Address" value={response.remoteAddr} />
|
||||||
<Row
|
<KeyValueRow
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
URL
|
URL
|
||||||
@@ -41,26 +39,7 @@ export function ResponseHeaders({ response }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</dl>
|
</KeyValueRows>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Row({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
labelClassName,
|
|
||||||
}: {
|
|
||||||
label: ReactNode;
|
|
||||||
value: ReactNode;
|
|
||||||
labelClassName?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<HStack space={3} className="py-0.5">
|
|
||||||
<dd className={classNames(labelClassName, 'w-1/3 text-gray-700 select-text cursor-text')}>
|
|
||||||
{label}
|
|
||||||
</dd>
|
|
||||||
<dt className="w-2/3 select-text cursor-text break-all">{value}</dt>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const SidebarActions = memo(function SidebarActions() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'create-grpc-request',
|
key: 'create-grpc-request',
|
||||||
label: 'GRPC Call',
|
label: 'gRPC Call',
|
||||||
onSelect: () => createGrpcRequest.mutate({}),
|
onSelect: () => createGrpcRequest.mutate({}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
<RequestMethodDropdown
|
<RequestMethodDropdown
|
||||||
method={method}
|
method={method}
|
||||||
onChange={onMethodChange}
|
onChange={onMethodChange}
|
||||||
className="!h-auto my-0.5 mr-0.5"
|
className="!h-auto my-0.5 ml-0.5"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
|||||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { HotKeyList } from './core/HotKeyList';
|
import { HotKeyList } from './core/HotKeyList';
|
||||||
|
import { HStack } from './core/Stacks';
|
||||||
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
||||||
import { HttpRequestLayout } from './HttpRequestLayout';
|
import { HttpRequestLayout } from './HttpRequestLayout';
|
||||||
import { Overlay } from './Overlay';
|
import { Overlay } from './Overlay';
|
||||||
@@ -160,7 +161,19 @@ export default function Workspace() {
|
|||||||
<WorkspaceHeader className="pointer-events-none" />
|
<WorkspaceHeader className="pointer-events-none" />
|
||||||
</HeaderSize>
|
</HeaderSize>
|
||||||
{activeRequest == null ? (
|
{activeRequest == null ? (
|
||||||
<HotKeyList hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']} />
|
<HotKeyList
|
||||||
|
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
|
||||||
|
bottomSlot={
|
||||||
|
<HStack space={1} justifyContent="center" className="mt-3">
|
||||||
|
<Button size="sm" color="gray">
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" color="gray">
|
||||||
|
New Request
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : activeRequest.model === 'grpc_request' ? (
|
) : activeRequest.model === 'grpc_request' ? (
|
||||||
<GrpcConnectionLayout style={body} />
|
<GrpcConnectionLayout style={body} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { HStack, VStack } from './Stacks';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hotkeys: HotkeyAction[];
|
hotkeys: HotkeyAction[];
|
||||||
|
bottomSlot?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HotKeyList = ({ hotkeys }: Props) => {
|
export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto h-full flex items-center text-gray-700 text-sm">
|
<div className="mx-auto h-full flex items-center text-gray-700 text-sm">
|
||||||
<VStack space={2}>
|
<VStack space={2}>
|
||||||
@@ -18,6 +19,7 @@ export const HotKeyList = ({ hotkeys }: Props) => {
|
|||||||
<HotKey className="ml-auto" action={hotkey} />
|
<HotKey className="ml-auto" action={hotkey} />
|
||||||
</HStack>
|
</HStack>
|
||||||
))}
|
))}
|
||||||
|
{bottomSlot}
|
||||||
</VStack>
|
</VStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import type { HTMLAttributes } from 'react';
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
|
alert: lucide.AlertTriangleIcon,
|
||||||
archive: lucide.ArchiveIcon,
|
archive: lucide.ArchiveIcon,
|
||||||
arrowBigDownDash: lucide.ArrowBigDownDashIcon,
|
arrowBigDownDash: lucide.ArrowBigDownDashIcon,
|
||||||
|
arrowBigLeftDash: lucide.ArrowBigLeftDashIcon,
|
||||||
|
arrowBigRightDash: lucide.ArrowBigRightDashIcon,
|
||||||
arrowBigUpDash: lucide.ArrowBigUpDashIcon,
|
arrowBigUpDash: lucide.ArrowBigUpDashIcon,
|
||||||
arrowDown: lucide.ArrowDownIcon,
|
arrowDown: lucide.ArrowDownIcon,
|
||||||
arrowDownToDot: lucide.ArrowDownToDotIcon,
|
arrowDownToDot: lucide.ArrowDownToDotIcon,
|
||||||
@@ -60,12 +63,14 @@ export interface IconProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||||
spin?: boolean;
|
spin?: boolean;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Icon = memo(function Icon({ icon, spin, size = 'md', className }: IconProps) {
|
export const Icon = memo(function Icon({ icon, spin, size = 'md', className, title }: IconProps) {
|
||||||
const Component = icons[icon] ?? icons.question;
|
const Component = icons[icon] ?? icons.question;
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
|
title={title}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'text-inherit flex-shrink-0',
|
'text-inherit flex-shrink-0',
|
||||||
|
|||||||
24
src-web/components/core/KeyValueRow.tsx
Normal file
24
src-web/components/core/KeyValueRow.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: ReactNode;
|
||||||
|
value: ReactNode;
|
||||||
|
labelClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeyValueRows({ children }: { children: ReactNode }) {
|
||||||
|
return <dl className="text-xs w-full font-mono divide-highlightSecondary">{children}</dl>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeyValueRow({ label, value, labelClassName }: Props) {
|
||||||
|
return (
|
||||||
|
<HStack space={3} className="py-0.5">
|
||||||
|
<dd className={classNames(labelClassName, 'w-1/3 text-gray-700 select-text cursor-text')}>
|
||||||
|
{label}
|
||||||
|
</dd>
|
||||||
|
<dt className="w-2/3 select-text cursor-text break-all">{value}</dt>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src-web/hooks/useGrpcEvents.ts
Normal file
23
src-web/hooks/useGrpcEvents.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import type { GrpcEvent } from '../lib/models';
|
||||||
|
|
||||||
|
export function grpcEventsQueryKey({ connectionId }: { connectionId: string }) {
|
||||||
|
return ['grpc_events', { connectionId }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGrpcEvents(connectionId: string | null) {
|
||||||
|
return (
|
||||||
|
useQuery<GrpcEvent[]>({
|
||||||
|
enabled: connectionId !== null,
|
||||||
|
initialData: [],
|
||||||
|
queryKey: grpcEventsQueryKey({ connectionId: connectionId ?? 'n/a' }),
|
||||||
|
queryFn: async () => {
|
||||||
|
return (await invoke('cmd_list_grpc_events', {
|
||||||
|
connectionId,
|
||||||
|
limit: 200,
|
||||||
|
})) as GrpcEvent[];
|
||||||
|
},
|
||||||
|
}).data ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { invoke } from '@tauri-apps/api';
|
|
||||||
import type { GrpcMessage } from '../lib/models';
|
|
||||||
|
|
||||||
export function grpcMessagesQueryKey({ connectionId }: { connectionId: string }) {
|
|
||||||
return ['grpc_messages', { connectionId }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGrpcMessages(connectionId: string | null) {
|
|
||||||
return (
|
|
||||||
useQuery<GrpcMessage[]>({
|
|
||||||
enabled: connectionId !== null,
|
|
||||||
initialData: [],
|
|
||||||
queryKey: grpcMessagesQueryKey({ connectionId: connectionId ?? 'n/a' }),
|
|
||||||
queryFn: async () => {
|
|
||||||
return (await invoke('cmd_list_grpc_messages', {
|
|
||||||
connectionId,
|
|
||||||
limit: 200,
|
|
||||||
})) as GrpcMessage[];
|
|
||||||
},
|
|
||||||
}).data ?? []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ export function trackEvent(
|
|||||||
| 'Workspace'
|
| 'Workspace'
|
||||||
| 'Environment'
|
| 'Environment'
|
||||||
| 'Folder'
|
| 'Folder'
|
||||||
| 'GrpcMessage'
|
| 'GrpcEvent'
|
||||||
| 'GrpcConnection'
|
| 'GrpcConnection'
|
||||||
| 'GrpcRequest'
|
| 'GrpcRequest'
|
||||||
| 'HttpRequest'
|
| 'HttpRequest'
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type Model =
|
|||||||
| Workspace
|
| Workspace
|
||||||
| GrpcConnection
|
| GrpcConnection
|
||||||
| GrpcRequest
|
| GrpcRequest
|
||||||
| GrpcMessage
|
| GrpcEvent
|
||||||
| HttpRequest
|
| HttpRequest
|
||||||
| HttpResponse
|
| HttpResponse
|
||||||
| KeyValue
|
| KeyValue
|
||||||
@@ -127,14 +127,14 @@ export interface GrpcRequest extends BaseModel {
|
|||||||
metadata: GrpcMetadataEntry[];
|
metadata: GrpcMetadataEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrpcMessage extends BaseModel {
|
export interface GrpcEvent extends BaseModel {
|
||||||
readonly workspaceId: string;
|
readonly workspaceId: string;
|
||||||
readonly requestId: string;
|
readonly requestId: string;
|
||||||
readonly connectionId: string;
|
readonly connectionId: string;
|
||||||
readonly model: 'grpc_message';
|
readonly model: 'grpc_event';
|
||||||
message: string;
|
content: string;
|
||||||
isServer: boolean;
|
eventType: 'info' | 'error' | 'client_message' | 'server_message' | 'connection_response';
|
||||||
isInfo: boolean;
|
metadata: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrpcConnection extends BaseModel {
|
export interface GrpcConnection extends BaseModel {
|
||||||
@@ -144,6 +144,11 @@ export interface GrpcConnection extends BaseModel {
|
|||||||
service: string;
|
service: string;
|
||||||
method: string;
|
method: string;
|
||||||
elapsed: number;
|
elapsed: number;
|
||||||
|
elapsedConnection: number;
|
||||||
|
status: number;
|
||||||
|
url: string;
|
||||||
|
error: string | null;
|
||||||
|
trailers: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpRequest extends BaseModel {
|
export interface HttpRequest extends BaseModel {
|
||||||
|
|||||||
Reference in New Issue
Block a user