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 {
|
if let Some(ref hostname) = self.current_host {
|
||||||
// Get disk count to determine how many disks to display
|
// Get storage pool count (renamed from disk_count in new architecture)
|
||||||
let disk_count =
|
let pool_count =
|
||||||
if let Some(count_metric) = metric_store.get_metric(hostname, "disk_count") {
|
if let Some(count_metric) = metric_store.get_metric(hostname, "disk_count") {
|
||||||
count_metric.value.as_i64().unwrap_or(0) as usize
|
count_metric.value.as_i64().unwrap_or(0) as usize
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
if disk_count == 0 {
|
if pool_count == 0 {
|
||||||
// No disks found - show error/waiting message
|
// No storage pools found - show error/waiting message
|
||||||
let content_chunks = ratatui::layout::Layout::default()
|
let content_chunks = ratatui::layout::Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let disk_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
let storage_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||||
frame.render_widget(disk_title, content_chunks[0]);
|
frame.render_widget(storage_title, content_chunks[0]);
|
||||||
|
|
||||||
let no_disks_spans =
|
let no_storage_spans =
|
||||||
StatusIcons::create_status_spans(Status::Unknown, "No mounted disks detected");
|
StatusIcons::create_status_spans(Status::Unknown, "No storage pools detected");
|
||||||
let no_disks_para = Paragraph::new(ratatui::text::Line::from(no_disks_spans));
|
let no_storage_para = Paragraph::new(ratatui::text::Line::from(no_storage_spans));
|
||||||
frame.render_widget(no_disks_para, content_chunks[1]);
|
frame.render_widget(no_storage_para, content_chunks[1]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group disks by physical device
|
// Discover storage pools from metrics (look for disk_{pool}_usage_percent patterns)
|
||||||
let mut physical_devices: std::collections::HashMap<String, Vec<usize>> =
|
let mut storage_pools: std::collections::HashMap<String, Vec<String>> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
for disk_index in 0..disk_count {
|
let all_metrics = metric_store.get_metrics_for_host(hostname);
|
||||||
if let Some(physical_device_metric) = metric_store
|
|
||||||
.get_metric(hostname, &format!("disk_{}_physical_device", disk_index))
|
// Find storage pools by looking for usage metrics
|
||||||
{
|
for metric in &all_metrics {
|
||||||
let physical_device = physical_device_metric.value.as_string();
|
if metric.name.starts_with("disk_") && metric.name.ends_with("_usage_percent") {
|
||||||
physical_devices
|
let pool_name = metric.name
|
||||||
.entry(physical_device)
|
.strip_prefix("disk_")
|
||||||
.or_insert_with(Vec::new)
|
.and_then(|s| s.strip_suffix("_usage_percent"))
|
||||||
.push(disk_index);
|
.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
|
// Find individual drives for each pool
|
||||||
let mut _total_lines_needed = 0;
|
for metric in &all_metrics {
|
||||||
for partitions in physical_devices.values() {
|
if metric.name.starts_with("disk_") && metric.name.contains("_") && metric.name.ends_with("_health") {
|
||||||
_total_lines_needed += 2 + partitions.len(); // title + health + usage_per_partition
|
// 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;
|
let available_lines = area.height as usize;
|
||||||
|
|
||||||
// Create constraints dynamically based on physical devices
|
|
||||||
let mut constraints = Vec::new();
|
let mut constraints = Vec::new();
|
||||||
let mut devices_to_show = Vec::new();
|
let mut pools_to_show = Vec::new();
|
||||||
let mut current_line = 0;
|
let mut current_line = 0;
|
||||||
|
|
||||||
// Sort physical devices by name for consistent ordering
|
// Sort storage pools by name for consistent ordering
|
||||||
let mut sorted_devices: Vec<_> = physical_devices.iter().collect();
|
let mut sorted_pools: Vec<_> = storage_pools.iter().collect();
|
||||||
sorted_devices.sort_by_key(|(device_name, _)| device_name.as_str());
|
sorted_pools.sort_by_key(|(pool_name, _)| pool_name.as_str());
|
||||||
|
|
||||||
for (physical_device, partitions) in sorted_devices {
|
for (pool_name, drives) in sorted_pools {
|
||||||
let lines_for_this_device = 2 + partitions.len();
|
// Calculate lines needed: title + usage + 1 line per drive
|
||||||
if current_line + lines_for_this_device <= available_lines {
|
let lines_for_this_pool = 2 + drives.len();
|
||||||
devices_to_show.push((physical_device.clone(), partitions.clone()));
|
if current_line + lines_for_this_pool <= available_lines {
|
||||||
|
pools_to_show.push((pool_name.clone(), drives.clone()));
|
||||||
|
|
||||||
// Add constraints for this device
|
// Add constraints for this pool
|
||||||
constraints.push(Constraint::Length(1)); // Device title
|
constraints.push(Constraint::Length(1)); // Pool title
|
||||||
constraints.push(Constraint::Length(1)); // Health line
|
constraints.push(Constraint::Length(1)); // Pool usage line
|
||||||
for _ in 0..partitions.len() {
|
for _ in 0..drives.len() {
|
||||||
constraints.push(Constraint::Length(1)); // Usage line per partition
|
constraints.push(Constraint::Length(1)); // Drive line (health/temp/wear)
|
||||||
}
|
}
|
||||||
|
|
||||||
current_line += lines_for_this_device;
|
current_line += lines_for_this_pool;
|
||||||
} else {
|
} else {
|
||||||
break; // Can't fit more devices
|
break; // Can't fit more pools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,102 +538,102 @@ impl TuiApp {
|
|||||||
|
|
||||||
let mut chunk_index = 0;
|
let mut chunk_index = 0;
|
||||||
|
|
||||||
// Display each physical device
|
// Display each storage pool
|
||||||
for (physical_device, partitions) in &devices_to_show {
|
for (pool_name, drives) in &pools_to_show {
|
||||||
// Device title
|
// Pool title with type indicator
|
||||||
let disk_title_text = format!("Disk {}:", physical_device);
|
let pool_display_name = if pool_name == "root" {
|
||||||
let disk_title_para =
|
"root".to_string()
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
pool_name.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let health_spans = StatusIcons::create_status_spans(
|
let pool_type = if drives.len() > 1 { "multi-drive" } else { "single" };
|
||||||
smart_health.1,
|
let pool_title_text = format!("Storage {} ({}):", pool_display_name, pool_type);
|
||||||
&format!("Health: {}{}", smart_health.0, temp_text),
|
let pool_title_para = Paragraph::new(pool_title_text).style(Typography::widget_title());
|
||||||
);
|
frame.render_widget(pool_title_para, content_chunks[chunk_index]);
|
||||||
let health_para = Paragraph::new(ratatui::text::Line::from(health_spans));
|
|
||||||
frame.render_widget(health_para, content_chunks[chunk_index]);
|
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
|
|
||||||
// Usage lines (one per partition/mount point)
|
// Pool usage information
|
||||||
// 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());
|
|
||||||
|
|
||||||
let usage_percent = metric_store
|
let usage_percent = metric_store
|
||||||
.get_metric(hostname, &format!("disk_{}_usage_percent", disk_index))
|
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||||
.and_then(|m| m.value.as_f32())
|
.and_then(|m| m.value.as_f32())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
let used_gb = metric_store
|
let used_gb = metric_store
|
||||||
.get_metric(hostname, &format!("disk_{}_used_gb", disk_index))
|
.get_metric(hostname, &format!("disk_{}_used_gb", pool_name))
|
||||||
.and_then(|m| m.value.as_f32())
|
.and_then(|m| m.value.as_f32())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
let total_gb = metric_store
|
let total_gb = metric_store
|
||||||
.get_metric(hostname, &format!("disk_{}_total_gb", disk_index))
|
.get_metric(hostname, &format!("disk_{}_total_gb", pool_name))
|
||||||
.and_then(|m| m.value.as_f32())
|
.and_then(|m| m.value.as_f32())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
let usage_status = metric_store
|
let usage_status = metric_store
|
||||||
.get_metric(hostname, &format!("disk_{}_usage_percent", disk_index))
|
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||||
.map(|m| m.status)
|
.map(|m| m.status)
|
||||||
.unwrap_or(Status::Unknown);
|
.unwrap_or(Status::Unknown);
|
||||||
|
|
||||||
// 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
|
|
||||||
} else {
|
|
||||||
mount_point.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let usage_spans = StatusIcons::create_status_spans(
|
let usage_spans = StatusIcons::create_status_spans(
|
||||||
usage_status,
|
usage_status,
|
||||||
&format!(
|
&format!(
|
||||||
"Usage @{}: {:.1}% • {:.1}/{:.1} GB",
|
"Usage: {:.1}% • {:.1}/{:.1} GB",
|
||||||
mount_display, usage_percent, used_gb, total_gb
|
usage_percent, used_gb, total_gb
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans));
|
let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans));
|
||||||
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
("Unknown".to_string(), Status::Unknown)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
// Show truncation indicator if we couldn't display all pools
|
||||||
if devices_to_show.len() < physical_devices.len() {
|
if pools_to_show.len() < storage_pools.len() {
|
||||||
if let Some(last_chunk) = content_chunks.last() {
|
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!(
|
let truncated_text = format!(
|
||||||
"... and {} more disk{}",
|
"... and {} more pool{}",
|
||||||
truncated_count,
|
truncated_count,
|
||||||
if truncated_count == 1 { "" } else { "s" }
|
if truncated_count == 1 { "" } else { "s" }
|
||||||
);
|
);
|
||||||
@ -630,8 +648,8 @@ impl TuiApp {
|
|||||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let disk_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
let storage_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||||
frame.render_widget(disk_title, content_chunks[0]);
|
frame.render_widget(storage_title, content_chunks[0]);
|
||||||
|
|
||||||
let no_host_spans =
|
let no_host_spans =
|
||||||
StatusIcons::create_status_spans(Status::Unknown, "No host connected");
|
StatusIcons::create_status_spans(Status::Unknown, "No host connected");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user