Refactor gRPC reflection!

This commit is contained in:
Gregory Schier
2024-06-20 12:49:58 -07:00
parent 8f06a834c8
commit c18d30b89f
15 changed files with 169 additions and 123 deletions

34
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "yaak-app", "name": "yaak",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "yaak-app", "name": "yaak",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@codemirror/commands": "^6.2.1", "@codemirror/commands": "^6.2.1",
@@ -497,9 +497,9 @@
"integrity": "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==" "integrity": "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg=="
}, },
"node_modules/@codemirror/autocomplete": { "node_modules/@codemirror/autocomplete": {
"version": "6.16.0", "version": "6.16.3",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz",
"integrity": "sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==", "integrity": "sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA==",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@@ -561,9 +561,9 @@
} }
}, },
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.10.1", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@@ -574,9 +574,9 @@
} }
}, },
"node_modules/@codemirror/lint": { "node_modules/@codemirror/lint": {
"version": "6.7.0", "version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.7.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz",
"integrity": "sha512-LTLOL2nT41ADNSCCCCw8Q/UmdAFzB23OUYSjsHTdsVaH0XEo+orhuqbDNWzrzodm14w6FOxqxpmy4LF8Lixqjw==", "integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.0.0",
@@ -599,9 +599,9 @@
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.26.3", "version": "6.28.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.1.tgz",
"integrity": "sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==", "integrity": "sha512-BUWr+zCJpMkA/u69HlJmR+YkV4yPpM81HeMkOMZuwFa8iM5uJdEPKAs1icIRZKkKmy0Ub1x9/G3PQLTXdpBxrQ==",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0", "style-mod": "^4.1.0",
@@ -7369,9 +7369,9 @@
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
}, },
"node_modules/json-schema-library": { "node_modules/json-schema-library": {
"version": "9.3.4", "version": "9.3.5",
"resolved": "https://registry.npmjs.org/json-schema-library/-/json-schema-library-9.3.4.tgz", "resolved": "https://registry.npmjs.org/json-schema-library/-/json-schema-library-9.3.5.tgz",
"integrity": "sha512-220lm9RVt9BUeF2QhBT711aX4IogUHhPT8Tjhkksc4CUw8WmChFMuf0mJdpDAHDfJDkI064jcZIH8P70HdPAOA==", "integrity": "sha512-5eBDx7cbfs+RjylsVO+N36b0GOPtv78rfqgf2uON+uaHUIC62h63Y8pkV2ovKbaL4ZpQcHp21968x5nx/dFwqQ==",
"dependencies": { "dependencies": {
"@sagold/json-pointer": "^5.1.2", "@sagold/json-pointer": "^5.1.2",
"@sagold/json-query": "^6.1.3", "@sagold/json-query": "^6.1.3",

View File

@@ -1,14 +1,15 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const tauriConfigPath = path.join(__dirname, '../src-tauri/tauri.conf.json');
const tauriConfig = fs.readFileSync(tauriConfigPath, 'utf8');
const version = process.env.YAAK_VERSION?.replace('v', ''); const version = process.env.YAAK_VERSION?.replace('v', '');
if (!version) { if (!version) {
throw new Error('YAAK_VERSION environment variable not set') throw new Error('YAAK_VERSION environment variable not set')
} }
const tauriConfigPath = path.join(__dirname, '../src-tauri/tauri.conf.json');
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf8'));
tauriConfig.version = version;
console.log('Writing version ' + version + ' to ' + tauriConfigPath) console.log('Writing version ' + version + ' to ' + tauriConfigPath)
const newTauriConfig = tauriConfig.replaceAll('__YAAK_VERSION__', version); fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriConfig, null, 2));
fs.writeFileSync(tauriConfigPath, newTauriConfig);

7
src-tauri/Cargo.lock generated
View File

@@ -2290,6 +2290,7 @@ dependencies = [
"hyper 0.14.29", "hyper 0.14.29",
"hyper-rustls 0.24.2", "hyper-rustls 0.24.2",
"log", "log",
"md5",
"prost", "prost",
"prost-reflect", "prost-reflect",
"prost-types", "prost-types",
@@ -3250,6 +3251,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.2" version = "2.7.2"

View File

@@ -20,3 +20,4 @@ hyper-rustls = { version = "0.24.0", features = ["http2"] }
uuid = { version = "1.7.0", features = ["v4"] } uuid = { version = "1.7.0", features = ["v4"] }
tauri = { version = "2.0.0-beta" } tauri = { version = "2.0.0-beta" }
tauri-plugin-shell = "2.0.0-beta" tauri-plugin-shell = "2.0.0-beta"
md5 = "0.7.0"

View File

@@ -16,7 +16,9 @@ use tonic::transport::Uri;
use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming}; use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming};
use crate::codec::DynamicCodec; use crate::codec::DynamicCodec;
use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path}; use crate::proto::{
fill_pool_from_files, fill_pool_from_reflection, get_transport, method_desc_to_path,
};
use crate::{json_schema, MethodDefinition, ServiceDefinition}; use crate::{json_schema, MethodDefinition, ServiceDefinition};
#[derive(Clone)] #[derive(Clone)]
@@ -182,39 +184,36 @@ impl GrpcHandle {
} }
impl GrpcHandle { impl GrpcHandle {
pub async fn services_from_files( pub async fn reflect(
&mut self,
id: &str,
paths: Vec<PathBuf>,
) -> Result<Vec<ServiceDefinition>, String> {
let pool_key = format!(
"{}-{}",
id,
paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join(":")
);
let pool = fill_pool_from_files(&self.app_handle, paths).await?;
self.pools.insert(pool_key, pool.clone());
Ok(self.services_from_pool(&pool))
}
pub async fn services_from_reflection(
&mut self, &mut self,
id: &str, id: &str,
uri: &str, uri: &str,
) -> Result<Vec<ServiceDefinition>, String> { proto_files: &Vec<PathBuf>,
// Short-circuit if no URL is set ) -> Result<(), String> {
if uri.is_empty() { let pool = if proto_files.is_empty() {
return Ok(Vec::new()); let full_uri = uri_from_str(uri)?;
} fill_pool_from_reflection(&full_uri).await
} else {
fill_pool_from_files(&self.app_handle, proto_files).await
}?;
let uri = uri_from_str(uri)?; self.pools
let pool = fill_pool(&uri).await?; .insert(make_pool_key(id, uri, proto_files), pool.clone());
let pool_key = format!("{}-{}", id, uri); Ok(())
self.pools.insert(pool_key, pool.clone()); }
pub async fn services(
&mut self,
id: &str,
uri: &str,
proto_files: &Vec<PathBuf>,
) -> Result<Vec<ServiceDefinition>, String> {
// Ensure reflection is up-to-date
self.reflect(id, uri, proto_files).await?;
let pool = self
.get_pool(id, uri, proto_files)
.ok_or("Failed to get pool".to_string())?;
Ok(self.services_from_pool(&pool)) Ok(self.services_from_pool(&pool))
} }
@@ -247,25 +246,26 @@ impl GrpcHandle {
&mut self, &mut self,
id: &str, id: &str,
uri: &str, uri: &str,
proto_files: Vec<PathBuf>, proto_files: &Vec<PathBuf>,
) -> Result<GrpcConnection, String> { ) -> Result<GrpcConnection, String> {
let uri = uri_from_str(uri)?; self.reflect(id, uri, proto_files).await?;
let pool = match self.pools.get(id) { let pool = self
Some(p) => p.clone(), .get_pool(id, uri, proto_files)
None => match proto_files.len() { .ok_or("Failed to get pool")?;
0 => fill_pool(&uri).await?,
_ => {
let pool = fill_pool_from_files(&self.app_handle, proto_files).await?;
self.pools.insert(id.to_string(), pool.clone());
pool
}
},
};
let uri = uri_from_str(uri)?;
let conn = get_transport(); let conn = get_transport();
let connection = GrpcConnection { pool, conn, uri }; let connection = GrpcConnection {
pool: pool.clone(),
conn,
uri,
};
Ok(connection) Ok(connection)
} }
fn get_pool(&self, id: &str, uri: &str, proto_files: &Vec<PathBuf>) -> Option<&DescriptorPool> {
self.pools.get(make_pool_key(id, uri, proto_files).as_str())
}
} }
fn decorate_req<T>(metadata: HashMap<String, String>, req: &mut Request<T>) -> Result<(), String> { fn decorate_req<T>(metadata: HashMap<String, String>, req: &mut Request<T>) -> Result<(), String> {
@@ -287,3 +287,18 @@ fn uri_from_str(uri_str: &str) -> Result<Uri, String> {
} }
} }
} }
fn make_pool_key(id: &str, uri: &str, proto_files: &Vec<PathBuf>) -> String {
let pool_key = format!(
"{}::{}::{}",
id,
uri,
proto_files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join(":")
);
format!("{:x}", md5::compute(pool_key))
}

