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