mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-22 16:58:28 +02:00
Proto selection UI/models
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message\n FROM grpc_requests\n WHERE workspace_id = ?\n ",
|
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json<Vec<String>>\"\n FROM grpc_requests\n WHERE id = ?\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -62,6 +62,11 @@
|
|||||||
"name": "message",
|
"name": "message",
|
||||||
"ordinal": 11,
|
"ordinal": 11,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "proto_files!: sqlx::types::Json<Vec<String>>",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -79,8 +84,9 @@
|
|||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "35c9607291ee400e7696393bec7f4aa254a2dd163c4f79ea368ac6ee5f74c365"
|
"hash": "7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message\n FROM grpc_requests\n WHERE id = ?\n ",
|
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json<Vec<String>>\"\n FROM grpc_requests\n WHERE workspace_id = ?\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -62,6 +62,11 @@
|
|||||||
"name": "message",
|
"name": "message",
|
||||||
"ordinal": 11,
|
"ordinal": 11,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "proto_files!: sqlx::types::Json<Vec<String>>",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -79,8 +84,9 @@
|
|||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "c5f9b12bca35fe65ae2a1625e7f09c7855ce107046113a45f6da2abcff41819f"
|
"hash": "761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7"
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 9
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "d2dc9a652fe08623d70b8b0a2e6823d096e8cbcb9f06544ab245f41425335a25"
|
|
||||||
}
|
|
||||||
12
src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json
generated
Normal file
12
src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message,\n proto_files\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message,\n proto_files = excluded.proto_files\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 10
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740"
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tonic = "0.10.2"
|
tonic = "0.10.2"
|
||||||
prost = "0.12"
|
prost = "0.12"
|
||||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "fs"] }
|
||||||
tonic-reflection = "0.10.2"
|
tonic-reflection = "0.10.2"
|
||||||
tokio-stream = "0.1.14"
|
tokio-stream = "0.1.14"
|
||||||
prost-types = "0.12.3"
|
prost-types = "0.12.3"
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
use prost_reflect::SerializeOptions;
|
use prost_reflect::SerializeOptions;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tonic::transport::Uri;
|
|
||||||
|
|
||||||
use crate::proto::fill_pool;
|
|
||||||
|
|
||||||
mod codec;
|
mod codec;
|
||||||
mod json_schema;
|
mod json_schema;
|
||||||
@@ -28,31 +25,3 @@ pub struct MethodDefinition {
|
|||||||
pub client_streaming: bool,
|
pub client_streaming: bool,
|
||||||
pub server_streaming: bool,
|
pub server_streaming: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reflect(uri: &Uri) -> Result<Vec<ServiceDefinition>, String> {
|
|
||||||
let (pool, _) = fill_pool(uri).await?;
|
|
||||||
|
|
||||||
Ok(pool
|
|
||||||
.services()
|
|
||||||
.map(|s| {
|
|
||||||
let mut def = ServiceDefinition {
|
|
||||||
name: s.full_name().to_string(),
|
|
||||||
methods: vec![],
|
|
||||||
};
|
|
||||||
for method in s.methods() {
|
|
||||||
let input_message = method.input();
|
|
||||||
def.methods.push(MethodDefinition {
|
|
||||||
name: method.name().to_string(),
|
|
||||||
server_streaming: method.is_server_streaming(),
|
|
||||||
client_streaming: method.is_client_streaming(),
|
|
||||||
schema: serde_json::to_string_pretty(&json_schema::message_to_json_schema(
|
|
||||||
&pool,
|
|
||||||
input_message,
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
def
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use hyper_rustls::HttpsConnector;
|
|||||||
use prost_reflect::DescriptorPool;
|
use prost_reflect::DescriptorPool;
|
||||||
pub use prost_reflect::DynamicMessage;
|
pub use prost_reflect::DynamicMessage;
|
||||||
use serde_json::Deserializer;
|
use serde_json::Deserializer;
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tonic::body::BoxBody;
|
use tonic::body::BoxBody;
|
||||||
@@ -15,6 +14,7 @@ use tonic::{IntoRequest, IntoStreamingRequest, Streaming};
|
|||||||
|
|
||||||
use crate::codec::DynamicCodec;
|
use crate::codec::DynamicCodec;
|
||||||
use crate::proto::{fill_pool, method_desc_to_path};
|
use crate::proto::{fill_pool, method_desc_to_path};
|
||||||
|
use crate::{json_schema, MethodDefinition, ServiceDefinition};
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, String>;
|
type Result<T> = std::result::Result<T, String>;
|
||||||
|
|
||||||
@@ -145,25 +145,61 @@ impl GrpcConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GrpcManager {
|
pub struct GrpcHandle {
|
||||||
connections: HashMap<String, GrpcConnection>,
|
connections: HashMap<String, GrpcConnection>,
|
||||||
pub send: mpsc::Sender<String>,
|
pools: HashMap<String, DescriptorPool>,
|
||||||
pub recv: mpsc::Receiver<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GrpcManager {
|
impl Default for GrpcHandle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let (send, recv) = mpsc::channel(100);
|
|
||||||
let connections = HashMap::new();
|
let connections = HashMap::new();
|
||||||
Self {
|
let pools = HashMap::new();
|
||||||
connections,
|
Self { connections, pools }
|
||||||
send,
|
|
||||||
recv,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrpcManager {
|
impl GrpcHandle {
|
||||||
|
pub async fn clean_reflect(&mut self, uri: &Uri) -> Result<Vec<ServiceDefinition>> {
|
||||||
|
self.pools.remove(&uri.to_string());
|
||||||
|
self.reflect(uri).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reflect(&mut self, uri: &Uri) -> Result<Vec<ServiceDefinition>> {
|
||||||
|
let pool = match self.pools.get(&uri.to_string()) {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => {
|
||||||
|
let (pool, _) = fill_pool(uri).await?;
|
||||||
|
self.pools.insert(uri.to_string(), pool.clone());
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
pool.services()
|
||||||
|
.map(|s| {
|
||||||
|
let mut def = ServiceDefinition {
|
||||||
|
name: s.full_name().to_string(),
|
||||||
|
methods: vec![],
|
||||||
|
};
|
||||||
|
for method in s.methods() {
|
||||||
|
let input_message = method.input();
|
||||||
|
def.methods.push(MethodDefinition {
|
||||||
|
name: method.name().to_string(),
|
||||||
|
server_streaming: method.is_server_streaming(),
|
||||||
|
client_streaming: method.is_client_streaming(),
|
||||||
|
schema: serde_json::to_string_pretty(
|
||||||
|
&json_schema::message_to_json_schema(&pool, input_message),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
def
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn server_streaming(
|
pub async fn server_streaming(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
@@ -9,17 +10,27 @@ use log::warn;
|
|||||||
use prost::Message;
|
use prost::Message;
|
||||||
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
||||||
use prost_types::FileDescriptorProto;
|
use prost_types::FileDescriptorProto;
|
||||||
|
use tokio::fs;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tonic::body::BoxBody;
|
use tonic::body::BoxBody;
|
||||||
use tonic::codegen::http::uri::PathAndQuery;
|
use tonic::codegen::http::uri::PathAndQuery;
|
||||||
use tonic::transport::Uri;
|
use tonic::transport::Uri;
|
||||||
use tonic::Code::Unimplemented;
|
|
||||||
use tonic::Request;
|
use tonic::Request;
|
||||||
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
|
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
|
||||||
use tonic_reflection::pb::server_reflection_request::MessageRequest;
|
use tonic_reflection::pb::server_reflection_request::MessageRequest;
|
||||||
use tonic_reflection::pb::server_reflection_response::MessageResponse;
|
use tonic_reflection::pb::server_reflection_response::MessageResponse;
|
||||||
use tonic_reflection::pb::ServerReflectionRequest;
|
use tonic_reflection::pb::ServerReflectionRequest;
|
||||||
|
|
||||||
|
pub async fn fill_pool_from_files(paths: Vec<PathBuf>) -> Result<DescriptorPool, String> {
|
||||||
|
let mut pool = DescriptorPool::new();
|
||||||
|
for p in paths {
|
||||||
|
let bytes = fs::read(p).await.unwrap();
|
||||||
|
let fdp = FileDescriptorProto::decode(bytes.deref()).unwrap();
|
||||||
|
pool.add_file_descriptor_proto(fdp)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
pub async fn fill_pool(
|
pub async fn fill_pool(
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
@@ -155,7 +166,9 @@ async fn send_reflection_request(
|
|||||||
.server_reflection_info(request)
|
.server_reflection_info(request)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match e.code() {
|
.map_err(|e| match e.code() {
|
||||||
Unimplemented => "Reflection not implemented for server".to_string(),
|
tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(),
|
||||||
|
tonic::Code::Unauthenticated => "Authentication failed".to_string(),
|
||||||
|
tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(),
|
||||||
_ => e.to_string(),
|
_ => e.to_string(),
|
||||||
})?
|
})?
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
|||||||
1
src-tauri/migrations/20240206191206_grpc-protos.sql
Normal file
1
src-tauri/migrations/20240206191206_grpc-protos.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE grpc_requests ADD COLUMN proto_files TEXT DEFAULT '[]' NOT NULL;
|
||||||
@@ -34,7 +34,7 @@ use tokio::sync::Mutex;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use window_shadows::set_shadow;
|
use window_shadows::set_shadow;
|
||||||
|
|
||||||
use grpc::manager::GrpcManager;
|
use grpc::manager::GrpcHandle;
|
||||||
use grpc::ServiceDefinition;
|
use grpc::ServiceDefinition;
|
||||||
use window_ext::TrafficLightWindowExt;
|
use window_ext::TrafficLightWindowExt;
|
||||||
|
|
||||||
@@ -95,19 +95,20 @@ async fn migrate_db(app_handle: AppHandle, db: &Mutex<Pool<Sqlite>>) -> Result<(
|
|||||||
async fn cmd_grpc_reflect(
|
async fn cmd_grpc_reflect(
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
|
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||||
) -> Result<Vec<ServiceDefinition>, String> {
|
) -> Result<Vec<ServiceDefinition>, String> {
|
||||||
let req = get_grpc_request(&app_handle, request_id)
|
let req = get_grpc_request(&app_handle, request_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
|
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
|
||||||
grpc::reflect(&uri).await
|
grpc_handle.lock().await.clean_reflect(&uri).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_grpc_call_unary(
|
async fn cmd_grpc_call_unary(
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
grpc_handle: State<'_, Mutex<GrpcManager>>,
|
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||||
) -> Result<GrpcMessage, String> {
|
) -> Result<GrpcMessage, String> {
|
||||||
let req = get_grpc_request(&app_handle, request_id)
|
let req = get_grpc_request(&app_handle, request_id)
|
||||||
.await
|
.await
|
||||||
@@ -316,7 +317,7 @@ async fn cmd_grpc_client_streaming(
|
|||||||
let conn = conn.clone();
|
let conn = conn.clone();
|
||||||
let req = req.clone();
|
let req = req.clone();
|
||||||
async move {
|
async move {
|
||||||
let grpc_handle = app_handle.state::<Mutex<GrpcManager>>();
|
let grpc_handle = app_handle.state::<Mutex<GrpcHandle>>();
|
||||||
let msg = grpc_handle
|
let msg = grpc_handle
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
@@ -404,7 +405,7 @@ async fn cmd_grpc_client_streaming(
|
|||||||
async fn cmd_grpc_streaming(
|
async fn cmd_grpc_streaming(
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
grpc_handle: State<'_, Mutex<GrpcManager>>,
|
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let req = get_grpc_request(&app_handle, request_id)
|
let req = get_grpc_request(&app_handle, request_id)
|
||||||
.await
|
.await
|
||||||
@@ -614,7 +615,7 @@ async fn cmd_grpc_streaming(
|
|||||||
async fn cmd_grpc_server_streaming(
|
async fn cmd_grpc_server_streaming(
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
grpc_handle: State<'_, Mutex<GrpcManager>>,
|
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||||
) -> Result<GrpcConnection, String> {
|
) -> Result<GrpcConnection, String> {
|
||||||
let req = get_grpc_request(&app_handle, request_id)
|
let req = get_grpc_request(&app_handle, request_id)
|
||||||
.await
|
.await
|
||||||
@@ -1627,7 +1628,7 @@ fn main() {
|
|||||||
app.manage(Mutex::new(yaak_updater));
|
app.manage(Mutex::new(yaak_updater));
|
||||||
|
|
||||||
// Add GRPC manager
|
// Add GRPC manager
|
||||||
let grpc_handle = GrpcManager::default();
|
let grpc_handle = GrpcHandle::default();
|
||||||
app.manage(Mutex::new(grpc_handle));
|
app.manage(Mutex::new(grpc_handle));
|
||||||
|
|
||||||
// Add DB handle
|
// Add DB handle
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ pub struct GrpcRequest {
|
|||||||
pub service: Option<String>,
|
pub service: Option<String>,
|
||||||
pub method: Option<String>,
|
pub method: Option<String>,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
pub proto_files: Json<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
@@ -497,9 +498,10 @@ pub async fn upsert_grpc_request(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO grpc_requests (
|
INSERT INTO grpc_requests (
|
||||||
id, name, workspace_id, folder_id, sort_priority, url, service, method, message
|
id, name, workspace_id, folder_id, sort_priority, url, service, method, message,
|
||||||
|
proto_files
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
updated_at = CURRENT_TIMESTAMP,
|
updated_at = CURRENT_TIMESTAMP,
|
||||||
name = excluded.name,
|
name = excluded.name,
|
||||||
@@ -508,7 +510,8 @@ pub async fn upsert_grpc_request(
|
|||||||
url = excluded.url,
|
url = excluded.url,
|
||||||
service = excluded.service,
|
service = excluded.service,
|
||||||
method = excluded.method,
|
method = excluded.method,
|
||||||
message = excluded.message
|
message = excluded.message,
|
||||||
|
proto_files = excluded.proto_files
|
||||||
"#,
|
"#,
|
||||||
id,
|
id,
|
||||||
trimmed_name,
|
trimmed_name,
|
||||||
@@ -519,6 +522,7 @@ pub async fn upsert_grpc_request(
|
|||||||
request.service,
|
request.service,
|
||||||
request.method,
|
request.method,
|
||||||
request.message,
|
request.message,
|
||||||
|
request.proto_files,
|
||||||
)
|
)
|
||||||
.execute(&db)
|
.execute(&db)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -539,7 +543,8 @@ pub async fn get_grpc_request(
|
|||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,
|
id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,
|
||||||
url, service, method, message
|
url, service, method, message,
|
||||||
|
proto_files AS "proto_files!: sqlx::types::Json<Vec<String>>"
|
||||||
FROM grpc_requests
|
FROM grpc_requests
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
"#,
|
"#,
|
||||||
@@ -559,7 +564,8 @@ pub async fn list_grpc_requests(
|
|||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,
|
id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,
|
||||||
url, service, method, message
|
url, service, method, message,
|
||||||
|
proto_files AS "proto_files!: sqlx::types::Json<Vec<String>>"
|
||||||
FROM grpc_requests
|
FROM grpc_requests
|
||||||
WHERE workspace_id = ?
|
WHERE workspace_id = ?
|
||||||
"#,
|
"#,
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
onReflectRefetch={grpc.reflect.refetch}
|
onReflectRefetch={grpc.reflect.refetch}
|
||||||
services={services ?? null}
|
services={services ?? null}
|
||||||
reflectionError={grpc.reflect.error as string | undefined}
|
reflectionError={grpc.reflect.error as string | undefined}
|
||||||
reflectionLoading={grpc.reflect.isLoading}
|
reflectionLoading={grpc.reflect.isFetching}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
secondSlot={({ style }) =>
|
secondSlot={({ style }) =>
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import type { ReflectResponseService } from '../hooks/useGrpc';
|
|||||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||||
import type { GrpcRequest } from '../lib/models';
|
import type { GrpcRequest } from '../lib/models';
|
||||||
import { Banner } from './core/Banner';
|
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { FormattedError } from './core/FormattedError';
|
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { RadioDropdown } from './core/RadioDropdown';
|
import { RadioDropdown } from './core/RadioDropdown';
|
||||||
@@ -77,6 +75,11 @@ export function GrpcConnectionSetupPane({
|
|||||||
[updateRequest],
|
[updateRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSelectProtoFiles = useCallback(
|
||||||
|
(paths: string[]) => updateRequest.mutateAsync({ protoFiles: paths }),
|
||||||
|
[updateRequest],
|
||||||
|
);
|
||||||
|
|
||||||
const select = useMemo(() => {
|
const select = useMemo(() => {
|
||||||
const options =
|
const options =
|
||||||
services?.flatMap((s) =>
|
services?.flatMap((s) =>
|
||||||
@@ -225,17 +228,14 @@ export function GrpcConnectionSetupPane({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
<GrpcEditor
|
<GrpcEditor
|
||||||
forceUpdateKey={activeRequest?.id ?? ''}
|
|
||||||
url={activeRequest.url ?? ''}
|
|
||||||
defaultValue={activeRequest.message}
|
|
||||||
onChange={handleChangeMessage}
|
onChange={handleChangeMessage}
|
||||||
service={activeRequest.service}
|
|
||||||
services={services}
|
services={services}
|
||||||
method={activeRequest.method}
|
|
||||||
className="bg-gray-50"
|
className="bg-gray-50"
|
||||||
reflectionError={reflectionError}
|
reflectionError={reflectionError}
|
||||||
reflectionLoading={reflectionLoading}
|
reflectionLoading={reflectionLoading}
|
||||||
onReflect={onReflectRefetch}
|
onReflect={onReflectRefetch}
|
||||||
|
onSelectProtoFiles={handleSelectProtoFiles}
|
||||||
|
request={activeRequest}
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,70 +1,72 @@
|
|||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
import type { EditorView } from 'codemirror';
|
import type { EditorView } from 'codemirror';
|
||||||
import { updateSchema } from 'codemirror-json-schema';
|
import { updateSchema } from 'codemirror-json-schema';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useAlert } from '../hooks/useAlert';
|
import { useAlert } from '../hooks/useAlert';
|
||||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
|
import type { GrpcRequest } from '../lib/models';
|
||||||
|
import { count } from '../lib/pluralize';
|
||||||
|
import { Banner } from './core/Banner';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import type { EditorProps } from './core/Editor';
|
import type { EditorProps } from './core/Editor';
|
||||||
import { Editor } from './core/Editor';
|
import { Editor } from './core/Editor';
|
||||||
import { FormattedError } from './core/FormattedError';
|
import { FormattedError } from './core/FormattedError';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { Link } from './core/Link';
|
||||||
import { HStack, VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { useDialog } from './DialogContext';
|
import { useDialog } from './DialogContext';
|
||||||
|
import { GrpcProtoSelection } from './GrpcProtoSelection';
|
||||||
|
|
||||||
type Props = Pick<
|
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
|
||||||
EditorProps,
|
|
||||||
'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'forceUpdateKey'
|
|
||||||
> & {
|
|
||||||
url: string;
|
|
||||||
service: string | null;
|
|
||||||
method: string | null;
|
|
||||||
services: ReflectResponseService[] | null;
|
services: ReflectResponseService[] | null;
|
||||||
reflectionError?: string;
|
reflectionError?: string;
|
||||||
reflectionLoading?: boolean;
|
reflectionLoading?: boolean;
|
||||||
|
request: GrpcRequest;
|
||||||
onReflect: () => void;
|
onReflect: () => void;
|
||||||
|
onSelectProtoFiles: (paths: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GrpcEditor({
|
export function GrpcEditor({
|
||||||
service,
|
|
||||||
method,
|
|
||||||
services,
|
services,
|
||||||
defaultValue,
|
|
||||||
reflectionError,
|
reflectionError,
|
||||||
reflectionLoading,
|
reflectionLoading,
|
||||||
onReflect,
|
onReflect,
|
||||||
|
onSelectProtoFiles,
|
||||||
|
request,
|
||||||
...extraEditorProps
|
...extraEditorProps
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const editorViewRef = useRef<EditorView>(null);
|
const editorViewRef = useRef<EditorView>(null);
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
// Find the schema for the selected service and method and update the editor
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorViewRef.current == null || services === null) return;
|
if (editorViewRef.current == null || services === null) return;
|
||||||
|
|
||||||
const s = services?.find((s) => s.name === service);
|
const s = services.find((s) => s.name === request.service);
|
||||||
if (service != null && s == null) {
|
if (request.service != null && s == null) {
|
||||||
alert({
|
alert({
|
||||||
id: 'grpc-find-service-error',
|
id: 'grpc-find-service-error',
|
||||||
title: "Couldn't Find Service",
|
title: "Couldn't Find Service",
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
Failed to find service <InlineCode>{service}</InlineCode> in schema
|
Failed to find service <InlineCode>{request.service}</InlineCode> in schema
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = s?.methods.find((m) => m.name === method)?.schema;
|
const schema = s?.methods.find((m) => m.name === request.method)?.schema;
|
||||||
if (method != null && schema == null) {
|
if (request.method != null && schema == null) {
|
||||||
alert({
|
alert({
|
||||||
id: 'grpc-find-schema-error',
|
id: 'grpc-find-schema-error',
|
||||||
title: "Couldn't Find Method",
|
title: "Couldn't Find Method",
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
Failed to find method <InlineCode>{method}</InlineCode> for{' '}
|
Failed to find method <InlineCode>{request.method}</InlineCode> for{' '}
|
||||||
<InlineCode>{service}</InlineCode> in schema
|
<InlineCode>{request.service}</InlineCode> in schema
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -84,66 +86,98 @@ export function GrpcEditor({
|
|||||||
body: (
|
body: (
|
||||||
<VStack space={4}>
|
<VStack space={4}>
|
||||||
<p>
|
<p>
|
||||||
For service <InlineCode>{service}</InlineCode> and method{' '}
|
For service <InlineCode>{request.service}</InlineCode> and method{' '}
|
||||||
<InlineCode>{method}</InlineCode>
|
<InlineCode>{request.method}</InlineCode>
|
||||||
</p>
|
</p>
|
||||||
<FormattedError>{String(err)}</FormattedError>
|
<FormattedError>{String(err)}</FormattedError>
|
||||||
</VStack>
|
</VStack>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
console.log('Failed to parse method schema', method, schema);
|
|
||||||
}
|
}
|
||||||
}, [alert, services, method, service]);
|
}, [alert, services, request.method, request.service]);
|
||||||
|
|
||||||
|
const reflectionUnavailable = reflectionError?.match(/unimplemented/i);
|
||||||
|
reflectionError = reflectionUnavailable ? undefined : reflectionError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||||
<Editor
|
<Editor
|
||||||
contentType="application/grpc"
|
contentType="application/grpc"
|
||||||
defaultValue={defaultValue}
|
forceUpdateKey={request.id}
|
||||||
|
defaultValue={request.message}
|
||||||
format={tryFormatJson}
|
format={tryFormatJson}
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
placeholder="..."
|
placeholder="..."
|
||||||
ref={editorViewRef}
|
ref={editorViewRef}
|
||||||
actions={
|
actions={[
|
||||||
reflectionError || reflectionLoading
|
<div key="reflection" className="!opacity-100">
|
||||||
? [
|
<Button
|
||||||
<div key="introspection" className="!opacity-100">
|
size="xs"
|
||||||
<Button
|
color={
|
||||||
key="introspection"
|
reflectionLoading
|
||||||
size="xs"
|
? 'gray'
|
||||||
color={reflectionError ? 'danger' : 'gray'}
|
: reflectionUnavailable
|
||||||
isLoading={reflectionLoading}
|
? 'secondary'
|
||||||
onClick={() => {
|
: reflectionError
|
||||||
dialog.show({
|
? 'danger'
|
||||||
title: 'Introspection Failed',
|
: 'gray'
|
||||||
size: 'dynamic',
|
}
|
||||||
id: 'introspection-failed',
|
isLoading={reflectionLoading}
|
||||||
render: () => (
|
onClick={() => {
|
||||||
<>
|
dialog.show({
|
||||||
<FormattedError>{reflectionError ?? 'unknown'}</FormattedError>
|
title: 'Configure Schema',
|
||||||
<HStack className="w-full my-4" space={2} justifyContent="end">
|
size: 'md',
|
||||||
<Button color="gray">Select .proto</Button>
|
id: 'reflection-failed',
|
||||||
|
render: ({ hide }) => (
|
||||||
|
<VStack space={6} className="pb-5">
|
||||||
|
{reflectionError && <FormattedError>{reflectionError}</FormattedError>}
|
||||||
|
{reflectionUnavailable && request.protoFiles.length === 0 && (
|
||||||
|
<Banner>
|
||||||
|
<VStack space={3}>
|
||||||
|
<p>
|
||||||
|
<InlineCode>{request.url}</InlineCode> doesn't implement{' '}
|
||||||
|
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
|
||||||
|
Server Reflection
|
||||||
|
</Link>{' '}
|
||||||
|
. Please manually add the <InlineCode>.proto</InlineCode> files to get
|
||||||
|
started.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="gray"
|
||||||
|
variant="border"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialog.hide('introspection-failed');
|
hide();
|
||||||
onReflect();
|
onReflect();
|
||||||
}}
|
}}
|
||||||
color="secondary"
|
|
||||||
>
|
>
|
||||||
Try Again
|
Retry Reflection
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</div>
|
||||||
</>
|
</VStack>
|
||||||
),
|
</Banner>
|
||||||
});
|
)}
|
||||||
}}
|
<GrpcProtoSelection requestId={request.id} />
|
||||||
>
|
</VStack>
|
||||||
{reflectionError ? 'Reflection Failed' : 'Reflecting'}
|
),
|
||||||
</Button>
|
});
|
||||||
</div>,
|
}}
|
||||||
]
|
>
|
||||||
: []
|
{reflectionLoading
|
||||||
}
|
? 'Inspecting Schema'
|
||||||
|
: reflectionUnavailable
|
||||||
|
? 'Select Proto Files'
|
||||||
|
: reflectionError
|
||||||
|
? 'Server Error'
|
||||||
|
: services != null
|
||||||
|
? 'Proto Schema'
|
||||||
|
: request.protoFiles.length > 0
|
||||||
|
? count('Proto File', request.protoFiles.length)
|
||||||
|
: 'Select Schema'}
|
||||||
|
</Button>
|
||||||
|
</div>,
|
||||||
|
]}
|
||||||
{...extraEditorProps}
|
{...extraEditorProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
79
src-web/components/GrpcProtoSelection.tsx
Normal file
79
src-web/components/GrpcProtoSelection.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
|
import { useGrpcRequest } from '../hooks/useGrpcRequest';
|
||||||
|
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { IconButton } from './core/IconButton';
|
||||||
|
import { HStack } from './core/Stacks';
|
||||||
|
|
||||||
|
export function GrpcProtoSelection({ requestId }: { requestId: string }) {
|
||||||
|
const request = useGrpcRequest(requestId);
|
||||||
|
const updateRequest = useUpdateGrpcRequest(request?.id ?? null);
|
||||||
|
|
||||||
|
if (request == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{request.protoFiles.length > 0 && (
|
||||||
|
<table className="w-full divide-y mb-3">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-gray-600">
|
||||||
|
<span className="font-mono text-sm">*.proto</span> Files
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{request.protoFiles.map((f, i) => (
|
||||||
|
<tr key={f + i} className="group">
|
||||||
|
<td className="pl-1 text-sm font-mono">{f.split('/').pop()}</td>
|
||||||
|
<td className="w-0 py-0.5">
|
||||||
|
<IconButton
|
||||||
|
title="Remove file"
|
||||||
|
size="sm"
|
||||||
|
icon="trash"
|
||||||
|
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100"
|
||||||
|
onClick={() => {
|
||||||
|
updateRequest.mutate({
|
||||||
|
protoFiles: request.protoFiles.filter((p) => p !== f),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
<HStack space={2} justifyContent="end">
|
||||||
|
<Button
|
||||||
|
color="gray"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
updateRequest.mutate({ protoFiles: [] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear Files
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
const files = await open({
|
||||||
|
title: 'Select Proto Files',
|
||||||
|
multiple: true,
|
||||||
|
filters: [{ name: 'Proto Files', extensions: ['proto'] }],
|
||||||
|
});
|
||||||
|
if (files == null || typeof files === 'string') return;
|
||||||
|
const newFiles = files.filter((f) => !request.protoFiles.includes(f));
|
||||||
|
updateRequest.mutate({ protoFiles: [...request.protoFiles, ...newFiles] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Files
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { Simulate } from 'react-dom/test-utils';
|
||||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||||
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
|
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
|
||||||
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
|
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
|
||||||
@@ -12,14 +13,18 @@ export const SidebarActions = memo(function SidebarActions() {
|
|||||||
const createHttpRequest = useCreateHttpRequest();
|
const createHttpRequest = useCreateHttpRequest();
|
||||||
const createGrpcRequest = useCreateGrpcRequest();
|
const createGrpcRequest = useCreateGrpcRequest();
|
||||||
const createFolder = useCreateFolder();
|
const createFolder = useCreateFolder();
|
||||||
const { hidden, toggle } = useSidebarHidden();
|
const { hidden, show, hide } = useSidebarHidden();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack>
|
<HStack>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
trackEvent('Sidebar', 'Toggle');
|
trackEvent('Sidebar', 'Toggle');
|
||||||
await toggle();
|
|
||||||
|
// NOTE: We're not using `toggle` because it may be out of sync
|
||||||
|
// from changes in other windows
|
||||||
|
if (hidden) await show();
|
||||||
|
else await hide();
|
||||||
}}
|
}}
|
||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
size === 'md' && 'h-md px-3',
|
size === 'md' && 'h-md px-3',
|
||||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||||
size === 'xs' && 'h-xs px-2 text-sm',
|
size === 'xs' && 'h-xs px-2 text-sm',
|
||||||
variant === 'border' && 'border',
|
|
||||||
// Solids
|
// Solids
|
||||||
variant === 'solid' && color === 'custom' && 'ring-blue-500/50',
|
variant === 'solid' && color === 'custom' && 'ring-blue-500/50',
|
||||||
variant === 'solid' &&
|
variant === 'solid' &&
|
||||||
@@ -82,12 +81,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
color === 'danger' &&
|
color === 'danger' &&
|
||||||
'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
|
'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
|
||||||
// Borders
|
// Borders
|
||||||
|
variant === 'border' && 'border',
|
||||||
variant === 'border' &&
|
variant === 'border' &&
|
||||||
color === 'default' &&
|
color === 'default' &&
|
||||||
'border-highlight text-gray-700 enabled:hocus:border-focus enabled:hocus:text-gray-1000 ring-blue-500/50',
|
'border-highlight text-gray-700 enabled:hocus:border-focus enabled:hocus:text-gray-800 ring-blue-500/50',
|
||||||
variant === 'border' &&
|
variant === 'border' &&
|
||||||
color === 'gray' &&
|
color === 'gray' &&
|
||||||
'border-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
'border-gray-500/70 text-gray-700 enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-800 ring-blue-500/50',
|
||||||
variant === 'border' &&
|
variant === 'border' &&
|
||||||
color === 'primary' &&
|
color === 'primary' &&
|
||||||
'border-blue-500/70 text-blue-700 enabled:hocus:border-blue-500 ring-blue-500/50',
|
'border-blue-500/70 text-blue-700 enabled:hocus:border-blue-500 ring-blue-500/50',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
|
|||||||
<code
|
<code
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'font-mono text-sm bg-highlight border-0 border-gray-200',
|
'font-mono text-xs bg-highlight border-0 border-gray-200/30',
|
||||||
'px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
|
'px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
35
src-web/components/core/Link.tsx
Normal file
35
src-web/components/core/Link.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link({ href, children, className, ...other }: Props) {
|
||||||
|
const isExternal = href.match(/^https?:\/\//);
|
||||||
|
|
||||||
|
className = classNames(className, 'relative underline hover:text-violet-600');
|
||||||
|
|
||||||
|
if (isExternal) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={classNames(className, 'pr-4')}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
<span className="underline">{children}</span>
|
||||||
|
<Icon className="inline absolute right-0.5 top-0.5" size="xs" icon="externalLink" />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouterLink to={href} className={className} {...other}>
|
||||||
|
{children}
|
||||||
|
</RouterLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { useCallback } from 'react';
|
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||||
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
|
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
|
||||||
|
import { useDebouncedValue } from './useDebouncedValue';
|
||||||
|
|
||||||
export interface ReflectResponseService {
|
export interface ReflectResponseService {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -50,11 +51,16 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
|
|||||||
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
|
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const reflect = useQuery<ReflectResponseService[]>({
|
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000);
|
||||||
queryKey: ['grpc_reflect', req?.url ?? 'n/a'],
|
const reflect = useQuery<ReflectResponseService[] | null>({
|
||||||
|
enabled: req != null && req.protoFiles.length === 0,
|
||||||
|
queryKey: ['grpc_reflect', debouncedUrl],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
console.log('REFLECTING...');
|
||||||
return (await invoke('cmd_grpc_reflect', { requestId })) as ReflectResponseService[];
|
return (await minPromiseMillis(
|
||||||
|
invoke('cmd_grpc_reflect', { requestId }),
|
||||||
|
1000,
|
||||||
|
)) as ReflectResponseService[];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
7
src-web/hooks/useGrpcRequest.ts
Normal file
7
src-web/hooks/useGrpcRequest.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { GrpcRequest } from '../lib/models';
|
||||||
|
import { useGrpcRequests } from './useGrpcRequests';
|
||||||
|
|
||||||
|
export function useGrpcRequest(id: string | null): GrpcRequest | null {
|
||||||
|
const requests = useGrpcRequests();
|
||||||
|
return requests.find((r) => r.id === id) ?? null;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { useHttpRequests } from './useHttpRequests';
|
import { useHttpRequests } from './useHttpRequests';
|
||||||
|
|
||||||
export function useRequest(id: string | null): HttpRequest | null {
|
export function useHttpRequest(id: string | null): HttpRequest | null {
|
||||||
const requests = useHttpRequests();
|
const requests = useHttpRequests();
|
||||||
return requests.find((r) => r.id === id) ?? null;
|
return requests.find((r) => r.id === id) ?? null;
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ export function useKeyValue<T extends Object | null>({
|
|||||||
const query = useQuery<T>({
|
const query = useQuery<T>({
|
||||||
queryKey: keyValueQueryKey({ namespace, key }),
|
queryKey: keyValueQueryKey({ namespace, key }),
|
||||||
queryFn: async () => getKeyValue({ namespace, key, fallback: defaultValue }),
|
queryFn: async () => getKeyValue({ namespace, key, fallback: defaultValue }),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutate = useMutation<void, unknown, T>({
|
const mutate = useMutation<void, unknown, T>({
|
||||||
|
|||||||
@@ -1,6 +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 type { GrpcRequest } from '../lib/models';
|
import type { GrpcRequest } from '../lib/models';
|
||||||
|
import { sleep } from '../lib/sleep';
|
||||||
import { getGrpcRequest } from '../lib/store';
|
import { getGrpcRequest } from '../lib/store';
|
||||||
import { grpcRequestsQueryKey } from './useGrpcRequests';
|
import { grpcRequestsQueryKey } from './useGrpcRequests';
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import type { GrpcRequest } from '../lib/models';
|
|||||||
import { useUpdateAnyGrpcRequest } from './useUpdateAnyGrpcRequest';
|
import { useUpdateAnyGrpcRequest } from './useUpdateAnyGrpcRequest';
|
||||||
|
|
||||||
export function useUpdateGrpcRequest(id: string | null) {
|
export function useUpdateGrpcRequest(id: string | null) {
|
||||||
const updateAnyRequest = useUpdateAnyGrpcRequest();
|
const updateAnyGrpcRequest = useUpdateAnyGrpcRequest();
|
||||||
return useMutation<void, unknown, Partial<GrpcRequest> | ((r: GrpcRequest) => GrpcRequest)>({
|
return useMutation<void, unknown, Partial<GrpcRequest> | ((r: GrpcRequest) => GrpcRequest)>({
|
||||||
mutationFn: async (update) => updateAnyRequest.mutateAsync({ id: id ?? 'n/a', update }),
|
mutationFn: async (update) => {
|
||||||
|
return updateAnyGrpcRequest.mutateAsync({ id: id ?? 'n/a', update });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { sleep } from './sleep';
|
import { sleep } from './sleep';
|
||||||
|
|
||||||
|
/** Ensures a promise takes at least a certain number of milliseconds to resolve */
|
||||||
export async function minPromiseMillis<T>(promise: Promise<T>, millis: number) {
|
export async function minPromiseMillis<T>(promise: Promise<T>, millis: number) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const result = await promise;
|
let result;
|
||||||
|
let err;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await promise;
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
const delayFor = millis - (Date.now() - start);
|
const delayFor = millis - (Date.now() - start);
|
||||||
await sleep(delayFor);
|
await sleep(delayFor);
|
||||||
return result;
|
if (err) throw err;
|
||||||
|
else return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export interface GrpcRequest extends BaseModel {
|
|||||||
service: string | null;
|
service: string | null;
|
||||||
method: string | null;
|
method: string | null;
|
||||||
message: string;
|
message: string;
|
||||||
|
protoFiles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrpcMessage extends BaseModel {
|
export interface GrpcMessage extends BaseModel {
|
||||||
|
|||||||
@@ -28,6 +28,15 @@
|
|||||||
@apply select-none cursor-default;
|
@apply select-none cursor-default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a * {
|
||||||
|
@apply cursor-pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
@apply text-left;
|
||||||
|
}
|
||||||
|
|
||||||
.hide-scrollbars {
|
.hide-scrollbars {
|
||||||
&::-webkit-scrollbar-corner,
|
&::-webkit-scrollbar-corner,
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
|
|||||||
Reference in New Issue
Block a user