151 lines
4.2 KiB
Rust
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()
|
|
}
|
|
}
|
|
|