use std::{ borrow::Cow, cmp::min, collections::{HashMap, hash_map::DefaultHasher}, hash::{Hash, Hasher}, sync::{OnceLock, RwLock}, }; use crate::{ i18n::t, ui::dashboard::{ MonitorStatus, MonitorViewState, model::{BORDER_LINES_VIEW, DashboardViewState, GroupViewState}, }, }; use once_cell::sync::Lazy; 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; static UP_SPAN: Lazy> = Lazy::new(|| Span::styled("■", Style::default().fg(Color::Green))); static DOWN_SPAN: Lazy> = Lazy::new(|| Span::styled("■", Style::default().fg(Color::Red))); static UNKNOWN_SPAN: Lazy> = Lazy::new(|| Span::styled("■", Style::default().fg(Color::Yellow))); static STATUS_LINE_CACHE: OnceLock>>>> = OnceLock::new(); pub fn render_monitor_list(main_frame: &mut Frame, area: Rect, state: &mut DashboardViewState) { let available_height = area.height as usize; let max_scroll = state.get_total_lenght().saturating_sub(available_height); if state.scroll_state.get_position() > max_scroll { state.scroll_state = state.scroll_state.position(max_scroll); } let scroll_pos = state.scroll_state.get_position(); let mut current_y = area.y as usize; let mut rendered_height = 0; let mut lines_skipped = 0; let half = state.get_total_lenght().saturating_div(2); for group in state.groups.iter() { let group_height = group.monitors.len() + BORDER_LINES_VIEW; if lines_skipped + group_height <= scroll_pos { lines_skipped += group_height; continue; } let visible_height = if lines_skipped < scroll_pos { group_height - (scroll_pos - lines_skipped) } else { group_height } .min(available_height - rendered_height); let group_area = Rect { x: area.x, y: current_y as u16, width: area.width, height: visible_height as u16, }; render_group(main_frame, group_area, group, half > rendered_height); current_y += visible_height; rendered_height += visible_height; lines_skipped += group_height; } } fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first_half: bool) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(0) .constraints([Constraint::Length(1), Constraint::Min(1)]) .split(area); if chunks[0].height == 0 || chunks[1].height == 0 || group.monitors.is_empty() { return; } 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(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, is_first_half); } } fn render_monitor_table( frame: &mut Frame, area: Rect, monitors: &[MonitorViewState], is_first_half: bool, ) { let max_items = area.height as usize; let items_to_show = min(monitors.len(), max_items); let index = if is_first_half { monitors.len().saturating_sub(max_items.saturating_sub(2)) // 2 = Table header + botton } else { 0 }; let monitors = &monitors[index..index + items_to_show.min(monitors.len() - index)]; 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_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) -> Cow<'static, str> { static UP_ICON: &str = "✅"; static DOWN_ICON: &str = "❌"; static UNKNOWN_ICON: &str = "❓"; match status { MonitorStatus::Up => Cow::Borrowed(UP_ICON), MonitorStatus::Down => Cow::Borrowed(DOWN_ICON), MonitorStatus::Unknown => Cow::Borrowed(UNKNOWN_ICON), } } 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: Cow = if monitor.name.len() > MAX_NAME_LENGTH { Cow::Owned(format!( "{:.width$}...", monitor.name, width = MAX_NAME_LENGTH - 3 )) } else { Cow::Borrowed(&monitor.name) }; let response_text = format!("{:>7}ms", monitor.response_time); let uptime_text = format!("{:>7}%", monitor.uptime_24h); let status_line_spans = get_cached_status_line(&monitor.status_history); Row::new(vec![ get_formated_line(format!("{} ", status_icon), status_color, Modifier::empty()), get_formated_line(display_name.to_string(), 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 get_status_span(status: &MonitorStatus) -> &'static Span<'static> { match status { MonitorStatus::Up => &UP_SPAN, MonitorStatus::Down => &DOWN_SPAN, MonitorStatus::Unknown => &UNKNOWN_SPAN, } } fn title_style() -> Style { Style::default() .fg(Color::Yellow) .add_modifier(Modifier::BOLD) } fn calculate_history_hash(status_history: &[MonitorStatus]) -> u64 { let mut hasher = DefaultHasher::new(); status_history .iter() .take(STATUS_LINE_LENGTH) .for_each(|status| { status.hash(&mut hasher); }); hasher.finish() } fn get_cached_status_line(status_history: &[MonitorStatus]) -> Line<'static> { let hash = calculate_history_hash(status_history); let cache = STATUS_LINE_CACHE.get_or_init(|| RwLock::new(HashMap::new())); { let read = cache.read().unwrap(); if let Some(spans) = read.get(&hash) { return Line::from(spans.iter().map(|&span| span.clone()).collect::>()); } } let spans: Vec<&'static Span<'static>> = status_history .iter() .rev() .take(STATUS_LINE_LENGTH) .map(|status| get_status_span(status)) .collect(); let mut write = cache.write().unwrap(); if write.len() > 1000 { let keys_to_remove: Vec<_> = write.keys().take(250).copied().collect(); for key in keys_to_remove { write.remove(&key); } } write.insert(hash, spans.clone()); Line::from(spans.iter().map(|&span| span.clone()).collect::>()) }