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 - host: 192.168.0.86
port: 8686 port: 8686
api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables 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: whisparr:
- host: 192.168.0.69 - host: 192.168.0.69
port: 6969 port: 6969
+4 -5
View File
@@ -1,5 +1,3 @@
use std::{cmp, iter};
#[cfg(test)] #[cfg(test)]
use crate::ui::ui_test_utils::test_utils::Utc; use crate::ui::ui_test_utils::test_utils::Utc;
use chrono::Duration; use chrono::Duration;
@@ -14,6 +12,7 @@ use ratatui::{
text::Text, text::Text,
widgets::Paragraph, widgets::Paragraph,
}; };
use std::{cmp, iter};
use super::{ use super::{
DrawUi, draw_tabs, DrawUi, draw_tabs,
@@ -101,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time, start_time,
.. ..
} = &app.data.lidarr_data; } = &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![ let mut constraints = vec![
Constraint::Length(1), Constraint::Length(1),
@@ -111,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append( constraints.append(
&mut iter::repeat_n( &mut iter::repeat_n(
Constraint::Length(1), Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1, monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
) )
.collect(), .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(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]); 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() { for i in 0..monitored_disk_space_vec.len() {
let DiskSpace { let DiskSpace {
path, 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]); 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() { for i in 0..monitored_root_folders.len() {
let RootFolder { let RootFolder {
path, free_space, .. path, free_space, ..
+3 -2
View File
@@ -93,6 +93,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time, start_time,
.. ..
} = &app.data.radarr_data; } = &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![ let mut constraints = vec![
Constraint::Length(1), Constraint::Length(1),
@@ -103,7 +105,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append( constraints.append(
&mut iter::repeat_n( &mut iter::repeat_n(
Constraint::Length(1), Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1, monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
) )
.collect(), .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(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]); 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() { for i in 0..monitored_disk_space_vec.len() {
let DiskSpace { let DiskSpace {
path, path,
+3 -3
View File
@@ -100,6 +100,8 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
start_time, start_time,
.. ..
} = &app.data.sonarr_data; } = &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![ let mut constraints = vec![
Constraint::Length(1), Constraint::Length(1),
@@ -110,7 +112,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
constraints.append( constraints.append(
&mut iter::repeat_n( &mut iter::repeat_n(
Constraint::Length(1), Constraint::Length(1),
disk_space_vec.len() + root_folders.items.len() + 1, monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
) )
.collect(), .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(uptime_paragraph, stat_item_areas[1]);
f.render_widget(storage, stat_item_areas[2]); 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() { for i in 0..monitored_disk_space_vec.len() {
let DiskSpace { let DiskSpace {
path, 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]); 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() { for i in 0..monitored_root_folders.len() {
let RootFolder { let RootFolder {
path, free_space, .. 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::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text}; use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap}; use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap};
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[cfg(test)] #[cfg(test)]
@@ -280,17 +280,22 @@ pub(super) fn extract_monitored_disk_space_vec(
if let Some(monitored_paths) = monitored_paths if let Some(monitored_paths) = monitored_paths
&& !monitored_paths.is_empty() && !monitored_paths.is_empty()
{ {
let paths: Vec<PathBuf> = monitored_paths.iter().map(PathBuf::from).collect(); let monitored: HashSet<&str> = monitored_paths.iter().map(|s| s.as_str()).collect();
disk_space_vec let mut seen_paths = HashSet::new();
.into_iter() let mut filtered_disk_space_vec = Vec::with_capacity(disk_space_vec.len());
.filter(|it| {
if let Some(path) = it.path.as_ref() { for ds in disk_space_vec {
paths.iter().any(|p| path == p) match ds.path.as_deref() {
} else { None => filtered_disk_space_vec.push(ds),
true Some(p) => {
if monitored.contains(p) && seen_paths.insert(p.to_owned()) {
filtered_disk_space_vec.push(ds)
}
} }
}) }
.collect() }
filtered_disk_space_vec
} else { } else {
disk_space_vec disk_space_vec
} }
+12 -6
View File
@@ -469,14 +469,19 @@ mod test {
fn test_extract_monitored_disk_space_vec() { fn test_extract_monitored_disk_space_vec() {
let mut app = App::test_default(); let mut app = App::test_default();
app.server_tabs.tabs[0].config = Some(ServarrConfig { 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() ..ServarrConfig::default()
}); });
let disk_space = DiskSpace { let disk_space = DiskSpace {
path: Some("/nfs".to_string()), path: Some("/data".to_string()),
free_space: 10, free_space: 10,
total_space: 1000, 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 { let disk_space_with_empty_path = DiskSpace {
path: None, path: None,
free_space: 10, free_space: 10,
@@ -486,17 +491,18 @@ mod test {
disk_space.clone(), disk_space.clone(),
disk_space_with_empty_path.clone(), disk_space_with_empty_path.clone(),
DiskSpace { DiskSpace {
path: Some("/nfs/some/subpath".to_string()), path: Some("/downloads/".to_string()),
free_space: 10, free_space: 100,
total_space: 1000, total_space: 10000,
}, },
disk_space_2.clone(),
]; ];
let monitored_disk_space = extract_monitored_disk_space_vec(&app, disk_spaces); let monitored_disk_space = extract_monitored_disk_space_vec(&app, disk_spaces);
assert_eq!( assert_eq!(
monitored_disk_space, monitored_disk_space,
vec![disk_space, disk_space_with_empty_path] vec![disk_space, disk_space_with_empty_path, disk_space_2]
); );
} }