From d614ec4d181d5590dde9e13de4bdf3b0906eaf52 Mon Sep 17 00:00:00 2001 From: Marco De Araujo Date: Wed, 24 Dec 2025 13:34:30 -0400 Subject: [PATCH] Moving from Line to Table --- src/locales/en-US/main.ftl | 3 + src/locales/pt-BR/main.ftl | 3 + src/ui/app.rs | 6 +- src/ui/components/monitor_list.rs | 178 ++++++++++++------------------ 4 files changed, 78 insertions(+), 112 deletions(-) diff --git a/src/locales/en-US/main.ftl b/src/locales/en-US/main.ftl index 39b6859..4a71df0 100644 --- a/src/locales/en-US/main.ftl +++ b/src/locales/en-US/main.ftl @@ -3,6 +3,7 @@ missing_api_key = ❌ API key not provided. Use --api-key or environment variabl success = ✅ Metrics received successfully! metrics_preview = 📋 First 200 characters of metrics: Response = Response +response = response invalid-json-status-page = ❌ Error parssing status page JSON invalid-json-heartbeat = ❌ Error parssing heartbeat JSON invalid-uptime-key-format = Invalid format for uptime key. Expected format "monitorID_period". Received key: {key} @@ -20,3 +21,5 @@ never = Never auto-update-failed = Automatic update failed update-fail = Failed to update data now = Now +uptime = Uptime +history = History diff --git a/src/locales/pt-BR/main.ftl b/src/locales/pt-BR/main.ftl index 1bf9c23..459b8be 100644 --- a/src/locales/pt-BR/main.ftl +++ b/src/locales/pt-BR/main.ftl @@ -3,6 +3,7 @@ metrics_preview = 📋 Primeiras 200 caracteres das métricas: missing_api_key = ❌ API key não fornecida. Use --api-key ou a variável de ambiente UPTIME_API_KEY missing_url = ❌ URL não fornecida. Use --url ou a variável de ambiente UPTIME_URL Response = Resposta +response = resposta invalid-json-status-page = ❌ Falha ao parsear JSON do status page invalid-json-heartbeat = ❌ Falha ao parsear JSON do heartbeat invalid-uptime-key-format = Formato inválido na chave de uptime. Chave esperada no formato "monitorID_periodo". Chave recebida: {key} @@ -10,6 +11,7 @@ invalid-monitor-id = ID de monitor inválido: {id} invalid-period-hours = Período em horas inválido: {hours} loading = Carregando... monitors = Monitors +monitor = Monitor unknown = Desconhecido services = Serviços monitor-not-found = Nenhum monitor encontrado @@ -20,3 +22,4 @@ never = Nunca auto-update-failed = Falha na atualização automática update-fail = Falha ao atualizar dados now = Agora +history = Historico diff --git a/src/ui/app.rs b/src/ui/app.rs index 7469422..aa121a7 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -16,7 +16,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span, Text}, - widgets::{Block, Borders, ListState, Padding, Paragraph}, + widgets::{Block, Borders, TableState, Padding, Paragraph}, }; use std::{ @@ -32,7 +32,7 @@ pub struct App { update_interval: Duration, endpoints: UptimeKumaEndpoints, client: UptimeKumaClient, - list_state: ListState, + list_state: TableState, } impl App { @@ -185,7 +185,7 @@ impl App { terminal.hide_cursor()?; let state = DashboardViewState::new(); - let mut list_state = ListState::default(); + let mut list_state = TableState::default(); list_state.select(Some(0)); Ok(Self { diff --git a/src/ui/components/monitor_list.rs b/src/ui/components/monitor_list.rs index b70f1f7..17ce655 100644 --- a/src/ui/components/monitor_list.rs +++ b/src/ui/components/monitor_list.rs @@ -6,61 +6,57 @@ use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState}, + widgets::{Block, Borders, List, ListItem, Row, Table, TableState}, }; -const STATUS_LINE_LENGTH: usize = 20; +const STATUS_LINE_LENGTH: usize = 100; const MAX_NAME_LENGTH: usize = 30; pub fn render_monitor_list( frame: &mut Frame, area: Rect, state: &DashboardViewState, - list_state: &mut ListState, + list_state: &mut TableState, ) { let block = Block::default() .title(t("monitors")) .borders(Borders::ALL) .style(Style::default().fg(Color::Blue)); - let items: Vec = state + let header = vec![ + "".to_string(), + t("monitor"), + t("Response"), + t("uptime"), + t("history"), + ]; + let constraints = vec![ + Constraint::Length(3), + Constraint::Length(MAX_NAME_LENGTH as u16), + Constraint::Length(10), + Constraint::Length(10), + Constraint::Length(STATUS_LINE_LENGTH as u16 + 16), + ]; + + let available_height = area.height.saturating_sub(2); + let visible_items = state.monitors.len().min(available_height as usize); + + let rows: Vec = state .monitors .iter() - .map(|monitor| create_monitor_item(monitor)) + .take(visible_items) + .enumerate() + .map(|(i, m)| create_monitor_item(m, list_state.selected() == Some(i))) .collect(); - let list = List::new(items) + let table = Table::new(rows, constraints) + .header(Row::new(header)) .block(block) - .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) - .highlight_symbol(">> "); + .column_spacing(1) + .highlight_symbol(">> ") + .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED)); - frame.render_stateful_widget(list, area, list_state); -} - -fn create_status_line_spans(status_history: &[MonitorStatus]) -> Vec> { - let mut spans = Vec::new(); - let recent_status: Vec<_> = status_history - .iter() - .rev() - .take(STATUS_LINE_LENGTH) - .collect(); - - for status in recent_status.iter().rev() { - let c = get_status_char(status); - let color = get_status_color(status); - - spans.push(Span::styled(c.to_string(), Style::default().fg(color))); - } - - spans.push(Span::raw(" ")); - spans.push(Span::styled( - t("now"), - Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::ITALIC), - )); - - spans + frame.render_stateful_widget(table, area, list_state); } pub fn get_status_char(status: &MonitorStatus) -> char { @@ -87,83 +83,47 @@ pub fn get_status_emoji(status: &MonitorStatus) -> &str { } } -fn create_monitor_item(monitor: &MonitorViewState) -> ListItem { +fn create_monitor_item(monitor: &MonitorViewState, item_selected: bool) -> Row<'_> { let status_icon = get_status_emoji(&monitor.status); let status_color = get_status_color(&monitor.status); - let status_line = create_status_line_spans(&monitor.status_history); - let item_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(3), - Constraint::Length(MAX_NAME_LENGTH as u16), - Constraint::Length(10), - Constraint::Length(10), - Constraint::Length(STATUS_LINE_LENGTH as u16), - ]) - .split(Rect { - x: 0, - y: 0, - width: 1000, - height: 1, - }); + let display_name: String = if monitor.name.len() > MAX_NAME_LENGTH { + format!("{:.width$}...", &monitor.name, width = MAX_NAME_LENGTH - 3) + } else { + monitor.name.clone() + }; - let status_span = Span::styled( + let response_text = format!("{:>7}ms", monitor.response_time); + let uptime_text = format!("{:>7}%", monitor.uptime_24h); + let status_line = create_status_line_text(&monitor.status_history); + + let style = if item_selected { + Style::default().add_modifier(Modifier::REVERSED) + } else { + Style::default() + }; + + Row::new(vec![ format!("{} ", status_icon), - Style::default() - .fg(status_color) - .add_modifier(Modifier::BOLD), - ); - - let mut name_text = monitor.name.clone(); - if name_text.len() > MAX_NAME_LENGTH { - name_text = format!("{}...", &name_text[..MAX_NAME_LENGTH - 3]); - } - - let name_span = Span::styled( - name_text, - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), - ); - - let response_span = Span::styled( - format!("{:>7}ms", monitor.response_time), - Style::default().fg(Color::Cyan), - ); - - let uptime_span = Span::styled( - format!("{:>7}%", monitor.uptime_24h), - Style::default().fg(Color::Magenta), - ); - - let status_spans = create_status_line_spans(&monitor.status_history); - let status_line_start_period = Span::styled( - "(1h)", - Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::ITALIC), - ); - let status_line_end_period = Span::styled( - t("now"), - Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::ITALIC), - ); - let mut lines = Vec::new(); - let main_line = Line::from(vec![ - status_span, - name_span, - Span::raw(" | "), - response_span, - Span::raw(" | "), - uptime_span, - Span::raw(" | "), - status_line_start_period, - ]); - lines.push(main_line); - - let status_line = Line::from(status_spans); - lines.push(status_line); - ListItem::new(lines) + display_name, + response_text, + uptime_text, + format!("(1h) {} ({})", status_line, t("now")), + ]) + .style(style) + .height(1) +} + +fn create_status_line_text(status_history: &[MonitorStatus]) -> String { + let mut line = String::new(); + let recent_status: Vec<_> = status_history + .iter() + .rev() + .take(STATUS_LINE_LENGTH) + .collect(); + + for status in recent_status.iter().rev() { + line.push(get_status_char(status)); + } + line }