mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 15:21:23 +02:00
Url parameters for websocket URLs
This commit is contained in:
12
src-tauri/Cargo.lock
generated
12
src-tauri/Cargo.lock
generated
@@ -7619,9 +7619,9 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"urlencoding",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaak-grpc",
|
"yaak-grpc",
|
||||||
|
"yaak-http",
|
||||||
"yaak-license",
|
"yaak-license",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
"yaak-plugins",
|
"yaak-plugins",
|
||||||
@@ -7659,6 +7659,15 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-http"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
"urlencoding",
|
||||||
|
"yaak-models",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-license"
|
name = "yaak-license"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -7774,6 +7783,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
|
"yaak-http",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
"yaak-plugins",
|
"yaak-plugins",
|
||||||
"yaak-templates",
|
"yaak-templates",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"yaak-grpc",
|
"yaak-grpc",
|
||||||
|
"yaak-http",
|
||||||
"yaak-license",
|
"yaak-license",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
"yaak-plugins",
|
"yaak-plugins",
|
||||||
@@ -70,9 +71,9 @@ tauri-plugin-window-state = "2.2.1"
|
|||||||
tokio = { version = "1.43.0", features = ["sync"] }
|
tokio = { version = "1.43.0", features = ["sync"] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
ts-rs = { workspace = true }
|
ts-rs = { workspace = true }
|
||||||
urlencoding = "2.1.3"
|
|
||||||
uuid = "1.12.1"
|
uuid = "1.12.1"
|
||||||
yaak-grpc = { path = "yaak-grpc" }
|
yaak-grpc = { path = "yaak-grpc" }
|
||||||
|
yaak-http = { workspace = true }
|
||||||
yaak-license = { path = "yaak-license" }
|
yaak-license = { path = "yaak-license" }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
yaak-plugins = { workspace = true }
|
yaak-plugins = { workspace = true }
|
||||||
@@ -92,5 +93,6 @@ thiserror = "2.0.3"
|
|||||||
ts-rs = "10.0.0"
|
ts-rs = "10.0.0"
|
||||||
yaak-models = { path = "yaak-models" }
|
yaak-models = { path = "yaak-models" }
|
||||||
yaak-plugins = { path = "yaak-plugins" }
|
yaak-plugins = { path = "yaak-plugins" }
|
||||||
|
yaak-http = { path = "yaak-http" }
|
||||||
yaak-sse = { path = "yaak-sse" }
|
yaak-sse = { path = "yaak-sse" }
|
||||||
yaak-templates = { path = "yaak-templates" }
|
yaak-templates = { path = "yaak-templates" }
|
||||||
|
|||||||
2
src-tauri/gen/schemas/acl-manifests.json
generated
2
src-tauri/gen/schemas/acl-manifests.json
generated
File diff suppressed because one or more lines are too long
10
src-tauri/gen/schemas/desktop-schema.json
generated
10
src-tauri/gen/schemas/desktop-schema.json
generated
@@ -5517,6 +5517,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "yaak-ws:allow-delete-request"
|
"const": "yaak-ws:allow-delete-request"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the duplicate_request command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "yaak-ws:allow-duplicate-request"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the list_connections command without any pre-configured scope.",
|
"description": "Enables the list_connections command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5587,6 +5592,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "yaak-ws:deny-delete-request"
|
"const": "yaak-ws:deny-delete-request"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the duplicate_request command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "yaak-ws:deny-duplicate-request"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the list_connections command without any pre-configured scope.",
|
"description": "Denies the list_connections command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
10
src-tauri/gen/schemas/macOS-schema.json
generated
10
src-tauri/gen/schemas/macOS-schema.json
generated
@@ -5517,6 +5517,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "yaak-ws:allow-delete-request"
|
"const": "yaak-ws:allow-delete-request"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the duplicate_request command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "yaak-ws:allow-duplicate-request"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the list_connections command without any pre-configured scope.",
|
"description": "Enables the list_connections command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5587,6 +5592,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "yaak-ws:deny-delete-request"
|
"const": "yaak-ws:deny-delete-request"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the duplicate_request command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "yaak-ws:deny-duplicate-request"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the list_connections command without any pre-configured scope.",
|
"description": "Denies the list_connections command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ pub async fn track_event<R: Runtime>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_os() -> &'static str {
|
pub fn get_os() -> &'static str {
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
"windows"
|
"windows"
|
||||||
} else if cfg!(target_os = "macos") {
|
} else if cfg!(target_os = "macos") {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use crate::analytics::get_num_launches;
|
use crate::analytics::{get_num_launches, get_os};
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
@@ -66,8 +66,9 @@ impl YaakNotifier {
|
|||||||
let req = reqwest::Client::default()
|
let req = reqwest::Client::default()
|
||||||
.request(Method::GET, "https://notify.yaak.app/notifications")
|
.request(Method::GET, "https://notify.yaak.app/notifications")
|
||||||
.query(&[
|
.query(&[
|
||||||
("version", info.version.to_string()),
|
("version", info.version.to_string().as_str()),
|
||||||
("launches", num_launches.to_string()),
|
("launches", num_launches.to_string().as_str()),
|
||||||
|
("platform", get_os())
|
||||||
]);
|
]);
|
||||||
let resp = req.send().await.map_err(|e| e.to_string())?;
|
let resp = req.send().await.map_err(|e| e.to_string())?;
|
||||||
if resp.status() != 200 {
|
if resp.status() != 200 {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use yaak_http::apply_path_placeholders;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
Environment, GrpcMetadataEntry, GrpcRequest, HttpRequest,
|
Environment, GrpcMetadataEntry, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
|
||||||
HttpRequestHeader, HttpUrlParameter,
|
|
||||||
};
|
};
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
|
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
|
||||||
@@ -99,17 +99,18 @@ pub async fn render_http_request<T: TemplateCallback>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let url = render(r.url.clone().as_str(), vars, cb).await;
|
let url = render(r.url.clone().as_str(), vars, cb).await;
|
||||||
let req = HttpRequest {
|
|
||||||
|
// This doesn't fit perfectly with the concept of "rendering" but it kind of does
|
||||||
|
let (url, url_parameters) = apply_path_placeholders(&url, url_parameters);
|
||||||
|
|
||||||
|
HttpRequest {
|
||||||
url,
|
url,
|
||||||
url_parameters,
|
url_parameters,
|
||||||
headers,
|
headers,
|
||||||
body,
|
body,
|
||||||
authentication,
|
authentication,
|
||||||
..r.to_owned()
|
..r.to_owned()
|
||||||
};
|
}
|
||||||
|
|
||||||
// This doesn't fit perfectly with the concept of "rendering" but it kind of does
|
|
||||||
apply_path_placeholders(req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render<T: TemplateCallback>(
|
pub async fn render<T: TemplateCallback>(
|
||||||
@@ -119,180 +120,3 @@ pub async fn render<T: TemplateCallback>(
|
|||||||
) -> String {
|
) -> String {
|
||||||
parse_and_render(template, vars, cb).await
|
parse_and_render(template, vars, cb).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
|
|
||||||
if !p.enabled {
|
|
||||||
return url.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.name.starts_with(":") {
|
|
||||||
return url.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
|
|
||||||
let result = re
|
|
||||||
.replace_all(url, |cap: ®ex::Captures| {
|
|
||||||
format!(
|
|
||||||
"{}{}{}",
|
|
||||||
cap[1].to_string(),
|
|
||||||
urlencoding::encode(p.value.as_str()),
|
|
||||||
cap[2].to_string()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_owned();
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_path_placeholders(rendered_request: HttpRequest) -> HttpRequest {
|
|
||||||
let mut url = rendered_request.url.to_owned();
|
|
||||||
let mut url_parameters = Vec::new();
|
|
||||||
for p in rendered_request.url_parameters.clone() {
|
|
||||||
if !p.enabled || p.name.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace path parameters with values from URL parameters
|
|
||||||
let old_url_string = url.clone();
|
|
||||||
url = replace_path_placeholder(&p, url.as_str());
|
|
||||||
|
|
||||||
// Remove as param if it modified the URL
|
|
||||||
if old_url_string == url {
|
|
||||||
url_parameters.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut request = rendered_request.clone();
|
|
||||||
request.url_parameters = url_parameters;
|
|
||||||
request.url = url;
|
|
||||||
request
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod placeholder_tests {
|
|
||||||
use crate::render::{apply_path_placeholders, replace_path_placeholder};
|
|
||||||
use yaak_models::models::{HttpRequest, HttpUrlParameter};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn placeholder_middle() {
|
|
||||||
let p = HttpUrlParameter {
|
|
||||||
name: ":foo".into(),
|
|
||||||
value: "xxx".into(),
|
|
||||||
enabled: true,
|
|
||||||
id: None,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
|
|
||||||
"https://example.com/xxx/bar",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn placeholder_end() {
|
|
||||||
let p = HttpUrlParameter {
|
|
||||||
name: ":foo".into(),
|
|
||||||
value: "xxx".into(),
|
|
||||||
enabled: true,
|
|
||||||
id: None,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
|
||||||
"https://example.com/xxx",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn placeholder_query() {
|
|
||||||
let p = HttpUrlParameter {
|
|
||||||
name: ":foo".into(),
|
|
||||||
value: "xxx".into(),
|
|
||||||
enabled: true,
|
|
||||||
id: None,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
|
|
||||||
"https://example.com/xxx?:foo",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn placeholder_missing() {
|
|
||||||
let p = HttpUrlParameter {
|
|
||||||
enabled: true,
|
|
||||||
name: "".to_string(),
|
|
||||||
value: "".to_string(),
|
|
||||||
id: None,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
replace_path_placeholder(&p, "https://example.com/:missing"),
|
|
||||||
"https://example.com/:missing",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn placeholder_disabled() {
|
|
||||||
let p = HttpUrlParameter {
|
|
||||||
enabled: false,
|
|
||||||
name: ":foo".to_string(),
|
|
||||||
value: "xxx".to_string(),
|
|
||||||
id: None,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
|
||||||
"https://example.com/:foo",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn placeholder_prefix() {
|
|
||||||
let p = HttpUrlParameter {
|
|
||||||
name: ":foo".into(),
|
|
||||||
value: "xxx".into(),
|
|
||||||
enabled: true,
|
|
||||||
id: None,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
replace_path_placeholder(&p, "https://example.com/:foooo"),
|
|
||||||
"https://example.com/:foooo",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn placeholder_encode() {
|
|
||||||
let p = HttpUrlParameter {
|
|
||||||
name: ":foo".into(),
|
|
||||||
value: "Hello World".into(),
|
|
||||||
enabled: true,
|
|
||||||
id: None,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
|
||||||
"https://example.com/Hello%20World",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn apply_placeholder() {
|
|
||||||
let result = apply_path_placeholders(HttpRequest {
|
|
||||||
url: "example.com/:a/bar".to_string(),
|
|
||||||
url_parameters: vec![
|
|
||||||
HttpUrlParameter {
|
|
||||||
name: "b".to_string(),
|
|
||||||
value: "bbb".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
id: None,
|
|
||||||
},
|
|
||||||
HttpUrlParameter {
|
|
||||||
name: ":a".to_string(),
|
|
||||||
value: "aaa".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
id: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
assert_eq!(result.url, "example.com/aaa/bar");
|
|
||||||
assert_eq!(result.url_parameters.len(), 1);
|
|
||||||
assert_eq!(result.url_parameters[0].name, "b");
|
|
||||||
assert_eq!(result.url_parameters[0].value, "bbb");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
10
src-tauri/yaak-http/Cargo.toml
Normal file
10
src-tauri/yaak-http/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "yaak-http"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
yaak-models = { workspace = true }
|
||||||
|
regex = "1.11.0"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
183
src-tauri/yaak-http/src/lib.rs
Normal file
183
src-tauri/yaak-http/src/lib.rs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
use yaak_models::models::HttpUrlParameter;
|
||||||
|
|
||||||
|
pub fn apply_path_placeholders(
|
||||||
|
url: &str,
|
||||||
|
parameters: Vec<HttpUrlParameter>,
|
||||||
|
) -> (String, Vec<HttpUrlParameter>) {
|
||||||
|
let mut new_parameters = Vec::new();
|
||||||
|
|
||||||
|
let mut url = url.to_string();
|
||||||
|
for p in parameters {
|
||||||
|
if !p.enabled || p.name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace path parameters with values from URL parameters
|
||||||
|
let old_url_string = url.clone();
|
||||||
|
url = replace_path_placeholder(&p, url.as_str());
|
||||||
|
|
||||||
|
// Remove as param if it modified the URL
|
||||||
|
if old_url_string == *url {
|
||||||
|
new_parameters.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(url, new_parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
|
||||||
|
if !p.enabled {
|
||||||
|
return url.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.name.starts_with(":") {
|
||||||
|
return url.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
|
||||||
|
let result = re
|
||||||
|
.replace_all(url, |cap: ®ex::Captures| {
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
cap[1].to_string(),
|
||||||
|
urlencoding::encode(p.value.as_str()),
|
||||||
|
cap[2].to_string()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_owned();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod placeholder_tests {
|
||||||
|
use crate::{apply_path_placeholders, replace_path_placeholder};
|
||||||
|
use yaak_models::models::{HttpRequest, HttpUrlParameter};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_middle() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
id: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
|
||||||
|
"https://example.com/xxx/bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_end() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
id: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
|
"https://example.com/xxx",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_query() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
id: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
|
||||||
|
"https://example.com/xxx?:foo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_missing() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
enabled: true,
|
||||||
|
name: "".to_string(),
|
||||||
|
value: "".to_string(),
|
||||||
|
id: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:missing"),
|
||||||
|
"https://example.com/:missing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_disabled() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
enabled: false,
|
||||||
|
name: ":foo".to_string(),
|
||||||
|
value: "xxx".to_string(),
|
||||||
|
id: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
|
"https://example.com/:foo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_prefix() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
id: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foooo"),
|
||||||
|
"https://example.com/:foooo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_encode() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "Hello World".into(),
|
||||||
|
enabled: true,
|
||||||
|
id: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
|
"https://example.com/Hello%20World",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_placeholder() {
|
||||||
|
let req = HttpRequest {
|
||||||
|
url: "example.com/:a/bar".to_string(),
|
||||||
|
url_parameters: vec![
|
||||||
|
HttpUrlParameter {
|
||||||
|
name: "b".to_string(),
|
||||||
|
value: "bbb".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
id: None,
|
||||||
|
},
|
||||||
|
HttpUrlParameter {
|
||||||
|
name: ":a".to_string(),
|
||||||
|
value: "aaa".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
id: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (url, url_parameters) = apply_path_placeholders(&req.url, req.url_parameters);
|
||||||
|
|
||||||
|
// Pattern match back to access it
|
||||||
|
assert_eq!(url, "example.com/aaa/bar");
|
||||||
|
assert_eq!(url_parameters.len(), 1);
|
||||||
|
assert_eq!(url_parameters[0].name, "b");
|
||||||
|
assert_eq!(url_parameters[0].value, "bbb");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1054,6 +1054,20 @@ pub async fn upsert_websocket_event<R: Runtime>(
|
|||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn duplicate_websocket_request<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
id: &str,
|
||||||
|
update_source: &UpdateSource,
|
||||||
|
) -> Result<WebsocketRequest> {
|
||||||
|
let mut request = match get_websocket_request(window, id).await? {
|
||||||
|
None => return Err(ModelNotFound(id.to_string())),
|
||||||
|
Some(r) => r,
|
||||||
|
};
|
||||||
|
request.id = "".to_string();
|
||||||
|
request.sort_priority = request.sort_priority + 0.001;
|
||||||
|
upsert_websocket_request(window, request, update_source).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn upsert_websocket_request<R: Runtime>(
|
pub async fn upsert_websocket_request<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
request: WebsocketRequest,
|
request: WebsocketRequest,
|
||||||
@@ -2653,7 +2667,9 @@ pub async fn get_workspace_export_resources<R: Runtime>(
|
|||||||
data.resources.folders.append(&mut list_folders(mgr, workspace_id).await?);
|
data.resources.folders.append(&mut list_folders(mgr, workspace_id).await?);
|
||||||
data.resources.http_requests.append(&mut list_http_requests(mgr, workspace_id).await?);
|
data.resources.http_requests.append(&mut list_http_requests(mgr, workspace_id).await?);
|
||||||
data.resources.grpc_requests.append(&mut list_grpc_requests(mgr, workspace_id).await?);
|
data.resources.grpc_requests.append(&mut list_grpc_requests(mgr, workspace_id).await?);
|
||||||
data.resources.websocket_requests.append(&mut list_websocket_requests(mgr, workspace_id).await?);
|
data.resources
|
||||||
|
.websocket_requests
|
||||||
|
.append(&mut list_websocket_requests(mgr, workspace_id).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nuke environments if we don't want them
|
// Nuke environments if we don't want them
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ thiserror = "2.0.11"
|
|||||||
tokio = { version = "1.0", default-features = false, features = ["macros", "time", "test-util"] }
|
tokio = { version = "1.0", default-features = false, features = ["macros", "time", "test-util"] }
|
||||||
tokio-tungstenite = { version = "0.26.1", default-features = false, features = ["rustls-tls-native-roots", "connect"] }
|
tokio-tungstenite = { version = "0.26.1", default-features = false, features = ["rustls-tls-native-roots", "connect"] }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
yaak-http = { workspace = true }
|
||||||
yaak-plugins = { workspace = true }
|
yaak-plugins = { workspace = true }
|
||||||
yaak-templates = { workspace = true }
|
yaak-templates = { workspace = true }
|
||||||
serde_json = "1.0.132"
|
serde_json = "1.0.132"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use tauri_plugin;
|
use tauri_plugin;
|
||||||
const COMMANDS: &[&str] = &[
|
const COMMANDS: &[&str] = &[
|
||||||
"close",
|
|
||||||
"connect",
|
"connect",
|
||||||
|
"close",
|
||||||
"delete_connection",
|
"delete_connection",
|
||||||
"delete_connections",
|
"delete_connections",
|
||||||
"delete_request",
|
"delete_request",
|
||||||
|
"duplicate_request",
|
||||||
"list_connections",
|
"list_connections",
|
||||||
"list_events",
|
"list_events",
|
||||||
"list_requests",
|
"list_requests",
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ export function upsertWebsocketRequest(
|
|||||||
}) as Promise<WebsocketRequest>;
|
}) as Promise<WebsocketRequest>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function duplicateWebsocketRequest(requestId: string) {
|
||||||
|
return invoke('plugin:yaak-ws|duplicate_request', {
|
||||||
|
requestId,
|
||||||
|
}) as Promise<WebsocketRequest>;
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteWebsocketRequest(requestId: string) {
|
export function deleteWebsocketRequest(requestId: string) {
|
||||||
return invoke('plugin:yaak-ws|delete_request', {
|
return invoke('plugin:yaak-ws|delete_request', {
|
||||||
requestId,
|
requestId,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-duplicate-request"
|
||||||
|
description = "Enables the duplicate_request command without any pre-configured scope."
|
||||||
|
commands.allow = ["duplicate_request"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-duplicate-request"
|
||||||
|
description = "Denies the duplicate_request command without any pre-configured scope."
|
||||||
|
commands.deny = ["duplicate_request"]
|
||||||
@@ -7,6 +7,7 @@ Default permissions for the plugin
|
|||||||
- `allow-delete-connection`
|
- `allow-delete-connection`
|
||||||
- `allow-delete-connections`
|
- `allow-delete-connections`
|
||||||
- `allow-delete-request`
|
- `allow-delete-request`
|
||||||
|
- `allow-duplicate-request`
|
||||||
- `allow-list-connections`
|
- `allow-list-connections`
|
||||||
- `allow-list-events`
|
- `allow-list-events`
|
||||||
- `allow-list-requests`
|
- `allow-list-requests`
|
||||||
@@ -181,6 +182,32 @@ Denies the delete_request command without any pre-configured scope.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
`yaak-ws:allow-duplicate-request`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the duplicate_request command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`yaak-ws:deny-duplicate-request`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the duplicate_request command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
`yaak-ws:allow-list-connections`
|
`yaak-ws:allow-list-connections`
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ permissions = [
|
|||||||
"allow-delete-connection",
|
"allow-delete-connection",
|
||||||
"allow-delete-connections",
|
"allow-delete-connections",
|
||||||
"allow-delete-request",
|
"allow-delete-request",
|
||||||
|
"allow-duplicate-request",
|
||||||
"allow-list-connections",
|
"allow-list-connections",
|
||||||
"allow-list-events",
|
"allow-list-events",
|
||||||
"allow-list-requests",
|
"allow-list-requests",
|
||||||
|
|||||||
@@ -354,6 +354,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-delete-request"
|
"const": "deny-delete-request"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the duplicate_request command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "allow-duplicate-request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the duplicate_request command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "deny-duplicate-request"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the list_connections command without any pre-configured scope.",
|
"description": "Enables the list_connections command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ use chrono::Utc;
|
|||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tauri::http::{HeaderMap, HeaderName};
|
use tauri::http::{HeaderMap, HeaderName};
|
||||||
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow};
|
use tauri::{AppHandle, Manager, Runtime, State, Url, WebviewWindow};
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use yaak_http::apply_path_placeholders;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent,
|
HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent,
|
||||||
WebsocketEventType, WebsocketRequest,
|
WebsocketEventType, WebsocketRequest,
|
||||||
@@ -33,6 +34,14 @@ pub(crate) async fn upsert_request<R: Runtime>(
|
|||||||
Ok(queries::upsert_websocket_request(&w, request, &UpdateSource::Window).await?)
|
Ok(queries::upsert_websocket_request(&w, request, &UpdateSource::Window).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub(crate) async fn duplicate_request<R: Runtime>(
|
||||||
|
request_id: &str,
|
||||||
|
w: WebviewWindow<R>,
|
||||||
|
) -> Result<WebsocketRequest> {
|
||||||
|
Ok(queries::duplicate_websocket_request(&w, request_id, &UpdateSource::Window).await?)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) async fn delete_request<R: Runtime>(
|
pub(crate) async fn delete_request<R: Runtime>(
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
@@ -290,7 +299,22 @@ pub(crate) async fn connect<R: Runtime>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = match ws_manager.connect(&connection.id, &request.url, headers, receive_tx).await
|
|
||||||
|
let (url, url_parameters) = apply_path_placeholders(&request.url, request.url_parameters);
|
||||||
|
|
||||||
|
// Add URL parameters to URL
|
||||||
|
let mut url = Url::parse(&url).unwrap();
|
||||||
|
{
|
||||||
|
let mut query_pairs = url.query_pairs_mut();
|
||||||
|
for p in url_parameters.clone() {
|
||||||
|
if !p.enabled || p.name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
query_pairs.append_pair(p.name.as_str(), p.value.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = match ws_manager.connect(&connection.id, url.as_str(), headers, receive_tx).await
|
||||||
{
|
{
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -298,6 +322,7 @@ pub(crate) async fn connect<R: Runtime>(
|
|||||||
&window,
|
&window,
|
||||||
&WebsocketConnection {
|
&WebsocketConnection {
|
||||||
error: Some(format!("{e:?}")),
|
error: Some(format!("{e:?}")),
|
||||||
|
state: WebsocketConnectionState::Closed,
|
||||||
..connection
|
..connection
|
||||||
},
|
},
|
||||||
&UpdateSource::Window,
|
&UpdateSource::Window,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ mod manager;
|
|||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use crate::cmd::{
|
use crate::cmd::{
|
||||||
close, connect, delete_connection, delete_connections, delete_request, list_connections,
|
connect, close, delete_connection, delete_connections, delete_request, duplicate_request,
|
||||||
list_events, list_requests, send, upsert_request,
|
list_connections, list_events, list_requests, send, upsert_request,
|
||||||
};
|
};
|
||||||
use crate::manager::WebsocketManager;
|
use crate::manager::WebsocketManager;
|
||||||
use tauri::plugin::{Builder, TauriPlugin};
|
use tauri::plugin::{Builder, TauriPlugin};
|
||||||
@@ -16,11 +16,12 @@ use tokio::sync::Mutex;
|
|||||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
Builder::new("yaak-ws")
|
Builder::new("yaak-ws")
|
||||||
.invoke_handler(generate_handler![
|
.invoke_handler(generate_handler![
|
||||||
close,
|
|
||||||
connect,
|
connect,
|
||||||
|
close,
|
||||||
delete_connection,
|
delete_connection,
|
||||||
delete_connections,
|
delete_connections,
|
||||||
delete_request,
|
delete_request,
|
||||||
|
duplicate_request,
|
||||||
list_connections,
|
list_connections,
|
||||||
list_events,
|
list_events,
|
||||||
list_requests,
|
list_requests,
|
||||||
|
|||||||
20
src-web/commands/duplicateWebsocketRequest.ts
Normal file
20
src-web/commands/duplicateWebsocketRequest.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { WebsocketRequest } from '@yaakapp-internal/models';
|
||||||
|
import { duplicateWebsocketRequest as cmdDuplicateWebsocketRequest } from '@yaakapp-internal/ws';
|
||||||
|
import { createFastMutation } from '../hooks/useFastMutation';
|
||||||
|
import { trackEvent } from '../lib/analytics';
|
||||||
|
import { router } from '../lib/router';
|
||||||
|
|
||||||
|
export const duplicateWebsocketRequest = createFastMutation({
|
||||||
|
mutationKey: ['delete_websocket_connection'],
|
||||||
|
mutationFn: async function (request: WebsocketRequest) {
|
||||||
|
return cmdDuplicateWebsocketRequest(request.id);
|
||||||
|
},
|
||||||
|
onSuccess: async (request) => {
|
||||||
|
trackEvent('websocket_request', 'duplicate');
|
||||||
|
await router.navigate({
|
||||||
|
to: '/workspaces/$workspaceId',
|
||||||
|
params: { workspaceId: request.workspaceId },
|
||||||
|
search: (prev) => ({ ...prev, request_id: request.id }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -81,13 +81,15 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
{activeConnection.error && (
|
|
||||||
<Banner color="danger" className="m-3">
|
|
||||||
{activeConnection.error}
|
|
||||||
</Banner>
|
|
||||||
)}
|
|
||||||
<AutoScroller
|
<AutoScroller
|
||||||
data={events}
|
data={events}
|
||||||
|
header={
|
||||||
|
activeConnection.error && (
|
||||||
|
<Banner color="danger" className="m-3">
|
||||||
|
{activeConnection.error}
|
||||||
|
</Banner>
|
||||||
|
)
|
||||||
|
}
|
||||||
render={(event) => (
|
render={(event) => (
|
||||||
<EventRow
|
<EventRow
|
||||||
key={event.id}
|
key={event.id}
|
||||||
|
|||||||
@@ -85,13 +85,15 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
|
|||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
{activeConnection.error && (
|
|
||||||
<Banner color="danger" className="m-3">
|
|
||||||
{activeConnection.error}
|
|
||||||
</Banner>
|
|
||||||
)}
|
|
||||||
<AutoScroller
|
<AutoScroller
|
||||||
data={events}
|
data={events}
|
||||||
|
header={
|
||||||
|
activeConnection.error && (
|
||||||
|
<Banner color="danger" className="m-3">
|
||||||
|
{activeConnection.error}
|
||||||
|
</Banner>
|
||||||
|
)
|
||||||
|
}
|
||||||
render={(event) => (
|
render={(event) => (
|
||||||
<EventRow
|
<EventRow
|
||||||
key={event.id}
|
key={event.id}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import {duplicateWebsocketRequest} from "../commands/duplicateWebsocketRequest";
|
||||||
import {
|
import {
|
||||||
useEnsureActiveCookieJar,
|
useEnsureActiveCookieJar,
|
||||||
useSubscribeActiveCookieJarId,
|
useSubscribeActiveCookieJarId,
|
||||||
@@ -253,10 +254,17 @@ function useGlobalWorkspaceHooks() {
|
|||||||
|
|
||||||
useHotKey('http_request.duplicate', async () => {
|
useHotKey('http_request.duplicate', async () => {
|
||||||
const activeRequest = getActiveRequest();
|
const activeRequest = getActiveRequest();
|
||||||
if (activeRequest?.model === 'http_request') {
|
if (activeRequest == null) {
|
||||||
|
// Nothing
|
||||||
|
} else if (activeRequest.model === 'http_request') {
|
||||||
await duplicateHttpRequest.mutateAsync();
|
await duplicateHttpRequest.mutateAsync();
|
||||||
} else {
|
} else if (activeRequest.model === 'grpc_request') {
|
||||||
await duplicateGrpcRequest.mutateAsync();
|
await duplicateGrpcRequest.mutateAsync();
|
||||||
|
} else if (activeRequest.model === 'websocket_request') {
|
||||||
|
await duplicateWebsocketRequest.mutateAsync(activeRequest);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
throw new Error('Failed to duplicate invalid request model: ' + (activeRequest as any).model);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import type { ReactElement, UIEvent } from 'react';
|
import type { ReactElement, ReactNode, UIEvent } from 'react';
|
||||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
render: (item: T, index: number) => ReactElement<HTMLElement>;
|
render: (item: T, index: number) => ReactElement<HTMLElement>;
|
||||||
|
header?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AutoScroller<T>({ data, render }: Props<T>) {
|
export function AutoScroller<T>({ data, render, header }: Props<T>) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [autoScroll, setAutoScroll] = useState<boolean>(true);
|
const [autoScroll, setAutoScroll] = useState<boolean>(true);
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ export function AutoScroller<T>({ data, render }: Props<T>) {
|
|||||||
}, [autoScroll, data.length]);
|
}, [autoScroll, data.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative">
|
<div className="h-full w-full relative grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
{!autoScroll && (
|
{!autoScroll && (
|
||||||
<div className="absolute bottom-0 right-0 m-2">
|
<div className="absolute bottom-0 right-0 m-2">
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -59,6 +60,7 @@ export function AutoScroller<T>({ data, render }: Props<T>) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{header ?? <span aria-hidden/>}
|
||||||
<div ref={containerRef} className="h-full w-full overflow-y-auto" onScroll={handleScroll}>
|
<div ref={containerRef} className="h-full w-full overflow-y-auto" onScroll={handleScroll}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -75,7 +77,7 @@ export function AutoScroller<T>({ data, render }: Props<T>) {
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
// height: `${virtualItem.size}px`,
|
height: `${virtualItem.size * 1000}px`,
|
||||||
transform: `translateY(${virtualItem.start}px)`,
|
transform: `translateY(${virtualItem.start}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,19 +9,17 @@ interface Props {
|
|||||||
|
|
||||||
export function Banner({ children, className, color }: Props) {
|
export function Banner({ children, className, color }: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
<div
|
className={classNames(
|
||||||
className={classNames(
|
className,
|
||||||
className,
|
`x-theme-banner--${color}`,
|
||||||
`x-theme-banner--${color}`,
|
'whitespace-pre-wrap',
|
||||||
'whitespace-pre-wrap',
|
'border border-dashed border-border bg-surface',
|
||||||
'border border-dashed border-border bg-surface',
|
'px-3 py-2 rounded select-auto',
|
||||||
'px-3 py-2 rounded select-auto',
|
'overflow-auto text-text',
|
||||||
'overflow-x-auto text-text',
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useFormatText } from '../../hooks/useFormatText';
|
|||||||
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
|
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
|
||||||
import { isJSON } from '../../lib/contentType';
|
import { isJSON } from '../../lib/contentType';
|
||||||
import { AutoScroller } from '../core/AutoScroller';
|
import { AutoScroller } from '../core/AutoScroller';
|
||||||
|
import { Banner } from '../core/Banner';
|
||||||
import { Button } from '../core/Button';
|
import { Button } from '../core/Button';
|
||||||
import type { EditorProps } from '../core/Editor/Editor';
|
import type { EditorProps } from '../core/Editor/Editor';
|
||||||
import { Editor } from '../core/Editor/Editor';
|
import { Editor } from '../core/Editor/Editor';
|
||||||
@@ -51,10 +52,26 @@ function ActualEventStreamViewer({ response }: Props) {
|
|||||||
defaultRatio={0.4}
|
defaultRatio={0.4}
|
||||||
minHeightPx={20}
|
minHeightPx={20}
|
||||||
firstSlot={() => (
|
firstSlot={() => (
|
||||||
<EventStreamEvents
|
<AutoScroller
|
||||||
events={events.data ?? []}
|
data={events.data ?? []}
|
||||||
activeEventIndex={activeEventIndex}
|
header={
|
||||||
setActiveEventIndex={setActiveEventIndex}
|
events.error && (
|
||||||
|
<Banner color="danger" className="m-3">
|
||||||
|
{String(events.error)}
|
||||||
|
</Banner>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
render={(event, i) => (
|
||||||
|
<EventRow
|
||||||
|
event={event}
|
||||||
|
isActive={i === activeEventIndex}
|
||||||
|
index={i}
|
||||||
|
onClick={() => {
|
||||||
|
if (i === activeEventIndex) setActiveEventIndex(null);
|
||||||
|
else setActiveEventIndex(i);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
secondSlot={
|
secondSlot={
|
||||||
@@ -112,34 +129,7 @@ function FormattedEditor({ text, language }: { text: string; language: EditorPro
|
|||||||
return <Editor readOnly defaultValue={formatted.data} language={language} stateKey={null} />;
|
return <Editor readOnly defaultValue={formatted.data} language={language} stateKey={null} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventStreamEvents({
|
function EventRow({
|
||||||
events,
|
|
||||||
activeEventIndex,
|
|
||||||
setActiveEventIndex,
|
|
||||||
}: {
|
|
||||||
events: ServerSentEvent[];
|
|
||||||
activeEventIndex: number | null;
|
|
||||||
setActiveEventIndex: (eventId: number | null) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<AutoScroller
|
|
||||||
data={events}
|
|
||||||
render={(event, i) => (
|
|
||||||
<EventStreamEvent
|
|
||||||
event={event}
|
|
||||||
isActive={i === activeEventIndex}
|
|
||||||
index={i}
|
|
||||||
onClick={() => {
|
|
||||||
if (i === activeEventIndex) setActiveEventIndex(null);
|
|
||||||
else setActiveEventIndex(i);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EventStreamEvent({
|
|
||||||
onClick,
|
onClick,
|
||||||
isActive,
|
isActive,
|
||||||
event,
|
event,
|
||||||
|
|||||||
Reference in New Issue
Block a user