Replace misleading system total quotas with actual service-specific quota detection. Services now only show quotas when real limits exist. Changes: - Add get_service_disk_quota method with filesystem quota detection - Add check_filesystem_quota and docker storage quota helpers - Remove automatic assignment of system totals as fake quotas - Update dashboard formatting to show usage only when no quota exists Display behavior: - Services with real limits: "2.1/8.0" (usage/quota) - Services without limits: "2.1" (usage only) This provides accurate monitoring instead of misleading system capacity values that suggested all services had massive quotas.
171 lines
5.6 KiB
Rust
171 lines
5.6 KiB
Rust
use ratatui::layout::Rect;
|
|
use ratatui::Frame;
|
|
|
|
use crate::app::HostDisplayData;
|
|
use crate::data::metrics::ServiceStatus;
|
|
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, connection_status_message, WidgetData, WidgetStatus, StatusLevel};
|
|
use crate::app::ConnectionStatus;
|
|
|
|
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
|
match host {
|
|
Some(data) => {
|
|
match (&data.connection_status, data.services.as_ref()) {
|
|
(ConnectionStatus::Connected, Some(metrics)) => {
|
|
render_metrics(frame, data, metrics, area);
|
|
}
|
|
(ConnectionStatus::Connected, None) => {
|
|
render_placeholder(
|
|
frame,
|
|
area,
|
|
"Services",
|
|
&format!("Host {} has no service metrics yet", data.name),
|
|
);
|
|
}
|
|
(status, _) => {
|
|
render_placeholder(
|
|
frame,
|
|
area,
|
|
"Services",
|
|
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
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 title = "Services".to_string();
|
|
|
|
// Use agent-calculated services status
|
|
let widget_status = status_level_from_agent_status(summary.services_status.as_ref());
|
|
|
|
let mut data = WidgetData::new(
|
|
title,
|
|
Some(WidgetStatus::new(widget_status)),
|
|
vec!["Service".to_string(), "RAM (GB)".to_string(), "CPU (%)".to_string(), "Disk (GB)".to_string()]
|
|
);
|
|
|
|
|
|
if metrics.services.is_empty() {
|
|
data.add_row(
|
|
None,
|
|
vec![],
|
|
vec![
|
|
"No services reported".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
],
|
|
);
|
|
render_widget_data(frame, area, data);
|
|
return;
|
|
}
|
|
|
|
let mut services = metrics.services.clone();
|
|
services.sort_by(|a, b| {
|
|
// First, determine the primary service name for grouping
|
|
let primary_a = a.sub_service.as_ref().unwrap_or(&a.name);
|
|
let primary_b = b.sub_service.as_ref().unwrap_or(&b.name);
|
|
|
|
// Sort by primary service name first
|
|
match primary_a.cmp(primary_b) {
|
|
std::cmp::Ordering::Equal => {
|
|
// Same primary service, put parent service first, then sub-services alphabetically
|
|
match (a.sub_service.as_ref(), b.sub_service.as_ref()) {
|
|
(None, Some(_)) => std::cmp::Ordering::Less, // Parent comes before sub-services
|
|
(Some(_), None) => std::cmp::Ordering::Greater, // Sub-services come after parent
|
|
_ => a.name.cmp(&b.name), // Both same type, sort by name
|
|
}
|
|
}
|
|
other => other, // Different primary services, sort alphabetically
|
|
}
|
|
});
|
|
|
|
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,
|
|
};
|
|
|
|
// Service row with optional description(s)
|
|
let description = if let Some(desc_vec) = &svc.description {
|
|
desc_vec.clone()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
if svc.sub_service.is_some() {
|
|
// Sub-services only show name and status, no memory/CPU/disk data
|
|
data.add_row_with_sub_service(
|
|
Some(WidgetStatus::new(status_level)),
|
|
description,
|
|
vec![
|
|
svc.name.clone(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
],
|
|
svc.sub_service.clone(),
|
|
);
|
|
} else {
|
|
// Regular services show all columns
|
|
data.add_row(
|
|
Some(WidgetStatus::new(status_level)),
|
|
description,
|
|
vec![
|
|
svc.name.clone(),
|
|
format_memory_value(svc.memory_used_mb, svc.memory_quota_mb),
|
|
format_cpu_value(svc.cpu_percent),
|
|
format_disk_value(svc.disk_used_gb, svc.disk_quota_gb),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
render_widget_data(frame, area, data);
|
|
}
|
|
|
|
|
|
|
|
fn format_memory_value(used: f32, quota: f32) -> String {
|
|
let used_gb = used / 1000.0;
|
|
let quota_gb = quota / 1000.0;
|
|
|
|
// Show usage/quota format only if quota exists, otherwise just usage
|
|
if quota > 0.05 {
|
|
format!("{:.1}/{:.1}", used_gb, quota_gb)
|
|
} else if used > 0.05 {
|
|
format!("{:.1}", used_gb)
|
|
} else {
|
|
"0.0".to_string()
|
|
}
|
|
}
|
|
|
|
fn format_cpu_value(cpu_percent: f32) -> String {
|
|
if cpu_percent >= 0.1 {
|
|
format!("{:.1}", cpu_percent)
|
|
} else {
|
|
"0.0".to_string()
|
|
}
|
|
}
|
|
|
|
fn format_disk_value(used: f32, quota: f32) -> String {
|
|
// Show usage/quota format only if quota exists, otherwise just usage
|
|
if quota > 0.05 {
|
|
format!("{:.1}/{:.1}", used, quota)
|
|
} else {
|
|
format!("{:.1}", used)
|
|
}
|
|
}
|
|
|