Skip to main content

adguardian/widgets/
table.rs

1use 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(query.client.as_str()).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
82// Given a timestamp, return a string representing how long ago that was
83fn 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
97// Return cell showing info about the request made in a given query
98fn make_request_cell(q: &Question) -> String {
99  format!("[{}] {} - {}", q.class, q.question_type, q.name)
100}
101
102// Return a cell showing the time taken for a query, and a color based on time
103fn make_time_taken_and_color(elapsed: &str) -> Result<(String, Color), anyhow::Error> {
104  let elapsed_f64 = elapsed.parse::<f64>()?;
105  let rounded_elapsed = (elapsed_f64 * 100.0).round() / 100.0;
106  let time_taken = format!("{:.2} ms", rounded_elapsed);
107  let color = if elapsed_f64 < 1.0 {
108    Color::Green
109  } else if (1.0..=20.0).contains(&elapsed_f64) {
110    Color::Yellow
111  } else {
112    Color::Red
113  };
114  Ok((time_taken, color))
115}
116
117// Return color for a row, based on the allow/block reason
118fn make_row_color(reason: &str) -> Color {
119  if reason == "NotFilteredNotFound" {
120    Color::Green
121  } else if reason == "FilteredBlackList" {
122    Color::Red
123  } else {
124    Color::Yellow
125  }
126}
127
128// Return text and color for the status cell based on allow/ block reason
129fn block_status_text(reason: &str, cached: bool) -> (String, Color) {
130  let (text, color) = if reason == "FilteredBlackList" {
131    ("Blacklisted".to_string(), Color::Red)
132  } else if cached {
133    ("Cached".to_string(), Color::Cyan)
134  } else if reason == "NotFilteredNotFound" {
135    ("Allowed".to_string(), Color::Green)
136  } else {
137    ("Other Block".to_string(), Color::Yellow)
138  };
139  (text, color)
140}