Tweak response pane and refactor timings

This commit is contained in:
Gregory Schier
2025-12-21 06:24:01 -08:00
parent 6b52a0cbed
commit 5776bab288
8 changed files with 165 additions and 194 deletions
+28 -39
View File
@@ -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(),
); );
+19 -44
View File
@@ -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,
)) ))
} }
} }
+4 -6
View File
@@ -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 -73
View File
@@ -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>
)} )}
+38 -26
View File
@@ -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>;
}
+2 -2
View File
@@ -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"
+4 -2
View File
@@ -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
+8 -2
View File
@@ -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>
); );
} }