feat: Full support for filtering disks and aggregating root folders in the UI's 'Stats' block
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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, ..
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user