use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ layout::{Alignment, Constraint, Margin, Rect}, style::{Color, Style}, text::Text, widgets::{ Block, Padding, Cell, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, }, Frame }; use super::{Component, PartialComponent, CurrentTab}; use crate::{ action::Action, config::Config, palette::StylePalette, types::{ActionLevel, ActionTarget} }; #[derive(Debug, Default)] struct LogDetails { time: chrono::DateTime, level: ActionLevel, message: String, } #[derive(Debug, Default)] pub struct EventLogs { is_active: bool, scroll_state: ScrollbarState, table_state: TableState, logs: std::collections::VecDeque, palette: StylePalette } impl EventLogs { const MAX_LOGS: usize = 50; fn add_new_log(&mut self, message: String, level: ActionLevel) { self.logs.push_front(LogDetails { time: chrono::Local::now(), level, message, }); if self.logs.len() > Self::MAX_LOGS { let _ = self.logs.pop_back(); } } fn first_row(&mut self) { if self.logs.len() > 0 { self.table_state.select(Some(0)); self.scroll_state = self.scroll_state.position(0); } } fn next_row(&mut self) { let i = match self.table_state.selected() { Some(i) => { if i >= self.logs.len() - 1 { i } else { i + 1 } }, None => 0, }; self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i); } fn last_row(&mut self) { if self.logs.len() > 0 { let last = self.logs.len() - 1; self.table_state.select(Some(last)); self.scroll_state = self.scroll_state.position(last); } } fn previous_row(&mut self) { let i = match self.table_state.selected() { Some(i) => { if i == 0 { 0 } else { i - 1 } }, None => 0 }; self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i); } } impl PartialComponent for EventLogs { fn set_active(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::EventLogs => self.is_active = true, _ => { self.is_active = false; self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); } } } } impl Component for EventLogs { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Validator) { self.palette.with_normal_style(style.get("normal_style").copied()); self.palette.with_hover_style(style.get("hover_style").copied()); self.palette.with_normal_border_style(style.get("normal_border_style").copied()); self.palette.with_hover_border_style(style.get("hover_border_style").copied()); self.palette.with_normal_title_style(style.get("normal_title_style").copied()); self.palette.with_hover_title_style(style.get("hover_title_style").copied()); self.palette.with_highlight_style(style.get("highlight_style").copied()); self.palette.with_scrollbar_style(style.get("scrollbar_style").copied()); } Ok(()) } fn handle_key_event(&mut self, key: KeyEvent) -> Result> { match key.code { KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(), KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(), KeyCode::Char('g') if self.is_active => self.first_row(), KeyCode::Char('G') if self.is_active => self.last_row(), _ => {}, }; Ok(None) } fn update(&mut self, action: Action) -> Result> { match action { Action::EventLog(message, level, target) if target == ActionTarget::ValidatorLog => self.add_new_log(message, level), _ => {} }; Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [_, _, _, place] = super::validator_layout(area); let (border_style, border_type) = self.palette.create_border_style(self.is_active); let error_style = Style::new().fg(Color::Red); let warn_style = Style::new().fg(Color::Yellow); let info_style = Style::new().fg(Color::Green); let table = Table::new( self.logs .iter() .map(|log| { let style = match log.level { ActionLevel::Info => info_style, ActionLevel::Warn => warn_style, ActionLevel::Error => error_style, }; Row::new(vec![ Cell::from(Text::from(log.time.format("%H:%M:%S").to_string()).style(style).alignment(Alignment::Left)), Cell::from(Text::from(log.message.clone()).style(style).alignment(Alignment::Left)), ]) }), [ Constraint::Max(8), Constraint::Min(0), ], ) .column_spacing(1) .highlight_style(self.palette.create_highlight_style()) .block(Block::bordered() .border_style(border_style) .border_type(border_type) .padding(Padding::right(2)) .title_alignment(Alignment::Right) .title_style(self.palette.create_title_style(false)) .title("Action Logs")); let scrollbar = Scrollbar::default() .orientation(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .style(self.palette.create_scrollbar_style()); frame.render_stateful_widget(table, place, &mut self.table_state); frame.render_stateful_widget( scrollbar, place.inner(Margin { vertical: 1, horizontal: 1 }), &mut self.scroll_state, ); Ok(()) } }