use ratatui::{
layout::{Constraint, Rect},
prelude::*,
widgets::{Block, Borders, Paragraph, Row, Table, TableState},
Frame,
};
use super::Theme;
use crate::app::App;
pub fn render(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
let block = Block::default()
.title(" Test History ")
.title_style(Style::default().fg(theme.primary))
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.style(Style::default().bg(theme.background));
let inner = block.inner(area);
frame.render_widget(block, area);
if app.history.is_empty() {
let placeholder = Paragraph::new(" No test history found. Run a test to get started!")
.style(Style::default().fg(theme.muted));
frame.render_widget(placeholder, inner);
return;
}
let header = Row::new(vec![
"Date", "Provider", "Download", "Upload", "Ping", "Location",
])
.style(Style::default().fg(theme.primary).bold())
.bottom_margin(1);
let rows: Vec<Row> = app
.history
.iter()
.rev()
.take(50) .map(|entry| {
let date = entry.timestamp.format("%m/%d %H:%M").to_string();
let download = entry
.download
.as_ref()
.map(|d| format!("{:.1} Mbps", d.speed_mbps))
.unwrap_or_else(|| "-".to_string());
let upload = entry
.upload
.as_ref()
.map(|u| format!("{:.1} Mbps", u.speed_mbps))
.unwrap_or_else(|| "-".to_string());
let ping = entry
.ping
.as_ref()
.map(|p| format!("{:.0} ms", p.latency_ms))
.unwrap_or_else(|| "-".to_string());
let location = entry
.connection_info
.as_ref()
.and_then(|c| c.location.clone())
.unwrap_or_else(|| "-".to_string());
Row::new(vec![
date,
entry.provider.clone(),
download,
upload,
ping,
location,
])
.style(Style::default().fg(theme.foreground))
})
.collect();
let table = Table::new(
rows,
[
Constraint::Length(12), Constraint::Length(12), Constraint::Length(12), Constraint::Length(12), Constraint::Length(8), Constraint::Min(15), ],
)
.header(header)
.row_highlight_style(
Style::default()
.bg(theme.primary)
.fg(theme.background)
.bold(),
)
.highlight_symbol("â–¶ ");
let mut state = TableState::default();
state.select(Some(app.history_selected_index));
frame.render_stateful_widget(table, inner, &mut state);
let instructions =
Paragraph::new("[↑/↓] Navigate [Enter] View Details [e] Export [Esc] Back")
.style(Style::default().fg(theme.muted))
.alignment(Alignment::Center);
let footer_area = Rect {
x: area.x,
y: area.y + area.height - 2,
width: area.width,
height: 1,
};
frame.render_widget(instructions, footer_area);
}
#[allow(dead_code)]
pub fn render_detail(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
let entry = match app.history.get(app.history_selected_index) {
Some(e) => e,
None => return,
};
let block = Block::default()
.title(" Test Details ")
.title_style(Style::default().fg(theme.primary))
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.style(Style::default().bg(theme.background));
let inner = block.inner(area);
frame.render_widget(block, area);
let mut lines = vec![
Line::from(vec![
Span::styled(" Date: ", Style::default().fg(theme.muted)),
Span::styled(
entry.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(),
Style::default().fg(theme.foreground),
),
]),
Line::from(vec![
Span::styled(" Provider: ", Style::default().fg(theme.muted)),
Span::styled(&entry.provider, Style::default().fg(theme.foreground)),
]),
Line::from(""),
];
if let Some(ref ping) = entry.ping {
lines.push(Line::from(vec![Span::styled(
" Ping:",
Style::default().fg(theme.secondary).bold(),
)]));
lines.push(Line::from(vec![
Span::styled(" Latency: ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.2} ms", ping.latency_ms),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(vec![
Span::styled(" Jitter: ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.2} ms", ping.jitter_ms),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(vec![
Span::styled(" Packet Loss: ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.1}%", ping.packet_loss_percent),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(""));
}
if let Some(ref download) = entry.download {
lines.push(Line::from(vec![Span::styled(
" Download:",
Style::default().fg(theme.success).bold(),
)]));
lines.push(Line::from(vec![
Span::styled(" Speed: ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.2} Mbps", download.speed_mbps),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(vec![
Span::styled(" Peak: ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.2} Mbps", download.peak_speed_mbps),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(vec![
Span::styled(" Data: ", Style::default().fg(theme.muted)),
Span::styled(
crate::utils::format_bytes(download.bytes_transferred),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(""));
}
if let Some(ref upload) = entry.upload {
lines.push(Line::from(vec![Span::styled(
" Upload:",
Style::default().fg(theme.secondary).bold(),
)]));
lines.push(Line::from(vec![
Span::styled(" Speed: ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.2} Mbps", upload.speed_mbps),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(vec![
Span::styled(" Peak: ", Style::default().fg(theme.muted)),
Span::styled(
format!("{:.2} Mbps", upload.peak_speed_mbps),
Style::default().fg(theme.foreground),
),
]));
lines.push(Line::from(vec![
Span::styled(" Data: ", Style::default().fg(theme.muted)),
Span::styled(
crate::utils::format_bytes(upload.bytes_transferred),
Style::default().fg(theme.foreground),
),
]));
}
let paragraph = Paragraph::new(lines);
frame.render_widget(paragraph, inner);
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_compiles() {
assert!(true);
}
}