use std::cmp::min; use crate::i18n::t; use crate::ui::dashboard::{ MonitorStatus, MonitorViewState, model::{DashboardViewState, GroupViewState}, }; use ratatui::{ Frame, layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Padding, Row, Table}, }; const STATUS_LINE_LENGTH: usize = 100; const MAX_NAME_LENGTH: usize = 30; pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) { let group_areas = layout_groups(area, &state.groups); for (i, (group, &group_area)) in state.groups.iter().zip(group_areas.iter()).enumerate() { render_group(frame, group_area, group, i == 0); } } fn layout_groups(area: Rect, groups: &[GroupViewState]) -> Vec { let total_lines: usize = groups.iter().map(|g| g.monitors.len() + 1).sum(); if total_lines == 0 { return Vec::new(); } let content_height = area.height.saturating_sub(2); let line_height = content_height as usize / total_lines; let mut current_y = area.y + 1; let mut areas = Vec::with_capacity(groups.len()); for group in groups { let group_lines = group.monitors.len() + 2; let group_height = (group_lines + line_height).max(1); areas.push(Rect { x: area.x, y: current_y, width: area.width, height: group_height as u16, }); current_y += group_height as u16; } areas } fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first: bool) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(0) .constraints([Constraint::Length(1), Constraint::Min(1)]) .split(area); let group_title = Line::from(vec![ Span::styled(format!("{} ", group.name), title_style()), Span::styled( format!("({})", group.monitors.len()), Style::default().fg(Color::Gray), ), ]); let title_block = Block::default() .borders(if is_first { Borders::TOP | Borders::LEFT | Borders::RIGHT } else { Borders::ALL }) .border_style(Style::default().fg(Color::Blue)) .title(group_title) .title_alignment(Alignment::Left); frame.render_widget(title_block, chunks[0]); if !group.monitors.is_empty() { render_monitor_table(frame, chunks[1], &group.monitors); } } fn render_monitor_table(frame: &mut Frame, area: Rect, monitors: &Vec) { let max_items = area.height as usize; let items_to_show = min(monitors.len(), max_items); let header_cells = vec![ "".to_string(), t("monitor"), t("Response"), t("uptime"), t("history"), ]; let header = Row::new(header_cells).style(title_style()).height(1); let rows: Vec = monitors .iter() .take(items_to_show) .map(|monitor| create_monitor_item(monitor)) .collect(); let widths = vec![ Constraint::Length(3), Constraint::Length(MAX_NAME_LENGTH as u16), Constraint::Length(10), Constraint::Length(10), Constraint::Length(STATUS_LINE_LENGTH as u16), ]; let table = Table::new(rows, widths) .header(header) .block( Block::default() .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) .border_style(Style::default().fg(Color::Blue)) .padding(Padding::horizontal(2)), ) .column_spacing(1) .style(Style::default()); frame.render_widget(table, area); } pub fn get_status_char(status: &MonitorStatus) -> char { match status { MonitorStatus::Up => '■', MonitorStatus::Down => '■', MonitorStatus::Unknown => '■', } } pub fn get_status_color(status: &MonitorStatus) -> Color { match status { MonitorStatus::Up => Color::Green, MonitorStatus::Down => Color::Red, MonitorStatus::Unknown => Color::Yellow, } } pub fn get_status_emoji(status: &MonitorStatus) -> &str { match status { MonitorStatus::Up => "✅", MonitorStatus::Down => "❌", MonitorStatus::Unknown => "❓", } } fn create_monitor_item(monitor: &MonitorViewState) -> Row<'_> { let status_icon = get_status_emoji(&monitor.status); let status_color = get_status_color(&monitor.status); 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 response_text = format!("{:>7}ms", monitor.response_time); let uptime_text = format!("{:>7}%", monitor.uptime_24h); let status_line_spans = create_status_line_spans(&monitor.status_history); Row::new(vec![ get_formated_line(format!("{} ", status_icon), status_color, Modifier::empty()), get_formated_line(display_name, Color::White, Modifier::empty()), get_formated_line(response_text, Color::Cyan, Modifier::empty()), get_formated_line(uptime_text, Color::Magenta, Modifier::empty()), status_line_spans, ]) .style(Style::default()) .height(1) } fn get_formated_line(text: String, color: Color, modifier: Modifier) -> Line<'static> { Line::from(vec![Span::styled( text, Style::default().fg(color).add_modifier(modifier), )]) } fn create_status_line_spans(status_history: &[MonitorStatus]) -> Line<'_> { let recent_status: Vec<_> = status_history .iter() .rev() .take(STATUS_LINE_LENGTH) .collect(); let mut spans = Vec::with_capacity(recent_status.len()); 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))); } Line::from(spans) } fn title_style() -> Style { Style::default() .fg(Color::Yellow) .add_modifier(Modifier::BOLD) }