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},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Padding, Paragraph},
widgets::{Block, Borders, Padding, Paragraph, Scrollbar, ScrollbarOrientation},
};
use std::{
@ -128,14 +128,26 @@ impl App {
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 {
Self::render_loading(frame, chunks[1]);
Self::render_loading(frame, main_chunks[0]);
} 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() {
Self::render_no_data(frame, chunks[1]);
Self::render_no_data(frame, main_chunks[0]);
} 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
@ -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<()> {
let timeout = Duration::from_secs(1);
@ -159,6 +187,12 @@ impl App {
match key.code {
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,43 +16,29 @@ use ratatui::{
const STATUS_LINE_LENGTH: usize = 100;
const MAX_NAME_LENGTH: usize = 30;
pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) {
let group_areas = layout_groups(area, &state.groups);
pub fn render_monitor_list(main_frame: &mut Frame, area: Rect, state: &DashboardViewState) {
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() {
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) {
let chunks = Layout::default()
.direction(Direction::Vertical)
@ -60,6 +46,10 @@ fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first:
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(area);
if chunks[0].height <= 0 || chunks[1].height <= 0 || group.monitors.is_empty() {
return;
}
let group_title = Line::from(vec![
Span::styled(format!("{} ", group.name), title_style()),
Span::styled(

View file

@ -1,8 +1,11 @@
use crate::core::models::{UnifiedData, UnifiedGroupData};
use crate::data::heartbeat::model::HeartbeatEntry;
use crate::i18n::t;
use ratatui::widgets::ScrollbarState;
use rayon::prelude::*;
const BORDER_LINES_VIEW: usize = 3;
#[derive(Debug, Clone, PartialEq)]
pub enum MonitorStatus {
Up,
@ -33,6 +36,7 @@ pub struct DashboardViewState {
pub is_loading: bool,
pub error_message: Option<String>,
pub auto_refresh_interval: u32,
pub scroll_state: ScrollbarState,
}
impl DashboardViewState {
@ -44,6 +48,7 @@ impl DashboardViewState {
is_loading: true,
error_message: None,
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 {
title: data.title,
descriptions: data.description,
@ -64,12 +74,17 @@ impl DashboardViewState {
is_loading: false,
error_message: None,
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> {
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> {
@ -94,36 +109,39 @@ fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
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) {
Some(1) => MonitorStatus::Up,
Some(0) => MonitorStatus::Down,
_ => MonitorStatus::Unknown,
};
let status = match monitor.heartbeats.last().map(|h| h.status) {
Some(1) => MonitorStatus::Up,
Some(0) => MonitorStatus::Down,
_ => MonitorStatus::Unknown,
};
let response_time = monitor
.heartbeats
.last()
.and_then(|h| h.ping)
.map(|ms| format!("{}", ms))
.unwrap_or_else(|| t("unknown").to_string() + " ");
let response_time = monitor
.heartbeats
.last()
.and_then(|h| h.ping)
.map(|ms| format!("{}", ms))
.unwrap_or_else(|| t("unknown").to_string() + " ");
let uptime_24h = monitor
.uptime_data
.map(|u| u.get_perc_formated())
.unwrap_or_else(|| t("unknown").to_string());
let uptime_24h = monitor
.uptime_data
.map(|u| u.get_perc_formated())
.unwrap_or_else(|| t("unknown").to_string());
MonitorViewState {
name: monitor.name,
status,
response_time,
uptime_24h,
status_history,
}
}).collect_into_vec(&mut monitors);
MonitorViewState {
name: monitor.name,
status,
response_time,
uptime_24h,
status_history,
}
})
.collect_into_vec(&mut monitors);
monitors.sort_by_key(|m| m.name.clone());