Use req/conn/msg models in unary/server

This commit is contained in:
Gregory Schier
2024-02-04 11:57:12 -08:00
parent d2b44cb7d2
commit 6d6f865fb7
27 changed files with 497 additions and 233 deletions

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, connection_id, created_at, message\n FROM grpc_messages\n WHERE id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE connection_id = ?\n ",
"describe": {
"columns": [
{
@@ -37,6 +37,16 @@
"name": "message",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "is_server",
"ordinal": 7,
"type_info": "Bool"
},
{
"name": "is_info",
"ordinal": 8,
"type_info": "Bool"
}
],
"parameters": {
@@ -49,8 +59,10 @@
false,
false,
false,
false,
false,
false
]
},
"hash": "f055a2b6ab6bc3fea115dbea3f287df833c9bb73cdf2fe38da21fe5f5f6ae639"
"hash": "196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, connection_id, created_at, message\n FROM grpc_messages\n WHERE workspace_id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -37,6 +37,16 @@
"name": "message",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "is_server",
"ordinal": 7,
"type_info": "Bool"
},
{
"name": "is_info",
"ordinal": 8,
"type_info": "Bool"
}
],
"parameters": {
@@ -49,8 +59,10 @@
false,
false,
false,
false,
false,
false
]
},
"hash": "a86d3e86c5638d5f666c0cbe26dd5b3e88c632c56c7c4f3f116a29867e41e6a2"
"hash": "3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "4abfe0884ba046534dc6c255409c41ce287d7b0137726b8ca09870382a8b8300"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message, is_server, is_info\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message,\n is_server = excluded.is_server,\n is_info = excluded.is_info\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method\n FROM grpc_connections\n WHERE workspace_id = ?\n ",
"query": "\n SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ",
"describe": {
"columns": [
{
@@ -58,5 +58,5 @@
false
]
},
"hash": "29d3bee45c4fca4c63231e1205edc708818737fe37137dbba6af2d784c3c0221"
"hash": "a7b969f33ed0424188b429227d6e3fac2bef52f2e1b0eb1d3846d1293d41f86c"
}

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap;
use prost_reflect::{DescriptorPool, MessageDescriptor};
use prost_types::field_descriptor_proto;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
@@ -50,8 +50,8 @@ impl Default for JsonType {
impl serde::Serialize for JsonType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
where
S: serde::Serializer,
{
match self {
JsonType::String => serializer.serialize_str("string"),
@@ -67,8 +67,8 @@ impl serde::Serialize for JsonType {
impl<'de> serde::Deserialize<'de> for JsonType {
fn deserialize<D>(deserializer: D) -> Result<JsonType, D::Error>
where
D: serde::Deserializer<'de>,
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
@@ -83,7 +83,10 @@ impl<'de> serde::Deserialize<'de> for JsonType {
}
}
pub fn message_to_json_schema(pool: &DescriptorPool, message: MessageDescriptor) -> JsonSchemaEntry {
pub fn message_to_json_schema(
pool: &DescriptorPool,
message: MessageDescriptor,
) -> JsonSchemaEntry {
let mut schema = JsonSchemaEntry {
title: Some(message.name().to_string()),
type_: JsonType::Object, // Messages are objects

View File

@@ -52,5 +52,7 @@ CREATE TABLE grpc_messages
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_server BOOLEAN NOT NULL,
is_info BOOLEAN NOT NULL,
message TEXT NOT NULL
);

View File

@@ -44,14 +44,15 @@ use crate::http::send_http_request;
use crate::models::{
cancel_pending_responses, create_response, delete_all_responses, delete_cookie_jar,
delete_environment, delete_folder, delete_request, delete_response, delete_workspace,
duplicate_grpc_request, duplicate_http_request, list_cookie_jars, list_folders, list_requests,
list_responses, list_workspaces, generate_id, get_cookie_jar, get_environment, get_folder,
get_grpc_request, get_http_request, get_key_value_raw, get_or_create_settings, get_response,
get_workspace, get_workspace_export_resources, list_environments, list_grpc_requests,
set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
upsert_environment, upsert_folder, upsert_grpc_request, upsert_http_request, upsert_workspace,
CookieJar, Environment, EnvironmentVariable, Folder, GrpcRequest, HttpRequest, HttpResponse,
KeyValue, Settings, Workspace,
duplicate_grpc_request, duplicate_http_request, generate_id, get_cookie_jar, get_environment,
get_folder, get_grpc_request, get_http_request, get_key_value_raw, get_or_create_settings,
get_response, get_workspace, get_workspace_export_resources, list_cookie_jars,
list_environments, list_folders, list_grpc_connections, list_grpc_messages, list_grpc_requests,
list_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_message, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar,
Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcMessage, GrpcRequest,
HttpRequest, HttpResponse, KeyValue, Settings, Workspace,
};
use crate::plugin::{ImportResources, ImportResult};
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
@@ -98,20 +99,80 @@ async fn cmd_grpc_reflect(endpoint: &str) -> Result<Vec<ServiceDefinition>, Stri
#[tauri::command]
async fn cmd_grpc_call_unary(
endpoint: &str,
service: &str,
method: &str,
message: &str,
request_id: &str,
app_handle: AppHandle<Wry>,
grpc_handle: State<'_, Mutex<GrpcManager>>,
) -> Result<String, String> {
let uri = safe_uri(endpoint).map_err(|e| e.to_string())?;
grpc_handle
db_state: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<GrpcMessage, String> {
let db = &*db_state.lock().await;
let req = get_grpc_request(db, request_id)
.await
.map_err(|e| e.to_string())?;
let conn = {
let req = req.clone();
upsert_grpc_connection(
db,
&GrpcConnection {
workspace_id: req.workspace_id,
request_id: req.id,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())?
};
emit_side_effect(app_handle.clone(), "created_model", conn.clone());
{
let req = req.clone();
let conn = conn.clone();
upsert_grpc_message(
db,
&GrpcMessage {
workspace_id: req.workspace_id,
request_id: req.id,
connection_id: conn.id,
is_info: true,
message: format!("Initiating connection to {}", req.url),
..Default::default()
},
)
.await
.map_err(|e| e.to_string())?;
};
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
let conn_id = generate_id(Some("grpc"));
let msg = match grpc_handle
.lock()
.await
.connect("default", uri)
.connect(&conn_id, uri)
.await
.unary(service, method, message)
.unary(
&req.service.unwrap_or_default(),
&req.method.unwrap_or_default(),
&req.message,
)
.await
{
Ok(msg) => {
upsert_grpc_message(
db,
&GrpcMessage {
message: msg,
workspace_id: req.workspace_id,
request_id: req.id,
connection_id: conn.id,
is_server: true,
..Default::default()
},
)
.await
}
Err(e) => return Err(e.to_string()),
};
msg.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -235,27 +296,47 @@ async fn cmd_grpc_bidi_streaming(
#[tauri::command]
async fn cmd_grpc_server_streaming(
endpoint: &str,
service: &str,
method: &str,
message: &str,
request_id: &str,
app_handle: AppHandle<Wry>,
grpc_handle: State<'_, Mutex<GrpcManager>>,
) -> Result<String, String> {
db_state: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<GrpcConnection, String> {
let db = &*db_state.lock().await;
let req = get_grpc_request(db, request_id)
.await
.map_err(|e| e.to_string())?;
let conn = {
let req = req.clone();
upsert_grpc_connection(
db,
&GrpcConnection {
workspace_id: req.workspace_id,
request_id: req.id,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())?
};
emit_side_effect(app_handle.clone(), "created_model", conn.clone());
let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false);
let uri = safe_uri(endpoint).map_err(|e| e.to_string())?;
let conn_id = generate_id(Some("grpc"));
let (service, method) = match (&req.service, &req.method) {
(Some(service), Some(method)) => (service, method),
_ => return Err("Service and method are required".to_string()),
};
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
let mut stream = grpc_handle
.lock()
.await
.server_streaming(&conn_id, uri, service, method, message)
.server_streaming(&conn.id, uri, &service, &method, &req.message)
.await
.unwrap();
#[derive(serde::Deserialize)]
enum GrpcMessage {
enum IncomingMsg {
Message(String),
Commit,
Cancel,
@@ -270,15 +351,15 @@ async fn cmd_grpc_server_streaming(
return;
}
match serde_json::from_str::<GrpcMessage>(ev.payload().unwrap()) {
Ok(GrpcMessage::Message(msg)) => {
match serde_json::from_str::<IncomingMsg>(ev.payload().unwrap()) {
Ok(IncomingMsg::Message(msg)) => {
println!("Received message: {}", msg);
}
Ok(GrpcMessage::Commit) => {
Ok(IncomingMsg::Commit) => {
println!("Received commit");
// TODO: Commit client streaming stream
}
Ok(GrpcMessage::Cancel) => {
Ok(IncomingMsg::Cancel) => {
println!("Received cancel");
cancelled_tx.send_replace(true);
}
@@ -289,19 +370,34 @@ async fn cmd_grpc_server_streaming(
}
};
let event_handler =
app_handle.listen_global(format!("grpc_client_msg_{}", conn_id).as_str(), cb);
app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
let grpc_listen = {
let db = db.clone();
let conn_id = conn.clone().id;
let app_handle = app_handle.clone();
let conn_id = conn_id.clone();
async move {
loop {
let req = req.clone();
let conn_id = conn_id.clone();
match stream.next().await {
Some(Ok(item)) => {
let item = serde_json::to_string_pretty(&item).unwrap();
app_handle
.emit_all(format!("grpc_server_msg_{}", &conn_id).as_str(), item)
.expect("Failed to emit");
let msg = upsert_grpc_message(
&db,
&GrpcMessage {
message: item,
workspace_id: req.workspace_id,
request_id: req.id,
connection_id: conn_id,
is_server: true,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
.expect("Failed to upsert message");
emit_side_effect(app_handle.clone(), "created_model", msg);
}
Some(Err(e)) => {
error!("gRPC stream error: {:?}", e);
@@ -328,7 +424,7 @@ async fn cmd_grpc_server_streaming(
app_handle.unlisten(event_handler);
});
Ok(conn_id)
Ok(conn)
}
#[tauri::command]
@@ -783,7 +879,7 @@ async fn cmd_duplicate_grpc_request(
let request = duplicate_grpc_request(db, id)
.await
.expect("Failed to duplicate grpc request");
emit_and_return(&window, "updated_model", request)
emit_and_return(&window, "created_model", request)
}
#[tauri::command]
@@ -823,7 +919,7 @@ async fn cmd_duplicate_http_request(
let request = duplicate_http_request(db, id)
.await
.expect("Failed to duplicate http request");
emit_and_return(&window, "updated_model", request)
emit_and_return(&window, "created_model", request)
}
#[tauri::command]
@@ -982,6 +1078,28 @@ async fn cmd_delete_environment(
emit_and_return(&window, "deleted_model", req)
}
#[tauri::command]
async fn cmd_list_grpc_connections(
request_id: &str,
db_state: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<GrpcConnection>, String> {
let db = &*db_state.lock().await;
list_grpc_connections(db, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_list_grpc_messages(
connection_id: &str,
db_state: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<GrpcMessage>, String> {
let db = &*db_state.lock().await;
list_grpc_messages(db, connection_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_list_grpc_requests(
workspace_id: &str,
@@ -990,8 +1108,7 @@ async fn cmd_list_grpc_requests(
let db = &*db_state.lock().await;
let requests = list_grpc_requests(db, workspace_id)
.await
.expect("Failed to find grpc requests");
// .map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;
Ok(requests)
}
@@ -1123,7 +1240,7 @@ async fn cmd_get_workspace(
}
#[tauri::command]
async fn cmd_list_responses(
async fn cmd_list_http_responses(
request_id: &str,
limit: Option<i64>,
db_state: State<'_, Mutex<Pool<Sqlite>>>,
@@ -1303,6 +1420,7 @@ fn main() {
cmd_delete_response,
cmd_delete_workspace,
cmd_duplicate_http_request,
cmd_duplicate_grpc_request,
cmd_export_data,
cmd_filter_response,
cmd_get_cookie_jar,
@@ -1324,7 +1442,9 @@ fn main() {
cmd_list_folders,
cmd_list_http_requests,
cmd_list_grpc_requests,
cmd_list_responses,
cmd_list_grpc_connections,
cmd_list_grpc_messages,
cmd_list_http_responses,
cmd_list_workspaces,
cmd_new_window,
cmd_send_ephemeral_request,

View File

@@ -228,6 +228,8 @@ pub struct GrpcMessage {
pub connection_id: String,
pub created_at: NaiveDateTime,
pub message: String,
pub is_server: bool,
pub is_info: bool,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -589,16 +591,17 @@ pub async fn get_grpc_connection(
pub async fn list_grpc_connections(
db: &Pool<Sqlite>,
workspace_id: &str,
request_id: &str,
) -> Result<Vec<GrpcConnection>, sqlx::Error> {
sqlx::query_as!(
GrpcConnection,
r#"
SELECT id, model, workspace_id, request_id, created_at, updated_at, service, method
FROM grpc_connections
WHERE workspace_id = ?
WHERE request_id = ?
ORDER BY created_at DESC
"#,
workspace_id,
request_id,
)
.fetch_all(db)
.await
@@ -615,30 +618,36 @@ pub async fn upsert_grpc_message(
sqlx::query!(
r#"
INSERT INTO grpc_messages (
id, workspace_id, request_id, connection_id, message
id, workspace_id, request_id, connection_id, message, is_server, is_info
)
VALUES (?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
message = excluded.message
message = excluded.message,
is_server = excluded.is_server,
is_info = excluded.is_info
"#,
id,
message.workspace_id,
message.request_id,
message.connection_id,
message.message,
message.is_server,
message.is_info,
)
.execute(db)
.await?;
crate::models::get_grpc_message(db, &id).await
get_grpc_message(db, &id).await
}
pub async fn get_grpc_message(db: &Pool<Sqlite>, id: &str) -> Result<GrpcMessage, sqlx::Error> {
sqlx::query_as!(
GrpcMessage,
r#"
SELECT id, model, workspace_id, request_id, connection_id, created_at, message
SELECT
id, model, workspace_id, request_id, connection_id, created_at, message,
is_server, is_info
FROM grpc_messages
WHERE id = ?
"#,
@@ -650,16 +659,18 @@ pub async fn get_grpc_message(db: &Pool<Sqlite>, id: &str) -> Result<GrpcMessage
pub async fn list_grpc_messages(
db: &Pool<Sqlite>,
workspace_id: &str,
connection_id: &str,
) -> Result<Vec<GrpcMessage>, sqlx::Error> {
sqlx::query_as!(
GrpcMessage,
r#"
SELECT id, model, workspace_id, request_id, connection_id, created_at, message
SELECT
id, model, workspace_id, request_id, connection_id, created_at, message,
is_server, is_info
FROM grpc_messages
WHERE workspace_id = ?
WHERE connection_id = ?
"#,
workspace_id,
connection_id,
)
.fetch_all(db)
.await

View File

@@ -3,20 +3,23 @@ import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { responsesQueryKey } from '../hooks/useResponses';
import { settingsQueryKey } from '../hooks/useSettings';
import { useSyncAppearance } from '../hooks/useSyncAppearance';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname';
@@ -49,7 +52,13 @@ export function GlobalHooks() {
payload.model === 'http_request'
? httpRequestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
? httpResponsesQueryKey(payload)
: payload.model === 'grpc_connection'
? grpcConnectionsQueryKey(payload)
: payload.model === 'grpc_message'
? grpcMessagesQueryKey(payload)
: payload.model === 'grpc_request'
? grpcRequestsQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
@@ -78,7 +87,13 @@ export function GlobalHooks() {
payload.model === 'http_request'
? httpRequestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
? httpResponsesQueryKey(payload)
: payload.model === 'grpc_connection'
? grpcConnectionsQueryKey(payload)
: payload.model === 'grpc_message'
? grpcMessagesQueryKey(payload)
: payload.model === 'grpc_request'
? grpcRequestsQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
@@ -113,11 +128,17 @@ export function GlobalHooks() {
if (shouldIgnoreModel(payload)) return;
if (payload.model === 'workspace') {
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(), removeById(payload));
queryClient.setQueryData(workspacesQueryKey(), removeById(payload));
} else if (payload.model === 'http_request') {
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey(payload), removeById(payload));
queryClient.setQueryData(httpRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'http_response') {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
queryClient.setQueryData(httpResponsesQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_request') {
queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_connection') {
queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_message') {
queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload));
} else if (payload.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
} else if (payload.model === 'cookie_jar') {

View File

@@ -5,12 +5,12 @@ import type { CSSProperties, FormEvent } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useAlert } from '../hooks/useAlert';
import type { GrpcMessage } from '../hooks/useGrpc';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useGrpcMessages } from '../hooks/useGrpcMessages';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Editor } from './core/Editor';
import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
@@ -30,9 +30,11 @@ export function GrpcConnectionLayout({ style }: Props) {
const activeRequest = useActiveRequest('grpc_request');
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
const alert = useAlert();
const [activeMessage, setActiveMessage] = useState<GrpcMessage | null>(null);
const [resp, setResp] = useState<string>('');
const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.id ?? null);
const connections = useGrpcConnections(activeRequest?.id ?? null);
const activeConnection = connections[0] ?? null;
const messages = useGrpcMessages(activeConnection?.id ?? null);
const activeMethod = useMemo(() => {
if (grpc.services == null || activeRequest == null) return null;
@@ -61,9 +63,9 @@ export function GrpcConnectionLayout({ style }: Props) {
if (activeMethod.clientStreaming && activeMethod.serverStreaming) {
await grpc.bidiStreaming.mutateAsync(activeRequest);
} else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) {
await grpc.serverStreaming.mutateAsync(activeRequest);
await grpc.serverStreaming.mutateAsync(activeRequest.id);
} else {
setResp(await grpc.unary.mutateAsync(activeRequest));
await grpc.unary.mutateAsync(activeRequest.id);
}
},
[activeMethod, activeRequest, alert, grpc.bidiStreaming, grpc.serverStreaming, grpc.unary],
@@ -127,8 +129,13 @@ export function GrpcConnectionLayout({ style }: Props) {
setPaneSize(entry.contentRect.width);
});
const activeMessage = useMemo(
() => messages.find((m) => m.id === activeMessageId) ?? null,
[activeMessageId, messages],
);
if (activeRequest == null) {
return;
return null;
}
return (
@@ -136,7 +143,7 @@ export function GrpcConnectionLayout({ style }: Props) {
name="grpc_layout"
className="p-3 gap-1.5"
style={style}
leftSlot={() => (
firstSlot={() => (
<VStack space={2}>
<div
ref={urlContainerEl}
@@ -218,7 +225,7 @@ export function GrpcConnectionLayout({ style }: Props) {
/>
</VStack>
)}
rightSlot={() =>
secondSlot={() =>
!grpc.unary.isLoading && (
<div
className={classNames(
@@ -231,20 +238,20 @@ export function GrpcConnectionLayout({ style }: Props) {
<Banner color="danger" className="m-2">
{grpc.unary.error}
</Banner>
) : (grpc.messages.value ?? []).length > 0 ? (
) : messages.length >= 0 ? (
<SplitLayout
name="grpc_messages2"
name="grpc_messages"
minHeightPx={20}
defaultRatio={0.25}
leftSlot={() => (
firstSlot={() => (
<div className="overflow-y-auto">
{...(grpc.messages.value ?? []).map((m, i) => (
{...messages.map((m) => (
<HStack
key={`${m.timestamp}::${m.message}::${i}`}
key={m.id}
space={2}
onClick={() => {
if (m === activeMessage) setActiveMessage(null);
else setActiveMessage(m);
if (m.id === activeMessageId) setActiveMessageId(null);
else setActiveMessageId(m.id);
}}
alignItems="center"
className={classNames(
@@ -254,29 +261,25 @@ export function GrpcConnectionLayout({ style }: Props) {
>
<Icon
className={
m.type === 'server'
m.isInfo
? 'text-gray-600'
: m.isServer
? 'text-blue-600'
: m.type === 'client'
? 'text-green-600'
: 'text-gray-600'
: 'text-green-600'
}
icon={
m.type === 'server'
? 'arrowBigDownDash'
: m.type === 'client'
? 'arrowBigUpDash'
: 'info'
m.isInfo ? 'info' : m.isServer ? 'arrowBigDownDash' : 'arrowBigUpDash'
}
/>
<div className="w-full truncate text-gray-800 text-2xs">{m.message}</div>
<div className="text-gray-600 text-2xs">
{format(m.timestamp, 'HH:mm:ss')}
{format(m.createdAt, 'HH:mm:ss')}
</div>
</HStack>
))}
</div>
)}
rightSlot={
secondSlot={
!activeMessage
? null
: () => (
@@ -293,15 +296,15 @@ export function GrpcConnectionLayout({ style }: Props) {
)
}
/>
) : resp ? (
<Editor
className="bg-gray-50 dark:bg-gray-100"
contentType="application/json"
defaultValue={resp}
readOnly
forceUpdateKey={resp}
/>
) : (
// ) : ? (
// <Editor
// readOnly
// className="bg-gray-50 dark:bg-gray-100"
// contentType="application/json"
// defaultValue={resp.message}
// forceUpdateKey={resp.id}
// />
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} />
)}
</div>

View File

@@ -14,10 +14,10 @@ export function HttpRequestLayout({ style }: Props) {
name="http_layout"
className="p-3 gap-1.5"
style={style}
leftSlot={({ orientation, style }) => (
firstSlot={({ orientation, style }) => (
<RequestPane style={style} fullHeight={orientation === 'horizontal'} />
)}
rightSlot={({ style }) => <ResponsePane style={style} />}
secondSlot={({ style }) => <ResponsePane style={style} />}
/>
);
}

View File

@@ -5,7 +5,7 @@ import { createGlobalState } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useResponseContentType } from '../hooks/useResponseContentType';
import { useResponses } from '../hooks/useResponses';
import { useHttpResponses } from '../hooks/useHttpResponses';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import type { HttpResponse } from '../lib/models';
import { isResponseLoading } from '../lib/models';
@@ -39,7 +39,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
const activeRequest = useActiveRequest();
const latestResponse = useLatestResponse(activeRequest?.id ?? null);
const responses = useResponses(activeRequest?.id ?? null);
const responses = useHttpResponses(activeRequest?.id ?? null);
const activeResponse: HttpResponse | null = pinnedResponseId
? responses.find((r) => r.id === pinnedResponseId) ?? null
: latestResponse ?? null;

View File

@@ -7,7 +7,6 @@ import { useKey, useKeyPressEvent } from 'react-use';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateFolder } from '../hooks/useCreateFolder';
@@ -15,7 +14,8 @@ import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useFolders } from '../hooks/useFolders';
import { useGrpcRequests } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey';
@@ -29,6 +29,7 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
@@ -58,14 +59,21 @@ interface TreeNode {
export function Sidebar({ className }: Props) {
const { hidden } = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequestId = useActiveRequestId();
const activeRequest = useActiveRequest();
const activeEnvironmentId = useActiveEnvironmentId();
const httpRequests = useHttpRequests();
const grpcRequests = useGrpcRequests();
const folders = useFolders();
const deleteAnyRequest = useDeleteAnyRequest();
const activeWorkspace = useActiveWorkspace();
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const routes = useAppRoutes();
const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -82,8 +90,12 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC,
});
useHotKey('http_request.duplicate', () => {
duplicateRequest.mutate();
useHotKey('http_request.duplicate', async () => {
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
});
const isCollapsed = useCallback(
@@ -146,9 +158,10 @@ export function Sidebar({ className }: Props) {
} = {},
) => {
const { forced, noFocusSidebar } = args;
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
const tree = forced?.tree ?? treeParentMap[activeRequest?.id ?? 'n/a'] ?? null;
const children = tree?.children ?? [];
const id = forced?.id ?? children.find((m) => m.item.id === activeRequestId)?.item.id ?? null;
const id =
forced?.id ?? children.find((m) => m.item.id === activeRequest?.id)?.item.id ?? null;
if (id == null) {
return;
}
@@ -160,7 +173,7 @@ export function Sidebar({ className }: Props) {
sidebarRef.current?.focus();
}
},
[activeRequestId, treeParentMap],
[activeRequest, treeParentMap],
);
const handleSelect = useCallback(
@@ -230,7 +243,7 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Enter', (e) => {
if (!hasFocus) return;
const selected = selectableRequests.find((r) => r.id === selectedId);
if (!selected || selected.id === activeRequestId || activeWorkspace == null) {
if (!selected || selected.id === activeRequest?.id || activeWorkspace == null) {
return;
}
@@ -541,11 +554,13 @@ const SidebarItem = forwardRef(function SidebarItem(
const createFolder = useCreateFolder();
const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(itemId);
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendRequest(itemId);
const sendManyRequests = useSendManyRequests();
const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateHttpRequest(itemId);
const updateHttpRequest = useUpdateHttpRequest(itemId);
const updateGrpcRequest = useUpdateGrpcRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder();
const prompt = usePrompt();
const [editing, setEditing] = useState<boolean>(false);
@@ -553,10 +568,15 @@ const SidebarItem = forwardRef(function SidebarItem(
const handleSubmitNameEdit = useCallback(
(el: HTMLInputElement) => {
updateRequest.mutate((r) => ({ ...r, name: el.value }));
if (activeRequest == null) return;
if (activeRequest.model === 'http_request') {
updateHttpRequest.mutate((r) => ({ ...r, name: el.value }));
} else if (activeRequest.model === 'grpc_request') {
updateGrpcRequest.mutate((r) => ({ ...r, name: el.value }));
}
setEditing(false);
},
[updateRequest],
[activeRequest, updateGrpcRequest, updateHttpRequest],
);
const handleFocus = useCallback((el: HTMLInputElement | null) => {
@@ -677,7 +697,9 @@ const SidebarItem = forwardRef(function SidebarItem(
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () => {
duplicateRequest.mutate();
itemModel === 'http_request'
? duplicateHttpRequest.mutate()
: duplicateGrpcRequest.mutate();
},
},
{

View File

@@ -34,7 +34,6 @@ export default function Workspace() {
const { setWidth, width, resetWidth } = useSidebarWidth();
const { hide, show, hidden } = useSidebarHidden();
const activeRequest = useActiveRequest();
const windowSize = useWindowSize();
const [floating, setFloating] = useState<boolean>(false);
const [isResizing, setIsResizing] = useState<boolean>(false);
@@ -47,7 +46,7 @@ export default function Workspace() {
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
if (shouldHide && !floating) {
setFloating(true);
hide();
hide().catch(console.error);
} else if (!shouldHide && floating) {
setFloating(false);
}
@@ -72,10 +71,10 @@ export default function Workspace() {
e.preventDefault(); // Prevent text selection and things
const newWidth = startWidth + (e.clientX - mouseStartX);
if (newWidth < 100) {
hide();
await hide();
resetWidth();
} else {
show();
await show();
setWidth(newWidth);
}
},

View File

@@ -16,8 +16,8 @@ interface SlotProps {
interface Props {
name: string;
leftSlot: (props: SlotProps) => ReactNode;
rightSlot: null | ((props: SlotProps) => ReactNode);
firstSlot: (props: SlotProps) => ReactNode;
secondSlot: null | ((props: SlotProps) => ReactNode);
style?: CSSProperties;
className?: string;
defaultRatio?: number;
@@ -33,8 +33,8 @@ const STACK_VERTICAL_WIDTH = 700;
export function SplitLayout({
style,
leftSlot,
rightSlot,
firstSlot,
secondSlot,
className,
name,
defaultRatio = 0.5,
@@ -54,7 +54,7 @@ export function SplitLayout({
null,
);
if (!rightSlot) {
if (!secondSlot) {
height = 0;
minHeightPx = 0;
}
@@ -145,8 +145,8 @@ export function SplitLayout({
return (
<div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}>
{leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
{rightSlot && (
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
{secondSlot && (
<>
<ResizeHandle
style={areaD}
@@ -158,7 +158,7 @@ export function SplitLayout({
side={vertical ? 'top' : 'left'}
justify="center"
/>
{rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
</>
)}
</div>

View File

@@ -7,7 +7,7 @@ import type { HttpRequest } from '../lib/models';
import { getHttpRequest } from '../lib/store';
import { useConfirm } from './useConfirm';
import { httpRequestsQueryKey } from './useHttpRequests';
import { responsesQueryKey } from './useResponses';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteAnyRequest() {
const queryClient = useQueryClient();
@@ -35,7 +35,7 @@ export function useDeleteAnyRequest() {
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId),
);

View File

@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '../lib/models';
import { responsesQueryKey } from './useResponses';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteResponse(id: string | null) {
const queryClient = useQueryClient();
@@ -12,7 +12,7 @@ export function useDeleteResponse(id: string | null) {
},
onSettled: () => trackEvent('HttpResponse', 'Delete'),
onSuccess: ({ requestId, id: responseId }) => {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
queryClient.setQueryData<HttpResponse[]>(httpResponsesQueryKey({ requestId }), (responses) =>
(responses ?? []).filter((response) => response.id !== responseId),
);
},

View File

@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import { responsesQueryKey } from './useResponses';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteResponses(requestId?: string) {
const queryClient = useQueryClient();
@@ -13,7 +13,7 @@ export function useDeleteResponses(requestId?: string) {
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
onSuccess: async () => {
if (requestId === undefined) return;
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []);
},
});
}

View File

@@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { GrpcRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { grpcRequestsQueryKey } from './useGrpcRequests';
export function useDuplicateGrpcRequest({
id,
navigateAfter,
}: {
id: string | null;
navigateAfter: boolean;
}) {
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const routes = useAppRoutes();
const queryClient = useQueryClient();
return useMutation<GrpcRequest, string>({
mutationFn: async () => {
if (id === null) throw new Error("Can't duplicate a null grpc request");
return invoke('cmd_duplicate_grpc_request', { id });
},
onSettled: () => trackEvent('GrpcRequest', 'Duplicate'),
onSuccess: async (request) => {
queryClient.setQueryData<GrpcRequest[]>(
grpcRequestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
if (navigateAfter && activeWorkspaceId !== null) {
routes.navigate('request', {
workspaceId: activeWorkspaceId,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
});
}
},
});
}

View File

@@ -7,7 +7,7 @@ import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useDuplicateRequest({
export function useDuplicateHttpRequest({
id,
navigateAfter,
}: {

View File

@@ -3,8 +3,7 @@ import { invoke } from '@tauri-apps/api';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { emit, listen } from '@tauri-apps/api/event';
import { useEffect, useRef, useState } from 'react';
import { tryFormatJson } from '../lib/formatters';
import type { GrpcRequest } from '../lib/models';
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
import { useKeyValue } from './useKeyValue';
interface ReflectResponseService {
@@ -12,12 +11,6 @@ interface ReflectResponseService {
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
}
export interface GrpcMessage {
message: string;
timestamp: string;
type: 'server' | 'client' | 'info';
}
export function useGrpc(url: string | null, requestId: string | null) {
const messages = useKeyValue<GrpcMessage[]>({
namespace: 'debug',
@@ -25,54 +18,30 @@ export function useGrpc(url: string | null, requestId: string | null) {
defaultValue: [],
});
const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null);
const unlisten = useRef<UnlistenFn | null>(null);
useEffect(() => {
setActiveConnectionId(null);
unlisten.current?.();
}, [requestId]);
const unary = useMutation<string, string, GrpcRequest>({
const unary = useMutation<GrpcMessage, string, string>({
mutationKey: ['grpc_unary', url],
mutationFn: async ({ service, method, message, url }) => {
if (url === null) throw new Error('No URL provided');
return (await invoke('cmd_grpc_call_unary', {
endpoint: url,
service,
method,
message,
})) as string;
mutationFn: async (id) => {
const message = (await invoke('cmd_grpc_call_unary', {
requestId: id,
})) as GrpcMessage;
await messages.set([message]);
console.log('MESSAGE', message);
return message;
},
});
const serverStreaming = useMutation<void, string, GrpcRequest>({
const serverStreaming = useMutation<void, string, string>({
mutationKey: ['grpc_server_streaming', url],
mutationFn: async ({ service, method, message, url }) => {
mutationFn: async (requestId) => {
if (url === null) throw new Error('No URL provided');
await messages.set([
{
type: 'client',
message: JSON.stringify(JSON.parse(message)),
timestamp: new Date().toISOString(),
},
]);
const id: string = await invoke('cmd_grpc_server_streaming', {
endpoint: url,
service,
method,
message,
});
unlisten.current = await listen(`grpc_server_msg_${id}`, async (event) => {
await messages.set((prev) => [
...prev,
{
message: tryFormatJson(event.payload as string, false),
timestamp: new Date().toISOString(),
type: 'server',
},
]);
});
setActiveConnectionId(id);
await messages.set([]);
const c = (await invoke('cmd_grpc_server_streaming', { requestId })) as GrpcConnection;
setActiveConnectionId(c.id);
},
});
@@ -86,20 +55,8 @@ export function useGrpc(url: string | null, requestId: string | null) {
method,
message,
});
await messages.set([
{ type: 'info', message: `Started connection ${id}`, timestamp: new Date().toISOString() },
]);
await messages.set([]);
setActiveConnectionId(id);
unlisten.current = await listen(`grpc_server_msg_${id}`, (event) => {
messages.set((prev) => [
...prev,
{
message: tryFormatJson(event.payload as string, false),
timestamp: new Date().toISOString(),
type: 'server',
},
]);
});
},
});
@@ -107,9 +64,10 @@ export function useGrpc(url: string | null, requestId: string | null) {
mutationKey: ['grpc_send', url],
mutationFn: async ({ message }: { message: string }) => {
if (activeConnectionId == null) throw new Error('No active connection');
await messages.set((m) => {
return [...m, { type: 'client', message, timestamp: new Date().toISOString() }];
});
await messages.set([]);
// await messages.set((m) => {
// return [...m, { type: 'client', message, timestamp: new Date().toISOString() }];
// });
await emit(`grpc_client_msg_${activeConnectionId}`, { Message: message });
},
});
@@ -118,12 +76,7 @@ export function useGrpc(url: string | null, requestId: string | null) {
mutationKey: ['grpc_cancel', url],
mutationFn: async () => {
setActiveConnectionId(null);
unlisten.current?.();
await emit('grpc_message_in', 'Cancel');
await messages.set((m) => [
...m,
{ type: 'info', message: 'Cancelled by client', timestamp: new Date().toISOString() },
]);
await emit(`grpc_client_msg_${activeConnectionId}`, 'Cancel');
},
});
@@ -141,7 +94,6 @@ export function useGrpc(url: string | null, requestId: string | null) {
bidiStreaming,
services: reflect.data,
cancel,
messages,
isStreaming: activeConnectionId !== null,
send,
};

View File

@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcConnection } from '../lib/models';
export function grpcConnectionsQueryKey({ requestId }: { requestId: string }) {
return ['grpc_connections', { requestId }];
}
export function useGrpcConnections(requestId: string | null) {
return (
useQuery<GrpcConnection[]>({
enabled: requestId !== null,
initialData: [],
queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('cmd_list_grpc_connections', {
requestId,
limit: 200,
})) as GrpcConnection[];
},
}).data ?? []
);
}

View File

@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcMessage } from '../lib/models';
export function grpcMessagesQueryKey({ connectionId }: { connectionId: string }) {
return ['grpc_messages', { connectionId }];
}
export function useGrpcMessages(connectionId: string | null) {
return (
useQuery<GrpcMessage[]>({
enabled: connectionId !== null,
initialData: [],
queryKey: grpcMessagesQueryKey({ connectionId: connectionId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('cmd_list_grpc_messages', {
connectionId,
limit: 200,
})) as GrpcMessage[];
},
}).data ?? []
);
}

View File

@@ -2,18 +2,18 @@ import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpResponse } from '../lib/models';
export function responsesQueryKey({ requestId }: { requestId: string }) {
export function httpResponsesQueryKey({ requestId }: { requestId: string }) {
return ['http_responses', { requestId }];
}
export function useResponses(requestId: string | null) {
export function useHttpResponses(requestId: string | null) {
return (
useQuery<HttpResponse[]>({
enabled: requestId !== null,
initialData: [],
queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('cmd_list_responses', { requestId, limit: 200 })) as HttpResponse[];
return (await invoke('cmd_list_http_responses', { requestId, limit: 200 })) as HttpResponse[];
},
}).data ?? []
);

View File

@@ -1,7 +1,7 @@
import type { HttpResponse } from '../lib/models';
import { useResponses } from './useResponses';
import { useHttpResponses } from './useHttpResponses';
export function useLatestResponse(requestId: string | null): HttpResponse | null {
const responses = useResponses(requestId);
const responses = useHttpResponses(requestId);
return responses[0] ?? null;
}

View File

@@ -12,7 +12,9 @@ export const AUTH_TYPE_BEARER = 'bearer';
export type Model =
| Settings
| Workspace
| GrpcConnection
| GrpcRequest
| GrpcMessage
| HttpRequest
| HttpResponse
| KeyValue
@@ -114,6 +116,24 @@ export interface GrpcRequest extends BaseModel {
message: string;
}
export interface GrpcMessage extends BaseModel {
readonly workspaceId: string;
readonly requestId: string;
readonly connectionId: string;
readonly model: 'grpc_message';
message: string;
isServer: boolean;
isInfo: boolean;
}
export interface GrpcConnection extends BaseModel {
readonly workspaceId: string;
readonly requestId: string;
readonly model: 'grpc_connection';
service: string;
method: string;
}
export interface HttpRequest extends BaseModel {
readonly workspaceId: string;
readonly model: 'http_request';