Refactor services widget with unified system metrics display
- Rename alerts widget to hosts widget for clarity - Add sub_service field to ServiceInfo for display differentiation - Integrate system metrics (CPU load, memory, temperature, disk) as service rows - Convert nginx sites to individual sub-service rows with tree structure - Remove nginx site checkmarks - status now shown via row indicators - Update dashboard layout to display system and service data together - Maintain description lines for connection counts and service details Services widget now shows: - System metrics as regular service rows with status - Nginx sites as sub-services with ├─/└─ tree formatting - Regular services with full resource data and descriptions - Unified status indication across all row types
This commit is contained in:
@@ -116,6 +116,8 @@ pub struct ServiceInfo {
|
||||
pub disk_used_gb: f32,
|
||||
#[serde(default)]
|
||||
pub description: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub sub_service: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -6,7 +6,7 @@ use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
use super::{alerts, backup, services, storage, system};
|
||||
use super::{hosts, backup, services, storage, system};
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App) {
|
||||
let host_summaries = app.host_display_data();
|
||||
@@ -56,7 +56,7 @@ pub fn render(frame: &mut Frame, app: &App) {
|
||||
backup::render(frame, primary_host.as_ref(), left_widgets[2]);
|
||||
services::render(frame, primary_host.as_ref(), services_area);
|
||||
|
||||
alerts::render(frame, &host_summaries, left_side[1]);
|
||||
hosts::render(frame, &host_summaries, left_side[1]);
|
||||
|
||||
if app.help_visible() {
|
||||
render_help(frame, size);
|
||||
|
||||
@@ -9,13 +9,13 @@ use crate::ui::widget::{render_widget_data, WidgetData, WidgetStatus, StatusLeve
|
||||
pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
||||
let (severity, _ok_count, _warn_count, _fail_count) = classify_hosts(hosts);
|
||||
|
||||
let title = "Alerts".to_string();
|
||||
let title = "Hosts".to_string();
|
||||
|
||||
let widget_status = match severity {
|
||||
AlertSeverity::Critical => StatusLevel::Error,
|
||||
AlertSeverity::Warning => StatusLevel::Warning,
|
||||
AlertSeverity::Healthy => StatusLevel::Ok,
|
||||
AlertSeverity::Unknown => StatusLevel::Unknown,
|
||||
HostSeverity::Critical => StatusLevel::Error,
|
||||
HostSeverity::Warning => StatusLevel::Warning,
|
||||
HostSeverity::Healthy => StatusLevel::Ok,
|
||||
HostSeverity::Unknown => StatusLevel::Unknown,
|
||||
};
|
||||
|
||||
let mut data = WidgetData::new(
|
||||
@@ -38,10 +38,10 @@ pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
||||
for host in hosts {
|
||||
let (status_text, severity, _emphasize) = host_status(host);
|
||||
let status_level = match severity {
|
||||
AlertSeverity::Critical => StatusLevel::Error,
|
||||
AlertSeverity::Warning => StatusLevel::Warning,
|
||||
AlertSeverity::Healthy => StatusLevel::Ok,
|
||||
AlertSeverity::Unknown => StatusLevel::Unknown,
|
||||
HostSeverity::Critical => StatusLevel::Error,
|
||||
HostSeverity::Warning => StatusLevel::Warning,
|
||||
HostSeverity::Healthy => StatusLevel::Ok,
|
||||
HostSeverity::Unknown => StatusLevel::Unknown,
|
||||
};
|
||||
let update = latest_timestamp(host)
|
||||
.map(|ts| ts.format("%Y-%m-%d %H:%M:%S").to_string())
|
||||
@@ -63,14 +63,14 @@ pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
enum AlertSeverity {
|
||||
enum HostSeverity {
|
||||
Healthy,
|
||||
Warning,
|
||||
Critical,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
fn classify_hosts(hosts: &[HostDisplayData]) -> (AlertSeverity, usize, usize, usize) {
|
||||
fn classify_hosts(hosts: &[HostDisplayData]) -> (HostSeverity, usize, usize, usize) {
|
||||
let mut ok = 0;
|
||||
let mut warn = 0;
|
||||
let mut fail = 0;
|
||||
@@ -78,81 +78,81 @@ fn classify_hosts(hosts: &[HostDisplayData]) -> (AlertSeverity, usize, usize, us
|
||||
for host in hosts {
|
||||
let severity = host_severity(host);
|
||||
match severity {
|
||||
AlertSeverity::Healthy => ok += 1,
|
||||
AlertSeverity::Warning => warn += 1,
|
||||
AlertSeverity::Critical => fail += 1,
|
||||
AlertSeverity::Unknown => warn += 1,
|
||||
HostSeverity::Healthy => ok += 1,
|
||||
HostSeverity::Warning => warn += 1,
|
||||
HostSeverity::Critical => fail += 1,
|
||||
HostSeverity::Unknown => warn += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let highest = if fail > 0 {
|
||||
AlertSeverity::Critical
|
||||
HostSeverity::Critical
|
||||
} else if warn > 0 {
|
||||
AlertSeverity::Warning
|
||||
HostSeverity::Warning
|
||||
} else if ok > 0 {
|
||||
AlertSeverity::Healthy
|
||||
HostSeverity::Healthy
|
||||
} else {
|
||||
AlertSeverity::Unknown
|
||||
HostSeverity::Unknown
|
||||
};
|
||||
|
||||
(highest, ok, warn, fail)
|
||||
}
|
||||
|
||||
fn host_severity(host: &HostDisplayData) -> AlertSeverity {
|
||||
fn host_severity(host: &HostDisplayData) -> HostSeverity {
|
||||
// Check connection status first
|
||||
match host.connection_status {
|
||||
ConnectionStatus::Error => return AlertSeverity::Critical,
|
||||
ConnectionStatus::Timeout => return AlertSeverity::Warning,
|
||||
ConnectionStatus::Unknown => return AlertSeverity::Unknown,
|
||||
ConnectionStatus::Error => return HostSeverity::Critical,
|
||||
ConnectionStatus::Timeout => return HostSeverity::Warning,
|
||||
ConnectionStatus::Unknown => return HostSeverity::Unknown,
|
||||
ConnectionStatus::Connected => {}, // Continue with other checks
|
||||
}
|
||||
|
||||
if host.last_error.is_some() {
|
||||
return AlertSeverity::Critical;
|
||||
return HostSeverity::Critical;
|
||||
}
|
||||
|
||||
if let Some(smart) = host.smart.as_ref() {
|
||||
if smart.summary.critical > 0 {
|
||||
return AlertSeverity::Critical;
|
||||
return HostSeverity::Critical;
|
||||
}
|
||||
if smart.summary.warning > 0 || !smart.issues.is_empty() {
|
||||
return AlertSeverity::Warning;
|
||||
return HostSeverity::Warning;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(services) = host.services.as_ref() {
|
||||
if services.summary.failed > 0 {
|
||||
return AlertSeverity::Critical;
|
||||
return HostSeverity::Critical;
|
||||
}
|
||||
if services.summary.degraded > 0 {
|
||||
return AlertSeverity::Warning;
|
||||
return HostSeverity::Warning;
|
||||
}
|
||||
|
||||
// TODO: Update to use agent-provided system statuses instead of evaluate_performance
|
||||
// let (perf_severity, _) = evaluate_performance(&services.summary);
|
||||
// match perf_severity {
|
||||
// PerfSeverity::Critical => return AlertSeverity::Critical,
|
||||
// PerfSeverity::Warning => return AlertSeverity::Warning,
|
||||
// PerfSeverity::Critical => return HostSeverity::Critical,
|
||||
// PerfSeverity::Warning => return HostSeverity::Warning,
|
||||
// PerfSeverity::Ok => {}
|
||||
// }
|
||||
}
|
||||
|
||||
if let Some(backup) = host.backup.as_ref() {
|
||||
match backup.overall_status.as_str() {
|
||||
"critical" => return AlertSeverity::Critical,
|
||||
"warning" => return AlertSeverity::Warning,
|
||||
"critical" => return HostSeverity::Critical,
|
||||
"warning" => return HostSeverity::Warning,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if host.smart.is_none() && host.services.is_none() && host.backup.is_none() {
|
||||
AlertSeverity::Unknown
|
||||
HostSeverity::Unknown
|
||||
} else {
|
||||
AlertSeverity::Healthy
|
||||
HostSeverity::Healthy
|
||||
}
|
||||
}
|
||||
|
||||
fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
fn host_status(host: &HostDisplayData) -> (String, HostSeverity, bool) {
|
||||
// Check connection status first
|
||||
match host.connection_status {
|
||||
ConnectionStatus::Error => {
|
||||
@@ -161,7 +161,7 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
} else {
|
||||
"Connection error".to_string()
|
||||
};
|
||||
return (msg, AlertSeverity::Critical, true);
|
||||
return (msg, HostSeverity::Critical, true);
|
||||
},
|
||||
ConnectionStatus::Timeout => {
|
||||
let msg = if let Some(error) = &host.last_error {
|
||||
@@ -169,28 +169,28 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
} else {
|
||||
"Keep-alive timeout".to_string()
|
||||
};
|
||||
return (msg, AlertSeverity::Warning, true);
|
||||
return (msg, HostSeverity::Warning, true);
|
||||
},
|
||||
ConnectionStatus::Unknown => {
|
||||
return ("No data received".to_string(), AlertSeverity::Unknown, true);
|
||||
return ("No data received".to_string(), HostSeverity::Unknown, true);
|
||||
},
|
||||
ConnectionStatus::Connected => {}, // Continue with other checks
|
||||
}
|
||||
|
||||
if let Some(error) = &host.last_error {
|
||||
return (format!("error: {}", error), AlertSeverity::Critical, true);
|
||||
return (format!("error: {}", error), HostSeverity::Critical, true);
|
||||
}
|
||||
|
||||
if let Some(smart) = host.smart.as_ref() {
|
||||
if smart.summary.critical > 0 {
|
||||
return (
|
||||
"critical: SMART critical".to_string(),
|
||||
AlertSeverity::Critical,
|
||||
HostSeverity::Critical,
|
||||
true,
|
||||
);
|
||||
}
|
||||
if let Some(issue) = smart.issues.first() {
|
||||
return (format!("warning: {}", issue), AlertSeverity::Warning, true);
|
||||
return (format!("warning: {}", issue), HostSeverity::Warning, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +198,14 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
if services.summary.failed > 0 {
|
||||
return (
|
||||
format!("critical: {} failed svc", services.summary.failed),
|
||||
AlertSeverity::Critical,
|
||||
HostSeverity::Critical,
|
||||
true,
|
||||
);
|
||||
}
|
||||
if services.summary.degraded > 0 {
|
||||
return (
|
||||
format!("warning: {} degraded svc", services.summary.degraded),
|
||||
AlertSeverity::Warning,
|
||||
HostSeverity::Warning,
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -217,14 +217,14 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
// PerfSeverity::Critical => {
|
||||
// return (
|
||||
// format!("critical: {}", reason_text),
|
||||
// AlertSeverity::Critical,
|
||||
// HostSeverity::Critical,
|
||||
// true,
|
||||
// );
|
||||
// }
|
||||
// PerfSeverity::Warning => {
|
||||
// return (
|
||||
// format!("warning: {}", reason_text),
|
||||
// AlertSeverity::Warning,
|
||||
// HostSeverity::Warning,
|
||||
// true,
|
||||
// );
|
||||
// }
|
||||
@@ -238,14 +238,14 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
"critical" => {
|
||||
return (
|
||||
"critical: backup failed".to_string(),
|
||||
AlertSeverity::Critical,
|
||||
HostSeverity::Critical,
|
||||
true,
|
||||
);
|
||||
}
|
||||
"warning" => {
|
||||
return (
|
||||
"warning: backup warning".to_string(),
|
||||
AlertSeverity::Warning,
|
||||
HostSeverity::Warning,
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -260,10 +260,10 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
"pending: no recent data"
|
||||
};
|
||||
|
||||
return (status.to_string(), AlertSeverity::Warning, false);
|
||||
return (status.to_string(), HostSeverity::Warning, false);
|
||||
}
|
||||
|
||||
("ok".to_string(), AlertSeverity::Healthy, false)
|
||||
("ok".to_string(), HostSeverity::Healthy, false)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod alerts;
|
||||
pub mod hosts;
|
||||
pub mod backup;
|
||||
pub mod dashboard;
|
||||
pub mod services;
|
||||
|
||||
@@ -91,16 +91,31 @@ fn render_metrics(
|
||||
vec![]
|
||||
};
|
||||
|
||||
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),
|
||||
],
|
||||
);
|
||||
if svc.sub_service {
|
||||
// Sub-services only show name and status, no memory/CPU/disk data
|
||||
data.add_row(
|
||||
Some(WidgetStatus::new(status_level)),
|
||||
description,
|
||||
vec![
|
||||
svc.name.clone(),
|
||||
"".to_string(),
|
||||
"".to_string(),
|
||||
"".to_string(),
|
||||
],
|
||||
);
|
||||
} 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render_widget_data(frame, area, data);
|
||||
|
||||
Reference in New Issue
Block a user