node_launchpad/components/
status.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use super::footer::NodesToStart;
10use super::header::SelectedMenuItem;
11use super::{
12    footer::Footer, header::Header, popup::manage_nodes::GB_PER_NODE, utils::centered_rect_fixed,
13    Component, Frame,
14};
15use crate::action::OptionsActions;
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::{MaintainNodesArgs, NodeManagement, NodeManagementTask, UpgradeNodesArgs};
22use crate::node_mgmt::{PORT_MAX, PORT_MIN};
23use crate::style::{COOL_GREY, INDIGO, SIZZLING_RED};
24use crate::tui::Event;
25use crate::upnp::UpnpSupport;
26use crate::{
27    action::{Action, StatusActions},
28    config::Config,
29    mode::{InputMode, Scene},
30    node_stats::NodeStats,
31    style::{
32        clear_area, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE,
33    },
34};
35use ant_bootstrap::InitialPeersConfig;
36use ant_node_manager::add_services::config::PortRange;
37use ant_node_manager::config::get_node_registry_path;
38use ant_service_management::{
39    control::ServiceController, NodeRegistry, NodeServiceData, ServiceStatus,
40};
41use color_eyre::eyre::{Ok, OptionExt, Result};
42use crossterm::event::KeyEvent;
43use ratatui::text::Span;
44use ratatui::{prelude::*, widgets::*};
45use std::fmt;
46use std::{
47    path::PathBuf,
48    time::{Duration, Instant},
49    vec,
50};
51use strum::Display;
52use throbber_widgets_tui::{self, Throbber, ThrobberState};
53use tokio::sync::mpsc::UnboundedSender;
54
55pub const FIXED_INTERVAL: u64 = 60_000;
56pub const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
57/// If nat detection fails for more than 3 times, we don't want to waste time running during every node start.
58const MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION: usize = 3;
59
60// Table Widths
61const 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    /// Whether the component is active right now, capturing keystrokes + drawing things.
77    active: bool,
78    action_sender: Option<UnboundedSender<Action>>,
79    config: Config,
80    // NAT
81    is_nat_status_determined: bool,
82    error_while_running_nat_detection: usize,
83    // Device Stats Section
84    node_stats: NodeStats,
85    node_stats_last_update: Instant,
86    // Nodes
87    node_services: Vec<NodeServiceData>,
88    items: Option<StatefulTable<NodeItem<'a>>>,
89    /// To pass into node services.
90    network_id: Option<u8>,
91    // Node Management
92    node_management: NodeManagement,
93    // Amount of nodes
94    nodes_to_start: usize,
95    // Rewards address
96    rewards_address: String,
97    // Currently the node registry file does not support concurrent actions and thus can lead to
98    // inconsistent state. Another solution would be to have a file lock/db.
99    lock_registry: Option<LockRegistryState>,
100    // Peers to pass into nodes for startup
101    init_peers_config: InitialPeersConfig,
102    // If path is provided, we don't fetch the binary from the network
103    antnode_path: Option<PathBuf>,
104    // Path where the node data is stored
105    data_dir_path: PathBuf,
106    // Connection mode
107    connection_mode: ConnectionMode,
108    // UPnP support
109    upnp_support: UpnpSupport,
110    // Port from
111    port_from: Option<u32>,
112    // Port to
113    port_to: Option<u32>,
114    error_popup: Option<ErrorPopup>,
115}
116
117#[derive(Clone, Display, Debug)]
118pub enum LockRegistryState {
119    StartingNodes,
120    StoppingNodes,
121    ResettingNodes,
122    UpdatingNodes,
123}
124
125pub struct StatusConfig {
126    pub allocated_disk_space: usize,
127    pub antnode_path: Option<PathBuf>,
128    pub connection_mode: ConnectionMode,
129    pub upnp_support: UpnpSupport,
130    pub data_dir_path: PathBuf,
131    pub network_id: Option<u8>,
132    pub init_peers_config: InitialPeersConfig,
133    pub port_from: Option<u32>,
134    pub port_to: Option<u32>,
135    pub rewards_address: String,
136}
137
138impl Status<'_> {
139    pub async fn new(config: StatusConfig) -> Result<Self> {
140        let mut status = Self {
141            init_peers_config: config.init_peers_config,
142            action_sender: Default::default(),
143            config: Default::default(),
144            active: true,
145            is_nat_status_determined: false,
146            error_while_running_nat_detection: 0,
147            network_id: config.network_id,
148            node_stats: NodeStats::default(),
149            node_stats_last_update: Instant::now(),
150            node_services: Default::default(),
151            node_management: NodeManagement::new()?,
152            items: None,
153            nodes_to_start: config.allocated_disk_space,
154            lock_registry: None,
155            rewards_address: config.rewards_address,
156            antnode_path: config.antnode_path,
157            data_dir_path: config.data_dir_path,
158            connection_mode: config.connection_mode,
159            upnp_support: config.upnp_support,
160            port_from: config.port_from,
161            port_to: config.port_to,
162            error_popup: None,
163        };
164
165        // Nodes registry
166        let now = Instant::now();
167        debug!("Refreshing node registry states on startup");
168        let mut node_registry = NodeRegistry::load(&get_node_registry_path()?)?;
169        ant_node_manager::refresh_node_registry(
170            &mut node_registry,
171            &ServiceController {},
172            false,
173            true,
174            false,
175        )
176        .await?;
177        node_registry.save()?;
178        debug!("Node registry states refreshed in {:?}", now.elapsed());
179        status.load_node_registry_and_update_states()?;
180
181        Ok(status)
182    }
183
184    fn update_node_items(&mut self, new_status: Option<NodeStatus>) -> Result<()> {
185        // Iterate over existing node services and update their corresponding NodeItem
186        if let Some(ref mut items) = self.items {
187            for node_item in self.node_services.iter() {
188                // Find the corresponding item by service name
189                if let Some(item) = items
190                    .items
191                    .iter_mut()
192                    .find(|i| i.name == node_item.service_name)
193                {
194                    if let Some(status) = new_status {
195                        item.status = status;
196                    } else if item.status == NodeStatus::Updating {
197                        item.spinner_state.calc_next();
198                    } else if new_status != Some(NodeStatus::Updating) {
199                        // Update status based on current node status
200                        item.status = match node_item.status {
201                            ServiceStatus::Running => {
202                                item.spinner_state.calc_next();
203                                NodeStatus::Running
204                            }
205                            ServiceStatus::Stopped => NodeStatus::Stopped,
206                            ServiceStatus::Added => NodeStatus::Added,
207                            ServiceStatus::Removed => NodeStatus::Removed,
208                        };
209
210                        // Starting is not part of ServiceStatus so we do it manually
211                        if let Some(LockRegistryState::StartingNodes) = self.lock_registry {
212                            item.spinner_state.calc_next();
213                            if item.status != NodeStatus::Running {
214                                item.status = NodeStatus::Starting;
215                            }
216                        }
217                    }
218
219                    // Update peers count
220                    item.peers = match node_item.connected_peers {
221                        Some(ref peers) => peers.len(),
222                        None => 0,
223                    };
224
225                    // Update individual stats if available
226                    if let Some(stats) = self
227                        .node_stats
228                        .individual_stats
229                        .iter()
230                        .find(|s| s.service_name == node_item.service_name)
231                    {
232                        item.attos = stats.rewards_wallet_balance;
233                        item.memory = stats.memory_usage_mb;
234                        item.mbps = format!(
235                            "↓{:0>5.0} ↑{:0>5.0}",
236                            (stats.bandwidth_inbound_rate * 8) as f64 / 1_000_000.0,
237                            (stats.bandwidth_outbound_rate * 8) as f64 / 1_000_000.0,
238                        );
239                        item.records = stats.max_records;
240                        item.connections = stats.connections;
241                    }
242                } else {
243                    // If not found, create a new NodeItem and add it to items
244                    let new_item = NodeItem {
245                        name: node_item.service_name.clone(),
246                        version: node_item.version.to_string(),
247                        attos: 0,
248                        memory: 0,
249                        mbps: "-".to_string(),
250                        records: 0,
251                        peers: 0,
252                        connections: 0,
253                        mode: NodeConnectionMode::from(node_item),
254                        status: NodeStatus::Added, // Set initial status as Added
255                        failure: node_item.get_critical_failure(),
256                        spinner: Throbber::default(),
257                        spinner_state: ThrobberState::default(),
258                    };
259                    items.items.push(new_item);
260                }
261            }
262        } else {
263            // If items is None, create a new list (fallback)
264            let node_items: Vec<NodeItem> = self
265                .node_services
266                .iter()
267                .filter_map(|node_item| {
268                    if node_item.status == ServiceStatus::Removed {
269                        return None;
270                    }
271                    // Update status based on current node status
272                    let status = match node_item.status {
273                        ServiceStatus::Running => NodeStatus::Running,
274                        ServiceStatus::Stopped => NodeStatus::Stopped,
275                        ServiceStatus::Added => NodeStatus::Added,
276                        ServiceStatus::Removed => NodeStatus::Removed,
277                    };
278
279                    // Create a new NodeItem for the first time
280                    Some(NodeItem {
281                        name: node_item.service_name.clone().to_string(),
282                        version: node_item.version.to_string(),
283                        attos: 0,
284                        memory: 0,
285                        mbps: "-".to_string(),
286                        records: 0,
287                        peers: 0,
288                        connections: 0,
289                        mode: NodeConnectionMode::from(node_item),
290                        status,
291                        failure: node_item.get_critical_failure(),
292                        spinner: Throbber::default(),
293                        spinner_state: ThrobberState::default(),
294                    })
295                })
296                .collect();
297            self.items = Some(StatefulTable::with_items(node_items));
298        }
299        Ok(())
300    }
301
302    fn clear_node_items(&mut self) {
303        debug!("Cleaning items on Status page");
304        if let Some(items) = self.items.as_mut() {
305            items.items.clear();
306            debug!("Cleared the items on status page");
307        }
308    }
309
310    /// Tries to trigger the update of node stats if the last update was more than `NODE_STAT_UPDATE_INTERVAL` ago.
311    /// The result is sent via the StatusActions::NodesStatsObtained action.
312    fn try_update_node_stats(&mut self, force_update: bool) -> Result<()> {
313        if self.node_stats_last_update.elapsed() > NODE_STAT_UPDATE_INTERVAL || force_update {
314            self.node_stats_last_update = Instant::now();
315
316            NodeStats::fetch_all_node_stats(&self.node_services, self.get_actions_sender()?);
317        }
318        Ok(())
319    }
320    fn get_actions_sender(&self) -> Result<UnboundedSender<Action>> {
321        self.action_sender
322            .clone()
323            .ok_or_eyre("Action sender not registered")
324    }
325
326    fn load_node_registry_and_update_states(&mut self) -> Result<()> {
327        let node_registry = NodeRegistry::load(&get_node_registry_path()?)?;
328        self.is_nat_status_determined = node_registry.nat_status.is_some();
329        self.node_services = node_registry
330            .nodes
331            .into_iter()
332            .filter(|node| node.status != ServiceStatus::Removed)
333            .collect();
334        info!(
335            "Loaded node registry. Maintaining {:?} nodes.",
336            self.node_services.len()
337        );
338
339        Ok(())
340    }
341
342    /// Only run NAT detection if we haven't determined the status yet and we haven't failed more than 3 times.
343    fn should_we_run_nat_detection(&self) -> bool {
344        self.connection_mode == ConnectionMode::Automatic
345            && !self.is_nat_status_determined
346            && self.error_while_running_nat_detection < MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION
347    }
348
349    fn get_running_nodes(&self) -> Vec<String> {
350        self.node_services
351            .iter()
352            .filter_map(|node| {
353                if node.status == ServiceStatus::Running {
354                    Some(node.service_name.clone())
355                } else {
356                    None
357                }
358            })
359            .collect()
360    }
361
362    fn get_service_names_and_peer_ids(&self) -> (Vec<String>, Vec<String>) {
363        let mut service_names = Vec::new();
364        let mut peers_ids = Vec::new();
365
366        for node in &self.node_services {
367            // Only include nodes with a valid peer_id
368            if let Some(peer_id) = &node.peer_id {
369                service_names.push(node.service_name.clone());
370                peers_ids.push(peer_id.to_string().clone());
371            }
372        }
373
374        (service_names, peers_ids)
375    }
376}
377
378impl Component for Status<'_> {
379    fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
380        self.action_sender = Some(tx);
381
382        // Update the stats to be shown as soon as the app is run
383        self.try_update_node_stats(true)?;
384
385        Ok(())
386    }
387
388    fn register_config_handler(&mut self, config: Config) -> Result<()> {
389        self.config = config;
390        Ok(())
391    }
392
393    fn handle_events(&mut self, event: Option<Event>) -> Result<Vec<Action>> {
394        let r = match event {
395            Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
396            _ => vec![],
397        };
398        Ok(r)
399    }
400
401    fn update(&mut self, action: Action) -> Result<Option<Action>> {
402        match action {
403            Action::Tick => {
404                self.try_update_node_stats(false)?;
405                let _ = self.update_node_items(None);
406            }
407            Action::SwitchScene(scene) => match scene {
408                Scene::Status | Scene::StatusRewardsAddressPopUp => {
409                    self.active = true;
410                    // make sure we're in navigation mode
411                    return Ok(Some(Action::SwitchInputMode(InputMode::Navigation)));
412                }
413                Scene::ManageNodesPopUp => self.active = true,
414                _ => self.active = false,
415            },
416            Action::StoreNodesToStart(count) => {
417                self.nodes_to_start = count;
418                if self.nodes_to_start == 0 {
419                    info!("Nodes to start set to 0. Sending command to stop all nodes.");
420                    return Ok(Some(Action::StatusActions(StatusActions::StopNodes)));
421                } else {
422                    info!("Nodes to start set to: {count}. Sending command to start nodes");
423                    return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
424                }
425            }
426            Action::StoreRewardsAddress(rewards_address) => {
427                debug!("Storing rewards address: {rewards_address:?}");
428                let has_changed = self.rewards_address != rewards_address;
429                let we_have_nodes = !self.node_services.is_empty();
430
431                self.rewards_address = rewards_address;
432
433                if we_have_nodes && has_changed {
434                    debug!("Setting lock_registry to ResettingNodes");
435                    self.lock_registry = Some(LockRegistryState::ResettingNodes);
436                    info!("Resetting antnode services because the Rewards Address was reset.");
437                    let action_sender = self.get_actions_sender()?;
438                    self.node_management
439                        .send_task(NodeManagementTask::ResetNodes {
440                            start_nodes_after_reset: false,
441                            action_sender,
442                        })?;
443                }
444            }
445            Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => {
446                debug!("Setting lock_registry to ResettingNodes");
447                self.lock_registry = Some(LockRegistryState::ResettingNodes);
448                info!("Resetting antnode services because the Storage Drive was changed.");
449                let action_sender = self.get_actions_sender()?;
450                self.node_management
451                    .send_task(NodeManagementTask::ResetNodes {
452                        start_nodes_after_reset: false,
453                        action_sender,
454                    })?;
455                self.data_dir_path =
456                    get_launchpad_nodes_data_dir_path(&drive_mountpoint.to_path_buf(), false)?;
457            }
458            Action::StoreConnectionMode(connection_mode) => {
459                debug!("Setting lock_registry to ResettingNodes");
460                self.lock_registry = Some(LockRegistryState::ResettingNodes);
461                self.connection_mode = connection_mode;
462                info!("Resetting antnode services because the Connection Mode range was changed.");
463                let action_sender = self.get_actions_sender()?;
464                self.node_management
465                    .send_task(NodeManagementTask::ResetNodes {
466                        start_nodes_after_reset: false,
467                        action_sender,
468                    })?;
469            }
470            Action::StorePortRange(port_from, port_range) => {
471                debug!("Setting lock_registry to ResettingNodes");
472                self.lock_registry = Some(LockRegistryState::ResettingNodes);
473                self.port_from = Some(port_from);
474                self.port_to = Some(port_range);
475                info!("Resetting antnode services because the Port Range was changed.");
476                let action_sender = self.get_actions_sender()?;
477                self.node_management
478                    .send_task(NodeManagementTask::ResetNodes {
479                        start_nodes_after_reset: false,
480                        action_sender,
481                    })?;
482            }
483            Action::SetUpnpSupport(ref upnp_support) => {
484                debug!("Setting UPnP support: {upnp_support:?}");
485                self.upnp_support = upnp_support.clone();
486            }
487            Action::StatusActions(status_action) => match status_action {
488                StatusActions::NodesStatsObtained(stats) => {
489                    self.node_stats = stats;
490                }
491                StatusActions::StartNodesCompleted | StatusActions::StopNodesCompleted => {
492                    self.lock_registry = None;
493                    self.load_node_registry_and_update_states()?;
494                }
495                StatusActions::UpdateNodesCompleted => {
496                    self.lock_registry = None;
497                    self.clear_node_items();
498                    self.load_node_registry_and_update_states()?;
499                    let _ = self.update_node_items(None);
500                    debug!("Update nodes completed");
501                }
502                StatusActions::ResetNodesCompleted { trigger_start_node } => {
503                    self.lock_registry = None;
504                    self.load_node_registry_and_update_states()?;
505                    self.clear_node_items();
506
507                    if trigger_start_node {
508                        debug!("Reset nodes completed. Triggering start nodes.");
509                        return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
510                    }
511                    debug!("Reset nodes completed");
512                }
513                StatusActions::SuccessfullyDetectedNatStatus => {
514                    debug!(
515                        "Successfully detected nat status, is_nat_status_determined set to true"
516                    );
517                    self.is_nat_status_determined = true;
518                }
519                StatusActions::ErrorWhileRunningNatDetection => {
520                    self.error_while_running_nat_detection += 1;
521                    debug!(
522                        "Error while running nat detection. Error count: {}",
523                        self.error_while_running_nat_detection
524                    );
525                }
526                StatusActions::ErrorLoadingNodeRegistry { raw_error }
527                | StatusActions::ErrorGettingNodeRegistryPath { raw_error } => {
528                    self.error_popup = Some(ErrorPopup::new(
529                        "Error".to_string(),
530                        "Error getting node registry path".to_string(),
531                        raw_error,
532                    ));
533                    if let Some(error_popup) = &mut self.error_popup {
534                        error_popup.show();
535                    }
536                    // Switch back to entry mode so we can handle key events
537                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
538                }
539                StatusActions::ErrorScalingUpNodes { raw_error } => {
540                    self.error_popup = Some(ErrorPopup::new(
541                        "Error".to_string(),
542                        "Error adding new nodes".to_string(),
543                        raw_error,
544                    ));
545                    if let Some(error_popup) = &mut self.error_popup {
546                        error_popup.show();
547                    }
548                    // Switch back to entry mode so we can handle key events
549                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
550                }
551                StatusActions::ErrorStoppingNodes { raw_error } => {
552                    self.error_popup = Some(ErrorPopup::new(
553                        "Error".to_string(),
554                        "Error stopping nodes".to_string(),
555                        raw_error,
556                    ));
557                    if let Some(error_popup) = &mut self.error_popup {
558                        error_popup.show();
559                    }
560                    // Switch back to entry mode so we can handle key events
561                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
562                }
563                StatusActions::ErrorUpdatingNodes { raw_error } => {
564                    self.error_popup = Some(ErrorPopup::new(
565                        "Error".to_string(),
566                        "Error upgrading nodes".to_string(),
567                        raw_error,
568                    ));
569                    if let Some(error_popup) = &mut self.error_popup {
570                        error_popup.show();
571                    }
572                    // Switch back to entry mode so we can handle key events
573                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
574                }
575                StatusActions::ErrorResettingNodes { raw_error } => {
576                    self.error_popup = Some(ErrorPopup::new(
577                        "Error".to_string(),
578                        "Error resetting nodes".to_string(),
579                        raw_error,
580                    ));
581                    if let Some(error_popup) = &mut self.error_popup {
582                        error_popup.show();
583                    }
584                    // Switch back to entry mode so we can handle key events
585                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
586                }
587                StatusActions::TriggerManageNodes => {
588                    return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp)));
589                }
590                StatusActions::PreviousTableItem => {
591                    if let Some(items) = &mut self.items {
592                        items.previous();
593                    }
594                }
595                StatusActions::NextTableItem => {
596                    if let Some(items) = &mut self.items {
597                        items.next();
598                    }
599                }
600                StatusActions::StartNodes => {
601                    debug!("Got action to start nodes");
602
603                    if self.rewards_address.is_empty() {
604                        info!("Rewards address is not set. Ask for input.");
605                        return Ok(Some(Action::StatusActions(
606                            StatusActions::TriggerRewardsAddress,
607                        )));
608                    }
609
610                    if self.nodes_to_start == 0 {
611                        info!("Nodes to start not set. Ask for input.");
612                        return Ok(Some(Action::StatusActions(
613                            StatusActions::TriggerManageNodes,
614                        )));
615                    }
616
617                    if self.lock_registry.is_some() {
618                        error!(
619                            "Registry is locked ({:?}) Cannot Start nodes now.",
620                            self.lock_registry
621                        );
622                        return Ok(None);
623                    }
624
625                    debug!("Setting lock_registry to StartingNodes");
626                    self.lock_registry = Some(LockRegistryState::StartingNodes);
627
628                    let port_range = PortRange::Range(
629                        self.port_from.unwrap_or(PORT_MIN) as u16,
630                        self.port_to.unwrap_or(PORT_MAX) as u16,
631                    );
632
633                    let action_sender = self.get_actions_sender()?;
634
635                    let maintain_nodes_args = MaintainNodesArgs {
636                        action_sender: action_sender.clone(),
637                        antnode_path: self.antnode_path.clone(),
638                        connection_mode: self.connection_mode,
639                        count: self.nodes_to_start as u16,
640                        data_dir_path: Some(self.data_dir_path.clone()),
641                        network_id: self.network_id,
642                        owner: self.rewards_address.clone(),
643                        init_peers_config: self.init_peers_config.clone(),
644                        port_range: Some(port_range),
645                        rewards_address: self.rewards_address.clone(),
646                        run_nat_detection: self.should_we_run_nat_detection(),
647                    };
648
649                    debug!("Calling maintain_n_running_nodes");
650
651                    self.node_management
652                        .send_task(NodeManagementTask::MaintainNodes {
653                            args: maintain_nodes_args,
654                        })?;
655                }
656                StatusActions::StopNodes => {
657                    debug!("Got action to stop nodes");
658                    if self.lock_registry.is_some() {
659                        error!(
660                            "Registry is locked ({:?}) Cannot Stop nodes now.",
661                            self.lock_registry
662                        );
663                        return Ok(None);
664                    }
665
666                    let running_nodes = self.get_running_nodes();
667                    debug!("Setting lock_registry to StoppingNodes");
668                    self.lock_registry = Some(LockRegistryState::StoppingNodes);
669                    let action_sender = self.get_actions_sender()?;
670                    info!("Stopping node service: {running_nodes:?}");
671
672                    self.node_management
673                        .send_task(NodeManagementTask::StopNodes {
674                            services: running_nodes,
675                            action_sender,
676                        })?;
677                }
678                StatusActions::TriggerRewardsAddress => {
679                    if self.rewards_address.is_empty() {
680                        return Ok(Some(Action::SwitchScene(Scene::StatusRewardsAddressPopUp)));
681                    } else {
682                        return Ok(None);
683                    }
684                }
685                StatusActions::TriggerNodeLogs => {
686                    if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
687                    {
688                        debug!("Got action to open node logs {:?}", node.name);
689                        open_logs(Some(node.name.clone()))?;
690                    } else {
691                        debug!("Got action to open node logs but no node was selected.");
692                    }
693                }
694            },
695            Action::OptionsActions(OptionsActions::UpdateNodes) => {
696                debug!("Got action to Update Nodes");
697                self.load_node_registry_and_update_states()?;
698                if self.lock_registry.is_some() {
699                    error!(
700                        "Registry is locked ({:?}) Cannot Update nodes now. Stop them first.",
701                        self.lock_registry
702                    );
703                    return Ok(None);
704                } else {
705                    debug!("Lock registry ({:?})", self.lock_registry);
706                };
707                debug!("Setting lock_registry to UpdatingNodes");
708                self.lock_registry = Some(LockRegistryState::UpdatingNodes);
709                let action_sender = self.get_actions_sender()?;
710                info!("Got action to update nodes");
711                let _ = self.update_node_items(Some(NodeStatus::Updating));
712                let (service_names, peer_ids) = self.get_service_names_and_peer_ids();
713
714                let upgrade_nodes_args = UpgradeNodesArgs {
715                    action_sender,
716                    connection_timeout_s: 5,
717                    do_not_start: true,
718                    custom_bin_path: None,
719                    force: false,
720                    fixed_interval: Some(FIXED_INTERVAL),
721                    peer_ids,
722                    provided_env_variables: None,
723                    service_names,
724                    url: None,
725                    version: None,
726                };
727                self.node_management
728                    .send_task(NodeManagementTask::UpgradeNodes {
729                        args: upgrade_nodes_args,
730                    })?;
731            }
732            Action::OptionsActions(OptionsActions::ResetNodes) => {
733                debug!("Got action to reset nodes");
734                if self.lock_registry.is_some() {
735                    error!(
736                        "Registry is locked ({:?}) Cannot Reset nodes now.",
737                        self.lock_registry
738                    );
739                    return Ok(None);
740                }
741
742                debug!("Setting lock_registry to ResettingNodes");
743                self.lock_registry = Some(LockRegistryState::ResettingNodes);
744                let action_sender = self.get_actions_sender()?;
745                info!("Got action to reset nodes");
746                self.node_management
747                    .send_task(NodeManagementTask::ResetNodes {
748                        start_nodes_after_reset: false,
749                        action_sender,
750                    })?;
751            }
752            _ => {}
753        }
754        Ok(None)
755    }
756
757    fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
758        if !self.active {
759            return Ok(());
760        }
761
762        let layout = Layout::new(
763            Direction::Vertical,
764            [
765                // Header
766                Constraint::Length(1),
767                // Device status
768                Constraint::Max(6),
769                // Node status
770                Constraint::Min(3),
771                // Footer
772                Constraint::Length(3),
773            ],
774        )
775        .split(area);
776
777        // ==== Header =====
778
779        let header = Header::new();
780        f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Status);
781
782        // ==== Device Status =====
783
784        // Device Status as a block with two tables so we can shrink the screen
785        // and preserve as much as we can information
786
787        let combined_block = Block::default()
788            .title(" Device Status ")
789            .bold()
790            .title_style(Style::default().fg(GHOST_WHITE))
791            .borders(Borders::ALL)
792            .padding(Padding::horizontal(1))
793            .style(Style::default().fg(VERY_LIGHT_AZURE));
794
795        f.render_widget(combined_block.clone(), layout[1]);
796
797        let storage_allocated_row = Row::new(vec![
798            Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE),
799            Cell::new(format!("{} GB", self.nodes_to_start * GB_PER_NODE)).fg(GHOST_WHITE),
800        ]);
801        let memory_use_val = if self.node_stats.total_memory_usage_mb as f64 / 1024_f64 > 1.0 {
802            format!(
803                "{:.2} GB",
804                self.node_stats.total_memory_usage_mb as f64 / 1024_f64
805            )
806        } else {
807            format!("{} MB", self.node_stats.total_memory_usage_mb)
808        };
809
810        let memory_use_row = Row::new(vec![
811            Cell::new("Memory Use".to_string()).fg(GHOST_WHITE),
812            Cell::new(memory_use_val).fg(GHOST_WHITE),
813        ]);
814
815        let connection_mode_string = match self.connection_mode {
816            ConnectionMode::HomeNetwork => "Home Network".to_string(),
817            ConnectionMode::UPnP => "UPnP".to_string(),
818            ConnectionMode::CustomPorts => format!(
819                "Custom Ports  {}-{}",
820                self.port_from.unwrap_or(PORT_MIN),
821                self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION)
822            ),
823            ConnectionMode::Automatic => "Automatic".to_string(),
824        };
825
826        let mut connection_mode_line = vec![Span::styled(
827            connection_mode_string,
828            Style::default().fg(GHOST_WHITE),
829        )];
830
831        if matches!(
832            self.connection_mode,
833            ConnectionMode::Automatic | ConnectionMode::UPnP
834        ) {
835            connection_mode_line.push(Span::styled(" (", Style::default().fg(GHOST_WHITE)));
836
837            if self.connection_mode == ConnectionMode::Automatic {
838                connection_mode_line.push(Span::styled("UPnP: ", Style::default().fg(GHOST_WHITE)));
839            }
840
841            let span = match self.upnp_support {
842                UpnpSupport::Supported => {
843                    Span::styled("supported", Style::default().fg(EUCALYPTUS))
844                }
845                UpnpSupport::Unsupported => {
846                    Span::styled("disabled / unsupported", Style::default().fg(SIZZLING_RED))
847                }
848                UpnpSupport::Loading => {
849                    Span::styled("loading..", Style::default().fg(LIGHT_PERIWINKLE))
850                }
851                UpnpSupport::Unknown => {
852                    Span::styled("unknown", Style::default().fg(LIGHT_PERIWINKLE))
853                }
854            };
855
856            connection_mode_line.push(span);
857
858            connection_mode_line.push(Span::styled(")", Style::default().fg(GHOST_WHITE)));
859        }
860
861        let connection_mode_row = Row::new(vec![
862            Cell::new("Connection".to_string()).fg(GHOST_WHITE),
863            Cell::new(Line::from(connection_mode_line)),
864        ]);
865
866        let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row];
867        let stats_width = [Constraint::Length(5)];
868        let column_constraints = [Constraint::Length(23), Constraint::Fill(1)];
869        let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints);
870
871        let wallet_not_set = if self.rewards_address.is_empty() {
872            vec![
873                Span::styled("Press ".to_string(), Style::default().fg(VIVID_SKY_BLUE)),
874                Span::styled("[Ctrl+B] ".to_string(), Style::default().fg(GHOST_WHITE)),
875                Span::styled(
876                    "to add your ".to_string(),
877                    Style::default().fg(VIVID_SKY_BLUE),
878                ),
879                Span::styled(
880                    "Wallet Address".to_string(),
881                    Style::default().fg(VIVID_SKY_BLUE).bold(),
882                ),
883            ]
884        } else {
885            vec![]
886        };
887
888        let total_attos_earned_and_wallet_row = Row::new(vec![
889            Cell::new("Attos Earned".to_string()).fg(VIVID_SKY_BLUE),
890            Cell::new(format!(
891                "{:?}",
892                self.node_stats.total_rewards_wallet_balance
893            ))
894            .fg(VIVID_SKY_BLUE)
895            .bold(),
896            Cell::new(Line::from(wallet_not_set).alignment(Alignment::Right)),
897        ]);
898
899        let attos_wallet_rows = vec![total_attos_earned_and_wallet_row];
900        let attos_wallet_width = [Constraint::Length(5)];
901        let column_constraints = [
902            Constraint::Length(23),
903            Constraint::Fill(1),
904            Constraint::Length(if self.rewards_address.is_empty() {
905                41 //TODO: make it dynamic with wallet_not_set
906            } else {
907                0
908            }),
909        ];
910        let attos_wallet_table =
911            Table::new(attos_wallet_rows, attos_wallet_width).widths(column_constraints);
912
913        let inner_area = combined_block.inner(layout[1]);
914        let device_layout = Layout::new(
915            Direction::Vertical,
916            vec![Constraint::Length(5), Constraint::Length(1)],
917        )
918        .split(inner_area);
919
920        // Render both tables inside the combined block
921        f.render_widget(stats_table, device_layout[0]);
922        f.render_widget(attos_wallet_table, device_layout[1]);
923
924        // ==== Node Status =====
925
926        // No nodes. Empty Table.
927        if let Some(ref items) = self.items {
928            if items.items.is_empty() || self.rewards_address.is_empty() {
929                let line1 = Line::from(vec![
930                    Span::styled("Press ", Style::default().fg(LIGHT_PERIWINKLE)),
931                    Span::styled("[Ctrl+G] ", Style::default().fg(GHOST_WHITE).bold()),
932                    Span::styled("to Add and ", Style::default().fg(LIGHT_PERIWINKLE)),
933                    Span::styled("Start Nodes ", Style::default().fg(GHOST_WHITE).bold()),
934                    Span::styled("on this device", Style::default().fg(LIGHT_PERIWINKLE)),
935                ]);
936
937                let line2 = Line::from(vec![Span::styled(
938                    format!(
939                        "Each node will use {}GB of storage and a small amount of memory, \
940                        CPU, and Network bandwidth. Most computers can run many nodes at once, \
941                        but we recommend you add them gradually",
942                        GB_PER_NODE
943                    ),
944                    Style::default().fg(LIGHT_PERIWINKLE),
945                )]);
946
947                f.render_widget(
948                    Paragraph::new(vec![Line::raw(""), line1, Line::raw(""), line2])
949                        .wrap(Wrap { trim: false })
950                        .fg(LIGHT_PERIWINKLE)
951                        .block(
952                            Block::default()
953                                .title(Line::from(vec![
954                                    Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
955                                    Span::styled(" (0) ", Style::default().fg(LIGHT_PERIWINKLE)),
956                                ]))
957                                .title_style(Style::default().fg(LIGHT_PERIWINKLE))
958                                .borders(Borders::ALL)
959                                .border_style(style::Style::default().fg(EUCALYPTUS))
960                                .padding(Padding::horizontal(1)),
961                        ),
962                    layout[2],
963                );
964            } else {
965                // Node/s block
966                let block_nodes = Block::default()
967                    .title(Line::from(vec![
968                        Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
969                        Span::styled(
970                            format!(" ({}) ", self.nodes_to_start),
971                            Style::default().fg(LIGHT_PERIWINKLE),
972                        ),
973                    ]))
974                    .padding(Padding::new(1, 1, 0, 0))
975                    .title_style(Style::default().fg(GHOST_WHITE))
976                    .borders(Borders::ALL)
977                    .border_style(Style::default().fg(EUCALYPTUS));
978
979                // Split the inner area of the combined block
980                let inner_area = block_nodes.inner(layout[2]);
981
982                // Column Widths
983                let node_widths = [
984                    Constraint::Min(NODE_WIDTH as u16),
985                    Constraint::Min(VERSION_WIDTH as u16),
986                    Constraint::Min(ATTOS_WIDTH as u16),
987                    Constraint::Min(MEMORY_WIDTH as u16),
988                    Constraint::Min(MBPS_WIDTH as u16),
989                    Constraint::Min(RECORDS_WIDTH as u16),
990                    Constraint::Min(PEERS_WIDTH as u16),
991                    Constraint::Min(CONNS_WIDTH as u16),
992                    Constraint::Min(MODE_WIDTH as u16),
993                    Constraint::Min(STATUS_WIDTH as u16),
994                    Constraint::Fill(FAILURE_WIDTH as u16),
995                    Constraint::Max(SPINNER_WIDTH as u16),
996                ];
997
998                // Header
999                let header_row = Row::new(vec![
1000                    Cell::new("Node").fg(COOL_GREY),
1001                    Cell::new("Version").fg(COOL_GREY),
1002                    Cell::new("Attos").fg(COOL_GREY),
1003                    Cell::new("Memory").fg(COOL_GREY),
1004                    Cell::new(
1005                        format!("{}{}", " ".repeat(MBPS_WIDTH - "Mbps".len()), "Mbps")
1006                            .fg(COOL_GREY),
1007                    ),
1008                    Cell::new("Recs").fg(COOL_GREY),
1009                    Cell::new("Peers").fg(COOL_GREY),
1010                    Cell::new("Conns").fg(COOL_GREY),
1011                    Cell::new("Mode").fg(COOL_GREY),
1012                    Cell::new("Status").fg(COOL_GREY),
1013                    Cell::new("Failure").fg(COOL_GREY),
1014                    Cell::new(" ").fg(COOL_GREY), // Spinner
1015                ])
1016                .style(Style::default().add_modifier(Modifier::BOLD));
1017
1018                let mut items: Vec<Row> = Vec::new();
1019                if let Some(ref mut items_table) = self.items {
1020                    for (i, node_item) in items_table.items.iter_mut().enumerate() {
1021                        let is_selected = items_table.state.selected() == Some(i);
1022                        items.push(node_item.render_as_row(i, layout[2], f, is_selected));
1023                    }
1024                }
1025
1026                // Table items
1027                let table = Table::new(items, node_widths)
1028                    .header(header_row)
1029                    .column_spacing(1)
1030                    .row_highlight_style(Style::default().bg(INDIGO))
1031                    .highlight_spacing(HighlightSpacing::Always);
1032
1033                f.render_widget(table, inner_area);
1034
1035                f.render_widget(block_nodes, layout[2]);
1036            }
1037        }
1038
1039        // ==== Footer =====
1040
1041        let footer = Footer::default();
1042        let footer_state = if let Some(ref items) = self.items {
1043            if !items.items.is_empty() || self.rewards_address.is_empty() {
1044                if !self.get_running_nodes().is_empty() {
1045                    &mut NodesToStart::Running
1046                } else {
1047                    &mut NodesToStart::Configured
1048                }
1049            } else {
1050                &mut NodesToStart::NotConfigured
1051            }
1052        } else {
1053            &mut NodesToStart::NotConfigured
1054        };
1055        f.render_stateful_widget(footer, layout[3], footer_state);
1056
1057        // ===== Popups =====
1058
1059        // Error Popup
1060        if let Some(error_popup) = &self.error_popup {
1061            if error_popup.is_visible() {
1062                error_popup.draw_error(f, area);
1063
1064                return Ok(());
1065            }
1066        }
1067
1068        // Status Popup
1069        if let Some(registry_state) = &self.lock_registry {
1070            let popup_text = match registry_state {
1071                LockRegistryState::StartingNodes => {
1072                    if self.should_we_run_nat_detection() {
1073                        vec![
1074                            Line::raw("Starting nodes..."),
1075                            Line::raw(""),
1076                            Line::raw(""),
1077                            Line::raw("Please wait, performing initial NAT detection"),
1078                            Line::raw("This may take a couple minutes."),
1079                        ]
1080                    } else {
1081                        // We avoid rendering the popup as we have status lines now
1082                        return Ok(());
1083                    }
1084                }
1085                LockRegistryState::StoppingNodes => {
1086                    vec![
1087                        Line::raw(""),
1088                        Line::raw(""),
1089                        Line::raw(""),
1090                        Line::raw("Stopping nodes..."),
1091                    ]
1092                }
1093                LockRegistryState::ResettingNodes => {
1094                    vec![
1095                        Line::raw(""),
1096                        Line::raw(""),
1097                        Line::raw(""),
1098                        Line::raw("Resetting nodes..."),
1099                    ]
1100                }
1101                LockRegistryState::UpdatingNodes => {
1102                    return Ok(());
1103                }
1104            };
1105            if !popup_text.is_empty() {
1106                let popup_area = centered_rect_fixed(50, 12, area);
1107                clear_area(f, popup_area);
1108
1109                let popup_border = Paragraph::new("").block(
1110                    Block::default()
1111                        .borders(Borders::ALL)
1112                        .title(" Manage Nodes ")
1113                        .bold()
1114                        .title_style(Style::new().fg(VIVID_SKY_BLUE))
1115                        .padding(Padding::uniform(2))
1116                        .border_style(Style::new().fg(GHOST_WHITE)),
1117                );
1118
1119                let centred_area = Layout::new(
1120                    Direction::Vertical,
1121                    vec![
1122                        // border
1123                        Constraint::Length(2),
1124                        // our text goes here
1125                        Constraint::Min(5),
1126                        // border
1127                        Constraint::Length(1),
1128                    ],
1129                )
1130                .split(popup_area);
1131                let text = Paragraph::new(popup_text)
1132                    .block(Block::default().padding(Padding::horizontal(2)))
1133                    .wrap(Wrap { trim: false })
1134                    .alignment(Alignment::Center)
1135                    .fg(EUCALYPTUS);
1136                f.render_widget(text, centred_area[1]);
1137
1138                f.render_widget(popup_border, popup_area);
1139            }
1140        }
1141
1142        Ok(())
1143    }
1144
1145    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
1146        debug!("Key received in Status: {:?}", key);
1147        if let Some(error_popup) = &mut self.error_popup {
1148            if error_popup.is_visible() {
1149                error_popup.handle_input(key);
1150                return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]);
1151            }
1152        }
1153        Ok(vec![])
1154    }
1155}
1156
1157#[allow(dead_code)]
1158#[derive(Default, Clone)]
1159struct StatefulTable<T> {
1160    state: TableState,
1161    items: Vec<T>,
1162    last_selected: Option<usize>,
1163}
1164
1165#[allow(dead_code)]
1166impl<T> StatefulTable<T> {
1167    fn with_items(items: Vec<T>) -> Self {
1168        StatefulTable {
1169            state: TableState::default(),
1170            items,
1171            last_selected: None,
1172        }
1173    }
1174
1175    fn next(&mut self) {
1176        let i = match self.state.selected() {
1177            Some(i) => {
1178                if i >= self.items.len() - 1 {
1179                    0
1180                } else {
1181                    i + 1
1182                }
1183            }
1184            None => self.last_selected.unwrap_or(0),
1185        };
1186        self.state.select(Some(i));
1187        self.last_selected = Some(i);
1188    }
1189
1190    fn previous(&mut self) {
1191        let i = match self.state.selected() {
1192            Some(i) => {
1193                if i == 0 {
1194                    self.items.len() - 1
1195                } else {
1196                    i - 1
1197                }
1198            }
1199            None => self.last_selected.unwrap_or(0),
1200        };
1201        self.state.select(Some(i));
1202        self.last_selected = Some(i);
1203    }
1204
1205    fn selected_item(&self) -> Option<&T> {
1206        self.state
1207            .selected()
1208            .and_then(|index| self.items.get(index))
1209    }
1210}
1211
1212#[derive(Default, Debug, Copy, Clone, PartialEq)]
1213enum NodeStatus {
1214    #[default]
1215    Added,
1216    Running,
1217    Starting,
1218    Stopped,
1219    Removed,
1220    Updating,
1221}
1222
1223impl fmt::Display for NodeStatus {
1224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1225        match *self {
1226            NodeStatus::Added => write!(f, "Added"),
1227            NodeStatus::Running => write!(f, "Running"),
1228            NodeStatus::Starting => write!(f, "Starting"),
1229            NodeStatus::Stopped => write!(f, "Stopped"),
1230            NodeStatus::Removed => write!(f, "Removed"),
1231            NodeStatus::Updating => write!(f, "Updating"),
1232        }
1233    }
1234}
1235
1236#[derive(Default, Debug, Clone)]
1237pub struct NodeItem<'a> {
1238    name: String,
1239    version: String,
1240    attos: usize,
1241    memory: usize,
1242    mbps: String,
1243    records: usize,
1244    peers: usize,
1245    connections: usize,
1246    mode: NodeConnectionMode,
1247    status: NodeStatus,
1248    failure: Option<(chrono::DateTime<chrono::Utc>, String)>,
1249    spinner: Throbber<'a>,
1250    spinner_state: ThrobberState,
1251}
1252
1253impl NodeItem<'_> {
1254    fn render_as_row(
1255        &mut self,
1256        index: usize,
1257        area: Rect,
1258        f: &mut Frame<'_>,
1259        is_selected: bool,
1260    ) -> Row {
1261        let mut row_style = if is_selected {
1262            Style::default().fg(GHOST_WHITE).bg(INDIGO)
1263        } else {
1264            Style::default().fg(GHOST_WHITE)
1265        };
1266        let mut spinner_state = self.spinner_state.clone();
1267        match self.status {
1268            NodeStatus::Running => {
1269                self.spinner = self
1270                    .spinner
1271                    .clone()
1272                    .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1273                    .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1274                    .use_type(throbber_widgets_tui::WhichUse::Spin);
1275                row_style = if is_selected {
1276                    Style::default().fg(EUCALYPTUS).bg(INDIGO)
1277                } else {
1278                    Style::default().fg(EUCALYPTUS)
1279                };
1280            }
1281            NodeStatus::Starting => {
1282                self.spinner = self
1283                    .spinner
1284                    .clone()
1285                    .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1286                    .throbber_set(throbber_widgets_tui::BOX_DRAWING)
1287                    .use_type(throbber_widgets_tui::WhichUse::Spin);
1288            }
1289            NodeStatus::Stopped => {
1290                self.spinner = self
1291                    .spinner
1292                    .clone()
1293                    .throbber_style(
1294                        Style::default()
1295                            .fg(GHOST_WHITE)
1296                            .add_modifier(Modifier::BOLD),
1297                    )
1298                    .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1299                    .use_type(throbber_widgets_tui::WhichUse::Full);
1300            }
1301            NodeStatus::Updating => {
1302                self.spinner = self
1303                    .spinner
1304                    .clone()
1305                    .throbber_style(
1306                        Style::default()
1307                            .fg(GHOST_WHITE)
1308                            .add_modifier(Modifier::BOLD),
1309                    )
1310                    .throbber_set(throbber_widgets_tui::VERTICAL_BLOCK)
1311                    .use_type(throbber_widgets_tui::WhichUse::Spin);
1312            }
1313            _ => {}
1314        };
1315
1316        let failure = self.failure.as_ref().map_or_else(
1317            || "-".to_string(),
1318            |(_dt, msg)| {
1319                if self.status == NodeStatus::Stopped {
1320                    msg.clone()
1321                } else {
1322                    "-".to_string()
1323                }
1324            },
1325        );
1326
1327        let row = vec![
1328            self.name.clone().to_string(),
1329            self.version.to_string(),
1330            format!(
1331                "{}{}",
1332                " ".repeat(ATTOS_WIDTH.saturating_sub(self.attos.to_string().len())),
1333                self.attos.to_string()
1334            ),
1335            format!(
1336                "{}{} MB",
1337                " ".repeat(MEMORY_WIDTH.saturating_sub(self.memory.to_string().len() + 4)),
1338                self.memory.to_string()
1339            ),
1340            format!(
1341                "{}{}",
1342                " ".repeat(MBPS_WIDTH.saturating_sub(self.mbps.to_string().len())),
1343                self.mbps.to_string()
1344            ),
1345            format!(
1346                "{}{}",
1347                " ".repeat(RECORDS_WIDTH.saturating_sub(self.records.to_string().len())),
1348                self.records.to_string()
1349            ),
1350            format!(
1351                "{}{}",
1352                " ".repeat(PEERS_WIDTH.saturating_sub(self.peers.to_string().len())),
1353                self.peers.to_string()
1354            ),
1355            format!(
1356                "{}{}",
1357                " ".repeat(CONNS_WIDTH.saturating_sub(self.connections.to_string().len())),
1358                self.connections.to_string()
1359            ),
1360            self.mode.to_string(),
1361            self.status.to_string(),
1362            failure,
1363        ];
1364        let throbber_area = Rect::new(area.width - 3, area.y + 2 + index as u16, 1, 1);
1365
1366        f.render_stateful_widget(self.spinner.clone(), throbber_area, &mut spinner_state);
1367
1368        Row::new(row).style(row_style)
1369    }
1370}