Use req/conn/msg models in unary/server

This commit is contained in:
Gregory Schier
2024-02-04 11:57:12 -08:00
parent d2b44cb7d2
commit 6d6f865fb7
27 changed files with 497 additions and 233 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, connection_id, created_at, message\n FROM grpc_messages\n WHERE id = ?\n ", "query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE connection_id = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -37,6 +37,16 @@
"name": "message", "name": "message",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
},
{
"name": "is_server",
"ordinal": 7,
"type_info": "Bool"
},
{
"name": "is_info",
"ordinal": 8,
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -49,8 +59,10 @@
false, false,
false, false,
false, false,
false,
false,
false false
] ]
}, },
"hash": "f055a2b6ab6bc3fea115dbea3f287df833c9bb73cdf2fe38da21fe5f5f6ae639" "hash": "196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, connection_id, created_at, message\n FROM grpc_messages\n WHERE workspace_id = ?\n ", "query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE id = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -37,6 +37,16 @@
"name": "message", "name": "message",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
},
{
"name": "is_server",
"ordinal": 7,
"type_info": "Bool"
},
{
"name": "is_info",
"ordinal": 8,
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -49,8 +59,10 @@
false, false,
false, false,
false, false,
false,
false,
false false
] ]
}, },
"hash": "a86d3e86c5638d5f666c0cbe26dd5b3e88c632c56c7c4f3f116a29867e41e6a2" "hash": "3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c"
} }

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "4abfe0884ba046534dc6c255409c41ce287d7b0137726b8ca09870382a8b8300"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message, is_server, is_info\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message,\n is_server = excluded.is_server,\n is_info = excluded.is_info\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method\n FROM grpc_connections\n WHERE workspace_id = ?\n ", "query": "\n SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -58,5 +58,5 @@
false false
] ]
}, },
"hash": "29d3bee45c4fca4c63231e1205edc708818737fe37137dbba6af2d784c3c0221" "hash": "a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c"
} }

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap;
use prost_reflect::{DescriptorPool, MessageDescriptor}; use prost_reflect::{DescriptorPool, MessageDescriptor};
use prost_types::field_descriptor_proto; use prost_types::field_descriptor_proto;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
@@ -50,8 +50,8 @@ impl Default for JsonType {
impl serde::Serialize for JsonType { impl serde::Serialize for JsonType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
match self { match self {
JsonType::String => serializer.serialize_str("string"), JsonType::String => serializer.serialize_str("string"),
@@ -67,8 +67,8 @@ impl serde::Serialize for JsonType {
impl<'de> serde::Deserialize<'de> for JsonType { impl<'de> serde::Deserialize<'de> for JsonType {
fn deserialize<D>(deserializer: D) -> Result<JsonType, D::Error> fn deserialize<D>(deserializer: D) -> Result<JsonType, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let s = String::deserialize(deserializer)?; let s = String::deserialize(deserializer)?;
match s.as_str() { match s.as_str() {
@@ -83,7 +83,10 @@ impl<'de> serde::Deserialize<'de> for JsonType {
} }
} }
pub fn message_to_json_schema(pool: &DescriptorPool, message: MessageDescriptor) -> JsonSchemaEntry { pub fn message_to_json_schema(
pool: &DescriptorPool,
message: MessageDescriptor,
) -> JsonSchemaEntry {
let mut schema = JsonSchemaEntry { let mut schema = JsonSchemaEntry {
title: Some(message.name().to_string()), title: Some(message.name().to_string()),
type_: JsonType::Object, // Messages are objects type_: JsonType::Object, // Messages are objects

View File

@@ -52,5 +52,7 @@ CREATE TABLE grpc_messages
ON DELETE CASCADE, ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_server BOOLEAN NOT NULL,
is_info BOOLEAN NOT NULL,
message TEXT NOT NULL message TEXT NOT NULL
); );

View File

@@ -44,14 +44,15 @@ use crate::http::send_http_request;
use crate::models::{ use crate::models::{
cancel_pending_responses, create_response, delete_all_responses, delete_cookie_jar, cancel_pending_responses, create_response, delete_all_responses, delete_cookie_jar,
delete_environment, delete_folder, delete_request, delete_response, delete_workspace, delete_environment, delete_folder, delete_request, delete_response, delete_workspace,
duplicate_grpc_request, duplicate_http_request, list_cookie_jars, list_folders, list_requests, duplicate_grpc_request, duplicate_http_request, generate_id, get_cookie_jar, get_environment,
list_responses, list_workspaces, generate_id, get_cookie_jar, get_environment, get_folder, get_folder, get_grpc_request, get_http_request, get_key_value_raw, get_or_create_settings,
get_grpc_request, get_http_request, get_key_value_raw, get_or_create_settings, get_response, get_response, get_workspace, get_workspace_export_resources, list_cookie_jars,
get_workspace, get_workspace_export_resources, list_environments, list_grpc_requests, list_environments, list_folders, list_grpc_connections, list_grpc_messages, list_grpc_requests,
set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, list_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id,
upsert_environment, upsert_folder, upsert_grpc_request, upsert_http_request, upsert_workspace, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
CookieJar, Environment, EnvironmentVariable, Folder, GrpcRequest, HttpRequest, HttpResponse, upsert_grpc_message, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar,
KeyValue, Settings, Workspace, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcMessage, GrpcRequest,
HttpRequest, HttpResponse, KeyValue, Settings, Workspace,
}; };
use crate::plugin::{ImportResources, ImportResult}; use crate::plugin::{ImportResources, ImportResult};
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
@@ -98,20 +99,80 @@ async fn cmd_grpc_reflect(endpoint: &str) -> Result<Vec<ServiceDefinition>, Stri
#[tauri::command] #[tauri::command]
async fn cmd_grpc_call_unary( async fn cmd_grpc_call_unary(
endpoint: &str, request_id: &str,
service: &str, app_handle: AppHandle<Wry>,
method: &str,
message: &str,
grpc_handle: State<'_, Mutex<GrpcManager>>, grpc_handle: State<'_, Mutex<GrpcManager>>,
) -> Result<String, String> { db_state: State<'_, Mutex<Pool<Sqlite>>>,
let uri = safe_uri(endpoint).map_err(|e| e.to_string())?; ) -> Result<GrpcMessage, String> {
grpc_handle let db = &*db_state.lock().await;
let req = get_grpc_request(db, request_id)
.await
.map_err(|e| e.to_string())?;
let conn = {
let req = req.clone();
upsert_grpc_connection(
db,
&GrpcConnection {
workspace_id: req.workspace_id,
request_id: req.id,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())?
};
emit_side_effect(app_handle.clone(), "created_model", conn.clone());
{
let req = req.clone();
let conn = conn.clone();
upsert_grpc_message(
db,
&GrpcMessage {
workspace_id: req.workspace_id,
request_id: req.id,
connection_id: conn.id,
is_info: true,
message: format!("Initiating connection to {}", req.url),
..Default::default()
},
)
.await
.map_err(|e| e.to_string())?;
};
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
let conn_id = generate_id(Some("grpc"));
let msg = match grpc_handle
.lock() .lock()
.await .await
.connect("default", uri) .connect(&conn_id, uri)
.await .await
.unary(service, method, message) .unary(
&req.service.unwrap_or_default(),
&req.method.unwrap_or_default(),
&req.message,
)
.await .await
{
Ok(msg) => {
upsert_grpc_message(
db,
&GrpcMessage {
message: msg,
workspace_id: req.workspace_id,
request_id: req.id,
connection_id: conn.id,
is_server: true,
..Default::default()
},
)
.await
}
Err(e) => return Err(e.to_string()),
};
msg.map_err(|e| e.to_string())
} }
#[tauri::command] #[tauri::command]
@@ -235,27 +296,47 @@ async fn cmd_grpc_bidi_streaming(
#[tauri::command] #[tauri::command]
async fn cmd_grpc_server_streaming( async fn cmd_grpc_server_streaming(
endpoint: &str, request_id: &str,
service: &str,
method: &str,
message: &str,
app_handle: AppHandle<Wry>, app_handle: AppHandle<Wry>,
grpc_handle: State<'_, Mutex<GrpcManager>>, grpc_handle: State<'_, Mutex<GrpcManager>>,
) -> Result<String, String> { db_state: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<GrpcConnection, String> {
let db = &*db_state.lock().await;
let req = get_grpc_request(db, request_id)
.await
.map_err(|e| e.to_string())?;
let conn = {
let req = req.clone();
upsert_grpc_connection(
db,
&GrpcConnection {
workspace_id: req.workspace_id,
request_id: req.id,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())?
};
emit_side_effect(app_handle.clone(), "created_model", conn.clone());
let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false); let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false);
let uri = safe_uri(endpoint).map_err(|e| e.to_string())?; let (service, method) = match (&req.service, &req.method) {
let conn_id = generate_id(Some("grpc")); (Some(service), Some(method)) => (service, method),
_ => return Err("Service and method are required".to_string()),
};
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
let mut stream = grpc_handle let mut stream = grpc_handle
.lock() .lock()
.await .await
.server_streaming(&conn_id, uri, service, method, message) .server_streaming(&conn.id, uri, &service, &method, &req.message)
.await .await
.unwrap(); .unwrap();
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
enum GrpcMessage { enum IncomingMsg {
Message(String), Message(String),
Commit, Commit,
Cancel, Cancel,
@@ -270,15 +351,15 @@ async fn cmd_grpc_server_streaming(
return; return;
} }
match serde_json::from_str::<GrpcMessage>(ev.payload().unwrap()) { match serde_json::from_str::<IncomingMsg>(ev.payload().unwrap()) {
Ok(GrpcMessage::Message(msg)) => { Ok(IncomingMsg::Message(msg)) => {
println!("Received message: {}", msg); println!("Received message: {}", msg);
} }
Ok(GrpcMessage::Commit) => { Ok(IncomingMsg::Commit) => {
println!("Received commit"); println!("Received commit");
// TODO: Commit client streaming stream // TODO: Commit client streaming stream
} }
Ok(GrpcMessage::Cancel) => { Ok(IncomingMsg::Cancel) => {
println!("Received cancel"); println!("Received cancel");
cancelled_tx.send_replace(true); cancelled_tx.send_replace(true);
} }
@@ -289,19 +370,34 @@ async fn cmd_grpc_server_streaming(
} }
}; };
let event_handler = let event_handler =
app_handle.listen_global(format!("grpc_client_msg_{}", conn_id).as_str(), cb); app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
let grpc_listen = { let grpc_listen = {
let db = db.clone();
let conn_id = conn.clone().id;
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
let conn_id = conn_id.clone();
async move { async move {
loop { loop {
let req = req.clone();
let conn_id = conn_id.clone();
match stream.next().await { match stream.next().await {
Some(Ok(item)) => { Some(Ok(item)) => {
let item = serde_json::to_string_pretty(&item).unwrap(); let item = serde_json::to_string_pretty(&item).unwrap();
app_handle let msg = upsert_grpc_message(
.emit_all(format!("grpc_server_msg_{}", &conn_id).as_str(), item) &db,
.expect("Failed to emit"); &GrpcMessage {
message: item,
workspace_id: req.workspace_id,
request_id: req.id,
connection_id: conn_id,
is_server: true,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
.expect("Failed to upsert message");
emit_side_effect(app_handle.clone(), "created_model", msg);
} }
Some(Err(e)) => { Some(Err(e)) => {
error!("gRPC stream error: {:?}", e); error!("gRPC stream error: {:?}", e);
@@ -328,7 +424,7 @@ async fn cmd_grpc_server_streaming(
app_handle.unlisten(event_handler); app_handle.unlisten(event_handler);
}); });
Ok(conn_id) Ok(conn)
} }
#[tauri::command] #[tauri::command]
@@ -783,7 +879,7 @@ async fn cmd_duplicate_grpc_request(
let request = duplicate_grpc_request(db, id) let request = duplicate_grpc_request(db, id)
.await .await
.expect("Failed to duplicate grpc request"); .expect("Failed to duplicate grpc request");
emit_and_return(&window, "updated_model", request) emit_and_return(&window, "created_model", request)
} }
#[tauri::command] #[tauri::command]
@@ -823,7 +919,7 @@ async fn cmd_duplicate_http_request(
let request = duplicate_http_request(db, id) let request = duplicate_http_request(db, id)
.await .await
.expect("Failed to duplicate http request"); .expect("Failed to duplicate http request");
emit_and_return(&window, "updated_model", request) emit_and_return(&window, "created_model", request)
} }
#[tauri::command] #[tauri::command]
@@ -982,6 +1078,28 @@ async fn cmd_delete_environment(
emit_and_return(&window, "deleted_model", req) emit_and_return(&window, "deleted_model", req)
} }
#[tauri::command]
async fn cmd_list_grpc_connections(
request_id: &str,
db_state: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<GrpcConnection>, String> {
let db = &*db_state.lock().await;
list_grpc_connections(db, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_list_grpc_messages(
connection_id: &str,
db_state: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<GrpcMessage>, String> {
let db = &*db_state.lock().await;
list_grpc_messages(db, connection_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
async fn cmd_list_grpc_requests( async fn cmd_list_grpc_requests(
workspace_id: &str, workspace_id: &str,
@@ -990,8 +1108,7 @@ async fn cmd_list_grpc_requests(
let db = &*db_state.lock().await; let db = &*db_state.lock().await;
let requests = list_grpc_requests(db, workspace_id) let requests = list_grpc_requests(db, workspace_id)
.await .await
.expect("Failed to find grpc requests"); .map_err(|e| e.to_string())?;
// .map_err(|e| e.to_string())
Ok(requests) Ok(requests)
} }
@@ -1123,7 +1240,7 @@ async fn cmd_get_workspace(
} }
#[tauri::command] #[tauri::command]
async fn cmd_list_responses( async fn cmd_list_http_responses(
request_id: &str, request_id: &str,
limit: Option<i64>, limit: Option<i64>,
db_state: State<'_, Mutex<Pool<Sqlite>>>, db_state: State<'_, Mutex<Pool<Sqlite>>>,
@@ -1303,6 +1420,7 @@ fn main() {
cmd_delete_response, cmd_delete_response,
cmd_delete_workspace, cmd_delete_workspace,
cmd_duplicate_http_request, cmd_duplicate_http_request,
cmd_duplicate_grpc_request,
cmd_export_data, cmd_export_data,
cmd_filter_response, cmd_filter_response,
cmd_get_cookie_jar, cmd_get_cookie_jar,
@@ -1324,7 +1442,9 @@ fn main() {
cmd_list_folders, cmd_list_folders,
cmd_list_http_requests, cmd_list_http_requests,
cmd_list_grpc_requests, cmd_list_grpc_requests,
cmd_list_responses, cmd_list_grpc_connections,
cmd_list_grpc_messages,
cmd_list_http_responses,
cmd_list_workspaces, cmd_list_workspaces,
cmd_new_window, cmd_new_window,
cmd_send_ephemeral_request, cmd_send_ephemeral_request,

View File

@@ -228,6 +228,8 @@ pub struct GrpcMessage {
pub connection_id: String, pub connection_id: String,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub message: String, pub message: String,
pub is_server: bool,
pub is_info: bool,
} }
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -589,16 +591,17 @@ pub async fn get_grpc_connection(
pub async fn list_grpc_connections( pub async fn list_grpc_connections(
db: &Pool<Sqlite>, db: &Pool<Sqlite>,
workspace_id: &str, request_id: &str,
) -> Result<Vec<GrpcConnection>, sqlx::Error> { ) -> Result<Vec<GrpcConnection>, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
GrpcConnection, GrpcConnection,
r#" 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
FROM grpc_connections FROM grpc_connections
WHERE workspace_id = ? WHERE request_id = ?
ORDER BY created_at DESC
"#, "#,
workspace_id, request_id,
) )
.fetch_all(db) .fetch_all(db)
.await .await
@@ -615,30 +618,36 @@ pub async fn upsert_grpc_message(
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO grpc_messages ( INSERT INTO grpc_messages (
id, workspace_id, request_id, connection_id, message id, workspace_id, request_id, connection_id, message, is_server, is_info
) )
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP,
message = excluded.message message = excluded.message,
is_server = excluded.is_server,
is_info = excluded.is_info
"#, "#,
id, id,
message.workspace_id, message.workspace_id,
message.request_id, message.request_id,
message.connection_id, message.connection_id,
message.message, message.message,
message.is_server,
message.is_info,
) )
.execute(db) .execute(db)
.await?; .await?;
crate::models::get_grpc_message(db, &id).await get_grpc_message(db, &id).await
} }
pub async fn get_grpc_message(db: &Pool<Sqlite>, id: &str) -> Result<GrpcMessage, sqlx::Error> { pub async fn get_grpc_message(db: &Pool<Sqlite>, id: &str) -> Result<GrpcMessage, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
GrpcMessage, GrpcMessage,
r#" r#"
SELECT id, model, workspace_id, request_id, connection_id, created_at, message SELECT
id, model, workspace_id, request_id, connection_id, created_at, message,
is_server, is_info
FROM grpc_messages FROM grpc_messages
WHERE id = ? WHERE id = ?
"#, "#,
@@ -650,16 +659,18 @@ pub async fn get_grpc_message(db: &Pool<Sqlite>, id: &str) -> Result<GrpcMessage
pub async fn list_grpc_messages( pub async fn list_grpc_messages(
db: &Pool<Sqlite>, db: &Pool<Sqlite>,
workspace_id: &str, connection_id: &str,
) -> Result<Vec<GrpcMessage>, sqlx::Error> { ) -> Result<Vec<GrpcMessage>, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
GrpcMessage, GrpcMessage,
r#" r#"
SELECT id, model, workspace_id, request_id, connection_id, created_at, message SELECT
id, model, workspace_id, request_id, connection_id, created_at, message,
is_server, is_info
FROM grpc_messages FROM grpc_messages
WHERE workspace_id = ? WHERE connection_id = ?
"#, "#,
workspace_id, connection_id,
) )
.fetch_all(db) .fetch_all(db)
.await .await

View File

@@ -3,20 +3,23 @@ import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { cookieJarsQueryKey } from '../hooks/useCookieJars'; import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue'; import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { responsesQueryKey } from '../hooks/useResponses';
import { settingsQueryKey } from '../hooks/useSettings'; import { settingsQueryKey } from '../hooks/useSettings';
import { useSyncAppearance } from '../hooks/useSyncAppearance'; import { useSyncAppearance } from '../hooks/useSyncAppearance';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle'; import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore'; import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models'; import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models'; import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname'; import { setPathname } from '../lib/persistPathname';
@@ -49,7 +52,13 @@ export function GlobalHooks() {
payload.model === 'http_request' payload.model === 'http_request'
? httpRequestsQueryKey(payload) ? httpRequestsQueryKey(payload)
: payload.model === 'http_response' : payload.model === 'http_response'
? responsesQueryKey(payload) ? httpResponsesQueryKey(payload)
: payload.model === 'grpc_connection'
? grpcConnectionsQueryKey(payload)
: payload.model === 'grpc_message'
? grpcMessagesQueryKey(payload)
: payload.model === 'grpc_request'
? grpcRequestsQueryKey(payload)
: payload.model === 'workspace' : payload.model === 'workspace'
? workspacesQueryKey(payload) ? workspacesQueryKey(payload)
: payload.model === 'key_value' : payload.model === 'key_value'
@@ -78,7 +87,13 @@ export function GlobalHooks() {
payload.model === 'http_request' payload.model === 'http_request'
? httpRequestsQueryKey(payload) ? httpRequestsQueryKey(payload)
: payload.model === 'http_response' : payload.model === 'http_response'
? responsesQueryKey(payload) ? httpResponsesQueryKey(payload)
: payload.model === 'grpc_connection'
? grpcConnectionsQueryKey(payload)
: payload.model === 'grpc_message'
? grpcMessagesQueryKey(payload)
: payload.model === 'grpc_request'
? grpcRequestsQueryKey(payload)
: payload.model === 'workspace' : payload.model === 'workspace'
? workspacesQueryKey(payload) ? workspacesQueryKey(payload)
: payload.model === 'key_value' : payload.model === 'key_value'
@@ -113,11 +128,17 @@ export function GlobalHooks() {
if (shouldIgnoreModel(payload)) return; if (shouldIgnoreModel(payload)) return;
if (payload.model === 'workspace') { if (payload.model === 'workspace') {
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(), removeById(payload)); queryClient.setQueryData(workspacesQueryKey(), removeById(payload));
} else if (payload.model === 'http_request') { } else if (payload.model === 'http_request') {
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey(payload), removeById(payload)); queryClient.setQueryData(httpRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'http_response') { } else if (payload.model === 'http_response') {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload)); queryClient.setQueryData(httpResponsesQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_request') {
queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_connection') {
queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_message') {
queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload));
} else if (payload.model === 'key_value') { } else if (payload.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload), undefined); queryClient.setQueryData(keyValueQueryKey(payload), undefined);
} else if (payload.model === 'cookie_jar') { } else if (payload.model === 'cookie_jar') {

View File

@@ -5,12 +5,12 @@ import type { CSSProperties, FormEvent } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useAlert } from '../hooks/useAlert'; import { useAlert } from '../hooks/useAlert';
import type { GrpcMessage } from '../hooks/useGrpc';
import { useGrpc } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useGrpcMessages } from '../hooks/useGrpcMessages';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Editor } from './core/Editor';
import { HotKeyList } from './core/HotKeyList'; import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
@@ -30,9 +30,11 @@ export function GrpcConnectionLayout({ style }: Props) {
const activeRequest = useActiveRequest('grpc_request'); const activeRequest = useActiveRequest('grpc_request');
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
const alert = useAlert(); const alert = useAlert();
const [activeMessage, setActiveMessage] = useState<GrpcMessage | null>(null); const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
const [resp, setResp] = useState<string>('');
const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.id ?? null); const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.id ?? null);
const connections = useGrpcConnections(activeRequest?.id ?? null);
const activeConnection = connections[0] ?? null;
const messages = useGrpcMessages(activeConnection?.id ?? null);
const activeMethod = useMemo(() => { const activeMethod = useMemo(() => {
if (grpc.services == null || activeRequest == null) return null; if (grpc.services == null || activeRequest == null) return null;
@@ -61,9 +63,9 @@ export function GrpcConnectionLayout({ style }: Props) {
if (activeMethod.clientStreaming && activeMethod.serverStreaming) { if (activeMethod.clientStreaming && activeMethod.serverStreaming) {
await grpc.bidiStreaming.mutateAsync(activeRequest); await grpc.bidiStreaming.mutateAsync(activeRequest);
} else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) { } else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) {
await grpc.serverStreaming.mutateAsync(activeRequest); await grpc.serverStreaming.mutateAsync(activeRequest.id);
} else { } else {
setResp(await grpc.unary.mutateAsync(activeRequest)); await grpc.unary.mutateAsync(activeRequest.id);
} }
}, },
[activeMethod, activeRequest, alert, grpc.bidiStreaming, grpc.serverStreaming, grpc.unary], [activeMethod, activeRequest, alert, grpc.bidiStreaming, grpc.serverStreaming, grpc.unary],
@@ -127,8 +129,13 @@ export function GrpcConnectionLayout({ style }: Props) {
setPaneSize(entry.contentRect.width); setPaneSize(entry.contentRect.width);
}); });
const activeMessage = useMemo(
() => messages.find((m) => m.id === activeMessageId) ?? null,
[activeMessageId, messages],
);
if (activeRequest == null) { if (activeRequest == null) {
return; return null;
} }
return ( return (
@@ -136,7 +143,7 @@ export function GrpcConnectionLayout({ style }: Props) {
name="grpc_layout" name="grpc_layout"
className="p-3 gap-1.5" className="p-3 gap-1.5"
style={style} style={style}
leftSlot={() => ( firstSlot={() => (
<VStack space={2}> <VStack space={2}>
<div <div
ref={urlContainerEl} ref={urlContainerEl}
@@ -218,7 +225,7 @@ export function GrpcConnectionLayout({ style }: Props) {
/> />
</VStack> </VStack>
)} )}
rightSlot={() => secondSlot={() =>
!grpc.unary.isLoading && ( !grpc.unary.isLoading && (
<div <div
className={classNames( className={classNames(
@@ -231,20 +238,20 @@ export function GrpcConnectionLayout({ style }: Props) {
<Banner color="danger" className="m-2"> <Banner color="danger" className="m-2">
{grpc.unary.error} {grpc.unary.error}
</Banner> </Banner>
) : (grpc.messages.value ?? []).length > 0 ? ( ) : messages.length >= 0 ? (
<SplitLayout <SplitLayout
name="grpc_messages2" name="grpc_messages"
minHeightPx={20} minHeightPx={20}
defaultRatio={0.25} defaultRatio={0.25}
leftSlot={() => ( firstSlot={() => (
<div className="overflow-y-auto"> <div className="overflow-y-auto">
{...(grpc.messages.value ?? []).map((m, i) => ( {...messages.map((m) => (
<HStack <HStack
key={`${m.timestamp}::${m.message}::${i}`} key={m.id}
space={2} space={2}
onClick={() => { onClick={() => {
if (m === activeMessage) setActiveMessage(null); if (m.id === activeMessageId) setActiveMessageId(null);
else setActiveMessage(m); else setActiveMessageId(m.id);
}} }}
alignItems="center" alignItems="center"
className={classNames( className={classNames(
@@ -254,29 +261,25 @@ export function GrpcConnectionLayout({ style }: Props) {
> >
<Icon <Icon
className={ className={
m.type === 'server' m.isInfo
? 'text-gray-600'
: m.isServer
? 'text-blue-600' ? 'text-blue-600'
: m.type === 'client' : 'text-green-600'
? 'text-green-600'
: 'text-gray-600'
} }
icon={ icon={
m.type === 'server' m.isInfo ? 'info' : m.isServer ? 'arrowBigDownDash' : 'arrowBigUpDash'
? 'arrowBigDownDash'
: m.type === 'client'
? 'arrowBigUpDash'
: 'info'
} }
/> />
<div className="w-full truncate text-gray-800 text-2xs">{m.message}</div> <div className="w-full truncate text-gray-800 text-2xs">{m.message}</div>
<div className="text-gray-600 text-2xs"> <div className="text-gray-600 text-2xs">
{format(m.timestamp, 'HH:mm:ss')} {format(m.createdAt, 'HH:mm:ss')}
</div> </div>
</HStack> </HStack>
))} ))}
</div> </div>
)} )}
rightSlot={ secondSlot={
!activeMessage !activeMessage
? null ? null
: () => ( : () => (
@@ -293,15 +296,15 @@ export function GrpcConnectionLayout({ style }: Props) {
) )
} }
/> />
) : resp ? (
<Editor
className="bg-gray-50 dark:bg-gray-100"
contentType="application/json"
defaultValue={resp}
readOnly
forceUpdateKey={resp}
/>
) : ( ) : (
// ) : ? (
// <Editor
// readOnly
// className="bg-gray-50 dark:bg-gray-100"
// contentType="application/json"
// defaultValue={resp.message}
// forceUpdateKey={resp.id}
// />
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} /> <HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} />
)} )}
</div> </div>

View File

@@ -14,10 +14,10 @@ export function HttpRequestLayout({ style }: Props) {
name="http_layout" name="http_layout"
className="p-3 gap-1.5" className="p-3 gap-1.5"
style={style} style={style}
leftSlot={({ orientation, style }) => ( firstSlot={({ orientation, style }) => (
<RequestPane style={style} fullHeight={orientation === 'horizontal'} /> <RequestPane style={style} fullHeight={orientation === 'horizontal'} />
)} )}
rightSlot={({ style }) => <ResponsePane style={style} />} secondSlot={({ style }) => <ResponsePane style={style} />}
/> />
); );
} }

View File

@@ -5,7 +5,7 @@ import { createGlobalState } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useLatestResponse } from '../hooks/useLatestResponse'; import { useLatestResponse } from '../hooks/useLatestResponse';
import { useResponseContentType } from '../hooks/useResponseContentType'; import { useResponseContentType } from '../hooks/useResponseContentType';
import { useResponses } from '../hooks/useResponses'; import { useHttpResponses } from '../hooks/useHttpResponses';
import { useResponseViewMode } from '../hooks/useResponseViewMode'; import { useResponseViewMode } from '../hooks/useResponseViewMode';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { isResponseLoading } from '../lib/models'; import { isResponseLoading } from '../lib/models';
@@ -39,7 +39,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null); const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const latestResponse = useLatestResponse(activeRequest?.id ?? null); const latestResponse = useLatestResponse(activeRequest?.id ?? null);
const responses = useResponses(activeRequest?.id ?? null); const responses = useHttpResponses(activeRequest?.id ?? null);
const activeResponse: HttpResponse | null = pinnedResponseId const activeResponse: HttpResponse | null = pinnedResponseId
? responses.find((r) => r.id === pinnedResponseId) ?? null ? responses.find((r) => r.id === pinnedResponseId) ?? null
: latestResponse ?? null; : latestResponse ?? null;

View File

@@ -7,7 +7,6 @@ import { useKey, useKeyPressEvent } from 'react-use';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId'; import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateFolder } from '../hooks/useCreateFolder'; import { useCreateFolder } from '../hooks/useCreateFolder';
@@ -15,7 +14,8 @@ import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest'; import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useDeleteFolder } from '../hooks/useDeleteFolder'; import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useFolders } from '../hooks/useFolders'; import { useFolders } from '../hooks/useFolders';
import { useGrpcRequests } from '../hooks/useGrpcRequests'; import { useGrpcRequests } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
@@ -29,6 +29,7 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore'; import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
@@ -58,14 +59,21 @@ interface TreeNode {
export function Sidebar({ className }: Props) { export function Sidebar({ className }: Props) {
const { hidden } = useSidebarHidden(); const { hidden } = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null); const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequestId = useActiveRequestId(); const activeRequest = useActiveRequest();
const activeEnvironmentId = useActiveEnvironmentId(); const activeEnvironmentId = useActiveEnvironmentId();
const httpRequests = useHttpRequests(); const httpRequests = useHttpRequests();
const grpcRequests = useGrpcRequests(); const grpcRequests = useGrpcRequests();
const folders = useFolders(); const folders = useFolders();
const deleteAnyRequest = useDeleteAnyRequest(); const deleteAnyRequest = useDeleteAnyRequest();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true }); const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const routes = useAppRoutes(); const routes = useAppRoutes();
const [hasFocus, setHasFocus] = useState<boolean>(false); const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -82,8 +90,12 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC, namespace: NAMESPACE_NO_SYNC,
}); });
useHotKey('http_request.duplicate', () => { useHotKey('http_request.duplicate', async () => {
duplicateRequest.mutate(); if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
}); });
const isCollapsed = useCallback( const isCollapsed = useCallback(
@@ -146,9 +158,10 @@ export function Sidebar({ className }: Props) {
} = {}, } = {},
) => { ) => {
const { forced, noFocusSidebar } = args; const { forced, noFocusSidebar } = args;
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null; const tree = forced?.tree ?? treeParentMap[activeRequest?.id ?? 'n/a'] ?? null;
const children = tree?.children ?? []; const children = tree?.children ?? [];
const id = forced?.id ?? children.find((m) => m.item.id === activeRequestId)?.item.id ?? null; const id =
forced?.id ?? children.find((m) => m.item.id === activeRequest?.id)?.item.id ?? null;
if (id == null) { if (id == null) {
return; return;
} }
@@ -160,7 +173,7 @@ export function Sidebar({ className }: Props) {
sidebarRef.current?.focus(); sidebarRef.current?.focus();
} }
}, },
[activeRequestId, treeParentMap], [activeRequest, treeParentMap],
); );
const handleSelect = useCallback( const handleSelect = useCallback(
@@ -230,7 +243,7 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Enter', (e) => { useKeyPressEvent('Enter', (e) => {
if (!hasFocus) return; if (!hasFocus) return;
const selected = selectableRequests.find((r) => r.id === selectedId); const selected = selectableRequests.find((r) => r.id === selectedId);
if (!selected || selected.id === activeRequestId || activeWorkspace == null) { if (!selected || selected.id === activeRequest?.id || activeWorkspace == null) {
return; return;
} }
@@ -541,11 +554,13 @@ const SidebarItem = forwardRef(function SidebarItem(
const createFolder = useCreateFolder(); const createFolder = useCreateFolder();
const deleteFolder = useDeleteFolder(itemId); const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(itemId); const deleteRequest = useDeleteRequest(itemId);
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true }); const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendRequest(itemId); const sendRequest = useSendRequest(itemId);
const sendManyRequests = useSendManyRequests(); const sendManyRequests = useSendManyRequests();
const latestResponse = useLatestResponse(itemId); const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateHttpRequest(itemId); const updateHttpRequest = useUpdateHttpRequest(itemId);
const updateGrpcRequest = useUpdateGrpcRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder(); const updateAnyFolder = useUpdateAnyFolder();
const prompt = usePrompt(); const prompt = usePrompt();
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
@@ -553,10 +568,15 @@ const SidebarItem = forwardRef(function SidebarItem(
const handleSubmitNameEdit = useCallback( const handleSubmitNameEdit = useCallback(
(el: HTMLInputElement) => { (el: HTMLInputElement) => {
updateRequest.mutate((r) => ({ ...r, name: el.value })); if (activeRequest == null) return;
if (activeRequest.model === 'http_request') {
updateHttpRequest.mutate((r) => ({ ...r, name: el.value }));
} else if (activeRequest.model === 'grpc_request') {
updateGrpcRequest.mutate((r) => ({ ...r, name: el.value }));
}
setEditing(false); setEditing(false);
}, },
[updateRequest], [activeRequest, updateGrpcRequest, updateHttpRequest],
); );
const handleFocus = useCallback((el: HTMLInputElement | null) => { const handleFocus = useCallback((el: HTMLInputElement | null) => {
@@ -677,7 +697,9 @@ const SidebarItem = forwardRef(function SidebarItem(
hotKeyLabelOnly: true, // Would trigger for every request (bad) hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
onSelect: () => { onSelect: () => {
duplicateRequest.mutate(); itemModel === 'http_request'
? duplicateHttpRequest.mutate()
: duplicateGrpcRequest.mutate();
}, },
}, },
{ {

View File

@@ -34,7 +34,6 @@ export default function Workspace() {
const { setWidth, width, resetWidth } = useSidebarWidth(); const { setWidth, width, resetWidth } = useSidebarWidth();
const { hide, show, hidden } = useSidebarHidden(); const { hide, show, hidden } = useSidebarHidden();
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const [floating, setFloating] = useState<boolean>(false); const [floating, setFloating] = useState<boolean>(false);
const [isResizing, setIsResizing] = useState<boolean>(false); const [isResizing, setIsResizing] = useState<boolean>(false);
@@ -47,7 +46,7 @@ export default function Workspace() {
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH; const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
if (shouldHide && !floating) { if (shouldHide && !floating) {
setFloating(true); setFloating(true);
hide(); hide().catch(console.error);
} else if (!shouldHide && floating) { } else if (!shouldHide && floating) {
setFloating(false); setFloating(false);
} }
@@ -72,10 +71,10 @@ export default function Workspace() {
e.preventDefault(); // Prevent text selection and things e.preventDefault(); // Prevent text selection and things
const newWidth = startWidth + (e.clientX - mouseStartX); const newWidth = startWidth + (e.clientX - mouseStartX);
if (newWidth < 100) { if (newWidth < 100) {
hide(); await hide();
resetWidth(); resetWidth();
} else { } else {
show(); await show();
setWidth(newWidth); setWidth(newWidth);
} }
}, },

View File

@@ -16,8 +16,8 @@ interface SlotProps {
interface Props { interface Props {
name: string; name: string;
leftSlot: (props: SlotProps) => ReactNode; firstSlot: (props: SlotProps) => ReactNode;
rightSlot: null | ((props: SlotProps) => ReactNode); secondSlot: null | ((props: SlotProps) => ReactNode);
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
defaultRatio?: number; defaultRatio?: number;
@@ -33,8 +33,8 @@ const STACK_VERTICAL_WIDTH = 700;
export function SplitLayout({ export function SplitLayout({
style, style,
leftSlot, firstSlot,
rightSlot, secondSlot,
className, className,
name, name,
defaultRatio = 0.5, defaultRatio = 0.5,
@@ -54,7 +54,7 @@ export function SplitLayout({
null, null,
); );
if (!rightSlot) { if (!secondSlot) {
height = 0; height = 0;
minHeightPx = 0; minHeightPx = 0;
} }
@@ -145,8 +145,8 @@ export function SplitLayout({
return ( return (
<div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}> <div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}>
{leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} {firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
{rightSlot && ( {secondSlot && (
<> <>
<ResizeHandle <ResizeHandle
style={areaD} style={areaD}
@@ -158,7 +158,7 @@ export function SplitLayout({
side={vertical ? 'top' : 'left'} side={vertical ? 'top' : 'left'}
justify="center" justify="center"
/> />
{rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })} {secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
</> </>
)} )}
</div> </div>

View File

@@ -7,7 +7,7 @@ import type { HttpRequest } from '../lib/models';
import { getHttpRequest } from '../lib/store'; import { getHttpRequest } from '../lib/store';
import { useConfirm } from './useConfirm'; import { useConfirm } from './useConfirm';
import { httpRequestsQueryKey } from './useHttpRequests'; import { httpRequestsQueryKey } from './useHttpRequests';
import { responsesQueryKey } from './useResponses'; import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteAnyRequest() { export function useDeleteAnyRequest() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -35,7 +35,7 @@ export function useDeleteAnyRequest() {
if (request === null) return; if (request === null) return;
const { workspaceId, id: requestId } = request; const { workspaceId, id: requestId } = request;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey({ workspaceId }), (requests) => queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId), (requests ?? []).filter((r) => r.id !== requestId),
); );

View File

@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { responsesQueryKey } from './useResponses'; import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteResponse(id: string | null) { export function useDeleteResponse(id: string | null) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -12,7 +12,7 @@ export function useDeleteResponse(id: string | null) {
}, },
onSettled: () => trackEvent('HttpResponse', 'Delete'), onSettled: () => trackEvent('HttpResponse', 'Delete'),
onSuccess: ({ requestId, id: responseId }) => { onSuccess: ({ requestId, id: responseId }) => {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) => queryClient.setQueryData<HttpResponse[]>(httpResponsesQueryKey({ requestId }), (responses) =>
(responses ?? []).filter((response) => response.id !== responseId), (responses ?? []).filter((response) => response.id !== responseId),
); );
}, },

View File

@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { responsesQueryKey } from './useResponses'; import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteResponses(requestId?: string) { export function useDeleteResponses(requestId?: string) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -13,7 +13,7 @@ export function useDeleteResponses(requestId?: string) {
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'), onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
onSuccess: async () => { onSuccess: async () => {
if (requestId === undefined) return; if (requestId === undefined) return;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []);
}, },
}); });
} }

View File

@@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { GrpcRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { grpcRequestsQueryKey } from './useGrpcRequests';
export function useDuplicateGrpcRequest({
id,
navigateAfter,
}: {
id: string | null;
navigateAfter: boolean;
}) {
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const routes = useAppRoutes();
const queryClient = useQueryClient();
return useMutation<GrpcRequest, string>({
mutationFn: async () => {
if (id === null) throw new Error("Can't duplicate a null grpc request");
return invoke('cmd_duplicate_grpc_request', { id });
},
onSettled: () => trackEvent('GrpcRequest', 'Duplicate'),
onSuccess: async (request) => {
queryClient.setQueryData<GrpcRequest[]>(
grpcRequestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
if (navigateAfter && activeWorkspaceId !== null) {
routes.navigate('request', {
workspaceId: activeWorkspaceId,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
});
}
},
});
}

View File

@@ -7,7 +7,7 @@ import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes'; import { useAppRoutes } from './useAppRoutes';
import { httpRequestsQueryKey } from './useHttpRequests'; import { httpRequestsQueryKey } from './useHttpRequests';
export function useDuplicateRequest({ export function useDuplicateHttpRequest({
id, id,
navigateAfter, navigateAfter,
}: { }: {

View File

@@ -3,8 +3,7 @@ import { invoke } from '@tauri-apps/api';
import type { UnlistenFn } from '@tauri-apps/api/event'; import type { UnlistenFn } from '@tauri-apps/api/event';
import { emit, listen } from '@tauri-apps/api/event'; import { emit, listen } from '@tauri-apps/api/event';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { tryFormatJson } from '../lib/formatters'; import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
import type { GrpcRequest } from '../lib/models';
import { useKeyValue } from './useKeyValue'; import { useKeyValue } from './useKeyValue';
interface ReflectResponseService { interface ReflectResponseService {
@@ -12,12 +11,6 @@ interface ReflectResponseService {
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[]; methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
} }
export interface GrpcMessage {
message: string;
timestamp: string;
type: 'server' | 'client' | 'info';
}
export function useGrpc(url: string | null, requestId: string | null) { export function useGrpc(url: string | null, requestId: string | null) {
const messages = useKeyValue<GrpcMessage[]>({ const messages = useKeyValue<GrpcMessage[]>({
namespace: 'debug', namespace: 'debug',
@@ -25,54 +18,30 @@ export function useGrpc(url: string | null, requestId: string | null) {
defaultValue: [], defaultValue: [],
}); });
const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null); const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null);
const unlisten = useRef<UnlistenFn | null>(null);
useEffect(() => { useEffect(() => {
setActiveConnectionId(null); setActiveConnectionId(null);
unlisten.current?.();
}, [requestId]); }, [requestId]);
const unary = useMutation<string, string, GrpcRequest>({ const unary = useMutation<GrpcMessage, string, string>({
mutationKey: ['grpc_unary', url], mutationKey: ['grpc_unary', url],
mutationFn: async ({ service, method, message, url }) => { mutationFn: async (id) => {
if (url === null) throw new Error('No URL provided'); const message = (await invoke('cmd_grpc_call_unary', {
return (await invoke('cmd_grpc_call_unary', { requestId: id,
endpoint: url, })) as GrpcMessage;
service, await messages.set([message]);
method, console.log('MESSAGE', message);
message, return message;
})) as string;
}, },
}); });
const serverStreaming = useMutation<void, string, GrpcRequest>({ const serverStreaming = useMutation<void, string, string>({
mutationKey: ['grpc_server_streaming', url], mutationKey: ['grpc_server_streaming', url],
mutationFn: async ({ service, method, message, url }) => { mutationFn: async (requestId) => {
if (url === null) throw new Error('No URL provided'); if (url === null) throw new Error('No URL provided');
await messages.set([ await messages.set([]);
{ const c = (await invoke('cmd_grpc_server_streaming', { requestId })) as GrpcConnection;
type: 'client', setActiveConnectionId(c.id);
message: JSON.stringify(JSON.parse(message)),
timestamp: new Date().toISOString(),
},
]);
const id: string = await invoke('cmd_grpc_server_streaming', {
endpoint: url,
service,
method,
message,
});
unlisten.current = await listen(`grpc_server_msg_${id}`, async (event) => {
await messages.set((prev) => [
...prev,
{
message: tryFormatJson(event.payload as string, false),
timestamp: new Date().toISOString(),
type: 'server',
},
]);
});
setActiveConnectionId(id);
}, },
}); });
@@ -86,20 +55,8 @@ export function useGrpc(url: string | null, requestId: string | null) {
method, method,
message, message,
}); });
await messages.set([ await messages.set([]);
{ type: 'info', message: `Started connection ${id}`, timestamp: new Date().toISOString() },
]);
setActiveConnectionId(id); setActiveConnectionId(id);
unlisten.current = await listen(`grpc_server_msg_${id}`, (event) => {
messages.set((prev) => [
...prev,
{
message: tryFormatJson(event.payload as string, false),
timestamp: new Date().toISOString(),
type: 'server',
},
]);
});
}, },
}); });
@@ -107,9 +64,10 @@ export function useGrpc(url: string | null, requestId: string | null) {
mutationKey: ['grpc_send', url], mutationKey: ['grpc_send', url],
mutationFn: async ({ message }: { message: string }) => { mutationFn: async ({ message }: { message: string }) => {
if (activeConnectionId == null) throw new Error('No active connection'); if (activeConnectionId == null) throw new Error('No active connection');
await messages.set((m) => { await messages.set([]);
return [...m, { type: 'client', message, timestamp: new Date().toISOString() }]; // await messages.set((m) => {
}); // return [...m, { type: 'client', message, timestamp: new Date().toISOString() }];
// });
await emit(`grpc_client_msg_${activeConnectionId}`, { Message: message }); await emit(`grpc_client_msg_${activeConnectionId}`, { Message: message });
}, },
}); });
@@ -118,12 +76,7 @@ export function useGrpc(url: string | null, requestId: string | null) {
mutationKey: ['grpc_cancel', url], mutationKey: ['grpc_cancel', url],
mutationFn: async () => { mutationFn: async () => {
setActiveConnectionId(null); setActiveConnectionId(null);
unlisten.current?.(); await emit(`grpc_client_msg_${activeConnectionId}`, 'Cancel');
await emit('grpc_message_in', 'Cancel');
await messages.set((m) => [
...m,
{ type: 'info', message: 'Cancelled by client', timestamp: new Date().toISOString() },
]);
}, },
}); });
@@ -141,7 +94,6 @@ export function useGrpc(url: string | null, requestId: string | null) {
bidiStreaming, bidiStreaming,
services: reflect.data, services: reflect.data,
cancel, cancel,
messages,
isStreaming: activeConnectionId !== null, isStreaming: activeConnectionId !== null,
send, send,
}; };

View File

@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcConnection } from '../lib/models';
export function grpcConnectionsQueryKey({ requestId }: { requestId: string }) {
return ['grpc_connections', { requestId }];
}
export function useGrpcConnections(requestId: string | null) {
return (
useQuery<GrpcConnection[]>({
enabled: requestId !== null,
initialData: [],
queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('cmd_list_grpc_connections', {
requestId,
limit: 200,
})) as GrpcConnection[];
},
}).data ?? []
);
}

View File

@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcMessage } from '../lib/models';
export function grpcMessagesQueryKey({ connectionId }: { connectionId: string }) {
return ['grpc_messages', { connectionId }];
}
export function useGrpcMessages(connectionId: string | null) {
return (
useQuery<GrpcMessage[]>({
enabled: connectionId !== null,
initialData: [],
queryKey: grpcMessagesQueryKey({ connectionId: connectionId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('cmd_list_grpc_messages', {
connectionId,
limit: 200,
})) as GrpcMessage[];
},
}).data ?? []
);
}

View File

@@ -2,18 +2,18 @@ import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
export function responsesQueryKey({ requestId }: { requestId: string }) { export function httpResponsesQueryKey({ requestId }: { requestId: string }) {
return ['http_responses', { requestId }]; return ['http_responses', { requestId }];
} }
export function useResponses(requestId: string | null) { export function useHttpResponses(requestId: string | null) {
return ( return (
useQuery<HttpResponse[]>({ useQuery<HttpResponse[]>({
enabled: requestId !== null, enabled: requestId !== null,
initialData: [], initialData: [],
queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }), queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => { queryFn: async () => {
return (await invoke('cmd_list_responses', { requestId, limit: 200 })) as HttpResponse[]; return (await invoke('cmd_list_http_responses', { requestId, limit: 200 })) as HttpResponse[];
}, },
}).data ?? [] }).data ?? []
); );

View File

@@ -1,7 +1,7 @@
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { useResponses } from './useResponses'; import { useHttpResponses } from './useHttpResponses';
export function useLatestResponse(requestId: string | null): HttpResponse | null { export function useLatestResponse(requestId: string | null): HttpResponse | null {
const responses = useResponses(requestId); const responses = useHttpResponses(requestId);
return responses[0] ?? null; return responses[0] ?? null;
} }

View File

@@ -12,7 +12,9 @@ export const AUTH_TYPE_BEARER = 'bearer';
export type Model = export type Model =
| Settings | Settings
| Workspace | Workspace
| GrpcConnection
| GrpcRequest | GrpcRequest
| GrpcMessage
| HttpRequest | HttpRequest
| HttpResponse | HttpResponse
| KeyValue | KeyValue
@@ -114,6 +116,24 @@ export interface GrpcRequest extends BaseModel {
message: string; message: string;
} }
export interface GrpcMessage extends BaseModel {
readonly workspaceId: string;
readonly requestId: string;
readonly connectionId: string;
readonly model: 'grpc_message';
message: string;
isServer: boolean;
isInfo: boolean;
}
export interface GrpcConnection extends BaseModel {
readonly workspaceId: string;
readonly requestId: string;
readonly model: 'grpc_connection';
service: string;
method: string;
}
export interface HttpRequest extends BaseModel { export interface HttpRequest extends BaseModel {
readonly workspaceId: string; readonly workspaceId: string;
readonly model: 'http_request'; readonly model: 'http_request';