diff --git a/package-lock.json b/package-lock.json index 42b4fa10..02a63d27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "codemirror": "^6.0.1", "codemirror-json-schema": "^0.6.1", "codemirror-json5": "^1.0.3", + "date-fns": "^3.3.1", "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", "framer-motion": "^9.0.4", @@ -3805,6 +3806,15 @@ "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", "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": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index 3ff30261..7c0e80b7 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "codemirror": "^6.0.1", "codemirror-json-schema": "^0.6.1", "codemirror-json5": "^1.0.3", + "date-fns": "^3.3.1", "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", "framer-motion": "^9.0.4", diff --git a/src-tauri/grpc/src/json_schema.rs b/src-tauri/grpc/src/json_schema.rs index 3d859d17..1bad1c02 100644 --- a/src-tauri/grpc/src/json_schema.rs +++ b/src-tauri/grpc/src/json_schema.rs @@ -22,8 +22,7 @@ pub struct JsonSchemaEntry { enum_: Option>, /// Don't allow any other properties in the object - #[serde(skip_serializing_if = "Option::is_none")] - additional_properties: Option, + additional_properties: bool, /// Set all properties to required #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src-tauri/grpc/src/lib.rs b/src-tauri/grpc/src/lib.rs index 2668fb29..56dcf85a 100644 --- a/src-tauri/grpc/src/lib.rs +++ b/src-tauri/grpc/src/lib.rs @@ -1,8 +1,10 @@ -use prost_reflect::DynamicMessage; +use prost::Message; +use prost_reflect::{DynamicMessage, SerializeOptions}; use serde::{Deserialize, Serialize}; use serde_json::Deserializer; -use tonic::IntoRequest; +use tokio_stream::{Stream, StreamExt}; use tonic::transport::Uri; +use tonic::{IntoRequest, Response, Streaming}; use crate::codec::DynamicCodec; use crate::proto::{fill_pool, method_desc_to_path}; @@ -11,19 +13,32 @@ mod codec; mod json_schema; mod proto; +pub fn serialize_options() -> SerializeOptions { + SerializeOptions::new().skip_default_fields(false) +} + #[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default, rename_all = "camelCase")] pub struct ServiceDefinition { pub name: String, pub methods: Vec, } #[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default, rename_all = "camelCase")] pub struct MethodDefinition { pub name: 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 { let (pool, conn) = fill_pool(uri).await; 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 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(); 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(); 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> { + println!("poll_next"); + todo!() + } +} + +pub async fn client_streaming( + uri: &Uri, + service: &str, + method: &str, + message_json: &str, +) -> Result { + 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"); 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>, 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 { @@ -60,12 +165,14 @@ pub async fn callable(uri: &Uri) -> Vec { .map(|s| { let mut def = ServiceDefinition { name: s.full_name().to_string(), - ..Default::default() + 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, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b504ab63..fb1e285c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,32 +8,33 @@ extern crate core; #[macro_use] extern crate objc; -use ::http::Uri; use std::collections::HashMap; 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::str::FromStr; +use ::http::Uri; use fern::colors::ColoredLevelConfig; -use grpc::ServiceDefinition; +use futures::StreamExt; use log::{debug, error, info, warn}; use rand::random; use serde::Serialize; use serde_json::{json, Value}; +use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; 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::{Manager, WindowEvent}; +#[cfg(target_os = "macos")] +use tauri::TitleBarStyle; use tauri_plugin_log::{fern, LogTarget}; use tauri_plugin_window_state::{StateFlags, WindowExt}; use tokio::sync::Mutex; use tokio::time::sleep; use window_shadows::set_shadow; +use grpc::ServiceDefinition; use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; @@ -106,7 +107,59 @@ async fn grpc_call_unary( } else { 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, + // db_instance: State<'_, Mutex>>, +) -> Result { + 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, + // db_instance: State<'_, Mutex>>, +) -> Result { + 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] @@ -937,6 +990,9 @@ fn main() { .level_for("reqwest", log::LevelFilter::Info) .level_for("tokio_util", 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()) .level(log::LevelFilter::Trace) .build(), @@ -1012,6 +1068,8 @@ fn main() { get_settings, get_workspace, grpc_call_unary, + grpc_client_streaming, + grpc_server_streaming, grpc_reflect, import_data, list_cookie_jars, diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx index a348111a..eb7785ed 100644 --- a/src-web/components/GrpcConnectionLayout.tsx +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -1,13 +1,19 @@ -import type { Props } from 'focus-trap-react'; +import classNames from 'classnames'; 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 { useKeyValue } from '../hooks/useKeyValue'; +import { Banner } from './core/Banner'; 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 { VStack } from './core/Stacks'; +import { HStack, VStack } from './core/Stacks'; import { GrpcEditor } from './GrpcEditor'; import { UrlBar } from './UrlBar'; +import { format } from 'date-fns'; interface Props { style: CSSProperties; @@ -15,6 +21,17 @@ interface Props { export function GrpcConnectionLayout({ style }: Props) { const url = useKeyValue({ namespace: 'debug', key: 'grpc_url', defaultValue: '' }); + const alert = useAlert(); + const service = useKeyValue({ + namespace: 'debug', + key: 'grpc_service', + defaultValue: null, + }); + const method = useKeyValue({ + namespace: 'debug', + key: 'grpc_method', + defaultValue: null, + }); const message = useKeyValue({ namespace: 'debug', key: 'grpc_message', @@ -22,23 +39,90 @@ export function GrpcConnectionLayout({ style }: Props) { }); const [resp, setResp] = useState(''); 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( async (e: FormEvent) => { e.preventDefault(); - setResp( - await grpc.callUnary.mutateAsync({ - service: 'helloworld.Greeter', - method: 'SayHello', + if (activeMethod == null) return; + + if (service.value == null || method.value == null) { + 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 ?? '', - }), - ); + }); + } 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(() => { - console.log('REFLECT SCHEMA', grpc.schema); - }, [grpc.schema]); + if (grpc.schema == null) return; + 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) { return null; @@ -49,33 +133,84 @@ export function GrpcConnectionLayout({ style }: Props) { style={style} leftSlot={() => ( - +
+ + { size="sm" value={settings.updateChannel} onChange={(updateChannel) => updateSettings.mutateAsync({ ...settings, updateChannel })} - options={{ - stable: 'Release', - beta: 'Early Bird (Beta)', - }} + options={[ + { + label: 'Release', + value: 'stable', + }, + { + label: 'Early Bird (Beta)', + value: 'beta', + }, + ]} /> diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index 77e4f650..d1ef61a3 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -3,6 +3,7 @@ import type { FormEvent } from 'react'; import { memo, useRef, useState } from 'react'; import { useHotKey } from '../hooks/useHotKey'; import type { HttpRequest } from '../lib/models'; +import type { IconProps } from './core/Icon'; import { IconButton } from './core/IconButton'; import { Input } from './core/Input'; import { RequestMethodDropdown } from './RequestMethodDropdown'; @@ -13,6 +14,7 @@ type Props = Pick & { placeholder: string; onSubmit: (e: FormEvent) => void; onUrlChange: (url: string) => void; + submitIcon?: IconProps['icon']; onMethodChange?: (method: string) => void; isLoading: boolean; forceUpdateKey: string; @@ -27,6 +29,7 @@ export const UrlBar = memo(function UrlBar({ className, onSubmit, onMethodChange, + submitIcon = 'sendHorizontal', isLoading, }: Props) { const inputRef = useRef(null); @@ -77,7 +80,7 @@ export const UrlBar = memo(function UrlBar({ title="Send Request" type="submit" className="w-8 mr-0.5 my-0.5" - icon={isLoading ? 'update' : 'sendHorizontal'} + icon={isLoading ? 'update' : submitIcon} spin={isLoading} hotkeyAction="request.send" /> diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index 5d310c7c..adbcff3a 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -29,6 +29,7 @@ const icons = { magicWand: lucide.Wand2Icon, moreVertical: lucide.MoreVerticalIcon, pencil: lucide.PencilIcon, + plug: lucide.Plug, plus: lucide.PlusIcon, plusCircle: lucide.PlusCircleIcon, question: lucide.ShieldQuestionIcon, @@ -39,6 +40,9 @@ const icons = { trash: lucide.TrashIcon, update: lucide.RefreshCcwIcon, upload: lucide.UploadIcon, + arrowUpFromDot: lucide.ArrowUpFromDotIcon, + arrowDownToDot: lucide.ArrowDownToDotIcon, + arrowUpDown: lucide.ArrowUpDownIcon, x: lucide.XIcon, empty: (props: HTMLAttributes) => , diff --git a/src-web/components/core/InlineCode.tsx b/src-web/components/core/InlineCode.tsx index 67d3848d..7c5ec681 100644 --- a/src-web/components/core/InlineCode.tsx +++ b/src-web/components/core/InlineCode.tsx @@ -6,7 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes diff --git a/src-web/components/core/Select.tsx b/src-web/components/core/Select.tsx index 4ef42883..8c733063 100644 --- a/src-web/components/core/Select.tsx +++ b/src-web/components/core/Select.tsx @@ -6,10 +6,11 @@ interface Props { labelPosition?: 'top' | 'left'; labelClassName?: string; hideLabel?: boolean; - value: string; - options: Record; + value: T; + options: { label: string; value: T }[]; onChange: (value: T) => void; size?: 'xs' | 'sm' | 'md' | 'lg'; + className?: string; } export function Select({ @@ -21,12 +22,14 @@ export function Select({ value, options, onChange, + className, size = 'md', }: Props) { const id = `input-${name}`; return (
({ style={selectBackgroundStyles} onChange={(e) => onChange(e.target.value as T)} 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', size === 'xs' && 'h-xs', size === 'sm' && 'h-sm', @@ -56,8 +59,8 @@ export function Select({ size === 'lg' && 'h-lg', )} > - {Object.entries(options).map(([value, label]) => ( - ))} @@ -68,7 +71,7 @@ export function Select({ 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")`, - backgroundPosition: 'right 0.5rem center', + backgroundPosition: 'right 0.3rem center', backgroundRepeat: 'no-repeat', backgroundSize: '1.5em 1.5em', }; diff --git a/src-web/hooks/useAlert.ts b/src-web/hooks/useAlert.ts index 0dbfac2a..2c3b84e0 100644 --- a/src-web/hooks/useAlert.ts +++ b/src-web/hooks/useAlert.ts @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import type { DialogProps } from '../components/core/Dialog'; import { useDialog } from '../components/DialogContext'; import type { AlertProps } from './Alert'; @@ -5,20 +6,16 @@ import { Alert } from './Alert'; export function useAlert() { const dialog = useDialog(); - return ({ - id, - title, - body, - }: { - id: string; - title: DialogProps['title']; - body: AlertProps['body']; - }) => - dialog.show({ - id, - title, - hideX: true, - size: 'sm', - render: ({ hide }) => Alert({ onHide: hide, body }), - }); + return useCallback( + ({ id, title, body }: { id: string; title: DialogProps['title']; body: AlertProps['body'] }) => + dialog.show({ + id, + title, + hideX: true, + size: 'sm', + render: ({ hide }) => Alert({ onHide: hide, body }), + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); } diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index 55006b1f..7b3939de 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -1,13 +1,30 @@ import { useMutation, useQuery } from '@tanstack/react-query'; 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) { - const callUnary = useMutation< - string, - unknown, - { service: string; method: string; message: string } - >({ - mutationKey: ['grpc_call_reflect', url], + const [messages, setMessages] = useState([]); + useListenToTauriEvent( + 'grpc_message', + (event) => { + console.log('GOT MESSAGE', event); + setMessages((prev) => [...prev, { message: event.payload, time: new Date() }]); + }, + [], + ); + const unary = useMutation({ + mutationKey: ['grpc_unary', url], mutationFn: async ({ service, method, message }) => { if (url === null) throw new Error('No URL provided'); return (await invoke('grpc_call_unary', { @@ -19,17 +36,36 @@ export function useGrpc(url: string | null) { }, }); - const reflect = useQuery({ + 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({ queryKey: ['grpc_reflect', url ?? ''], queryFn: async () => { - if (url === null) return null; + if (url === null) return []; console.log('GETTING SCHEMA', url); - return (await invoke('grpc_reflect', { endpoint: url })) as string; + return (await invoke('grpc_reflect', { endpoint: url })) as ReflectResponseService[]; }, }); return { - callUnary, + unary, + serverStreaming, schema: reflect.data, + messages, }; } diff --git a/src-web/hooks/useHotKey.ts b/src-web/hooks/useHotKey.ts index 0796729d..e9da3aea 100644 --- a/src-web/hooks/useHotKey.ts +++ b/src-web/hooks/useHotKey.ts @@ -5,44 +5,47 @@ import { debounce } from '../lib/debounce'; import { useOsInfo } from './useOsInfo'; export type HotkeyAction = - | 'request.send' + | 'environmentEditor.toggle' + | 'grpc.send' + | 'hotkeys.showHelp' | 'request.create' | 'request.duplicate' - | 'sidebar.toggle' - | 'sidebar.focus' - | 'urlBar.focus' - | 'environmentEditor.toggle' - | 'hotkeys.showHelp' - | 'requestSwitcher.prev' + | 'request.send' | 'requestSwitcher.next' - | 'settings.show'; + | 'requestSwitcher.prev' + | 'settings.show' + | 'sidebar.focus' + | 'sidebar.toggle' + | 'urlBar.focus'; const hotkeys: Record = { - '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.duplicate': ['CmdCtrl+d'], - 'sidebar.toggle': ['CmdCtrl+b'], - 'sidebar.focus': ['CmdCtrl+1'], - 'urlBar.focus': ['CmdCtrl+l'], - 'environmentEditor.toggle': ['CmdCtrl+Shift+e'], - 'hotkeys.showHelp': ['CmdCtrl+Shift+/'], - 'settings.show': ['CmdCtrl+,'], - 'requestSwitcher.prev': ['Control+Tab'], + 'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'], '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 = { - 'request.send': 'Send Request', + 'environmentEditor.toggle': 'Edit Environments', + 'grpc.send': 'Send Message', + 'hotkeys.showHelp': 'Show Keyboard Shortcuts', 'request.create': 'New Request', 'request.duplicate': 'Duplicate Request', - 'sidebar.toggle': 'Toggle Sidebar', - 'sidebar.focus': 'Focus Sidebar', - 'urlBar.focus': 'Focus URL', - 'environmentEditor.toggle': 'Edit Environments', - 'hotkeys.showHelp': 'Show Keyboard Shortcuts', - 'requestSwitcher.prev': 'Go To Next Request', + 'request.send': 'Send Request', 'requestSwitcher.next': 'Go To Previous Request', + 'requestSwitcher.prev': 'Go To Next Request', '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)[];