use eframe::egui::{self, RichText};
use crate::http::ApiResponse;
use crate::hurl::RunStatus;
use crate::session::Session;
use super::app::{GuiApp, ResponseSection};
use super::widgets::{self, status_color};
fn selected_response(session: &Session, ci: usize) -> (Option<&ApiResponse>, bool) {
let entry = session
.collections
.get(ci)
.and_then(|col| col.entries.get(col.selected_entry));
let loading = entry.is_some_and(|e| e.last_run == RunStatus::Running);
(entry.and_then(|e| e.last_response.as_ref()), loading)
}
pub fn ui(app: &mut GuiApp, ui: &mut egui::Ui) {
let theme = app.theme;
let (
lbl_sending,
lbl_error,
lbl_no_response,
lbl_copy,
lbl_copy_body,
lbl_compact,
lbl_compact_hint,
lbl_empty_body,
lbl_no_headers,
lbl_no_assertions,
lbl_body,
lbl_headers,
lbl_asserts,
) = {
let s = &app.strings;
(
s.sending,
s.gui_error,
s.gui_no_response_yet,
s.gui_copy,
s.gui_copy_body,
s.gui_compact,
s.gui_compact_hint,
s.gui_empty_body,
s.gui_no_headers,
s.gui_no_assertions,
s.gui_sec_body,
s.gui_sec_headers,
s.gui_sec_asserts,
)
};
let (shown, loading) = selected_response(&app.session, app.active_ci());
let (status, status_text, body, error, headers, asserts, duration) = match shown {
Some(r) => (
r.status,
r.status_text.clone(),
r.body.clone(),
r.error.clone(),
r.headers.clone(),
r.assert_results.clone(),
r.duration_ms,
),
None => (
0,
String::new(),
std::sync::Arc::from(""),
String::new(),
Vec::new(),
Vec::new(),
None,
),
};
ui.horizontal(|ui| {
if loading {
ui.spinner();
ui.colored_label(theme.dim, lbl_sending);
} else if !error.is_empty() {
ui.colored_label(
theme.err,
format!("{} {}", super::icons::WARNING, lbl_error),
);
} else if status > 0 {
let col = status_color(&theme, status);
ui.label(
RichText::new(format!("{status} {status_text}"))
.strong()
.color(col),
);
} else {
ui.colored_label(theme.dim, lbl_no_response);
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if let Some(ms) = duration {
ui.colored_label(theme.dim, format!("{ms} ms"));
}
if !body.is_empty() {
ui.colored_label(theme.dim, format!("{} B", body.len()));
if ui.button(lbl_copy).on_hover_text(lbl_copy_body).clicked() {
ui.ctx().copy_text(body.to_string());
}
if app.response_section == ResponseSection::Body {
let mut compact = app.response_compact;
if ui
.selectable_label(compact, lbl_compact)
.on_hover_text(lbl_compact_hint)
.clicked()
{
compact = !compact;
}
app.response_compact = compact;
}
}
});
});
if !error.is_empty() {
ui.add_space(4.0);
ui.colored_label(theme.err, error);
}
ui.add_space(2.0);
let failed = asserts.iter().filter(|a| !a.passed).count();
let assert_label = if asserts.is_empty() {
lbl_asserts.to_string()
} else if failed == 0 {
format!("{lbl_asserts} {} ({})", super::icons::PASS, asserts.len())
} else {
format!(
"{lbl_asserts} {} ({failed}/{})",
super::icons::FAIL,
asserts.len()
)
};
widgets::section_tabs(
ui,
&theme,
&mut app.response_section,
&[
(ResponseSection::Body, lbl_body),
(ResponseSection::Headers, lbl_headers),
(ResponseSection::Asserts, assert_label.as_str()),
],
);
ui.separator();
egui::ScrollArea::both()
.auto_shrink([false, false])
.show(ui, |ui| match app.response_section {
ResponseSection::Body => {
if body.is_empty() {
ui.colored_label(theme.dim, lbl_empty_body);
} else {
let mut text = if app.response_compact {
crate::shared_utils::compact_long_strings(&body[..])
} else {
body.to_string()
};
ui.add(
egui::TextEdit::multiline(&mut text)
.code_editor()
.desired_width(f32::INFINITY)
.desired_rows(12)
.interactive(false),
);
}
}
ResponseSection::Headers => {
if headers.is_empty() {
ui.colored_label(theme.dim, lbl_no_headers);
} else {
egui::Grid::new("resp_headers")
.num_columns(2)
.spacing([12.0, 4.0])
.striped(true)
.show(ui, |ui| {
for (k, v) in &headers {
ui.label(RichText::new(k).strong().color(theme.accent));
ui.label(RichText::new(v).monospace().color(theme.text));
ui.end_row();
}
});
}
}
ResponseSection::Asserts => {
if asserts.is_empty() {
ui.colored_label(theme.dim, lbl_no_assertions);
} else {
for a in &asserts {
ui.horizontal(|ui| {
if a.passed {
ui.colored_label(theme.ok, super::icons::PASS);
} else {
ui.colored_label(theme.err, super::icons::FAIL);
}
ui.label(RichText::new(&a.expr).monospace().color(theme.text));
});
if !a.passed && !a.detail.is_empty() {
ui.horizontal(|ui| {
ui.add_space(18.0);
ui.colored_label(theme.err, &a.detail);
});
}
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collection::Collection;
use crate::hurl::HurlEntry;
fn session_with_two_answered_requests() -> Session {
let mut a = HurlEntry {
title: "First".to_string(),
..Default::default()
};
a.last_response = Some(ApiResponse {
status: 201,
body: std::sync::Arc::from("first body"),
..Default::default()
});
let mut b = HurlEntry {
title: "Second".to_string(),
..Default::default()
};
b.last_response = Some(ApiResponse {
status: 404,
body: std::sync::Arc::from("second body"),
..Default::default()
});
let mut session = Session::default();
session.collections.clear();
session
.collections
.push(Collection::new("api".to_string(), vec![a, b]));
session
}
#[test]
fn the_response_panel_follows_the_selected_request_not_the_last_one_run() {
let mut session = session_with_two_answered_requests();
session.collections[0].selected_entry = 1;
let (shown, _) = selected_response(&session, 0);
assert_eq!(
shown.map(|r| r.status),
Some(404),
"the second request is selected, so its own answer is shown"
);
session.collections[0].selected_entry = 0;
let (shown, _) = selected_response(&session, 0);
assert_eq!(
shown.map(|r| r.status),
Some(201),
"selecting the first request shows what the first request got back"
);
assert_eq!(
shown.map(|r| r.body.to_string()).as_deref(),
Some("first body")
);
}
#[test]
fn only_the_request_actually_in_flight_shows_as_sending() {
let mut session = session_with_two_answered_requests();
session.collections[0].entries[1].last_run = RunStatus::Running;
session.collections[0].selected_entry = 1;
let (_, loading) = selected_response(&session, 0);
assert!(loading, "the selected request is the one on the wire");
session.collections[0].selected_entry = 0;
let (shown, loading) = selected_response(&session, 0);
assert!(
!loading,
"a settled request doesn't borrow another request's spinner"
);
assert_eq!(shown.map(|r| r.status), Some(201));
}
#[test]
fn a_request_that_has_never_run_shows_nothing_rather_than_a_neighbours_answer() {
let mut session = session_with_two_answered_requests();
session.collections[0].entries.push(HurlEntry {
title: "Third".to_string(),
..Default::default()
});
session.collections[0].selected_entry = 2;
let (shown, loading) = selected_response(&session, 0);
assert!(
shown.is_none(),
"an unrun request has no response of its own to show"
);
assert!(!loading);
}
#[test]
fn an_index_with_no_collection_behind_it_is_simply_empty() {
let session = Session::default();
let (shown, loading) = selected_response(&session, 0);
assert!(shown.is_none());
assert!(!loading);
}
}