From 2ea7e6ba275500265b2b71be554e2c6d1e4fef16 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 6 Feb 2024 19:20:32 -0800 Subject: [PATCH] gRPC schema from files! --- src-tauri/Cargo.lock | 92 ++++++++-- src-tauri/grpc/Cargo.toml | 4 +- src-tauri/grpc/src/manager.rs | 127 ++++++++------ src-tauri/grpc/src/proto.rs | 77 ++++++--- src-tauri/src/main.rs | 72 +++++++- .../components/GrpcConnectionSetupPane.tsx | 10 +- src-web/components/GrpcEditor.tsx | 59 ++----- src-web/components/GrpcProtoSelection.tsx | 163 +++++++++++++----- .../components/WorkspaceActionsDropdown.tsx | 30 ++-- src-web/components/core/Button.tsx | 20 +-- src-web/components/core/Dropdown.tsx | 2 +- src-web/hooks/Confirm.tsx | 8 +- src-web/hooks/useGrpc.ts | 8 +- 13 files changed, 442 insertions(+), 230 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d9d840d8..6f33f539 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1161,12 +1161,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1696,12 +1696,14 @@ dependencies = [ "prost", "prost-reflect", "prost-types", + "protoc-bin-vendored", "serde", "serde_json", "tokio", "tokio-stream", "tonic", "tonic-reflection", + "uuid", ] [[package]] @@ -2379,9 +2381,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -2422,9 +2424,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "litemap" @@ -3430,6 +3432,56 @@ dependencies = [ "prost", ] +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + [[package]] name = "psl-types" version = "2.0.11" @@ -3777,15 +3829,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4926,15 +4978,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.4.1", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5457,9 +5508,9 @@ checksum = "64a8922555b9500e3d865caed19330172cd67cbf82203f1a3311d8c305cc9f33" [[package]] name = "uuid" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom 0.2.11", ] @@ -5890,6 +5941,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/src-tauri/grpc/Cargo.toml b/src-tauri/grpc/Cargo.toml index 3d1dda67..4f32754c 100644 --- a/src-tauri/grpc/Cargo.toml +++ b/src-tauri/grpc/Cargo.toml @@ -16,5 +16,7 @@ prost-reflect = { version = "0.12.0", features = ["serde", "derive"] } log = "0.4.20" once_cell = { version = "1.19.0", features = [] } anyhow = "1.0.79" -hyper = { version = "0.14"} +hyper = { version = "0.14" } hyper-rustls = { version = "0.24.0", features = ["http2"] } +protoc-bin-vendored = "3.0.0" +uuid = { version = "1.7.0", features = ["v4"] } diff --git a/src-tauri/grpc/src/manager.rs b/src-tauri/grpc/src/manager.rs index 98031cca..49812f3a 100644 --- a/src-tauri/grpc/src/manager.rs +++ b/src-tauri/grpc/src/manager.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::PathBuf; use hyper::client::HttpConnector; use hyper::Client; @@ -13,11 +14,9 @@ use tonic::transport::Uri; use tonic::{IntoRequest, IntoStreamingRequest, Streaming}; use crate::codec::DynamicCodec; -use crate::proto::{fill_pool, method_desc_to_path}; +use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path}; use crate::{json_schema, MethodDefinition, ServiceDefinition}; -type Result = std::result::Result; - #[derive(Clone)] pub struct GrpcConnection { pool: DescriptorPool, @@ -26,7 +25,12 @@ pub struct GrpcConnection { } impl GrpcConnection { - pub async fn unary(&self, service: &str, method: &str, message: &str) -> Result { + pub async fn unary( + &self, + service: &str, + method: &str, + message: &str, + ) -> Result { let service = self.pool.get_service_by_name(service).unwrap(); let method = &service.methods().find(|m| m.name() == method).unwrap(); let input_message = method.input(); @@ -55,7 +59,7 @@ impl GrpcConnection { service: &str, method: &str, stream: ReceiverStream, - ) -> Result> { + ) -> Result, String> { let service = self.pool.get_service_by_name(service).unwrap(); let method = &service.methods().find(|m| m.name() == method).unwrap(); @@ -87,7 +91,7 @@ impl GrpcConnection { service: &str, method: &str, stream: ReceiverStream, - ) -> Result { + ) -> Result { let service = self.pool.get_service_by_name(service).unwrap(); let method = &service.methods().find(|m| m.name() == method).unwrap(); let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); @@ -121,7 +125,7 @@ impl GrpcConnection { service: &str, method: &str, message: &str, - ) -> Result> { + ) -> Result, String> { let service = self.pool.get_service_by_name(service).unwrap(); let method = &service.methods().find(|m| m.name() == method).unwrap(); let input_message = method.input(); @@ -159,56 +163,56 @@ impl Default for GrpcHandle { } impl GrpcHandle { - pub async fn clean_reflect(&mut self, uri: &Uri) -> Result> { - self.pools.remove(&uri.to_string()); - self.reflect(uri).await + pub async fn services_from_files( + &self, + paths: Vec, + ) -> Result, String> { + let pool = fill_pool_from_files(paths).await?; + Ok(self.services_from_pool(&pool)) + } + pub async fn services_from_reflection( + &mut self, + uri: &Uri, + ) -> Result, String> { + let pool = fill_pool(uri).await?; + Ok(self.services_from_pool(&pool)) } - pub async fn reflect(&mut self, uri: &Uri) -> Result> { - 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::>(); - - Ok(result) + fn services_from_pool(&self, pool: &DescriptorPool) -> Vec { + 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::>() } pub async fn server_streaming( &mut self, id: &str, uri: Uri, + proto_files: Vec, service: &str, method: &str, message: &str, - ) -> Result> { - self.connect(id, uri) + ) -> Result, String> { + self.connect(id, uri, proto_files) .await? .server_streaming(service, method, message) .await @@ -218,11 +222,12 @@ impl GrpcHandle { &mut self, id: &str, uri: Uri, + proto_files: Vec, service: &str, method: &str, stream: ReceiverStream, - ) -> Result { - self.connect(id, uri) + ) -> Result { + self.connect(id, uri, proto_files) .await? .client_streaming(service, method, stream) .await @@ -232,18 +237,36 @@ impl GrpcHandle { &mut self, id: &str, uri: Uri, + proto_files: Vec, service: &str, method: &str, stream: ReceiverStream, - ) -> Result> { - self.connect(id, uri) + ) -> Result, String> { + self.connect(id, uri, proto_files) .await? .streaming(service, method, stream) .await } - pub async fn connect(&mut self, id: &str, uri: Uri) -> Result { - let (pool, conn) = fill_pool(&uri).await?; + pub async fn connect( + &mut self, + id: &str, + uri: Uri, + proto_files: Vec, + ) -> Result { + let pool = match self.pools.get(id) { + Some(p) => p.clone(), + None => match proto_files.len() { + 0 => fill_pool(&uri).await?, + _ => { + let pool = fill_pool_from_files(proto_files).await?; + self.pools.insert(id.to_string(), pool.clone()); + pool + } + }, + }; + + let conn = get_transport(); let connection = GrpcConnection { pool, conn, uri }; self.connections.insert(id.to_string(), connection.clone()); Ok(connection) diff --git a/src-tauri/grpc/src/proto.rs b/src-tauri/grpc/src/proto.rs index c49a4e18..3b26d598 100644 --- a/src-tauri/grpc/src/proto.rs +++ b/src-tauri/grpc/src/proto.rs @@ -1,15 +1,17 @@ +use std::env::temp_dir; use std::ops::Deref; use std::path::PathBuf; +use std::process::Command; use std::str::FromStr; use anyhow::anyhow; use hyper::client::HttpConnector; use hyper::Client; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; -use log::warn; +use log::{debug, warn}; use prost::Message; use prost_reflect::{DescriptorPool, MethodDescriptor}; -use prost_types::FileDescriptorProto; +use prost_types::{FileDescriptorProto, FileDescriptorSet}; use tokio::fs; use tokio_stream::StreamExt; use tonic::body::BoxBody; @@ -23,36 +25,71 @@ use tonic_reflection::pb::ServerReflectionRequest; pub async fn fill_pool_from_files(paths: Vec) -> Result { let mut pool = DescriptorPool::new(); + let random_file_name = format!("{}.desc", uuid::Uuid::new_v4()); + let desc_path = temp_dir().join(random_file_name); + let bin = protoc_bin_vendored::protoc_bin_path().unwrap(); + + let mut cmd = Command::new(bin.clone()); + cmd.arg("--include_imports") + .arg("--include_source_info") + .arg("-o") + .arg(&desc_path); + 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())?; + if p.as_path().exists() { + cmd.arg(p.as_path().to_string_lossy().as_ref()); + } else { + continue; + } + + let parent = p.as_path().parent(); + if let Some(parent_path) = parent { + cmd.arg("-I").arg(parent_path); + } else { + debug!("ignoring {:?} since it does not exist.", parent) + } } + + debug!("Running: {:?}", cmd); + + let output = cmd.output().map_err(|e| e.to_string())?; + if !output.status.success() { + return Err(format!( + "protoc failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + let bytes = fs::read(desc_path.as_path()) + .await + .map_err(|e| e.to_string())?; + let fdp = FileDescriptorSet::decode(bytes.deref()).map_err(|e| e.to_string())?; + pool.add_file_descriptor_set(fdp) + .map_err(|e| e.to_string())?; + + fs::remove_file(desc_path.as_path()) + .await + .map_err(|e| e.to_string())?; + Ok(pool) } -pub async fn fill_pool( - uri: &Uri, -) -> Result< - ( - DescriptorPool, - Client, BoxBody>, - ), - String, -> { - let mut pool = DescriptorPool::new(); + +pub fn get_transport() -> Client, BoxBody> { let connector = HttpsConnectorBuilder::new().with_native_roots(); let connector = connector.https_or_http().enable_http2().wrap_connector({ let mut http_connector = HttpConnector::new(); http_connector.enforce_http(false); http_connector }); - let transport = Client::builder() + Client::builder() .pool_max_idle_per_host(0) .http2_only(true) - .build(connector); + .build(connector) +} - let mut client = ServerReflectionClient::with_origin(transport.clone(), uri.clone()); +pub async fn fill_pool(uri: &Uri) -> Result { + let mut pool = DescriptorPool::new(); + let mut client = ServerReflectionClient::with_origin(get_transport(), uri.clone()); for service in list_services(&mut client).await? { if service == "grpc.reflection.v1alpha.ServerReflection" { @@ -61,7 +98,7 @@ pub async fn fill_pool( file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await; } - Ok((pool, transport)) + Ok(pool) } async fn list_services( diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4e54ab3a..e0a169e6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,6 +11,7 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; use std::fs::{create_dir_all, read_to_string, File}; +use std::path::PathBuf; use std::process::exit; use std::str::FromStr; @@ -100,8 +101,28 @@ async fn cmd_grpc_reflect( let req = get_grpc_request(&app_handle, request_id) .await .map_err(|e| e.to_string())?; - let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; - grpc_handle.lock().await.clean_reflect(&uri).await + if req.proto_files.0.len() > 0 { + println!("REFLECT FROM FILES"); + grpc_handle + .lock() + .await + .services_from_files( + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await + } else { + println!("REFLECT FROM URI"); + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; + grpc_handle + .lock() + .await + .services_from_reflection(&uri) + .await + } } #[tauri::command] @@ -150,7 +171,15 @@ async fn cmd_grpc_call_unary( let msg = match grpc_handle .lock() .await - .connect(&conn.clone().id, uri) + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) .await? .unary( &req.service.unwrap_or_default(), @@ -321,7 +350,18 @@ async fn cmd_grpc_client_streaming( let msg = grpc_handle .lock() .await - .client_streaming(&conn.id, uri, &service, &method, in_msg_stream) + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await + .unwrap() + .client_streaming(&service, &method, in_msg_stream) .await .unwrap(); let message = serde_json::to_string(&msg).unwrap(); @@ -461,7 +501,17 @@ async fn cmd_grpc_streaming( let mut stream = grpc_handle .lock() .await - .streaming(&conn.id, uri, &service, &method, in_msg_stream) + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await? + .streaming(&service, &method, in_msg_stream) .await .unwrap(); @@ -664,7 +714,17 @@ async fn cmd_grpc_server_streaming( let mut stream = grpc_handle .lock() .await - .server_streaming(&conn.id, uri, &service, &method, &req.message) + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await? + .server_streaming(&service, &method, &req.message) .await .unwrap(); diff --git a/src-web/components/GrpcConnectionSetupPane.tsx b/src-web/components/GrpcConnectionSetupPane.tsx index 7b193a6d..886aa0b6 100644 --- a/src-web/components/GrpcConnectionSetupPane.tsx +++ b/src-web/components/GrpcConnectionSetupPane.tsx @@ -75,11 +75,6 @@ export function GrpcConnectionSetupPane({ [updateRequest], ); - const handleSelectProtoFiles = useCallback( - (paths: string[]) => updateRequest.mutateAsync({ protoFiles: paths }), - [updateRequest], - ); - const select = useMemo(() => { const options = services?.flatMap((s) => @@ -159,10 +154,12 @@ export function GrpcConnectionSetupPane({ shortLabel: o.label, }))} extraItems={[ + { type: 'separator' }, { - label: 'Custom', + label: 'Refresh', type: 'default', key: 'custom', + leftSlot: , }, ]} > @@ -234,7 +231,6 @@ export function GrpcConnectionSetupPane({ reflectionError={reflectionError} reflectionLoading={reflectionLoading} onReflect={onReflectRefetch} - onSelectProtoFiles={handleSelectProtoFiles} request={activeRequest} /> diff --git a/src-web/components/GrpcEditor.tsx b/src-web/components/GrpcEditor.tsx index 189ad6a7..c6f45788 100644 --- a/src-web/components/GrpcEditor.tsx +++ b/src-web/components/GrpcEditor.tsx @@ -1,4 +1,4 @@ -import { open } from '@tauri-apps/api/dialog'; +import classNames from 'classnames'; import type { EditorView } from 'codemirror'; import { updateSchema } from 'codemirror-json-schema'; import { useEffect, useRef } from 'react'; @@ -7,14 +7,12 @@ import type { ReflectResponseService } from '../hooks/useGrpc'; 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 type { EditorProps } from './core/Editor'; import { Editor } from './core/Editor'; import { FormattedError } from './core/FormattedError'; import { InlineCode } from './core/InlineCode'; -import { Link } from './core/Link'; -import { HStack, VStack } from './core/Stacks'; +import { VStack } from './core/Stacks'; import { useDialog } from './DialogContext'; import { GrpcProtoSelection } from './GrpcProtoSelection'; @@ -23,16 +21,12 @@ type Props = Pick & { reflectionError?: string; reflectionLoading?: boolean; request: GrpcRequest; - onReflect: () => void; - onSelectProtoFiles: (paths: string[]) => void; }; export function GrpcEditor({ services, reflectionError, reflectionLoading, - onReflect, - onSelectProtoFiles, request, ...extraEditorProps }: Props) { @@ -97,6 +91,7 @@ export function GrpcEditor({ }, [alert, services, request.method, request.service]); const reflectionUnavailable = reflectionError?.match(/unimplemented/i); + const reflectionSuccess = !reflectionError && services != null && request.protoFiles.length === 0; reflectionError = reflectionUnavailable ? undefined : reflectionError; return ( @@ -110,7 +105,7 @@ export function GrpcEditor({ placeholder="..." ref={editorViewRef} actions={[ -
+
-
- - - )} - - - ), + render: ({ hide }) => { + return ( + + + + ); + }, }); }} > @@ -170,10 +139,10 @@ export function GrpcEditor({ ? 'Select Proto Files' : reflectionError ? 'Server Error' - : services != null - ? 'Proto Schema' : request.protoFiles.length > 0 - ? count('Proto File', request.protoFiles.length) + ? count('File', request.protoFiles.length) + : services != null && request.protoFiles.length === 0 + ? 'Schema Detected' : 'Select Schema'}
, diff --git a/src-web/components/GrpcProtoSelection.tsx b/src-web/components/GrpcProtoSelection.tsx index 1cb80c6d..bf8703f2 100644 --- a/src-web/components/GrpcProtoSelection.tsx +++ b/src-web/components/GrpcProtoSelection.tsx @@ -1,62 +1,42 @@ import { open } from '@tauri-apps/api/dialog'; +import { useGrpc } from '../hooks/useGrpc'; import { useGrpcRequest } from '../hooks/useGrpcRequest'; import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; +import { count } from '../lib/pluralize'; +import { Banner } from './core/Banner'; import { Button } from './core/Button'; +import { FormattedError } from './core/FormattedError'; import { IconButton } from './core/IconButton'; -import { HStack } from './core/Stacks'; +import { InlineCode } from './core/InlineCode'; +import { Link } from './core/Link'; +import { HStack, VStack } from './core/Stacks'; -export function GrpcProtoSelection({ requestId }: { requestId: string }) { +interface Props { + requestId: string; + onDone: () => void; +} + +export function GrpcProtoSelection({ requestId }: Props) { const request = useGrpcRequest(requestId); + const grpc = useGrpc(request, null); const updateRequest = useUpdateGrpcRequest(request?.id ?? null); + const services = grpc.reflect.data; + const serverReflection = request?.protoFiles.length === 0 && services != null; + let reflectError = grpc.reflect.error ?? null; + const reflectionUnimplemented = `${reflectError}`.match(/unimplemented/i); + + if (reflectionUnimplemented) { + reflectError = null; + } if (request == null) { return null; } return ( -
- {request.protoFiles.length > 0 && ( - - - - - - - - - {request.protoFiles.map((f, i) => ( - - - - - ))} - -
- *.proto Files -
{f.split('/').pop()} - { - updateRequest.mutate({ - protoFiles: request.protoFiles.filter((p) => p !== f), - }); - }} - /> -
- )} - - + + {/* Buttons on top so they get focus first */} + + -
+ + {!serverReflection && services != null && services.length > 0 && ( + +

+ Found services + {services?.slice(0, 5).map((s, i) => { + return ( + + {s.name} + {i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '} + + ); + })} + {services?.length > 5 && count('other', services?.length - 5)} +

+
+ )} + {serverReflection && services != null && services.length > 0 && ( + +

+ Server reflection found services + {services?.map((s, i) => { + return ( + + {s.name} + {i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '} + + ); + })} + . You can override this schema by manually selecting *.proto{' '} + files. +

+
+ )} + + {request.protoFiles.length > 0 && ( + + + + + + + + + {request.protoFiles.map((f, i) => ( + + + + + ))} + +
+ *.proto Files +
{f.split('/').pop()} + { + await updateRequest.mutateAsync({ + protoFiles: request.protoFiles.filter((p) => p !== f), + }); + grpc.reflect.remove(); + }} + /> +
+ )} + {reflectError && {reflectError}} + {reflectionUnimplemented && request.protoFiles.length === 0 && ( + + {request.url} doesn't implement{' '} + + Server Reflection + {' '} + . Please manually add the .proto files to get started. + + )} +
+ ); } diff --git a/src-web/components/WorkspaceActionsDropdown.tsx b/src-web/components/WorkspaceActionsDropdown.tsx index 3c33d433..707c15f5 100644 --- a/src-web/components/WorkspaceActionsDropdown.tsx +++ b/src-web/components/WorkspaceActionsDropdown.tsx @@ -51,7 +51,23 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ ), render: ({ hide }) => { return ( - + + - ); }, diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index 918d5aeb..cc55226d 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -64,22 +64,14 @@ export const Button = forwardRef(function Button variant === 'solid' && color === 'custom' && 'ring-blue-500/50', variant === 'solid' && color === 'default' && - 'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50', + 'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-500/50', variant === 'solid' && color === 'gray' && - 'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50', - variant === 'solid' && - color === 'primary' && - 'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50', - variant === 'solid' && - color === 'secondary' && - 'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50', - variant === 'solid' && - color === 'warning' && - 'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50', - variant === 'solid' && - color === 'danger' && - 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50', + 'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-gray-400', + variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700', + variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700', + variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700', + variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700', // Borders variant === 'border' && 'border', variant === 'border' && diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index bac04ebd..4a802793 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -399,7 +399,7 @@ const Menu = forwardRef, MenuPro {items.map((item, i) => { if (item.type === 'separator') { return ( - + {item.label} ); diff --git a/src-web/hooks/Confirm.tsx b/src-web/hooks/Confirm.tsx index 127ff9c0..255e1316 100644 --- a/src-web/hooks/Confirm.tsx +++ b/src-web/hooks/Confirm.tsx @@ -30,13 +30,13 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps) }; return ( - + + - ); } diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index 9dc4946e..796e0a76 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -52,14 +52,14 @@ export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) { }); const debouncedUrl = useDebouncedValue(req?.url ?? 'n/a', 1000); - const reflect = useQuery({ - enabled: req != null && req.protoFiles.length === 0, + const reflect = useQuery({ + enabled: req != null, queryKey: ['grpc_reflect', debouncedUrl], + refetchOnWindowFocus: false, queryFn: async () => { - console.log('REFLECTING...'); return (await minPromiseMillis( invoke('cmd_grpc_reflect', { requestId }), - 1000, + 300, )) as ReflectResponseService[]; }, });