mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-12 01:14:27 +02:00
Server sent event response viewer (#126)
This commit is contained in:
Generated
+51
@@ -14,6 +14,7 @@
|
|||||||
"src-tauri/yaak_plugin_runtime",
|
"src-tauri/yaak_plugin_runtime",
|
||||||
"src-tauri/yaak_sync",
|
"src-tauri/yaak_sync",
|
||||||
"src-tauri/yaak_templates",
|
"src-tauri/yaak_templates",
|
||||||
|
"src-tauri/yaak_sse",
|
||||||
"src-web"
|
"src-web"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2650,6 +2651,33 @@
|
|||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.10.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
|
||||||
|
"integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.10.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.10.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
|
||||||
|
"integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/api": {
|
"node_modules/@tauri-apps/api": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.2.tgz",
|
||||||
@@ -3345,6 +3373,10 @@
|
|||||||
"resolved": "plugin-runtime",
|
"resolved": "plugin-runtime",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@yaakapp-internal/sse": {
|
||||||
|
"resolved": "src-tauri/yaak_sse",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@yaakapp-internal/template": {
|
"node_modules/@yaakapp-internal/template": {
|
||||||
"resolved": "src-tauri/yaak_templates",
|
"resolved": "src-tauri/yaak_templates",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -10922,6 +10954,19 @@
|
|||||||
"react-dom": "*"
|
"react-dom": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-virtuoso": {
|
||||||
|
"version": "4.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.10.4.tgz",
|
||||||
|
"integrity": "sha512-G/gprhTbK+lzMxoo/iStcZxVEGph/cIhc3WANEpt92RuMw+LiCZOmBfKoeoZOHlm/iyftTrDJhGaTCpxyucnkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16 || >=17 || >= 18",
|
||||||
|
"react-dom": ">=16 || >=17 || >= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -13738,6 +13783,10 @@
|
|||||||
"name": "@yaakapp-internal/plugin",
|
"name": "@yaakapp-internal/plugin",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
|
"src-tauri/yaak_sse": {
|
||||||
|
"name": "@yaakapp-internal/sse",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
"src-tauri/yaak_templates": {
|
"src-tauri/yaak_templates": {
|
||||||
"name": "@yaakapp-internal/template",
|
"name": "@yaakapp-internal/template",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
@@ -13757,6 +13806,7 @@
|
|||||||
"@react-hook/resize-observer": "^2.0.2",
|
"@react-hook/resize-observer": "^2.0.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tanstack/react-query": "^5.55.4",
|
"@tanstack/react-query": "^5.55.4",
|
||||||
|
"@tanstack/react-virtual": "^3.10.8",
|
||||||
"@tauri-apps/api": "^2.0.1",
|
"@tauri-apps/api": "^2.0.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
@@ -13788,6 +13838,7 @@
|
|||||||
"react-pdf": "^9.1.0",
|
"react-pdf": "^9.1.0",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
|
"react-virtuoso": "^4.10.4",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"xml-formatter": "^3.6.3"
|
"xml-formatter": "^3.6.3"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"src-tauri/yaak_plugin_runtime",
|
"src-tauri/yaak_plugin_runtime",
|
||||||
"src-tauri/yaak_sync",
|
"src-tauri/yaak_sync",
|
||||||
"src-tauri/yaak_templates",
|
"src-tauri/yaak_templates",
|
||||||
|
"src-tauri/yaak_sse",
|
||||||
"src-web"
|
"src-web"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+42
@@ -1623,6 +1623,21 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eventsource-client"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "git+https://github.com/yaakapp/rust-eventsource-client#e9e1e52421f11f0409179389b997aa49275a8461"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"hyper 0.14.30",
|
||||||
|
"hyper-rustls 0.24.2",
|
||||||
|
"hyper-timeout 0.4.1",
|
||||||
|
"log",
|
||||||
|
"pin-project",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exr"
|
name = "exr"
|
||||||
version = "1.72.0"
|
version = "1.72.0"
|
||||||
@@ -1807,6 +1822,21 @@ dependencies = [
|
|||||||
"new_debug_unreachable",
|
"new_debug_unreachable",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
@@ -1893,6 +1923,7 @@ version = "0.3.30"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -7823,6 +7854,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"cocoa 0.26.0",
|
"cocoa 0.26.0",
|
||||||
"datetime",
|
"datetime",
|
||||||
|
"eventsource-client",
|
||||||
"hex_color",
|
"hex_color",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"log",
|
"log",
|
||||||
@@ -7854,6 +7886,7 @@ dependencies = [
|
|||||||
"yaak_grpc",
|
"yaak_grpc",
|
||||||
"yaak_models",
|
"yaak_models",
|
||||||
"yaak_plugin_runtime",
|
"yaak_plugin_runtime",
|
||||||
|
"yaak_sse",
|
||||||
"yaak_templates",
|
"yaak_templates",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7926,6 +7959,15 @@ dependencies = [
|
|||||||
"yaak_models",
|
"yaak_models",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak_sse"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ts-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak_templates"
|
name = "yaak_templates"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models"]
|
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models", "yaak_sse"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "yaak-app"
|
name = "yaak-app"
|
||||||
@@ -30,6 +30,7 @@ yaak_grpc = { path = "yaak_grpc" }
|
|||||||
yaak_templates = { path = "yaak_templates" }
|
yaak_templates = { path = "yaak_templates" }
|
||||||
yaak_plugin_runtime = { workspace = true }
|
yaak_plugin_runtime = { workspace = true }
|
||||||
yaak_models = { workspace = true }
|
yaak_models = { workspace = true }
|
||||||
|
yaak_sse = { path = "yaak_sse" }
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
base64 = "0.22.0"
|
base64 = "0.22.0"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
@@ -59,6 +60,7 @@ uuid = "1.7.0"
|
|||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
|
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.13.0" }
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
yaak_models = { path = "yaak_models" }
|
yaak_models = { path = "yaak_models" }
|
||||||
|
|||||||
Generated
+1
-1
@@ -1 +1 @@
|
|||||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-internal-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||||
@@ -21,19 +21,21 @@ use tauri::{Manager, Runtime, WebviewWindow};
|
|||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::fs::{create_dir_all, File};
|
use tokio::fs::{create_dir_all, File};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tokio::sync::watch::Receiver;
|
use tokio::sync::watch::Receiver;
|
||||||
|
use tokio::sync::{oneshot, Mutex};
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
|
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
|
||||||
HttpResponseState,
|
HttpResponseState,
|
||||||
};
|
};
|
||||||
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
|
use yaak_models::queries::{
|
||||||
|
get_http_response, get_workspace, update_response_if_id, upsert_cookie_jar,
|
||||||
|
};
|
||||||
use yaak_plugin_runtime::events::{RenderPurpose, WindowContext};
|
use yaak_plugin_runtime::events::{RenderPurpose, WindowContext};
|
||||||
|
|
||||||
pub async fn send_http_request<R: Runtime>(
|
pub async fn send_http_request<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
request: &HttpRequest,
|
request: &HttpRequest,
|
||||||
response: &HttpResponse,
|
og_response: &HttpResponse,
|
||||||
environment: Option<Environment>,
|
environment: Option<Environment>,
|
||||||
cookie_jar: Option<CookieJar>,
|
cookie_jar: Option<CookieJar>,
|
||||||
cancelled_rx: &mut Receiver<bool>,
|
cancelled_rx: &mut Receiver<bool>,
|
||||||
@@ -47,6 +49,9 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
RenderPurpose::Send,
|
RenderPurpose::Send,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let response_id = og_response.id.clone();
|
||||||
|
let response = Arc::new(Mutex::new(og_response.clone()));
|
||||||
|
|
||||||
let rendered_request =
|
let rendered_request =
|
||||||
render_http_request(&request, &workspace, environment.as_ref(), &cb).await;
|
render_http_request(&request, &workspace, environment.as_ref(), &cb).await;
|
||||||
|
|
||||||
@@ -116,7 +121,7 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Ok(response_err(
|
return Ok(response_err(
|
||||||
response,
|
&*response.lock().await,
|
||||||
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
|
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
|
||||||
window,
|
window,
|
||||||
)
|
)
|
||||||
@@ -128,7 +133,7 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Ok(response_err(
|
return Ok(response_err(
|
||||||
response,
|
&*response.lock().await,
|
||||||
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
|
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
|
||||||
window,
|
window,
|
||||||
)
|
)
|
||||||
@@ -275,7 +280,7 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
request_builder = request_builder.body(f);
|
request_builder = request_builder.body(f);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Ok(response_err(response, e, window).await);
|
return Ok(response_err(&*response.lock().await, e, window).await);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
||||||
@@ -301,9 +306,12 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
match fs::read(file_path.clone()).await {
|
match fs::read(file_path.clone()).await {
|
||||||
Ok(f) => multipart::Part::bytes(f),
|
Ok(f) => multipart::Part::bytes(f),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Ok(
|
return Ok(response_err(
|
||||||
response_err(response, e.to_string(), window).await
|
&*response.lock().await,
|
||||||
);
|
e.to_string(),
|
||||||
|
window,
|
||||||
|
)
|
||||||
|
.await);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -351,7 +359,7 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
let sendable_req = match request_builder.build() {
|
let sendable_req = match request_builder.build() {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Ok(response_err(response, e.to_string(), window).await);
|
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -368,62 +376,60 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
Ok(r) = resp_rx => r,
|
Ok(r) = resp_rx => r,
|
||||||
_ = cancelled_rx.changed() => {
|
_ = cancelled_rx.changed() => {
|
||||||
debug!("Request cancelled");
|
debug!("Request cancelled");
|
||||||
return Ok(response_err(response, "Request was cancelled".to_string(), window).await);
|
return Ok(response_err(&*response.lock().await, "Request was cancelled".to_string(), window).await);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
let response = response.clone();
|
|
||||||
let cancelled_rx = cancelled_rx.clone();
|
let cancelled_rx = cancelled_rx.clone();
|
||||||
|
let response_id = response_id.clone();
|
||||||
|
let response = response.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let result = match raw_response {
|
match raw_response {
|
||||||
Ok(mut v) => {
|
Ok(mut v) => {
|
||||||
let mut response = response.clone();
|
let content_length = v.content_length();
|
||||||
let response_headers = v.headers().clone();
|
let response_headers = v.headers().clone();
|
||||||
response.elapsed_headers = start.elapsed().as_millis() as i32;
|
|
||||||
response.status = v.status().as_u16() as i32;
|
|
||||||
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
|
||||||
response.headers = response_headers
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| HttpResponseHeader {
|
|
||||||
name: k.as_str().to_string(),
|
|
||||||
value: v.to_str().unwrap_or_default().to_string(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
response.url = v.url().to_string();
|
|
||||||
response.remote_addr = v.remote_addr().map(|a| a.to_string());
|
|
||||||
response.version = match v.version() {
|
|
||||||
reqwest::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
|
|
||||||
reqwest::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
|
|
||||||
reqwest::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
|
|
||||||
reqwest::Version::HTTP_2 => Some("HTTP/2".to_string()),
|
|
||||||
reqwest::Version::HTTP_3 => Some("HTTP/3".to_string()),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let dir = window.app_handle().path().app_data_dir().unwrap();
|
let dir = window.app_handle().path().app_data_dir().unwrap();
|
||||||
let base_dir = dir.join("responses");
|
let base_dir = dir.join("responses");
|
||||||
create_dir_all(base_dir.clone())
|
create_dir_all(base_dir.clone())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create responses dir");
|
.expect("Failed to create responses dir");
|
||||||
let body_path = if response.id.is_empty() {
|
let body_path = if response_id.is_empty() {
|
||||||
base_dir.join(response.id.clone())
|
base_dir.join(response_id.clone())
|
||||||
} else {
|
} else {
|
||||||
base_dir.join(uuid::Uuid::new_v4().to_string())
|
base_dir.join(uuid::Uuid::new_v4().to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
response.body_path = Some(
|
{
|
||||||
body_path
|
let mut r = response.lock().await;
|
||||||
.to_str()
|
r.body_path = Some(body_path.to_str().unwrap().to_string());
|
||||||
.expect("Failed to get body path")
|
r.elapsed_headers = start.elapsed().as_millis() as i32;
|
||||||
.to_string(),
|
r.status = v.status().as_u16() as i32;
|
||||||
);
|
r.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
||||||
|
r.headers = response_headers
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| HttpResponseHeader {
|
||||||
|
name: k.as_str().to_string(),
|
||||||
|
value: v.to_str().unwrap_or_default().to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
r.url = v.url().to_string();
|
||||||
|
r.remote_addr = v.remote_addr().map(|a| a.to_string());
|
||||||
|
r.version = match v.version() {
|
||||||
|
reqwest::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
|
||||||
|
reqwest::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
|
||||||
|
reqwest::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
|
||||||
|
reqwest::Version::HTTP_2 => Some("HTTP/2".to_string()),
|
||||||
|
reqwest::Version::HTTP_3 => Some("HTTP/3".to_string()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
let content_length = v.content_length();
|
r.state = HttpResponseState::Connected;
|
||||||
response.state = HttpResponseState::Connected;
|
update_response_if_id(&window, &r)
|
||||||
response = update_response_if_id(&window, &response)
|
.await
|
||||||
.await
|
.expect("Failed to update response after connected");
|
||||||
.expect("Failed to update response after connected");
|
}
|
||||||
|
|
||||||
// Write body to FS
|
// Write body to FS
|
||||||
let mut f = File::options()
|
let mut f = File::options()
|
||||||
@@ -446,9 +452,10 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
f.write_all(&bytes).await.expect("Failed to write to file");
|
f.write_all(&bytes).await.expect("Failed to write to file");
|
||||||
f.flush().await.expect("Failed to flush file");
|
f.flush().await.expect("Failed to flush file");
|
||||||
written_bytes += bytes.len();
|
written_bytes += bytes.len();
|
||||||
response.elapsed = start.elapsed().as_millis() as i32;
|
let mut r = response.lock().await;
|
||||||
response.content_length = Some(written_bytes as i32);
|
r.elapsed = start.elapsed().as_millis() as i32;
|
||||||
response = update_response_if_id(&window, &response)
|
r.content_length = Some(written_bytes as i32);
|
||||||
|
update_response_if_id(&window, &r)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to update response");
|
.expect("Failed to update response");
|
||||||
}
|
}
|
||||||
@@ -456,21 +463,24 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
response = response_err(&response, e.to_string(), &window).await;
|
response_err(&*response.lock().await, e.to_string(), &window).await;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set final content length
|
// Set final content length
|
||||||
response.content_length = match content_length {
|
{
|
||||||
Some(l) => Some(l as i32),
|
let mut r = response.lock().await;
|
||||||
None => Some(written_bytes as i32),
|
r.content_length = match content_length {
|
||||||
|
Some(l) => Some(l as i32),
|
||||||
|
None => Some(written_bytes as i32),
|
||||||
|
};
|
||||||
|
r.state = HttpResponseState::Closed;
|
||||||
|
update_response_if_id(&window, &r)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update response");
|
||||||
};
|
};
|
||||||
response.state = HttpResponseState::Closed;
|
|
||||||
response = update_response_if_id(&window, &response)
|
|
||||||
.await
|
|
||||||
.expect("Failed to update response");
|
|
||||||
|
|
||||||
// Add cookie store if specified
|
// Add cookie store if specified
|
||||||
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
|
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
|
||||||
@@ -497,19 +507,29 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
error!("Failed to update cookie jar: {}", e);
|
error!("Failed to update cookie jar: {}", e);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
response
|
|
||||||
}
|
}
|
||||||
Err(e) => response_err(&response, e.to_string(), &window).await,
|
Err(e) => {
|
||||||
|
response_err(&*response.lock().await, e.to_string(), &window).await;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
done_tx.send(result.clone()).unwrap();
|
let r = response.lock().await.clone();
|
||||||
|
done_tx.send(r).unwrap();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(tokio::select! {
|
Ok(tokio::select! {
|
||||||
Ok(r) = done_rx => r,
|
Ok(r) = done_rx => r,
|
||||||
_ = cancelled_rx.changed() => {
|
_ = cancelled_rx.changed() => {
|
||||||
response_err(&response, "Request was cancelled".to_string(), &window).await
|
match get_http_response(window, response_id.as_str()).await {
|
||||||
|
Ok(mut r) => {
|
||||||
|
r.state = HttpResponseState::Closed;
|
||||||
|
update_response_if_id(&window, &r).await.expect("Failed to update response")
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
response_err(&*response.lock().await, "Ephemeral request was cancelled".to_string(), &window).await
|
||||||
|
}.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-5
@@ -3,7 +3,7 @@ extern crate core;
|
|||||||
extern crate objc;
|
extern crate objc;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs::{create_dir_all, read_to_string, File};
|
use std::fs::{create_dir_all, File};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -13,6 +13,7 @@ use std::{fs, panic};
|
|||||||
use base64::prelude::BASE64_STANDARD;
|
use base64::prelude::BASE64_STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use eventsource_client::{EventParser, SSE};
|
||||||
use fern::colors::ColoredLevelConfig;
|
use fern::colors::ColoredLevelConfig;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
@@ -27,6 +28,7 @@ use tauri::{Manager, WindowEvent};
|
|||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use tauri_plugin_log::{fern, Target, TargetKind};
|
use tauri_plugin_log::{fern, Target, TargetKind};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
use tokio::fs::read_to_string;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
||||||
@@ -69,6 +71,7 @@ use yaak_plugin_runtime::events::{
|
|||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use yaak_plugin_runtime::plugin_handle::PluginHandle;
|
use yaak_plugin_runtime::plugin_handle::PluginHandle;
|
||||||
|
use yaak_sse::sse::ServerSentEvent;
|
||||||
use yaak_templates::{Parser, Tokens};
|
use yaak_templates::{Parser, Tokens};
|
||||||
|
|
||||||
mod analytics;
|
mod analytics;
|
||||||
@@ -337,7 +340,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
&GrpcConnection {
|
&GrpcConnection {
|
||||||
elapsed: start.elapsed().as_millis() as i32,
|
elapsed: start.elapsed().as_millis() as i32,
|
||||||
error: Some(err.clone()),
|
error: Some(err.clone()),
|
||||||
state: GrpcConnectionState::Initialized,
|
state: GrpcConnectionState::Closed,
|
||||||
..conn.clone()
|
..conn.clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -797,7 +800,7 @@ async fn cmd_filter_response<R: Runtime>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = read_to_string(response.body_path.unwrap()).unwrap();
|
let body = read_to_string(response.body_path.unwrap()).await.unwrap();
|
||||||
|
|
||||||
// TODO: Have plugins register their own content type (regex?)
|
// TODO: Have plugins register their own content type (regex?)
|
||||||
plugin_manager
|
plugin_manager
|
||||||
@@ -806,14 +809,36 @@ async fn cmd_filter_response<R: Runtime>(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_get_sse_events(file_path: &str) -> Result<Vec<ServerSentEvent>, String> {
|
||||||
|
let body = fs::read(file_path).map_err(|e| e.to_string())?;
|
||||||
|
let mut p = EventParser::new();
|
||||||
|
p.process_bytes(body.into()).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
while let Some(e) = p.get_event() {
|
||||||
|
if let SSE::Event(e) = e {
|
||||||
|
events.push(ServerSentEvent {
|
||||||
|
event_type: e.event_type,
|
||||||
|
data: e.data,
|
||||||
|
id: e.id,
|
||||||
|
retry: e.retry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(events)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_import_data<R: Runtime>(
|
async fn cmd_import_data<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
plugin_manager: State<'_, PluginManager>,
|
plugin_manager: State<'_, PluginManager>,
|
||||||
file_path: &str,
|
file_path: &str,
|
||||||
) -> Result<WorkspaceExportResources, String> {
|
) -> Result<WorkspaceExportResources, String> {
|
||||||
let file =
|
let file = read_to_string(file_path)
|
||||||
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
|
.await
|
||||||
|
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
|
||||||
let file_contents = file.as_str();
|
let file_contents = file.as_str();
|
||||||
let (import_result, plugin_name) = plugin_manager
|
let (import_result, plugin_name) = plugin_manager
|
||||||
.import_data(&window, file_contents)
|
.import_data(&window, file_contents)
|
||||||
@@ -1801,6 +1826,7 @@ pub fn run() {
|
|||||||
])
|
])
|
||||||
.level_for("plugin_runtime", log::LevelFilter::Info)
|
.level_for("plugin_runtime", log::LevelFilter::Info)
|
||||||
.level_for("cookie_store", log::LevelFilter::Info)
|
.level_for("cookie_store", log::LevelFilter::Info)
|
||||||
|
.level_for("eventsource_client::event_parser", log::LevelFilter::Info)
|
||||||
.level_for("h2", log::LevelFilter::Info)
|
.level_for("h2", log::LevelFilter::Info)
|
||||||
.level_for("hyper", log::LevelFilter::Info)
|
.level_for("hyper", log::LevelFilter::Info)
|
||||||
.level_for("hyper_util", log::LevelFilter::Info)
|
.level_for("hyper_util", log::LevelFilter::Info)
|
||||||
@@ -1901,6 +1927,7 @@ pub fn run() {
|
|||||||
cmd_get_folder,
|
cmd_get_folder,
|
||||||
cmd_get_grpc_request,
|
cmd_get_grpc_request,
|
||||||
cmd_get_http_request,
|
cmd_get_http_request,
|
||||||
|
cmd_get_sse_events,
|
||||||
cmd_get_key_value,
|
cmd_get_key_value,
|
||||||
cmd_get_settings,
|
cmd_get_settings,
|
||||||
cmd_get_workspace,
|
cmd_get_workspace,
|
||||||
|
|||||||
@@ -488,6 +488,7 @@ pub async fn upsert_grpc_connection<R: Runtime>(
|
|||||||
GrpcConnectionIden::Method,
|
GrpcConnectionIden::Method,
|
||||||
GrpcConnectionIden::Elapsed,
|
GrpcConnectionIden::Elapsed,
|
||||||
GrpcConnectionIden::Status,
|
GrpcConnectionIden::Status,
|
||||||
|
GrpcConnectionIden::State,
|
||||||
GrpcConnectionIden::Error,
|
GrpcConnectionIden::Error,
|
||||||
GrpcConnectionIden::Trailers,
|
GrpcConnectionIden::Trailers,
|
||||||
GrpcConnectionIden::Url,
|
GrpcConnectionIden::Url,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "yaak_sse"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
|
serde_json = "1.0.122"
|
||||||
|
ts-rs = { version = "10.0.0", features = ["serde-json-impl"] }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ServerSentEvent = { eventType: string, data: string, id: string | null, retry: bigint | null, };
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './bindings/sse';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "@yaakapp-internal/sse",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.ts"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod sse;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "sse.ts")]
|
||||||
|
pub struct ServerSentEvent {
|
||||||
|
pub event_type: String,
|
||||||
|
pub data: String,
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub retry: Option<u64>,
|
||||||
|
}
|
||||||
@@ -188,7 +188,7 @@ function EventRow({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
|
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
|
||||||
'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded',
|
'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded',
|
||||||
isActive && '!bg-surface-highlight !text',
|
isActive && '!bg-surface-highlight !text-text',
|
||||||
'text-text-subtle hover:text',
|
'text-text-subtle hover:text',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { ResponseHeaders } from './ResponseHeaders';
|
|||||||
import { ResponseInfo } from './ResponseInfo';
|
import { ResponseInfo } from './ResponseInfo';
|
||||||
import { AudioViewer } from './responseViewers/AudioViewer';
|
import { AudioViewer } from './responseViewers/AudioViewer';
|
||||||
import { CsvViewer } from './responseViewers/CsvViewer';
|
import { CsvViewer } from './responseViewers/CsvViewer';
|
||||||
|
import { EventStreamViewer } from './responseViewers/EventStreamViewer';
|
||||||
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
|
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
|
||||||
import { ImageViewer } from './responseViewers/ImageViewer';
|
import { ImageViewer } from './responseViewers/ImageViewer';
|
||||||
import { PdfViewer } from './responseViewers/PdfViewer';
|
import { PdfViewer } from './responseViewers/PdfViewer';
|
||||||
@@ -163,15 +164,17 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
|||||||
<div className="pb-2 h-full">
|
<div className="pb-2 h-full">
|
||||||
<EmptyStateText>Empty Body</EmptyStateText>
|
<EmptyStateText>Empty Body</EmptyStateText>
|
||||||
</div>
|
</div>
|
||||||
) : contentType?.startsWith('image') ? (
|
) : contentType?.match(/^text\/event-stream$/i) && viewMode === 'pretty' ? (
|
||||||
|
<EventStreamViewer response={activeResponse} />
|
||||||
|
) : contentType?.match(/^image/i) ? (
|
||||||
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
|
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
|
||||||
) : contentType?.startsWith('audio') ? (
|
) : contentType?.match(/^audio/i) ? (
|
||||||
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
|
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
|
||||||
) : contentType?.startsWith('video') ? (
|
) : contentType?.match(/^video/i) ? (
|
||||||
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
|
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
|
||||||
) : contentType?.match(/pdf/) ? (
|
) : contentType?.match(/pdf/i) ? (
|
||||||
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
|
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
|
||||||
) : contentType?.match(/csv|tab-separated/) ? (
|
) : contentType?.match(/csv|tab-separated/i) ? (
|
||||||
<CsvViewer className="pb-2" response={activeResponse} />
|
<CsvViewer className="pb-2" response={activeResponse} />
|
||||||
) : (
|
) : (
|
||||||
// ) : viewMode === 'pretty' && contentType?.includes('json') ? (
|
// ) : viewMode === 'pretty' && contentType?.includes('json') ? (
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
|
|||||||
<code
|
<code
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'select-text cursor-text',
|
|
||||||
'font-mono text-shrink bg-surface-highlight border border-border-subtle',
|
'font-mono text-shrink bg-surface-highlight border border-border-subtle',
|
||||||
'px-1.5 py-0.5 rounded text shadow-inner break-words',
|
'px-1.5 py-0.5 rounded text shadow-inner break-words',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
|
import type { ServerSentEvent } from '@yaakapp-internal/sse';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import React, { Fragment, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
|
||||||
|
import { isJSON } from '../../lib/contentType';
|
||||||
|
import { tryFormatJson } from '../../lib/formatters';
|
||||||
|
import { Button } from '../core/Button';
|
||||||
|
import { Editor } from '../core/Editor';
|
||||||
|
import { Icon } from '../core/Icon';
|
||||||
|
import { InlineCode } from '../core/InlineCode';
|
||||||
|
import { Separator } from '../core/Separator';
|
||||||
|
import { SplitLayout } from '../core/SplitLayout';
|
||||||
|
import { HStack, VStack } from '../core/Stacks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
response: HttpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventStreamViewer({ response }: Props) {
|
||||||
|
return (
|
||||||
|
<Fragment
|
||||||
|
key={response.id} // force a refresh when the response changes
|
||||||
|
>
|
||||||
|
<ActualEventStreamViewer response={response} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActualEventStreamViewer({ response }: Props) {
|
||||||
|
const [showLarge, setShowLarge] = useState<boolean>(false);
|
||||||
|
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
||||||
|
const [activeEventIndex, setActiveEventIndex] = useState<number | null>(null);
|
||||||
|
const events = useResponseBodyEventSource(response);
|
||||||
|
const activeEvent = useMemo(
|
||||||
|
() => (activeEventIndex == null ? null : events.data?.[activeEventIndex]),
|
||||||
|
[activeEventIndex, events],
|
||||||
|
);
|
||||||
|
|
||||||
|
const language = useMemo<'text' | 'json'>(() => {
|
||||||
|
if (!activeEvent?.data) return 'text';
|
||||||
|
return isJSON(activeEvent?.data) ? 'json' : 'text';
|
||||||
|
}, [activeEvent?.data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SplitLayout
|
||||||
|
layout="vertical"
|
||||||
|
name="grpc_events"
|
||||||
|
defaultRatio={0.4}
|
||||||
|
minHeightPx={20}
|
||||||
|
firstSlot={() => (
|
||||||
|
<EventStreamEventsVirtual
|
||||||
|
events={events.data ?? []}
|
||||||
|
activeEventIndex={activeEventIndex}
|
||||||
|
setActiveEventIndex={setActiveEventIndex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
secondSlot={
|
||||||
|
activeEvent
|
||||||
|
? () => (
|
||||||
|
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
|
<div className="pb-3 px-2">
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
<div className="pl-2 overflow-y-auto">
|
||||||
|
<div className="mb-2 select-text cursor-text font-semibold">Message Received</div>
|
||||||
|
{!showLarge && activeEvent.data.length > 1000 * 1000 ? (
|
||||||
|
<VStack space={2} className="italic text-text-subtlest">
|
||||||
|
Message previews larger than 1MB are hidden
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowingLarge(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowLarge(true);
|
||||||
|
setShowingLarge(false);
|
||||||
|
}, 500);
|
||||||
|
}}
|
||||||
|
isLoading={showingLarge}
|
||||||
|
color="secondary"
|
||||||
|
variant="border"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Try Showing
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<Editor
|
||||||
|
readOnly
|
||||||
|
forceUpdateKey={activeEvent.id ?? activeEvent.data}
|
||||||
|
defaultValue={tryFormatJson(activeEvent.data)}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventStreamEventsVirtual({
|
||||||
|
events,
|
||||||
|
activeEventIndex,
|
||||||
|
setActiveEventIndex,
|
||||||
|
}: {
|
||||||
|
events: ServerSentEvent[];
|
||||||
|
activeEventIndex: number | null;
|
||||||
|
setActiveEventIndex: (eventId: number | null) => void;
|
||||||
|
}) {
|
||||||
|
// The scrollable element for your list
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// The virtualizer
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: events.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 28, // react-virtual requires a height, so we'll give it one
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={parentRef} className="overflow-y-auto">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
|
||||||
|
const event = events[virtualItem.index]!;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={virtualItem.key}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: `${virtualItem.size}px`,
|
||||||
|
transform: `translateY(${virtualItem.start}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EventStreamEvent
|
||||||
|
event={event}
|
||||||
|
isActive={virtualItem.index === activeEventIndex}
|
||||||
|
onClick={() => {
|
||||||
|
if (virtualItem.index === activeEventIndex) setActiveEventIndex(null);
|
||||||
|
else setActiveEventIndex(virtualItem.index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventStreamEvent({
|
||||||
|
onClick,
|
||||||
|
isActive,
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
isActive: boolean;
|
||||||
|
event: ServerSentEvent;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
'w-full grid grid-cols-[auto_auto_minmax(0,3fr)] gap-2 items-center text-left',
|
||||||
|
'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded',
|
||||||
|
isActive && '!bg-surface-highlight !text-text',
|
||||||
|
'text-text-subtle hover:text',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className={classNames('text-info')} title="Server Message" icon="arrow_big_down_dash" />
|
||||||
|
<HStack space={1.5} className="text-sm">
|
||||||
|
{event.eventType && (
|
||||||
|
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
|
||||||
|
{event.eventType}
|
||||||
|
</InlineCode>
|
||||||
|
)}
|
||||||
|
{event.id && (
|
||||||
|
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
|
||||||
|
{event.id}
|
||||||
|
</InlineCode>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<div className={classNames('w-full truncate text-xs')}>{event.data.slice(0, 1000)}</div>
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
|
import type { ServerSentEvent } from '@yaakapp-internal/sse';
|
||||||
|
import { getResponseBodyEventSource } from '../lib/responseBody';
|
||||||
|
|
||||||
|
export function useResponseBodyEventSource(response: HttpResponse) {
|
||||||
|
return useQuery<ServerSentEvent[]>({
|
||||||
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
|
queryKey: ['response-body-event-source', response.id, response.contentLength],
|
||||||
|
queryFn: () => getResponseBodyEventSource(response),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -35,3 +35,15 @@ function detectFromContent(
|
|||||||
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isJSON(content: string | null | undefined): boolean {
|
||||||
|
if (typeof content !== 'string') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(content)
|
||||||
|
return true;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+23
-15
@@ -1,26 +1,34 @@
|
|||||||
import { readFile } from '@tauri-apps/plugin-fs';
|
import { readFile } from '@tauri-apps/plugin-fs';
|
||||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
|
import type { ServerSentEvent } from '@yaakapp-internal/sse';
|
||||||
import { getCharsetFromContentType } from './model_util';
|
import { getCharsetFromContentType } from './model_util';
|
||||||
|
import { invokeCmd } from './tauri';
|
||||||
|
|
||||||
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
|
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
|
||||||
if (response.bodyPath) {
|
if (!response.bodyPath) return null;
|
||||||
const bytes = await readFile(response.bodyPath);
|
|
||||||
const charset = getCharsetFromContentType(response.headers);
|
|
||||||
|
|
||||||
try {
|
const bytes = await readFile(response.bodyPath);
|
||||||
return new TextDecoder(charset ?? 'utf-8', { fatal: true }).decode(bytes);
|
const charset = getCharsetFromContentType(response.headers);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (err) {
|
try {
|
||||||
// Failed to decode as text, so return null
|
return new TextDecoder(charset ?? 'utf-8', { fatal: true }).decode(bytes);
|
||||||
return null;
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
}
|
} catch (err) {
|
||||||
|
// Failed to decode as text, so return null
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
|
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
|
||||||
if (response.bodyPath) {
|
if (!response.bodyPath) return null;
|
||||||
return readFile(response.bodyPath);
|
return readFile(response.bodyPath);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
export async function getResponseBodyEventSource(
|
||||||
|
response: HttpResponse,
|
||||||
|
): Promise<ServerSentEvent[]> {
|
||||||
|
if (!response.bodyPath) return [];
|
||||||
|
return invokeCmd<ServerSentEvent[]>('cmd_get_sse_events', {
|
||||||
|
filePath: response.bodyPath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type TauriCmd =
|
|||||||
| 'cmd_get_folder'
|
| 'cmd_get_folder'
|
||||||
| 'cmd_get_grpc_request'
|
| 'cmd_get_grpc_request'
|
||||||
| 'cmd_get_http_request'
|
| 'cmd_get_http_request'
|
||||||
|
| 'cmd_get_sse_events'
|
||||||
| 'cmd_get_key_value'
|
| 'cmd_get_key_value'
|
||||||
| 'cmd_get_settings'
|
| 'cmd_get_settings'
|
||||||
| 'cmd_get_workspace'
|
| 'cmd_get_workspace'
|
||||||
|
|||||||
+5
-13
@@ -19,22 +19,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Disable user selection to make it more "app-like" */
|
/* Disable user selection to make it more "app-like" */
|
||||||
h1,
|
:not(a),
|
||||||
h2,
|
:not(input):not(textarea),
|
||||||
h3,
|
:not(input):not(textarea)::after,
|
||||||
h4,
|
:not(input):not(textarea)::before {
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
p,
|
|
||||||
blockquote,
|
|
||||||
label,
|
|
||||||
code,
|
|
||||||
pre,
|
|
||||||
li {
|
|
||||||
@apply select-none cursor-default;
|
@apply select-none cursor-default;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea {
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
@apply text-placeholder;
|
@apply text-placeholder;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@react-hook/resize-observer": "^2.0.2",
|
"@react-hook/resize-observer": "^2.0.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tanstack/react-query": "^5.55.4",
|
"@tanstack/react-query": "^5.55.4",
|
||||||
|
"@tanstack/react-virtual": "^3.10.8",
|
||||||
"@tauri-apps/api": "^2.0.1",
|
"@tauri-apps/api": "^2.0.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user