Update backup widget layout and fix system widget Single label

Backup widget:
- Restructure to match new layout specification
- Add section headers: Latest backup, Disk, Repos
- Show timestamp with status icon and duration as sub-item
- Display disk info with product name, S/N, and usage in tree structure
- List repositories with archive count and size
- Remove old render methods and unused imports

System widget:
- Hide (Single) storage type label for cleaner display
This commit is contained in:
Christoffer Martinsson 2025-10-23 19:53:00 +02:00
parent 997b30a9c0
commit b391448d33
2 changed files with 95 additions and 126 deletions

View File

@ -1,6 +1,6 @@
use cm_dashboard_shared::{Metric, Status};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
layout::Rect,
widgets::Paragraph,
Frame,
};
@ -340,138 +340,103 @@ impl Widget for BackupWidget {
}
fn render(&mut self, frame: &mut Frame, area: Rect) {
// Split area into header and services list
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(6), // Header with "Latest backup" title, status, P/N, and S/N
Constraint::Min(0), // Service list
])
.split(area);
let mut lines = Vec::new();
// Render backup overview
self.render_backup_overview(frame, chunks[0]);
// Latest backup section
lines.push(ratatui::text::Line::from(vec![
ratatui::text::Span::styled("Latest backup:", Typography::widget_title())
]));
// Timestamp with status icon
let timestamp_text = if let Some(timestamp) = self.last_run_timestamp {
self.format_timestamp(timestamp)
} else {
"Unknown".to_string()
};
let timestamp_spans = StatusIcons::create_status_spans(
self.overall_status,
&timestamp_text
);
lines.push(ratatui::text::Line::from(timestamp_spans));
// Render services list
self.render_services_list(frame, chunks[1]);
// Duration as sub-item
if let Some(duration) = self.duration_seconds {
let duration_text = self.format_duration(duration);
lines.push(ratatui::text::Line::from(vec![
ratatui::text::Span::raw(" └─ "),
ratatui::text::Span::styled(format!("Duration: {}", duration_text), Typography::secondary())
]));
}
// Disk section
lines.push(ratatui::text::Line::from(vec![
ratatui::text::Span::styled("Disk:", Typography::widget_title())
]));
// Disk product name with status
if let Some(product) = &self.backup_disk_product_name {
let disk_spans = StatusIcons::create_status_spans(
Status::Ok, // Assuming disk is OK if we have data
product
);
lines.push(ratatui::text::Line::from(disk_spans));
// Serial number as sub-item
if let Some(serial) = &self.backup_disk_serial_number {
lines.push(ratatui::text::Line::from(vec![
ratatui::text::Span::raw(" ├─ "),
ratatui::text::Span::styled(format!("S/N: {}", serial), Typography::secondary())
]));
}
// Usage as sub-item
if let (Some(used), Some(total)) = (self.backup_disk_used_gb, self.backup_disk_total_gb) {
let used_str = Self::format_size_with_proper_units(used);
let total_str = Self::format_size_with_proper_units(total);
lines.push(ratatui::text::Line::from(vec![
ratatui::text::Span::raw(" └─ "),
ratatui::text::Span::styled(format!("Usage: {}/{}", used_str, total_str), Typography::secondary())
]));
}
}
// Repos section
lines.push(ratatui::text::Line::from(vec![
ratatui::text::Span::styled("Repos:", Typography::widget_title())
]));
// Repository list
for service in &self.service_metrics {
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);
let repo_text = format!("{} ({}) {}", service.name, archives, size_str);
let repo_spans = StatusIcons::create_status_spans(service.status, &repo_text);
lines.push(ratatui::text::Line::from(repo_spans));
}
}
let paragraph = Paragraph::new(ratatui::text::Text::from(lines));
frame.render_widget(paragraph, area);
}
}
impl BackupWidget {
/// Render backup overview section
fn render_backup_overview(&self, frame: &mut Frame, area: Rect) {
let content_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // "Latest backup" header
Constraint::Length(1), // Status line
Constraint::Length(1), // Duration and last run
Constraint::Length(1), // Repository size
Constraint::Length(1), // Product name
Constraint::Length(1), // Serial number
Constraint::Min(0), // Remaining space
])
.split(area);
// "Latest backup" header
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: {}",
match self.overall_status {
Status::Ok => "OK",
Status::Pending => "Pending",
Status::Warning => "Warning",
Status::Critical => "Failed",
Status::Unknown => "Unknown",
}
);
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(),
self.format_last_run()
);
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());
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());
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());
frame.render_widget(serial_para, content_chunks[5]);
/// Format timestamp for display
fn format_timestamp(&self, timestamp: i64) -> String {
let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
.unwrap_or_else(|| chrono::Utc::now());
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
}
/// 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 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;
if available_lines > 0 {
let last_line_area = Rect {
x: area.x,
y: area.y + (available_lines - 1) as u16,
width: area.width,
height: 1,
};
let more_text = format!("... and {} more services", more_count);
let more_para = Paragraph::new(more_text).style(Typography::muted());
frame.render_widget(more_para, last_line_area);
}
/// Format duration in seconds to human readable format
fn format_duration(&self, duration_seconds: i64) -> String {
let minutes = duration_seconds / 60;
let seconds = duration_seconds % 60;
if minutes > 0 {
format!("{}.{}m", minutes, seconds / 6) // Show 1 decimal for minutes
} else {
format!("{}s", seconds)
}
}
}

View File

@ -254,7 +254,11 @@ impl SystemWidget {
_ => "—% —GB/—GB".to_string(),
};
let pool_label = format!("{} ({}):", pool.mount_point, pool.pool_type);
let pool_label = if pool.pool_type.to_lowercase() == "single" {
format!("{}:", pool.mount_point)
} else {
format!("{} ({}):", pool.mount_point, pool.pool_type)
};
let pool_spans = StatusIcons::create_status_spans(
pool.status.clone(),
&pool_label