Close to the ui final version

This commit is contained in:
Marco De Araujo 2025-12-24 14:47:57 -04:00
parent d614ec4d18
commit b130c4550d
9 changed files with 46 additions and 69 deletions

View file

@ -2,4 +2,3 @@ pub mod data;
pub mod models; pub mod models;
pub use data::unify_data; pub use data::unify_data;
pub use models::{UnifiedData, UnifiedMonitorData};

View file

@ -1,5 +1,2 @@
pub mod heartbeat; pub mod heartbeat;
pub mod status_page; pub mod status_page;
pub use heartbeat::HeartbeatResponse;
pub use status_page::StatusPageResponse;

View file

@ -1,4 +1,3 @@
pub mod model; pub mod model;
pub mod parser; pub mod parser;
pub use model::StatusPageResponse;
pub use parser::parse_response; pub use parser::parse_response;

View file

@ -16,7 +16,7 @@ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, Borders, TableState, Padding, Paragraph}, widgets::{Block, Borders, Padding, Paragraph},
}; };
use std::{ use std::{
@ -32,7 +32,6 @@ pub struct App {
update_interval: Duration, update_interval: Duration,
endpoints: UptimeKumaEndpoints, endpoints: UptimeKumaEndpoints,
client: UptimeKumaClient, client: UptimeKumaClient,
list_state: TableState,
} }
impl App { impl App {
@ -102,21 +101,7 @@ impl App {
frame.render_widget(no_data, area); frame.render_widget(no_data, area);
} }
fn move_selection(&mut self, direction: isize) {
if self.state.monitors.is_empty() {
return;
}
let current = self.list_state.selected().unwrap_or(0);
let new_index = (current as isize + direction) as usize;
let new_index = new_index.clamp(0, self.state.monitors.len() - 1);
self.list_state.select(Some(new_index));
self.state.selected_index = new_index;
}
fn render(&mut self) { fn render(&mut self) {
let mut list_state = self.list_state.clone();
let _ = self.terminal.draw(|frame| { let _ = self.terminal.draw(|frame| {
let area = frame.area(); let area = frame.area();
let chunks = Layout::default() let chunks = Layout::default()
@ -134,7 +119,7 @@ impl App {
} else if self.state.monitors.is_empty() { } else if self.state.monitors.is_empty() {
Self::render_no_data(frame, chunks[1]); Self::render_no_data(frame, chunks[1]);
} else { } else {
render_monitor_list(frame, chunks[1], &self.state, &mut list_state); render_monitor_list(frame, chunks[1], &self.state);
} }
}); });
} }
@ -148,8 +133,6 @@ impl App {
match key.code { match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true, KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
KeyCode::Up => self.move_selection(-1),
KeyCode::Down => self.move_selection(1),
_ => {} _ => {}
} }
} }
@ -185,8 +168,6 @@ impl App {
terminal.hide_cursor()?; terminal.hide_cursor()?;
let state = DashboardViewState::new(); let state = DashboardViewState::new();
let mut list_state = TableState::default();
list_state.select(Some(0));
Ok(Self { Ok(Self {
state, state,
@ -196,7 +177,6 @@ impl App {
update_interval: Duration::from_secs(30), update_interval: Duration::from_secs(30),
endpoints, endpoints,
client: UptimeKumaClient::new(), client: UptimeKumaClient::new(),
list_state: list_state,
}) })
} }

View file

