#![allow(dead_code)]
use crate::api::models::LogEntry;
use std::collections::HashMap;
use super::{PaginatedList, UpdateTracker, MAX_ITEMS_IN_MEMORY, MIN_REFRESH_INTERVAL};
#[derive(Debug, Clone)]
pub struct LogsState {
logs: PaginatedList<LogEntry>,
pub selected_index: usize,
pub show_detail: bool,
cached_detail: Option<LogEntry>,
cached_detail_timestamp: Option<i64>,
pub search_query: String,
pub filters: HashMap<String, String>,
pub auto_scroll: bool,
pub scroll_offset: usize,
pub detail_scroll: u16,
pub error: Option<String>,
update_tracker: UpdateTracker,
}
impl Default for LogsState {
fn default() -> Self {
Self {
logs: PaginatedList::new(MAX_ITEMS_IN_MEMORY),
selected_index: 0,
show_detail: false,
cached_detail: None,
cached_detail_timestamp: None,
search_query: String::new(),
filters: HashMap::new(),
auto_scroll: false,
scroll_offset: 0,
detail_scroll: 0,
error: None,
update_tracker: UpdateTracker::new(MIN_REFRESH_INTERVAL),
}
}
}
impl LogsState {
pub fn new() -> Self {
Self::default()
}
pub fn update_logs(&mut self, new_logs: Vec<LogEntry>) {
if !self.update_tracker.should_update() {
return;
}
self.logs.replace(new_logs);
self.update_tracker.mark_updated();
if self.auto_scroll && !self.logs.is_empty() {
self.selected_index = self.logs.len() - 1;
}
if self.selected_index >= self.logs.len() && !self.logs.is_empty() {
self.selected_index = self.logs.len() - 1;
}
}
pub fn filtered_logs(&self) -> Vec<&LogEntry> {
self.logs
.items()
.iter()
.filter(|log| {
if !self.search_query.is_empty() {
let query = self.search_query.to_lowercase();
let matches = log.body.to_lowercase().contains(&query)
|| log.severity.to_lowercase().contains(&query)
|| log
.attributes
.values()
.any(|v: &String| v.to_lowercase().contains(&query));
if !matches {
return false;
}
}
for (field, value) in &self.filters {
match field.as_str() {
"severity" => {
if !log.severity.eq_ignore_ascii_case(value) {
return false;
}
},
_ => {
if let Some(attr_value) = log.attributes.get(field) {
if !attr_value.eq_ignore_ascii_case(value.as_str()) {
return false;
}
} else {
return false;
}
},
}
}
true
})
.collect()
}
pub fn selected_log(&self) -> Option<&LogEntry> {
let filtered = self.filtered_logs();
filtered.get(self.selected_index).copied()
}
pub fn selected_log_detail(&self) -> Option<&LogEntry> {
self.cached_detail.as_ref()
}
pub fn refresh_detail_cache(&mut self) {
let current_ts = self.selected_log().map(|l| l.timestamp);
if current_ts != self.cached_detail_timestamp {
self.cached_detail = self.selected_log().cloned();
self.cached_detail_timestamp = current_ts;
}
}
pub fn select_previous(&mut self) {
if self.selected_index > 0 {
self.selected_index -= 1;
self.auto_scroll = false;
if self.show_detail {
self.detail_scroll = 0;
self.refresh_detail_cache();
}
}
}
pub fn select_next(&mut self) {
let filtered_count = self.filtered_logs().len();
if filtered_count > 0 && self.selected_index < filtered_count - 1 {
self.selected_index += 1;
self.auto_scroll = false;
if self.show_detail {
self.detail_scroll = 0;
self.refresh_detail_cache();
}
}
}
pub fn select_page_up(&mut self, n: usize) {
self.selected_index = self.selected_index.saturating_sub(n);
self.auto_scroll = false;
if self.show_detail {
self.detail_scroll = 0;
self.refresh_detail_cache();
}
}
pub fn select_page_down(&mut self, n: usize) {
let filtered_count = self.filtered_logs().len();
if filtered_count > 0 {
self.selected_index = (self.selected_index + n).min(filtered_count - 1);
self.auto_scroll = false;
if self.show_detail {
self.detail_scroll = 0;
self.refresh_detail_cache();
}
}
}
pub fn toggle_detail(&mut self) {
self.show_detail = !self.show_detail;
}
pub fn show_detail_panel(&mut self) {
self.show_detail = true;
self.detail_scroll = 0;
self.refresh_detail_cache();
}
pub fn hide_detail_panel(&mut self) {
self.show_detail = false;
self.detail_scroll = 0;
}
pub fn scroll_detail_down(&mut self, n: u16) {
self.detail_scroll = self.detail_scroll.saturating_add(n);
}
pub fn scroll_detail_up(&mut self, n: u16) {
self.detail_scroll = self.detail_scroll.saturating_sub(n);
}
pub fn set_search_query(&mut self, query: String) {
self.search_query = query;
self.selected_index = 0;
}
pub fn clear_search(&mut self) {
self.search_query.clear();
self.selected_index = 0;
}
pub fn set_filter(&mut self, field: String, value: String) {
self.filters.insert(field, value);
self.selected_index = 0;
}
pub fn remove_filter(&mut self, field: &str) {
self.filters.remove(field);
self.selected_index = 0;
}
pub fn clear_filters(&mut self) {
self.filters.clear();
self.selected_index = 0;
}
pub fn toggle_auto_scroll(&mut self) {
self.auto_scroll = !self.auto_scroll;
if self.auto_scroll && !self.logs.is_empty() {
self.selected_index = self.logs.len() - 1;
}
}
pub fn set_error(&mut self, error: String) {
self.error = Some(error);
}
pub fn clear_error(&mut self) {
self.error = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::models::Resource;
use crate::state::StateManager;
fn create_test_log(body: &str, severity: &str) -> LogEntry {
LogEntry {
timestamp: 1713360000000000000, severity: severity.to_string(),
severity_text: None,
body: body.to_string(),
attributes: HashMap::new(),
resource: Some(Resource {
attributes: HashMap::new(),
}),
trace_id: None,
span_id: None,
}
}
impl StateManager for LogsState {
fn apply_pagination(&mut self) {
}
fn cleanup_old_data(&mut self) {
}
fn item_count(&self) -> usize {
self.logs.len()
}
}
#[test]
fn test_logs_state_default() {
let state = LogsState::default();
assert_eq!(state.logs.len(), 0);
assert_eq!(state.selected_index, 0);
assert!(!state.show_detail);
assert!(!state.auto_scroll);
}
#[test]
fn test_update_logs() {
let mut state = LogsState::new();
let logs = vec![
create_test_log("Log 1", "INFO"),
create_test_log("Log 2", "ERROR"),
];
state.update_logs(logs);
assert_eq!(state.logs.len(), 2);
assert_eq!(state.selected_index, 0); }
#[test]
fn test_navigation() {
let mut state = LogsState::new();
let logs = vec![
create_test_log("Log 1", "INFO"),
create_test_log("Log 2", "ERROR"),
create_test_log("Log 3", "WARN"),
];
state.update_logs(logs);
state.selected_index = 1;
state.select_next();
assert_eq!(state.selected_index, 2);
state.select_previous();
assert_eq!(state.selected_index, 1);
}
#[test]
fn test_search_filtering() {
let mut state = LogsState::new();
let logs = vec![
create_test_log("User logged in", "INFO"),
create_test_log("Database error", "ERROR"),
create_test_log("User logged out", "INFO"),
];
state.update_logs(logs);
state.set_search_query("user".to_string());
let filtered = state.filtered_logs();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_severity_filtering() {
let mut state = LogsState::new();
let logs = vec![
create_test_log("Log 1", "INFO"),
create_test_log("Log 2", "ERROR"),
create_test_log("Log 3", "INFO"),
];
state.update_logs(logs);
state.set_filter("severity".to_string(), "ERROR".to_string());
let filtered = state.filtered_logs();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].severity, "ERROR");
}
#[test]
fn test_auto_scroll_toggle() {
let mut state = LogsState::new();
assert!(!state.auto_scroll);
state.toggle_auto_scroll();
assert!(state.auto_scroll);
state.toggle_auto_scroll();
assert!(!state.auto_scroll);
}
}