mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-21 16:21:25 +02:00
Hacky server streaming done
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -30,6 +30,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"codemirror-json-schema": "^0.6.1",
|
"codemirror-json-schema": "^0.6.1",
|
||||||
"codemirror-json5": "^1.0.3",
|
"codemirror-json5": "^1.0.3",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
@@ -3805,6 +3806,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
|
||||||
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
|
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"codemirror-json-schema": "^0.6.1",
|
"codemirror-json-schema": "^0.6.1",
|
||||||
"codemirror-json5": "^1.0.3",
|
"codemirror-json5": "^1.0.3",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ pub struct JsonSchemaEntry {
|
|||||||
enum_: Option<Vec<String>>,
|
enum_: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Don't allow any other properties in the object
|
/// Don't allow any other properties in the object
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
additional_properties: bool,
|
||||||
additional_properties: Option<bool>,
|
|
||||||
|
|
||||||
/// Set all properties to required
|
/// Set all properties to required
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use prost_reflect::DynamicMessage;
|
use prost::Message;
|
||||||
|
use prost_reflect::{DynamicMessage, SerializeOptions};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Deserializer;
|
use serde_json::Deserializer;
|
||||||
use tonic::IntoRequest;
|
use tokio_stream::{Stream, StreamExt};
|
||||||
use tonic::transport::Uri;
|
use tonic::transport::Uri;
|
||||||
|
use tonic::{IntoRequest, Response, 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};
|
||||||
@@ -11,19 +13,32 @@ mod codec;
|
|||||||
mod json_schema;
|
mod json_schema;
|
||||||
mod proto;
|
mod proto;
|
||||||
|
|
||||||
|
pub fn serialize_options() -> SerializeOptions {
|
||||||
|
SerializeOptions::new().skip_default_fields(false)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct ServiceDefinition {
|
pub struct ServiceDefinition {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub methods: Vec<MethodDefinition>,
|
pub methods: Vec<MethodDefinition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct MethodDefinition {
|
pub struct MethodDefinition {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub schema: String,
|
pub schema: String,
|
||||||
|
pub client_streaming: bool,
|
||||||
|
pub server_streaming: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn call(uri: &Uri, service: &str, method: &str, message_json: &str) -> String {
|
pub async fn unary(
|
||||||
|
uri: &Uri,
|
||||||
|
service: &str,
|
||||||
|
method: &str,
|
||||||
|
message_json: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
let (pool, conn) = fill_pool(uri).await;
|
let (pool, conn) = fill_pool(uri).await;
|
||||||
|
|
||||||
let service = pool.get_service_by_name(service).unwrap();
|
let service = pool.get_service_by_name(service).unwrap();
|
||||||
@@ -31,7 +46,8 @@ pub async fn call(uri: &Uri, service: &str, method: &str, message_json: &str) ->
|
|||||||
let input_message = method.input();
|
let input_message = method.input();
|
||||||
|
|
||||||
let mut deserializer = Deserializer::from_str(message_json);
|
let mut deserializer = Deserializer::from_str(message_json);
|
||||||
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer).unwrap();
|
let req_message =
|
||||||
|
DynamicMessage::deserialize(input_message, &mut deserializer).map_err(|e| e.to_string())?;
|
||||||
deserializer.end().unwrap();
|
deserializer.end().unwrap();
|
||||||
|
|
||||||
let mut client = tonic::client::Grpc::new(conn);
|
let mut client = tonic::client::Grpc::new(conn);
|
||||||
@@ -47,10 +63,99 @@ pub async fn call(uri: &Uri, service: &str, method: &str, message_json: &str) ->
|
|||||||
client.ready().await.unwrap();
|
client.ready().await.unwrap();
|
||||||
|
|
||||||
let resp = client.unary(req, path, codec).await.unwrap();
|
let resp = client.unary(req, path, codec).await.unwrap();
|
||||||
|
let msg = resp.into_inner();
|
||||||
|
let response_json = serde_json::to_string_pretty(&msg).expect("json to string");
|
||||||
|
println!("\n---------- RECEIVING ---------------\n{}", response_json,);
|
||||||
|
|
||||||
|
Ok(response_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ClientStream {}
|
||||||
|
|
||||||
|
impl Stream for ClientStream {
|
||||||
|
type Item = DynamicMessage;
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Option<Self::Item>> {
|
||||||
|
println!("poll_next");
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client_streaming(
|
||||||
|
uri: &Uri,
|
||||||
|
service: &str,
|
||||||
|
method: &str,
|
||||||
|
message_json: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let (pool, conn) = fill_pool(uri).await;
|
||||||
|
|
||||||
|
let service = pool.get_service_by_name(service).unwrap();
|
||||||
|
let method = &service.methods().find(|m| m.name() == method).unwrap();
|
||||||
|
let input_message = method.input();
|
||||||
|
|
||||||
|
let mut deserializer = Deserializer::from_str(message_json);
|
||||||
|
let req_message =
|
||||||
|
DynamicMessage::deserialize(input_message, &mut deserializer).map_err(|e| e.to_string())?;
|
||||||
|
deserializer.end().unwrap();
|
||||||
|
|
||||||
|
let mut client = tonic::client::Grpc::new(conn);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\n---------- SENDING -----------------\n{}",
|
||||||
|
serde_json::to_string_pretty(&req_message).expect("json")
|
||||||
|
);
|
||||||
|
|
||||||
|
let req = tonic::Request::new(ClientStream {});
|
||||||
|
|
||||||
|
let path = method_desc_to_path(method);
|
||||||
|
let codec = DynamicCodec::new(method.clone());
|
||||||
|
client.ready().await.unwrap();
|
||||||
|
|
||||||
|
let resp = client.client_streaming(req, path, codec).await.unwrap();
|
||||||
let response_json = serde_json::to_string_pretty(&resp.into_inner()).expect("json to string");
|
let response_json = serde_json::to_string_pretty(&resp.into_inner()).expect("json to string");
|
||||||
println!("\n---------- RECEIVING ---------------\n{}", response_json,);
|
println!("\n---------- RECEIVING ---------------\n{}", response_json,);
|
||||||
|
|
||||||
response_json
|
Ok(response_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn server_streaming(
|
||||||
|
uri: &Uri,
|
||||||
|
service: &str,
|
||||||
|
method: &str,
|
||||||
|
message_json: &str,
|
||||||
|
) -> Result<Response<Streaming<DynamicMessage>>, String> {
|
||||||
|
let (pool, conn) = fill_pool(uri).await;
|
||||||
|
|
||||||
|
let service = pool.get_service_by_name(service).unwrap();
|
||||||
|
let method = &service.methods().find(|m| m.name() == method).unwrap();
|
||||||
|
let input_message = method.input();
|
||||||
|
|
||||||
|
let mut deserializer = Deserializer::from_str(message_json);
|
||||||
|
let req_message =
|
||||||
|
DynamicMessage::deserialize(input_message, &mut deserializer).map_err(|e| e.to_string())?;
|
||||||
|
deserializer.end().unwrap();
|
||||||
|
|
||||||
|
let mut client = tonic::client::Grpc::new(conn);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\n---------- SENDING -----------------\n{}",
|
||||||
|
serde_json::to_string_pretty(&req_message).expect("json")
|
||||||
|
);
|
||||||
|
|
||||||
|
let req = req_message.into_request();
|
||||||
|
let path = method_desc_to_path(method);
|
||||||
|
let codec = DynamicCodec::new(method.clone());
|
||||||
|
client.ready().await.unwrap();
|
||||||
|
|
||||||
|
let resp = client.server_streaming(req, path, codec).await.unwrap();
|
||||||
|
// let response_json = serde_json::to_string_pretty(&resp.into_inner()).expect("json to string");
|
||||||
|
// println!("\n---------- RECEIVING ---------------\n{}", response_json,);
|
||||||
|
|
||||||
|
// Ok(response_json)
|
||||||
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn callable(uri: &Uri) -> Vec<ServiceDefinition> {
|
pub async fn callable(uri: &Uri) -> Vec<ServiceDefinition> {
|
||||||
@@ -60,12 +165,14 @@ pub async fn callable(uri: &Uri) -> Vec<ServiceDefinition> {
|
|||||||
.map(|s| {
|
.map(|s| {
|
||||||
let mut def = ServiceDefinition {
|
let mut def = ServiceDefinition {
|
||||||
name: s.full_name().to_string(),
|
name: s.full_name().to_string(),
|
||||||
..Default::default()
|
methods: vec![],
|
||||||
};
|
};
|
||||||
for method in s.methods() {
|
for method in s.methods() {
|
||||||
let input_message = method.input();
|
let input_message = method.input();
|
||||||
def.methods.push(MethodDefinition {
|
def.methods.push(MethodDefinition {
|
||||||
name: method.name().to_string(),
|
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(
|
schema: serde_json::to_string_pretty(&json_schema::message_to_json_schema(
|
||||||
&pool,
|
&pool,
|
||||||
input_message,
|
input_message,
|
||||||
|
|||||||
@@ -8,32 +8,33 @@ extern crate core;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate objc;
|
extern crate objc;
|
||||||
|
|
||||||
use ::http::Uri;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env::current_dir;
|
use std::env::current_dir;
|
||||||
use std::fs::{create_dir_all, read_to_string, File};
|
use std::fs::{create_dir_all, File, read_to_string};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use ::http::Uri;
|
||||||
use fern::colors::ColoredLevelConfig;
|
use fern::colors::ColoredLevelConfig;
|
||||||
use grpc::ServiceDefinition;
|
use futures::StreamExt;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
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::types::Json;
|
use sqlx::types::Json;
|
||||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use tauri::TitleBarStyle;
|
|
||||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||||
use tauri::{Manager, WindowEvent};
|
use tauri::{Manager, WindowEvent};
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use tauri::TitleBarStyle;
|
||||||
use tauri_plugin_log::{fern, LogTarget};
|
use tauri_plugin_log::{fern, LogTarget};
|
||||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use window_shadows::set_shadow;
|
use window_shadows::set_shadow;
|
||||||
|
|
||||||
|
use grpc::ServiceDefinition;
|
||||||
use window_ext::TrafficLightWindowExt;
|
use window_ext::TrafficLightWindowExt;
|
||||||
|
|
||||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||||
@@ -106,7 +107,59 @@ async fn grpc_call_unary(
|
|||||||
} else {
|
} else {
|
||||||
Uri::from_str(&format!("http://{}", endpoint)).map_err(|e| e.to_string())?
|
Uri::from_str(&format!("http://{}", endpoint)).map_err(|e| e.to_string())?
|
||||||
};
|
};
|
||||||
Ok(grpc::call(&uri, service, method, message).await)
|
grpc::unary(&uri, service, method, message).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn grpc_client_streaming(
|
||||||
|
endpoint: &str,
|
||||||
|
service: &str,
|
||||||
|
method: &str,
|
||||||
|
message: &str,
|
||||||
|
// app_handle: AppHandle<Wry>,
|
||||||
|
// db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let uri = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
|
||||||
|
Uri::from_str(endpoint).map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
Uri::from_str(&format!("http://{}", endpoint)).map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
|
grpc::client_streaming(&uri, service, method, message).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn grpc_server_streaming(
|
||||||
|
endpoint: &str,
|
||||||
|
service: &str,
|
||||||
|
method: &str,
|
||||||
|
message: &str,
|
||||||
|
app_handle: AppHandle<Wry>,
|
||||||
|
// db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let uri = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
|
||||||
|
Uri::from_str(endpoint).map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
Uri::from_str(&format!("http://{}", endpoint)).map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stream = grpc::server_streaming(&uri, service, method, message)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_inner();
|
||||||
|
while let Some(item) = stream.next().await {
|
||||||
|
match item {
|
||||||
|
Ok(item) => {
|
||||||
|
let s = serde_json::to_string(&item).unwrap();
|
||||||
|
emit_side_effect(&app_handle, "grpc_message", s.clone());
|
||||||
|
println!("GOt item: {}", s);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("\terror: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok("foo".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -937,6 +990,9 @@ fn main() {
|
|||||||
.level_for("reqwest", log::LevelFilter::Info)
|
.level_for("reqwest", log::LevelFilter::Info)
|
||||||
.level_for("tokio_util", log::LevelFilter::Info)
|
.level_for("tokio_util", log::LevelFilter::Info)
|
||||||
.level_for("cookie_store", log::LevelFilter::Info)
|
.level_for("cookie_store", log::LevelFilter::Info)
|
||||||
|
.level_for("h2", log::LevelFilter::Info)
|
||||||
|
.level_for("tower", log::LevelFilter::Info)
|
||||||
|
.level_for("tonic", log::LevelFilter::Info)
|
||||||
.with_colors(ColoredLevelConfig::default())
|
.with_colors(ColoredLevelConfig::default())
|
||||||
.level(log::LevelFilter::Trace)
|
.level(log::LevelFilter::Trace)
|
||||||
.build(),
|
.build(),
|
||||||
@@ -1012,6 +1068,8 @@ fn main() {
|
|||||||
get_settings,
|
get_settings,
|
||||||
get_workspace,
|
get_workspace,
|
||||||
grpc_call_unary,
|
grpc_call_unary,
|
||||||
|
grpc_client_streaming,
|
||||||
|
grpc_server_streaming,
|
||||||
grpc_reflect,
|
grpc_reflect,
|
||||||
import_data,
|
import_data,
|
||||||
list_cookie_jars,
|
list_cookie_jars,
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import type { Props } from 'focus-trap-react';
|
import classNames from 'classnames';
|
||||||
import type { CSSProperties, FormEvent } from 'react';
|
import type { CSSProperties, FormEvent } from 'react';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useAlert } from '../hooks/useAlert';
|
||||||
import { useGrpc } from '../hooks/useGrpc';
|
import { useGrpc } from '../hooks/useGrpc';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
|
import { Banner } from './core/Banner';
|
||||||
import { Editor } from './core/Editor';
|
import { Editor } from './core/Editor';
|
||||||
|
import { HotKeyList } from './core/HotKeyList';
|
||||||
|
import { Icon } from './core/Icon';
|
||||||
|
import { Select } from './core/Select';
|
||||||
import { SplitLayout } from './core/SplitLayout';
|
import { SplitLayout } from './core/SplitLayout';
|
||||||
import { VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { GrpcEditor } from './GrpcEditor';
|
import { GrpcEditor } from './GrpcEditor';
|
||||||
import { UrlBar } from './UrlBar';
|
import { UrlBar } from './UrlBar';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
@@ -15,6 +21,17 @@ interface Props {
|
|||||||
|
|
||||||
export function GrpcConnectionLayout({ style }: Props) {
|
export function GrpcConnectionLayout({ style }: Props) {
|
||||||
const url = useKeyValue<string>({ namespace: 'debug', key: 'grpc_url', defaultValue: '' });
|
const url = useKeyValue<string>({ namespace: 'debug', key: 'grpc_url', defaultValue: '' });
|
||||||
|
const alert = useAlert();
|
||||||
|
const service = useKeyValue<string | null>({
|
||||||
|
namespace: 'debug',
|
||||||
|
key: 'grpc_service',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
|
const method = useKeyValue<string | null>({
|
||||||
|
namespace: 'debug',
|
||||||
|
key: 'grpc_method',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
const message = useKeyValue<string>({
|
const message = useKeyValue<string>({
|
||||||
namespace: 'debug',
|
namespace: 'debug',
|
||||||
key: 'grpc_message',
|
key: 'grpc_message',
|
||||||
@@ -22,23 +39,90 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
});
|
});
|
||||||
const [resp, setResp] = useState<string>('');
|
const [resp, setResp] = useState<string>('');
|
||||||
const grpc = useGrpc(url.value ?? null);
|
const grpc = useGrpc(url.value ?? null);
|
||||||
|
|
||||||
|
const activeMethod = useMemo(() => {
|
||||||
|
if (grpc.schema == null) return null;
|
||||||
|
const s = grpc.schema.find((s) => s.name === service.value);
|
||||||
|
if (s == null) return null;
|
||||||
|
return s.methods.find((m) => m.name === method.value);
|
||||||
|
}, [grpc.schema, method.value, service.value]);
|
||||||
|
|
||||||
const handleConnect = useCallback(
|
const handleConnect = useCallback(
|
||||||
async (e: FormEvent) => {
|
async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setResp(
|
if (activeMethod == null) return;
|
||||||
await grpc.callUnary.mutateAsync({
|
|
||||||
service: 'helloworld.Greeter',
|
if (service.value == null || method.value == null) {
|
||||||
method: 'SayHello',
|
alert({
|
||||||
|
id: 'grpc-invalid-service-method',
|
||||||
|
title: 'Error',
|
||||||
|
body: 'Service or method not selected',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (activeMethod.serverStreaming && !activeMethod.clientStreaming) {
|
||||||
|
await grpc.serverStreaming.mutateAsync({
|
||||||
|
service: service.value ?? 'n/a',
|
||||||
|
method: method.value ?? 'n/a',
|
||||||
message: message.value ?? '',
|
message: message.value ?? '',
|
||||||
}),
|
});
|
||||||
);
|
} else {
|
||||||
|
setResp(
|
||||||
|
await grpc.unary.mutateAsync({
|
||||||
|
service: service.value ?? 'n/a',
|
||||||
|
method: method.value ?? 'n/a',
|
||||||
|
message: message.value ?? '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[grpc.callUnary, message.value],
|
[
|
||||||
|
activeMethod,
|
||||||
|
alert,
|
||||||
|
grpc.serverStreaming,
|
||||||
|
grpc.unary,
|
||||||
|
message.value,
|
||||||
|
method.value,
|
||||||
|
service.value,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('REFLECT SCHEMA', grpc.schema);
|
if (grpc.schema == null) return;
|
||||||
}, [grpc.schema]);
|
const s = grpc.schema.find((s) => s.name === service.value);
|
||||||
|
if (s == null) {
|
||||||
|
service.set(grpc.schema[0]?.name ?? null);
|
||||||
|
method.set(grpc.schema[0]?.methods[0]?.name ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = s.methods.find((m) => m.name === method.value);
|
||||||
|
if (m == null) {
|
||||||
|
method.set(s.methods[0]?.name ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [grpc.schema, method, service]);
|
||||||
|
|
||||||
|
const handleChangeService = useCallback(
|
||||||
|
(v: string) => {
|
||||||
|
const [serviceName, methodName] = v.split('/', 2);
|
||||||
|
if (serviceName == null || methodName == null) throw new Error('Should never happen');
|
||||||
|
method.set(methodName);
|
||||||
|
service.set(serviceName);
|
||||||
|
},
|
||||||
|
[method, service],
|
||||||
|
);
|
||||||
|
|
||||||
|
const select = useMemo(() => {
|
||||||
|
const options =
|
||||||
|
grpc.schema?.flatMap((s) =>
|
||||||
|
s.methods.map((m) => ({
|
||||||
|
label: `${s.name.split('.', 2).pop() ?? s.name}/${m.name}`,
|
||||||
|
value: `${s.name}/${m.name}`,
|
||||||
|
})),
|
||||||
|
) ?? [];
|
||||||
|
const value = `${service.value ?? ''}/${method.value ?? ''}`;
|
||||||
|
return { value, options };
|
||||||
|
}, [grpc.schema, method.value, service.value]);
|
||||||
|
|
||||||
if (url.isLoading || url.value == null) {
|
if (url.isLoading || url.value == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -49,33 +133,84 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
style={style}
|
style={style}
|
||||||
leftSlot={() => (
|
leftSlot={() => (
|
||||||
<VStack space={2}>
|
<VStack space={2}>
|
||||||
<UrlBar
|
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-1.5">
|
||||||
id="foo"
|
<UrlBar
|
||||||
url={url.value ?? ''}
|
id="foo"
|
||||||
method={null}
|
url={url.value ?? ''}
|
||||||
placeholder="localhost:50051"
|
method={null}
|
||||||
onSubmit={handleConnect}
|
forceUpdateKey="to-do"
|
||||||
isLoading={false}
|
placeholder="localhost:50051"
|
||||||
onUrlChange={url.set}
|
onSubmit={handleConnect}
|
||||||
forceUpdateKey={''}
|
isLoading={grpc.unary.isLoading}
|
||||||
/>
|
onUrlChange={url.set}
|
||||||
|
submitIcon={
|
||||||
|
!activeMethod?.clientStreaming && activeMethod?.serverStreaming
|
||||||
|
? 'arrowDownToDot'
|
||||||
|
: activeMethod?.clientStreaming && !activeMethod?.serverStreaming
|
||||||
|
? 'arrowUpFromDot'
|
||||||
|
: activeMethod?.clientStreaming && activeMethod?.serverStreaming
|
||||||
|
? 'arrowUpDown'
|
||||||
|
: 'sendHorizontal'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
hideLabel
|
||||||
|
name="service"
|
||||||
|
label="Service"
|
||||||
|
size="sm"
|
||||||
|
value={select.value}
|
||||||
|
onChange={handleChangeService}
|
||||||
|
options={select.options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<GrpcEditor
|
<GrpcEditor
|
||||||
|
forceUpdateKey={[service, method].join('::')}
|
||||||
url={url.value ?? ''}
|
url={url.value ?? ''}
|
||||||
defaultValue={message.value}
|
defaultValue={message.value}
|
||||||
onChange={message.set}
|
onChange={message.set}
|
||||||
|
service={service.value ?? null}
|
||||||
|
method={method.value ?? null}
|
||||||
className="bg-gray-50"
|
className="bg-gray-50"
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
rightSlot={() => (
|
rightSlot={() =>
|
||||||
<Editor
|
!grpc.unary.isLoading && (
|
||||||
className="bg-gray-50 border border-highlight"
|
<div
|
||||||
contentType="application/json"
|
className={classNames(
|
||||||
defaultValue={resp}
|
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
|
||||||
readOnly
|
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
|
||||||
forceUpdateKey={resp}
|
'shadow shadow-gray-100 dark:shadow-gray-0 relative py-1',
|
||||||
/>
|
)}
|
||||||
)}
|
>
|
||||||
|
{grpc.unary.error ? (
|
||||||
|
<Banner color="danger" className="m-2">
|
||||||
|
{grpc.unary.error}
|
||||||
|
</Banner>
|
||||||
|
) : grpc.messages.length > 0 ? (
|
||||||
|
<VStack className="h-full overflow-y-auto">
|
||||||
|
{[...grpc.messages].reverse().map((m, i) => (
|
||||||
|
<HStack key={m.time.getTime()} space={3} className="px-2 py-1 font-mono text-xs">
|
||||||
|
<Icon icon="arrowDownToDot" />
|
||||||
|
<div>{format(m.time, 'HH:mm:ss')}</div>
|
||||||
|
<div>{m.message}</div>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
) : resp ? (
|
||||||
|
<Editor
|
||||||
|
className="bg-gray-50 dark:bg-gray-100"
|
||||||
|
contentType="application/json"
|
||||||
|
defaultValue={resp}
|
||||||
|
readOnly
|
||||||
|
forceUpdateKey={resp}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HotKeyList hotkeys={['grpc.send', 'sidebar.toggle', 'urlBar.focus']} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,83 @@
|
|||||||
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 { useGrpc } from '../hooks/useGrpc';
|
import { useGrpc } from '../hooks/useGrpc';
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
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 { InlineCode } from './core/InlineCode';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
|
||||||
type Props = Pick<
|
type Props = Pick<
|
||||||
EditorProps,
|
EditorProps,
|
||||||
'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'forceUpdateKey'
|
'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'forceUpdateKey'
|
||||||
> & {
|
> & {
|
||||||
url: string;
|
url: string;
|
||||||
|
service: string | null;
|
||||||
|
method: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GrpcEditor({ url, defaultValue, ...extraEditorProps }: Props) {
|
export function GrpcEditor({ url, service, method, defaultValue, ...extraEditorProps }: Props) {
|
||||||
const editorViewRef = useRef<EditorView>(null);
|
const editorViewRef = useRef<EditorView>(null);
|
||||||
const { schema } = useGrpc(url);
|
const grpc = useGrpc(url);
|
||||||
|
const alert = useAlert();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorViewRef.current == null || schema == null) return;
|
if (editorViewRef.current == null || grpc.schema == null) return;
|
||||||
const foo = schema[0].methods[0].schema;
|
const s = grpc.schema?.find((s) => s.name === service);
|
||||||
console.log('UPDATE SCHEMA', foo);
|
if (service != null && s == null) {
|
||||||
updateSchema(editorViewRef.current, JSON.parse(foo));
|
alert({
|
||||||
}, [schema]);
|
id: 'grpc-find-service-error',
|
||||||
|
title: "Couldn't Find Service",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
Failed to find service <InlineCode>{service}</InlineCode> in schema
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = s?.methods.find((m) => m.name === method)?.schema;
|
||||||
|
if (method != null && schema == null) {
|
||||||
|
alert({
|
||||||
|
id: 'grpc-find-schema-error',
|
||||||
|
title: "Couldn't Find Method",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
Failed to find method <InlineCode>{method}</InlineCode> for{' '}
|
||||||
|
<InlineCode>{service}</InlineCode> in schema
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateSchema(editorViewRef.current, JSON.parse(schema));
|
||||||
|
} catch (err) {
|
||||||
|
alert({
|
||||||
|
id: 'grpc-parse-schema-error',
|
||||||
|
title: 'Failed to Parse Schema',
|
||||||
|
body: (
|
||||||
|
<VStack space={4}>
|
||||||
|
<p>
|
||||||
|
For service <InlineCode>{service}</InlineCode> and method{' '}
|
||||||
|
<InlineCode>{method}</InlineCode>
|
||||||
|
</p>
|
||||||
|
<FormattedError>{String(err)}</FormattedError>
|
||||||
|
</VStack>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
console.log('Failed to parse method schema', method, schema);
|
||||||
|
}
|
||||||
|
}, [alert, grpc.schema, method, service]);
|
||||||
|
|
||||||
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)]">
|
||||||
|
|||||||
@@ -29,11 +29,20 @@ export const SettingsDialog = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
value={settings.appearance}
|
value={settings.appearance}
|
||||||
onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })}
|
onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })}
|
||||||
options={{
|
options={[
|
||||||
system: 'System',
|
{
|
||||||
light: 'Light',
|
label: 'System',
|
||||||
dark: 'Dark',
|
value: 'system',
|
||||||
}}
|
},
|
||||||
|
{
|
||||||
|
label: 'Light',
|
||||||
|
value: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dark',
|
||||||
|
value: 'dark',
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
@@ -44,10 +53,16 @@ export const SettingsDialog = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
value={settings.updateChannel}
|
value={settings.updateChannel}
|
||||||
onChange={(updateChannel) => updateSettings.mutateAsync({ ...settings, updateChannel })}
|
onChange={(updateChannel) => updateSettings.mutateAsync({ ...settings, updateChannel })}
|
||||||
options={{
|
options={[
|
||||||
stable: 'Release',
|
{
|
||||||
beta: 'Early Bird (Beta)',
|
label: 'Release',
|
||||||
}}
|
value: 'stable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Early Bird (Beta)',
|
||||||
|
value: 'beta',
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { FormEvent } from 'react';
|
|||||||
import { memo, useRef, useState } from 'react';
|
import { memo, useRef, useState } from 'react';
|
||||||
import { useHotKey } from '../hooks/useHotKey';
|
import { useHotKey } from '../hooks/useHotKey';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
|
import type { IconProps } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { Input } from './core/Input';
|
import { Input } from './core/Input';
|
||||||
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
||||||
@@ -13,6 +14,7 @@ type Props = Pick<HttpRequest, 'id' | 'url'> & {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
onSubmit: (e: FormEvent) => void;
|
onSubmit: (e: FormEvent) => void;
|
||||||
onUrlChange: (url: string) => void;
|
onUrlChange: (url: string) => void;
|
||||||
|
submitIcon?: IconProps['icon'];
|
||||||
onMethodChange?: (method: string) => void;
|
onMethodChange?: (method: string) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
@@ -27,6 +29,7 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
className,
|
className,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onMethodChange,
|
onMethodChange,
|
||||||
|
submitIcon = 'sendHorizontal',
|
||||||
isLoading,
|
isLoading,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const inputRef = useRef<EditorView>(null);
|
const inputRef = useRef<EditorView>(null);
|
||||||
@@ -77,7 +80,7 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
title="Send Request"
|
title="Send Request"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-8 mr-0.5 my-0.5"
|
className="w-8 mr-0.5 my-0.5"
|
||||||
icon={isLoading ? 'update' : 'sendHorizontal'}
|
icon={isLoading ? 'update' : submitIcon}
|
||||||
spin={isLoading}
|
spin={isLoading}
|
||||||
hotkeyAction="request.send"
|
hotkeyAction="request.send"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const icons = {
|
|||||||
magicWand: lucide.Wand2Icon,
|
magicWand: lucide.Wand2Icon,
|
||||||
moreVertical: lucide.MoreVerticalIcon,
|
moreVertical: lucide.MoreVerticalIcon,
|
||||||
pencil: lucide.PencilIcon,
|
pencil: lucide.PencilIcon,
|
||||||
|
plug: lucide.Plug,
|
||||||
plus: lucide.PlusIcon,
|
plus: lucide.PlusIcon,
|
||||||
plusCircle: lucide.PlusCircleIcon,
|
plusCircle: lucide.PlusCircleIcon,
|
||||||
question: lucide.ShieldQuestionIcon,
|
question: lucide.ShieldQuestionIcon,
|
||||||
@@ -39,6 +40,9 @@ const icons = {
|
|||||||
trash: lucide.TrashIcon,
|
trash: lucide.TrashIcon,
|
||||||
update: lucide.RefreshCcwIcon,
|
update: lucide.RefreshCcwIcon,
|
||||||
upload: lucide.UploadIcon,
|
upload: lucide.UploadIcon,
|
||||||
|
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
|
||||||
|
arrowDownToDot: lucide.ArrowDownToDotIcon,
|
||||||
|
arrowUpDown: lucide.ArrowUpDownIcon,
|
||||||
x: lucide.XIcon,
|
x: lucide.XIcon,
|
||||||
|
|
||||||
empty: (props: HTMLAttributes<HTMLSpanElement>) => <span {...props} />,
|
empty: (props: HTMLAttributes<HTMLSpanElement>) => <span {...props} />,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ 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 px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
|
'font-mono text-sm bg-highlight border-0 border-gray-200',
|
||||||
|
'px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ interface Props<T extends string> {
|
|||||||
labelPosition?: 'top' | 'left';
|
labelPosition?: 'top' | 'left';
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
value: string;
|
value: T;
|
||||||
options: Record<T, string>;
|
options: { label: string; value: T }[];
|
||||||
onChange: (value: T) => void;
|
onChange: (value: T) => void;
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Select<T extends string>({
|
export function Select<T extends string>({
|
||||||
@@ -21,12 +22,14 @@ export function Select<T extends string>({
|
|||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
|
className,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const id = `input-${name}`;
|
const id = `input-${name}`;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
className,
|
||||||
'w-full',
|
'w-full',
|
||||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||||
labelPosition === 'left' && 'flex items-center gap-2',
|
labelPosition === 'left' && 'flex items-center gap-2',
|
||||||
@@ -48,7 +51,7 @@ export function Select<T extends string>({
|
|||||||
style={selectBackgroundStyles}
|
style={selectBackgroundStyles}
|
||||||
onChange={(e) => onChange(e.target.value as T)}
|
onChange={(e) => onChange(e.target.value as T)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'font-mono text-xs border w-full px-2 outline-none bg-transparent',
|
'font-mono text-xs border w-full outline-none bg-transparent pl-2 pr-7',
|
||||||
'border-highlight focus:border-focus',
|
'border-highlight focus:border-focus',
|
||||||
size === 'xs' && 'h-xs',
|
size === 'xs' && 'h-xs',
|
||||||
size === 'sm' && 'h-sm',
|
size === 'sm' && 'h-sm',
|
||||||
@@ -56,8 +59,8 @@ export function Select<T extends string>({
|
|||||||
size === 'lg' && 'h-lg',
|
size === 'lg' && 'h-lg',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Object.entries<string>(options).map(([value, label]) => (
|
{options.map(({ label, value }) => (
|
||||||
<option key={value} value={value}>
|
<option key={label} value={value}>
|
||||||
{label}
|
{label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
@@ -68,7 +71,7 @@ export function Select<T extends string>({
|
|||||||
|
|
||||||
const selectBackgroundStyles = {
|
const selectBackgroundStyles = {
|
||||||
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||||
backgroundPosition: 'right 0.5rem center',
|
backgroundPosition: 'right 0.3rem center',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundSize: '1.5em 1.5em',
|
backgroundSize: '1.5em 1.5em',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
import type { DialogProps } from '../components/core/Dialog';
|
import type { DialogProps } from '../components/core/Dialog';
|
||||||
import { useDialog } from '../components/DialogContext';
|
import { useDialog } from '../components/DialogContext';
|
||||||
import type { AlertProps } from './Alert';
|
import type { AlertProps } from './Alert';
|
||||||
@@ -5,20 +6,16 @@ import { Alert } from './Alert';
|
|||||||
|
|
||||||
export function useAlert() {
|
export function useAlert() {
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
return ({
|
return useCallback(
|
||||||
id,
|
({ id, title, body }: { id: string; title: DialogProps['title']; body: AlertProps['body'] }) =>
|
||||||
title,
|
dialog.show({
|
||||||
body,
|
id,
|
||||||
}: {
|
title,
|
||||||
id: string;
|
hideX: true,
|
||||||
title: DialogProps['title'];
|
size: 'sm',
|
||||||
body: AlertProps['body'];
|
render: ({ hide }) => Alert({ onHide: hide, body }),
|
||||||
}) =>
|
}),
|
||||||
dialog.show({
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
id,
|
[],
|
||||||
title,
|
);
|
||||||
hideX: true,
|
|
||||||
size: 'sm',
|
|
||||||
render: ({ hide }) => Alert({ onHide: hide, body }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
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 { useState } from 'react';
|
||||||
|
import { useListenToTauriEvent } from './useListenToTauriEvent';
|
||||||
|
|
||||||
|
interface ReflectResponseService {
|
||||||
|
name: string;
|
||||||
|
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
message: string;
|
||||||
|
time: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export function useGrpc(url: string | null) {
|
export function useGrpc(url: string | null) {
|
||||||
const callUnary = useMutation<
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
string,
|
useListenToTauriEvent<string>(
|
||||||
unknown,
|
'grpc_message',
|
||||||
{ service: string; method: string; message: string }
|
(event) => {
|
||||||
>({
|
console.log('GOT MESSAGE', event);
|
||||||
mutationKey: ['grpc_call_reflect', url],
|
setMessages((prev) => [...prev, { message: event.payload, time: new Date() }]);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const unary = useMutation<string, string, { service: string; method: string; message: string }>({
|
||||||
|
mutationKey: ['grpc_unary', url],
|
||||||
mutationFn: async ({ service, method, message }) => {
|
mutationFn: async ({ service, method, message }) => {
|
||||||
if (url === null) throw new Error('No URL provided');
|
if (url === null) throw new Error('No URL provided');
|
||||||
return (await invoke('grpc_call_unary', {
|
return (await invoke('grpc_call_unary', {
|
||||||
@@ -19,17 +36,36 @@ export function useGrpc(url: string | null) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const reflect = useQuery<string | null>({
|
const serverStreaming = useMutation<
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
{ service: string; method: string; message: string }
|
||||||
|
>({
|
||||||
|
mutationKey: ['grpc_server_streaming', url],
|
||||||
|
mutationFn: async ({ service, method, message }) => {
|
||||||
|
if (url === null) throw new Error('No URL provided');
|
||||||
|
return (await invoke('grpc_server_streaming', {
|
||||||
|
endpoint: url,
|
||||||
|
service,
|
||||||
|
method,
|
||||||
|
message,
|
||||||
|
})) as string;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const reflect = useQuery<ReflectResponseService[]>({
|
||||||
queryKey: ['grpc_reflect', url ?? ''],
|
queryKey: ['grpc_reflect', url ?? ''],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (url === null) return null;
|
if (url === null) return [];
|
||||||
console.log('GETTING SCHEMA', url);
|
console.log('GETTING SCHEMA', url);
|
||||||
return (await invoke('grpc_reflect', { endpoint: url })) as string;
|
return (await invoke('grpc_reflect', { endpoint: url })) as ReflectResponseService[];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
callUnary,
|
unary,
|
||||||
|
serverStreaming,
|
||||||
schema: reflect.data,
|
schema: reflect.data,
|
||||||
|
messages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,44 +5,47 @@ import { debounce } from '../lib/debounce';
|
|||||||
import { useOsInfo } from './useOsInfo';
|
import { useOsInfo } from './useOsInfo';
|
||||||
|
|
||||||
export type HotkeyAction =
|
export type HotkeyAction =
|
||||||
| 'request.send'
|
| 'environmentEditor.toggle'
|
||||||
|
| 'grpc.send'
|
||||||
|
| 'hotkeys.showHelp'
|
||||||
| 'request.create'
|
| 'request.create'
|
||||||
| 'request.duplicate'
|
| 'request.duplicate'
|
||||||
| 'sidebar.toggle'
|
| 'request.send'
|
||||||
| 'sidebar.focus'
|
|
||||||
| 'urlBar.focus'
|
|
||||||
| 'environmentEditor.toggle'
|
|
||||||
| 'hotkeys.showHelp'
|
|
||||||
| 'requestSwitcher.prev'
|
|
||||||
| 'requestSwitcher.next'
|
| 'requestSwitcher.next'
|
||||||
| 'settings.show';
|
| 'requestSwitcher.prev'
|
||||||
|
| 'settings.show'
|
||||||
|
| 'sidebar.focus'
|
||||||
|
| 'sidebar.toggle'
|
||||||
|
| 'urlBar.focus';
|
||||||
|
|
||||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||||
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
|
||||||
|
'grpc.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||||
|
'hotkeys.showHelp': ['CmdCtrl+Shift+/'],
|
||||||
'request.create': ['CmdCtrl+n'],
|
'request.create': ['CmdCtrl+n'],
|
||||||
'request.duplicate': ['CmdCtrl+d'],
|
'request.duplicate': ['CmdCtrl+d'],
|
||||||
'sidebar.toggle': ['CmdCtrl+b'],
|
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||||
'sidebar.focus': ['CmdCtrl+1'],
|
|
||||||
'urlBar.focus': ['CmdCtrl+l'],
|
|
||||||
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
|
|
||||||
'hotkeys.showHelp': ['CmdCtrl+Shift+/'],
|
|
||||||
'settings.show': ['CmdCtrl+,'],
|
|
||||||
'requestSwitcher.prev': ['Control+Tab'],
|
|
||||||
'requestSwitcher.next': ['Control+Shift+Tab'],
|
'requestSwitcher.next': ['Control+Shift+Tab'],
|
||||||
|
'requestSwitcher.prev': ['Control+Tab'],
|
||||||
|
'settings.show': ['CmdCtrl+,'],
|
||||||
|
'sidebar.focus': ['CmdCtrl+1'],
|
||||||
|
'sidebar.toggle': ['CmdCtrl+b'],
|
||||||
|
'urlBar.focus': ['CmdCtrl+l'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const hotkeyLabels: Record<HotkeyAction, string> = {
|
const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||||
'request.send': 'Send Request',
|
'environmentEditor.toggle': 'Edit Environments',
|
||||||
|
'grpc.send': 'Send Message',
|
||||||
|
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
||||||
'request.create': 'New Request',
|
'request.create': 'New Request',
|
||||||
'request.duplicate': 'Duplicate Request',
|
'request.duplicate': 'Duplicate Request',
|
||||||
'sidebar.toggle': 'Toggle Sidebar',
|
'request.send': 'Send Request',
|
||||||
'sidebar.focus': 'Focus Sidebar',
|
|
||||||
'urlBar.focus': 'Focus URL',
|
|
||||||
'environmentEditor.toggle': 'Edit Environments',
|
|
||||||
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
|
||||||
'requestSwitcher.prev': 'Go To Next Request',
|
|
||||||
'requestSwitcher.next': 'Go To Previous Request',
|
'requestSwitcher.next': 'Go To Previous Request',
|
||||||
|
'requestSwitcher.prev': 'Go To Next Request',
|
||||||
'settings.show': 'Open Settings',
|
'settings.show': 'Open Settings',
|
||||||
|
'sidebar.focus': 'Focus Sidebar',
|
||||||
|
'sidebar.toggle': 'Toggle Sidebar',
|
||||||
|
'urlBar.focus': 'Focus URL',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||||
|
|||||||
Reference in New Issue
Block a user