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:
parent
997b30a9c0
commit
b391448d33
@ -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,
|
||||
×tamp_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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user