mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-12 09:24:29 +02:00
Tweak response pane and refactor timings
This commit is contained in:
@@ -256,16 +256,24 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
let transaction = HttpTransaction::new(sender);
|
let transaction = HttpTransaction::new(sender);
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Capture request headers before sending (headers will be moved)
|
// Capture request headers before sending
|
||||||
let request_headers: Vec<HttpResponseHeader> = sendable_request
|
let request_headers: Vec<HttpResponseHeader> = sendable_request
|
||||||
.headers
|
.headers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, value)| HttpResponseHeader { name: name.clone(), value: value.clone() })
|
.map(|(name, value)| HttpResponseHeader { name: name.clone(), value: value.clone() })
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
{
|
||||||
|
// Update response with headers info and mark as connected
|
||||||
|
let mut r = response.lock().await;
|
||||||
|
r.url = sendable_request.url.clone();
|
||||||
|
r.request_headers = request_headers.clone();
|
||||||
|
app_handle.db().update_http_response_if_id(&r, &update_source)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the transaction with cancellation support
|
// Execute the transaction with cancellation support
|
||||||
// This returns the response with headers, but body is not yet consumed
|
// This returns the response with headers, but body is not yet consumed
|
||||||
let (http_response, _events) =
|
let (mut http_response, _events) =
|
||||||
transaction.execute_with_cancellation(sendable_request, cancelled_rx.clone()).await?;
|
transaction.execute_with_cancellation(sendable_request, cancelled_rx.clone()).await?;
|
||||||
|
|
||||||
// Prepare the response path before consuming the body
|
// Prepare the response path before consuming the body
|
||||||
@@ -280,37 +288,30 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Extract metadata before consuming the body (headers are available immediately)
|
// Extract metadata before consuming the body (headers are available immediately)
|
||||||
let status = http_response.status;
|
// Url might change, so update again
|
||||||
let status_reason = http_response.status_reason.clone();
|
|
||||||
let url = http_response.url.clone();
|
|
||||||
let remote_addr = http_response.remote_addr.clone();
|
|
||||||
let version = http_response.version.clone();
|
|
||||||
let content_length = http_response.content_length;
|
|
||||||
let headers: Vec<HttpResponseHeader> = http_response
|
let headers: Vec<HttpResponseHeader> = http_response
|
||||||
.headers
|
.headers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, value)| HttpResponseHeader { name: name.clone(), value: value.clone() })
|
.map(|(name, value)| HttpResponseHeader { name: name.clone(), value: value.clone() })
|
||||||
.collect();
|
.collect();
|
||||||
let headers_timing = http_response.timing.headers;
|
|
||||||
|
|
||||||
// Update response with headers info and mark as connected
|
|
||||||
{
|
{
|
||||||
|
// Update response with headers info and mark as connected
|
||||||
let mut r = response.lock().await;
|
let mut r = response.lock().await;
|
||||||
r.body_path = Some(
|
r.body_path = Some(body_path.to_string_lossy().to_string());
|
||||||
body_path
|
r.elapsed_headers = start.elapsed().as_millis() as i32;
|
||||||
.to_str()
|
r.status = http_response.status as i32;
|
||||||
.ok_or(GenericError(format!("Invalid path {body_path:?}")))?
|
r.status_reason = http_response.status_reason.clone().clone();
|
||||||
.to_string(),
|
r.url = http_response.url.clone().clone();
|
||||||
);
|
r.remote_addr = http_response.remote_addr.clone();
|
||||||
r.elapsed_headers = headers_timing.as_millis() as i32;
|
r.version = http_response.version.clone().clone();
|
||||||
r.elapsed = start.elapsed().as_millis() as i32;
|
|
||||||
r.status = status as i32;
|
|
||||||
r.status_reason = status_reason.clone();
|
|
||||||
r.url = url.clone();
|
|
||||||
r.remote_addr = remote_addr.clone();
|
|
||||||
r.version = version.clone();
|
|
||||||
r.headers = headers.clone();
|
r.headers = headers.clone();
|
||||||
r.request_headers = request_headers.clone();
|
r.content_length = http_response.content_length.map(|l| l as i32);
|
||||||
|
r.request_headers = http_response
|
||||||
|
.request_headers
|
||||||
|
.iter()
|
||||||
|
.map(|(n, v)| HttpResponseHeader { name: n.clone(), value: v.clone() })
|
||||||
|
.collect();
|
||||||
r.state = HttpResponseState::Connected;
|
r.state = HttpResponseState::Connected;
|
||||||
app_handle.db().update_http_response_if_id(&r, &update_source)?;
|
app_handle.db().update_http_response_if_id(&r, &update_source)?;
|
||||||
}
|
}
|
||||||
@@ -332,7 +333,7 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
let mut buf = [0u8; 8192];
|
let mut buf = [0u8; 8192];
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Check for cancellation - if we already have headers/body, just close cleanly
|
// Check for cancellation. If we already have headers/body, just close cleanly without error
|
||||||
if *cancelled_rx.borrow() {
|
if *cancelled_rx.borrow() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -350,7 +351,7 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
|
|
||||||
// Update response in DB with progress
|
// Update response in DB with progress
|
||||||
let mut r = response.lock().await;
|
let mut r = response.lock().await;
|
||||||
r.elapsed = start.elapsed().as_millis() as i32;
|
r.elapsed = start.elapsed().as_millis() as i32; // Approx until the end
|
||||||
r.content_length = Some(written_bytes as i32);
|
r.content_length = Some(written_bytes as i32);
|
||||||
app_handle.db().update_http_response_if_id(&r, &update_source)?;
|
app_handle.db().update_http_response_if_id(&r, &update_source)?;
|
||||||
}
|
}
|
||||||
@@ -362,20 +363,8 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
|
|
||||||
// Final update with closed state
|
// Final update with closed state
|
||||||
let mut resp = response.lock().await.clone();
|
let mut resp = response.lock().await.clone();
|
||||||
resp.headers = headers;
|
|
||||||
resp.request_headers = request_headers;
|
|
||||||
resp.status = status as i32;
|
|
||||||
resp.status_reason = status_reason;
|
|
||||||
resp.url = url;
|
|
||||||
resp.remote_addr = remote_addr;
|
|
||||||
resp.version = version;
|
|
||||||
resp.state = HttpResponseState::Closed;
|
|
||||||
resp.content_length = match content_length {
|
|
||||||
Some(l) => Some(l as i32),
|
|
||||||
None => Some(written_bytes as i32),
|
|
||||||
};
|
|
||||||
resp.elapsed = start.elapsed().as_millis() as i32;
|
resp.elapsed = start.elapsed().as_millis() as i32;
|
||||||
resp.elapsed_headers = headers_timing.as_millis() as i32;
|
resp.state = HttpResponseState::Closed;
|
||||||
resp.body_path = Some(
|
resp.body_path = Some(
|
||||||
body_path.to_str().ok_or(GenericError(format!("Invalid path {body_path:?}",)))?.to_string(),
|
body_path.to_str().ok_or(GenericError(format!("Invalid path {body_path:?}",)))?.to_string(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,16 +7,10 @@ use reqwest::{Client, Method, Version};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
|
use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
|
||||||
use tokio_util::io::StreamReader;
|
use tokio_util::io::StreamReader;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct HttpResponseTiming {
|
|
||||||
pub headers: Duration,
|
|
||||||
pub body: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum HttpResponseEvent {
|
pub enum HttpResponseEvent {
|
||||||
Setting(String, String),
|
Setting(String, String),
|
||||||
@@ -68,6 +62,8 @@ pub struct HttpResponse {
|
|||||||
pub status_reason: Option<String>,
|
pub status_reason: Option<String>,
|
||||||
/// Response headers
|
/// Response headers
|
||||||
pub headers: HashMap<String, String>,
|
pub headers: HashMap<String, String>,
|
||||||
|
/// Request headers
|
||||||
|
pub request_headers: HashMap<String, String>,
|
||||||
/// Content-Length from headers (may differ from actual body size)
|
/// Content-Length from headers (may differ from actual body size)
|
||||||
pub content_length: Option<u64>,
|
pub content_length: Option<u64>,
|
||||||
/// Final URL (after redirects)
|
/// Final URL (after redirects)
|
||||||
@@ -76,15 +72,11 @@ pub struct HttpResponse {
|
|||||||
pub remote_addr: Option<String>,
|
pub remote_addr: Option<String>,
|
||||||
/// HTTP version (e.g., "HTTP/1.1", "HTTP/2")
|
/// HTTP version (e.g., "HTTP/1.1", "HTTP/2")
|
||||||
pub version: Option<String>,
|
pub version: Option<String>,
|
||||||
/// Timing information
|
|
||||||
pub timing: HttpResponseTiming,
|
|
||||||
|
|
||||||
/// The body stream (consumed when calling bytes(), text(), write_to_file(), or drain())
|
/// The body stream (consumed when calling bytes(), text(), write_to_file(), or drain())
|
||||||
body_stream: Option<BodyStream>,
|
body_stream: Option<BodyStream>,
|
||||||
/// Content-Encoding for decompression
|
/// Content-Encoding for decompression
|
||||||
encoding: ContentEncoding,
|
encoding: ContentEncoding,
|
||||||
/// Start time for timing the body read
|
|
||||||
start_time: Instant,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for HttpResponse {
|
impl std::fmt::Debug for HttpResponse {
|
||||||
@@ -97,7 +89,6 @@ impl std::fmt::Debug for HttpResponse {
|
|||||||
.field("url", &self.url)
|
.field("url", &self.url)
|
||||||
.field("remote_addr", &self.remote_addr)
|
.field("remote_addr", &self.remote_addr)
|
||||||
.field("version", &self.version)
|
.field("version", &self.version)
|
||||||
.field("timing", &self.timing)
|
|
||||||
.field("body_stream", &"<stream>")
|
.field("body_stream", &"<stream>")
|
||||||
.field("encoding", &self.encoding)
|
.field("encoding", &self.encoding)
|
||||||
.finish()
|
.finish()
|
||||||
@@ -111,33 +102,31 @@ impl HttpResponse {
|
|||||||
status: u16,
|
status: u16,
|
||||||
status_reason: Option<String>,
|
status_reason: Option<String>,
|
||||||
headers: HashMap<String, String>,
|
headers: HashMap<String, String>,
|
||||||
|
request_headers: HashMap<String, String>,
|
||||||
content_length: Option<u64>,
|
content_length: Option<u64>,
|
||||||
url: String,
|
url: String,
|
||||||
remote_addr: Option<String>,
|
remote_addr: Option<String>,
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
timing: HttpResponseTiming,
|
|
||||||
body_stream: BodyStream,
|
body_stream: BodyStream,
|
||||||
encoding: ContentEncoding,
|
encoding: ContentEncoding,
|
||||||
start_time: Instant,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
status,
|
status,
|
||||||
status_reason,
|
status_reason,
|
||||||
headers,
|
headers,
|
||||||
|
request_headers,
|
||||||
content_length,
|
content_length,
|
||||||
url,
|
url,
|
||||||
remote_addr,
|
remote_addr,
|
||||||
version,
|
version,
|
||||||
timing,
|
|
||||||
body_stream: Some(body_stream),
|
body_stream: Some(body_stream),
|
||||||
encoding,
|
encoding,
|
||||||
start_time,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume the body and return it as bytes (loads entire body into memory).
|
/// Consume the body and return it as bytes (loads entire body into memory).
|
||||||
/// Also decompresses the body if Content-Encoding is set.
|
/// Also decompresses the body if Content-Encoding is set.
|
||||||
pub async fn bytes(mut self) -> Result<(Vec<u8>, BodyStats, HttpResponseTiming)> {
|
pub async fn bytes(mut self) -> Result<(Vec<u8>, BodyStats)> {
|
||||||
let stream = self.body_stream.take().ok_or_else(|| {
|
let stream = self.body_stream.take().ok_or_else(|| {
|
||||||
Error::RequestError("Response body has already been consumed".to_string())
|
Error::RequestError("Response body has already been consumed".to_string())
|
||||||
})?;
|
})?;
|
||||||
@@ -163,9 +152,6 @@ impl HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut timing = self.timing.clone();
|
|
||||||
timing.body = self.start_time.elapsed();
|
|
||||||
|
|
||||||
let stats = BodyStats {
|
let stats = BodyStats {
|
||||||
// For now, we can't easily track compressed size when streaming through decoder
|
// For now, we can't easily track compressed size when streaming through decoder
|
||||||
// Use content_length as an approximation, or decompressed size if identity encoding
|
// Use content_length as an approximation, or decompressed size if identity encoding
|
||||||
@@ -173,21 +159,21 @@ impl HttpResponse {
|
|||||||
size_decompressed: decompressed.len() as u64,
|
size_decompressed: decompressed.len() as u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((decompressed, stats, timing))
|
Ok((decompressed, stats))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume the body and return it as a UTF-8 string.
|
/// Consume the body and return it as a UTF-8 string.
|
||||||
pub async fn text(self) -> Result<(String, BodyStats, HttpResponseTiming)> {
|
pub async fn text(self) -> Result<(String, BodyStats)> {
|
||||||
let (bytes, stats, timing) = self.bytes().await?;
|
let (bytes, stats) = self.bytes().await?;
|
||||||
let text = String::from_utf8(bytes)
|
let text = String::from_utf8(bytes)
|
||||||
.map_err(|e| Error::RequestError(format!("Response is not valid UTF-8: {}", e)))?;
|
.map_err(|e| Error::RequestError(format!("Response is not valid UTF-8: {}", e)))?;
|
||||||
Ok((text, stats, timing))
|
Ok((text, stats))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take the body stream for manual consumption.
|
/// Take the body stream for manual consumption.
|
||||||
/// Returns an AsyncRead that decompresses on-the-fly if Content-Encoding is set.
|
/// Returns an AsyncRead that decompresses on-the-fly if Content-Encoding is set.
|
||||||
/// The caller is responsible for reading and processing the stream.
|
/// The caller is responsible for reading and processing the stream.
|
||||||
pub fn into_body_stream(mut self) -> Result<Box<dyn AsyncRead + Unpin + Send>> {
|
pub fn into_body_stream(&mut self) -> Result<Box<dyn AsyncRead + Unpin + Send>> {
|
||||||
let stream = self.body_stream.take().ok_or_else(|| {
|
let stream = self.body_stream.take().ok_or_else(|| {
|
||||||
Error::RequestError("Response body has already been consumed".to_string())
|
Error::RequestError("Response body has already been consumed".to_string())
|
||||||
})?;
|
})?;
|
||||||
@@ -199,7 +185,7 @@ impl HttpResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Discard the body without reading it (useful for redirects).
|
/// Discard the body without reading it (useful for redirects).
|
||||||
pub async fn drain(mut self) -> Result<HttpResponseTiming> {
|
pub async fn drain(mut self) -> Result<()> {
|
||||||
let stream = self.body_stream.take().ok_or_else(|| {
|
let stream = self.body_stream.take().ok_or_else(|| {
|
||||||
Error::RequestError("Response body has already been consumed".to_string())
|
Error::RequestError("Response body has already been consumed".to_string())
|
||||||
})?;
|
})?;
|
||||||
@@ -220,10 +206,7 @@ impl HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut timing = self.timing.clone();
|
Ok(())
|
||||||
timing.body = self.start_time.elapsed();
|
|
||||||
|
|
||||||
Ok(timing)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,9 +280,6 @@ impl HttpSender for ReqwestSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
let mut timing = HttpResponseTiming::default();
|
|
||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
let sendable_req = req_builder.build()?;
|
let sendable_req = req_builder.build()?;
|
||||||
events.push(HttpResponseEvent::Setting(
|
events.push(HttpResponseEvent::Setting(
|
||||||
@@ -316,11 +296,11 @@ impl HttpSender for ReqwestSender {
|
|||||||
method: sendable_req.method().to_string(),
|
method: sendable_req.method().to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut request_headers = HashMap::new();
|
||||||
for (name, value) in sendable_req.headers() {
|
for (name, value) in sendable_req.headers() {
|
||||||
events.push(HttpResponseEvent::HeaderUp(
|
let v = value.to_str().unwrap_or_default().to_string();
|
||||||
name.to_string(),
|
request_headers.insert(name.to_string(), v.clone());
|
||||||
value.to_str().unwrap_or_default().to_string(),
|
events.push(HttpResponseEvent::HeaderUp(name.to_string(), v));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
events.push(HttpResponseEvent::HeaderUpDone);
|
events.push(HttpResponseEvent::HeaderUpDone);
|
||||||
events.push(HttpResponseEvent::Info("Sending request to server".to_string()));
|
events.push(HttpResponseEvent::Info("Sending request to server".to_string()));
|
||||||
@@ -341,17 +321,13 @@ impl HttpSender for ReqwestSender {
|
|||||||
let url = response.url().to_string();
|
let url = response.url().to_string();
|
||||||
let remote_addr = response.remote_addr().map(|a| a.to_string());
|
let remote_addr = response.remote_addr().map(|a| a.to_string());
|
||||||
let version = Some(version_to_str(&response.version()));
|
let version = Some(version_to_str(&response.version()));
|
||||||
|
let content_length = response.content_length();
|
||||||
|
|
||||||
events.push(HttpResponseEvent::ReceiveUrl {
|
events.push(HttpResponseEvent::ReceiveUrl {
|
||||||
version: response.version(),
|
version: response.version(),
|
||||||
status: response.status().to_string(),
|
status: response.status().to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
timing.headers = start.elapsed();
|
|
||||||
|
|
||||||
// Extract content length
|
|
||||||
let content_length = response.content_length();
|
|
||||||
|
|
||||||
// Extract headers
|
// Extract headers
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
for (key, value) in response.headers() {
|
for (key, value) in response.headers() {
|
||||||
@@ -385,14 +361,13 @@ impl HttpSender for ReqwestSender {
|
|||||||
status,
|
status,
|
||||||
status_reason,
|
status_reason,
|
||||||
headers,
|
headers,
|
||||||
|
request_headers,
|
||||||
content_length,
|
content_length,
|
||||||
url,
|
url,
|
||||||
remote_addr,
|
remote_addr,
|
||||||
version,
|
version,
|
||||||
timing,
|
|
||||||
body_stream,
|
body_stream,
|
||||||
encoding,
|
encoding,
|
||||||
start,
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,12 +201,11 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decompress::ContentEncoding;
|
use crate::decompress::ContentEncoding;
|
||||||
use crate::sender::{HttpResponseEvent, HttpResponseTiming, HttpSender};
|
use crate::sender::{HttpResponseEvent, HttpSender};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
@@ -246,14 +245,13 @@ mod tests {
|
|||||||
mock.status,
|
mock.status,
|
||||||
None, // status_reason
|
None, // status_reason
|
||||||
mock.headers,
|
mock.headers,
|
||||||
|
HashMap::new(),
|
||||||
None, // content_length
|
None, // content_length
|
||||||
"https://example.com".to_string(), // url
|
"https://example.com".to_string(), // url
|
||||||
None, // remote_addr
|
None, // remote_addr
|
||||||
Some("HTTP/1.1".to_string()), // version
|
Some("HTTP/1.1".to_string()), // version
|
||||||
HttpResponseTiming::default(),
|
|
||||||
body_stream,
|
body_stream,
|
||||||
ContentEncoding::Identity,
|
ContentEncoding::Identity,
|
||||||
Instant::now(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +275,7 @@ mod tests {
|
|||||||
assert_eq!(result.status, 200);
|
assert_eq!(result.status, 200);
|
||||||
|
|
||||||
// Consume the body to verify it
|
// Consume the body to verify it
|
||||||
let (body, _, _) = result.bytes().await.unwrap();
|
let (body, _) = result.bytes().await.unwrap();
|
||||||
assert_eq!(body, b"OK");
|
assert_eq!(body, b"OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +306,7 @@ mod tests {
|
|||||||
let (result, _) = transaction.execute_with_cancellation(request, rx).await.unwrap();
|
let (result, _) = transaction.execute_with_cancellation(request, rx).await.unwrap();
|
||||||
assert_eq!(result.status, 200);
|
assert_eq!(result.status, 200);
|
||||||
|
|
||||||
let (body, _, _) = result.bytes().await.unwrap();
|
let (body, _) = result.bytes().await.unwrap();
|
||||||
assert_eq!(body, b"Final");
|
assert_eq!(body, b"Final");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
{
|
{
|
||||||
value: TAB_BODY,
|
value: TAB_BODY,
|
||||||
label: 'Preview Mode',
|
label: 'Preview Mode',
|
||||||
|
hidden: (activeResponse?.contentLength || 0) === 0,
|
||||||
options: {
|
options: {
|
||||||
value: viewMode,
|
value: viewMode,
|
||||||
onChange: setViewMode,
|
onChange: setViewMode,
|
||||||
@@ -92,6 +93,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
setViewMode,
|
setViewMode,
|
||||||
viewMode,
|
viewMode,
|
||||||
activeResponse?.requestHeaders.length,
|
activeResponse?.requestHeaders.length,
|
||||||
|
activeResponse?.contentLength,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const activeTab = activeTabs?.[activeRequestId];
|
const activeTab = activeTabs?.[activeRequestId];
|
||||||
@@ -163,79 +165,66 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
</Banner>
|
</Banner>
|
||||||
)}
|
)}
|
||||||
{/* Show tabs if we have any data (headers, body, etc.) even if there's an error */}
|
{/* Show tabs if we have any data (headers, body, etc.) even if there's an error */}
|
||||||
{(activeResponse?.headers.length > 0 ||
|
<Tabs
|
||||||
activeResponse?.bodyPath ||
|
key={activeRequestId} // Freshen tabs on request change
|
||||||
!activeResponse?.error) && (
|
value={activeTab}
|
||||||
<Tabs
|
onChangeValue={setActiveTab}
|
||||||
key={activeRequestId} // Freshen tabs on request change
|
tabs={tabs}
|
||||||
value={activeTab}
|
label="Response"
|
||||||
onChangeValue={setActiveTab}
|
className="ml-3 mr-3 mb-3 min-h-0 flex-1"
|
||||||
tabs={tabs}
|
tabListClassName="mt-0.5"
|
||||||
label="Response"
|
>
|
||||||
className="ml-3 mr-3 mb-3 min-h-0 flex-1"
|
<TabContent value={TAB_BODY}>
|
||||||
tabListClassName="mt-0.5"
|
<ErrorBoundary name="Http Response Viewer">
|
||||||
>
|
<Suspense>
|
||||||
<TabContent value={TAB_BODY}>
|
<ConfirmLargeResponse response={activeResponse}>
|
||||||
<ErrorBoundary name="Http Response Viewer">
|
{activeResponse.state === 'initialized' ? (
|
||||||
<Suspense>
|
<EmptyStateText>
|
||||||
<ConfirmLargeResponse response={activeResponse}>
|
<VStack space={3}>
|
||||||
{activeResponse.state === 'initialized' ? (
|
<HStack space={3}>
|
||||||
<EmptyStateText>
|
<LoadingIcon className="text-text-subtlest" />
|
||||||
<VStack space={3}>
|
Sending Request
|
||||||
<HStack space={3}>
|
</HStack>
|
||||||
<LoadingIcon className="text-text-subtlest" />
|
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>
|
||||||
Sending Request
|
Cancel
|
||||||
</HStack>
|
</Button>
|
||||||
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>
|
</VStack>
|
||||||
Cancel
|
</EmptyStateText>
|
||||||
</Button>
|
) : activeResponse.state === 'closed' &&
|
||||||
</VStack>
|
activeResponse.contentLength === 0 ? (
|
||||||
</EmptyStateText>
|
<EmptyStateText>Empty </EmptyStateText>
|
||||||
) : activeResponse.state === 'closed' &&
|
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
|
||||||
activeResponse.contentLength === 0 ? (
|
<EventStreamViewer response={activeResponse} />
|
||||||
<EmptyStateText>Empty </EmptyStateText>
|
) : mimeType?.match(/^image\/svg/) ? (
|
||||||
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
|
<SvgViewer response={activeResponse} />
|
||||||
<EventStreamViewer response={activeResponse} />
|
) : mimeType?.match(/^image/i) ? (
|
||||||
) : mimeType?.match(/^image\/svg/) ? (
|
<EnsureCompleteResponse response={activeResponse} Component={ImageViewer} />
|
||||||
<SvgViewer response={activeResponse} />
|
) : mimeType?.match(/^audio/i) ? (
|
||||||
) : mimeType?.match(/^image/i) ? (
|
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
|
||||||
<EnsureCompleteResponse
|
) : mimeType?.match(/^video/i) ? (
|
||||||
response={activeResponse}
|
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
|
||||||
Component={ImageViewer}
|
) : mimeType?.match(/pdf/i) ? (
|
||||||
/>
|
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
|
||||||
) : mimeType?.match(/^audio/i) ? (
|
) : mimeType?.match(/csv|tab-separated/i) ? (
|
||||||
<EnsureCompleteResponse
|
<CsvViewer className="pb-2" response={activeResponse} />
|
||||||
response={activeResponse}
|
) : (
|
||||||
Component={AudioViewer}
|
<HTMLOrTextViewer
|
||||||
/>
|
textViewerClassName="-mr-2 bg-surface" // Pull to the right
|
||||||
) : mimeType?.match(/^video/i) ? (
|
response={activeResponse}
|
||||||
<EnsureCompleteResponse
|
pretty={viewMode === 'pretty'}
|
||||||
response={activeResponse}
|
/>
|
||||||
Component={VideoViewer}
|
)}
|
||||||
/>
|
</ConfirmLargeResponse>
|
||||||
) : mimeType?.match(/pdf/i) ? (
|
</Suspense>
|
||||||
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
|
</ErrorBoundary>
|
||||||
) : mimeType?.match(/csv|tab-separated/i) ? (
|
</TabContent>
|
||||||
<CsvViewer className="pb-2" response={activeResponse} />
|
<TabContent value={TAB_HEADERS}>
|
||||||
) : (
|
<ResponseHeaders response={activeResponse} />
|
||||||
<HTMLOrTextViewer
|
</TabContent>
|
||||||
textViewerClassName="-mr-2 bg-surface" // Pull to the right
|
<TabContent value={TAB_INFO}>
|
||||||
response={activeResponse}
|
<ResponseInfo response={activeResponse} />
|
||||||
pretty={viewMode === 'pretty'}
|
</TabContent>
|
||||||
/>
|
</Tabs>
|
||||||
)}
|
|
||||||
</ConfirmLargeResponse>
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</TabContent>
|
|
||||||
<TabContent value={TAB_HEADERS}>
|
|
||||||
<ResponseHeaders response={activeResponse} />
|
|
||||||
</TabContent>
|
|
||||||
<TabContent value={TAB_INFO}>
|
|
||||||
<ResponseInfo response={activeResponse} />
|
|
||||||
</TabContent>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,41 +25,53 @@ export function ResponseHeaders({ response }: Props) {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5">
|
<div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5">
|
||||||
|
<DetailsBanner
|
||||||
|
storageKey={`${response.requestId}.request_headers`}
|
||||||
|
summary={
|
||||||
|
<h2 className="flex items-center">
|
||||||
|
Request <CountBadge showZero count={requestHeaders.length} />
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{requestHeaders.length === 0 ? (
|
||||||
|
<NoHeaders />
|
||||||
|
) : (
|
||||||
|
<KeyValueRows>
|
||||||
|
{requestHeaders.map((h, i) => (
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||||
|
<KeyValueRow labelColor="primary" key={i} label={h.name}>
|
||||||
|
{h.value}
|
||||||
|
</KeyValueRow>
|
||||||
|
))}
|
||||||
|
</KeyValueRows>
|
||||||
|
)}
|
||||||
|
</DetailsBanner>
|
||||||
<DetailsBanner
|
<DetailsBanner
|
||||||
defaultOpen
|
defaultOpen
|
||||||
storageKey={`${response.requestId}.response_headers`}
|
storageKey={`${response.requestId}.response_headers`}
|
||||||
summary={
|
summary={
|
||||||
<h2 className="flex items-center">
|
<h2 className="flex items-center">
|
||||||
Response <CountBadge count={responseHeaders.length} />
|
Response <CountBadge showZero count={responseHeaders.length} />
|
||||||
</h2>
|
</h2>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<KeyValueRows>
|
{responseHeaders.length === 0 ? (
|
||||||
{responseHeaders.map((h, i) => (
|
<NoHeaders />
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
) : (
|
||||||
<KeyValueRow labelColor="primary" key={i} label={h.name}>
|
<KeyValueRows>
|
||||||
{h.value}
|
{responseHeaders.map((h, i) => (
|
||||||
</KeyValueRow>
|
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||||
))}
|
<KeyValueRow labelColor="primary" key={i} label={h.name}>
|
||||||
</KeyValueRows>
|
{h.value}
|
||||||
</DetailsBanner>
|
</KeyValueRow>
|
||||||
<DetailsBanner
|
))}
|
||||||
storageKey={`${response.requestId}.request_headers`}
|
</KeyValueRows>
|
||||||
summary={
|
)}
|
||||||
<h2 className="flex items-center">
|
|
||||||
Request <CountBadge count={requestHeaders.length} />
|
|
||||||
</h2>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<KeyValueRows>
|
|
||||||
{requestHeaders.map((h, i) => (
|
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
|
||||||
<KeyValueRow labelColor="primary" key={i} label={h.name}>
|
|
||||||
{h.value}
|
|
||||||
</KeyValueRow>
|
|
||||||
))}
|
|
||||||
</KeyValueRows>
|
|
||||||
</DetailsBanner>
|
</DetailsBanner>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NoHeaders() {
|
||||||
|
return <span className="text-text-subtlest text-sm italic">No Headers</span>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export function ResponseInfo({ response }: Props) {
|
|||||||
<div className="overflow-auto h-full pb-4">
|
<div className="overflow-auto h-full pb-4">
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow labelColor="info" label="Version">
|
<KeyValueRow labelColor="info" label="Version">
|
||||||
{response.version}
|
{response.version ?? <span className="text-text-subtlest">--</span>}
|
||||||
</KeyValueRow>
|
</KeyValueRow>
|
||||||
<KeyValueRow labelColor="info" label="Remote Address">
|
<KeyValueRow labelColor="info" label="Remote Address">
|
||||||
{response.remoteAddr}
|
{response.remoteAddr ?? <span className="text-text-subtlest">--</span>}
|
||||||
</KeyValueRow>
|
</KeyValueRow>
|
||||||
<KeyValueRow
|
<KeyValueRow
|
||||||
labelColor="info"
|
labelColor="info"
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ interface Props {
|
|||||||
count2?: number | true;
|
count2?: number | true;
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: Color;
|
color?: Color;
|
||||||
|
showZero?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CountBadge({ count, count2, className, color }: Props) {
|
export function CountBadge({ count, count2, className, color, showZero }: Props) {
|
||||||
if (count === 0) return null;
|
if (count === 0 && !showZero) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ export function PairEditorRow({
|
|||||||
size="xs"
|
size="xs"
|
||||||
icon={isLast || disabled ? 'empty' : 'chevron_down'}
|
icon={isLast || disabled ? 'empty' : 'chevron_down'}
|
||||||
title="Select form data type"
|
title="Select form data type"
|
||||||
className="text-text-subtle"
|
className="text-text-subtlest"
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
@@ -798,7 +798,13 @@ function FileActionsDropdown({
|
|||||||
items={fileItems}
|
items={fileItems}
|
||||||
itemsAfter={itemsAfter}
|
itemsAfter={itemsAfter}
|
||||||
>
|
>
|
||||||
<IconButton iconSize="sm" size="xs" icon="chevron_down" title="Select form data type" />
|
<IconButton
|
||||||
|
iconSize="sm"
|
||||||
|
size="xs"
|
||||||
|
icon="chevron_down"
|
||||||
|
title="Select form data type"
|
||||||
|
className="text-text-subtlest"
|
||||||
|
/>
|
||||||
</RadioDropdown>
|
</RadioDropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user