diff --git a/src-tauri/.sqlx/query-f055a2b6ab6bc3fea115dbea3f287df833c9bb73cdf2fe38da21fe5f5f6ae639.json b/src-tauri/.sqlx/query-196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json similarity index 62% rename from src-tauri/.sqlx/query-f055a2b6ab6bc3fea115dbea3f287df833c9bb73cdf2fe38da21fe5f5f6ae639.json rename to src-tauri/.sqlx/query-196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json index 411270cb..572209d2 100644 --- a/src-tauri/.sqlx/query-f055a2b6ab6bc3fea115dbea3f287df833c9bb73cdf2fe38da21fe5f5f6ae639.json +++ b/src-tauri/.sqlx/query-196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT id, model, workspace_id, request_id, connection_id, created_at, message\n FROM grpc_messages\n WHERE id = ?\n ", + "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 ", "describe": { "columns": [ { @@ -37,6 +37,16 @@ "name": "message", "ordinal": 6, "type_info": "Text" + }, + { + "name": "is_server", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "is_info", + "ordinal": 8, + "type_info": "Bool" } ], "parameters": { @@ -49,8 +59,10 @@ false, false, false, + false, + false, false ] }, - "hash": "f055a2b6ab6bc3fea115dbea3f287df833c9bb73cdf2fe38da21fe5f5f6ae639" + "hash": "196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59" } diff --git a/src-tauri/.sqlx/query-a86d3e86c5638d5f666c0cbe26dd5b3e88c632c56c7c4f3f116a29867e41e6a2.json b/src-tauri/.sqlx/query-3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c.json similarity index 63% rename from src-tauri/.sqlx/query-a86d3e86c5638d5f666c0cbe26dd5b3e88c632c56c7c4f3f116a29867e41e6a2.json rename to src-tauri/.sqlx/query-3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c.json index 90d96b80..c7bf9ae3 100644 --- a/src-tauri/.sqlx/query-a86d3e86c5638d5f666c0cbe26dd5b3e88c632c56c7c4f3f116a29867e41e6a2.json +++ b/src-tauri/.sqlx/query-3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT id, model, workspace_id, request_id, connection_id, created_at, message\n FROM grpc_messages\n WHERE workspace_id = ?\n ", + "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 ", "describe": { "columns": [ { @@ -37,6 +37,16 @@ "name": "message", "ordinal": 6, "type_info": "Text" + }, + { + "name": "is_server", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "is_info", + "ordinal": 8, + "type_info": "Bool" } ], "parameters": { @@ -49,8 +59,10 @@ false, false, false, + false, + false, false ] }, - "hash": "a86d3e86c5638d5f666c0cbe26dd5b3e88c632c56c7c4f3f116a29867e41e6a2" + "hash": "3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c" } diff --git a/src-tauri/.sqlx/query-4abfe0884ba046534dc6c255409c41ce287d7b0137726b8ca09870382a8b8300.json b/src-tauri/.sqlx/query-4abfe0884ba046534dc6c255409c41ce287d7b0137726b8ca09870382a8b8300.json deleted file mode 100644 index e3a2660a..00000000 --- a/src-tauri/.sqlx/query-4abfe0884ba046534dc6c255409c41ce287d7b0137726b8ca09870382a8b8300.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "4abfe0884ba046534dc6c255409c41ce287d7b0137726b8ca09870382a8b8300" -} diff --git a/src-tauri/.sqlx/query-4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460.json b/src-tauri/.sqlx/query-4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460.json new file mode 100644 index 00000000..0329c901 --- /dev/null +++ b/src-tauri/.sqlx/query-4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460.json @@ -0,0 +1,12 @@ +{ + "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" +} diff --git a/src-tauri/.sqlx/query-29d3bee45c4fca4c63231e1205edc708818737fe37137dbba6af2d784c3c0221.json b/src-tauri/.sqlx/query-a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c.json similarity index 86% rename from src-tauri/.sqlx/query-29d3bee45c4fca4c63231e1205edc708818737fe37137dbba6af2d784c3c0221.json rename to src-tauri/.sqlx/query-a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c.json index 0644eee1..92ac12a9 100644 --- a/src-tauri/.sqlx/query-29d3bee45c4fca4c63231e1205edc708818737fe37137dbba6af2d784c3c0221.json +++ b/src-tauri/.sqlx/query-a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method\n FROM grpc_connections\n WHERE workspace_id = ?\n ", + "query": "\n SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ", "describe": { "columns": [ { @@ -58,5 +58,5 @@ false ] }, - "hash": "29d3bee45c4fca4c63231e1205edc708818737fe37137dbba6af2d784c3c0221" + "hash": "a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c" } diff --git a/src-tauri/grpc/src/json_schema.rs b/src-tauri/grpc/src/json_schema.rs index 1bad1c02..4a6bb643 100644 --- a/src-tauri/grpc/src/json_schema.rs +++ b/src-tauri/grpc/src/json_schema.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; use prost_reflect::{DescriptorPool, MessageDescriptor}; use prost_types::field_descriptor_proto; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Default, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -50,8 +50,8 @@ impl Default for JsonType { impl serde::Serialize for JsonType { fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, + where + S: serde::Serializer, { match self { JsonType::String => serializer.serialize_str("string"), @@ -67,8 +67,8 @@ impl serde::Serialize for JsonType { impl<'de> serde::Deserialize<'de> for JsonType { fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, + where + D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; match s.as_str() { @@ -83,7 +83,10 @@ impl<'de> serde::Deserialize<'de> for JsonType { } } -pub fn message_to_json_schema(pool: &DescriptorPool, message: MessageDescriptor) -> JsonSchemaEntry { +pub fn message_to_json_schema( + pool: &DescriptorPool, + message: MessageDescriptor, +) -> JsonSchemaEntry { let mut schema = JsonSchemaEntry { title: Some(message.name().to_string()), type_: JsonType::Object, // Messages are objects diff --git a/src-tauri/migrations/20240203164833_grpc.sql b/src-tauri/migrations/20240203164833_grpc.sql index 0f7fd93d..17430cce 100644 --- a/src-tauri/migrations/20240203164833_grpc.sql +++ b/src-tauri/migrations/20240203164833_grpc.sql @@ -52,5 +52,7 @@ CREATE TABLE grpc_messages ON DELETE CASCADE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_server BOOLEAN NOT NULL, + is_info BOOLEAN NOT NULL, message TEXT NOT NULL ); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2cf28ea2..4370c2a3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -44,14 +44,15 @@ use crate::http::send_http_request; use crate::models::{ cancel_pending_responses, create_response, delete_all_responses, delete_cookie_jar, delete_environment, delete_folder, delete_request, delete_response, delete_workspace, - duplicate_grpc_request, duplicate_http_request, list_cookie_jars, list_folders, list_requests, - list_responses, list_workspaces, generate_id, get_cookie_jar, get_environment, get_folder, - get_grpc_request, get_http_request, get_key_value_raw, get_or_create_settings, get_response, - get_workspace, get_workspace_export_resources, list_environments, list_grpc_requests, - set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, - upsert_environment, upsert_folder, upsert_grpc_request, upsert_http_request, upsert_workspace, - CookieJar, Environment, EnvironmentVariable, Folder, GrpcRequest, HttpRequest, HttpResponse, - KeyValue, Settings, Workspace, + duplicate_grpc_request, duplicate_http_request, generate_id, get_cookie_jar, get_environment, + get_folder, get_grpc_request, get_http_request, get_key_value_raw, get_or_create_settings, + get_response, get_workspace, 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_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_grpc_request, upsert_http_request, upsert_workspace, CookieJar, + Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcMessage, GrpcRequest, + HttpRequest, HttpResponse, KeyValue, Settings, Workspace, }; use crate::plugin::{ImportResources, ImportResult}; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; @@ -98,20 +99,80 @@ async fn cmd_grpc_reflect(endpoint: &str) -> Result, Stri #[tauri::command] async fn cmd_grpc_call_unary( - endpoint: &str, - service: &str, - method: &str, - message: &str, + request_id: &str, + app_handle: AppHandle, grpc_handle: State<'_, Mutex>, -) -> Result { - let uri = safe_uri(endpoint).map_err(|e| e.to_string())?; - grpc_handle + db_state: State<'_, Mutex>>, +) -> Result { + let db = &*db_state.lock().await; + let req = get_grpc_request(db, request_id) + .await + .map_err(|e| e.to_string())?; + let conn = { + let req = req.clone(); + upsert_grpc_connection( + db, + &GrpcConnection { + workspace_id: req.workspace_id, + request_id: req.id, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())? + }; + emit_side_effect(app_handle.clone(), "created_model", conn.clone()); + + { + let req = req.clone(); + let conn = conn.clone(); + upsert_grpc_message( + db, + &GrpcMessage { + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_info: true, + message: format!("Initiating connection to {}", req.url), + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())?; + }; + + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; + let conn_id = generate_id(Some("grpc")); + let msg = match grpc_handle .lock() .await - .connect("default", uri) + .connect(&conn_id, uri) .await - .unary(service, method, message) + .unary( + &req.service.unwrap_or_default(), + &req.method.unwrap_or_default(), + &req.message, + ) .await + { + Ok(msg) => { + upsert_grpc_message( + db, + &GrpcMessage { + message: msg, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_server: true, + ..Default::default() + }, + ) + .await + } + Err(e) => return Err(e.to_string()), + }; + + msg.map_err(|e| e.to_string()) } #[tauri::command] @@ -235,27 +296,47 @@ async fn cmd_grpc_bidi_streaming( #[tauri::command] async fn cmd_grpc_server_streaming( - endpoint: &str, - service: &str, - method: &str, - message: &str, + request_id: &str, app_handle: AppHandle, grpc_handle: State<'_, Mutex>, -) -> Result { + db_state: State<'_, Mutex>>, +) -> Result { + let db = &*db_state.lock().await; + let req = get_grpc_request(db, request_id) + .await + .map_err(|e| e.to_string())?; + let conn = { + let req = req.clone(); + upsert_grpc_connection( + db, + &GrpcConnection { + workspace_id: req.workspace_id, + request_id: req.id, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())? + }; + emit_side_effect(app_handle.clone(), "created_model", conn.clone()); + let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false); - let uri = safe_uri(endpoint).map_err(|e| e.to_string())?; - let conn_id = generate_id(Some("grpc")); + let (service, method) = match (&req.service, &req.method) { + (Some(service), Some(method)) => (service, method), + _ => return Err("Service and method are required".to_string()), + }; + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; let mut stream = grpc_handle .lock() .await - .server_streaming(&conn_id, uri, service, method, message) + .server_streaming(&conn.id, uri, &service, &method, &req.message) .await .unwrap(); #[derive(serde::Deserialize)] - enum GrpcMessage { + enum IncomingMsg { Message(String), Commit, Cancel, @@ -270,15 +351,15 @@ async fn cmd_grpc_server_streaming( return; } - match serde_json::from_str::(ev.payload().unwrap()) { - Ok(GrpcMessage::Message(msg)) => { + match serde_json::from_str::(ev.payload().unwrap()) { + Ok(IncomingMsg::Message(msg)) => { println!("Received message: {}", msg); } - Ok(GrpcMessage::Commit) => { + Ok(IncomingMsg::Commit) => { println!("Received commit"); // TODO: Commit client streaming stream } - Ok(GrpcMessage::Cancel) => { + Ok(IncomingMsg::Cancel) => { println!("Received cancel"); cancelled_tx.send_replace(true); } @@ -289,19 +370,34 @@ async fn cmd_grpc_server_streaming( } }; let event_handler = - app_handle.listen_global(format!("grpc_client_msg_{}", conn_id).as_str(), cb); + app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb); let grpc_listen = { + let db = db.clone(); + let conn_id = conn.clone().id; let app_handle = app_handle.clone(); - let conn_id = conn_id.clone(); async move { loop { + let req = req.clone(); + let conn_id = conn_id.clone(); match stream.next().await { Some(Ok(item)) => { let item = serde_json::to_string_pretty(&item).unwrap(); - app_handle - .emit_all(format!("grpc_server_msg_{}", &conn_id).as_str(), item) - .expect("Failed to emit"); + let msg = upsert_grpc_message( + &db, + &GrpcMessage { + message: item, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn_id, + is_server: true, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string()) + .expect("Failed to upsert message"); + emit_side_effect(app_handle.clone(), "created_model", msg); } Some(Err(e)) => { error!("gRPC stream error: {:?}", e); @@ -328,7 +424,7 @@ async fn cmd_grpc_server_streaming( app_handle.unlisten(event_handler); }); - Ok(conn_id) + Ok(conn) } #[tauri::command] @@ -783,7 +879,7 @@ async fn cmd_duplicate_grpc_request( let request = duplicate_grpc_request(db, id) .await .expect("Failed to duplicate grpc request"); - emit_and_return(&window, "updated_model", request) + emit_and_return(&window, "created_model", request) } #[tauri::command] @@ -823,7 +919,7 @@ async fn cmd_duplicate_http_request( let request = duplicate_http_request(db, id) .await .expect("Failed to duplicate http request"); - emit_and_return(&window, "updated_model", request) + emit_and_return(&window, "created_model", request) } #[tauri::command] @@ -982,6 +1078,28 @@ async fn cmd_delete_environment( emit_and_return(&window, "deleted_model", req) } +#[tauri::command] +async fn cmd_list_grpc_connections( + request_id: &str, + db_state: State<'_, Mutex>>, +) -> Result, String> { + let db = &*db_state.lock().await; + list_grpc_connections(db, request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_list_grpc_messages( + connection_id: &str, + db_state: State<'_, Mutex>>, +) -> Result, String> { + let db = &*db_state.lock().await; + list_grpc_messages(db, connection_id) + .await + .map_err(|e| e.to_string()) +} + #[tauri::command] async fn cmd_list_grpc_requests( workspace_id: &str, @@ -990,8 +1108,7 @@ async fn cmd_list_grpc_requests( let db = &*db_state.lock().await; let requests = list_grpc_requests(db, workspace_id) .await - .expect("Failed to find grpc requests"); - // .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; Ok(requests) } @@ -1123,7 +1240,7 @@ async fn cmd_get_workspace( } #[tauri::command] -async fn cmd_list_responses( +async fn cmd_list_http_responses( request_id: &str, limit: Option, db_state: State<'_, Mutex>>, @@ -1303,6 +1420,7 @@ fn main() { cmd_delete_response, cmd_delete_workspace, cmd_duplicate_http_request, + cmd_duplicate_grpc_request, cmd_export_data, cmd_filter_response, cmd_get_cookie_jar, @@ -1324,7 +1442,9 @@ fn main() { cmd_list_folders, cmd_list_http_requests, cmd_list_grpc_requests, - cmd_list_responses, + cmd_list_grpc_connections, + cmd_list_grpc_messages, + cmd_list_http_responses, cmd_list_workspaces, cmd_new_window, cmd_send_ephemeral_request, diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 8e969421..16f018d0 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -228,6 +228,8 @@ pub struct GrpcMessage { pub connection_id: String, pub created_at: NaiveDateTime, pub message: String, + pub is_server: bool, + pub is_info: bool, } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] @@ -589,16 +591,17 @@ pub async fn get_grpc_connection( pub async fn list_grpc_connections( db: &Pool, - workspace_id: &str, + request_id: &str, ) -> Result, sqlx::Error> { sqlx::query_as!( GrpcConnection, r#" SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method FROM grpc_connections - WHERE workspace_id = ? + WHERE request_id = ? + ORDER BY created_at DESC "#, - workspace_id, + request_id, ) .fetch_all(db) .await @@ -615,30 +618,36 @@ pub async fn upsert_grpc_message( sqlx::query!( r#" INSERT INTO grpc_messages ( - id, workspace_id, request_id, connection_id, message + id, workspace_id, request_id, connection_id, message, is_server, is_info ) - VALUES (?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, - message = excluded.message + message = excluded.message, + is_server = excluded.is_server, + is_info = excluded.is_info "#, id, message.workspace_id, message.request_id, message.connection_id, message.message, + message.is_server, + message.is_info, ) .execute(db) .await?; - crate::models::get_grpc_message(db, &id).await + get_grpc_message(db, &id).await } pub async fn get_grpc_message(db: &Pool, id: &str) -> Result { sqlx::query_as!( GrpcMessage, r#" - SELECT id, model, workspace_id, request_id, connection_id, created_at, message + SELECT + id, model, workspace_id, request_id, connection_id, created_at, message, + is_server, is_info FROM grpc_messages WHERE id = ? "#, @@ -650,16 +659,18 @@ pub async fn get_grpc_message(db: &Pool, id: &str) -> Result, - workspace_id: &str, + connection_id: &str, ) -> Result, sqlx::Error> { sqlx::query_as!( GrpcMessage, r#" - SELECT id, model, workspace_id, request_id, connection_id, created_at, message + SELECT + id, model, workspace_id, request_id, connection_id, created_at, message, + is_server, is_info FROM grpc_messages - WHERE workspace_id = ? + WHERE connection_id = ? "#, - workspace_id, + connection_id, ) .fetch_all(db) .await diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index fbcc875a..2273c78a 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -3,20 +3,23 @@ import { appWindow } from '@tauri-apps/api/window'; import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { cookieJarsQueryKey } from '../hooks/useCookieJars'; +import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections'; +import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages'; +import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests'; +import { httpRequestsQueryKey } from '../hooks/useHttpRequests'; +import { httpResponsesQueryKey } from '../hooks/useHttpResponses'; import { keyValueQueryKey } from '../hooks/useKeyValue'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; -import { httpRequestsQueryKey } from '../hooks/useHttpRequests'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; -import { responsesQueryKey } from '../hooks/useResponses'; import { settingsQueryKey } from '../hooks/useSettings'; import { useSyncAppearance } from '../hooks/useSyncAppearance'; import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle'; import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore'; -import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models'; +import type { Model } from '../lib/models'; import { modelsEq } from '../lib/models'; import { setPathname } from '../lib/persistPathname'; @@ -49,7 +52,13 @@ export function GlobalHooks() { payload.model === 'http_request' ? httpRequestsQueryKey(payload) : payload.model === 'http_response' - ? responsesQueryKey(payload) + ? httpResponsesQueryKey(payload) + : payload.model === 'grpc_connection' + ? grpcConnectionsQueryKey(payload) + : payload.model === 'grpc_message' + ? grpcMessagesQueryKey(payload) + : payload.model === 'grpc_request' + ? grpcRequestsQueryKey(payload) : payload.model === 'workspace' ? workspacesQueryKey(payload) : payload.model === 'key_value' @@ -78,7 +87,13 @@ export function GlobalHooks() { payload.model === 'http_request' ? httpRequestsQueryKey(payload) : payload.model === 'http_response' - ? responsesQueryKey(payload) + ? httpResponsesQueryKey(payload) + : payload.model === 'grpc_connection' + ? grpcConnectionsQueryKey(payload) + : payload.model === 'grpc_message' + ? grpcMessagesQueryKey(payload) + : payload.model === 'grpc_request' + ? grpcRequestsQueryKey(payload) : payload.model === 'workspace' ? workspacesQueryKey(payload) : payload.model === 'key_value' @@ -113,11 +128,17 @@ export function GlobalHooks() { if (shouldIgnoreModel(payload)) return; if (payload.model === 'workspace') { - queryClient.setQueryData(workspacesQueryKey(), removeById(payload)); + queryClient.setQueryData(workspacesQueryKey(), removeById(payload)); } else if (payload.model === 'http_request') { - queryClient.setQueryData(httpRequestsQueryKey(payload), removeById(payload)); + queryClient.setQueryData(httpRequestsQueryKey(payload), removeById(payload)); } else if (payload.model === 'http_response') { - queryClient.setQueryData(responsesQueryKey(payload), removeById(payload)); + queryClient.setQueryData(httpResponsesQueryKey(payload), removeById(payload)); + } else if (payload.model === 'grpc_request') { + queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload)); + } else if (payload.model === 'grpc_connection') { + queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload)); + } else if (payload.model === 'grpc_message') { + queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload)); } else if (payload.model === 'key_value') { queryClient.setQueryData(keyValueQueryKey(payload), undefined); } else if (payload.model === 'cookie_jar') { diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx index c059cd9a..1885d51e 100644 --- a/src-web/components/GrpcConnectionLayout.tsx +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -5,12 +5,12 @@ import type { CSSProperties, FormEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useAlert } from '../hooks/useAlert'; -import type { GrpcMessage } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc'; +import { useGrpcConnections } from '../hooks/useGrpcConnections'; +import { useGrpcMessages } from '../hooks/useGrpcMessages'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; -import { Editor } from './core/Editor'; import { HotKeyList } from './core/HotKeyList'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; @@ -30,9 +30,11 @@ export function GrpcConnectionLayout({ style }: Props) { const activeRequest = useActiveRequest('grpc_request'); const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); const alert = useAlert(); - const [activeMessage, setActiveMessage] = useState(null); - const [resp, setResp] = useState(''); + const [activeMessageId, setActiveMessageId] = useState(null); const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.id ?? null); + const connections = useGrpcConnections(activeRequest?.id ?? null); + const activeConnection = connections[0] ?? null; + const messages = useGrpcMessages(activeConnection?.id ?? null); const activeMethod = useMemo(() => { if (grpc.services == null || activeRequest == null) return null; @@ -61,9 +63,9 @@ export function GrpcConnectionLayout({ style }: Props) { if (activeMethod.clientStreaming && activeMethod.serverStreaming) { await grpc.bidiStreaming.mutateAsync(activeRequest); } else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) { - await grpc.serverStreaming.mutateAsync(activeRequest); + await grpc.serverStreaming.mutateAsync(activeRequest.id); } else { - setResp(await grpc.unary.mutateAsync(activeRequest)); + await grpc.unary.mutateAsync(activeRequest.id); } }, [activeMethod, activeRequest, alert, grpc.bidiStreaming, grpc.serverStreaming, grpc.unary], @@ -127,8 +129,13 @@ export function GrpcConnectionLayout({ style }: Props) { setPaneSize(entry.contentRect.width); }); + const activeMessage = useMemo( + () => messages.find((m) => m.id === activeMessageId) ?? null, + [activeMessageId, messages], + ); + if (activeRequest == null) { - return; + return null; } return ( @@ -136,7 +143,7 @@ export function GrpcConnectionLayout({ style }: Props) { name="grpc_layout" className="p-3 gap-1.5" style={style} - leftSlot={() => ( + firstSlot={() => (
)} - rightSlot={() => + secondSlot={() => !grpc.unary.isLoading && (
{grpc.unary.error} - ) : (grpc.messages.value ?? []).length > 0 ? ( + ) : messages.length >= 0 ? ( ( + firstSlot={() => (
- {...(grpc.messages.value ?? []).map((m, i) => ( + {...messages.map((m) => ( { - if (m === activeMessage) setActiveMessage(null); - else setActiveMessage(m); + if (m.id === activeMessageId) setActiveMessageId(null); + else setActiveMessageId(m.id); }} alignItems="center" className={classNames( @@ -254,29 +261,25 @@ export function GrpcConnectionLayout({ style }: Props) { >
{m.message}
- {format(m.timestamp, 'HH:mm:ss')} + {format(m.createdAt, 'HH:mm:ss')}
))}
)} - rightSlot={ + secondSlot={ !activeMessage ? null : () => ( @@ -293,15 +296,15 @@ export function GrpcConnectionLayout({ style }: Props) { ) } /> - ) : resp ? ( - ) : ( + // ) : ? ( + // )}
diff --git a/src-web/components/HttpRequestLayout.tsx b/src-web/components/HttpRequestLayout.tsx index db6f51d4..98f4bd43 100644 --- a/src-web/components/HttpRequestLayout.tsx +++ b/src-web/components/HttpRequestLayout.tsx @@ -14,10 +14,10 @@ export function HttpRequestLayout({ style }: Props) { name="http_layout" className="p-3 gap-1.5" style={style} - leftSlot={({ orientation, style }) => ( + firstSlot={({ orientation, style }) => ( )} - rightSlot={({ style }) => } + secondSlot={({ style }) => } /> ); } diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 18f9fd7a..da3e744d 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -5,7 +5,7 @@ import { createGlobalState } from 'react-use'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useLatestResponse } from '../hooks/useLatestResponse'; import { useResponseContentType } from '../hooks/useResponseContentType'; -import { useResponses } from '../hooks/useResponses'; +import { useHttpResponses } from '../hooks/useHttpResponses'; import { useResponseViewMode } from '../hooks/useResponseViewMode'; import type { HttpResponse } from '../lib/models'; import { isResponseLoading } from '../lib/models'; @@ -39,7 +39,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro const [pinnedResponseId, setPinnedResponseId] = useState(null); const activeRequest = useActiveRequest(); const latestResponse = useLatestResponse(activeRequest?.id ?? null); - const responses = useResponses(activeRequest?.id ?? null); + const responses = useHttpResponses(activeRequest?.id ?? null); const activeResponse: HttpResponse | null = pinnedResponseId ? responses.find((r) => r.id === pinnedResponseId) ?? null : latestResponse ?? null; diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index e49549b5..8fdf4205 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -7,7 +7,6 @@ import { useKey, useKeyPressEvent } from 'react-use'; import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId'; import { useActiveRequest } from '../hooks/useActiveRequest'; -import { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useAppRoutes } from '../hooks/useAppRoutes'; import { useCreateFolder } from '../hooks/useCreateFolder'; @@ -15,7 +14,8 @@ import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest'; import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest'; import { useDeleteFolder } from '../hooks/useDeleteFolder'; import { useDeleteRequest } from '../hooks/useDeleteRequest'; -import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; +import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest'; +import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest'; import { useFolders } from '../hooks/useFolders'; import { useGrpcRequests } from '../hooks/useGrpcRequests'; import { useHotKey } from '../hooks/useHotKey'; @@ -29,6 +29,7 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; +import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore'; @@ -58,14 +59,21 @@ interface TreeNode { export function Sidebar({ className }: Props) { const { hidden } = useSidebarHidden(); const sidebarRef = useRef(null); - const activeRequestId = useActiveRequestId(); + const activeRequest = useActiveRequest(); const activeEnvironmentId = useActiveEnvironmentId(); const httpRequests = useHttpRequests(); const grpcRequests = useGrpcRequests(); const folders = useFolders(); const deleteAnyRequest = useDeleteAnyRequest(); const activeWorkspace = useActiveWorkspace(); - const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true }); + const duplicateHttpRequest = useDuplicateHttpRequest({ + id: activeRequest?.id ?? null, + navigateAfter: true, + }); + const duplicateGrpcRequest = useDuplicateGrpcRequest({ + id: activeRequest?.id ?? null, + navigateAfter: true, + }); const routes = useAppRoutes(); const [hasFocus, setHasFocus] = useState(false); const [selectedId, setSelectedId] = useState(null); @@ -82,8 +90,12 @@ export function Sidebar({ className }: Props) { namespace: NAMESPACE_NO_SYNC, }); - useHotKey('http_request.duplicate', () => { - duplicateRequest.mutate(); + useHotKey('http_request.duplicate', async () => { + if (activeRequest?.model === 'http_request') { + await duplicateHttpRequest.mutateAsync(); + } else { + await duplicateGrpcRequest.mutateAsync(); + } }); const isCollapsed = useCallback( @@ -146,9 +158,10 @@ export function Sidebar({ className }: Props) { } = {}, ) => { const { forced, noFocusSidebar } = args; - const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null; + const tree = forced?.tree ?? treeParentMap[activeRequest?.id ?? 'n/a'] ?? null; const children = tree?.children ?? []; - const id = forced?.id ?? children.find((m) => m.item.id === activeRequestId)?.item.id ?? null; + const id = + forced?.id ?? children.find((m) => m.item.id === activeRequest?.id)?.item.id ?? null; if (id == null) { return; } @@ -160,7 +173,7 @@ export function Sidebar({ className }: Props) { sidebarRef.current?.focus(); } }, - [activeRequestId, treeParentMap], + [activeRequest, treeParentMap], ); const handleSelect = useCallback( @@ -230,7 +243,7 @@ export function Sidebar({ className }: Props) { useKeyPressEvent('Enter', (e) => { if (!hasFocus) return; const selected = selectableRequests.find((r) => r.id === selectedId); - if (!selected || selected.id === activeRequestId || activeWorkspace == null) { + if (!selected || selected.id === activeRequest?.id || activeWorkspace == null) { return; } @@ -541,11 +554,13 @@ const SidebarItem = forwardRef(function SidebarItem( const createFolder = useCreateFolder(); const deleteFolder = useDeleteFolder(itemId); const deleteRequest = useDeleteRequest(itemId); - const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true }); + const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true }); + const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true }); const sendRequest = useSendRequest(itemId); const sendManyRequests = useSendManyRequests(); const latestResponse = useLatestResponse(itemId); - const updateRequest = useUpdateHttpRequest(itemId); + const updateHttpRequest = useUpdateHttpRequest(itemId); + const updateGrpcRequest = useUpdateGrpcRequest(itemId); const updateAnyFolder = useUpdateAnyFolder(); const prompt = usePrompt(); const [editing, setEditing] = useState(false); @@ -553,10 +568,15 @@ const SidebarItem = forwardRef(function SidebarItem( const handleSubmitNameEdit = useCallback( (el: HTMLInputElement) => { - updateRequest.mutate((r) => ({ ...r, name: el.value })); + if (activeRequest == null) return; + if (activeRequest.model === 'http_request') { + updateHttpRequest.mutate((r) => ({ ...r, name: el.value })); + } else if (activeRequest.model === 'grpc_request') { + updateGrpcRequest.mutate((r) => ({ ...r, name: el.value })); + } setEditing(false); }, - [updateRequest], + [activeRequest, updateGrpcRequest, updateHttpRequest], ); const handleFocus = useCallback((el: HTMLInputElement | null) => { @@ -677,7 +697,9 @@ const SidebarItem = forwardRef(function SidebarItem( hotKeyLabelOnly: true, // Would trigger for every request (bad) leftSlot: , onSelect: () => { - duplicateRequest.mutate(); + itemModel === 'http_request' + ? duplicateHttpRequest.mutate() + : duplicateGrpcRequest.mutate(); }, }, { diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index c8e1d061..89394a08 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -34,7 +34,6 @@ export default function Workspace() { const { setWidth, width, resetWidth } = useSidebarWidth(); const { hide, show, hidden } = useSidebarHidden(); const activeRequest = useActiveRequest(); - const windowSize = useWindowSize(); const [floating, setFloating] = useState(false); const [isResizing, setIsResizing] = useState(false); @@ -47,7 +46,7 @@ export default function Workspace() { const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH; if (shouldHide && !floating) { setFloating(true); - hide(); + hide().catch(console.error); } else if (!shouldHide && floating) { setFloating(false); } @@ -72,10 +71,10 @@ export default function Workspace() { e.preventDefault(); // Prevent text selection and things const newWidth = startWidth + (e.clientX - mouseStartX); if (newWidth < 100) { - hide(); + await hide(); resetWidth(); } else { - show(); + await show(); setWidth(newWidth); } }, diff --git a/src-web/components/core/SplitLayout.tsx b/src-web/components/core/SplitLayout.tsx index 81c6ca40..a97cc52e 100644 --- a/src-web/components/core/SplitLayout.tsx +++ b/src-web/components/core/SplitLayout.tsx @@ -16,8 +16,8 @@ interface SlotProps { interface Props { name: string; - leftSlot: (props: SlotProps) => ReactNode; - rightSlot: null | ((props: SlotProps) => ReactNode); + firstSlot: (props: SlotProps) => ReactNode; + secondSlot: null | ((props: SlotProps) => ReactNode); style?: CSSProperties; className?: string; defaultRatio?: number; @@ -33,8 +33,8 @@ const STACK_VERTICAL_WIDTH = 700; export function SplitLayout({ style, - leftSlot, - rightSlot, + firstSlot, + secondSlot, className, name, defaultRatio = 0.5, @@ -54,7 +54,7 @@ export function SplitLayout({ null, ); - if (!rightSlot) { + if (!secondSlot) { height = 0; minHeightPx = 0; } @@ -145,8 +145,8 @@ export function SplitLayout({ return (
- {leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} - {rightSlot && ( + {firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} + {secondSlot && ( <> - {rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })} + {secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })} )}
diff --git a/src-web/hooks/useDeleteAnyRequest.tsx b/src-web/hooks/useDeleteAnyRequest.tsx index 48df4266..af54cf25 100644 --- a/src-web/hooks/useDeleteAnyRequest.tsx +++ b/src-web/hooks/useDeleteAnyRequest.tsx @@ -7,7 +7,7 @@ import type { HttpRequest } from '../lib/models'; import { getHttpRequest } from '../lib/store'; import { useConfirm } from './useConfirm'; import { httpRequestsQueryKey } from './useHttpRequests'; -import { responsesQueryKey } from './useResponses'; +import { httpResponsesQueryKey } from './useHttpResponses'; export function useDeleteAnyRequest() { const queryClient = useQueryClient(); @@ -35,7 +35,7 @@ export function useDeleteAnyRequest() { if (request === null) return; const { workspaceId, id: requestId } = request; - queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted + queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), (requests) => (requests ?? []).filter((r) => r.id !== requestId), ); diff --git a/src-web/hooks/useDeleteResponse.ts b/src-web/hooks/useDeleteResponse.ts index f763587c..3f460acd 100644 --- a/src-web/hooks/useDeleteResponse.ts +++ b/src-web/hooks/useDeleteResponse.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; import type { HttpResponse } from '../lib/models'; -import { responsesQueryKey } from './useResponses'; +import { httpResponsesQueryKey } from './useHttpResponses'; export function useDeleteResponse(id: string | null) { const queryClient = useQueryClient(); @@ -12,7 +12,7 @@ export function useDeleteResponse(id: string | null) { }, onSettled: () => trackEvent('HttpResponse', 'Delete'), onSuccess: ({ requestId, id: responseId }) => { - queryClient.setQueryData(responsesQueryKey({ requestId }), (responses) => + queryClient.setQueryData(httpResponsesQueryKey({ requestId }), (responses) => (responses ?? []).filter((response) => response.id !== responseId), ); }, diff --git a/src-web/hooks/useDeleteResponses.ts b/src-web/hooks/useDeleteResponses.ts index d3f896f0..5a5fd366 100644 --- a/src-web/hooks/useDeleteResponses.ts +++ b/src-web/hooks/useDeleteResponses.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; -import { responsesQueryKey } from './useResponses'; +import { httpResponsesQueryKey } from './useHttpResponses'; export function useDeleteResponses(requestId?: string) { const queryClient = useQueryClient(); @@ -13,7 +13,7 @@ export function useDeleteResponses(requestId?: string) { onSettled: () => trackEvent('HttpResponse', 'DeleteMany'), onSuccess: async () => { if (requestId === undefined) return; - queryClient.setQueryData(responsesQueryKey({ requestId }), []); + queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); }, }); } diff --git a/src-web/hooks/useDuplicateGrpcRequest.ts b/src-web/hooks/useDuplicateGrpcRequest.ts new file mode 100644 index 00000000..14efd4bb --- /dev/null +++ b/src-web/hooks/useDuplicateGrpcRequest.ts @@ -0,0 +1,41 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import type { GrpcRequest } from '../lib/models'; +import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useAppRoutes } from './useAppRoutes'; +import { grpcRequestsQueryKey } from './useGrpcRequests'; + +export function useDuplicateGrpcRequest({ + id, + navigateAfter, +}: { + id: string | null; + navigateAfter: boolean; +}) { + const activeWorkspaceId = useActiveWorkspaceId(); + const activeEnvironmentId = useActiveEnvironmentId(); + const routes = useAppRoutes(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + if (id === null) throw new Error("Can't duplicate a null grpc request"); + return invoke('cmd_duplicate_grpc_request', { id }); + }, + onSettled: () => trackEvent('GrpcRequest', 'Duplicate'), + onSuccess: async (request) => { + queryClient.setQueryData( + grpcRequestsQueryKey({ workspaceId: request.workspaceId }), + (requests) => [...(requests ?? []), request], + ); + if (navigateAfter && activeWorkspaceId !== null) { + routes.navigate('request', { + workspaceId: activeWorkspaceId, + requestId: request.id, + environmentId: activeEnvironmentId ?? undefined, + }); + } + }, + }); +} diff --git a/src-web/hooks/useDuplicateRequest.ts b/src-web/hooks/useDuplicateHttpRequest.ts similarity index 97% rename from src-web/hooks/useDuplicateRequest.ts rename to src-web/hooks/useDuplicateHttpRequest.ts index f844067e..6e1f33f9 100644 --- a/src-web/hooks/useDuplicateRequest.ts +++ b/src-web/hooks/useDuplicateHttpRequest.ts @@ -7,7 +7,7 @@ import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useAppRoutes } from './useAppRoutes'; import { httpRequestsQueryKey } from './useHttpRequests'; -export function useDuplicateRequest({ +export function useDuplicateHttpRequest({ id, navigateAfter, }: { diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index f8e833a2..2741315c 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -3,8 +3,7 @@ import { invoke } from '@tauri-apps/api'; import type { UnlistenFn } from '@tauri-apps/api/event'; import { emit, listen } from '@tauri-apps/api/event'; import { useEffect, useRef, useState } from 'react'; -import { tryFormatJson } from '../lib/formatters'; -import type { GrpcRequest } from '../lib/models'; +import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models'; import { useKeyValue } from './useKeyValue'; interface ReflectResponseService { @@ -12,12 +11,6 @@ interface ReflectResponseService { methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[]; } -export interface GrpcMessage { - message: string; - timestamp: string; - type: 'server' | 'client' | 'info'; -} - export function useGrpc(url: string | null, requestId: string | null) { const messages = useKeyValue({ namespace: 'debug', @@ -25,54 +18,30 @@ export function useGrpc(url: string | null, requestId: string | null) { defaultValue: [], }); const [activeConnectionId, setActiveConnectionId] = useState(null); - const unlisten = useRef(null); useEffect(() => { setActiveConnectionId(null); - unlisten.current?.(); }, [requestId]); - const unary = useMutation({ + const unary = useMutation({ mutationKey: ['grpc_unary', url], - mutationFn: async ({ service, method, message, url }) => { - if (url === null) throw new Error('No URL provided'); - return (await invoke('cmd_grpc_call_unary', { - endpoint: url, - service, - method, - message, - })) as string; + mutationFn: async (id) => { + const message = (await invoke('cmd_grpc_call_unary', { + requestId: id, + })) as GrpcMessage; + await messages.set([message]); + console.log('MESSAGE', message); + return message; }, }); - const serverStreaming = useMutation({ + const serverStreaming = useMutation({ mutationKey: ['grpc_server_streaming', url], - mutationFn: async ({ service, method, message, url }) => { + mutationFn: async (requestId) => { if (url === null) throw new Error('No URL provided'); - await messages.set([ - { - type: 'client', - message: JSON.stringify(JSON.parse(message)), - timestamp: new Date().toISOString(), - }, - ]); - const id: string = await invoke('cmd_grpc_server_streaming', { - endpoint: url, - service, - method, - message, - }); - unlisten.current = await listen(`grpc_server_msg_${id}`, async (event) => { - await messages.set((prev) => [ - ...prev, - { - message: tryFormatJson(event.payload as string, false), - timestamp: new Date().toISOString(), - type: 'server', - }, - ]); - }); - setActiveConnectionId(id); + await messages.set([]); + const c = (await invoke('cmd_grpc_server_streaming', { requestId })) as GrpcConnection; + setActiveConnectionId(c.id); }, }); @@ -86,20 +55,8 @@ export function useGrpc(url: string | null, requestId: string | null) { method, message, }); - await messages.set([ - { type: 'info', message: `Started connection ${id}`, timestamp: new Date().toISOString() }, - ]); + await messages.set([]); setActiveConnectionId(id); - unlisten.current = await listen(`grpc_server_msg_${id}`, (event) => { - messages.set((prev) => [ - ...prev, - { - message: tryFormatJson(event.payload as string, false), - timestamp: new Date().toISOString(), - type: 'server', - }, - ]); - }); }, }); @@ -107,9 +64,10 @@ export function useGrpc(url: string | null, requestId: string | null) { mutationKey: ['grpc_send', url], mutationFn: async ({ message }: { message: string }) => { if (activeConnectionId == null) throw new Error('No active connection'); - await messages.set((m) => { - return [...m, { type: 'client', message, timestamp: new Date().toISOString() }]; - }); + await messages.set([]); + // await messages.set((m) => { + // return [...m, { type: 'client', message, timestamp: new Date().toISOString() }]; + // }); await emit(`grpc_client_msg_${activeConnectionId}`, { Message: message }); }, }); @@ -118,12 +76,7 @@ export function useGrpc(url: string | null, requestId: string | null) { mutationKey: ['grpc_cancel', url], mutationFn: async () => { setActiveConnectionId(null); - unlisten.current?.(); - await emit('grpc_message_in', 'Cancel'); - await messages.set((m) => [ - ...m, - { type: 'info', message: 'Cancelled by client', timestamp: new Date().toISOString() }, - ]); + await emit(`grpc_client_msg_${activeConnectionId}`, 'Cancel'); }, }); @@ -141,7 +94,6 @@ export function useGrpc(url: string | null, requestId: string | null) { bidiStreaming, services: reflect.data, cancel, - messages, isStreaming: activeConnectionId !== null, send, }; diff --git a/src-web/hooks/useGrpcConnections.ts b/src-web/hooks/useGrpcConnections.ts new file mode 100644 index 00000000..85ce2770 --- /dev/null +++ b/src-web/hooks/useGrpcConnections.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { GrpcConnection } from '../lib/models'; + +export function grpcConnectionsQueryKey({ requestId }: { requestId: string }) { + return ['grpc_connections', { requestId }]; +} + +export function useGrpcConnections(requestId: string | null) { + return ( + useQuery({ + enabled: requestId !== null, + initialData: [], + queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }), + queryFn: async () => { + return (await invoke('cmd_list_grpc_connections', { + requestId, + limit: 200, + })) as GrpcConnection[]; + }, + }).data ?? [] + ); +} diff --git a/src-web/hooks/useGrpcMessages.ts b/src-web/hooks/useGrpcMessages.ts new file mode 100644 index 00000000..d7c8f100 --- /dev/null +++ b/src-web/hooks/useGrpcMessages.ts @@ -0,0 +1,23 @@ +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({ + 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 ?? [] + ); +} diff --git a/src-web/hooks/useResponses.ts b/src-web/hooks/useHttpResponses.ts similarity index 52% rename from src-web/hooks/useResponses.ts rename to src-web/hooks/useHttpResponses.ts index ac9cc2f3..8d8b9173 100644 --- a/src-web/hooks/useResponses.ts +++ b/src-web/hooks/useHttpResponses.ts @@ -2,18 +2,18 @@ import { useQuery } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import type { HttpResponse } from '../lib/models'; -export function responsesQueryKey({ requestId }: { requestId: string }) { +export function httpResponsesQueryKey({ requestId }: { requestId: string }) { return ['http_responses', { requestId }]; } -export function useResponses(requestId: string | null) { +export function useHttpResponses(requestId: string | null) { return ( useQuery({ enabled: requestId !== null, initialData: [], - queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }), + queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }), queryFn: async () => { - return (await invoke('cmd_list_responses', { requestId, limit: 200 })) as HttpResponse[]; + return (await invoke('cmd_list_http_responses', { requestId, limit: 200 })) as HttpResponse[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useLatestResponse.ts b/src-web/hooks/useLatestResponse.ts index 9f802715..906af2ed 100644 --- a/src-web/hooks/useLatestResponse.ts +++ b/src-web/hooks/useLatestResponse.ts @@ -1,7 +1,7 @@ import type { HttpResponse } from '../lib/models'; -import { useResponses } from './useResponses'; +import { useHttpResponses } from './useHttpResponses'; export function useLatestResponse(requestId: string | null): HttpResponse | null { - const responses = useResponses(requestId); + const responses = useHttpResponses(requestId); return responses[0] ?? null; } diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 8a752f17..fba838a8 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -12,7 +12,9 @@ export const AUTH_TYPE_BEARER = 'bearer'; export type Model = | Settings | Workspace + | GrpcConnection | GrpcRequest + | GrpcMessage | HttpRequest | HttpResponse | KeyValue @@ -114,6 +116,24 @@ export interface GrpcRequest extends BaseModel { message: string; } +export interface GrpcMessage extends BaseModel { + readonly workspaceId: string; + readonly requestId: string; + readonly connectionId: string; + readonly model: 'grpc_message'; + message: string; + isServer: boolean; + isInfo: boolean; +} + +export interface GrpcConnection extends BaseModel { + readonly workspaceId: string; + readonly requestId: string; + readonly model: 'grpc_connection'; + service: string; + method: string; +} + export interface HttpRequest extends BaseModel { readonly workspaceId: string; readonly model: 'http_request';