From d4531ef2e83180ce57c776271f7fa36de245ad70 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Mon, 20 Oct 2025 13:01:42 +0200 Subject: [PATCH] Hide backup panel when no backup data is present - Add has_data() method to BackupWidget to check if backup metrics exist - Modify dashboard layout to conditionally show backup panel only when data exists - When no backup data: system panel takes full left side height - When backup data exists: system and backup panels share left side equally Prevents empty backup panel from taking up screen space unnecessarily. --- dashboard/src/ui/mod.rs | 37 ++++-- dashboard/src/ui/widgets/backup.rs | 183 +++++++++++++++++------------ 2 files changed, 134 insertions(+), 86 deletions(-) diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index f56427c..16ce86e 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -275,21 +275,40 @@ impl TuiApp { ]) .split(main_chunks[1]); - // Left side: system on top, backup on bottom (equal height) - let left_chunks = ratatui::layout::Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(ThemeLayout::SYSTEM_PANEL_HEIGHT), // System section - Constraint::Percentage(ThemeLayout::BACKUP_PANEL_HEIGHT), // Backup section - ]) - .split(content_chunks[0]); + // Check if backup panel should be shown + let show_backup = if let Some(hostname) = self.current_host.clone() { + let host_widgets = self.get_or_create_host_widgets(&hostname); + host_widgets.backup_widget.has_data() + } else { + false + }; + + // Left side: dynamic layout based on backup data availability + let left_chunks = if show_backup { + // Show both system and backup panels + ratatui::layout::Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(ThemeLayout::SYSTEM_PANEL_HEIGHT), // System section + Constraint::Percentage(ThemeLayout::BACKUP_PANEL_HEIGHT), // Backup section + ]) + .split(content_chunks[0]) + } else { + // Show only system panel (full height) + ratatui::layout::Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(100)]) // System section takes full height + .split(content_chunks[0]) + }; // Render title bar self.render_btop_title(frame, main_chunks[0], metric_store); // Render new panel layout self.render_system_panel(frame, left_chunks[0], metric_store); - self.render_backup_panel(frame, left_chunks[1]); + if show_backup && left_chunks.len() > 1 { + self.render_backup_panel(frame, left_chunks[1]); + } // Render services widget for current host if let Some(hostname) = self.current_host.clone() { diff --git a/dashboard/src/ui/widgets/backup.rs b/dashboard/src/ui/widgets/backup.rs index c566ec9..54f0f4b 100644 --- a/dashboard/src/ui/widgets/backup.rs +++ b/dashboard/src/ui/widgets/backup.rs @@ -7,7 +7,7 @@ use ratatui::{ use tracing::debug; use super::Widget; -use crate::ui::theme::{Typography, StatusIcons}; +use crate::ui::theme::{StatusIcons, Typography}; /// Backup widget displaying backup status, services, and repository information #[derive(Clone)] @@ -73,7 +73,13 @@ impl BackupWidget { has_data: false, } } - + + /// Check if the backup widget has any data to display + pub fn has_data(&self) -> bool { + self.has_data + } + + /// Format duration for display fn format_duration(&self) -> String { match self.duration_seconds { @@ -89,7 +95,7 @@ impl BackupWidget { None => "—".to_string(), } } - + /// Format timestamp for display fn format_last_run(&self) -> String { match self.last_run_timestamp { @@ -106,8 +112,7 @@ impl BackupWidget { None => "—".to_string(), } } - - + /// Format disk usage in format "usedGB/totalGB" fn format_repo_size(&self) -> String { match (self.backup_disk_used_gb, self.backup_disk_total_gb) { @@ -123,7 +128,7 @@ impl BackupWidget { _ => "—".to_string(), } } - + /// Format size with proper units (xxxkB/MB/GB/TB) fn format_size_with_proper_units(size_gb: f32) -> String { if size_gb >= 1000.0 { @@ -137,7 +142,7 @@ impl BackupWidget { let size_mb = size_gb * 1024.0; format!("{:.1}MB", size_mb) } else if size_gb >= 0.000001 { - // kB range (size_gb * 1024 * 1024 = kB) + // kB range (size_gb * 1024 * 1024 = kB) let size_kb = size_gb * 1024.0 * 1024.0; format!("{:.0}kB", size_kb) } else { @@ -146,9 +151,7 @@ impl BackupWidget { format!("{:.0}B", size_bytes) } } - - - + /// Format product name display fn format_product_name(&self) -> String { if let Some(ref product_name) = self.backup_disk_product_name { @@ -157,7 +160,7 @@ impl BackupWidget { "P/N: Unknown".to_string() } } - + /// Format serial number display fn format_serial_number(&self) -> String { if let Some(ref serial) = self.backup_disk_serial_number { @@ -166,12 +169,12 @@ impl BackupWidget { "S/N: Unknown".to_string() } } - + /// Extract service name from metric name (e.g., "backup_service_gitea_status" -> "gitea") fn extract_service_name(metric_name: &str) -> Option { if metric_name.starts_with("backup_service_") { let name_part = &metric_name[15..]; // Remove "backup_service_" prefix - + // Try to extract service name by removing known suffixes if let Some(service_name) = name_part.strip_suffix("_status") { Some(service_name.to_string()) @@ -196,23 +199,31 @@ impl Widget for BackupWidget { fn update_from_metrics(&mut self, metrics: &[&Metric]) { debug!("Backup widget updating with {} metrics", metrics.len()); for metric in metrics { - debug!("Backup metric: {} = {:?} (status: {:?})", metric.name, metric.value, metric.status); + debug!( + "Backup metric: {} = {:?} (status: {:?})", + metric.name, metric.value, metric.status + ); } - + // Also debug the service_data after processing debug!("Processing individual service metrics..."); - + // Log how many metrics are backup service metrics - let service_metric_count = metrics.iter() + let service_metric_count = metrics + .iter() .filter(|m| m.name.starts_with("backup_service_")) .count(); - debug!("Found {} backup_service_ metrics out of {} total backup metrics", - service_metric_count, metrics.len()); - + debug!( + "Found {} backup_service_ metrics out of {} total backup metrics", + service_metric_count, + metrics.len() + ); + // Reset service metrics self.service_metrics.clear(); - let mut service_data: std::collections::HashMap = std::collections::HashMap::new(); - + let mut service_data: std::collections::HashMap = + std::collections::HashMap::new(); + for metric in metrics { match metric.name.as_str() { "backup_overall_status" => { @@ -263,15 +274,20 @@ impl Widget for BackupWidget { _ => { // Handle individual service metrics if let Some(service_name) = Self::extract_service_name(&metric.name) { - debug!("Extracted service name '{}' from metric '{}'", service_name, metric.name); - let entry = service_data.entry(service_name.clone()).or_insert_with(|| ServiceMetricData { - name: service_name, - status: Status::Unknown, - exit_code: None, - archive_count: None, - repo_size_gb: None, + debug!( + "Extracted service name '{}' from metric '{}'", + service_name, metric.name + ); + let entry = service_data.entry(service_name.clone()).or_insert_with(|| { + ServiceMetricData { + name: service_name, + status: Status::Unknown, + exit_code: None, + archive_count: None, + repo_size_gb: None, + } }); - + if metric.name.ends_with("_status") { entry.status = metric.status; debug!("Set status for {}: {:?}", entry.name, entry.status); @@ -279,35 +295,50 @@ impl Widget for BackupWidget { entry.exit_code = metric.value.as_i64(); } else if metric.name.ends_with("_archive_count") { entry.archive_count = metric.value.as_i64(); - debug!("Set archive_count for {}: {:?}", entry.name, entry.archive_count); + debug!( + "Set archive_count for {}: {:?}", + entry.name, entry.archive_count + ); } else if metric.name.ends_with("_repo_size_gb") { entry.repo_size_gb = metric.value.as_f32(); - debug!("Set repo_size_gb for {}: {:?}", entry.name, entry.repo_size_gb); + debug!( + "Set repo_size_gb for {}: {:?}", + entry.name, entry.repo_size_gb + ); } } else { - debug!("Could not extract service name from metric: {}", metric.name); + debug!( + "Could not extract service name from metric: {}", + metric.name + ); } } } } - + // Convert service data to sorted vector let mut services: Vec = service_data.into_values().collect(); services.sort_by(|a, b| a.name.cmp(&b.name)); self.service_metrics = services; - + self.has_data = !metrics.is_empty(); - - debug!("Backup widget updated: status={:?}, services={}, total_size={:?}GB", - self.overall_status, self.service_metrics.len(), self.total_repo_size_gb); - + + debug!( + "Backup widget updated: status={:?}, services={}, total_size={:?}GB", + self.overall_status, + self.service_metrics.len(), + self.total_repo_size_gb + ); + // Debug individual service data for service in &self.service_metrics { - debug!("Service {}: status={:?}, archives={:?}, size={:?}GB", - service.name, service.status, service.archive_count, service.repo_size_gb); + debug!( + "Service {}: status={:?}, archives={:?}, size={:?}GB", + service.name, service.status, service.archive_count, service.repo_size_gb + ); } } - + fn render(&mut self, frame: &mut Frame, area: Rect) { // Split area into header and services list let chunks = Layout::default() @@ -317,10 +348,10 @@ impl Widget for BackupWidget { Constraint::Min(0), // Service list ]) .split(area); - + // Render backup overview self.render_backup_overview(frame, chunks[0]); - + // Render services list self.render_services_list(frame, chunks[1]); } @@ -341,14 +372,14 @@ impl BackupWidget { Constraint::Min(0), // Remaining space ]) .split(area); - + // "Latest backup" header - let header_para = Paragraph::new("Latest backup:") - .style(Typography::widget_title()); + let header_para = Paragraph::new("Latest backup:").style(Typography::widget_title()); frame.render_widget(header_para, content_chunks[0]); - + // Status line - let status_text = format!("Status: {}", + let status_text = format!( + "Status: {}", match self.overall_status { Status::Ok => "OK", Status::Warning => "Warning", @@ -359,72 +390,71 @@ impl BackupWidget { let status_spans = StatusIcons::create_status_spans(self.overall_status, &status_text); let status_para = Paragraph::new(ratatui::text::Line::from(status_spans)); frame.render_widget(status_para, content_chunks[1]); - + // Duration and last run - let time_text = format!("Duration: {} • Last: {}", - self.format_duration(), + let time_text = format!( + "Duration: {} • Last: {}", + self.format_duration(), self.format_last_run() ); - let time_para = Paragraph::new(time_text) - .style(Typography::secondary()); + let time_para = Paragraph::new(time_text).style(Typography::secondary()); frame.render_widget(time_para, content_chunks[2]); - + // Repository size let size_text = format!("Disk usage: {}", self.format_repo_size()); - let size_para = Paragraph::new(size_text) - .style(Typography::secondary()); + let size_para = Paragraph::new(size_text).style(Typography::secondary()); frame.render_widget(size_para, content_chunks[3]); - + // Product name let product_text = self.format_product_name(); - let product_para = Paragraph::new(product_text) - .style(Typography::secondary()); + let product_para = Paragraph::new(product_text).style(Typography::secondary()); frame.render_widget(product_para, content_chunks[4]); - + // Serial number let serial_text = self.format_serial_number(); - let serial_para = Paragraph::new(serial_text) - .style(Typography::secondary()); + let serial_para = Paragraph::new(serial_text).style(Typography::secondary()); frame.render_widget(serial_para, content_chunks[5]); } - + /// Render services list section fn render_services_list(&self, frame: &mut Frame, area: Rect) { if area.height < 1 { return; } - + let available_lines = area.height as usize; let services_to_show = self.service_metrics.iter().take(available_lines); - + let mut y_offset = 0; for service in services_to_show { if y_offset >= available_lines { break; } - + let service_area = Rect { x: area.x, y: area.y + y_offset as u16, width: area.width, height: 1, }; - - let service_info = if let (Some(archives), Some(size_gb)) = (service.archive_count, service.repo_size_gb) { + + let service_info = if let (Some(archives), Some(size_gb)) = + (service.archive_count, service.repo_size_gb) + { let size_str = Self::format_size_with_proper_units(size_gb); format!(" {}archives {}", archives, size_str) } else { String::new() }; - + let service_text = format!("{}{}", service.name, service_info); let service_spans = StatusIcons::create_status_spans(service.status, &service_text); let service_para = Paragraph::new(ratatui::text::Line::from(service_spans)); - + frame.render_widget(service_para, service_area); y_offset += 1; } - + // If there are more services than we can show, indicate that if self.service_metrics.len() > available_lines { let more_count = self.service_metrics.len() - available_lines; @@ -435,11 +465,10 @@ impl BackupWidget { width: area.width, height: 1, }; - + let more_text = format!("... and {} more services", more_count); - let more_para = Paragraph::new(more_text) - .style(Typography::muted()); - + let more_para = Paragraph::new(more_text).style(Typography::muted()); + frame.render_widget(more_para, last_line_area); } } @@ -450,4 +479,4 @@ impl Default for BackupWidget { fn default() -> Self { Self::new() } -} \ No newline at end of file +}