View File

@@ -28,7 +28,7 @@ use tonic_reflection::pb::ServerReflectionRequest;
pub async fn fill_pool_from_files( pub async fn fill_pool_from_files(
app_handle: &AppHandle, app_handle: &AppHandle,
paths: Vec<PathBuf>, paths: &Vec<PathBuf>,
) -> Result<DescriptorPool, String> { ) -> Result<DescriptorPool, String> {
let mut pool = DescriptorPool::new(); let mut pool = DescriptorPool::new();
let random_file_name = format!("{}.desc", uuid::Uuid::new_v4()); let random_file_name = format!("{}.desc", uuid::Uuid::new_v4());
@@ -121,7 +121,7 @@ pub async fn fill_pool_from_files(
Ok(pool) Ok(pool)
} }
pub async fn fill_pool(uri: &Uri) -> Result<DescriptorPool, String> { pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, String> {
let mut pool = DescriptorPool::new(); let mut pool = DescriptorPool::new();
let mut client = ServerReflectionClient::with_origin(get_transport(), uri.clone()); let mut client = ServerReflectionClient::with_origin(get_transport(), uri.clone());

View File

@@ -6,7 +6,7 @@ extern crate objc;
use std::collections::HashMap; use std::collections::HashMap;
use std::env::current_dir; use std::env::current_dir;
use std::fs; use std::fs;
use std::fs::{create_dir_all, File, read_to_string}; use std::fs::{create_dir_all, read_to_string, File};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
@@ -17,45 +17,45 @@ use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use rand::random; use rand::random;
use serde_json::{json, Value}; use serde_json::{json, Value};
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
use sqlx::types::Json; use sqlx::types::Json;
use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow}; use sqlx::{Pool, Sqlite, SqlitePool};
use tauri::{Manager, WindowEvent};
use tauri::path::BaseDirectory; use tauri::path::BaseDirectory;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Manager, WindowEvent};
use tauri_plugin_log::{fern, Target, TargetKind}; use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition};
use ::grpc::manager::{DynamicMessage, GrpcHandle}; use ::grpc::manager::{DynamicMessage, GrpcHandle};
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::analytics::{AnalyticsAction, AnalyticsResource};
use crate::grpc::metadata_to_map; use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request; use crate::http_request::send_http_request;
use crate::models::{ use crate::models::{
cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
duplicate_http_request, Environment, EnvironmentVariable, Folder, generate_model_id, generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, get_or_create_settings, get_workspace, get_workspace_export_resources, list_cookie_jars,
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, KeyValue, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests,
list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_http_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id,
list_grpc_requests, list_http_requests, list_responses, list_workspaces, ModelType, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar,
upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
WorkspaceExportResources, WorkspaceExportResources,
}; };
use crate::notifications::YaakNotifier; use crate::notifications::YaakNotifier;
use crate::plugin::{ use crate::plugin::{
find_plugins, get_plugin, ImportResult, PluginCapability, find_plugins, get_plugin, run_plugin_export_curl, run_plugin_filter, run_plugin_import,
run_plugin_export_curl, run_plugin_filter, run_plugin_import, ImportResult, PluginCapability,
}; };
use crate::render::{render_request, variables_from_environment}; use crate::render::{render_request, variables_from_environment};
use crate::updates::{UpdateMode, YaakUpdater}; use crate::updates::{UpdateMode, YaakUpdater};
@@ -135,26 +135,18 @@ async fn cmd_grpc_reflect(
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let uri = safe_uri(req.url.as_str()); grpc_handle
if proto_files.len() > 0 { .lock()
grpc_handle .await
.lock() .services(
.await &req.id,
.services_from_files( &req.url,
&req.id, &proto_files
proto_files .iter()
.iter() .map(|p| PathBuf::from_str(p).unwrap())
.map(|p| PathBuf::from_str(p).unwrap()) .collect(),
.collect(), )
) .await
.await
} else {
grpc_handle
.lock()
.await
.services_from_reflection(&req.id, uri.as_str())
.await
}
} }
#[tauri::command] #[tauri::command]
@@ -231,6 +223,7 @@ async fn cmd_grpc_go(
workspace_id: req.workspace_id, workspace_id: req.workspace_id,
request_id: req.id, request_id: req.id,
status: -1, status: -1,
elapsed: 0,
url: req.url.clone(), url: req.url.clone(),
..Default::default() ..Default::default()
}, },
@@ -238,6 +231,7 @@ async fn cmd_grpc_go(
.await .await
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
}; };
let conn_id = conn.id.clone(); let conn_id = conn.id.clone();
let base_msg = GrpcEvent { let base_msg = GrpcEvent {
@@ -270,12 +264,29 @@ async fn cmd_grpc_go(
.connect( .connect(
&req.clone().id, &req.clone().id,
uri.as_str(), uri.as_str(),
proto_files &proto_files
.iter() .iter()
.map(|p| PathBuf::from_str(p).unwrap()) .map(|p| PathBuf::from_str(p).unwrap())
.collect(), .collect(),
) )
.await?; .await;
let connection = match connection {
Ok(c) => c,
Err(err) => {
upsert_grpc_connection(
&w,
&GrpcConnection {
elapsed: start.elapsed().as_millis() as i64,
error: Some(err.clone()),
..conn.clone()
},
)
.await
.map_err(|e| e.to_string())?;
return Ok(conn_id);
}
};
let method_desc = connection let method_desc = connection
.method(&service, &method) .method(&service, &method)

View File

@@ -1,6 +1,6 @@
{ {
"productName": "yaak", "productName": "yaak",
"version": "__YAAK_VERSION__", "version": "0.0.0",
"identifier": "app.yaak.desktop", "identifier": "app.yaak.desktop",
"build": { "build": {
"beforeBuildCommand": "npm run build", "beforeBuildCommand": "npm run build",

View File

@@ -86,6 +86,7 @@ export function GrpcConnectionLayout({ style }: Props) {
activeRequest={activeRequest} activeRequest={activeRequest}
protoFiles={protoFiles} protoFiles={protoFiles}
methodType={methodType} methodType={methodType}
isStreaming={grpc.isStreaming}
onGo={grpc.go.mutate} onGo={grpc.go.mutate}
onCommit={grpc.commit.mutate} onCommit={grpc.commit.mutate}
onCancel={grpc.cancel.mutate} onCancel={grpc.cancel.mutate}

View File

@@ -6,6 +6,8 @@ import { useGrpcEvents } from '../hooks/useGrpcEvents';
import { usePinnedGrpcConnection } from '../hooks/usePinnedGrpcConnection'; import { usePinnedGrpcConnection } from '../hooks/usePinnedGrpcConnection';
import { useStateWithDeps } from '../hooks/useStateWithDeps'; import { useStateWithDeps } from '../hooks/useStateWithDeps';
import type { GrpcEvent, GrpcRequest } from '../lib/models'; import type { GrpcEvent, GrpcRequest } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { JsonAttributeTree } from './core/JsonAttributeTree'; import { JsonAttributeTree } from './core/JsonAttributeTree';
@@ -64,7 +66,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
<HStack className="pl-3 mb-1 font-mono"> <HStack className="pl-3 mb-1 font-mono">
<HStack space={2}> <HStack space={2}>
<span>{events.length} messages</span> <span>{events.length} messages</span>
{activeConnection.elapsed === 0 && ( {isResponseLoading(activeConnection) && (
<Icon icon="refresh" size="sm" spin className="text-fg-subtler" /> <Icon icon="refresh" size="sm" spin className="text-fg-subtler" />
)} )}
</HStack> </HStack>
@@ -75,6 +77,11 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
/> />
</HStack> </HStack>
<div className="overflow-y-auto h-full"> <div className="overflow-y-auto h-full">
{activeConnection.error && (
<Banner color="danger" className="m-3">
{activeConnection.error}
</Banner>
)}
{...events.map((e) => ( {...events.map((e) => (
<EventRow <EventRow
key={e.id} key={e.id}

View File

@@ -4,7 +4,6 @@ import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from '../hooks/useGrpc';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import type { GrpcMetadataEntry, GrpcRequest } from '../lib/models'; import type { GrpcMetadataEntry, GrpcRequest } from '../lib/models';
@@ -37,6 +36,7 @@ interface Props {
| 'streaming' | 'streaming'
| 'no-schema' | 'no-schema'
| 'no-method'; | 'no-method';
isStreaming: boolean;
onCommit: () => void; onCommit: () => void;
onCancel: () => void; onCancel: () => void;
onSend: (v: { message: string }) => void; onSend: (v: { message: string }) => void;
@@ -54,15 +54,13 @@ export function GrpcConnectionSetupPane({
protoFiles, protoFiles,
reflectionError, reflectionError,
reflectionLoading, reflectionLoading,
isStreaming,
onGo, onGo,
onCommit, onCommit,
onCancel, onCancel,
onSend, onSend,
}: Props) { }: Props) {
const connections = useGrpcConnections(activeRequest.id ?? null);
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
const activeConnection = connections[0] ?? null;
const isStreaming = activeConnection?.elapsed === 0;
const [activeTab, setActiveTab] = useActiveTab(); const [activeTab, setActiveTab] = useActiveTab();
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
@@ -78,7 +76,9 @@ export function GrpcConnectionSetupPane({
); );
const handleChangeMessage = useCallback( const handleChangeMessage = useCallback(
(message: string) => updateRequest.mutateAsync({ message }), (message: string) => {
return updateRequest.mutateAsync({ message });
},
[updateRequest], [updateRequest],
); );

View File

@@ -6,8 +6,8 @@ import {
handleRefresh, handleRefresh,
jsonCompletion, jsonCompletion,
jsonSchemaLinter, jsonSchemaLinter,
stateExtensions,
updateSchema, updateSchema,
stateExtensions,
} from 'codemirror-json-schema'; } from 'codemirror-json-schema';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { useAlert } from '../hooks/useAlert'; import { useAlert } from '../hooks/useAlert';
@@ -41,6 +41,7 @@ export function GrpcEditor({
...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();

View File

@@ -838,7 +838,7 @@ const SidebarItem = forwardRef(function SidebarItem(
</div> </div>
{latestGrpcConnection ? ( {latestGrpcConnection ? (
<div className="ml-auto"> <div className="ml-auto">
{latestGrpcConnection.elapsed === 0 && ( {isResponseLoading(latestGrpcConnection) && (
<Icon spin size="sm" icon="update" className="text-fg-subtler" /> <Icon spin size="sm" icon="update" className="text-fg-subtler" />
)} )}
</div> </div>

View File

@@ -44,11 +44,12 @@ export function useGrpc(
onSettled: () => trackEvent('grpc_connection', 'commit'), onSettled: () => trackEvent('grpc_connection', 'commit'),
}); });
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 500); const debouncedUrl = useDebouncedValue<string>(req?.url ?? '', 1000);
const debouncedMessage = useDebouncedValue<string>(req?.message ?? '', 1000);
const reflect = useQuery<ReflectResponseService[], string>({ const reflect = useQuery<ReflectResponseService[], string>({
enabled: req != null, enabled: req != null,
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl, protoFiles], queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl, debouncedMessage, protoFiles],
refetchOnWindowFocus: false,
queryFn: async () => queryFn: async () =>
(await minPromiseMillis( (await minPromiseMillis(
invokeCmd('cmd_grpc_reflect', { requestId, protoFiles }), invokeCmd('cmd_grpc_reflect', { requestId, protoFiles }),
@@ -56,12 +57,13 @@ export function useGrpc(
)) as ReflectResponseService[], )) as ReflectResponseService[],
}); });
console.log('CONN', conn);
return { return {
go, go,
reflect, reflect,
cancel, cancel,
commit, commit,
isStreaming: conn?.elapsed === 0, isStreaming: conn != null && conn.elapsed === 0,
send, send,
}; };
} }

View File

@@ -207,7 +207,7 @@ export interface HttpResponse extends BaseModel {
readonly headers: HttpHeader[]; readonly headers: HttpHeader[];
} }
export function isResponseLoading(response: HttpResponse): boolean { export function isResponseLoading(response: HttpResponse | GrpcConnection): boolean {
return response.elapsed === 0; return response.elapsed === 0;
} }