2025-10-12 14:53:27 +02:00

151 lines
4.2 KiB
Rust

use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::Frame;
use crate::app::HostDisplayData;
use crate::data::metrics::{ServiceStatus, ServiceSummary};
use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, WidgetValue, StatusLevel};
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
match host {
Some(data) => {
if let Some(metrics) = data.services.as_ref() {
render_metrics(frame, data, metrics, area);
} else {
render_placeholder(
frame,
area,
"Services",
&format!("Host {} has no service metrics yet", data.name),
);
}
}
None => render_placeholder(frame, area, "Services", "No hosts configured"),
}
}
fn render_metrics(
frame: &mut Frame,
_host: &HostDisplayData,
metrics: &crate::data::metrics::ServiceMetrics,
area: Rect,
) {
let summary = &metrics.summary;
let color = summary_color(summary);
let title = format!(
"Services • ok:{} warn:{} fail:{}",
summary.healthy, summary.degraded, summary.failed
);
let widget_status = if summary.failed > 0 {
StatusLevel::Error
} else if summary.degraded > 0 {
StatusLevel::Warning
} else {
StatusLevel::Ok
};
let mut data = WidgetData::new(
title,
Some(WidgetStatus::new(widget_status)),
vec!["Service".to_string(), "Memory".to_string(), "Disk".to_string(), "Description".to_string()]
);
if metrics.services.is_empty() {
data.add_row(
None,
"",
vec![
WidgetValue::new("No services reported"),
WidgetValue::new(""),
WidgetValue::new(""),
WidgetValue::new(""),
],
);
render_widget_data(frame, area, data);
return;
}
let mut services = metrics.services.clone();
services.sort_by(|a, b| {
status_weight(&a.status)
.cmp(&status_weight(&b.status))
.then_with(|| a.name.cmp(&b.name))
});
for svc in services {
let status_level = match svc.status {
ServiceStatus::Running => StatusLevel::Ok,
ServiceStatus::Degraded => StatusLevel::Warning,
ServiceStatus::Restarting => StatusLevel::Warning,
ServiceStatus::Stopped => StatusLevel::Error,
};
data.add_row(
Some(WidgetStatus::new(status_level)),
"",
vec![
WidgetValue::new(svc.name.clone()),
WidgetValue::new(format_memory_value(svc.memory_used_mb, svc.memory_quota_mb)),
WidgetValue::new(format_disk_value(svc.disk_used_gb)),
WidgetValue::new(svc.description.as_deref().unwrap_or("")),
],
);
}
render_widget_data(frame, area, data);
}
fn status_weight(status: &ServiceStatus) -> i32 {
match status {
ServiceStatus::Stopped => 0,
ServiceStatus::Degraded => 1,
ServiceStatus::Restarting => 2,
ServiceStatus::Running => 3,
}
}
fn status_symbol(status: &ServiceStatus) -> (&'static str, Color) {
match status {
ServiceStatus::Running => ("", Color::Green),
ServiceStatus::Degraded => ("!", Color::Yellow),
ServiceStatus::Restarting => ("", Color::Yellow),
ServiceStatus::Stopped => ("", Color::Red),
}
}
fn summary_color(summary: &ServiceSummary) -> Color {
if summary.failed > 0 {
Color::Red
} else if summary.degraded > 0 {
Color::Yellow
} else {
Color::Green
}
}
fn format_memory_value(used: f32, quota: f32) -> String {
if quota > 0.05 {
format!("{:.1}/{:.1} MiB", used, quota)
} else if used > 0.05 {
format!("{:.1} MiB", used)
} else {
"".to_string()
}
}
fn format_disk_value(used: f32) -> String {
if used >= 1.0 {
format!("{:.1} GiB", used)
} else if used >= 0.001 {
// 1 MB or more
format!("{:.0} MiB", used * 1024.0)
} else if used > 0.0 {
format!("<1 MiB")
} else {
"".to_string()
}
}