Refactor model emit, and recent conn dropdown

This commit is contained in:
Gregory Schier
2024-02-05 10:39:47 -08:00
parent e113f86c5d
commit 3ed00c0955
29 changed files with 691 additions and 458 deletions

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

68
src-tauri/Cargo.lock generated
View File

@@ -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"

View File

@@ -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",

View File

@@ -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

View File

@@ -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",

View File

@@ -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::<Vec<_>>(),
);
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);
};
}

View File

@@ -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<String, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<KeyValue, String> {
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<KeyValue, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<KeyValue, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<Workspace, String> {
let created_workspace = upsert_workspace(&app_handle, Workspace::new(name.to_string()))
async fn cmd_create_workspace(name: &str, app_handle: AppHandle) -> Result<Workspace, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<CookieJar, String> {
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<Wry>,
app_handle: AppHandle,
cookie_jar_id: &str,
) -> Result<CookieJar, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<CookieJar, String> {
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<EnvironmentVariable>,
window: Window<Wry>,
app_handle: AppHandle,
) -> Result<Environment, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<GrpcRequest, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<GrpcRequest, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<HttpRequest, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<HttpRequest, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<Workspace, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<Environment, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<GrpcRequest, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<HttpRequest, String> {
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<Wry>,
app_handle: AppHandle,
request_id: &str,
) -> Result<HttpRequest, String> {
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<Wry>,
app_handle: AppHandle,
request_id: &str,
) -> Result<HttpRequest, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<Folder, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<Folder, String> {
let updated_folder = upsert_folder(&app_handle, folder)
async fn cmd_update_folder(folder: Folder, app_handle: AppHandle) -> Result<Folder, String> {
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<Wry>,
app_handle: AppHandle,
folder_id: &str,
) -> Result<Folder, String> {
let req = delete_folder(&app_handle, folder_id)
async fn cmd_delete_folder(app_handle: AppHandle, folder_id: &str) -> Result<Folder, String> {
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<Wry>,
app_handle: AppHandle,
environment_id: &str,
) -> Result<Environment, String> {
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<Settings, ()> {
#[tauri::command]
async fn cmd_update_settings(
settings: Settings,
window: Window<Wry>,
app_handle: AppHandle,
) -> Result<Settings, String> {
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<Wry>,
app_handle: AppHandle,
) -> Result<HttpResponse, String> {
let response = delete_response(&app_handle, id)
async fn cmd_delete_http_response(id: &str, app_handle: AppHandle) -> Result<HttpResponse, String> {
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<GrpcConnection, String> {
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<Wry>, url: &str) -> Result<(), String> {
#[tauri::command]
async fn cmd_delete_workspace(
window: Window<Wry>,
app_handle: AppHandle,
workspace_id: &str,
) -> Result<Workspace, String> {
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<Wry> {
win
}
/// Emit an event to all windows, with a source window
fn emit_and_return<S: Serialize + Clone, E>(
current_window: &Window<Wry>,
event: &str,
payload: S,
) -> Result<S, E> {
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<S: Serialize + Clone>(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())

View File

@@ -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<Worksp
.await;
for r in list_responses_by_workspace_id(app_handle, id).await? {
delete_response(app_handle, &r.id).await?;
delete_http_response(app_handle, &r.id).await?;
}
Ok(workspace)
emit_deleted_model(app_handle, workspace)
}
pub async fn get_cookie_jar(app_handle: &AppHandle, id: &str) -> Result<CookieJar, sqlx::Error> {
@@ -471,7 +472,7 @@ pub async fn delete_cookie_jar(app_handle: &AppHandle, id: &str) -> Result<Cooki
.execute(&db)
.await;
Ok(cookie_jar)
emit_deleted_model(app_handle, cookie_jar)
}
pub async fn duplicate_grpc_request(
@@ -522,7 +523,10 @@ pub async fn upsert_grpc_request(
.execute(&db)
.await?;
get_grpc_request(app_handle, &id).await
match get_grpc_request(app_handle, &id).await {
Ok(m) => 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<Settings, sqlx::Error> {
@@ -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<Environment, sqlx::Error> {
@@ -924,7 +947,7 @@ pub async fn list_folders(
}
pub async fn delete_folder(app_handle: &AppHandle, id: &str) -> Result<Folder, sqlx::Error> {
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<Folder, s
.execute(&db)
.await;
Ok(env)
emit_deleted_model(app_handle, folder)
}
pub async fn upsert_folder(app_handle: &AppHandle, r: Folder) -> Result<Folder, sqlx::Error> {
@@ -968,7 +991,10 @@ pub async fn upsert_folder(app_handle: &AppHandle, r: Folder) -> Result<Folder,
.execute(&db)
.await?;
get_folder(&app_handle, &id).await
match get_folder(app_handle, &id).await {
Ok(m) => 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<HttpRequest, sqlx::Error> {
pub async fn delete_http_request(
app_handle: &AppHandle,
id: &str,
) -> Result<HttpRequest, sqlx::Error> {
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<HttpRequ
.execute(&db)
.await;
Ok(req)
emit_deleted_model(app_handle, req)
}
#[allow(clippy::too_many_arguments)]
@@ -1148,7 +1180,7 @@ pub async fn create_response(
.execute(&db)
.await?;
get_response(app_handle, &id).await
get_http_response(app_handle, &id).await
}
pub async fn cancel_pending_responses(app_handle: &AppHandle) -> 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<HttpResponse, sqlx::Error> {
pub async fn get_http_response(
app_handle: &AppHandle,
id: &str,
) -> Result<HttpResponse, sqlx::Error> {
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<GrpcConnection, sqlx::Error> {
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<HttpResponse, sqlx::Error> {
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<S: Serialize + Clone>(app_handle: &AppHandle, model: S) -> S {
fn emit_upserted_model<S: Serialize + Clone>(app_handle: &AppHandle, model: S) -> S {
app_handle
.emit_all("upserted_model", model.clone())
.unwrap();
model
}
fn emit_deleted_model<S: Serialize + Clone, E>(app_handle: &AppHandle, model: S) -> Result<S, E> {
app_handle.emit_all("deleted_model", model.clone()).unwrap();
Ok(model)
}
async fn get_db(app_handle: &AppHandle) -> Pool<Sqlite> {
let db_state = app_handle.state::<Mutex<Pool<Sqlite>>>();
let db = &*db_state.lock().await;

View File

@@ -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={() => (
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)]">
<HStack className="px-3 mb-2">
<div className="font-mono">
{grpc.isStreaming ? (
<HStack alignItems="center" space={2}>
<Icon icon="refresh" size="sm" spin />
Connected
</HStack>
) : (
'Done'
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
<HStack alignItems="center" space={2}>
{count('message', messages.filter((m) => !m.isInfo).length)}
{grpc.isStreaming && (
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
)}
</div>
</HStack>
{activeConnection && (
<RecentConnectionsDropdown
connections={connections}
activeConnection={activeConnection}
onPinned={() => {
// todo
}}
/>
)}
</HStack>
<div className="overflow-y-auto h-full">
{...messages.map((m) => (

View File

@@ -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 (
<SplitLayout
name="http_layout"
className="p-3 gap-1.5"
style={style}
firstSlot={({ orientation, style }) => (
<RequestPane style={style} fullHeight={orientation === 'horizontal'} />
<RequestPane
style={style}
activeRequest={activeRequest}
fullHeight={orientation === 'horizontal'}
/>
)}
secondSlot={({ style }) => <ResponsePane style={style} />}
secondSlot={({ style }) => <ResponsePane activeRequest={activeRequest} style={style} />}
/>
);
}

View File

@@ -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 (
<Dropdown
items={[
{
key: 'clear-single',
label: 'Clear Response',
onSelect: deleteResponse.mutate,
disabled: connections.length === 0,
},
{
key: 'clear-all',
label: `Clear ${connections.length} ${pluralize('Response', connections.length)}`,
onSelect: deleteAllResponses.mutate,
hidden: connections.length <= 1,
disabled: connections.length === 0,
},
{ type: 'separator', label: 'History' },
...connections.slice(0, 20).map((c) => ({
key: c.id,
label: (
<HStack space={2} alignItems="center">
<span className="font-mono text-xs">{c.elapsed}ms</span>
</HStack>
),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinned(c),
})),
]}
>
<IconButton
title="Show response history"
icon="chevronDown"
className="ml-auto"
size="sm"
iconSize="md"
/>
</Dropdown>
);
};

View File

@@ -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 (
<Dropdown

View File

@@ -2,7 +2,6 @@ import classNames from 'classnames';
import type { CSSProperties, FormEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendRequest } from '../hooks/useSendRequest';
@@ -38,128 +37,128 @@ interface Props {
style: CSSProperties;
fullHeight: boolean;
className?: string;
activeRequest: HttpRequest;
}
const useActiveTab = createGlobalState<string>('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<number>(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<HttpRequest> = { 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: (
<div className="flex items-center">
Params
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
</div>
),
},
{
value: 'headers',
label: (
<div className="flex items-center">
Headers
<CountBadge count={activeRequest.headers.filter((h) => h.name).length} />
</div>
),
},
{
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<HttpRequest> = { 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: (
<div className="flex items-center">
Params
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
</div>
),
},
{
value: 'headers',
label: (
<div className="flex items-center">
Headers
<CountBadge count={activeRequest.headers.filter((h) => h.name).length} />
</div>
),
},
{
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 ? (

View File

@@ -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<string>('body');
export const ResponsePane = memo(function ResponsePane({ style, className }: Props) {
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(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 (
<div
style={style}

View File

@@ -21,7 +21,7 @@ import { useGrpcRequests } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { useKeyValue } from '../hooks/useKeyValue';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { usePrompt } from '../hooks/usePrompt';
import { useSendManyRequests } from '../hooks/useSendFolder';
import { useSendRequest } from '../hooks/useSendRequest';
@@ -558,7 +558,7 @@ const SidebarItem = forwardRef(function SidebarItem(
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendRequest(itemId);
const sendManyRequests = useSendManyRequests();
const latestResponse = useLatestResponse(itemId);
const latestResponse = useLatestHttpResponse(itemId);
const updateHttpRequest = useUpdateHttpRequest(itemId);
const updateGrpcRequest = useUpdateGrpcRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder();

View File

@@ -14,6 +14,7 @@ import { useOsInfo } from '../hooks/useOsInfo';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useSidebarWidth } from '../hooks/useSidebarWidth';
import { Button } from './core/Button';
import { HotKeyList } from './core/HotKeyList';
import { HStack } from './core/Stacks';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HttpRequestLayout } from './HttpRequestLayout';
@@ -165,10 +166,12 @@ export default function Workspace() {
>
<WorkspaceHeader className="pointer-events-none" />
</HeaderSize>
{activeRequest?.model === 'grpc_request' ? (
{activeRequest == null ? (
<HotKeyList hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']} />
) : activeRequest.model === 'grpc_request' ? (
<GrpcConnectionLayout style={body} />
) : (
<HttpRequestLayout style={body} />
<HttpRequestLayout activeRequest={activeRequest} style={body} />
)}
</div>
);

View File

@@ -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<GrpcConnection>({
mutationFn: async () => {
return await invoke('cmd_delete_grpc_connection', { id: id });
},
onSettled: () => trackEvent('GrpcConnection', 'Delete'),
onSuccess: ({ requestId, id: connectionId }) => {
queryClient.setQueryData<GrpcConnection[]>(
grpcConnectionsQueryKey({ requestId }),
(connections) => (connections ?? []).filter((c) => c.id !== connectionId),
);
},
});
}

View File

@@ -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 }), []);
},
});
}

View File

@@ -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<HttpResponse>({
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 }) => {

View File

@@ -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 () => {

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -9,6 +9,8 @@ export function trackEvent(
| 'Workspace'
| 'Environment'
| 'Folder'
| 'GrpcMessage'
| 'GrpcConnection'
| 'GrpcRequest'
| 'HttpRequest'
| 'HttpResponse'

View File

@@ -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;

View File

@@ -132,6 +132,7 @@ export interface GrpcConnection extends BaseModel {
readonly model: 'grpc_connection';
service: string;
method: string;
elapsed: number;
}
export interface HttpRequest extends BaseModel {