Show vertical scrollbar only if height is lower than total monitors

This commit is contained in:
Marco De Araujo 2026-01-08 09:25:26 -04:00
parent d4e3ce6e27
commit 142f76684b
3 changed files with 105 additions and 63 deletions

View file

@ -18,7 +18,7 @@ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, Borders, Padding, Paragraph}, widgets::{Block, Borders, Padding, Paragraph, Scrollbar, ScrollbarOrientation},
}; };
use std::{ use std::{
@ -128,14 +128,26 @@ impl App {
render_header(frame, chunks[0], &self.state); render_header(frame, chunks[0], &self.state);
let mut main_constraint = Vec::with_capacity(2);
main_constraint.push(Constraint::Min(1));
if self.state.show_vertical_scrollbar(chunks[1].height) {
main_constraint.push(Constraint::Length(1));
}
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(main_constraint)
.split(chunks[1]);
if self.state.is_loading { if self.state.is_loading {
Self::render_loading(frame, chunks[1]); Self::render_loading(frame, main_chunks[0]);
} else if let Some(error) = &self.state.error_message { } else if let Some(error) = &self.state.error_message {
Self::render_error(frame, chunks[1], error); Self::render_error(frame, main_chunks[0], error);
} else if self.state.groups.is_empty() || self.state.get_all_monitors().is_empty() { } else if self.state.groups.is_empty() || self.state.get_all_monitors().is_empty() {
Self::render_no_data(frame, chunks[1]); Self::render_no_data(frame, main_chunks[0]);
} else { } else {
render_monitor_list(frame, chunks[1], &self.state); Self::render_main(frame, main_chunks.to_vec(), &mut self.state);
} }
let seconds_until_update = self let seconds_until_update = self
@ -148,6 +160,22 @@ impl App {
}); });
} }
fn render_main(frame: &mut Frame, area: Vec<Rect>, state: &mut DashboardViewState) {
render_monitor_list(frame, area[0], state);
let total_monitors = state.get_all_monitors().len();
if area[0].height > total_monitors as u16 {
return;
}
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some(""));
frame.render_stateful_widget(scrollbar, area[1], &mut state.scroll_state);
}
fn handle_events(&mut self) -> io::Result<()> { fn handle_events(&mut self) -> io::Result<()> {
let timeout = Duration::from_secs(1); let timeout = Duration::from_secs(1);
@ -159,6 +187,12 @@ impl App {
match key.code { match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true, KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
KeyCode::Up | KeyCode::Char('k') => {
self.state.scroll_state.prev();
}
KeyCode::Down | KeyCode::Char('j') => {
self.state.scroll_state.next();
}
_ => {} _ => {}
} }
} }

View file

