#![allow(dead_code)]
use crate::api::models::{Metric, MetricValue};
use std::collections::HashMap;
use super::{
PaginatedList, StateManager, UpdateTracker, MAX_ITEMS_IN_MEMORY, MIN_REFRESH_INTERVAL,
};
#[derive(Debug, Clone)]
pub struct MetricsState {
metrics: PaginatedList<Metric>,
pub selected_index: usize,
pub show_detail: bool,
pub search_query: String,
pub filters: HashMap<String, String>,
pub scroll_offset: usize,
pub error: Option<String>,
update_tracker: UpdateTracker,
metric_history: HashMap<String, Vec<f64>>,
}
impl Default for MetricsState {
fn default() -> Self {
Self {
metrics: PaginatedList::new(MAX_ITEMS_IN_MEMORY),
selected_index: 0,
show_detail: false,
search_query: String::new(),
filters: HashMap::new(),
scroll_offset: 0,
error: None,
update_tracker: UpdateTracker::new(MIN_REFRESH_INTERVAL),
metric_history: HashMap::new(),
}
}
}
impl MetricsState {
pub fn new() -> Self {
Self::default()
}
pub fn update_metrics(&mut self, new_metrics: Vec<Metric>) {
if !self.update_tracker.should_update() {
return;
}
for metric in &new_metrics {
if let Some(value) = Self::extract_numeric_value(&metric.value) {
let history = self.metric_history.entry(metric.name.clone()).or_default();
history.push(value);
if history.len() > 60 {
history.remove(0);
}
}
}
self.metrics.replace(new_metrics);
self.update_tracker.mark_updated();
if self.selected_index >= self.metrics.len() && !self.metrics.is_empty() {
self.selected_index = self.metrics.len() - 1;
}
}
fn extract_numeric_value(value: &MetricValue) -> Option<f64> {
match value {
MetricValue::Gauge(v) => Some(*v),
MetricValue::Counter(v) => Some(*v as f64),
MetricValue::Histogram(h) => Some(h.sum / h.count as f64), MetricValue::Summary(s) => Some(s.sum / s.count as f64), }
}
pub fn get_metric_history(&self, metric_name: &str) -> Option<&Vec<f64>> {
self.metric_history.get(metric_name)
}
pub fn metrics(&self) -> &[Metric] {
self.metrics.items()
}
pub fn should_update(&self) -> bool {
self.update_tracker.should_update()
}
pub fn unique_filtered_metrics(&self) -> Vec<(&Metric, usize)> {
let candidates = self.filtered_metrics();
let mut by_name: std::collections::HashMap<&str, (&Metric, usize)> =
std::collections::HashMap::new();
for metric in candidates {
let entry = by_name.entry(metric.name.as_str()).or_insert((metric, 0));
entry.1 += 1;
if metric.timestamp > entry.0.timestamp {
entry.0 = metric;
}
}
let mut result: Vec<(&Metric, usize)> = by_name.into_values().collect();
result.sort_by(|a, b| a.0.name.cmp(&b.0.name));
result
}
pub fn filtered_metrics(&self) -> Vec<&Metric> {
self.metrics
.items()
.iter()
.filter(|metric| {
if !self.search_query.is_empty() {
let query = self.search_query.to_lowercase();
let matches = metric.name.to_lowercase().contains(&query)
|| metric
.description
.as_ref()
.is_some_and(|d: &String| d.to_lowercase().contains(&query))
|| metric
.unit
.as_ref()
.is_some_and(|u: &String| u.to_lowercase().contains(&query));
if !matches {
return false;
}
}
for (field, value) in &self.filters {
match field.as_str() {
"type" if !metric.metric_type.eq_ignore_ascii_case(value) => {
return false;
},
"unit" => {
if let Some(unit) = &metric.unit {
if !unit.eq_ignore_ascii_case(value.as_str()) {
return false;
}
} else {
return false;
}
},
_ => {},
}
}
true
})
.collect()
}
pub fn selected_metric(&self) -> Option<&Metric> {
let unique = self.unique_filtered_metrics();
unique.get(self.selected_index).map(|(m, _)| *m)
}
pub fn select_previous(&mut self) {
if self.selected_index > 0 {
self.selected_index -= 1;
}
}
pub fn select_next(&mut self) {
let count = self.unique_filtered_metrics().len();
if count > 0 && self.selected_index < count - 1 {
self.selected_index += 1;
}
}
pub fn select_page_up(&mut self, n: usize) {
self.selected_index = self.selected_index.saturating_sub(n);
}
pub fn select_page_down(&mut self, n: usize) {
let count = self.unique_filtered_metrics().len();
if count > 0 {
self.selected_index = (self.selected_index + n).min(count - 1);
}
}
pub fn toggle_detail(&mut self) {
self.show_detail = !self.show_detail;
}
pub fn show_detail_panel(&mut self) {
self.show_detail = true;
}
pub fn hide_detail_panel(&mut self) {
self.show_detail = false;
}
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 set_error(&mut self, error: String) {
self.error = Some(error);
}
pub fn clear_error(&mut self) {
self.error = None;
}
}
impl StateManager for MetricsState {
fn apply_pagination(&mut self) {
}
fn cleanup_old_data(&mut self) {
}
fn item_count(&self) -> usize {
self.metrics.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::models::MetricValue;
fn create_test_metric(name: &str, metric_type: &str, unit: Option<&str>) -> Metric {
Metric {
name: name.to_string(),
description: Some(format!("Test metric: {}", name)),
unit: unit.map(|u| u.to_string()),
metric_type: metric_type.to_string(),
value: MetricValue::Gauge(42.0),
timestamp: 1713360000000000000,
attributes: HashMap::new(),
resource: None,
}
}
#[test]
fn test_metrics_state_default() {
let state = MetricsState::default();
assert_eq!(state.metrics.len(), 0);
assert_eq!(state.selected_index, 0);
assert!(!state.show_detail);
}
#[test]
fn test_update_metrics() {
let mut state = MetricsState::new();
let metrics = vec![
create_test_metric("cpu.usage", "gauge", Some("percent")),
create_test_metric("memory.used", "gauge", Some("bytes")),
];
state.update_metrics(metrics);
assert_eq!(state.metrics.len(), 2);
}
#[test]
fn test_navigation() {
let mut state = MetricsState::new();
let metrics = vec![
create_test_metric("cpu.usage", "gauge", Some("percent")),
create_test_metric("memory.used", "gauge", Some("bytes")),
create_test_metric("disk.io", "counter", Some("operations")),
];
state.update_metrics(metrics);
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 = MetricsState::new();
let metrics = vec![
create_test_metric("cpu.usage", "gauge", Some("percent")),
create_test_metric("memory.used", "gauge", Some("bytes")),
create_test_metric("cpu.temperature", "gauge", Some("celsius")),
];
state.update_metrics(metrics);
state.set_search_query("cpu".to_string());
let filtered = state.filtered_metrics();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_type_filtering() {
let mut state = MetricsState::new();
let metrics = vec![
create_test_metric("cpu.usage", "gauge", Some("percent")),
create_test_metric("requests.total", "counter", Some("count")),
create_test_metric("memory.used", "gauge", Some("bytes")),
];
state.update_metrics(metrics);
state.set_filter("type".to_string(), "gauge".to_string());
let filtered = state.filtered_metrics();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_unit_filtering() {
let mut state = MetricsState::new();
let metrics = vec![
create_test_metric("cpu.usage", "gauge", Some("percent")),
create_test_metric("memory.usage", "gauge", Some("percent")),
create_test_metric("memory.used", "gauge", Some("bytes")),
];
state.update_metrics(metrics);
state.set_filter("unit".to_string(), "percent".to_string());
let filtered = state.filtered_metrics();
assert_eq!(filtered.len(), 2);
}
}