@ -3,21 +3,16 @@ use crate::ui::dashboard::model::DashboardViewState;
use crate::ui::dashboard::{MonitorStatus, MonitorViewState}; use crate::ui::dashboard::{MonitorStatus, MonitorViewState};
use ratatui::{ use ratatui::{
Frame, Frame,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Row, Table, TableState}, widgets::{Block, Borders, Row, Table},
}; };
const STATUS_LINE_LENGTH: usize = 100; const STATUS_LINE_LENGTH: usize = 100;
const MAX_NAME_LENGTH: usize = 30; const MAX_NAME_LENGTH: usize = 30;
pub fn render_monitor_list( pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) {
frame: &mut Frame,
area: Rect,
state: &DashboardViewState,
list_state: &mut TableState,
) {
let block = Block::default() let block = Block::default()
.title(t("monitors")) .title(t("monitors"))
.borders(Borders::ALL) .borders(Borders::ALL)
@ -45,8 +40,7 @@ pub fn render_monitor_list(
.monitors .monitors
.iter() .iter()
.take(visible_items) .take(visible_items)
.enumerate() .map(|m| create_monitor_item(m))
.map(|(i, m)| create_monitor_item(m, list_state.selected() == Some(i)))
.collect(); .collect();
let table = Table::new(rows, constraints) let table = Table::new(rows, constraints)
@ -56,7 +50,7 @@ pub fn render_monitor_list(
.highlight_symbol(">> ") .highlight_symbol(">> ")
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED)); .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
frame.render_stateful_widget(table, area, list_state); frame.render_widget(table, area);
} }
pub fn get_status_char(status: &MonitorStatus) -> char { pub fn get_status_char(status: &MonitorStatus) -> char {
@ -83,7 +77,7 @@ pub fn get_status_emoji(status: &MonitorStatus) -> &str {
} }
} }
fn create_monitor_item(monitor: &MonitorViewState, item_selected: bool) -> Row<'_> { fn create_monitor_item(monitor: &MonitorViewState) -> Row<'_> {
let status_icon = get_status_emoji(&monitor.status); let status_icon = get_status_emoji(&monitor.status);
let status_color = get_status_color(&monitor.status); let status_color = get_status_color(&monitor.status);
@ -95,35 +89,55 @@ fn create_monitor_item(monitor: &MonitorViewState, item_selected: bool) -> Row<'
let response_text = format!("{:>7}ms", monitor.response_time); let response_text = format!("{:>7}ms", monitor.response_time);
let uptime_text = format!("{:>7}%", monitor.uptime_24h); let uptime_text = format!("{:>7}%", monitor.uptime_24h);
let status_line = create_status_line_text(&monitor.status_history);
let style = if item_selected { let status_line_spans = create_status_line_spans(&monitor.status_history);
Style::default().add_modifier(Modifier::REVERSED)
} else {
Style::default()
};
Row::new(vec![ Row::new(vec![
format!("{} ", status_icon), get_formated_line(format!("{} ", status_icon), status_color, Modifier::empty()),
display_name, get_formated_line(display_name, Color::White, Modifier::empty()),
response_text, get_formated_line(response_text, Color::Cyan, Modifier::empty()),
uptime_text, get_formated_line(uptime_text, Color::Magenta, Modifier::empty()),
format!("(1h) {} ({})", status_line, t("now")), status_line_spans,
]) ])
.style(style) .style(Style::default())
.height(1) .height(1)
} }
fn create_status_line_text(status_history: &[MonitorStatus]) -> String { fn get_formated_line(text: String, color: Color, modifier: Modifier) -> Line<'static> {
let mut line = String::new(); Line::from(vec![Span::styled(
text,
Style::default().fg(color).add_modifier(modifier),
)])
}
fn create_status_line_spans(status_history: &[MonitorStatus]) -> Line<'_> {
let mut spans = Vec::new();
let recent_status: Vec<_> = status_history let recent_status: Vec<_> = status_history
.iter() .iter()
.rev() .rev()
.take(STATUS_LINE_LENGTH) .take(STATUS_LINE_LENGTH)
.collect(); .collect();
spans.push(Span::styled(
"(1h)",
Style::default()
.fg(Color::Gray)
.add_modifier(Modifier::ITALIC),
));
spans.push(Span::raw(" "));
for status in recent_status.iter().rev() { for status in recent_status.iter().rev() {
line.push(get_status_char(status)); let c = get_status_char(status);
let color = get_status_color(status);
spans.push(Span::styled(c.to_string(), Style::default().fg(color)));
} }
line spans.push(Span::raw(" "));
spans.push(Span::styled(
t("now"),
Style::default()
.fg(Color::Gray)
.add_modifier(Modifier::ITALIC),
));
Line::from(spans)
} }

View file

@ -1,2 +1,2 @@
pub mod model; pub mod model;
pub use model::{DashboardViewState, MonitorStatus, MonitorViewState}; pub use model::{MonitorStatus, MonitorViewState};

View file

@ -1,4 +1,4 @@
use crate::core::models::{UnifiedData, UnifiedMonitorData}; use crate::core::models::UnifiedData;
use crate::data::heartbeat::model::HeartbeatEntry; use crate::data::heartbeat::model::HeartbeatEntry;
use crate::i18n::t; use crate::i18n::t;
@ -96,10 +96,6 @@ impl DashboardViewState {
error_message: None, error_message: None,
} }
} }
pub fn get_selected_monitor(&self) -> Option<&MonitorViewState> {
self.monitors.get(self.selected_index)
}
} }
fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> { fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {

View file

@ -1,6 +0,0 @@
#[derive(Debug)]
pub enum AppEvent {
KeyPress(char),
RefreshData,
Quit,
}

View file

@ -1,6 +1,4 @@
pub mod app; pub mod app;
pub mod components; pub mod components;
pub mod dashboard; pub mod dashboard;
pub mod events;
pub use app::App; pub use app::App;