@ -16,41 +16,27 @@ use ratatui::{
const STATUS_LINE_LENGTH: usize = 100; const STATUS_LINE_LENGTH: usize = 100;
const MAX_NAME_LENGTH: usize = 30; const MAX_NAME_LENGTH: usize = 30;
pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) { pub fn render_monitor_list(main_frame: &mut Frame, area: Rect, state: &DashboardViewState) {
let group_areas = layout_groups(area, &state.groups); let constraints: Vec<Constraint> = state
.groups
.iter()
.map(|g| {
let height_neeed = 3 + g.monitors.len();
Constraint::Length(height_neeed as u16)
})
.collect();
if constraints.is_empty() {
return;
}
let group_areas = Layout::vertical(constraints).split(area);
for (i, (group, &group_area)) in state.groups.iter().zip(group_areas.iter()).enumerate() { for (i, (group, &group_area)) in state.groups.iter().zip(group_areas.iter()).enumerate() {
render_group(frame, group_area, group, i == 0); if group_area.height > 0 {
render_group(main_frame, group_area, group, i == 0);
} }
}
fn layout_groups(area: Rect, groups: &[GroupViewState]) -> Vec<Rect> {
let total_lines: usize = groups.iter().map(|g| g.monitors.len() + 1).sum();
if total_lines == 0 {
return Vec::new();
} }
let content_height = area.height.saturating_sub(2);
let line_height = content_height as usize / total_lines;
let mut current_y = area.y + 1;
let mut areas = Vec::with_capacity(groups.len());
for group in groups {
let group_lines = group.monitors.len() + 2;
let group_height = (group_lines + line_height).max(1);
areas.push(Rect {
x: area.x,
y: current_y,
width: area.width,
height: group_height as u16,
});
current_y += group_height as u16;
}
areas
} }
fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first: bool) { fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first: bool) {
@ -60,6 +46,10 @@ fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first:
.constraints([Constraint::Length(1), Constraint::Min(1)]) .constraints([Constraint::Length(1), Constraint::Min(1)])
.split(area); .split(area);
if chunks[0].height <= 0 || chunks[1].height <= 0 || group.monitors.is_empty() {
return;
}
let group_title = Line::from(vec![ let group_title = Line::from(vec![
Span::styled(format!("{} ", group.name), title_style()), Span::styled(format!("{} ", group.name), title_style()),
Span::styled( Span::styled(

View file

@ -1,8 +1,11 @@
use crate::core::models::{UnifiedData, UnifiedGroupData}; use crate::core::models::{UnifiedData, UnifiedGroupData};
use crate::data::heartbeat::model::HeartbeatEntry; use crate::data::heartbeat::model::HeartbeatEntry;
use crate::i18n::t; use crate::i18n::t;
use ratatui::widgets::ScrollbarState;
use rayon::prelude::*; use rayon::prelude::*;
const BORDER_LINES_VIEW: usize = 3;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum MonitorStatus { pub enum MonitorStatus {
Up, Up,
@ -33,6 +36,7 @@ pub struct DashboardViewState {
pub is_loading: bool, pub is_loading: bool,
pub error_message: Option<String>, pub error_message: Option<String>,
pub auto_refresh_interval: u32, pub auto_refresh_interval: u32,
pub scroll_state: ScrollbarState,
} }
impl DashboardViewState { impl DashboardViewState {
@ -44,6 +48,7 @@ impl DashboardViewState {
is_loading: true, is_loading: true,
error_message: None, error_message: None,
auto_refresh_interval: 300, auto_refresh_interval: 300,
scroll_state: ScrollbarState::new(0),
} }
} }
@ -57,6 +62,11 @@ impl DashboardViewState {
}); });
} }
let content_length = groups
.iter()
.map(|g| g.monitors.len() + BORDER_LINES_VIEW)
.sum::<usize>();
Self { Self {
title: data.title, title: data.title,
descriptions: data.description, descriptions: data.description,
@ -64,12 +74,17 @@ impl DashboardViewState {
is_loading: false, is_loading: false,
error_message: None, error_message: None,
auto_refresh_interval: data.auto_refresh_interval.max(30), auto_refresh_interval: data.auto_refresh_interval.max(30),
scroll_state: ScrollbarState::new(content_length.saturating_sub(1)),
} }
} }
pub fn get_all_monitors(&self) -> Vec<&MonitorViewState> { pub fn get_all_monitors(&self) -> Vec<&MonitorViewState> {
self.groups.iter().flat_map(|g| g.monitors.iter()).collect() self.groups.iter().flat_map(|g| g.monitors.iter()).collect()
} }
pub fn show_vertical_scrollbar(&self, height: u16) -> bool {
height < self.get_all_monitors().len() as u16
}
} }
fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> { fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
@ -94,8 +109,10 @@ fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> { fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
let mut monitors = Vec::with_capacity(group.monitors.len()); let mut monitors = Vec::with_capacity(group.monitors.len());
group.monitors.into_par_iter().map(|monitor| { group
.monitors
.into_par_iter()
.map(|monitor| {
let status_history = get_status_history(&monitor.heartbeats); let status_history = get_status_history(&monitor.heartbeats);
let status = match monitor.heartbeats.last().map(|h| h.status) { let status = match monitor.heartbeats.last().map(|h| h.status) {
@ -123,7 +140,8 @@ fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
uptime_24h, uptime_24h,
status_history, status_history,
} }
}).collect_into_vec(&mut monitors); })
.collect_into_vec(&mut monitors);
monitors.sort_by_key(|m| m.name.clone()); monitors.sort_by_key(|m| m.name.clone());