1use super::footer::NodesToStart;
10use super::header::SelectedMenuItem;
11use super::popup::manage_nodes::GB;
12use super::utils::centered_rect_fixed;
13use super::{footer::Footer, header::Header, popup::manage_nodes::GB_PER_NODE, Component, Frame};
14use crate::action::OptionsActions;
15use crate::components::popup::manage_nodes::MAX_NODE_COUNT;
16use crate::components::popup::port_range::PORT_ALLOCATION;
17use crate::components::utils::open_logs;
18use crate::config::get_launchpad_nodes_data_dir_path;
19use crate::connection_mode::{ConnectionMode, NodeConnectionMode};
20use crate::error::ErrorPopup;
21use crate::node_mgmt::{
22 MaintainNodesArgs, NodeManagement, NodeManagementTask, UpgradeNodesArgs, FIXED_INTERVAL,
23 NODES_ALL,
24};
25use crate::node_mgmt::{PORT_MAX, PORT_MIN};
26use crate::style::{clear_area, COOL_GREY, INDIGO, SIZZLING_RED};
27use crate::system::{get_available_space_b, get_drive_name};
28use crate::tui::Event;
29use crate::upnp::UpnpSupport;
30use crate::{
31 action::{Action, StatusActions},
32 config::Config,
33 mode::{InputMode, Scene},
34 node_stats::NodeStats,
35 style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE},
36};
37use ant_bootstrap::InitialPeersConfig;
38use ant_node_manager::add_services::config::PortRange;
39use ant_node_manager::config::get_node_registry_path;
40use ant_service_management::{
41 control::ServiceController, NodeRegistry, NodeServiceData, ServiceStatus,
42};
43use color_eyre::eyre::{Ok, OptionExt, Result};
44use crossterm::event::KeyEvent;
45use ratatui::text::Span;
46use ratatui::{prelude::*, widgets::*};
47use std::fmt;
48use std::{
49 path::PathBuf,
50 time::{Duration, Instant},
51 vec,
52};
53use throbber_widgets_tui::{self, Throbber, ThrobberState};
54use tokio::sync::mpsc::UnboundedSender;
55
56pub const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
57const MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION: usize = 3;
59
60const NODE_WIDTH: usize = 10;
62const VERSION_WIDTH: usize = 7;
63const ATTOS_WIDTH: usize = 5;
64const MEMORY_WIDTH: usize = 7;
65const MBPS_WIDTH: usize = 13;
66const RECORDS_WIDTH: usize = 4;
67const PEERS_WIDTH: usize = 5;
68const CONNS_WIDTH: usize = 5;
69const MODE_WIDTH: usize = 7;
70const STATUS_WIDTH: usize = 8;
71const FAILURE_WIDTH: usize = 64;
72const SPINNER_WIDTH: usize = 1;
73
74#[derive(Clone)]
75pub struct Status<'a> {
76 active: bool,
78 action_sender: Option<UnboundedSender<Action>>,
79 config: Config,
80 is_nat_status_determined: bool,
82 error_while_running_nat_detection: usize,
83 node_stats: NodeStats,
85 node_stats_last_update: Instant,
86 node_services: Vec<NodeServiceData>,
88 items: Option<StatefulTable<NodeItem<'a>>>,
89 network_id: Option<u8>,
91 node_management: NodeManagement,
93 nodes_to_start: usize,
95 rewards_address: String,
97 init_peers_config: InitialPeersConfig,
99 antnode_path: Option<PathBuf>,
101 data_dir_path: PathBuf,
103 connection_mode: ConnectionMode,
105 upnp_support: UpnpSupport,
107 port_from: Option<u32>,
109 port_to: Option<u32>,
111 storage_mountpoint: PathBuf,
112 available_disk_space_gb: usize,
113 error_popup: Option<ErrorPopup>,
114}
115
116pub struct StatusConfig {
117 pub allocated_disk_space: usize,
118 pub antnode_path: Option<PathBuf>,
119 pub connection_mode: ConnectionMode,
120 pub upnp_support: UpnpSupport,
121 pub data_dir_path: PathBuf,
122 pub network_id: Option<u8>,
123 pub init_peers_config: InitialPeersConfig,
124 pub port_from: Option<u32>,
125 pub port_to: Option<u32>,
126 pub storage_mountpoint: PathBuf,
127 pub rewards_address: String,
128}
129
130impl Status<'_> {
131 pub async fn new(config: StatusConfig) -> Result<Self> {
132 let mut status = Self {
133 init_peers_config: config.init_peers_config,
134 action_sender: Default::default(),
135 config: Default::default(),
136 active: true,
137 is_nat_status_determined: false,
138 error_while_running_nat_detection: 0,
139 network_id: config.network_id,
140 node_stats: NodeStats::default(),
141 node_stats_last_update: Instant::now(),
142 node_services: Default::default(),
143 node_management: NodeManagement::new()?,
144 items: None,
145 nodes_to_start: config.allocated_disk_space,
146 rewards_address: config.rewards_address,
147 antnode_path: config.antnode_path,
148 data_dir_path: config.data_dir_path,
149 connection_mode: config.connection_mode,
150 upnp_support: config.upnp_support,
151 port_from: config.port_from,
152 port_to: config.port_to,
153 error_popup: None,
154 storage_mountpoint: config.storage_mountpoint.clone(),
155 available_disk_space_gb: get_available_space_b(&config.storage_mountpoint)? / GB,
156 };
157
158 let now = Instant::now();
160 debug!("Refreshing node registry states on startup");
161 let mut node_registry = NodeRegistry::load(&get_node_registry_path()?)?;
162 ant_node_manager::refresh_node_registry(
163 &mut node_registry,
164 &ServiceController {},
165 false,
166 true,
167 false,
168 )
169 .await?;
170 node_registry.save()?;
171 debug!("Node registry states refreshed in {:?}", now.elapsed());
172 status.load_node_registry_and_update_states()?;
173
174 Ok(status)
175 }
176
177 fn set_lock(&mut self, service_name: &str, locked: bool) {
178 if let Some(ref mut items) = self.items {
179 for item in &mut items.items {
180 if item.name == *service_name {
181 item.locked = locked;
182 }
183 }
184 }
185 }
186
187 fn _lock_service(&mut self, service_name: &str) {
189 self.set_lock(service_name, true);
190 }
191
192 fn unlock_service(&mut self, service_name: &str) {
193 self.set_lock(service_name, false);
194 }
195
196 fn update_item(&mut self, service_name: String, status: NodeStatus) -> Result<()> {
203 if let Some(ref mut items) = self.items {
204 for item in &mut items.items {
205 if item.name == service_name {
206 item.status = status;
207 }
208 }
209 }
210 Ok(())
211 }
212
213 fn update_node_items(&mut self, new_status: Option<NodeStatus>) -> Result<()> {
214 if let Some(ref mut items) = self.items {
216 for node_item in self.node_services.iter() {
217 if let Some(item) = items
219 .items
220 .iter_mut()
221 .find(|i| i.name == node_item.service_name)
222 {
223 if let Some(status) = new_status {
224 item.status = status;
225 } else if item.status == NodeStatus::Updating
226 || item.status == NodeStatus::Starting
227 {
228 item.spinner_state.calc_next();
229 } else if new_status != Some(NodeStatus::Updating) {
230 item.status = match node_item.status {
232 ServiceStatus::Running => {
233 item.spinner_state.calc_next();
234 NodeStatus::Running
235 }
236 ServiceStatus::Stopped => NodeStatus::Stopped,
237 ServiceStatus::Added => NodeStatus::Added,
238 ServiceStatus::Removed => NodeStatus::Removed,
239 };
240 }
241
242 item.peers = match node_item.connected_peers {
244 Some(ref peers) => peers.len(),
245 None => 0,
246 };
247
248 if let Some(stats) = self
250 .node_stats
251 .individual_stats
252 .iter()
253 .find(|s| s.service_name == node_item.service_name)
254 {
255 item.attos = stats.rewards_wallet_balance;
256 item.memory = stats.memory_usage_mb;
257 item.mbps = format!(
258 "↓{:0>5.0} ↑{:0>5.0}",
259 (stats.bandwidth_inbound_rate * 8) as f64 / 1_000_000.0,
260 (stats.bandwidth_outbound_rate * 8) as f64 / 1_000_000.0,
261 );
262 item.records = stats.max_records;
263 item.connections = stats.connections;
264 }
265 } else {
266 let new_item = NodeItem {
268 name: node_item.service_name.clone(),
269 version: node_item.version.to_string(),
270 attos: 0,
271 memory: 0,
272 mbps: "-".to_string(),
273 records: 0,
274 peers: 0,
275 connections: 0,
276 mode: NodeConnectionMode::from(node_item),
277 locked: false,
278 status: NodeStatus::Added, failure: node_item.get_critical_failure(),
280 spinner: Throbber::default(),
281 spinner_state: ThrobberState::default(),
282 };
283 items.items.push(new_item);
284 }
285 }
286 } else {
287 let node_items: Vec<NodeItem> = self
289 .node_services
290 .iter()
291 .filter_map(|node_item| {
292 if node_item.status == ServiceStatus::Removed {
293 return None;
294 }
295 let status = match node_item.status {
297 ServiceStatus::Running => NodeStatus::Running,
298 ServiceStatus::Stopped => NodeStatus::Stopped,
299 ServiceStatus::Added => NodeStatus::Added,
300 ServiceStatus::Removed => NodeStatus::Removed,
301 };
302
303 Some(NodeItem {
305 name: node_item.service_name.clone().to_string(),
306 version: node_item.version.to_string(),
307 attos: 0,
308 memory: 0,
309 mbps: "-".to_string(),
310 records: 0,
311 peers: 0,
312 connections: 0,
313 locked: false,
314 mode: NodeConnectionMode::from(node_item),
315 status,
316 failure: node_item.get_critical_failure(),
317 spinner: Throbber::default(),
318 spinner_state: ThrobberState::default(),
319 })
320 })
321 .collect();
322 self.items = Some(StatefulTable::with_items(node_items));
323 }
324 Ok(())
325 }
326
327 fn clear_node_items(&mut self) {
328 debug!("Cleaning items on Status page");
329 if let Some(items) = self.items.as_mut() {
330 items.items.clear();
331 debug!("Cleared the items on status page");
332 }
333 }
334
335 fn try_update_node_stats(&mut self, force_update: bool) -> Result<()> {
338 if self.node_stats_last_update.elapsed() > NODE_STAT_UPDATE_INTERVAL || force_update {
339 self.node_stats_last_update = Instant::now();
340
341 NodeStats::fetch_all_node_stats(&self.node_services, self.get_actions_sender()?);
342 }
343 Ok(())
344 }
345 fn get_actions_sender(&self) -> Result<UnboundedSender<Action>> {
346 self.action_sender
347 .clone()
348 .ok_or_eyre("Action sender not registered")
349 }
350
351 fn load_node_registry_and_update_states(&mut self) -> Result<()> {
352 let node_registry = NodeRegistry::load(&get_node_registry_path()?)?;
353 self.is_nat_status_determined = node_registry.nat_status.is_some();
354 self.node_services = node_registry
355 .nodes
356 .into_iter()
357 .filter(|node| node.status != ServiceStatus::Removed)
358 .collect();
359 info!(
360 "Loaded node registry. Maintaining {:?} nodes.",
361 self.node_services.len()
362 );
363
364 Ok(())
365 }
366
367 fn should_we_run_nat_detection(&self) -> bool {
369 self.connection_mode == ConnectionMode::Automatic
370 && !self.is_nat_status_determined
371 && self.error_while_running_nat_detection < MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION
372 }
373
374 fn nodes_starting(&self) -> bool {
375 if let Some(items) = &self.items {
376 items
377 .items
378 .iter()
379 .any(|item| item.status == NodeStatus::Starting)
380 } else {
381 false
382 }
383 }
384
385 fn get_running_nodes(&self) -> Vec<String> {
386 self.node_services
387 .iter()
388 .filter_map(|node| {
389 if node.status == ServiceStatus::Running {
390 Some(node.service_name.clone())
391 } else {
392 None
393 }
394 })
395 .collect()
396 }
397
398 fn get_service_names_and_peer_ids(&self) -> (Vec<String>, Vec<String>) {
399 let mut service_names = Vec::new();
400 let mut peers_ids = Vec::new();
401
402 for node in &self.node_services {
403 if let Some(peer_id) = &node.peer_id {
405 service_names.push(node.service_name.clone());
406 peers_ids.push(peer_id.to_string().clone());
407 }
408 }
409
410 (service_names, peers_ids)
411 }
412}
413
414impl Component for Status<'_> {
415 fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
416 self.action_sender = Some(tx);
417
418 self.try_update_node_stats(true)?;
420
421 Ok(())
422 }
423
424 fn register_config_handler(&mut self, config: Config) -> Result<()> {
425 self.config = config;
426 Ok(())
427 }
428
429 fn handle_events(&mut self, event: Option<Event>) -> Result<Vec<Action>> {
430 let r = match event {
431 Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
432 _ => vec![],
433 };
434 Ok(r)
435 }
436
437 fn update(&mut self, action: Action) -> Result<Option<Action>> {
438 match action {
439 Action::Tick => {
440 self.try_update_node_stats(false)?;
441 let _ = self.update_node_items(None);
442 }
443 Action::SwitchScene(scene) => match scene {
444 Scene::Status | Scene::StatusRewardsAddressPopUp | Scene::RemoveNodePopUp => {
445 self.active = true;
446 return Ok(Some(Action::SwitchInputMode(InputMode::Navigation)));
448 }
449 Scene::ManageNodesPopUp { .. } => self.active = true,
450 _ => self.active = false,
451 },
452 Action::StoreNodesToStart(count) => {
453 self.nodes_to_start = count;
454 if self.nodes_to_start == 0 {
455 info!("Nodes to start set to 0. Sending command to stop all nodes.");
456 return Ok(Some(Action::StatusActions(StatusActions::StopNodes)));
457 } else {
458 info!("Nodes to start set to: {count}. Sending command to start nodes");
459 return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
460 }
461 }
462 Action::StoreRewardsAddress(rewards_address) => {
463 debug!("Storing rewards address: {rewards_address:?}");
464 let has_changed = self.rewards_address != rewards_address;
465 let we_have_nodes = !self.node_services.is_empty();
466
467 self.rewards_address = rewards_address;
468
469 if we_have_nodes && has_changed {
470 info!("Resetting antnode services because the Rewards Address was reset.");
471 let action_sender = self.get_actions_sender()?;
472 self.node_management
473 .send_task(NodeManagementTask::ResetNodes {
474 start_nodes_after_reset: false,
475 action_sender,
476 })?;
477 }
478 }
479 Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => {
480 info!("Resetting antnode services because the Storage Drive was changed.");
481 let action_sender = self.get_actions_sender()?;
482 self.node_management
483 .send_task(NodeManagementTask::ResetNodes {
484 start_nodes_after_reset: false,
485 action_sender,
486 })?;
487 self.data_dir_path =
488 get_launchpad_nodes_data_dir_path(&drive_mountpoint.to_path_buf(), false)?;
489 }
490 Action::StoreConnectionMode(connection_mode) => {
491 self.connection_mode = connection_mode;
492 info!("Resetting antnode services because the Connection Mode range was changed.");
493 let action_sender = self.get_actions_sender()?;
494 self.node_management
495 .send_task(NodeManagementTask::ResetNodes {
496 start_nodes_after_reset: false,
497 action_sender,
498 })?;
499 }
500 Action::StorePortRange(port_from, port_range) => {
501 self.port_from = Some(port_from);
502 self.port_to = Some(port_range);
503 info!("Resetting antnode services because the Port Range was changed.");
504 let action_sender = self.get_actions_sender()?;
505 self.node_management
506 .send_task(NodeManagementTask::ResetNodes {
507 start_nodes_after_reset: false,
508 action_sender,
509 })?;
510 }
511 Action::SetUpnpSupport(ref upnp_support) => {
512 debug!("Setting UPnP support: {upnp_support:?}");
513 self.upnp_support = upnp_support.clone();
514 }
515 Action::StatusActions(status_action) => match status_action {
516 StatusActions::NodesStatsObtained(stats) => {
517 self.node_stats = stats;
518 }
519 StatusActions::StartNodesCompleted { service_name } => {
520 if service_name == *NODES_ALL {
521 if let Some(items) = &self.items {
522 let items_clone = items.clone();
523 for item in &items_clone.items {
524 self.unlock_service(item.name.as_str());
525 self.update_item(item.name.clone(), NodeStatus::Running)?;
526 }
527 }
528 } else {
529 self.unlock_service(service_name.as_str());
530 self.update_item(service_name, NodeStatus::Running)?;
531 }
532 self.load_node_registry_and_update_states()?;
533 }
534 StatusActions::StopNodesCompleted { service_name } => {
535 self.unlock_service(service_name.as_str());
536 self.update_item(service_name, NodeStatus::Stopped)?;
537 self.load_node_registry_and_update_states()?;
538 }
539 StatusActions::UpdateNodesCompleted => {
540 if let Some(items) = &self.items {
541 let items_clone = items.clone();
542 for item in &items_clone.items {
543 self.unlock_service(item.name.as_str());
544 }
545 }
546 self.clear_node_items();
547 self.load_node_registry_and_update_states()?;
548 let _ = self.update_node_items(None);
549 debug!("Update nodes completed");
550 }
551 StatusActions::ResetNodesCompleted { trigger_start_node } => {
552 if let Some(items) = &self.items {
553 let items_clone = items.clone();
554 for item in &items_clone.items {
555 self.unlock_service(item.name.as_str());
556 }
557 }
558 self.load_node_registry_and_update_states()?;
559 self.clear_node_items();
560
561 if trigger_start_node {
562 debug!("Reset nodes completed. Triggering start nodes.");
563 return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
564 }
565 debug!("Reset nodes completed");
566 }
567 StatusActions::AddNodesCompleted { service_name } => {
568 self.unlock_service(service_name.as_str());
569 self.update_item(service_name.clone(), NodeStatus::Stopped)?;
570 self.load_node_registry_and_update_states()?;
571 debug!("Adding {:?} completed", service_name.clone());
572 }
573 StatusActions::RemoveNodesCompleted { service_name } => {
574 self.unlock_service(service_name.as_str());
575 self.update_item(service_name, NodeStatus::Removed)?;
576 self.load_node_registry_and_update_states()?;
577 let _ = self.update_node_items(None);
578 debug!("Removing nodes completed");
579 }
580 StatusActions::SuccessfullyDetectedNatStatus => {
581 debug!(
582 "Successfully detected nat status, is_nat_status_determined set to true"
583 );
584 self.is_nat_status_determined = true;
585 }
586 StatusActions::ErrorWhileRunningNatDetection => {
587 self.error_while_running_nat_detection += 1;
588 debug!(
589 "Error while running nat detection. Error count: {}",
590 self.error_while_running_nat_detection
591 );
592 }
593 StatusActions::ErrorLoadingNodeRegistry { raw_error }
594 | StatusActions::ErrorGettingNodeRegistryPath { raw_error } => {
595 self.error_popup = Some(ErrorPopup::new(
596 "Error".to_string(),
597 "Error getting node registry path".to_string(),
598 raw_error,
599 ));
600 if let Some(error_popup) = &mut self.error_popup {
601 error_popup.show();
602 }
603 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
605 }
606 StatusActions::ErrorScalingUpNodes { raw_error } => {
607 self.error_popup = Some(ErrorPopup::new(
608 "Error".to_string(),
609 "Error adding new nodes".to_string(),
610 raw_error,
611 ));
612 if let Some(error_popup) = &mut self.error_popup {
613 error_popup.show();
614 }
615 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
617 }
618 StatusActions::ErrorStoppingNodes {
619 services,
620 raw_error,
621 } => {
622 for service_name in services {
623 self.unlock_service(service_name.as_str());
624 }
625 self.error_popup = Some(ErrorPopup::new(
626 "Error".to_string(),
627 "Error stopping nodes".to_string(),
628 raw_error,
629 ));
630 if let Some(error_popup) = &mut self.error_popup {
631 error_popup.show();
632 }
633 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
635 }
636 StatusActions::ErrorUpdatingNodes { raw_error } => {
637 if let Some(items) = &self.items {
638 let items_clone = items.clone();
639 for item in &items_clone.items {
640 self.unlock_service(item.name.as_str());
641 }
642 }
643 self.error_popup = Some(ErrorPopup::new(
644 "Error".to_string(),
645 "Error upgrading nodes".to_string(),
646 raw_error,
647 ));
648 if let Some(error_popup) = &mut self.error_popup {
649 error_popup.show();
650 }
651 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
653 }
654 StatusActions::ErrorResettingNodes { raw_error } => {
655 self.error_popup = Some(ErrorPopup::new(
656 "Error".to_string(),
657 "Error resetting nodes".to_string(),
658 raw_error,
659 ));
660 if let Some(error_popup) = &mut self.error_popup {
661 error_popup.show();
662 }
663 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
665 }
666 StatusActions::ErrorAddingNodes { raw_error } => {
667 self.error_popup = Some(ErrorPopup::new(
668 "Error".to_string(),
669 "Error adding node".to_string(),
670 raw_error,
671 ));
672 if let Some(error_popup) = &mut self.error_popup {
673 error_popup.show();
674 }
675 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
677 }
678 StatusActions::ErrorRemovingNodes {
679 services,
680 raw_error,
681 } => {
682 for service_name in services {
683 self.unlock_service(service_name.as_str());
684 }
685 self.error_popup = Some(ErrorPopup::new(
686 "Error".to_string(),
687 "Error removing node".to_string(),
688 raw_error,
689 ));
690 if let Some(error_popup) = &mut self.error_popup {
691 error_popup.show();
692 }
693 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
695 }
696 StatusActions::ErrorStartingNodes {
697 services,
698 raw_error,
699 } => {
700 for service_name in services {
701 self.unlock_service(service_name.as_str());
702 }
703 self.error_popup = Some(ErrorPopup::new(
704 "Error".to_string(),
705 "Error starting node. Please try again.".to_string(),
706 raw_error,
707 ));
708 if let Some(error_popup) = &mut self.error_popup {
709 error_popup.show();
710 }
711 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
713 }
714 StatusActions::TriggerManageNodes => {
715 let mut amount_of_nodes = 0;
716 if let Some(items) = &mut self.items {
717 amount_of_nodes = items.items.len();
718 }
719
720 return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp {
721 amount_of_nodes,
722 })));
723 }
724 StatusActions::TriggerRemoveNode => {
725 if let Some(_node) = self.items.as_ref().and_then(|items| items.selected_item())
726 {
727 return Ok(Some(Action::SwitchScene(Scene::RemoveNodePopUp)));
728 } else {
729 debug!("No items to be removed");
730 return Ok(None);
731 }
732 }
733 StatusActions::PreviousTableItem => {
734 if let Some(items) = &mut self.items {
735 items.previous();
736 }
737 }
738 StatusActions::NextTableItem => {
739 if let Some(items) = &mut self.items {
740 items.next();
741 }
742 }
743 StatusActions::StartStopNode => {
744 debug!("Start/Stop node");
745
746 if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
748 {
749 let node_index = self
750 .items
751 .as_ref()
752 .unwrap()
753 .items
754 .iter()
755 .position(|item| item.name == node.name)
756 .unwrap();
757 let action_sender = self.get_actions_sender()?;
758 let node = &mut self.items.as_mut().unwrap().items[node_index];
759
760 if node.status == NodeStatus::Removed {
761 debug!("Node is removed. Cannot be started.");
762 return Ok(None);
763 }
764
765 if node.locked {
766 debug!("Node still performing operation");
767 return Ok(None);
768 }
769 node.locked = true; let service_name = vec![node.name.clone()];
772
773 match node.status {
774 NodeStatus::Stopped | NodeStatus::Added => {
775 debug!("Starting Node {:?}", node.name);
776 self.node_management
777 .send_task(NodeManagementTask::StartNode {
778 services: service_name,
779 action_sender,
780 })?;
781 node.status = NodeStatus::Starting;
782 }
783 NodeStatus::Running => {
784 debug!("Stopping Node {:?}", node.name);
785 self.node_management
786 .send_task(NodeManagementTask::StopNodes {
787 services: service_name,
788 action_sender,
789 })?;
790 }
791 _ => {
792 debug!("Cannot Start/Stop node. Node status is {:?}", node.status);
793 }
794 }
795 } else {
796 debug!("Got action to Start/Stop node but no node was selected.");
797 return Ok(None);
798 }
799 }
800 StatusActions::StartNodes => {
801 debug!("Got action to start nodes");
802
803 if self.rewards_address.is_empty() {
804 info!("Rewards address is not set. Ask for input.");
805 return Ok(Some(Action::StatusActions(
806 StatusActions::TriggerRewardsAddress,
807 )));
808 }
809
810 if self.nodes_to_start == 0 {
811 info!("Nodes to start not set. Ask for input.");
812 return Ok(Some(Action::StatusActions(
813 StatusActions::TriggerManageNodes,
814 )));
815 }
816
817 if let Some(ref mut items) = self.items {
819 for item in &mut items.items {
820 if item.status == NodeStatus::Added
821 || item.status == NodeStatus::Stopped
822 {
823 item.status = NodeStatus::Starting;
824 item.locked = true;
825 }
826 }
827 }
828
829 let port_range = PortRange::Range(
830 self.port_from.unwrap_or(PORT_MIN) as u16,
831 self.port_to.unwrap_or(PORT_MAX) as u16,
832 );
833
834 let action_sender = self.get_actions_sender()?;
835
836 let maintain_nodes_args = MaintainNodesArgs {
837 action_sender: action_sender.clone(),
838 antnode_path: self.antnode_path.clone(),
839 connection_mode: self.connection_mode,
840 count: self.nodes_to_start as u16,
841 data_dir_path: Some(self.data_dir_path.clone()),
842 network_id: self.network_id,
843 owner: self.rewards_address.clone(),
844 init_peers_config: self.init_peers_config.clone(),
845 port_range: Some(port_range),
846 rewards_address: self.rewards_address.clone(),
847 run_nat_detection: self.should_we_run_nat_detection(),
848 };
849
850 debug!("Calling maintain_n_running_nodes");
851
852 self.node_management
853 .send_task(NodeManagementTask::MaintainNodes {
854 args: maintain_nodes_args,
855 })?;
856 }
857 StatusActions::StopNodes => {
858 debug!("Got action to stop nodes");
859
860 let running_nodes = self.get_running_nodes();
861 let action_sender = self.get_actions_sender()?;
862 info!("Stopping node service: {running_nodes:?}");
863
864 self.node_management
865 .send_task(NodeManagementTask::StopNodes {
866 services: running_nodes,
867 action_sender,
868 })?;
869 }
870 StatusActions::AddNode => {
871 debug!("Got action to Add node");
872
873 if GB_PER_NODE > self.available_disk_space_gb {
875 self.error_popup = Some(ErrorPopup::new(
876 "Cannot Add Node".to_string(),
877 format!("\nEach Node requires {}GB of available space.", GB_PER_NODE),
878 format!("{} has only {}GB remaining.\n\nYou can free up some space or change to different drive in the options.", get_drive_name(&self.storage_mountpoint)?, self.available_disk_space_gb),
879 ));
880 if let Some(error_popup) = &mut self.error_popup {
881 error_popup.show();
882 }
883 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
885 }
886
887 let amount_of_nodes = if let Some(ref items) = self.items {
889 items.items.len()
890 } else {
891 0
892 };
893
894 if amount_of_nodes + 1 > MAX_NODE_COUNT {
895 self.error_popup = Some(ErrorPopup::new(
896 "Cannot Add Node".to_string(),
897 format!(
898 "There are not enough ports available in your\ncustom port range to start another node ({}).",
899 MAX_NODE_COUNT
900 ),
901 "\nVisit autonomi.com/support/port-error for help".to_string(),
902 ));
903 if let Some(error_popup) = &mut self.error_popup {
904 error_popup.show();
905 }
906 return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
908 }
909
910 if self.rewards_address.is_empty() {
911 info!("Rewards address is not set. Ask for input.");
912 return Ok(Some(Action::StatusActions(
913 StatusActions::TriggerRewardsAddress,
914 )));
915 }
916
917 if self.nodes_to_start == 0 {
918 info!("Nodes to start not set. Ask for input.");
919 return Ok(Some(Action::StatusActions(
920 StatusActions::TriggerManageNodes,
921 )));
922 }
923
924 let port_range = PortRange::Range(
925 self.port_from.unwrap_or(PORT_MIN) as u16,
926 self.port_to.unwrap_or(PORT_MAX) as u16,
927 );
928
929 let action_sender = self.get_actions_sender()?;
930
931 let add_node_args = MaintainNodesArgs {
932 action_sender: action_sender.clone(),
933 antnode_path: self.antnode_path.clone(),
934 connection_mode: self.connection_mode,
935 count: 1,
936 data_dir_path: Some(self.data_dir_path.clone()),
937 network_id: self.network_id,
938 owner: self.rewards_address.clone(),
939 init_peers_config: self.init_peers_config.clone(),
940 port_range: Some(port_range),
941 rewards_address: self.rewards_address.clone(),
942 run_nat_detection: self.should_we_run_nat_detection(),
943 };
944
945 self.node_management
946 .send_task(NodeManagementTask::AddNode {
947 args: add_node_args,
948 })?;
949 }
950 StatusActions::RemoveNodes => {
951 debug!("Got action to remove node");
952 if self
954 .items
955 .as_ref()
956 .and_then(|items| items.selected_item())
957 .is_none()
958 {
959 debug!("Got action to Start/Stop node but no node was selected.");
960 return Ok(None);
961 }
962
963 let node_index =
964 self.items
965 .as_ref()
966 .and_then(|items| {
967 items.items.iter().position(|item| {
968 item.name == items.selected_item().unwrap().name
969 })
970 })
971 .unwrap();
972
973 let action_sender = self.get_actions_sender()?;
974
975 let node = &mut self.items.as_mut().unwrap().items[node_index];
976
977 if node.locked {
978 debug!("Node still performing operation");
979 return Ok(None);
980 } else {
981 node.locked = true;
983 }
984
985 let service_name = vec![node.name.clone()];
986
987 self.node_management
989 .send_task(NodeManagementTask::RemoveNodes {
990 services: service_name,
991 action_sender,
992 })?;
993 }
994 StatusActions::TriggerRewardsAddress => {
995 if self.rewards_address.is_empty() {
996 return Ok(Some(Action::SwitchScene(Scene::StatusRewardsAddressPopUp)));
997 } else {
998 return Ok(None);
999 }
1000 }
1001 StatusActions::TriggerNodeLogs => {
1002 if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
1003 {
1004 debug!("Got action to open node logs {:?}", node.name);
1005 open_logs(Some(node.name.clone()))?;
1006 } else {
1007 debug!("Got action to open node logs but no node was selected.");
1008 }
1009 }
1010 },
1011 Action::OptionsActions(OptionsActions::UpdateNodes) => {
1012 debug!("Got action to Update Nodes");
1013 self.load_node_registry_and_update_states()?;
1014 let action_sender = self.get_actions_sender()?;
1015 info!("Got action to update nodes");
1016 let _ = self.update_node_items(Some(NodeStatus::Updating));
1017 let (service_names, peer_ids) = self.get_service_names_and_peer_ids();
1018
1019 let upgrade_nodes_args = UpgradeNodesArgs {
1020 action_sender,
1021 connection_timeout_s: 5,
1022 do_not_start: true,
1023 custom_bin_path: None,
1024 force: false,
1025 fixed_interval: Some(FIXED_INTERVAL),
1026 peer_ids,
1027 provided_env_variables: None,
1028 service_names,
1029 url: None,
1030 version: None,
1031 };
1032 self.node_management
1033 .send_task(NodeManagementTask::UpgradeNodes {
1034 args: upgrade_nodes_args,
1035 })?;
1036 }
1037 Action::OptionsActions(OptionsActions::ResetNodes) => {
1038 debug!("Got action to reset nodes");
1039 let action_sender = self.get_actions_sender()?;
1040 info!("Got action to reset nodes");
1041 self.node_management
1042 .send_task(NodeManagementTask::ResetNodes {
1043 start_nodes_after_reset: false,
1044 action_sender,
1045 })?;
1046 }
1047 Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, _drive_name)) => {
1048 self.storage_mountpoint.clone_from(&mountpoint);
1049 self.available_disk_space_gb = get_available_space_b(&mountpoint)? / GB;
1050 }
1051 _ => {}
1052 }
1053 Ok(None)
1054 }
1055
1056 fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
1057 if !self.active {
1058 return Ok(());
1059 }
1060
1061 let layout = Layout::new(
1062 Direction::Vertical,
1063 [
1064 Constraint::Length(1),
1066 Constraint::Max(6),
1068 Constraint::Min(3),
1070 Constraint::Length(3),
1072 ],
1073 )
1074 .split(area);
1075
1076 let header = Header::new();
1079 f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Status);
1080
1081 let combined_block = Block::default()
1087 .title(" Device Status ")
1088 .bold()
1089 .title_style(Style::default().fg(GHOST_WHITE))
1090 .borders(Borders::ALL)
1091 .padding(Padding::horizontal(1))
1092 .style(Style::default().fg(VERY_LIGHT_AZURE));
1093
1094 f.render_widget(combined_block.clone(), layout[1]);
1095
1096 let storage_allocated_row = Row::new(vec![
1097 Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE),
1098 Cell::new(format!("{} GB", self.nodes_to_start * GB_PER_NODE)).fg(GHOST_WHITE),
1099 ]);
1100 let memory_use_val = if self.node_stats.total_memory_usage_mb as f64 / 1024_f64 > 1.0 {
1101 format!(
1102 "{:.2} GB",
1103 self.node_stats.total_memory_usage_mb as f64 / 1024_f64
1104 )
1105 } else {
1106 format!("{} MB", self.node_stats.total_memory_usage_mb)
1107 };
1108
1109 let memory_use_row = Row::new(vec![
1110 Cell::new("Memory Use".to_string()).fg(GHOST_WHITE),
1111 Cell::new(memory_use_val).fg(GHOST_WHITE),
1112 ]);
1113
1114 let connection_mode_string = match self.connection_mode {
1115 ConnectionMode::HomeNetwork => "Home Network".to_string(),
1116 ConnectionMode::UPnP => "UPnP".to_string(),
1117 ConnectionMode::CustomPorts => format!(
1118 "Custom Ports {}-{}",
1119 self.port_from.unwrap_or(PORT_MIN),
1120 self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION)
1121 ),
1122 ConnectionMode::Automatic => "Automatic".to_string(),
1123 };
1124
1125 let mut connection_mode_line = vec![Span::styled(
1126 connection_mode_string,
1127 Style::default().fg(GHOST_WHITE),
1128 )];
1129
1130 if matches!(
1131 self.connection_mode,
1132 ConnectionMode::Automatic | ConnectionMode::UPnP
1133 ) {
1134 connection_mode_line.push(Span::styled(" (", Style::default().fg(GHOST_WHITE)));
1135
1136 if self.connection_mode == ConnectionMode::Automatic {
1137 connection_mode_line.push(Span::styled("UPnP: ", Style::default().fg(GHOST_WHITE)));
1138 }
1139
1140 let span = match self.upnp_support {
1141 UpnpSupport::Supported => {
1142 Span::styled("supported", Style::default().fg(EUCALYPTUS))
1143 }
1144 UpnpSupport::Unsupported => {
1145 Span::styled("disabled / unsupported", Style::default().fg(SIZZLING_RED))
1146 }
1147 UpnpSupport::Loading => {
1148 Span::styled("loading..", Style::default().fg(LIGHT_PERIWINKLE))
1149 }
1150 UpnpSupport::Unknown => {
1151 Span::styled("unknown", Style::default().fg(LIGHT_PERIWINKLE))
1152 }
1153 };
1154
1155 connection_mode_line.push(span);
1156
1157 connection_mode_line.push(Span::styled(")", Style::default().fg(GHOST_WHITE)));
1158 }
1159
1160 let connection_mode_row = Row::new(vec![
1161 Cell::new("Connection".to_string()).fg(GHOST_WHITE),
1162 Cell::new(Line::from(connection_mode_line)),
1163 ]);
1164
1165 let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row];
1166 let stats_width = [Constraint::Length(5)];
1167 let column_constraints = [Constraint::Length(23), Constraint::Fill(1)];
1168 let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints);
1169
1170 let wallet_not_set = if self.rewards_address.is_empty() {
1171 vec![
1172 Span::styled("Press ".to_string(), Style::default().fg(VIVID_SKY_BLUE)),
1173 Span::styled("[Ctrl+B] ".to_string(), Style::default().fg(GHOST_WHITE)),
1174 Span::styled(
1175 "to add your ".to_string(),
1176 Style::default().fg(VIVID_SKY_BLUE),
1177 ),
1178 Span::styled(
1179 "Wallet Address".to_string(),
1180 Style::default().fg(VIVID_SKY_BLUE).bold(),
1181 ),
1182 ]
1183 } else {
1184 vec![]
1185 };
1186
1187 let total_attos_earned_and_wallet_row = Row::new(vec![
1188 Cell::new("Attos Earned".to_string()).fg(VIVID_SKY_BLUE),
1189 Cell::new(format!(
1190 "{:?}",
1191 self.node_stats.total_rewards_wallet_balance
1192 ))
1193 .fg(VIVID_SKY_BLUE)
1194 .bold(),
1195 Cell::new(Line::from(wallet_not_set).alignment(Alignment::Right)),
1196 ]);
1197
1198 let attos_wallet_rows = vec![total_attos_earned_and_wallet_row];
1199 let attos_wallet_width = [Constraint::Length(5)];
1200 let column_constraints = [
1201 Constraint::Length(23),
1202 Constraint::Fill(1),
1203 Constraint::Length(if self.rewards_address.is_empty() {
1204 41 } else {
1206 0
1207 }),
1208 ];
1209 let attos_wallet_table =
1210 Table::new(attos_wallet_rows, attos_wallet_width).widths(column_constraints);
1211
1212 let inner_area = combined_block.inner(layout[1]);
1213 let device_layout = Layout::new(
1214 Direction::Vertical,
1215 vec![Constraint::Length(5), Constraint::Length(1)],
1216 )
1217 .split(inner_area);
1218
1219 f.render_widget(stats_table, device_layout[0]);
1221 f.render_widget(attos_wallet_table, device_layout[1]);
1222
1223 if let Some(ref items) = self.items {
1227 if items.items.is_empty() || self.rewards_address.is_empty() {
1228 let line1 = Line::from(vec![
1229 Span::styled("Press ", Style::default().fg(LIGHT_PERIWINKLE)),
1230 Span::styled("[+] ", Style::default().fg(GHOST_WHITE).bold()),
1231 Span::styled("to Add and ", Style::default().fg(LIGHT_PERIWINKLE)),
1232 Span::styled(
1233 "Start your first node ",
1234 Style::default().fg(GHOST_WHITE).bold(),
1235 ),
1236 Span::styled("on this device", Style::default().fg(LIGHT_PERIWINKLE)),
1237 ]);
1238
1239 let line2 = Line::from(vec![Span::styled(
1240 format!(
1241 "Each node will use {}GB of storage and a small amount of memory, \
1242 CPU, and Network bandwidth. Most computers can run many nodes at once, \
1243 but we recommend you add them gradually",
1244 GB_PER_NODE
1245 ),
1246 Style::default().fg(LIGHT_PERIWINKLE),
1247 )]);
1248
1249 f.render_widget(
1250 Paragraph::new(vec![Line::raw(""), line1, Line::raw(""), line2])
1251 .wrap(Wrap { trim: false })
1252 .fg(LIGHT_PERIWINKLE)
1253 .block(
1254 Block::default()
1255 .title(Line::from(vec![
1256 Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
1257 Span::styled(" (0) ", Style::default().fg(LIGHT_PERIWINKLE)),
1258 ]))
1259 .title_style(Style::default().fg(LIGHT_PERIWINKLE))
1260 .borders(Borders::ALL)
1261 .border_style(style::Style::default().fg(EUCALYPTUS))
1262 .padding(Padding::horizontal(1)),
1263 ),
1264 layout[2],
1265 );
1266 } else {
1267 let block_nodes = Block::default()
1269 .title(Line::from(vec![
1270 Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
1271 Span::styled(
1272 format!(
1273 " ({}) ",
1274 if let Some(ref items) = self.items {
1275 items.items.len()
1276 } else {
1277 0
1278 }
1279 ),
1280 Style::default().fg(LIGHT_PERIWINKLE),
1281 ),
1282 ]))
1283 .padding(Padding::new(1, 1, 0, 0))
1284 .title_style(Style::default().fg(GHOST_WHITE))
1285 .borders(Borders::ALL)
1286 .border_style(Style::default().fg(EUCALYPTUS));
1287
1288 let inner_area = block_nodes.inner(layout[2]);
1290
1291 let node_widths = [
1293 Constraint::Min(NODE_WIDTH as u16),
1294 Constraint::Min(VERSION_WIDTH as u16),
1295 Constraint::Min(ATTOS_WIDTH as u16),
1296 Constraint::Min(MEMORY_WIDTH as u16),
1297 Constraint::Min(MBPS_WIDTH as u16),
1298 Constraint::Min(RECORDS_WIDTH as u16),
1299 Constraint::Min(PEERS_WIDTH as u16),
1300 Constraint::Min(CONNS_WIDTH as u16),
1301 Constraint::Min(MODE_WIDTH as u16),
1302 Constraint::Min(STATUS_WIDTH as u16),
1303 Constraint::Fill(FAILURE_WIDTH as u16),
1304 Constraint::Max(SPINNER_WIDTH as u16),
1305 ];
1306
1307 let header_row = Row::new(vec![
1309 Cell::new("Node").fg(COOL_GREY),
1310 Cell::new("Version").fg(COOL_GREY),
1311 Cell::new("Attos").fg(COOL_GREY),
1312 Cell::new("Memory").fg(COOL_GREY),
1313 Cell::new(
1314 format!("{}{}", " ".repeat(MBPS_WIDTH - "Mbps".len()), "Mbps")
1315 .fg(COOL_GREY),
1316 ),
1317 Cell::new("Recs").fg(COOL_GREY),
1318 Cell::new("Peers").fg(COOL_GREY),
1319 Cell::new("Conns").fg(COOL_GREY),
1320 Cell::new("Mode").fg(COOL_GREY),
1321 Cell::new("Status").fg(COOL_GREY),
1322 Cell::new("Failure").fg(COOL_GREY),
1323 Cell::new(" ").fg(COOL_GREY), ])
1325 .style(Style::default().add_modifier(Modifier::BOLD));
1326
1327 let mut items: Vec<Row> = Vec::new();
1328 if let Some(ref mut items_table) = self.items {
1329 for (i, node_item) in items_table.items.iter_mut().enumerate() {
1330 let is_selected = items_table.state.selected() == Some(i);
1331 items.push(node_item.render_as_row(i, layout[2], f, is_selected));
1332 }
1333 }
1334
1335 let table = Table::new(items, node_widths)
1337 .header(header_row)
1338 .column_spacing(1)
1339 .row_highlight_style(Style::default().bg(INDIGO))
1340 .highlight_spacing(HighlightSpacing::Always);
1341
1342 f.render_widget(table, inner_area);
1343
1344 f.render_widget(block_nodes, layout[2]);
1345 }
1346 }
1347
1348 let selected = self
1351 .items
1352 .as_ref()
1353 .and_then(|items| items.selected_item())
1354 .is_some();
1355
1356 let footer = Footer::default();
1357 let footer_state = if let Some(ref items) = self.items {
1358 if !items.items.is_empty() || !self.rewards_address.is_empty() {
1359 if !self.get_running_nodes().is_empty() {
1360 if selected {
1361 &mut NodesToStart::RunningSelected
1362 } else {
1363 &mut NodesToStart::Running
1364 }
1365 } else if selected {
1366 &mut NodesToStart::NotRunningSelected
1367 } else {
1368 &mut NodesToStart::NotRunning
1369 }
1370 } else {
1371 &mut NodesToStart::NotRunning
1372 }
1373 } else {
1374 &mut NodesToStart::NotRunning
1375 };
1376 f.render_stateful_widget(footer, layout[3], footer_state);
1377
1378 if let Some(error_popup) = &self.error_popup {
1382 if error_popup.is_visible() {
1383 error_popup.draw_error(f, area);
1384
1385 return Ok(());
1386 }
1387 }
1388
1389 if self.nodes_starting() && self.should_we_run_nat_detection() {
1390 let popup_text = vec![
1391 Line::raw("Starting nodes..."),
1392 Line::raw(""),
1393 Line::raw(""),
1394 Line::raw("Please wait, performing initial NAT detection"),
1395 Line::raw("This may take a couple minutes."),
1396 ];
1397
1398 let popup_area = centered_rect_fixed(50, 12, area);
1399 clear_area(f, popup_area);
1400
1401 let popup_border = Paragraph::new("").block(
1402 Block::default()
1403 .borders(Borders::ALL)
1404 .title(" Manage Nodes ")
1405 .bold()
1406 .title_style(Style::new().fg(VIVID_SKY_BLUE))
1407 .padding(Padding::uniform(2))
1408 .border_style(Style::new().fg(GHOST_WHITE)),
1409 );
1410
1411 let centred_area = Layout::new(
1412 Direction::Vertical,
1413 vec![
1414 Constraint::Length(2),
1416 Constraint::Min(5),
1418 Constraint::Length(1),
1420 ],
1421 )
1422 .split(popup_area);
1423 let text = Paragraph::new(popup_text)
1424 .block(Block::default().padding(Padding::horizontal(2)))
1425 .wrap(Wrap { trim: false })
1426 .alignment(Alignment::Center)
1427 .fg(EUCALYPTUS);
1428 f.render_widget(text, centred_area[1]);
1429
1430 f.render_widget(popup_border, popup_area);
1431 }
1432
1433 Ok(())
1434 }
1435
1436 fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
1437 debug!("Key received in Status: {:?}", key);
1438 if let Some(error_popup) = &mut self.error_popup {
1439 if error_popup.is_visible() {
1440 error_popup.handle_input(key);
1441 return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]);
1442 }
1443 }
1444 Ok(vec![])
1445 }
1446}
1447
1448#[allow(dead_code)]
1449#[derive(Default, Clone)]
1450struct StatefulTable<T> {
1451 state: TableState,
1452 items: Vec<T>,
1453 last_selected: Option<usize>,
1454}
1455
1456#[allow(dead_code)]
1457impl<T> StatefulTable<T> {
1458 fn with_items(items: Vec<T>) -> Self {
1459 StatefulTable {
1460 state: TableState::default(),
1461 items,
1462 last_selected: None,
1463 }
1464 }
1465
1466 fn next(&mut self) {
1467 let i = match self.state.selected() {
1468 Some(i) => {
1469 if !self.items.is_empty() {
1470 if i >= self.items.len() - 1 {
1471 0
1472 } else {
1473 i + 1
1474 }
1475 } else {
1476 0
1477 }
1478 }
1479 None => self.last_selected.unwrap_or(0),
1480 };
1481 self.state.select(Some(i));
1482 self.last_selected = Some(i);
1483 }
1484
1485 fn previous(&mut self) {
1486 let i = match self.state.selected() {
1487 Some(i) => {
1488 if !self.items.is_empty() {
1489 if i == 0 {
1490 self.items.len() - 1
1491 } else {
1492 i - 1
1493 }
1494 } else {
1495 0
1496 }
1497 }
1498 None => self.last_selected.unwrap_or(0),
1499 };
1500 self.state.select(Some(i));
1501 self.last_selected = Some(i);
1502 }
1503
1504 fn selected_item(&self) -> Option<&T> {
1505 self.state
1506 .selected()
1507 .and_then(|index| self.items.get(index))
1508 }
1509}
1510
1511#[derive(Default, Debug, Copy, Clone, PartialEq)]
1512enum NodeStatus {
1513 #[default]
1514 Added,
1515 Running,
1516 Starting,
1517 Stopped,
1518 Removed,
1519 Updating,
1520}
1521
1522impl fmt::Display for NodeStatus {
1523 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1524 match *self {
1525 NodeStatus::Added => write!(f, "Added"),
1526 NodeStatus::Running => write!(f, "Running"),
1527 NodeStatus::Starting => write!(f, "Starting"),
1528 NodeStatus::Stopped => write!(f, "Stopped"),
1529 NodeStatus::Removed => write!(f, "Removed"),
1530 NodeStatus::Updating => write!(f, "Updating"),
1531 }
1532 }
1533}
1534
1535#[derive(Default, Debug, Clone)]
1536pub struct NodeItem<'a> {
1537 name: String,
1538 version: String,
1539 attos: usize,
1540 memory: usize,
1541 mbps: String,
1542 records: usize,
1543 peers: usize,
1544 connections: usize,
1545 locked: bool, mode: NodeConnectionMode,
1547 status: NodeStatus,
1548 failure: Option<(chrono::DateTime<chrono::Utc>, String)>,
1549 spinner: Throbber<'a>,
1550 spinner_state: ThrobberState,
1551}
1552
1553impl NodeItem<'_> {
1554 fn render_as_row(
1555 &mut self,
1556 index: usize,
1557 area: Rect,
1558 f: &mut Frame<'_>,
1559 is_selected: bool,
1560 ) -> Row {
1561 let mut row_style = if is_selected {
1562 Style::default().fg(GHOST_WHITE).bg(INDIGO)
1563 } else {
1564 Style::default().fg(GHOST_WHITE)
1565 };
1566 let mut spinner_state = self.spinner_state.clone();
1567 match self.status {
1568 NodeStatus::Running => {
1569 self.spinner = self
1570 .spinner
1571 .clone()
1572 .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1573 .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1574 .use_type(throbber_widgets_tui::WhichUse::Spin);
1575 row_style = if is_selected {
1576 Style::default().fg(EUCALYPTUS).bg(INDIGO)
1577 } else {
1578 Style::default().fg(EUCALYPTUS)
1579 };
1580 }
1581 NodeStatus::Starting => {
1582 self.spinner = self
1583 .spinner
1584 .clone()
1585 .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1586 .throbber_set(throbber_widgets_tui::BOX_DRAWING)
1587 .use_type(throbber_widgets_tui::WhichUse::Spin);
1588 }
1589 NodeStatus::Stopped => {
1590 self.spinner = self
1591 .spinner
1592 .clone()
1593 .throbber_style(
1594 Style::default()
1595 .fg(GHOST_WHITE)
1596 .add_modifier(Modifier::BOLD),
1597 )
1598 .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1599 .use_type(throbber_widgets_tui::WhichUse::Full);
1600 }
1601 NodeStatus::Updating => {
1602 self.spinner = self
1603 .spinner
1604 .clone()
1605 .throbber_style(
1606 Style::default()
1607 .fg(GHOST_WHITE)
1608 .add_modifier(Modifier::BOLD),
1609 )
1610 .throbber_set(throbber_widgets_tui::VERTICAL_BLOCK)
1611 .use_type(throbber_widgets_tui::WhichUse::Spin);
1612 }
1613 _ => {}
1614 };
1615
1616 let failure = self.failure.as_ref().map_or_else(
1617 || "-".to_string(),
1618 |(_dt, msg)| {
1619 if self.status == NodeStatus::Stopped {
1620 msg.clone()
1621 } else {
1622 "-".to_string()
1623 }
1624 },
1625 );
1626
1627 let row = vec![
1628 self.name.clone().to_string(),
1629 self.version.to_string(),
1630 format!(
1631 "{}{}",
1632 " ".repeat(ATTOS_WIDTH.saturating_sub(self.attos.to_string().len())),
1633 self.attos.to_string()
1634 ),
1635 format!(
1636 "{}{} MB",
1637 " ".repeat(MEMORY_WIDTH.saturating_sub(self.memory.to_string().len() + 4)),
1638 self.memory.to_string()
1639 ),
1640 format!(
1641 "{}{}",
1642 " ".repeat(MBPS_WIDTH.saturating_sub(self.mbps.to_string().len())),
1643 self.mbps.to_string()
1644 ),
1645 format!(
1646 "{}{}",
1647 " ".repeat(RECORDS_WIDTH.saturating_sub(self.records.to_string().len())),
1648 self.records.to_string()
1649 ),
1650 format!(
1651 "{}{}",
1652 " ".repeat(PEERS_WIDTH.saturating_sub(self.peers.to_string().len())),
1653 self.peers.to_string()
1654 ),
1655 format!(
1656 "{}{}",
1657 " ".repeat(CONNS_WIDTH.saturating_sub(self.connections.to_string().len())),
1658 self.connections.to_string()
1659 ),
1660 self.mode.to_string(),
1661 self.status.to_string(),
1662 failure,
1663 ];
1664 let throbber_area = Rect::new(area.width - 3, area.y + 2 + index as u16, 1, 1);
1665
1666 f.render_stateful_widget(self.spinner.clone(), throbber_area, &mut spinner_state);
1667
1668 Row::new(row).style(row_style)
1669 }
1670}