From 6b18cdf56281aab89d881ad8aad7783db4b1c7d0 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Thu, 23 Oct 2025 21:01:11 +0200 Subject: [PATCH] Fix keyboard navigation and panel scrolling issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Network panel from navigation cycle - Fix system panel scrolling to work in both directions - Add complete scroll support to Services and Backup panels - Update panel cycling to System → Services → Backup only - Enhance scroll indicators with proper bounds checking - Clean up unused Network panel code and references Resolves issues with non-functional up/down scrolling and mystery network panel appearing during navigation. --- dashboard/src/ui/mod.rs | 40 +++++++--------- dashboard/src/ui/widgets/backup.rs | 34 +++++++++++++- dashboard/src/ui/widgets/services.rs | 68 ++++++++++++++++++++-------- dashboard/src/ui/widgets/system.rs | 12 +++-- 4 files changed, 106 insertions(+), 48 deletions(-) diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index f61426f..eee6690 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -24,32 +24,29 @@ pub enum PanelType { System, Services, Backup, - Network, } impl PanelType { /// Get all panel types in order - pub fn all() -> [PanelType; 4] { - [PanelType::System, PanelType::Services, PanelType::Backup, PanelType::Network] + pub fn all() -> [PanelType; 3] { + [PanelType::System, PanelType::Services, PanelType::Backup] } - /// Get the next panel in cycle + /// Get the next panel in cycle (System → Services → Backup → System) pub fn next(self) -> PanelType { match self { PanelType::System => PanelType::Services, PanelType::Services => PanelType::Backup, - PanelType::Backup => PanelType::Network, - PanelType::Network => PanelType::System, + PanelType::Backup => PanelType::System, } } - /// Get the previous panel in cycle + /// Get the previous panel in cycle (System ← Services ← Backup ← System) pub fn previous(self) -> PanelType { match self { - PanelType::System => PanelType::Network, + PanelType::System => PanelType::Backup, PanelType::Services => PanelType::System, PanelType::Backup => PanelType::Services, - PanelType::Network => PanelType::Backup, } } } @@ -67,7 +64,6 @@ pub struct HostWidgets { pub system_scroll_offset: usize, pub services_scroll_offset: usize, pub backup_scroll_offset: usize, - pub network_scroll_offset: usize, /// Last update time for this host pub last_update: Option, } @@ -81,7 +77,6 @@ impl HostWidgets { system_scroll_offset: 0, services_scroll_offset: 0, backup_scroll_offset: 0, - network_scroll_offset: 0, last_update: None, } } @@ -350,14 +345,6 @@ impl TuiApp { } info!("Backup panel scroll offset: {}", host_widgets.backup_scroll_offset); } - PanelType::Network => { - if direction > 0 { - host_widgets.network_scroll_offset = host_widgets.network_scroll_offset.saturating_add(1); - } else { - host_widgets.network_scroll_offset = host_widgets.network_scroll_offset.saturating_sub(1); - } - info!("Network panel scroll offset: {}", host_widgets.network_scroll_offset); - } } } } @@ -430,10 +417,14 @@ impl TuiApp { // Render services widget for current host if let Some(hostname) = self.current_host.clone() { let is_focused = self.focused_panel == PanelType::Services; + let scroll_offset = { + let host_widgets = self.get_or_create_host_widgets(&hostname); + host_widgets.services_scroll_offset + }; let host_widgets = self.get_or_create_host_widgets(&hostname); host_widgets .services_widget - .render_with_focus(frame, content_chunks[1], is_focused); // Services takes full right side + .render_with_focus_and_scroll(frame, content_chunks[1], is_focused, scroll_offset); // Services takes full right side } // Render statusbar at the bottom @@ -571,9 +562,6 @@ impl TuiApp { PanelType::Backup => { shortcuts.push("B: Trigger Backup".to_string()); } - PanelType::Network => { - shortcuts.push("N: Network Info".to_string()); - } } // Always show quit @@ -612,8 +600,12 @@ impl TuiApp { // Get current host widgets for backup widget if let Some(hostname) = self.current_host.clone() { + let scroll_offset = { + let host_widgets = self.get_or_create_host_widgets(&hostname); + host_widgets.backup_scroll_offset + }; let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.backup_widget.render(frame, inner_area); + host_widgets.backup_widget.render_with_scroll(frame, inner_area, scroll_offset); } } diff --git a/dashboard/src/ui/widgets/backup.rs b/dashboard/src/ui/widgets/backup.rs index 86d1505..fbc8dac 100644 --- a/dashboard/src/ui/widgets/backup.rs +++ b/dashboard/src/ui/widgets/backup.rs @@ -325,6 +325,13 @@ impl Widget for BackupWidget { } fn render(&mut self, frame: &mut Frame, area: Rect) { + self.render_with_scroll(frame, area, 0); + } +} + +impl BackupWidget { + /// Render with scroll offset support + pub fn render_with_scroll(&mut self, frame: &mut Frame, area: Rect, scroll_offset: usize) { let mut lines = Vec::new(); // Latest backup section @@ -422,8 +429,31 @@ impl Widget for BackupWidget { ])); } - let paragraph = Paragraph::new(ratatui::text::Text::from(lines)); - frame.render_widget(paragraph, area); + // Apply scroll offset + let total_lines = lines.len(); + let available_height = area.height as usize; + + // Calculate scroll boundaries + let max_scroll = if total_lines > available_height { + total_lines - available_height + } else { + total_lines.saturating_sub(1) + }; + let effective_scroll = scroll_offset.min(max_scroll); + + // Apply scrolling if needed + if scroll_offset > 0 || total_lines > available_height { + let visible_lines: Vec<_> = lines + .into_iter() + .skip(effective_scroll) + .take(available_height) + .collect(); + let paragraph = Paragraph::new(ratatui::text::Text::from(visible_lines)); + frame.render_widget(paragraph, area); + } else { + let paragraph = Paragraph::new(ratatui::text::Text::from(lines)); + frame.render_widget(paragraph, area); + } } } diff --git a/dashboard/src/ui/widgets/services.rs b/dashboard/src/ui/widgets/services.rs index 17497c0..d9aa5c9 100644 --- a/dashboard/src/ui/widgets/services.rs +++ b/dashboard/src/ui/widgets/services.rs @@ -313,8 +313,13 @@ impl Widget for ServicesWidget { } impl ServicesWidget { - /// Render with optional focus indicator + /// Render with optional focus indicator and scroll support pub fn render_with_focus(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) { + self.render_with_focus_and_scroll(frame, area, is_focused, 0); + } + + /// Render with focus indicator and scroll offset + pub fn render_with_focus_and_scroll(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) { let services_block = if is_focused { Components::focused_widget_block("services") } else { @@ -374,9 +379,26 @@ impl ServicesWidget { } } - // Render all lines within available space + // Apply scroll offset and render visible lines let available_lines = content_chunks[1].height as usize; - let lines_to_show = available_lines.min(display_lines.len()); + let total_lines = display_lines.len(); + + // Calculate scroll boundaries + let max_scroll = if total_lines > available_lines { + total_lines - available_lines + } else { + total_lines.saturating_sub(1) + }; + let effective_scroll = scroll_offset.min(max_scroll); + + // Get visible lines after scrolling + let visible_lines: Vec<_> = display_lines + .iter() + .skip(effective_scroll) + .take(available_lines) + .collect(); + + let lines_to_show = visible_lines.len(); if lines_to_show > 0 { let service_chunks = Layout::default() @@ -384,8 +406,7 @@ impl ServicesWidget { .constraints(vec![Constraint::Length(1); lines_to_show]) .split(content_chunks[1]); - for (i, (line_text, line_status, is_sub, sub_info)) in - display_lines.iter().take(lines_to_show).enumerate() + for (i, (line_text, line_status, is_sub, sub_info)) in visible_lines.iter().enumerate() { let spans = if *is_sub && sub_info.is_some() { // Use custom sub-service span creation @@ -400,20 +421,31 @@ impl ServicesWidget { } } - // Show indicator if there are more services than we can display - if display_lines.len() > available_lines { - let more_count = display_lines.len() - available_lines; - if available_lines > 0 { - let last_line_area = Rect { - x: content_chunks[1].x, - y: content_chunks[1].y + (available_lines - 1) as u16, - width: content_chunks[1].width, - height: 1, + // Show scroll indicator if there are more services than we can display + if total_lines > available_lines { + let hidden_above = effective_scroll; + let hidden_below = total_lines.saturating_sub(effective_scroll + available_lines); + + if hidden_above > 0 || hidden_below > 0 { + let scroll_text = if hidden_above > 0 && hidden_below > 0 { + format!("... {} above, {} below", hidden_above, hidden_below) + } else if hidden_above > 0 { + format!("... {} more above", hidden_above) + } else { + format!("... {} more below", hidden_below) }; - - let more_text = format!("... and {} more services", more_count); - let more_para = Paragraph::new(more_text).style(Typography::muted()); - frame.render_widget(more_para, last_line_area); + + if available_lines > 0 && lines_to_show > 0 { + let last_line_area = Rect { + x: content_chunks[1].x, + y: content_chunks[1].y + (lines_to_show - 1) as u16, + width: content_chunks[1].width, + height: 1, + }; + + let scroll_para = Paragraph::new(scroll_text).style(Typography::muted()); + frame.render_widget(scroll_para, last_line_area); + } } } } diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 276e6e5..b6527b4 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -520,9 +520,13 @@ impl SystemWidget { let total_lines = lines.len(); let available_height = area.height as usize; - if total_lines > available_height { - // Content is larger than area, apply scrolling - let max_scroll = total_lines.saturating_sub(available_height); + // Always apply scrolling if scroll_offset > 0, even if content fits + if scroll_offset > 0 || total_lines > available_height { + let max_scroll = if total_lines > available_height { + total_lines - available_height + } else { + total_lines.saturating_sub(1) + }; let effective_scroll = scroll_offset.min(max_scroll); // Take only the visible portion after scrolling @@ -535,7 +539,7 @@ impl SystemWidget { let paragraph = Paragraph::new(Text::from(visible_lines)); frame.render_widget(paragraph, area); } else { - // All content fits, render normally + // All content fits and no scroll offset, render normally let paragraph = Paragraph::new(Text::from(lines)); frame.render_widget(paragraph, area); }