Implement service selection cursor and improve panel navigation
Service Selection Features: - Add selection cursor for Services panel with visual highlighting - Up/Down arrows move service selection instead of scrolling - Track selected service for future action implementation - Selection state maintained per host Panel Navigation Improvements: - Fix panel switching to only cycle through visible panels - Dynamic panel list based on backup data availability - Smart recovery when focused panel becomes invisible - No more navigation to hidden backup panel Backup Panel Scrolling Fix: - Fix backup panel scroll to show actual repository content - Replace static overflow indicator with proper scroll behavior - Add scroll position indicators (above/below) - Show all repositories when scrolling instead of truncated list Navigation now works correctly with actual UI layout and provides proper service selection for future action implementation.
This commit is contained in:
parent
6b18cdf562
commit
c851590aaa
@ -297,15 +297,47 @@ impl TuiApp {
|
||||
info!("Switched to host: {}", self.current_host.as_ref().unwrap());
|
||||
}
|
||||
|
||||
/// Switch to next panel (Shift+Tab)
|
||||
/// Switch to next panel (Shift+Tab) - only cycles through visible panels
|
||||
pub fn next_panel(&mut self) {
|
||||
self.focused_panel = self.focused_panel.next();
|
||||
let visible_panels = self.get_visible_panels();
|
||||
if visible_panels.len() <= 1 {
|
||||
return; // Can't switch if only one or no panels visible
|
||||
}
|
||||
|
||||
// Find current panel index in visible panels
|
||||
if let Some(current_index) = visible_panels.iter().position(|&p| p == self.focused_panel) {
|
||||
// Move to next visible panel
|
||||
let next_index = (current_index + 1) % visible_panels.len();
|
||||
self.focused_panel = visible_panels[next_index];
|
||||
} else {
|
||||
// Current panel not visible, switch to first visible panel
|
||||
self.focused_panel = visible_panels[0];
|
||||
}
|
||||
|
||||
info!("Switched to panel: {:?}", self.focused_panel);
|
||||
}
|
||||
|
||||
/// Switch to previous panel (Shift+Tab in reverse)
|
||||
/// Switch to previous panel (Shift+Tab in reverse) - only cycles through visible panels
|
||||
pub fn previous_panel(&mut self) {
|
||||
self.focused_panel = self.focused_panel.previous();
|
||||
let visible_panels = self.get_visible_panels();
|
||||
if visible_panels.len() <= 1 {
|
||||
return; // Can't switch if only one or no panels visible
|
||||
}
|
||||
|
||||
// Find current panel index in visible panels
|
||||
if let Some(current_index) = visible_panels.iter().position(|&p| p == self.focused_panel) {
|
||||
// Move to previous visible panel
|
||||
let prev_index = if current_index == 0 {
|
||||
visible_panels.len() - 1
|
||||
} else {
|
||||
current_index - 1
|
||||
};
|
||||
self.focused_panel = visible_panels[prev_index];
|
||||
} else {
|
||||
// Current panel not visible, switch to last visible panel
|
||||
self.focused_panel = visible_panels[visible_panels.len() - 1];
|
||||
}
|
||||
|
||||
info!("Switched to panel: {:?}", self.focused_panel);
|
||||
}
|
||||
|
||||
@ -330,12 +362,16 @@ impl TuiApp {
|
||||
info!("System panel scroll offset: {}", host_widgets.system_scroll_offset);
|
||||
}
|
||||
PanelType::Services => {
|
||||
// For services panel, Up/Down moves selection cursor, not scroll
|
||||
let total_services = host_widgets.services_widget.get_total_services_count();
|
||||
|
||||
if direction > 0 {
|
||||
host_widgets.services_scroll_offset = host_widgets.services_scroll_offset.saturating_add(1);
|
||||
host_widgets.services_widget.select_next(total_services);
|
||||
info!("Services selection moved down");
|
||||
} else {
|
||||
host_widgets.services_scroll_offset = host_widgets.services_scroll_offset.saturating_sub(1);
|
||||
host_widgets.services_widget.select_previous();
|
||||
info!("Services selection moved up");
|
||||
}
|
||||
info!("Services panel scroll offset: {}", host_widgets.services_scroll_offset);
|
||||
}
|
||||
PanelType::Backup => {
|
||||
if direction > 0 {
|
||||
@ -349,6 +385,31 @@ impl TuiApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get total count of services for bounds checking
|
||||
fn get_total_services_count(&self, hostname: &str) -> usize {
|
||||
if let Some(host_widgets) = self.host_widgets.get(hostname) {
|
||||
host_widgets.services_widget.get_total_services_count()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of currently visible panels
|
||||
fn get_visible_panels(&self) -> Vec<PanelType> {
|
||||
let mut visible_panels = vec![PanelType::System, PanelType::Services];
|
||||
|
||||
// Check if backup panel should be shown
|
||||
if let Some(hostname) = &self.current_host {
|
||||
if let Some(host_widgets) = self.host_widgets.get(hostname) {
|
||||
if host_widgets.backup_widget.has_data() {
|
||||
visible_panels.push(PanelType::Backup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible_panels
|
||||
}
|
||||
|
||||
/// Render the dashboard (real btop-style multi-panel layout)
|
||||
pub fn render(&mut self, frame: &mut Frame, metric_store: &MetricStore) {
|
||||
let size = frame.size();
|
||||
|
||||
@ -397,37 +397,15 @@ impl BackupWidget {
|
||||
ratatui::text::Span::styled("Repos:", Typography::widget_title())
|
||||
]));
|
||||
|
||||
// Repository list with overflow handling
|
||||
let remaining_space = area.height.saturating_sub(lines.len() as u16);
|
||||
let mut repo_lines = Vec::new();
|
||||
|
||||
// Add all repository lines (no truncation here - scroll will handle display)
|
||||
for service in &self.service_metrics {
|
||||
if let (Some(archives), Some(size_gb)) = (service.archive_count, service.repo_size_gb) {
|
||||
let size_str = Self::format_size_with_proper_units(size_gb);
|
||||
let repo_text = format!("{} ({}) {}", service.name, archives, size_str);
|
||||
let repo_spans = StatusIcons::create_status_spans(service.status, &repo_text);
|
||||
repo_lines.push(ratatui::text::Line::from(repo_spans));
|
||||
lines.push(ratatui::text::Line::from(repo_spans));
|
||||
}
|
||||
}
|
||||
|
||||
if repo_lines.len() <= remaining_space as usize {
|
||||
// All repos fit
|
||||
lines.extend(repo_lines);
|
||||
} else if remaining_space >= 2 {
|
||||
// Show what we can and add overflow indicator
|
||||
let lines_to_show = (remaining_space - 1) as usize; // Reserve 1 line for overflow
|
||||
lines.extend(repo_lines.iter().take(lines_to_show).cloned());
|
||||
|
||||
let hidden_repos = repo_lines.len() - lines_to_show;
|
||||
let overflow_text = format!(
|
||||
"... and {} more repo{}",
|
||||
hidden_repos,
|
||||
if hidden_repos == 1 { "" } else { "s" }
|
||||
);
|
||||
lines.push(ratatui::text::Line::from(vec![
|
||||
ratatui::text::Span::styled(overflow_text, Typography::muted())
|
||||
]));
|
||||
}
|
||||
|
||||
// Apply scroll offset
|
||||
let total_lines = lines.len();
|
||||
@ -443,11 +421,34 @@ impl BackupWidget {
|
||||
|
||||
// Apply scrolling if needed
|
||||
if scroll_offset > 0 || total_lines > available_height {
|
||||
let visible_lines: Vec<_> = lines
|
||||
let mut visible_lines: Vec<_> = lines
|
||||
.into_iter()
|
||||
.skip(effective_scroll)
|
||||
.take(available_height)
|
||||
.collect();
|
||||
|
||||
// Add scroll indicator if there are hidden lines
|
||||
if total_lines > available_height {
|
||||
let hidden_above = effective_scroll;
|
||||
let hidden_below = total_lines.saturating_sub(effective_scroll + available_height);
|
||||
|
||||
if (hidden_above > 0 || hidden_below > 0) && !visible_lines.is_empty() {
|
||||
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)
|
||||
};
|
||||
|
||||
// Replace last line with scroll indicator
|
||||
visible_lines.pop();
|
||||
visible_lines.push(ratatui::text::Line::from(vec![
|
||||
ratatui::text::Span::styled(scroll_text, Typography::muted())
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(ratatui::text::Text::from(visible_lines));
|
||||
frame.render_widget(paragraph, area);
|
||||
} else {
|
||||
|
||||
@ -22,6 +22,8 @@ pub struct ServicesWidget {
|
||||
status: Status,
|
||||
/// Last update indicator
|
||||
has_data: bool,
|
||||
/// Currently selected service index (for navigation cursor)
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -40,6 +42,7 @@ impl ServicesWidget {
|
||||
sub_services: HashMap::new(),
|
||||
status: Status::Unknown,
|
||||
has_data: false,
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,6 +196,65 @@ impl ServicesWidget {
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Move selection up
|
||||
pub fn select_previous(&mut self) {
|
||||
if self.selected_index > 0 {
|
||||
self.selected_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move selection down
|
||||
pub fn select_next(&mut self, total_services: usize) {
|
||||
if self.selected_index < total_services.saturating_sub(1) {
|
||||
self.selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get currently selected service name (for actions)
|
||||
pub fn get_selected_service(&self) -> Option<String> {
|
||||
// Build the same display list to find the selected service
|
||||
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new();
|
||||
|
||||
let mut parent_services: Vec<_> = self.parent_services.iter().collect();
|
||||
parent_services.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (parent_name, parent_info) in parent_services {
|
||||
display_lines.push((parent_name.clone(), parent_info.widget_status, false, None));
|
||||
|
||||
if let Some(sub_list) = self.sub_services.get(parent_name) {
|
||||
let mut sorted_subs = sub_list.clone();
|
||||
sorted_subs.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (i, (sub_name, sub_info)) in sorted_subs.iter().enumerate() {
|
||||
let is_last_sub = i == sorted_subs.len() - 1;
|
||||
display_lines.push((
|
||||
format!("{}_{}", parent_name, sub_name), // Use parent_sub format for sub-services
|
||||
sub_info.widget_status,
|
||||
true,
|
||||
Some((sub_info.clone(), is_last_sub)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
display_lines.get(self.selected_index).map(|(name, _, _, _)| name.clone())
|
||||
}
|
||||
|
||||
/// Get total count of services (parent + sub-services)
|
||||
pub fn get_total_services_count(&self) -> usize {
|
||||
let mut count = 0;
|
||||
|
||||
// Count parent services
|
||||
count += self.parent_services.len();
|
||||
|
||||
// Count sub-services
|
||||
for sub_list in self.sub_services.values() {
|
||||
count += sub_list.len();
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ServicesWidget {
|
||||
@ -408,6 +470,9 @@ impl ServicesWidget {
|
||||
|
||||
for (i, (line_text, line_status, is_sub, sub_info)) in visible_lines.iter().enumerate()
|
||||
{
|
||||
let actual_index = effective_scroll + i; // Real index in the full list
|
||||
let is_selected = actual_index == self.selected_index;
|
||||
|
||||
let spans = if *is_sub && sub_info.is_some() {
|
||||
// Use custom sub-service span creation
|
||||
let (service_info, is_last) = sub_info.as_ref().unwrap();
|
||||
@ -416,7 +481,18 @@ impl ServicesWidget {
|
||||
// Use regular status spans for parent services
|
||||
StatusIcons::create_status_spans(*line_status, line_text)
|
||||
};
|
||||
let service_para = Paragraph::new(ratatui::text::Line::from(spans));
|
||||
|
||||
let mut service_para = Paragraph::new(ratatui::text::Line::from(spans));
|
||||
|
||||
// Apply selection highlighting
|
||||
if is_selected {
|
||||
service_para = service_para.style(
|
||||
Style::default()
|
||||
.bg(Theme::highlight())
|
||||
.fg(Theme::background())
|
||||
);
|
||||
}
|
||||
|
||||
frame.render_widget(service_para, service_chunks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user