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 cm_dashboard_shared::{Metric, Status};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::Rect,
|
||||||
widgets::Paragraph,
|
widgets::Paragraph,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
@ -340,138 +340,103 @@ impl Widget for BackupWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
// Split area into header and services list
|
let mut lines = Vec::new();
|
||||||
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);
|
|
||||||
|
|
||||||
// Render backup overview
|
// Latest backup section
|
||||||
self.render_backup_overview(frame, chunks[0]);
|
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
|
// Duration as sub-item
|
||||||
self.render_services_list(frame, chunks[1]);
|
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 {
|
impl BackupWidget {
|
||||||
/// Render backup overview section
|
/// Format timestamp for display
|
||||||
fn render_backup_overview(&self, frame: &mut Frame, area: Rect) {
|
fn format_timestamp(&self, timestamp: i64) -> String {
|
||||||
let content_chunks = Layout::default()
|
let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
|
||||||
.direction(Direction::Vertical)
|
.unwrap_or_else(|| chrono::Utc::now());
|
||||||
.constraints([
|
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render services list section
|
/// Format duration in seconds to human readable format
|
||||||
fn render_services_list(&self, frame: &mut Frame, area: Rect) {
|
fn format_duration(&self, duration_seconds: i64) -> String {
|
||||||
if area.height < 1 {
|
let minutes = duration_seconds / 60;
|
||||||
return;
|
let seconds = duration_seconds % 60;
|
||||||
}
|
|
||||||
|
if minutes > 0 {
|
||||||
let available_lines = area.height as usize;
|
format!("{}.{}m", minutes, seconds / 6) // Show 1 decimal for minutes
|
||||||
let services_to_show = self.service_metrics.iter().take(available_lines);
|
} else {
|
||||||
|
format!("{}s", seconds)
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -254,7 +254,11 @@ impl SystemWidget {
|
|||||||
_ => "—% —GB/—GB".to_string(),
|
_ => "—% —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(
|
let pool_spans = StatusIcons::create_status_spans(
|
||||||
pool.status.clone(),
|
pool.status.clone(),
|
||||||
&pool_label
|
&pool_label
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user