Complete unified pool visualization with filesystem children
All checks were successful
Build and Release / build-and-release (push) Successful in 2m17s
All checks were successful
Build and Release / build-and-release (push) Successful in 2m17s
- Implement filesystem children display under physical drive pools - Agent generates individual filesystem metrics for each mount point - Dashboard parses filesystem metrics and displays as tree children - Add filesystem usage, total, and available space metrics - Support target format: drive info + filesystem children hierarchy - Fix compilation warnings by properly using available_bytes calculation
This commit is contained in:
@@ -48,6 +48,7 @@ struct StoragePool {
|
||||
pool_type: String, // "single", "mergerfs (2+1)", "RAID5 (3+1)", etc.
|
||||
pool_health: Option<String>, // "healthy", "degraded", "critical", "rebuilding"
|
||||
drives: Vec<StorageDrive>,
|
||||
filesystems: Vec<FileSystem>, // For physical drive pools: individual filesystem children
|
||||
usage_percent: Option<f32>,
|
||||
used_gb: Option<f32>,
|
||||
total_gb: Option<f32>,
|
||||
@@ -63,6 +64,15 @@ struct StorageDrive {
|
||||
status: Status,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FileSystem {
|
||||
mount_point: String,
|
||||
usage_percent: Option<f32>,
|
||||
used_gb: Option<f32>,
|
||||
total_gb: Option<f32>,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
impl SystemWidget {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -160,6 +170,7 @@ impl SystemWidget {
|
||||
pool_type: "single".to_string(), // Default, will be updated
|
||||
pool_health: None,
|
||||
drives: Vec::new(),
|
||||
filesystems: Vec::new(),
|
||||
usage_percent: None,
|
||||
used_gb: None,
|
||||
total_gb: None,
|
||||
@@ -230,6 +241,75 @@ impl SystemWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if metric.name.contains("_fs_") {
|
||||
// Handle filesystem metrics for physical drive pools (disk_{pool}_fs_{fs_name}_{metric})
|
||||
if let (Some(fs_name), Some(metric_type)) = self.extract_filesystem_metric(&metric.name) {
|
||||
// Find or create filesystem entry
|
||||
let fs_exists = pool.filesystems.iter().any(|fs| {
|
||||
let fs_id = if fs.mount_point == "/" {
|
||||
"root".to_string()
|
||||
} else {
|
||||
fs.mount_point.trim_start_matches('/').replace('/', "_")
|
||||
};
|
||||
fs_id == fs_name
|
||||
});
|
||||
|
||||
if !fs_exists {
|
||||
// Extract actual mount point from mount_point metric if available
|
||||
let mount_point = if metric_type == "mount_point" {
|
||||
if let MetricValue::String(mount) = &metric.value {
|
||||
mount.clone()
|
||||
} else {
|
||||
format!("/{}", fs_name.replace('_', "/"))
|
||||
}
|
||||
} else {
|
||||
format!("/{}", fs_name.replace('_', "/"))
|
||||
};
|
||||
|
||||
pool.filesystems.push(FileSystem {
|
||||
mount_point,
|
||||
usage_percent: None,
|
||||
used_gb: None,
|
||||
total_gb: None,
|
||||
status: Status::Unknown,
|
||||
});
|
||||
}
|
||||
|
||||
// Update the filesystem with the metric value
|
||||
if let Some(filesystem) = pool.filesystems.iter_mut().find(|fs| {
|
||||
let fs_id = if fs.mount_point == "/" {
|
||||
"root".to_string()
|
||||
} else {
|
||||
fs.mount_point.trim_start_matches('/').replace('/', "_")
|
||||
};
|
||||
fs_id == fs_name
|
||||
}) {
|
||||
match metric_type.as_str() {
|
||||
"usage_percent" => {
|
||||
if let MetricValue::Float(usage) = metric.value {
|
||||
filesystem.usage_percent = Some(usage);
|
||||
filesystem.status = metric.status.clone();
|
||||
}
|
||||
}
|
||||
"used_gb" => {
|
||||
if let MetricValue::Float(used) = metric.value {
|
||||
filesystem.used_gb = Some(used);
|
||||
}
|
||||
}
|
||||
"total_gb" => {
|
||||
if let MetricValue::Float(total) = metric.value {
|
||||
filesystem.total_gb = Some(total);
|
||||
}
|
||||
}
|
||||
"mount_point" => {
|
||||
if let MetricValue::String(mount) = &metric.value {
|
||||
filesystem.mount_point = mount.clone();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +352,25 @@ impl SystemWidget {
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract filesystem name and metric type from filesystem metric names
|
||||
/// Pattern: disk_{pool}_fs_{filesystem_name}_{metric_type}
|
||||
fn extract_filesystem_metric(&self, metric_name: &str) -> (Option<String>, Option<String>) {
|
||||
if metric_name.starts_with("disk_") && metric_name.contains("_fs_") {
|
||||
// Find the _fs_ part
|
||||
if let Some(fs_start) = metric_name.find("_fs_") {
|
||||
let after_fs = &metric_name[fs_start + 4..]; // Skip "_fs_"
|
||||
|
||||
// Find the last underscore to separate filesystem name from metric type
|
||||
if let Some(last_underscore) = after_fs.rfind('_') {
|
||||
let fs_name = after_fs[..last_underscore].to_string();
|
||||
let metric_type = after_fs[last_underscore + 1..].to_string();
|
||||
return (Some(fs_name), Some(metric_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None)
|
||||
}
|
||||
|
||||
/// Extract drive name from disk metric name
|
||||
fn extract_drive_name(&self, metric_name: &str) -> Option<String> {
|
||||
// Pattern: disk_{pool_name}_{drive_name}_{metric_type}
|
||||
@@ -337,7 +436,9 @@ impl SystemWidget {
|
||||
};
|
||||
|
||||
let has_drives = !pool.drives.is_empty();
|
||||
let tree_symbol = if has_drives { "├─" } else { "└─" };
|
||||
let has_filesystems = !pool.filesystems.is_empty();
|
||||
let has_children = has_drives || has_filesystems;
|
||||
let tree_symbol = if has_children { "├─" } else { "└─" };
|
||||
let mut usage_spans = vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(tree_symbol, Typography::tree()),
|
||||
@@ -397,6 +498,54 @@ impl SystemWidget {
|
||||
self.render_drive_line(&mut lines, drive, tree_symbol);
|
||||
}
|
||||
}
|
||||
} else if pool.pool_type.starts_with("drive (") {
|
||||
// Physical drive pools: show drive info + filesystem children
|
||||
// First show drive information
|
||||
for drive in &pool.drives {
|
||||
let mut drive_info = Vec::new();
|
||||
if let Some(temp) = drive.temperature {
|
||||
drive_info.push(format!("T: {:.0}°C", temp));
|
||||
}
|
||||
if let Some(wear) = drive.wear_percent {
|
||||
drive_info.push(format!("W: {:.0}%", wear));
|
||||
}
|
||||
let drive_text = if drive_info.is_empty() {
|
||||
format!("Drive: {}", drive.name)
|
||||
} else {
|
||||
format!("Drive: {}", drive_info.join(" "))
|
||||
};
|
||||
|
||||
let has_filesystems = !pool.filesystems.is_empty();
|
||||
let tree_symbol = if has_filesystems { "├─" } else { "└─" };
|
||||
let mut drive_spans = vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(tree_symbol, Typography::tree()),
|
||||
Span::raw(" "),
|
||||
];
|
||||
drive_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text));
|
||||
lines.push(Line::from(drive_spans));
|
||||
}
|
||||
|
||||
// Then show filesystem children
|
||||
for (i, filesystem) in pool.filesystems.iter().enumerate() {
|
||||
let is_last = i == pool.filesystems.len() - 1;
|
||||
let tree_symbol = if is_last { "└─" } else { "├─" };
|
||||
|
||||
let fs_text = match (filesystem.usage_percent, filesystem.used_gb, filesystem.total_gb) {
|
||||
(Some(pct), Some(used), Some(total)) => {
|
||||
format!("{}: {:.0}% {:.1}GB/{:.1}GB", filesystem.mount_point, pct, used, total)
|
||||
}
|
||||
_ => format!("{}: —% —GB/—GB", filesystem.mount_point),
|
||||
};
|
||||
|
||||
let mut fs_spans = vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(tree_symbol, Typography::tree()),
|
||||
Span::raw(" "),
|
||||
];
|
||||
fs_spans.extend(StatusIcons::create_status_spans(filesystem.status.clone(), &fs_text));
|
||||
lines.push(Line::from(fs_spans));
|
||||
}
|
||||
} else {
|
||||
// Single drive or simple pools
|
||||
for (i, drive) in pool.drives.iter().enumerate() {
|
||||
|
||||
Reference in New Issue
Block a user