adguardian/widgets/
table.rs1use chrono::{DateTime, Utc};
2use tui::{
3 layout::Constraint,
4 style::{Color, Modifier, Style},
5 text::Span,
6 widgets::{Block, Borders, Cell, Row, Table},
7};
8
9use crate::fetch::fetch_query_log::{Query, Question};
10pub fn make_query_table(data: &[Query], width: u16) -> Table<'_> {
11 let rows = data
12 .iter()
13 .map(|query| {
14 let time = Cell::from(time_ago(query.time.as_str()).unwrap_or("unknown".to_string()))
15 .style(Style::default().fg(Color::Gray));
16
17 let question = Cell::from(make_request_cell(&query.question))
18 .style(Style::default().add_modifier(Modifier::BOLD));
19
20 let client = Cell::from(client_label(query)).style(Style::default().fg(Color::Blue));
21
22 let (time_taken, elapsed_color) = make_time_taken_and_color(&query.elapsed_ms)
23 .unwrap_or_else(|_| ("? ms".to_string(), Color::Gray));
24 let elapsed_ms = Cell::from(time_taken).style(Style::default().fg(elapsed_color));
25
26 let (status_txt, status_color) = block_status_text(&query.reason, query.cached);
27 let status = Cell::from(status_txt).style(Style::default().fg(status_color));
28
29 let upstream = Cell::from(query.upstream.as_str()).style(Style::default().fg(Color::Blue));
30
31 let color = make_row_color(&query.reason);
32 Row::new(vec![time, question, status, elapsed_ms, client, upstream])
33 .style(Style::default().fg(color))
34 })
35 .collect::<Vec<Row>>();
36
37 let title = Span::styled("Query Log", Style::default().add_modifier(Modifier::BOLD));
38
39 let block = Block::default().title(title).borders(Borders::ALL);
40
41 let mut headers = vec![
42 Cell::from(Span::raw("Time")),
43 Cell::from(Span::raw("Request")),
44 Cell::from(Span::raw("Status")),
45 Cell::from(Span::raw("Time Taken")),
46 ];
47
48 if width > 120 {
49 headers.extend(vec![
50 Cell::from(Span::raw("Client")),
51 Cell::from(Span::raw("Upstream DNS")),
52 ]);
53
54 let widths = &[
55 Constraint::Percentage(15),
56 Constraint::Percentage(35),
57 Constraint::Percentage(10),
58 Constraint::Percentage(10),
59 Constraint::Percentage(15),
60 Constraint::Percentage(15),
61 ];
62
63 Table::new(rows)
64 .header(Row::new(headers))
65 .widths(widths)
66 .block(block)
67 } else {
68 let widths = &[
69 Constraint::Percentage(20),
70 Constraint::Percentage(40),
71 Constraint::Percentage(20),
72 Constraint::Percentage(20),
73 ];
74
75 Table::new(rows)
76 .header(Row::new(headers))
77 .widths(widths)
78 .block(block)
79 }
80}
81
82fn time_ago(timestamp: &str) -> Result<String, anyhow::Error> {
84 let datetime = DateTime::parse_from_rfc3339(timestamp)?;
85 let datetime_utc = datetime.with_timezone(&Utc);
86 let now = Utc::now();
87
88 let duration = now - datetime_utc;
89
90 if duration.num_minutes() < 1 {
91 Ok(format!("{} sec ago", duration.num_seconds()))
92 } else {
93 Ok(format!("{} min ago", duration.num_minutes()))
94 }
95}
96
97fn make_request_cell(q: &Question) -> String {
99 format!("[{}] {} - {}", q.class, q.question_type, q.name)
100}
101
102fn client_label(q: &Query) -> &str {
104 match &q.client_info {
105 Some(info) if !info.name.is_empty() => &info.name,
106 _ => &q.client,
107 }
108}
109
110fn make_time_taken_and_color(elapsed: &str) -> Result<(String, Color), anyhow::Error> {
112 let elapsed_f64 = elapsed.parse::<f64>()?;
113 let rounded_elapsed = (elapsed_f64 * 100.0).round() / 100.0;
114 let time_taken = format!("{:.2} ms", rounded_elapsed);
115 let color = if elapsed_f64 < 1.0 {
116 Color::Green
117 } else if (1.0..=20.0).contains(&elapsed_f64) {
118 Color::Yellow
119 } else {
120 Color::Red
121 };
122 Ok((time_taken, color))
123}
124
125fn make_row_color(reason: &str) -> Color {
127 if reason == "NotFilteredNotFound" {
128 Color::Green
129 } else if reason == "FilteredBlackList" {
130 Color::Red
131 } else {
132 Color::Yellow
133 }
134}
135
136fn block_status_text(reason: &str, cached: bool) -> (String, Color) {
138 let (text, color) = if reason == "FilteredBlackList" {
139 ("Blacklisted".to_string(), Color::Red)
140 } else if cached {
141 ("Cached".to_string(), Color::Cyan)
142 } else if reason == "NotFilteredNotFound" {
143 ("Allowed".to_string(), Color::Green)
144 } else {
145 ("Other Block".to_string(), Color::Yellow)
146 };
147 (text, color)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn client_label_prefers_name_and_falls_back_to_ip() {
156 let label = |json: &str| client_label(&serde_json::from_str(json).unwrap()).to_string();
157 let named = r#"{"client":"1.2.3.4","client_info":{"name":"laptop"}}"#;
158 assert_eq!(label(named), "laptop");
159 for unnamed in [
160 r#"{"client":"1.2.3.4","client_info":{"name":""}}"#,
161 r#"{"client":"1.2.3.4","client_info":null}"#,
162 r#"{"client":"1.2.3.4"}"#,
163 ] {
164 assert_eq!(label(unnamed), "1.2.3.4");
165 }
166 }
167}