feat: Full support for filtering disks and aggregating root folders in the UI's 'Stats' block

This commit is contained in:
2026-01-26 11:10:59 -07:00
parent f388dccc08
commit fdc331865e
6 changed files with 45 additions and 27 deletions
+7
View File
@@ -380,6 +380,13 @@ lidarr:
- host: 192.168.0.86
port: 8686
api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables
monitored_storage_paths: # Filter which Root Folders or Disk Storage you want displayed in the UI's 'Stats' block
# Note: Setting these values does not affect what shows up in the 'Root Folders' tab of the UI.
- /nfs # An example disk (i.e. '<servarr> list disk-space' command) you want displayed in the UI under 'Storage:'
- /media # An example root folder you want displayed in the UI
# Root folders collapse up to the super-directory to reduce duplication in the UI. For example:
# if you have root folders '/media/tv', '/media/cartoons' and '/media/reality', and you set this
# monitored path, the UI will show '/media/[tv,cartoons,reality]' under Root Folders
whisparr:
- host: 192.168.0.69
port: 6969
+4 -5
View File
@@ -1,5 +1,3 @@
use std::{cmp, iter};
#[cfg(test)]
use crate::ui::ui_test_utils::test_utils::Utc;
use chrono::Duration;
@@ -14,6 +12,7 @@ use ratatui::{
text::Text,
widgets::Paragraph,
};
use std::{cmp, iter};
use super::{
DrawUi, draw_tabs,
@@ -101,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time,
..
} = &app.data.lidarr_data;
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
let mut constraints = vec![
Constraint::Length(1),
@@ -111,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append(
&mut iter::repeat_n(
Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1,
monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
)
.collect(),
);
@@ -147,7 +148,6 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]);
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
for i in 0..monitored_disk_space_vec.len() {
let DiskSpace {
path,
@@ -172,7 +172,6 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(folders, stat_item_areas[monitored_disk_space_vec.len() + 3]);
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
for i in 0..monitored_root_folders.len() {
let RootFolder {
path, free_space, ..
+3 -2
View File
@@ -93,6 +93,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time,
..
} = &app.data.radarr_data;
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
let mut constraints = vec![
Constraint::Length(1),
@@ -103,7 +105,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append(
&mut iter::repeat_n(
Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1,
monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
)
.collect(),
);
@@ -139,7 +141,6 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]);
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
for i in 0..monitored_disk_space_vec.len() {
let DiskSpace {
path,
+3 -3
View File
@@ -100,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time,
..
} = &app.data.sonarr_data;
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
let mut constraints = vec![
Constraint::Length(1),
@@ -110,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append(
&mut iter::repeat_n(
Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1,
monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
)
.collect(),
);
@@ -146,7 +148,6 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]);
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
for i in 0..monitored_disk_space_vec.len() {
let DiskSpace {
path,
@@ -172,7 +173,6 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(folders, stat_item_areas[monitored_disk_space_vec.len() + 3]);
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
for i in 0..monitored_root_folders.len() {
let RootFolder {
path, free_space, ..
+16 -11
View File
@@ -9,7 +9,7 @@ use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[cfg(test)]
@@ -280,17 +280,22 @@ pub(super) fn extract_monitored_disk_space_vec(
if let Some(monitored_paths) = monitored_paths
&& !monitored_paths.is_empty()
{
let paths: Vec<PathBuf> = monitored_paths.iter().map(PathBuf::from).collect();
disk_space_vec
.into_iter()
.filter(|it| {
if let Some(path) = it.path.as_ref() {
paths.iter().any(|p| path == p)
} else {
true
let monitored: HashSet<&str> = monitored_paths.iter().map(|s| s.as_str()).collect();
let mut seen_paths = HashSet::new();
let mut filtered_disk_space_vec = Vec::with_capacity(disk_space_vec.len());
for ds in disk_space_vec {
match ds.path.as_deref() {
None => filtered_disk_space_vec.push(ds),
Some(p) => {
if monitored.contains(p) && seen_paths.insert(p.to_owned()) {
filtered_disk_space_vec.push(ds)
}
}
})
.collect()
}
}
filtered_disk_space_vec
} else {
disk_space_vec
}
+12 -6
View File
@@ -469,14 +469,19 @@ mod test {
fn test_extract_monitored_disk_space_vec() {
let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig {
monitored_storage_paths: Some(vec!["/nfs".to_owned()]),
monitored_storage_paths: Some(vec!["/data".to_owned(), "/downloads".to_owned()]),
..ServarrConfig::default()
});
let disk_space = DiskSpace {
path: Some("/nfs".to_string()),
path: Some("/data".to_string()),
free_space: 10,
total_space: 1000,
};
let disk_space_2 = DiskSpace {
path: Some("/downloads".to_string()),
free_space: 100,
total_space: 10000,
};
let disk_space_with_empty_path = DiskSpace {
path: None,
free_space: 10,
@@ -486,17 +491,18 @@ mod test {
disk_space.clone(),
disk_space_with_empty_path.clone(),
DiskSpace {
path: Some("/nfs/some/subpath".to_string()),
free_space: 10,
total_space: 1000,
path: Some("/downloads/".to_string()),
free_space: 100,
total_space: 10000,
},
disk_space_2.clone(),
];
let monitored_disk_space = extract_monitored_disk_space_vec(&app, disk_spaces);
assert_eq!(
monitored_disk_space,
vec![disk_space, disk_space_with_empty_path]
vec![disk_space, disk_space_with_empty_path, disk_space_2]
);
}