diff --git a/Cargo.lock b/Cargo.lock index 2ac5165..7ca3600 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.281" +version = "0.1.282" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.281" +version = "0.1.282" dependencies = [ "anyhow", "async-trait", @@ -325,7 +325,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.281" +version = "0.1.282" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 76a3c16..0b7ac65 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.281" +version = "0.1.282" edition = "2021" [dependencies] diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index a0c287a..adb75e1 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.281" +version = "0.1.282" edition = "2021" [dependencies] diff --git a/dashboard/src/app.rs b/dashboard/src/app.rs index dfa42a8..e854249 100644 --- a/dashboard/src/app.rs +++ b/dashboard/src/app.rs @@ -24,6 +24,7 @@ pub struct Dashboard { config: DashboardConfig, system_area: Rect, // Store system area for mouse event handling services_area: Rect, // Store services area for mouse event handling + hosts_area: Rect, // Store hosts area for mouse event handling } impl Dashboard { @@ -125,6 +126,7 @@ impl Dashboard { config, system_area: Rect::default(), services_area: Rect::default(), + hosts_area: Rect::default(), }) } @@ -272,9 +274,10 @@ impl Dashboard { // Render TUI regardless of terminal size if let Err(e) = terminal.draw(|frame| { - let (_title_area, system_area, services_area) = tui_app.render(frame, &self.metric_store); + let (_title_area, system_area, services_area, hosts_area) = tui_app.render(frame, &self.metric_store); self.system_area = system_area; self.services_area = services_area; + self.hosts_area = hosts_area; }) { error!("Error rendering TUI: {}", e); break; @@ -381,8 +384,9 @@ impl Dashboard { // Determine which panel the mouse is over let in_system_area = is_in_area(x, y, &self.system_area); let in_services_area = is_in_area(x, y, &self.services_area); + let in_hosts_area = is_in_area(x, y, &self.hosts_area); - if !in_system_area && !in_services_area { + if !in_system_area && !in_services_area && !in_hosts_area { return Ok(()); } @@ -431,6 +435,40 @@ impl Dashboard { } } MouseEventKind::Down(button) => { + // Handle clicks in hosts area + if in_hosts_area { + if let MouseButton::Left = button { + if let Some(ref mut tui_app) = self.tui_app { + // Calculate which host was clicked based on position + let relative_x = x.saturating_sub(self.hosts_area.x + 1) as usize; // +1 for border + let relative_y = y.saturating_sub(self.hosts_area.y + 1) as usize; // +1 for border + + // Calculate column width based on hostnames + let max_name_len = tui_app.available_hosts.iter().map(|h| h.len()).max().unwrap_or(8); + let col_width = 2 + 2 + max_name_len + 1 + 1; // icon + arrow + name + asterisk + padding + + // Calculate number of columns and rows + let inner_width = self.hosts_area.width.saturating_sub(2) as usize; + let num_columns = (inner_width / col_width).max(1); + let rows_per_column = (tui_app.available_hosts.len() + num_columns - 1) / num_columns; + + // Determine which column was clicked + let clicked_column = relative_x / col_width; + let clicked_row = relative_y; + + // Calculate host index + let host_index = clicked_column * rows_per_column + clicked_row; + + if host_index < tui_app.available_hosts.len() { + let hostname = tui_app.available_hosts[host_index].clone(); + debug!("Clicked host: {} (index: {})", hostname, host_index); + tui_app.set_current_host(Some(hostname)); + } + } + } + return Ok(()); + } + // Only handle clicks in services area (not system area) if !in_services_area { return Ok(()); diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index 21ead0e..38dd792 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -63,7 +63,7 @@ pub struct TuiApp { /// Current active host pub current_host: Option, /// Available hosts - available_hosts: Vec, + pub available_hosts: Vec, /// Should quit application should_quit: bool, /// Track if user manually navigated away from localhost @@ -440,6 +440,13 @@ impl TuiApp { self.switch_to_host(&prev_host); } + /// Set current host (for mouse click selection) + pub fn set_current_host(&mut self, hostname: Option) { + if let Some(host) = hostname { + self.switch_to_host(&host); + } + } + @@ -465,7 +472,7 @@ impl TuiApp { /// Render the dashboard (real btop-style multi-panel layout) - pub fn render(&mut self, frame: &mut Frame, metric_store: &MetricStore) -> (Rect, Rect, Rect) { + pub fn render(&mut self, frame: &mut Frame, metric_store: &MetricStore) -> (Rect, Rect, Rect, Rect) { let size = frame.size(); // Clear background to true black like btop @@ -519,7 +526,8 @@ impl TuiApp { self.render_btop_title(frame, main_chunks[0], metric_store); // Render hosts panel on left - self.render_hosts_panel(frame, left_chunks[0], metric_store); + let hosts_area = left_chunks[0]; + self.render_hosts_panel(frame, hosts_area, metric_store); // Render system panel below hosts let system_area = left_chunks[1]; @@ -543,7 +551,7 @@ impl TuiApp { } // Return all areas for mouse event handling - (main_chunks[0], system_area, services_area) + (main_chunks[0], system_area, services_area, hosts_area) } /// Render btop-style minimal title with host status colors diff --git a/dashboard/src/ui/widgets/services.rs b/dashboard/src/ui/widgets/services.rs index 2b1f388..7783e13 100644 --- a/dashboard/src/ui/widgets/services.rs +++ b/dashboard/src/ui/widgets/services.rs @@ -19,61 +19,64 @@ struct ColumnVisibility { show_ram: bool, show_uptime: bool, show_restarts: bool, + name_width: u16, } impl ColumnVisibility { - /// Calculate actual width needed for all columns - const NAME_WIDTH: u16 = 23; + /// Fixed column widths for non-name columns const STATUS_WIDTH: u16 = 10; const RAM_WIDTH: u16 = 8; const UPTIME_WIDTH: u16 = 8; const RESTARTS_WIDTH: u16 = 5; - const COLUMN_SPACING: u16 = 1; // Space between columns + const COLUMN_SPACING: u16 = 1; + const MIN_NAME_WIDTH: u16 = 10; + + /// Determine which columns to show based on available width and service names + fn from_width_and_names(width: u16, max_name_len: usize) -> Self { + // Calculate name width: icon(2) + name + padding(1), min 10 + let name_width = ((2 + max_name_len + 1) as u16).max(Self::MIN_NAME_WIDTH); - /// Determine which columns to show based on available width - /// Priority order: Name > Status > RAM > Uptime > Restarts - fn from_width(width: u16) -> Self { // Calculate cumulative widths for each configuration - let minimal = Self::NAME_WIDTH + Self::COLUMN_SPACING + Self::STATUS_WIDTH; // 34 - let with_ram = minimal + Self::COLUMN_SPACING + Self::RAM_WIDTH; // 43 - let with_uptime = with_ram + Self::COLUMN_SPACING + Self::UPTIME_WIDTH; // 52 - let full = with_uptime + Self::COLUMN_SPACING + Self::RESTARTS_WIDTH; // 58 + let minimal = name_width + Self::COLUMN_SPACING + Self::STATUS_WIDTH; + let with_ram = minimal + Self::COLUMN_SPACING + Self::RAM_WIDTH; + let with_uptime = with_ram + Self::COLUMN_SPACING + Self::UPTIME_WIDTH; + let full = with_uptime + Self::COLUMN_SPACING + Self::RESTARTS_WIDTH; if width >= full { - // Show all columns Self { show_name: true, show_status: true, show_ram: true, show_uptime: true, show_restarts: true, + name_width, } } else if width >= with_uptime { - // Hide restarts Self { show_name: true, show_status: true, show_ram: true, show_uptime: true, show_restarts: false, + name_width, } } else if width >= with_ram { - // Hide uptime and restarts Self { show_name: true, show_status: true, show_ram: true, show_uptime: false, show_restarts: false, + name_width, } } else { - // Minimal: Name + Status only Self { show_name: true, show_status: true, show_ram: false, show_uptime: false, show_restarts: false, + name_width, } } } @@ -125,6 +128,15 @@ impl ServicesWidget { self.status } + /// Get max service name length for column width calculation + fn get_max_service_name_len(&self) -> usize { + self.parent_services + .keys() + .map(|name| name.len()) + .max() + .unwrap_or(8) + } + /// Extract service name and determine if it's a parent or sub-service #[allow(dead_code)] fn extract_service_info(metric_name: &str) -> Option<(String, Option)> { @@ -156,7 +168,7 @@ impl ServicesWidget { /// Format parent service line - returns text without icon for span formatting fn format_parent_service_line(&self, name: &str, info: &ServiceInfo, columns: ColumnVisibility) -> String { // Account for icon prefix "● " (2 chars) in name column width - let name_width = ColumnVisibility::NAME_WIDTH.saturating_sub(2) as usize; + let name_width = columns.name_width.saturating_sub(2) as usize; // Truncate long service names to fit layout let max_name_len = name_width.saturating_sub(3); // -3 for "..." let short_name = if name.len() > max_name_len { @@ -724,13 +736,16 @@ impl ServicesWidget { .constraints([Constraint::Length(1), Constraint::Min(0)]) .split(area); - // Determine which columns to show based on available width - let columns = ColumnVisibility::from_width(area.width); + // Calculate max service name length for dynamic column width + let max_name_len = self.get_max_service_name_len(); + + // Determine which columns to show based on available width and service names + let columns = ColumnVisibility::from_width_and_names(area.width, max_name_len); // Build header - columns must align with service row format let mut header_parts = Vec::new(); if columns.show_name { - header_parts.push(format!("{: