diff --git a/src-tauri/.sqlx/query-1ca8044e7eebe465e740623d4b5491e9fedc0e24a7bd3cc78ef8de795fdaef01.json b/src-tauri/.sqlx/query-3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8.json similarity index 71% rename from src-tauri/.sqlx/query-1ca8044e7eebe465e740623d4b5491e9fedc0e24a7bd3cc78ef8de795fdaef01.json rename to src-tauri/.sqlx/query-3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8.json index 68632e5b..ea56c49f 100644 --- a/src-tauri/.sqlx/query-1ca8044e7eebe465e740623d4b5491e9fedc0e24a7bd3cc78ef8de795fdaef01.json +++ b/src-tauri/.sqlx/query-3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8.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 id = ?\n ", + "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 ", "describe": { "columns": [ { @@ -42,6 +42,11 @@ "name": "method", "ordinal": 7, "type_info": "Text" + }, + { + "name": "elapsed", + "ordinal": 8, + "type_info": "Int64" } ], "parameters": { @@ -55,8 +60,9 @@ false, false, false, + false, false ] }, - "hash": "1ca8044e7eebe465e740623d4b5491e9fedc0e24a7bd3cc78ef8de795fdaef01" + "hash": "3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8" } diff --git a/src-tauri/.sqlx/query-42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd.json b/src-tauri/.sqlx/query-42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd.json new file mode 100644 index 00000000..4276a419 --- /dev/null +++ b/src-tauri/.sqlx/query-42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM grpc_connections\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd" +} diff --git a/src-tauri/.sqlx/query-a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c.json b/src-tauri/.sqlx/query-80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407.json similarity index 68% rename from src-tauri/.sqlx/query-a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c.json rename to src-tauri/.sqlx/query-80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407.json index 92ac12a9..9cacb528 100644 --- a/src-tauri/.sqlx/query-a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c.json +++ b/src-tauri/.sqlx/query-80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407.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 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\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ", "describe": { "columns": [ { @@ -42,6 +42,11 @@ "name": "method", "ordinal": 7, "type_info": "Text" + }, + { + "name": "elapsed", + "ordinal": 8, + "type_info": "Int64" } ], "parameters": { @@ -55,8 +60,9 @@ false, false, false, + false, false ] }, - "hash": "a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c" + "hash": "80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407" } diff --git a/src-tauri/.sqlx/query-9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163.json b/src-tauri/.sqlx/query-9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163.json new file mode 100644 index 00000000..e6d6e8bb --- /dev/null +++ b/src-tauri/.sqlx/query-9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163.json @@ -0,0 +1,12 @@ +{ + "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" +} diff --git a/src-tauri/.sqlx/query-f7df06213eff80e2ce5100b77ec244c83de39048e77c5af0b0b5d188d3279ca4.json b/src-tauri/.sqlx/query-f7df06213eff80e2ce5100b77ec244c83de39048e77c5af0b0b5d188d3279ca4.json deleted file mode 100644 index cc93d656..00000000 --- a/src-tauri/.sqlx/query-f7df06213eff80e2ce5100b77ec244c83de39048e77c5af0b0b5d188d3279ca4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO grpc_connections (\n id, workspace_id, request_id, service, method\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n service = excluded.service,\n method = excluded.method\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "f7df06213eff80e2ce5100b77ec244c83de39048e77c5af0b0b5d188d3279ca4" -} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7d92cec2..d9d840d8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -161,6 +161,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-write-file" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" +dependencies = [ + "nix", + "rand 0.8.5", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -2392,9 +2402,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -2641,6 +2651,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -4220,9 +4241,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4233,9 +4254,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ "ahash", "atoi", @@ -4278,9 +4299,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" dependencies = [ "proc-macro2", "quote", @@ -4291,10 +4312,11 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" dependencies = [ + "atomic-write-file", "dotenvy", "either", "heck 0.4.1", @@ -4317,9 +4339,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", "base64 0.21.5", @@ -4361,9 +4383,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", "base64 0.21.5", @@ -4402,9 +4424,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" dependencies = [ "atoi", "chrono", @@ -4422,6 +4444,7 @@ dependencies = [ "time", "tracing", "url", + "urlencoding", ] [[package]] @@ -5402,6 +5425,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -5666,12 +5695,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.24.0" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webview2-com" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 45c74430..2f38ebc0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,7 +33,7 @@ reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "br cookie = { version = "0.18.0" } serde = { version = "1.0.195", features = ["derive"] } serde_json = { version = "1.0.111", features = ["raw_value"] } -sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] } +sqlx = { version = "0.7.3", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] } tauri = { version = "1.5.2", features = [ "config-toml", "devtools", diff --git a/src-tauri/migrations/20240203164833_grpc.sql b/src-tauri/migrations/20240203164833_grpc.sql index 17430cce..f7dc2d63 100644 --- a/src-tauri/migrations/20240203164833_grpc.sql +++ b/src-tauri/migrations/20240203164833_grpc.sql @@ -33,7 +33,8 @@ CREATE TABLE grpc_connections created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, service TEXT NOT NULL, - method TEXT NOT NULL + method TEXT NOT NULL, + elapsed INTEGER NOT NULL ); CREATE TABLE grpc_messages diff --git a/src-tauri/src/analytics.rs b/src-tauri/src/analytics.rs index 9992ab6a..5388c003 100644 --- a/src-tauri/src/analytics.rs +++ b/src-tauri/src/analytics.rs @@ -14,6 +14,8 @@ pub enum AnalyticsResource { Dialog, Environment, Folder, + GrpcConnection, + GrpcMessage, GrpcRequest, HttpRequest, HttpResponse, @@ -30,6 +32,8 @@ impl AnalyticsResource { "CookieJar" => Some(AnalyticsResource::CookieJar), "Environment" => Some(AnalyticsResource::Environment), "Folder" => Some(AnalyticsResource::Folder), + "GrpcConnection" => Some(AnalyticsResource::GrpcConnection), + "GrpcMessage" => Some(AnalyticsResource::GrpcMessage), "GrpcRequest" => Some(AnalyticsResource::GrpcRequest), "HttpRequest" => Some(AnalyticsResource::HttpRequest), "HttpResponse" => Some(AnalyticsResource::HttpResponse), @@ -91,6 +95,8 @@ fn resource_name(resource: AnalyticsResource) -> &'static str { AnalyticsResource::Environment => "environment", AnalyticsResource::Folder => "folder", AnalyticsResource::GrpcRequest => "grpc_request", + AnalyticsResource::GrpcConnection => "grpc_connection", + AnalyticsResource::GrpcMessage => "grpc_message", AnalyticsResource::HttpRequest => "http_request", AnalyticsResource::HttpResponse => "http_response", AnalyticsResource::KeyValue => "key_value", diff --git a/src-tauri/src/http.rs b/src-tauri/src/http.rs index 1214cb46..0ac86b42 100644 --- a/src-tauri/src/http.rs +++ b/src-tauri/src/http.rs @@ -15,7 +15,7 @@ use reqwest::{multipart, Url}; use sqlx::types::{Json, JsonValue}; use tauri::AppHandle; -use crate::{emit_side_effect, models, render, response_err}; +use crate::{models, render, response_err}; pub async fn send_http_request( app_handle: &AppHandle, @@ -363,9 +363,6 @@ pub async fn send_http_request( response = models::update_response_if_id(app_handle, &response) .await .expect("Failed to update response"); - if !request.id.is_empty() { - emit_side_effect(app_handle, "upserted_model", &response); - } // Copy response to download path, if specified match (download_path, response.body_path.clone()) { @@ -395,13 +392,8 @@ pub async fn send_http_request( .collect::>(), ); cookie_jar.cookies = json_cookies; - match models::upsert_cookie_jar(&app_handle, &cookie_jar).await { - Ok(updated_jar) => { - emit_side_effect(app_handle, "upserted_model", &updated_jar); - } - Err(e) => { - error!("Failed to update cookie jar: {}", e); - } + if let Err(e) = models::upsert_cookie_jar(&app_handle, &cookie_jar).await { + error!("Failed to update cookie jar: {}", e); }; } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5df21a48..5617d1ca 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -20,7 +20,6 @@ use fern::colors::ColoredLevelConfig; use futures::StreamExt; use log::{debug, error, info, warn}; use rand::random; -use serde::Serialize; use serde_json::{json, Value}; use sqlx::migrate::Migrator; use sqlx::types::Json; @@ -42,11 +41,12 @@ use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; 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, 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, + cancel_pending_responses, create_response, delete_all_grpc_connections, + delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, + delete_grpc_connection, delete_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_http_response, get_key_value_raw, + get_or_create_settings, 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, @@ -119,7 +119,6 @@ async fn cmd_grpc_call_unary( .await .map_err(|e| e.to_string())? }; - emit_side_effect(&app_handle, "upserted_model", conn.clone()); { let req = req.clone(); @@ -140,11 +139,11 @@ async fn cmd_grpc_call_unary( }; let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; - let conn_id = generate_id(Some("grpc")); + let start = std::time::Instant::now(); let msg = match grpc_handle .lock() .await - .connect(&conn_id, uri) + .connect(&conn.clone().id, uri) .await .unary( &req.service.unwrap_or_default(), @@ -160,7 +159,7 @@ async fn cmd_grpc_call_unary( message: msg, workspace_id: req.workspace_id, request_id: req.id, - connection_id: conn.id, + connection_id: conn.clone().id, is_server: true, ..Default::default() }, @@ -170,6 +169,16 @@ async fn cmd_grpc_call_unary( Err(e) => return Err(e.to_string()), }; + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ) + .await + .map_err(|e| e.to_string())?; + msg.map_err(|e| e.to_string()) } @@ -194,7 +203,6 @@ async fn cmd_grpc_client_streaming( .await .map_err(|e| e.to_string())? }; - emit_side_effect(&app_handle, "upserted_model", conn.clone()); { let conn = conn.clone(); @@ -296,6 +304,7 @@ async fn cmd_grpc_client_streaming( let event_handler = app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb); + let start = std::time::Instant::now(); let grpc_listen = { let app_handle = app_handle.clone(); let conn = conn.clone(); @@ -308,10 +317,11 @@ async fn cmd_grpc_client_streaming( .client_streaming(&conn.id, uri, &service, &method, in_msg_stream) .await .unwrap(); + let message = serde_json::to_string(&msg).unwrap(); upsert_grpc_message( &app_handle, &GrpcMessage { - message: msg.to_string(), + message, workspace_id: req.workspace_id, request_id: req.id, connection_id: conn.id, @@ -336,12 +346,21 @@ async fn cmd_grpc_client_streaming( message: "Connection completed".to_string(), workspace_id: req.workspace_id, request_id: req.id, - connection_id: conn.id, + connection_id: conn.clone().id, is_info: true, ..Default::default() }, ) - .await.map_err(|e| e.to_string()).unwrap(); + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ) + .await + .unwrap(); }, _ = cancelled_rx.changed() => { upsert_grpc_message( @@ -350,13 +369,22 @@ async fn cmd_grpc_client_streaming( message: "Connection cancelled".to_string(), workspace_id: req.workspace_id, request_id: req.id, - connection_id: conn.id, + connection_id: conn.clone().id, is_info: true, ..Default::default() }, ) + .await.unwrap(); + + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ) .await - .map_err(|e| e.to_string()).unwrap(); + .unwrap(); }, } app_handle.unlisten(event_handler); @@ -388,7 +416,6 @@ async fn cmd_grpc_streaming( .await .map_err(|e| e.to_string())? }; - emit_side_effect(&app_handle, "upserted_model", conn.clone()); { let conn = conn.clone(); @@ -423,6 +450,7 @@ async fn cmd_grpc_streaming( } }; + let start = std::time::Instant::now(); let mut stream = grpc_handle .lock() .await @@ -533,26 +561,40 @@ async fn cmd_grpc_streaming( message: "Connection completed".to_string(), workspace_id: req.workspace_id, request_id: req.id, - connection_id: conn.id, + connection_id: conn.clone().id, is_info: true, ..Default::default() }, ) - .await.map_err(|e| e.to_string()).unwrap(); + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); }, _ = cancelled_rx.changed() => { - upsert_grpc_message( - &app_handle, - &GrpcMessage { - message: "Connection cancelled".to_string(), - workspace_id: req.workspace_id, - request_id: req.id, - connection_id: conn.id, - is_info: true, - ..Default::default() - }, - ) - .await.map_err(|e| e.to_string()).unwrap(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection cancelled".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); }, } app_handle.unlisten(event_handler); @@ -585,7 +627,6 @@ async fn cmd_grpc_server_streaming( .await .map_err(|e| e.to_string())? }; - emit_side_effect(&app_handle, "upserted_model", conn.clone()); { let req = req.clone(); @@ -647,6 +688,7 @@ async fn cmd_grpc_server_streaming( let event_handler = app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb); + let start = std::time::Instant::now(); let grpc_listen = { let conn_id = conn.clone().id; let app_handle = app_handle.clone(); @@ -694,32 +736,46 @@ async fn cmd_grpc_server_streaming( tauri::async_runtime::spawn(async move { tokio::select! { _ = grpc_listen => { - upsert_grpc_message( - &app_handle, - &GrpcMessage { - message: "Connection completed".to_string(), - workspace_id: req.workspace_id, - request_id: req.id, - connection_id: conn.id, - is_info: true, - ..Default::default() - }, - ) - .await.unwrap(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection completed".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); }, _ = cancelled_rx.changed() => { - upsert_grpc_message( - &app_handle, - &GrpcMessage { - message: "Connection cancelled".to_string(), - workspace_id: req.workspace_id, - request_id: req.id, - connection_id: conn.id, - is_info: true, - ..Default::default() - }, - ) - .await.unwrap(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection cancelled".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); }, } app_handle.unlisten(event_handler); @@ -774,7 +830,7 @@ async fn cmd_filter_response( response_id: &str, filter: &str, ) -> Result { - let response = get_response(&app_handle, response_id) + let response = get_http_response(&app_handle, response_id) .await .expect("Failed to get response"); @@ -958,8 +1014,6 @@ async fn cmd_send_request( None }; - emit_side_effect(&app_handle, "upserted_model", response.clone()); - send_http_request( &app_handle, request.clone(), @@ -982,7 +1036,6 @@ async fn response_err( response = update_response_if_id(&app_handle, &response) .await .expect("Failed to update response"); - emit_side_effect(&app_handle, "upserted_model", &response); Ok(response) } @@ -1009,12 +1062,10 @@ async fn cmd_track_event( } #[tauri::command] -async fn cmd_set_update_mode( - update_mode: &str, - window: Window, - app_handle: AppHandle, -) -> Result { - cmd_set_key_value("app", "update_mode", update_mode, window, app_handle).await +async fn cmd_set_update_mode(update_mode: &str, app_handle: AppHandle) -> Result { + cmd_set_key_value("app", "update_mode", update_mode, app_handle) + .await + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1032,64 +1083,46 @@ async fn cmd_set_key_value( namespace: &str, key: &str, value: &str, - window: Window, app_handle: AppHandle, ) -> Result { - let (key_value, created) = set_key_value_raw(&app_handle, namespace, key, value).await; - - if created { - emit_and_return(&window, "upserted_model", key_value) - } else { - emit_and_return(&window, "upserted_model", key_value) - } + let (key_value, _created) = set_key_value_raw(&app_handle, namespace, key, value).await; + Ok(key_value) } #[tauri::command] -async fn cmd_create_workspace( - name: &str, - window: Window, - app_handle: AppHandle, -) -> Result { - let created_workspace = upsert_workspace(&app_handle, Workspace::new(name.to_string())) +async fn cmd_create_workspace(name: &str, app_handle: AppHandle) -> Result { + upsert_workspace(&app_handle, Workspace::new(name.to_string())) .await - .expect("Failed to create Workspace"); - - emit_and_return(&window, "upserted_model", created_workspace) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_update_cookie_jar( cookie_jar: CookieJar, - window: Window, app_handle: AppHandle, ) -> Result { - let updated = upsert_cookie_jar(&app_handle, &cookie_jar) + upsert_cookie_jar(&app_handle, &cookie_jar) .await - .expect("Failed to update cookie jar"); - - emit_and_return(&window, "upserted_model", updated) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_delete_cookie_jar( - window: Window, app_handle: AppHandle, cookie_jar_id: &str, ) -> Result { - let req = delete_cookie_jar(&app_handle, cookie_jar_id) + delete_cookie_jar(&app_handle, cookie_jar_id) .await - .expect("Failed to delete cookie jar"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_create_cookie_jar( workspace_id: &str, name: &str, - window: Window, app_handle: AppHandle, ) -> Result { - let created_cookie_jar = upsert_cookie_jar( + upsert_cookie_jar( &app_handle, &CookieJar { name: name.to_string(), @@ -1098,9 +1131,7 @@ async fn cmd_create_cookie_jar( }, ) .await - .expect("Failed to create cookie jar"); - - emit_and_return(&window, "upserted_model", created_cookie_jar) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1108,10 +1139,9 @@ async fn cmd_create_environment( workspace_id: &str, name: &str, variables: Vec, - window: Window, app_handle: AppHandle, ) -> Result { - let created_environment = upsert_environment( + upsert_environment( &app_handle, Environment { workspace_id: workspace_id.to_string(), @@ -1121,9 +1151,7 @@ async fn cmd_create_environment( }, ) .await - .expect("Failed to create environment"); - - emit_and_return(&window, "upserted_model", created_environment) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1132,10 +1160,9 @@ async fn cmd_create_grpc_request( name: &str, sort_priority: f64, folder_id: Option<&str>, - window: Window, app_handle: AppHandle, ) -> Result { - let created = upsert_grpc_request( + upsert_grpc_request( &app_handle, &GrpcRequest { workspace_id: workspace_id.to_string(), @@ -1146,21 +1173,17 @@ async fn cmd_create_grpc_request( }, ) .await - .expect("Failed to create grpc request"); - - emit_and_return(&window, "upserted_model", created) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_duplicate_grpc_request( id: &str, - window: Window, app_handle: AppHandle, ) -> Result { - let request = duplicate_grpc_request(&app_handle, id) + duplicate_grpc_request(&app_handle, id) .await - .expect("Failed to duplicate grpc request"); - emit_and_return(&window, "upserted_model", request) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1169,10 +1192,9 @@ async fn cmd_create_http_request( name: &str, sort_priority: f64, folder_id: Option<&str>, - window: Window, app_handle: AppHandle, ) -> Result { - let created_request = upsert_http_request( + upsert_http_request( &app_handle, HttpRequest { workspace_id: workspace_id.to_string(), @@ -1184,95 +1206,77 @@ async fn cmd_create_http_request( }, ) .await - .expect("Failed to create http request"); - - emit_and_return(&window, "upserted_model", created_request) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_duplicate_http_request( id: &str, - window: Window, app_handle: AppHandle, ) -> Result { - let request = duplicate_http_request(&app_handle, id) + duplicate_http_request(&app_handle, id) .await - .expect("Failed to duplicate http request"); - emit_and_return(&window, "upserted_model", request) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_update_workspace( workspace: Workspace, - window: Window, app_handle: AppHandle, ) -> Result { - let updated_workspace = upsert_workspace(&app_handle, workspace) + upsert_workspace(&app_handle, workspace) .await - .expect("Failed to update request"); - - emit_and_return(&window, "upserted_model", updated_workspace) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_update_environment( environment: Environment, - window: Window, app_handle: AppHandle, ) -> Result { - let updated_environment = upsert_environment(&app_handle, environment) + upsert_environment(&app_handle, environment) .await - .expect("Failed to update environment"); - - emit_and_return(&window, "upserted_model", updated_environment) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_update_grpc_request( request: GrpcRequest, - window: Window, app_handle: AppHandle, ) -> Result { - let updated_request = upsert_grpc_request(&app_handle, &request) + upsert_grpc_request(&app_handle, &request) .await - .expect("Failed to update grpc request"); - emit_and_return(&window, "upserted_model", updated_request) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_update_http_request( request: HttpRequest, - window: Window, app_handle: AppHandle, ) -> Result { - let updated_request = upsert_http_request(&app_handle, request) + upsert_http_request(&app_handle, request) .await - .expect("Failed to update request"); - emit_and_return(&window, "upserted_model", updated_request) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_delete_grpc_request( - window: Window, app_handle: AppHandle, request_id: &str, ) -> Result { - let req = delete_request(&app_handle, request_id) + delete_http_request(&app_handle, request_id) .await - .expect("Failed to delete request"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_delete_http_request( - window: Window, app_handle: AppHandle, request_id: &str, ) -> Result { - let req = delete_request(&app_handle, request_id) + delete_http_request(&app_handle, request_id) .await - .expect("Failed to delete request"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1291,10 +1295,9 @@ async fn cmd_create_folder( name: &str, sort_priority: f64, folder_id: Option<&str>, - window: Window, app_handle: AppHandle, ) -> Result { - let created_request = upsert_folder( + upsert_folder( &app_handle, Folder { workspace_id: workspace_id.to_string(), @@ -1305,45 +1308,31 @@ async fn cmd_create_folder( }, ) .await - .expect("Failed to create folder"); - - emit_and_return(&window, "upserted_model", created_request) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn cmd_update_folder( - folder: Folder, - window: Window, - app_handle: AppHandle, -) -> Result { - let updated_folder = upsert_folder(&app_handle, folder) +async fn cmd_update_folder(folder: Folder, app_handle: AppHandle) -> Result { + upsert_folder(&app_handle, folder) .await - .expect("Failed to update request"); - emit_and_return(&window, "upserted_model", updated_folder) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn cmd_delete_folder( - window: Window, - app_handle: AppHandle, - folder_id: &str, -) -> Result { - let req = delete_folder(&app_handle, folder_id) +async fn cmd_delete_folder(app_handle: AppHandle, folder_id: &str) -> Result { + delete_folder(&app_handle, folder_id) .await - .expect("Failed to delete folder"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] async fn cmd_delete_environment( - window: Window, app_handle: AppHandle, environment_id: &str, ) -> Result { - let req = delete_environment(&app_handle, environment_id) + delete_environment(&app_handle, environment_id) .await - .expect("Failed to delete environment"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1409,14 +1398,11 @@ async fn cmd_get_settings(app_handle: AppHandle) -> Result { #[tauri::command] async fn cmd_update_settings( settings: Settings, - window: Window, app_handle: AppHandle, ) -> Result { - let updated_settings = update_settings(&app_handle, settings) + update_settings(&app_handle, settings) .await - .expect("Failed to update settings"); - - emit_and_return(&window, "upserted_model", updated_settings) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1497,20 +1483,38 @@ async fn cmd_list_http_responses( } #[tauri::command] -async fn cmd_delete_response( - id: &str, - window: Window, - app_handle: AppHandle, -) -> Result { - let response = delete_response(&app_handle, id) +async fn cmd_delete_http_response(id: &str, app_handle: AppHandle) -> Result { + delete_http_response(&app_handle, id) .await - .expect("Failed to delete response"); - emit_and_return(&window, "deleted_model", response) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn cmd_delete_all_responses(request_id: &str, app_handle: AppHandle) -> Result<(), String> { - delete_all_responses(&app_handle, request_id) +async fn cmd_delete_grpc_connection( + id: &str, + app_handle: AppHandle, +) -> Result { + delete_grpc_connection(&app_handle, id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_delete_all_grpc_connections( + request_id: &str, + app_handle: AppHandle, +) -> Result<(), String> { + delete_all_grpc_connections(&app_handle, request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_delete_all_http_responses( + request_id: &str, + app_handle: AppHandle, +) -> Result<(), String> { + delete_all_http_responses(&app_handle, request_id) .await .map_err(|e| e.to_string()) } @@ -1544,14 +1548,12 @@ async fn cmd_new_window(window: Window, url: &str) -> Result<(), String> { #[tauri::command] async fn cmd_delete_workspace( - window: Window, app_handle: AppHandle, workspace_id: &str, ) -> Result { - let workspace = delete_workspace(&app_handle, workspace_id) + delete_workspace(&app_handle, workspace_id) .await - .expect("Failed to delete Workspace"); - emit_and_return(&window, "deleted_model", workspace) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1646,13 +1648,15 @@ fn main() { cmd_create_grpc_request, cmd_create_http_request, cmd_create_workspace, - cmd_delete_all_responses, + cmd_delete_all_http_responses, + cmd_delete_all_grpc_connections, cmd_delete_cookie_jar, cmd_delete_environment, cmd_delete_folder, cmd_delete_grpc_request, + cmd_delete_grpc_connection, cmd_delete_http_request, - cmd_delete_response, + cmd_delete_http_response, cmd_delete_workspace, cmd_duplicate_http_request, cmd_duplicate_grpc_request, @@ -1858,21 +1862,6 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> Window { win } -/// Emit an event to all windows, with a source window -fn emit_and_return( - current_window: &Window, - event: &str, - payload: S, -) -> Result { - current_window.emit_all(event, &payload).unwrap(); - Ok(payload) -} - -/// Emit an event to all windows, used for side-effects where there is no source window to attribute. This -fn emit_side_effect(app_handle: &AppHandle, event: &str, payload: S) { - app_handle.emit_all(event, &payload).unwrap(); -} - async fn get_update_mode(app_handle: &AppHandle) -> UpdateMode { let settings = get_or_create_settings(app_handle).await; update_mode_from_str(settings.update_channel.as_str()) diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 7f70cce1..c285ba68 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -217,6 +217,7 @@ pub struct GrpcConnection { pub updated_at: NaiveDateTime, pub service: String, pub method: String, + pub elapsed: i64, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -416,10 +417,10 @@ pub async fn delete_workspace(app_handle: &AppHandle, id: &str) -> Result Result { @@ -471,7 +472,7 @@ pub async fn delete_cookie_jar(app_handle: &AppHandle, id: &str) -> Result Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn get_grpc_request( @@ -577,24 +581,29 @@ pub async fn upsert_grpc_connection( sqlx::query!( r#" INSERT INTO grpc_connections ( - id, workspace_id, request_id, service, method + id, workspace_id, request_id, service, method, elapsed ) - VALUES (?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, service = excluded.service, - method = excluded.method + method = excluded.method, + elapsed = excluded.elapsed "#, id, connection.workspace_id, connection.request_id, connection.service, connection.method, + connection.elapsed, ) .execute(&db) .await?; - get_grpc_connection(app_handle, &id).await + match get_grpc_connection(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn get_grpc_connection( @@ -605,7 +614,9 @@ pub async fn get_grpc_connection( sqlx::query_as!( GrpcConnection, r#" - SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method + SELECT + id, model, workspace_id, request_id, created_at, updated_at, service, + method, elapsed FROM grpc_connections WHERE id = ? "#, @@ -623,7 +634,9 @@ pub async fn list_grpc_connections( sqlx::query_as!( GrpcConnection, r#" - SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method + SELECT + id, model, workspace_id, request_id, created_at, updated_at, service, + method, elapsed FROM grpc_connections WHERE request_id = ? ORDER BY created_at DESC @@ -666,9 +679,8 @@ pub async fn upsert_grpc_message( .execute(&db) .await?; - let msg = get_grpc_message(app_handle, &id).await; - match msg { - Ok(msg) => Ok(emit_upserted_model(app_handle, msg.clone()).await), + match get_grpc_message(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), Err(e) => Err(e), } } @@ -743,7 +755,10 @@ pub async fn upsert_cookie_jar( .execute(&db) .await?; - get_cookie_jar(&app_handle, &id).await + match get_cookie_jar(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn list_environments( @@ -781,7 +796,7 @@ pub async fn delete_environment( .execute(&db) .await; - Ok(env) + emit_deleted_model(app_handle, env) } async fn get_settings(app_handle: &AppHandle) -> Result { @@ -837,7 +852,11 @@ pub async fn update_settings( ) .execute(&db) .await?; - get_settings(app_handle).await + + match get_settings(app_handle).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn upsert_environment( @@ -868,7 +887,11 @@ pub async fn upsert_environment( ) .execute(&db) .await?; - get_environment(app_handle, &id).await + + match get_environment(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn get_environment(app_handle: &AppHandle, id: &str) -> Result { @@ -924,7 +947,7 @@ pub async fn list_folders( } pub async fn delete_folder(app_handle: &AppHandle, id: &str) -> Result { - let env = get_folder(app_handle, id).await?; + let folder = get_folder(app_handle, id).await?; let db = get_db(app_handle).await; let _ = sqlx::query!( r#" @@ -936,7 +959,7 @@ pub async fn delete_folder(app_handle: &AppHandle, id: &str) -> Result Result { @@ -968,7 +991,10 @@ pub async fn upsert_folder(app_handle: &AppHandle, r: Folder) -> Result Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn duplicate_http_request( @@ -1032,7 +1058,10 @@ pub async fn upsert_http_request( .execute(&db) .await?; - get_http_request(app_handle, &id).await + match get_http_request(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn list_requests( @@ -1084,11 +1113,14 @@ pub async fn get_http_request( .await } -pub async fn delete_request(app_handle: &AppHandle, id: &str) -> Result { +pub async fn delete_http_request( + app_handle: &AppHandle, + id: &str, +) -> Result { let req = get_http_request(app_handle, id).await?; // DB deletes will cascade but this will delete the files - delete_all_responses(app_handle, id).await?; + delete_all_http_responses(app_handle, id).await?; let db = get_db(app_handle).await; let _ = sqlx::query!( @@ -1101,7 +1133,7 @@ pub async fn delete_request(app_handle: &AppHandle, id: &str) -> Result Result<(), sqlx::Error> { @@ -1214,7 +1246,10 @@ pub async fn upsert_workspace( .execute(&db) .await?; - get_workspace(app_handle, &id).await + match get_workspace(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn update_response( @@ -1245,10 +1280,17 @@ pub async fn update_response( ) .execute(&db) .await?; - get_response(app_handle, &response.id).await + + match get_http_response(app_handle, &response.id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } -pub async fn get_response(app_handle: &AppHandle, id: &str) -> Result { +pub async fn get_http_response( + app_handle: &AppHandle, + id: &str, +) -> Result { let db = get_db(app_handle).await; sqlx::query_as!( HttpResponse, @@ -1317,11 +1359,31 @@ pub async fn list_responses_by_workspace_id( .await } -pub async fn delete_response( +pub async fn delete_grpc_connection( + app_handle: &AppHandle, + id: &str, +) -> Result { + let resp = get_grpc_connection(app_handle, id).await?; + + let db = get_db(app_handle).await; + let _ = sqlx::query!( + r#" + DELETE FROM grpc_connections + WHERE id = ? + "#, + id, + ) + .execute(&db) + .await; + + emit_deleted_model(app_handle, resp) +} + +pub async fn delete_http_response( app_handle: &AppHandle, id: &str, ) -> Result { - let resp = get_response(app_handle, id).await?; + let resp = get_http_response(app_handle, id).await?; // Delete the body file if it exists if let Some(p) = resp.body_path.clone() { @@ -1341,15 +1403,25 @@ pub async fn delete_response( .execute(&db) .await; - Ok(resp) + emit_deleted_model(app_handle, resp) } -pub async fn delete_all_responses( +pub async fn delete_all_grpc_connections( + app_handle: &AppHandle, + request_id: &str, +) -> Result<(), sqlx::Error> { + for r in list_grpc_connections(app_handle, request_id).await? { + delete_grpc_connection(app_handle, &r.id).await?; + } + Ok(()) +} + +pub async fn delete_all_http_responses( app_handle: &AppHandle, request_id: &str, ) -> Result<(), sqlx::Error> { for r in list_responses(app_handle, request_id, None).await? { - delete_response(app_handle, &r.id).await?; + delete_http_response(app_handle, &r.id).await?; } Ok(()) } @@ -1406,13 +1478,18 @@ pub async fn get_workspace_export_resources( }; } -async fn emit_upserted_model(app_handle: &AppHandle, model: S) -> S { +fn emit_upserted_model(app_handle: &AppHandle, model: S) -> S { app_handle .emit_all("upserted_model", model.clone()) .unwrap(); model } +fn emit_deleted_model(app_handle: &AppHandle, model: S) -> Result { + app_handle.emit_all("deleted_model", model.clone()).unwrap(); + Ok(model) +} + async fn get_db(app_handle: &AppHandle) -> Pool { let db_state = app_handle.state::>>(); let db = &*db_state.lock().await; diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx index 6df09f20..5d9c0a25 100644 --- a/src-web/components/GrpcConnectionLayout.tsx +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -9,6 +9,7 @@ import { useGrpc } from '../hooks/useGrpc'; import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useGrpcMessages } from '../hooks/useGrpcMessages'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; +import { count, pluralize } from '../lib/pluralize'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { HotKeyList } from './core/HotKeyList'; @@ -20,6 +21,7 @@ import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; import { HStack, VStack } from './core/Stacks'; import { GrpcEditor } from './GrpcEditor'; +import { RecentConnectionsDropdown } from './RecentConnectionsDropdown'; import { UrlBar } from './UrlBar'; interface Props { @@ -266,7 +268,7 @@ export function GrpcConnectionLayout({ style }: Props) { className={classNames( 'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1', 'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight', - 'shadow shadow-gray-100 dark:shadow-gray-0 relative pt-1', + 'shadow shadow-gray-100 dark:shadow-gray-0 relative', )} > {grpc.unary.error ? ( @@ -286,18 +288,23 @@ export function GrpcConnectionLayout({ style }: Props) { } minHeightPx={20} firstSlot={() => ( -
- -
- {grpc.isStreaming ? ( - - - Connected - - ) : ( - 'Done' +
+ + + {count('message', messages.filter((m) => !m.isInfo).length)} + {grpc.isStreaming && ( + )} -
+ + {activeConnection && ( + { + // todo + }} + /> + )}
{...messages.map((m) => ( diff --git a/src-web/components/HttpRequestLayout.tsx b/src-web/components/HttpRequestLayout.tsx index 98f4bd43..50e68ad3 100644 --- a/src-web/components/HttpRequestLayout.tsx +++ b/src-web/components/HttpRequestLayout.tsx @@ -1,23 +1,29 @@ import type { CSSProperties } from 'react'; import React from 'react'; +import type { HttpRequest } from '../lib/models'; import { SplitLayout } from './core/SplitLayout'; import { RequestPane } from './RequestPane'; import { ResponsePane } from './ResponsePane'; interface Props { + activeRequest: HttpRequest; style: CSSProperties; } -export function HttpRequestLayout({ style }: Props) { +export function HttpRequestLayout({ activeRequest, style }: Props) { return ( ( - + )} - secondSlot={({ style }) => } + secondSlot={({ style }) => } /> ); } diff --git a/src-web/components/RecentConnectionsDropdown.tsx b/src-web/components/RecentConnectionsDropdown.tsx new file mode 100644 index 00000000..442b77cc --- /dev/null +++ b/src-web/components/RecentConnectionsDropdown.tsx @@ -0,0 +1,62 @@ +import { useDeleteGrpcConnection } from '../hooks/useDeleteGrpcConnection'; +import { useDeleteGrpcConnections } from '../hooks/useDeleteGrpcConnections'; +import type { GrpcConnection } from '../lib/models'; +import { pluralize } from '../lib/pluralize'; +import { Dropdown } from './core/Dropdown'; +import { Icon } from './core/Icon'; +import { IconButton } from './core/IconButton'; +import { HStack } from './core/Stacks'; + +interface Props { + connections: GrpcConnection[]; + activeConnection: GrpcConnection; + onPinned: (r: GrpcConnection) => void; +} + +export const RecentConnectionsDropdown = function ResponsePane({ + activeConnection, + connections, + onPinned, +}: Props) { + const deleteResponse = useDeleteGrpcConnection(activeConnection?.id ?? null); + const deleteAllResponses = useDeleteGrpcConnections(activeConnection?.requestId); + + return ( + ({ + key: c.id, + label: ( + + {c.elapsed}ms + + ), + leftSlot: activeConnection?.id === c.id ? : , + onSelect: () => onPinned(c), + })), + ]} + > + + + ); +}; diff --git a/src-web/components/RecentResponsesDropdown.tsx b/src-web/components/RecentResponsesDropdown.tsx index e2b2210c..f28340b8 100644 --- a/src-web/components/RecentResponsesDropdown.tsx +++ b/src-web/components/RecentResponsesDropdown.tsx @@ -1,5 +1,5 @@ -import { useDeleteResponse } from '../hooks/useDeleteResponse'; -import { useDeleteResponses } from '../hooks/useDeleteResponses'; +import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse'; +import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses'; import type { HttpResponse } from '../lib/models'; import { Dropdown } from './core/Dropdown'; import { pluralize } from '../lib/pluralize'; @@ -19,8 +19,8 @@ export const RecentResponsesDropdown = function ResponsePane({ responses, onPinnedResponse, }: Props) { - const deleteResponse = useDeleteResponse(activeResponse?.id ?? null); - const deleteAllResponses = useDeleteResponses(activeResponse?.requestId); + const deleteResponse = useDeleteHttpResponse(activeResponse?.id ?? null); + const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId); return ( ('body'); -export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) { - const activeRequest = useActiveRequest('http_request'); - const activeRequestId = activeRequest?.id ?? null; +export const RequestPane = memo(function RequestPane({ + style, + fullHeight, + className, + activeRequest, +}: Props) { + const activeRequestId = activeRequest.id; const updateRequest = useUpdateHttpRequest(activeRequestId); const [activeTab, setActiveTab] = useActiveTab(); const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState(0); - const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest?.id ?? null); + const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); const tabs: TabItem[] = useMemo( - () => - activeRequest === null - ? [] - : [ - { - value: 'body', - options: { - value: activeRequest.bodyType, - items: [ - { type: 'separator', label: 'Form Data' }, - { label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, - { label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, - { type: 'separator', label: 'Text Content' }, - { label: 'JSON', value: BODY_TYPE_JSON }, - { label: 'XML', value: BODY_TYPE_XML }, - { label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, - { type: 'separator', label: 'Other' }, - { label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE }, - ], - onChange: async (bodyType) => { - const patch: Partial = { bodyType }; - if (bodyType === BODY_TYPE_NONE) { - patch.headers = activeRequest?.headers.filter( - (h) => h.name.toLowerCase() !== 'content-type', - ); - } else if ( - bodyType === BODY_TYPE_FORM_URLENCODED || - bodyType === BODY_TYPE_FORM_MULTIPART || - bodyType === BODY_TYPE_JSON || - bodyType === BODY_TYPE_XML - ) { - patch.method = 'POST'; - patch.headers = [ - ...(activeRequest?.headers.filter( - (h) => h.name.toLowerCase() !== 'content-type', - ) ?? []), - { - name: 'Content-Type', - value: bodyType, - enabled: true, - }, - ]; - } else if (bodyType == BODY_TYPE_GRAPHQL) { - patch.method = 'POST'; - patch.headers = [ - ...(activeRequest?.headers.filter( - (h) => h.name.toLowerCase() !== 'content-type', - ) ?? []), - { - name: 'Content-Type', - value: 'application/json', - enabled: true, - }, - ]; - } - - // Force update header editor so any changed headers are reflected - setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100); - - updateRequest.mutate(patch); - }, - }, - }, - { - value: 'params', - label: ( -
- Params - p.name).length} /> -
- ), - }, - { - value: 'headers', - label: ( -
- Headers - h.name).length} /> -
- ), - }, - { - value: 'auth', - label: 'Auth', - options: { - value: activeRequest.authenticationType, - items: [ - { label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC }, - { label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER }, - { type: 'separator' }, - { label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE }, - ], - onChange: async (authenticationType) => { - let authentication: HttpRequest['authentication'] = activeRequest?.authentication; - if (authenticationType === AUTH_TYPE_BASIC) { - authentication = { - username: authentication.username ?? '', - password: authentication.password ?? '', - }; - } else if (authenticationType === AUTH_TYPE_BEARER) { - authentication = { - token: authentication.token ?? '', - }; - } - updateRequest.mutate({ authenticationType, authentication }); - }, - }, - }, + () => [ + { + value: 'body', + options: { + value: activeRequest.bodyType, + items: [ + { type: 'separator', label: 'Form Data' }, + { label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, + { label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, + { type: 'separator', label: 'Text Content' }, + { label: 'JSON', value: BODY_TYPE_JSON }, + { label: 'XML', value: BODY_TYPE_XML }, + { label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, + { type: 'separator', label: 'Other' }, + { label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE }, ], + onChange: async (bodyType) => { + const patch: Partial = { bodyType }; + if (bodyType === BODY_TYPE_NONE) { + patch.headers = activeRequest.headers.filter( + (h) => h.name.toLowerCase() !== 'content-type', + ); + } else if ( + bodyType === BODY_TYPE_FORM_URLENCODED || + bodyType === BODY_TYPE_FORM_MULTIPART || + bodyType === BODY_TYPE_JSON || + bodyType === BODY_TYPE_XML + ) { + patch.method = 'POST'; + patch.headers = [ + ...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ?? + []), + { + name: 'Content-Type', + value: bodyType, + enabled: true, + }, + ]; + } else if (bodyType == BODY_TYPE_GRAPHQL) { + patch.method = 'POST'; + patch.headers = [ + ...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ?? + []), + { + name: 'Content-Type', + value: 'application/json', + enabled: true, + }, + ]; + } + + // Force update header editor so any changed headers are reflected + setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100); + + updateRequest.mutate(patch); + }, + }, + }, + { + value: 'params', + label: ( +
+ Params + p.name).length} /> +
+ ), + }, + { + value: 'headers', + label: ( +
+ Headers + h.name).length} /> +
+ ), + }, + { + value: 'auth', + label: 'Auth', + options: { + value: activeRequest.authenticationType, + items: [ + { label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC }, + { label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER }, + { type: 'separator' }, + { label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE }, + ], + onChange: async (authenticationType) => { + let authentication: HttpRequest['authentication'] = activeRequest.authentication; + if (authenticationType === AUTH_TYPE_BASIC) { + authentication = { + username: authentication.username ?? '', + password: authentication.password ?? '', + }; + } else if (authenticationType === AUTH_TYPE_BEARER) { + authentication = { + token: authentication.token ?? '', + }; + } + updateRequest.mutate({ authenticationType, authentication }); + }, + }, + }, + ], [activeRequest, updateRequest], ); @@ -180,7 +179,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN [updateRequest], ); - const sendRequest = useSendRequest(activeRequest?.id ?? null); + const sendRequest = useSendRequest(activeRequest.id ?? null); const handleSend = useCallback( async (e: FormEvent) => { e.preventDefault(); @@ -267,7 +266,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN placeholder="..." className="!bg-gray-50" heightMode={fullHeight ? 'full' : 'auto'} - defaultValue={`${activeRequest?.body?.text ?? ''}`} + defaultValue={`${activeRequest.body?.text ?? ''}`} contentType="application/json" onChange={handleBodyTextChange} format={tryFormatJson} @@ -280,7 +279,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN placeholder="..." className="!bg-gray-50" heightMode={fullHeight ? 'full' : 'auto'} - defaultValue={`${activeRequest?.body?.text ?? ''}`} + defaultValue={`${activeRequest.body?.text ?? ''}`} contentType="text/xml" onChange={handleBodyTextChange} /> @@ -289,7 +288,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN forceUpdateKey={forceUpdateKey} baseRequest={activeRequest} className="!bg-gray-50" - defaultValue={`${activeRequest?.body?.text ?? ''}`} + defaultValue={`${activeRequest.body?.text ?? ''}`} onChange={handleBodyTextChange} /> ) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? ( diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index da3e744d..23840e63 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -2,18 +2,16 @@ import classNames from 'classnames'; import type { CSSProperties } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { createGlobalState } from 'react-use'; -import { useActiveRequest } from '../hooks/useActiveRequest'; -import { useLatestResponse } from '../hooks/useLatestResponse'; -import { useResponseContentType } from '../hooks/useResponseContentType'; import { useHttpResponses } from '../hooks/useHttpResponses'; +import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; +import { useResponseContentType } from '../hooks/useResponseContentType'; import { useResponseViewMode } from '../hooks/useResponseViewMode'; -import type { HttpResponse } from '../lib/models'; +import type { HttpRequest, HttpResponse } from '../lib/models'; import { isResponseLoading } from '../lib/models'; import { Banner } from './core/Banner'; import { CountBadge } from './core/CountBadge'; import { DurationTag } from './core/DurationTag'; import { HotKeyList } from './core/HotKeyList'; -import { JsonAttributeTree } from './core/JsonAttributeTree'; import { SizeTag } from './core/SizeTag'; import { HStack } from './core/Stacks'; import { StatusTag } from './core/StatusTag'; @@ -24,22 +22,21 @@ import { RecentResponsesDropdown } from './RecentResponsesDropdown'; import { ResponseHeaders } from './ResponseHeaders'; import { CsvViewer } from './responseViewers/CsvViewer'; import { ImageViewer } from './responseViewers/ImageViewer'; -import { JsonViewer } from './responseViewers/JsonViewer'; import { TextViewer } from './responseViewers/TextViewer'; import { WebPageViewer } from './responseViewers/WebPageViewer'; interface Props { style?: CSSProperties; className?: string; + activeRequest: HttpRequest; } const useActiveTab = createGlobalState('body'); -export const ResponsePane = memo(function ResponsePane({ style, className }: Props) { +export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) { const [pinnedResponseId, setPinnedResponseId] = useState(null); - const activeRequest = useActiveRequest(); - const latestResponse = useLatestResponse(activeRequest?.id ?? null); - const responses = useHttpResponses(activeRequest?.id ?? null); + const latestResponse = useLatestHttpResponse(activeRequest.id); + const responses = useHttpResponses(activeRequest.id); const activeResponse: HttpResponse | null = pinnedResponseId ? responses.find((r) => r.id === pinnedResponseId) ?? null : latestResponse ?? null; @@ -87,10 +84,6 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro [activeResponse?.headers, contentType, setViewMode, viewMode], ); - if (activeRequest === null) { - return null; - } - return (
- {activeRequest?.model === 'grpc_request' ? ( + {activeRequest == null ? ( + + ) : activeRequest.model === 'grpc_request' ? ( ) : ( - + )}
); diff --git a/src-web/hooks/useDeleteGrpcConnection.ts b/src-web/hooks/useDeleteGrpcConnection.ts new file mode 100644 index 00000000..e01d8f23 --- /dev/null +++ b/src-web/hooks/useDeleteGrpcConnection.ts @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import type { GrpcConnection } from '../lib/models'; +import { grpcConnectionsQueryKey } from './useGrpcConnections'; + +export function useDeleteGrpcConnection(id: string | null) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + return await invoke('cmd_delete_grpc_connection', { id: id }); + }, + onSettled: () => trackEvent('GrpcConnection', 'Delete'), + onSuccess: ({ requestId, id: connectionId }) => { + queryClient.setQueryData( + grpcConnectionsQueryKey({ requestId }), + (connections) => (connections ?? []).filter((c) => c.id !== connectionId), + ); + }, + }); +} diff --git a/src-web/hooks/useDeleteGrpcConnections.ts b/src-web/hooks/useDeleteGrpcConnections.ts new file mode 100644 index 00000000..5440ba2b --- /dev/null +++ b/src-web/hooks/useDeleteGrpcConnections.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import { grpcConnectionsQueryKey } from './useGrpcConnections'; + +export function useDeleteGrpcConnections(requestId?: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + if (requestId === undefined) return; + await invoke('cmd_delete_all_grpc_connections', { requestId }); + }, + onSettled: () => trackEvent('GrpcConnection', 'DeleteMany'), + onSuccess: async () => { + if (requestId === undefined) return; + queryClient.setQueryData(grpcConnectionsQueryKey({ requestId }), []); + }, + }); +} diff --git a/src-web/hooks/useDeleteResponse.ts b/src-web/hooks/useDeleteHttpResponse.ts similarity index 84% rename from src-web/hooks/useDeleteResponse.ts rename to src-web/hooks/useDeleteHttpResponse.ts index 3f460acd..3ee0bfee 100644 --- a/src-web/hooks/useDeleteResponse.ts +++ b/src-web/hooks/useDeleteHttpResponse.ts @@ -4,11 +4,11 @@ import { trackEvent } from '../lib/analytics'; import type { HttpResponse } from '../lib/models'; import { httpResponsesQueryKey } from './useHttpResponses'; -export function useDeleteResponse(id: string | null) { +export function useDeleteHttpResponse(id: string | null) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { - return await invoke('cmd_delete_response', { id: id }); + return await invoke('cmd_delete_http_response', { id: id }); }, onSettled: () => trackEvent('HttpResponse', 'Delete'), onSuccess: ({ requestId, id: responseId }) => { diff --git a/src-web/hooks/useDeleteResponses.ts b/src-web/hooks/useDeleteHttpResponses.ts similarity index 81% rename from src-web/hooks/useDeleteResponses.ts rename to src-web/hooks/useDeleteHttpResponses.ts index 5a5fd366..264a87dd 100644 --- a/src-web/hooks/useDeleteResponses.ts +++ b/src-web/hooks/useDeleteHttpResponses.ts @@ -3,12 +3,12 @@ import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; import { httpResponsesQueryKey } from './useHttpResponses'; -export function useDeleteResponses(requestId?: string) { +export function useDeleteHttpResponses(requestId?: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { if (requestId === undefined) return; - await invoke('cmd_delete_all_responses', { requestId }); + await invoke('cmd_delete_all_http_responses', { requestId }); }, onSettled: () => trackEvent('HttpResponse', 'DeleteMany'), onSuccess: async () => { diff --git a/src-web/hooks/useIsResponseLoading.ts b/src-web/hooks/useIsResponseLoading.ts index 56d8b33d..4fd86abf 100644 --- a/src-web/hooks/useIsResponseLoading.ts +++ b/src-web/hooks/useIsResponseLoading.ts @@ -1,8 +1,8 @@ import { isResponseLoading } from '../lib/models'; -import { useLatestResponse } from './useLatestResponse'; +import { useLatestHttpResponse } from './useLatestHttpResponse'; export function useIsResponseLoading(requestId: string | null): boolean { - const response = useLatestResponse(requestId); + const response = useLatestHttpResponse(requestId); if (response === null) return false; return isResponseLoading(response); } diff --git a/src-web/hooks/useLatestResponse.ts b/src-web/hooks/useLatestHttpResponse.ts similarity index 68% rename from src-web/hooks/useLatestResponse.ts rename to src-web/hooks/useLatestHttpResponse.ts index 906af2ed..085aab80 100644 --- a/src-web/hooks/useLatestResponse.ts +++ b/src-web/hooks/useLatestHttpResponse.ts @@ -1,7 +1,7 @@ import type { HttpResponse } from '../lib/models'; import { useHttpResponses } from './useHttpResponses'; -export function useLatestResponse(requestId: string | null): HttpResponse | null { +export function useLatestHttpResponse(requestId: string | null): HttpResponse | null { const responses = useHttpResponses(requestId); return responses[0] ?? null; } diff --git a/src-web/lib/analytics.ts b/src-web/lib/analytics.ts index 742ae760..0865a0cf 100644 --- a/src-web/lib/analytics.ts +++ b/src-web/lib/analytics.ts @@ -9,6 +9,8 @@ export function trackEvent( | 'Workspace' | 'Environment' | 'Folder' + | 'GrpcMessage' + | 'GrpcConnection' | 'GrpcRequest' | 'HttpRequest' | 'HttpResponse' diff --git a/src-web/lib/fallbackRequestName.ts b/src-web/lib/fallbackRequestName.ts index bb7a0ed5..712df566 100644 --- a/src-web/lib/fallbackRequestName.ts +++ b/src-web/lib/fallbackRequestName.ts @@ -14,12 +14,17 @@ export function fallbackRequestName(r: HttpRequest | GrpcRequest | null): string const fixedUrl = r.url.match(/^https?:\/\//) ? r.url : 'http://' + r.url; - try { - const url = new URL(fixedUrl); - const pathname = url.pathname === '/' ? '' : url.pathname; - return `${url.host}${pathname}`; - } catch (_) { - // Nothing + if (r.model === 'grpc_request' && r.service != null && r.method != null) { + const shortService = r.service.split('.').pop(); + return `${shortService}/${r.method}`; + } else { + try { + const url = new URL(fixedUrl); + const pathname = url.pathname === '/' ? '' : url.pathname; + return `${url.host}${pathname}`; + } catch (_) { + // Nothing + } } return r.url; diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index fba838a8..74b7a5b5 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -132,6 +132,7 @@ export interface GrpcConnection extends BaseModel { readonly model: 'grpc_connection'; service: string; method: string; + elapsed: number; } export interface HttpRequest extends BaseModel {