Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3fc5a181d | |||
| 3f45a172b3 | |||
| 5b12c12228 | |||
| 651b801de3 | |||
| 71b9f93d7c |
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "cm-dashboard"
|
||||
version = "0.1.22"
|
||||
version = "0.1.27"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -291,7 +291,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cm-dashboard-agent"
|
||||
version = "0.1.22"
|
||||
version = "0.1.27"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -314,7 +314,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cm-dashboard-shared"
|
||||
version = "0.1.22"
|
||||
version = "0.1.27"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-dashboard-agent"
|
||||
version = "0.1.23"
|
||||
version = "0.1.28"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-dashboard"
|
||||
version = "0.1.23"
|
||||
version = "0.1.28"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -14,7 +14,7 @@ use app::Dashboard;
|
||||
|
||||
/// Get hardcoded version
|
||||
fn get_version() -> &'static str {
|
||||
"v0.1.23"
|
||||
"v0.1.28"
|
||||
}
|
||||
|
||||
/// Check if running inside tmux session
|
||||
|
||||
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
|
||||
pub mod theme;
|
||||
@@ -28,12 +28,6 @@ pub enum UiCommand {
|
||||
TriggerBackup { hostname: String },
|
||||
}
|
||||
|
||||
/// Command execution status for visual feedback
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CommandStatus {
|
||||
/// Command is executing
|
||||
InProgress { command_type: CommandType, target: String, start_time: std::time::Instant },
|
||||
}
|
||||
|
||||
/// Types of commands for status tracking
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -70,8 +64,8 @@ pub struct HostWidgets {
|
||||
pub backup_scroll_offset: usize,
|
||||
/// Last update time for this host
|
||||
pub last_update: Option<Instant>,
|
||||
/// Active command status for visual feedback
|
||||
pub command_status: Option<CommandStatus>,
|
||||
/// Pending service transitions for immediate visual feedback
|
||||
pub pending_service_transitions: HashMap<String, (CommandType, String, Instant)>, // service_name -> (command_type, original_status, start_time)
|
||||
}
|
||||
|
||||
impl HostWidgets {
|
||||
@@ -84,7 +78,7 @@ impl HostWidgets {
|
||||
services_scroll_offset: 0,
|
||||
backup_scroll_offset: 0,
|
||||
last_update: None,
|
||||
command_status: None,
|
||||
pending_service_transitions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,8 +127,6 @@ impl TuiApp {
|
||||
|
||||
/// Update widgets with metrics from store (only for current host)
|
||||
pub fn update_metrics(&mut self, metric_store: &MetricStore) {
|
||||
// Check for command timeouts first
|
||||
self.check_command_timeouts();
|
||||
|
||||
// Check for rebuild completion by agent hash change
|
||||
|
||||
@@ -168,6 +160,9 @@ impl TuiApp {
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
// Clear completed transitions first
|
||||
self.clear_completed_transitions(&hostname, &service_metrics);
|
||||
|
||||
// Now get host widgets and update them
|
||||
let host_widgets = self.get_or_create_host_widgets(&hostname);
|
||||
|
||||
@@ -209,9 +204,9 @@ impl TuiApp {
|
||||
// Sort hosts alphabetically
|
||||
let mut sorted_hosts = hosts.clone();
|
||||
|
||||
// Keep hosts that have ongoing commands even if they're offline
|
||||
// Keep hosts that have pending transitions even if they're offline
|
||||
for (hostname, host_widgets) in &self.host_widgets {
|
||||
if let Some(CommandStatus::InProgress { .. }) = &host_widgets.command_status {
|
||||
if !host_widgets.pending_service_transitions.is_empty() {
|
||||
if !sorted_hosts.contains(hostname) {
|
||||
sorted_hosts.push(hostname.clone());
|
||||
}
|
||||
@@ -427,7 +422,7 @@ impl TuiApp {
|
||||
None
|
||||
}
|
||||
|
||||
/// Start command execution and track status for visual feedback (with state validation)
|
||||
/// Start command execution with immediate visual feedback
|
||||
pub fn start_command(&mut self, hostname: &str, command_type: CommandType, target: String) -> bool {
|
||||
// Get current service status to validate command
|
||||
let current_status = self.get_current_service_status(hostname, &target);
|
||||
@@ -438,13 +433,11 @@ impl TuiApp {
|
||||
(CommandType::ServiceStop, Some("active")) => true,
|
||||
(CommandType::ServiceRestart, Some("active") | Some("inactive") | Some("failed") | Some("dead")) => true,
|
||||
(CommandType::ServiceStart, Some("active")) => {
|
||||
// Already running - show brief feedback but don't execute
|
||||
// TODO: Could show a brief "already running" message
|
||||
// Already running - don't execute
|
||||
false
|
||||
},
|
||||
(CommandType::ServiceStop, Some("inactive") | Some("failed") | Some("dead")) => {
|
||||
// Already stopped - show brief feedback but don't execute
|
||||
// TODO: Could show a brief "already stopped" message
|
||||
// Already stopped - don't execute
|
||||
false
|
||||
},
|
||||
(_, None) => {
|
||||
@@ -454,38 +447,58 @@ impl TuiApp {
|
||||
_ => true, // Default: allow other combinations
|
||||
};
|
||||
|
||||
if should_execute {
|
||||
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
|
||||
host_widgets.command_status = Some(CommandStatus::InProgress {
|
||||
command_type,
|
||||
target,
|
||||
start_time: Instant::now(),
|
||||
});
|
||||
}
|
||||
// ALWAYS store the pending transition for immediate visual feedback, even if we don't execute
|
||||
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
|
||||
host_widgets.pending_service_transitions.insert(
|
||||
target.clone(),
|
||||
(command_type, current_status.unwrap_or_else(|| "unknown".to_string()), Instant::now())
|
||||
);
|
||||
}
|
||||
|
||||
should_execute
|
||||
}
|
||||
|
||||
/// Check for command timeouts and automatically clear them
|
||||
pub fn check_command_timeouts(&mut self) {
|
||||
let now = Instant::now();
|
||||
let mut hosts_to_clear = Vec::new();
|
||||
/// Clear pending transitions when real status updates arrive or timeout
|
||||
fn clear_completed_transitions(&mut self, hostname: &str, service_metrics: &[&Metric]) {
|
||||
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
|
||||
let mut completed_services = Vec::new();
|
||||
let now = Instant::now();
|
||||
|
||||
for (hostname, host_widgets) in &self.host_widgets {
|
||||
if let Some(CommandStatus::InProgress { command_type: _, start_time, .. }) = &host_widgets.command_status {
|
||||
let timeout_duration = Duration::from_secs(30); // 30 seconds for service commands
|
||||
// Check each pending transition to see if real status has changed or timed out
|
||||
for (service_name, (command_type, original_status, start_time)) in &host_widgets.pending_service_transitions {
|
||||
// Clear if too much time has passed (3 seconds for redundant commands)
|
||||
if now.duration_since(*start_time).as_secs() > 3 {
|
||||
completed_services.push(service_name.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
if now.duration_since(*start_time) > timeout_duration {
|
||||
hosts_to_clear.push(hostname.clone());
|
||||
// Look for status metric for this service
|
||||
for metric in service_metrics {
|
||||
if metric.name == format!("service_{}_status", service_name) {
|
||||
let new_status = metric.value.as_string();
|
||||
|
||||
// Check if status has changed from original (command completed)
|
||||
if &new_status != original_status {
|
||||
// Verify it changed in the expected direction
|
||||
let expected_change = match command_type {
|
||||
CommandType::ServiceStart => &new_status == "active",
|
||||
CommandType::ServiceStop => &new_status != "active",
|
||||
CommandType::ServiceRestart => true, // Any change indicates restart completed
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if expected_change {
|
||||
completed_services.push(service_name.clone());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear timed out commands
|
||||
for hostname in hosts_to_clear {
|
||||
if let Some(host_widgets) = self.host_widgets.get_mut(&hostname) {
|
||||
host_widgets.command_status = None;
|
||||
// Remove completed transitions
|
||||
for service_name in completed_services {
|
||||
host_widgets.pending_service_transitions.remove(&service_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -614,14 +627,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, command_status) = {
|
||||
let (scroll_offset, pending_transitions) = {
|
||||
let host_widgets = self.get_or_create_host_widgets(&hostname);
|
||||
(host_widgets.services_scroll_offset, host_widgets.command_status.clone())
|
||||
(host_widgets.services_scroll_offset, host_widgets.pending_service_transitions.clone())
|
||||
};
|
||||
let host_widgets = self.get_or_create_host_widgets(&hostname);
|
||||
host_widgets
|
||||
.services_widget
|
||||
.render_with_command_status(frame, content_chunks[1], is_focused, scroll_offset, command_status.as_ref()); // Services takes full right side
|
||||
.render_with_transitions(frame, content_chunks[1], is_focused, scroll_offset, &pending_transitions); // Services takes full right side
|
||||
}
|
||||
|
||||
// Render statusbar at the bottom
|
||||
|
||||
@@ -9,7 +9,7 @@ use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::{Components, StatusIcons, Theme, Typography};
|
||||
use crate::ui::{CommandStatus, CommandType};
|
||||
use crate::ui::CommandType;
|
||||
use ratatui::style::Style;
|
||||
|
||||
/// Services widget displaying hierarchical systemd service statuses
|
||||
@@ -128,25 +128,18 @@ impl ServicesWidget {
|
||||
)
|
||||
}
|
||||
|
||||
/// Get status icon for service, considering command status for visual feedback
|
||||
fn get_service_icon_and_status(&self, service_name: &str, info: &ServiceInfo, command_status: Option<&CommandStatus>) -> (String, String, ratatui::prelude::Color) {
|
||||
// Check if this service is currently being operated on
|
||||
if let Some(status) = command_status {
|
||||
match status {
|
||||
CommandStatus::InProgress { command_type, target, .. } => {
|
||||
if target == service_name {
|
||||
// Only show special icons for service commands
|
||||
if let Some((icon, status_text)) = match command_type {
|
||||
CommandType::ServiceRestart => Some(("↻", "restarting")),
|
||||
CommandType::ServiceStart => Some(("↑", "starting")),
|
||||
CommandType::ServiceStop => Some(("↓", "stopping")),
|
||||
_ => None, // Don't handle non-service commands here
|
||||
} {
|
||||
return (icon.to_string(), status_text.to_string(), Theme::highlight());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Get status icon for service, considering pending transitions for visual feedback
|
||||
fn get_service_icon_and_status(&self, service_name: &str, info: &ServiceInfo, pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>) -> (String, String, ratatui::prelude::Color) {
|
||||
// Check if this service has a pending transition
|
||||
if let Some((command_type, _original_status, _start_time)) = pending_transitions.get(service_name) {
|
||||
// Show transitional icons for pending commands
|
||||
let (icon, status_text) = match command_type {
|
||||
CommandType::ServiceRestart => ("↻", "restarting"),
|
||||
CommandType::ServiceStart => ("↑", "starting"),
|
||||
CommandType::ServiceStop => ("↓", "stopping"),
|
||||
_ => return (StatusIcons::get_icon(info.widget_status).to_string(), info.status.clone(), Theme::status_color(info.widget_status)), // Not a service command
|
||||
};
|
||||
return (icon.to_string(), status_text.to_string(), Theme::highlight());
|
||||
}
|
||||
|
||||
// Normal status display
|
||||
@@ -163,13 +156,13 @@ impl ServicesWidget {
|
||||
}
|
||||
|
||||
|
||||
/// Create spans for sub-service with icon next to name, considering command status
|
||||
fn create_sub_service_spans_with_status(
|
||||
/// Create spans for sub-service with icon next to name, considering pending transitions
|
||||
fn create_sub_service_spans_with_transitions(
|
||||
&self,
|
||||
name: &str,
|
||||
info: &ServiceInfo,
|
||||
is_last: bool,
|
||||
command_status: Option<&CommandStatus>,
|
||||
pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>,
|
||||
) -> Vec<ratatui::text::Span<'static>> {
|
||||
// Truncate long sub-service names to fit layout (accounting for indentation)
|
||||
let short_name = if name.len() > 18 {
|
||||
@@ -178,11 +171,11 @@ impl ServicesWidget {
|
||||
name.to_string()
|
||||
};
|
||||
|
||||
// Get status icon and text, considering command status
|
||||
let (icon, mut status_str, status_color) = self.get_service_icon_and_status(name, info, command_status);
|
||||
// Get status icon and text, considering pending transitions
|
||||
let (icon, mut status_str, status_color) = self.get_service_icon_and_status(name, info, pending_transitions);
|
||||
|
||||
// For sub-services, prefer latency if available (unless command is in progress)
|
||||
if command_status.is_none() {
|
||||
// For sub-services, prefer latency if available (unless transition is pending)
|
||||
if !pending_transitions.contains_key(name) {
|
||||
if let Some(latency) = info.latency_ms {
|
||||
status_str = if latency < 0.0 {
|
||||
"timeout".to_string()
|
||||
@@ -240,13 +233,14 @@ impl ServicesWidget {
|
||||
/// 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 display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>, String)> = 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));
|
||||
let parent_line = self.format_parent_service_line(parent_name, parent_info);
|
||||
display_lines.push((parent_line, parent_info.widget_status, false, None, parent_name.clone()));
|
||||
|
||||
if let Some(sub_list) = self.sub_services.get(parent_name) {
|
||||
let mut sorted_subs = sub_list.clone();
|
||||
@@ -254,17 +248,19 @@ impl ServicesWidget {
|
||||
|
||||
for (i, (sub_name, sub_info)) in sorted_subs.iter().enumerate() {
|
||||
let is_last_sub = i == sorted_subs.len() - 1;
|
||||
let full_sub_name = format!("{}_{}", parent_name, sub_name);
|
||||
display_lines.push((
|
||||
format!("{}_{}", parent_name, sub_name), // Use parent_sub format for sub-services
|
||||
sub_name.clone(),
|
||||
sub_info.widget_status,
|
||||
true,
|
||||
Some((sub_info.clone(), is_last_sub)),
|
||||
full_sub_name,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
display_lines.get(self.selected_index).map(|(name, _, _, _)| name.clone())
|
||||
display_lines.get(self.selected_index).map(|(_, _, _, _, raw_name)| raw_name.clone())
|
||||
}
|
||||
|
||||
/// Get total count of selectable services (parent services only, not sub-services)
|
||||
@@ -446,8 +442,8 @@ impl Widget for ServicesWidget {
|
||||
|
||||
impl ServicesWidget {
|
||||
|
||||
/// Render with focus, scroll, and command status for visual feedback
|
||||
pub fn render_with_command_status(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, command_status: Option<&CommandStatus>) {
|
||||
/// Render with focus, scroll, and pending transitions for visual feedback
|
||||
pub fn render_with_transitions(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>) {
|
||||
let services_block = if is_focused {
|
||||
Components::focused_widget_block("services")
|
||||
} else {
|
||||
@@ -476,14 +472,14 @@ impl ServicesWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the existing render logic but with command status
|
||||
self.render_services_with_status(frame, content_chunks[1], is_focused, scroll_offset, command_status);
|
||||
// Use the existing render logic but with pending transitions
|
||||
self.render_services_with_transitions(frame, content_chunks[1], is_focused, scroll_offset, pending_transitions);
|
||||
}
|
||||
|
||||
/// Render services list with command status awareness
|
||||
fn render_services_with_status(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, command_status: Option<&CommandStatus>) {
|
||||
// Build hierarchical service list for display (same as existing logic)
|
||||
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new();
|
||||
/// Render services list with pending transitions awareness
|
||||
fn render_services_with_transitions(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, pending_transitions: &HashMap<String, (CommandType, String, std::time::Instant)>) {
|
||||
// Build hierarchical service list for display - include raw service name for pending transition lookups
|
||||
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>, String)> = Vec::new(); // Added raw service name
|
||||
|
||||
// Sort parent services alphabetically for consistent order
|
||||
let mut parent_services: Vec<_> = self.parent_services.iter().collect();
|
||||
@@ -492,7 +488,7 @@ impl ServicesWidget {
|
||||
for (parent_name, parent_info) in parent_services {
|
||||
// Add parent service line
|
||||
let parent_line = self.format_parent_service_line(parent_name, parent_info);
|
||||
display_lines.push((parent_line, parent_info.widget_status, false, None)); // false = not sub-service
|
||||
display_lines.push((parent_line, parent_info.widget_status, false, None, parent_name.clone())); // Include raw name
|
||||
|
||||
// Add sub-services for this parent (if any)
|
||||
if let Some(sub_list) = self.sub_services.get(parent_name) {
|
||||
@@ -502,12 +498,14 @@ impl ServicesWidget {
|
||||
|
||||
for (i, (sub_name, sub_info)) in sorted_subs.iter().enumerate() {
|
||||
let is_last_sub = i == sorted_subs.len() - 1;
|
||||
let full_sub_name = format!("{}_{}", parent_name, sub_name);
|
||||
// Store sub-service info for custom span rendering
|
||||
display_lines.push((
|
||||
sub_name.clone(),
|
||||
sub_info.widget_status,
|
||||
true,
|
||||
Some((sub_info.clone(), is_last_sub)),
|
||||
full_sub_name, // Raw service name for pending transition lookup
|
||||
)); // true = sub-service, with is_last info
|
||||
}
|
||||
}
|
||||
@@ -540,7 +538,7 @@ impl ServicesWidget {
|
||||
.constraints(vec![Constraint::Length(1); lines_to_show])
|
||||
.split(area);
|
||||
|
||||
for (i, (line_text, line_status, is_sub, sub_info)) in visible_lines.iter().enumerate()
|
||||
for (i, (line_text, line_status, is_sub, sub_info, raw_service_name)) in visible_lines.iter().enumerate()
|
||||
{
|
||||
let actual_index = effective_scroll + i; // Real index in the full list
|
||||
|
||||
@@ -554,42 +552,34 @@ impl ServicesWidget {
|
||||
};
|
||||
|
||||
let mut spans = if *is_sub && sub_info.is_some() {
|
||||
// Use custom sub-service span creation WITH command status
|
||||
// Use custom sub-service span creation WITH pending transitions
|
||||
let (service_info, is_last) = sub_info.as_ref().unwrap();
|
||||
self.create_sub_service_spans_with_status(line_text, service_info, *is_last, command_status)
|
||||
self.create_sub_service_spans_with_transitions(line_text, service_info, *is_last, pending_transitions)
|
||||
} else {
|
||||
// Parent services - check if this parent service has a command in progress
|
||||
let service_spans = if let Some(status) = command_status {
|
||||
match status {
|
||||
CommandStatus::InProgress { target, .. } => {
|
||||
if target == line_text {
|
||||
// Create spans with progress status
|
||||
let (icon, status_text, status_color) = self.get_service_icon_and_status(line_text, &ServiceInfo {
|
||||
status: "".to_string(),
|
||||
memory_mb: None,
|
||||
disk_gb: None,
|
||||
latency_ms: None,
|
||||
widget_status: *line_status
|
||||
}, command_status);
|
||||
vec![
|
||||
ratatui::text::Span::styled(format!("{} ", icon), Style::default().fg(status_color)),
|
||||
ratatui::text::Span::styled(line_text.clone(), Style::default().fg(Theme::primary_text())),
|
||||
ratatui::text::Span::styled(format!(" {}", status_text), Style::default().fg(status_color)),
|
||||
]
|
||||
} else {
|
||||
StatusIcons::create_status_spans(*line_status, line_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parent services - check if this parent service has a pending transition using RAW service name
|
||||
if pending_transitions.contains_key(raw_service_name) {
|
||||
// Create spans with transitional status
|
||||
let (icon, status_text, status_color) = self.get_service_icon_and_status(raw_service_name, &ServiceInfo {
|
||||
status: "".to_string(),
|
||||
memory_mb: None,
|
||||
disk_gb: None,
|
||||
latency_ms: None,
|
||||
widget_status: *line_status
|
||||
}, pending_transitions);
|
||||
vec![
|
||||
ratatui::text::Span::styled(format!("{} ", icon), Style::default().fg(status_color)),
|
||||
ratatui::text::Span::styled(line_text.clone(), Style::default().fg(Theme::primary_text())),
|
||||
ratatui::text::Span::styled(format!(" {}", status_text), Style::default().fg(status_color)),
|
||||
]
|
||||
} else {
|
||||
StatusIcons::create_status_spans(*line_status, line_text)
|
||||
};
|
||||
service_spans
|
||||
}
|
||||
};
|
||||
|
||||
// Apply selection highlighting to parent services only, preserving status icon color
|
||||
// Only show selection when Services panel is focused
|
||||
if is_selected && !*is_sub && is_focused {
|
||||
// IMPORTANT: Don't override transitional icons that show pending commands
|
||||
if is_selected && !*is_sub && is_focused && !pending_transitions.contains_key(raw_service_name) {
|
||||
for (i, span) in spans.iter_mut().enumerate() {
|
||||
if i == 0 {
|
||||
// First span is the status icon - preserve its color
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-dashboard-shared"
|
||||
version = "0.1.23"
|
||||
version = "0.1.28"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
Reference in New Issue
Block a user