Update storage widget for enhanced disk collector metrics
Restructure storage display to handle new individual metrics architecture:
- Parse disk_{pool}_* metrics instead of indexed disk_{index}_* format
- Support individual drive metrics disk_{pool}_{drive}_health/temperature/wear
- Display tree structure: "Storage {pool} ({type}): drive details"
- Show pool usage summary with individual drive health/temp/wear status
- Auto-discover storage pools and drives from metric patterns
- Maintain proper status aggregation from individual metrics
The dashboard now correctly displays the new enhanced disk collector output
with storage pools containing multiple drives and their individual metrics.
This commit is contained in:
parent
08d3454683
commit
1591565b1b
@ -432,79 +432,97 @@ impl TuiApp {
|
||||
}
|
||||
|
||||
if let Some(ref hostname) = self.current_host {
|
||||
// Get disk count to determine how many disks to display
|
||||
let disk_count =
|
||||
// Get storage pool count (renamed from disk_count in new architecture)
|
||||
let pool_count =
|
||||
if let Some(count_metric) = metric_store.get_metric(hostname, "disk_count") {
|
||||
count_metric.value.as_i64().unwrap_or(0) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if disk_count == 0 {
|
||||
// No disks found - show error/waiting message
|
||||
if pool_count == 0 {
|
||||
// No storage pools found - show error/waiting message
|
||||
let content_chunks = ratatui::layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
|
||||
let disk_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||
frame.render_widget(disk_title, content_chunks[0]);
|
||||
let storage_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||
frame.render_widget(storage_title, content_chunks[0]);
|
||||
|
||||
let no_disks_spans =
|
||||
StatusIcons::create_status_spans(Status::Unknown, "No mounted disks detected");
|
||||
let no_disks_para = Paragraph::new(ratatui::text::Line::from(no_disks_spans));
|
||||
frame.render_widget(no_disks_para, content_chunks[1]);
|
||||
let no_storage_spans =
|
||||
StatusIcons::create_status_spans(Status::Unknown, "No storage pools detected");
|
||||
let no_storage_para = Paragraph::new(ratatui::text::Line::from(no_storage_spans));
|
||||
frame.render_widget(no_storage_para, content_chunks[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Group disks by physical device
|
||||
let mut physical_devices: std::collections::HashMap<String, Vec<usize>> =
|
||||
// Discover storage pools from metrics (look for disk_{pool}_usage_percent patterns)
|
||||
let mut storage_pools: std::collections::HashMap<String, Vec<String>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for disk_index in 0..disk_count {
|
||||
if let Some(physical_device_metric) = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_physical_device", disk_index))
|
||||
{
|
||||
let physical_device = physical_device_metric.value.as_string();
|
||||
physical_devices
|
||||
.entry(physical_device)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(disk_index);
|
||||
let all_metrics = metric_store.get_metrics_for_host(hostname);
|
||||
|
||||
// Find storage pools by looking for usage metrics
|
||||
for metric in &all_metrics {
|
||||
if metric.name.starts_with("disk_") && metric.name.ends_with("_usage_percent") {
|
||||
let pool_name = metric.name
|
||||
.strip_prefix("disk_")
|
||||
.and_then(|s| s.strip_suffix("_usage_percent"))
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
if !pool_name.is_empty() && pool_name != "tmp" {
|
||||
storage_pools.entry(pool_name.clone()).or_insert_with(Vec::new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate how many lines we need
|
||||
let mut _total_lines_needed = 0;
|
||||
for partitions in physical_devices.values() {
|
||||
_total_lines_needed += 2 + partitions.len(); // title + health + usage_per_partition
|
||||
// Find individual drives for each pool
|
||||
for metric in &all_metrics {
|
||||
if metric.name.starts_with("disk_") && metric.name.contains("_") && metric.name.ends_with("_health") {
|
||||
// Parse disk_{pool}_{drive}_health format
|
||||
let parts: Vec<&str> = metric.name.split('_').collect();
|
||||
if parts.len() >= 4 && parts[0] == "disk" && parts[parts.len()-1] == "health" {
|
||||
// Extract pool name (everything between "disk_" and "_{drive}_health")
|
||||
let drive_name = parts[parts.len()-2].to_string();
|
||||
let pool_part_end = parts.len() - 2;
|
||||
let pool_name = parts[1..pool_part_end].join("_");
|
||||
|
||||
if let Some(drives) = storage_pools.get_mut(&pool_name) {
|
||||
if !drives.contains(&drive_name) {
|
||||
drives.push(drive_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let available_lines = area.height as usize;
|
||||
|
||||
// Create constraints dynamically based on physical devices
|
||||
let mut constraints = Vec::new();
|
||||
let mut devices_to_show = Vec::new();
|
||||
let mut pools_to_show = Vec::new();
|
||||
let mut current_line = 0;
|
||||
|
||||
// Sort physical devices by name for consistent ordering
|
||||
let mut sorted_devices: Vec<_> = physical_devices.iter().collect();
|
||||
sorted_devices.sort_by_key(|(device_name, _)| device_name.as_str());
|
||||
// Sort storage pools by name for consistent ordering
|
||||
let mut sorted_pools: Vec<_> = storage_pools.iter().collect();
|
||||
sorted_pools.sort_by_key(|(pool_name, _)| pool_name.as_str());
|
||||
|
||||
for (physical_device, partitions) in sorted_devices {
|
||||
let lines_for_this_device = 2 + partitions.len();
|
||||
if current_line + lines_for_this_device <= available_lines {
|
||||
devices_to_show.push((physical_device.clone(), partitions.clone()));
|
||||
for (pool_name, drives) in sorted_pools {
|
||||
// Calculate lines needed: title + usage + 1 line per drive
|
||||
let lines_for_this_pool = 2 + drives.len();
|
||||
if current_line + lines_for_this_pool <= available_lines {
|
||||
pools_to_show.push((pool_name.clone(), drives.clone()));
|
||||
|
||||
// Add constraints for this device
|
||||
constraints.push(Constraint::Length(1)); // Device title
|
||||
constraints.push(Constraint::Length(1)); // Health line
|
||||
for _ in 0..partitions.len() {
|
||||
constraints.push(Constraint::Length(1)); // Usage line per partition
|
||||
// Add constraints for this pool
|
||||
constraints.push(Constraint::Length(1)); // Pool title
|
||||
constraints.push(Constraint::Length(1)); // Pool usage line
|
||||
for _ in 0..drives.len() {
|
||||
constraints.push(Constraint::Length(1)); // Drive line (health/temp/wear)
|
||||
}
|
||||
|
||||
current_line += lines_for_this_device;
|
||||
current_line += lines_for_this_pool;
|
||||
} else {
|
||||
break; // Can't fit more devices
|
||||
break; // Can't fit more pools
|
||||
}
|
||||
}
|
||||
|
||||
@ -520,102 +538,102 @@ impl TuiApp {
|
||||
|
||||
let mut chunk_index = 0;
|
||||
|
||||
// Display each physical device
|
||||
for (physical_device, partitions) in &devices_to_show {
|
||||
// Device title
|
||||
let disk_title_text = format!("Disk {}:", physical_device);
|
||||
let disk_title_para =
|
||||
Paragraph::new(disk_title_text).style(Typography::widget_title());
|
||||
frame.render_widget(disk_title_para, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
|
||||
// Health status (one per physical device)
|
||||
let smart_health = metric_store
|
||||
.get_metric(hostname, &format!("disk_smart_{}_health", physical_device))
|
||||
.map(|m| (m.value.as_string(), m.status))
|
||||
.unwrap_or_else(|| ("Unknown".to_string(), Status::Unknown));
|
||||
|
||||
let smart_temp = metric_store
|
||||
.get_metric(
|
||||
hostname,
|
||||
&format!("disk_smart_{}_temperature", physical_device),
|
||||
)
|
||||
.and_then(|m| m.value.as_f32());
|
||||
|
||||
let temp_text = if let Some(temp) = smart_temp {
|
||||
format!(" {}°C", temp as i32)
|
||||
// Display each storage pool
|
||||
for (pool_name, drives) in &pools_to_show {
|
||||
// Pool title with type indicator
|
||||
let pool_display_name = if pool_name == "root" {
|
||||
"root".to_string()
|
||||
} else {
|
||||
String::new()
|
||||
pool_name.clone()
|
||||
};
|
||||
|
||||
let health_spans = StatusIcons::create_status_spans(
|
||||
smart_health.1,
|
||||
&format!("Health: {}{}", smart_health.0, temp_text),
|
||||
);
|
||||
let health_para = Paragraph::new(ratatui::text::Line::from(health_spans));
|
||||
frame.render_widget(health_para, content_chunks[chunk_index]);
|
||||
let pool_type = if drives.len() > 1 { "multi-drive" } else { "single" };
|
||||
let pool_title_text = format!("Storage {} ({}):", pool_display_name, pool_type);
|
||||
let pool_title_para = Paragraph::new(pool_title_text).style(Typography::widget_title());
|
||||
frame.render_widget(pool_title_para, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
|
||||
// Usage lines (one per partition/mount point)
|
||||
// Sort partitions by disk index for consistent ordering
|
||||
let mut sorted_partitions = partitions.clone();
|
||||
sorted_partitions.sort();
|
||||
for &disk_index in &sorted_partitions {
|
||||
let mount_point = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_mount_point", disk_index))
|
||||
.map(|m| m.value.as_string())
|
||||
.unwrap_or("?".to_string());
|
||||
// Pool usage information
|
||||
let usage_percent = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let usage_percent = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", disk_index))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
let used_gb = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_used_gb", pool_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let used_gb = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_used_gb", disk_index))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
let total_gb = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_total_gb", pool_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let total_gb = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_total_gb", disk_index))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
let usage_status = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||
.map(|m| m.status)
|
||||
.unwrap_or(Status::Unknown);
|
||||
|
||||
let usage_status = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", disk_index))
|
||||
.map(|m| m.status)
|
||||
.unwrap_or(Status::Unknown);
|
||||
let usage_spans = StatusIcons::create_status_spans(
|
||||
usage_status,
|
||||
&format!(
|
||||
"Usage: {:.1}% • {:.1}/{:.1} GB",
|
||||
usage_percent, used_gb, total_gb
|
||||
),
|
||||
);
|
||||
let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans));
|
||||
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
|
||||
// Format mount point for usage line
|
||||
let mount_display = if mount_point == "/" {
|
||||
"root".to_string()
|
||||
} else if mount_point == "/boot" {
|
||||
"boot".to_string()
|
||||
} else if mount_point.starts_with("/") {
|
||||
mount_point[1..].to_string() // Remove leading slash
|
||||
// Individual drive information
|
||||
let mut sorted_drives = drives.clone();
|
||||
sorted_drives.sort();
|
||||
for drive_name in &sorted_drives {
|
||||
// Get drive health
|
||||
let health_metric = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_{}_health", pool_name, drive_name));
|
||||
let (health_status, health_color) = if let Some(metric) = health_metric {
|
||||
(metric.value.as_string(), metric.status)
|
||||
} else {
|
||||
mount_point.clone()
|
||||
("Unknown".to_string(), Status::Unknown)
|
||||
};
|
||||
|
||||
let usage_spans = StatusIcons::create_status_spans(
|
||||
usage_status,
|
||||
&format!(
|
||||
"Usage @{}: {:.1}% • {:.1}/{:.1} GB",
|
||||
mount_display, usage_percent, used_gb, total_gb
|
||||
),
|
||||
);
|
||||
let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans));
|
||||
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
||||
// Get drive temperature
|
||||
let temp_text = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_{}_temperature", pool_name, drive_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.map(|temp| format!(" {}°C", temp as i32))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Get drive wear level (SSDs)
|
||||
let wear_text = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_{}_wear_percent", pool_name, drive_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.map(|wear| format!(" {}%", wear as i32))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build drive status line
|
||||
let mut drive_info = format!("{}: {}", drive_name, health_status);
|
||||
if !temp_text.is_empty() {
|
||||
drive_info.push_str(&temp_text);
|
||||
}
|
||||
if !wear_text.is_empty() {
|
||||
drive_info.push_str(&format!(" wear{}", wear_text));
|
||||
}
|
||||
|
||||
let drive_spans = StatusIcons::create_status_spans(health_color, &drive_info);
|
||||
let drive_para = Paragraph::new(ratatui::text::Line::from(drive_spans));
|
||||
frame.render_widget(drive_para, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Show truncation indicator if we couldn't display all devices
|
||||
if devices_to_show.len() < physical_devices.len() {
|
||||
// Show truncation indicator if we couldn't display all pools
|
||||
if pools_to_show.len() < storage_pools.len() {
|
||||
if let Some(last_chunk) = content_chunks.last() {
|
||||
let truncated_count = physical_devices.len() - devices_to_show.len();
|
||||
let truncated_count = storage_pools.len() - pools_to_show.len();
|
||||
let truncated_text = format!(
|
||||
"... and {} more disk{}",
|
||||
"... and {} more pool{}",
|
||||
truncated_count,
|
||||
if truncated_count == 1 { "" } else { "s" }
|
||||
);
|
||||
@ -630,8 +648,8 @@ impl TuiApp {
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
|
||||
let disk_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||
frame.render_widget(disk_title, content_chunks[0]);
|
||||
let storage_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||
frame.render_widget(storage_title, content_chunks[0]);
|
||||
|
||||
let no_host_spans =
|
||||
StatusIcons::create_status_spans(Status::Unknown, "No host connected");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user