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