node_launchpad/components/popup/
manage_nodes.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 std::path::PathBuf;
10
11use crate::action::OptionsActions;
12use crate::system::get_available_space_b;
13use color_eyre::Result;
14use crossterm::event::{Event, KeyCode, KeyEvent};
15use ratatui::{prelude::*, widgets::*};
16use tui_input::{backend::crossterm::EventHandler, Input};
17
18use crate::{
19    action::Action,
20    mode::{InputMode, Scene},
21    style::{clear_area, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VIVID_SKY_BLUE},
22};
23
24use super::super::{utils::centered_rect_fixed, Component};
25
26pub const GB_PER_NODE: usize = 35;
27pub const MB: usize = 1000 * 1000;
28pub const GB: usize = MB * 1000;
29pub const MAX_NODE_COUNT: usize = 50;
30
31pub struct ManageNodes {
32    /// Whether the component is active right now, capturing keystrokes + drawing things.
33    active: bool,
34    available_disk_space_gb: usize,
35    storage_mountpoint: PathBuf,
36    nodes_to_start_input: Input,
37    // cache the old value incase user presses Esc.
38    old_value: String,
39}
40
41impl ManageNodes {
42    pub fn new(nodes_to_start: usize, storage_mountpoint: PathBuf) -> Result<Self> {
43        let nodes_to_start = std::cmp::min(nodes_to_start, MAX_NODE_COUNT);
44        let new = Self {
45            active: false,
46            available_disk_space_gb: get_available_space_b(&storage_mountpoint)? / GB,
47            nodes_to_start_input: Input::default().with_value(nodes_to_start.to_string()),
48            old_value: Default::default(),
49            storage_mountpoint: storage_mountpoint.clone(),
50        };
51        Ok(new)
52    }
53
54    fn get_nodes_to_start_val(&self) -> usize {
55        self.nodes_to_start_input.value().parse().unwrap_or(0)
56    }
57
58    // Returns the max number of nodes to start
59    // It is the minimum of the available disk space and the max nodes limit
60    fn max_nodes_to_start(&self) -> usize {
61        std::cmp::min(self.available_disk_space_gb / GB_PER_NODE, MAX_NODE_COUNT)
62    }
63}
64
65impl Component for ManageNodes {
66    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
67        if !self.active {
68            return Ok(vec![]);
69        }
70
71        // while in entry mode, key bindings are not captured, so gotta exit entry mode from here
72        let send_back = match key.code {
73            KeyCode::Enter => {
74                let nodes_to_start_str = self.nodes_to_start_input.value().to_string();
75                let nodes_to_start =
76                    std::cmp::min(self.get_nodes_to_start_val(), self.max_nodes_to_start());
77
78                // set the new value
79                self.nodes_to_start_input = self
80                    .nodes_to_start_input
81                    .clone()
82                    .with_value(nodes_to_start.to_string());
83
84                debug!(
85                        "Got Enter, value found to be {nodes_to_start} derived from input: {nodes_to_start_str:?} and switching scene",
86                    );
87                vec![
88                    Action::StoreNodesToStart(nodes_to_start),
89                    Action::SwitchScene(Scene::Status),
90                ]
91            }
92            KeyCode::Esc => {
93                debug!(
94                    "Got Esc, restoring the old value {} and switching to home",
95                    self.old_value
96                );
97                // reset to old value
98                self.nodes_to_start_input = self
99                    .nodes_to_start_input
100                    .clone()
101                    .with_value(self.old_value.clone());
102                vec![Action::SwitchScene(Scene::Status)]
103            }
104            KeyCode::Char(c) if c.is_numeric() => {
105                // don't allow leading zeros
106                if c == '0' && self.nodes_to_start_input.value().is_empty() {
107                    return Ok(vec![]);
108                }
109                let number = c.to_string().parse::<usize>().unwrap_or(0);
110                let new_value = format!("{}{}", self.get_nodes_to_start_val(), number)
111                    .parse::<usize>()
112                    .unwrap_or(0);
113                // if it might exceed the available space or if more than max_node_count, then enter the max
114                if new_value * GB_PER_NODE > self.available_disk_space_gb
115                    || new_value > MAX_NODE_COUNT
116                {
117                    self.nodes_to_start_input = self
118                        .nodes_to_start_input
119                        .clone()
120                        .with_value(self.max_nodes_to_start().to_string());
121                    return Ok(vec![]);
122                }
123                self.nodes_to_start_input.handle_event(&Event::Key(key));
124                vec![]
125            }
126            KeyCode::Backspace => {
127                self.nodes_to_start_input.handle_event(&Event::Key(key));
128                vec![]
129            }
130            KeyCode::Up | KeyCode::Down => {
131                let nodes_to_start = {
132                    let current_val = self.get_nodes_to_start_val();
133
134                    if key.code == KeyCode::Up {
135                        if current_val + 1 >= MAX_NODE_COUNT {
136                            MAX_NODE_COUNT
137                        } else if (current_val + 1) * GB_PER_NODE <= self.available_disk_space_gb {
138                            current_val + 1
139                        } else {
140                            current_val
141                        }
142                    } else {
143                        // Key::Down
144                        if current_val == 0 {
145                            0
146                        } else {
147                            current_val - 1
148                        }
149                    }
150                };
151                // set the new value
152                self.nodes_to_start_input = self
153                    .nodes_to_start_input
154                    .clone()
155                    .with_value(nodes_to_start.to_string());
156                vec![]
157            }
158            _ => {
159                vec![]
160            }
161        };
162        Ok(send_back)
163    }
164
165    fn update(&mut self, action: Action) -> Result<Option<Action>> {
166        let send_back = match action {
167            Action::SwitchScene(scene) => match scene {
168                Scene::ManageNodesPopUp { amount_of_nodes } => {
169                    self.nodes_to_start_input = self
170                        .nodes_to_start_input
171                        .clone()
172                        .with_value(amount_of_nodes.to_string());
173                    self.active = true;
174                    self.old_value = self.nodes_to_start_input.value().to_string();
175                    // set to entry input mode as we want to handle everything within our handle_key_events
176                    // so by default if this scene is active, we capture inputs.
177                    Some(Action::SwitchInputMode(InputMode::Entry))
178                }
179                _ => {
180                    self.active = false;
181                    None
182                }
183            },
184            Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, _drive_name)) => {
185                self.storage_mountpoint.clone_from(&mountpoint);
186                self.available_disk_space_gb = get_available_space_b(&mountpoint)? / GB;
187                None
188            }
189            _ => None,
190        };
191        Ok(send_back)
192    }
193
194    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
195        if !self.active {
196            return Ok(());
197        }
198
199        let layer_zero = centered_rect_fixed(52, 15, area);
200        let layer_one = Layout::new(
201            Direction::Vertical,
202            [
203                // for the pop_up_border
204                Constraint::Length(2),
205                // for the input field
206                Constraint::Length(1),
207                // for the info field telling how much gb used
208                Constraint::Length(1),
209                // gap before help
210                Constraint::Length(1),
211                // for the help
212                Constraint::Length(7),
213                // for the dash
214                Constraint::Min(1),
215                // for the buttons
216                Constraint::Length(1),
217                // for the pop_up_border
218                Constraint::Length(1),
219            ],
220        )
221        .split(layer_zero);
222        let pop_up_border = Paragraph::new("").block(
223            Block::default()
224                .borders(Borders::ALL)
225                .title(" Manage Nodes ")
226                .bold()
227                .title_style(Style::new().fg(GHOST_WHITE))
228                .title_style(Style::new().fg(EUCALYPTUS))
229                .padding(Padding::uniform(2))
230                .border_style(Style::new().fg(EUCALYPTUS)),
231        );
232        clear_area(f, layer_zero);
233
234        // ==== input field ====
235        let layer_input_field = Layout::new(
236            Direction::Horizontal,
237            [
238                // for the gap
239                Constraint::Min(5),
240                // Start
241                Constraint::Length(5),
242                // Input box
243                Constraint::Length(5),
244                // Nodes(s)
245                Constraint::Length(8),
246                // gap
247                Constraint::Min(5),
248            ],
249        )
250        .split(layer_one[1]);
251
252        let start = Paragraph::new("Start ").style(Style::default().fg(GHOST_WHITE));
253        f.render_widget(start, layer_input_field[1]);
254
255        let width = layer_input_field[2].width.max(3) - 3;
256        let scroll = self.nodes_to_start_input.visual_scroll(width as usize);
257        let input = Paragraph::new(self.get_nodes_to_start_val().to_string())
258            .style(Style::new().fg(VIVID_SKY_BLUE))
259            .scroll((0, scroll as u16))
260            .alignment(Alignment::Center);
261
262        f.render_widget(input, layer_input_field[2]);
263
264        let nodes_text = Paragraph::new("Node(s)").fg(GHOST_WHITE);
265        f.render_widget(nodes_text, layer_input_field[3]);
266
267        // ==== info field ====
268        let available_space_gb = self.available_disk_space_gb;
269        let info_style = Style::default().fg(VIVID_SKY_BLUE);
270        let info = Line::from(vec![
271            Span::styled("Using", info_style),
272            Span::styled(
273                format!(" {}GB ", self.get_nodes_to_start_val() * GB_PER_NODE),
274                info_style.bold(),
275            ),
276            Span::styled(
277                format!("of {available_space_gb}GB available space"),
278                info_style,
279            ),
280        ]);
281        let info = Paragraph::new(info).alignment(Alignment::Center);
282        f.render_widget(info, layer_one[2]);
283
284        // ==== help ====
285        let help = Paragraph::new(vec![
286            Line::raw(format!(
287                "Note: Each node will use a small amount of CPU Memory and Network Bandwidth. \
288                 We recommend starting no more than 2 at a time (max {MAX_NODE_COUNT} nodes)."
289            )),
290            Line::raw(""),
291            Line::raw("▲▼ to change the number of nodes to start."),
292        ])
293        .wrap(Wrap { trim: false })
294        .block(Block::default().padding(Padding::horizontal(4)))
295        .alignment(Alignment::Center)
296        .fg(GHOST_WHITE);
297        f.render_widget(help, layer_one[4]);
298
299        // ==== dash ====
300        let dash = Block::new()
301            .borders(Borders::BOTTOM)
302            .border_style(Style::new().fg(GHOST_WHITE));
303        f.render_widget(dash, layer_one[5]);
304
305        // ==== buttons ====
306        let buttons_layer =
307            Layout::horizontal(vec![Constraint::Percentage(45), Constraint::Percentage(55)])
308                .split(layer_one[6]);
309
310        let button_no = Line::from(vec![Span::styled(
311            "  Close [Esc]",
312            Style::default().fg(LIGHT_PERIWINKLE),
313        )]);
314        f.render_widget(button_no, buttons_layer[0]);
315        let button_yes = Line::from(vec![Span::styled(
316            "Start Node(s) [Enter]  ",
317            Style::default().fg(EUCALYPTUS),
318        )]);
319        let button_yes = Paragraph::new(button_yes).alignment(Alignment::Right);
320        f.render_widget(button_yes, buttons_layer[1]);
321
322        f.render_widget(pop_up_border, layer_zero);
323
324        Ok(())
325    }
326}