From fbc5293b5b2ec104eeff343fb7787415293cec4d Mon Sep 17 00:00:00 2001 From: Roland Sherwin Date: Tue, 4 Jun 2024 19:39:03 +0530 Subject: [PATCH] feat(launchpad): enable user to reset nodes --- node-launchpad/.config/config.json5 | 5 +- node-launchpad/src/action.rs | 4 +- node-launchpad/src/app.rs | 4 +- node-launchpad/src/components.rs | 1 + node-launchpad/src/components/footer.rs | 6 +- node-launchpad/src/components/home.rs | 39 +++- node-launchpad/src/components/reset_popup.rs | 193 +++++++++++++++++++ node-launchpad/src/mode.rs | 1 + 8 files changed, 240 insertions(+), 13 deletions(-) create mode 100644 node-launchpad/src/components/reset_popup.rs diff --git a/node-launchpad/.config/config.json5 b/node-launchpad/.config/config.json5 index 2a312b2215..bc1857fbfc 100644 --- a/node-launchpad/.config/config.json5 +++ b/node-launchpad/.config/config.json5 @@ -20,11 +20,12 @@ "": {"HomeActions":"TriggerManageNodes"}, "": {"HomeActions":"TriggerManageNodes"}, "": {"HomeActions":"TriggerManageNodes"}, - "": {"HomeActions":"TriggerManageNodes"}, "": {"HomeActions":"TriggerHelp"}, "": {"HomeActions":"TriggerHelp"}, "": {"HomeActions":"TriggerHelp"}, - + "": {"HomeActions":"TriggerResetNodesPopUp"}, + "": {"HomeActions":"TriggerResetNodesPopUp"}, + "": {"HomeActions":"TriggerResetNodesPopUp"}, "": "Quit", "": "Quit", diff --git a/node-launchpad/src/action.rs b/node-launchpad/src/action.rs index 2b63773da4..6ddb8c5ed9 100644 --- a/node-launchpad/src/action.rs +++ b/node-launchpad/src/action.rs @@ -36,11 +36,12 @@ pub enum Action { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Display, Deserialize)] pub enum HomeActions { + ResetNodes, StartNodes, StopNodes, StartNodesCompleted, StopNodesCompleted, - ResetNodesCompleted, + ResetNodesCompleted { trigger_start_node: bool }, SuccessfullyDetectedNatStatus, ErrorWhileRunningNatDetection, @@ -49,6 +50,7 @@ pub enum HomeActions { TriggerBetaProgramme, TriggerManageNodes, TriggerHelp, + TriggerResetNodesPopUp, PreviousTableItem, NextTableItem, diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index 54319ef901..924c7e8e06 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -12,7 +12,7 @@ use crate::{ action::Action, components::{ beta_programme::BetaProgramme, footer::Footer, help::HelpPopUp, home::Home, - manage_nodes::ManageNodes, Component, + manage_nodes::ManageNodes, reset_popup::ResetNodesPopup, Component, }, config::{AppData, Config}, mode::{InputMode, Scene}, @@ -58,6 +58,7 @@ impl App { let manage_nodes = ManageNodes::new(app_data.nodes_to_start)?; let footer = Footer::new(app_data.nodes_to_start > 0); let help = HelpPopUp::default(); + let reset_nodes = ResetNodesPopup::default(); Ok(Self { config, @@ -70,6 +71,7 @@ impl App { Box::new(discord_username_input), Box::new(manage_nodes), Box::new(help), + Box::new(reset_nodes), ], should_quit: false, should_suspend: false, diff --git a/node-launchpad/src/components.rs b/node-launchpad/src/components.rs index 0ab3f8b364..71534dca28 100644 --- a/node-launchpad/src/components.rs +++ b/node-launchpad/src/components.rs @@ -23,6 +23,7 @@ pub mod help; pub mod home; pub mod manage_nodes; pub mod options; +pub mod reset_popup; pub mod tab; pub mod utils; diff --git a/node-launchpad/src/components/footer.rs b/node-launchpad/src/components/footer.rs index 2c5b6b57c1..6022220bfe 100644 --- a/node-launchpad/src/components/footer.rs +++ b/node-launchpad/src/components/footer.rs @@ -89,7 +89,11 @@ impl Component for Footer { }; let (line1, line2) = match self.current_scene { - Scene::Home | Scene::BetaProgramme | Scene::HelpPopUp | Scene::ManageNodes => { + Scene::Home + | Scene::BetaProgramme + | Scene::HelpPopUp + | Scene::ManageNodes + | Scene::ResetPopUp => { let line1 = Line::from(vec![ Span::styled(" [Ctrl+S] ", command_style), Span::styled("Start all Nodes ", text_style), diff --git a/node-launchpad/src/components/home.rs b/node-launchpad/src/components/home.rs index 4fed5fe3a9..a857a8993e 100644 --- a/node-launchpad/src/components/home.rs +++ b/node-launchpad/src/components/home.rs @@ -218,7 +218,10 @@ impl Component for Home { // make sure we're in navigation mode return Ok(Some(Action::SwitchInputMode(InputMode::Navigation))); } - Scene::BetaProgramme | Scene::ManageNodes | Scene::HelpPopUp => self.active = true, + Scene::BetaProgramme + | Scene::ManageNodes + | Scene::HelpPopUp + | Scene::ResetPopUp => self.active = true, _ => self.active = false, }, Action::StoreNodesToStart(count) => { @@ -243,7 +246,7 @@ impl Component for Home { self.lock_registry = Some(LockRegistryState::ResettingNodes); info!("Resetting safenode services because the discord username was reset."); let action_sender = self.get_actions_sender()?; - reset_nodes(action_sender); + reset_nodes(action_sender, true); } } Action::HomeActions(HomeActions::StartNodes) => { @@ -283,6 +286,18 @@ impl Component for Home { stop_nodes(running_nodes, action_sender); } + Action::HomeActions(HomeActions::ResetNodes) => { + if self.lock_registry.is_some() { + error!("Registry is locked. Cannot reset nodes now."); + return Ok(None); + } + + self.lock_registry = Some(LockRegistryState::ResettingNodes); + let action_sender = self.get_actions_sender()?; + info!("Got action to reset nodes"); + reset_nodes(action_sender, false); + } + Action::Tick => { self.try_update_node_stats(false)?; } @@ -294,12 +309,15 @@ impl Component for Home { self.lock_registry = None; self.load_node_registry_and_update_states()?; } - Action::HomeActions(HomeActions::ResetNodesCompleted) => { + Action::HomeActions(HomeActions::ResetNodesCompleted { trigger_start_node }) => { self.lock_registry = None; self.load_node_registry_and_update_states()?; - // trigger start nodes. - return Ok(Some(Action::HomeActions(HomeActions::StartNodes))); + if trigger_start_node { + debug!("Reset nodes completed. Triggering start nodes."); + return Ok(Some(Action::HomeActions(HomeActions::StartNodes))); + } + debug!("Reset nodes completed"); } Action::HomeActions(HomeActions::SuccessfullyDetectedNatStatus) => { debug!("Successfully detected nat status, is_nat_status_determined set to true"); @@ -322,7 +340,9 @@ impl Component for Home { Action::HomeActions(HomeActions::TriggerHelp) => { return Ok(Some(Action::SwitchScene(Scene::HelpPopUp))); } - + Action::HomeActions(HomeActions::TriggerResetNodesPopUp) => { + return Ok(Some(Action::SwitchScene(Scene::ResetPopUp))); + } Action::HomeActions(HomeActions::PreviousTableItem) => { self.select_previous_table_item(); } @@ -650,14 +670,17 @@ fn maintain_n_running_nodes( }); } -fn reset_nodes(action_sender: UnboundedSender) { +fn reset_nodes(action_sender: UnboundedSender, start_nodes_after_reset: bool) { tokio::task::spawn_local(async move { if let Err(err) = sn_node_manager::cmd::node::reset(true, VerbosityLevel::Minimal).await { error!("Error while resetting services {err:?}"); } else { info!("Successfully reset services"); } - if let Err(err) = action_sender.send(Action::HomeActions(HomeActions::ResetNodesCompleted)) + if let Err(err) = + action_sender.send(Action::HomeActions(HomeActions::ResetNodesCompleted { + trigger_start_node: start_nodes_after_reset, + })) { error!("Error while sending action: {err:?}"); } diff --git a/node-launchpad/src/components/reset_popup.rs b/node-launchpad/src/components/reset_popup.rs new file mode 100644 index 0000000000..4cc408c4ac --- /dev/null +++ b/node-launchpad/src/components/reset_popup.rs @@ -0,0 +1,193 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use super::{utils::centered_rect_fixed, Component}; +use crate::{ + action::{Action, HomeActions}, + mode::{InputMode, Scene}, + style::{clear_area, GHOST_WHITE, LIGHT_PERIWINKLE, VIVID_SKY_BLUE}, +}; +use color_eyre::Result; +use crossterm::event::{Event, KeyCode, KeyEvent}; +use ratatui::{prelude::*, widgets::*}; +use tui_input::{backend::crossterm::EventHandler, Input}; + +#[derive(Default)] +pub struct ResetNodesPopup { + /// Whether the component is active right now, capturing keystrokes + draw things. + active: bool, + confirmation_input_field: Input, +} + +impl Component for ResetNodesPopup { + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + if !self.active { + return Ok(vec![]); + } + let send_back = match key.code { + KeyCode::Enter => { + let input = self.confirmation_input_field.value().to_string(); + + if input.to_lowercase() == "reset" { + debug!("Got reset, sending Reset action and switching to home"); + vec![ + Action::SwitchScene(Scene::Home), + Action::HomeActions(HomeActions::ResetNodes), + ] + } else { + debug!("Got Enter, but RESET is not typed. Switching to home"); + vec![Action::SwitchScene(Scene::Home)] + } + } + KeyCode::Esc => { + debug!("Got Esc, switching to home"); + vec![Action::SwitchScene(Scene::Home)] + } + KeyCode::Char(' ') => vec![], + KeyCode::Backspace => { + // if max limit reached, we should allow Backspace to work. + self.confirmation_input_field.handle_event(&Event::Key(key)); + vec![] + } + _ => { + // max char limit + if self.confirmation_input_field.value().chars().count() < 10 { + self.confirmation_input_field.handle_event(&Event::Key(key)); + } + vec![] + } + }; + Ok(send_back) + } + + fn update(&mut self, action: Action) -> Result> { + let send_back = match action { + Action::SwitchScene(scene) => match scene { + Scene::ResetPopUp => { + self.active = true; + self.confirmation_input_field = self + .confirmation_input_field + .clone() + .with_value(String::new()); + // set to entry input mode as we want to handle everything within our handle_key_events + // so by default if this scene is active, we capture inputs. + Some(Action::SwitchInputMode(InputMode::Entry)) + } + _ => { + self.active = false; + None + } + }, + _ => None, + }; + Ok(send_back) + } + + fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { + if !self.active { + return Ok(()); + } + + let layer_zero = centered_rect_fixed(52, 15, area); + + let layer_one = Layout::new( + Direction::Vertical, + [ + // for the pop_up_border + Constraint::Length(2), + // for the input field + Constraint::Min(1), + // for the pop_up_border + Constraint::Length(1), + ], + ) + .split(layer_zero); + + // layer zero + let pop_up_border = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title("Reset Nodes") + .title_style(Style::new().fg(VIVID_SKY_BLUE)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(VIVID_SKY_BLUE)), + ); + clear_area(f, layer_zero); + + // split into 4 parts, for the prompt, input, text, dash , and buttons + let layer_two = Layout::new( + Direction::Vertical, + [ + // for the prompt text + Constraint::Length(4), + // for the input + Constraint::Length(2), + // for the text + Constraint::Length(3), + // gap + Constraint::Length(3), + // for the buttons + Constraint::Length(1), + ], + ) + .split(layer_one[1]); + + let prompt = Paragraph::new("Type in 'reset' and press Enter to Reset all your nodes") + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center) + .fg(GHOST_WHITE); + + f.render_widget(prompt, layer_two[0]); + + let input = Paragraph::new(self.confirmation_input_field.value()) + .alignment(Alignment::Center) + .fg(VIVID_SKY_BLUE); + f.set_cursor( + // Put cursor past the end of the input text + layer_two[1].x + + (layer_two[1].width / 2) as u16 + + (self.confirmation_input_field.value().len() / 2) as u16 + + if self.confirmation_input_field.value().len() % 2 != 0 { + 1 + } else { + 0 + }, + layer_two[1].y, + ); + f.render_widget(input, layer_two[1]); + + let text = Paragraph::new(" This will clear out all the nodes and all \n the stored data. You should still keep all\n your earned rewards."); + f.render_widget(text.fg(GHOST_WHITE), layer_two[2]); + + let dash = Block::new() + .borders(Borders::BOTTOM) + .border_style(Style::new().fg(GHOST_WHITE)); + f.render_widget(dash, layer_two[3]); + + let buttons_layer = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layer_two[4]); + + let button_no = Line::from(vec![Span::styled( + " No, Cancel [Esc]", + Style::default().fg(LIGHT_PERIWINKLE), + )]); + + f.render_widget(button_no, buttons_layer[0]); + + let button_yes = Line::from(vec![Span::styled( + "Reset Nodes [Enter]", + Style::default().fg(LIGHT_PERIWINKLE), + )]); + f.render_widget(button_yes, buttons_layer[1]); + + f.render_widget(pop_up_border, layer_zero); + + Ok(()) + } +} diff --git a/node-launchpad/src/mode.rs b/node-launchpad/src/mode.rs index e198ee9acc..3968c059a0 100644 --- a/node-launchpad/src/mode.rs +++ b/node-launchpad/src/mode.rs @@ -16,6 +16,7 @@ pub enum Scene { BetaProgramme, ManageNodes, HelpPopUp, + ResetPopUp, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]