node_launchpad/components/popup/
port_range.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::rc::Rc;
10
11use super::super::super::node_mgmt::{PORT_MAX, PORT_MIN};
12use super::super::utils::centered_rect_fixed;
13use super::super::Component;
14use super::manage_nodes::MAX_NODE_COUNT;
15use crate::style::RED;
16use crate::{
17    action::{Action, OptionsActions},
18    connection_mode::ConnectionMode,
19    mode::{InputMode, Scene},
20    style::{clear_area, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, VIVID_SKY_BLUE},
21};
22use color_eyre::Result;
23use crossterm::event::{Event, KeyCode, KeyEvent};
24use ratatui::{prelude::*, widgets::*};
25use tui_input::{backend::crossterm::EventHandler, Input};
26
27pub const PORT_ALLOCATION: u32 = MAX_NODE_COUNT as u32 - 1; // We count the port_from as well
28const INPUT_SIZE: u32 = 5;
29const INPUT_AREA: u32 = INPUT_SIZE + 2; // +2 for the left and right padding
30
31#[derive(Default)]
32enum PortRangeState {
33    #[default]
34    Selection,
35    ConfirmChange,
36    PortForwardingInfo,
37}
38
39pub struct PortRangePopUp {
40    active: bool,
41    state: PortRangeState,
42    connection_mode: ConnectionMode,
43    connection_mode_old_value: Option<ConnectionMode>,
44    port_from: Input,
45    port_to: Input,
46    port_from_old_value: u32,
47    port_to_old_value: u32,
48    can_save: bool,
49    first_stroke: bool,
50}
51
52impl PortRangePopUp {
53    pub fn new(connection_mode: ConnectionMode, port_from: u32, port_to: u32) -> Self {
54        Self {
55            active: false,
56            state: PortRangeState::Selection,
57            connection_mode,
58            connection_mode_old_value: None,
59            port_from: Input::default().with_value(port_from.to_string()),
60            port_to: Input::default().with_value(port_to.to_string()),
61            port_from_old_value: Default::default(),
62            port_to_old_value: Default::default(),
63            can_save: false,
64            first_stroke: true,
65        }
66    }
67
68    pub fn validate(&mut self) {
69        if self.port_from.value().is_empty() {
70            self.can_save = false;
71        } else {
72            let port_from: u32 = self.port_from.value().parse().unwrap_or_default();
73            let port_to: u32 = self.port_to.value().parse().unwrap_or_default();
74            self.can_save = (PORT_MIN..=PORT_MAX).contains(&port_from)
75                && (PORT_MIN..=PORT_MAX).contains(&port_to)
76                && port_from <= port_to;
77        }
78    }
79
80    // -- Draw functions --
81
82    // Draws the Port Selection screen
83    fn draw_selection_state(
84        &mut self,
85        f: &mut crate::tui::Frame<'_>,
86        layer_zero: Rect,
87        layer_one: Rc<[Rect]>,
88    ) -> Paragraph {
89        // layer zero
90        let pop_up_border = Paragraph::new("").block(
91            Block::default()
92                .borders(Borders::ALL)
93                .title(" Custom Ports ")
94                .bold()
95                .title_style(Style::new().fg(VIVID_SKY_BLUE))
96                .padding(Padding::uniform(2))
97                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
98        );
99        clear_area(f, layer_zero);
100
101        // split into 4 parts, for the prompt, input, text, dash , and buttons
102        let layer_two = Layout::new(
103            Direction::Vertical,
104            [
105                // for the prompt text
106                Constraint::Length(3),
107                // for the input
108                Constraint::Length(2),
109                // for the text
110                Constraint::Length(3),
111                // gap
112                Constraint::Length(3),
113                // for the buttons
114                Constraint::Length(1),
115            ],
116        )
117        .split(layer_one[1]);
118
119        let prompt = Paragraph::new("Enter Port Number")
120            .bold()
121            .alignment(Alignment::Center);
122
123        f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]);
124
125        let spaces_from = " ".repeat((INPUT_AREA - 1) as usize - self.port_from.value().len());
126
127        let input_line = Line::from(vec![
128            Span::styled(
129                format!("{}{} ", spaces_from, self.port_from.value()),
130                Style::default()
131                    .fg(if self.can_save { VIVID_SKY_BLUE } else { RED })
132                    .bg(INDIGO)
133                    .underlined(),
134            ),
135            Span::styled(" to ", Style::default().fg(GHOST_WHITE)),
136            Span::styled(self.port_to.value(), Style::default().fg(LIGHT_PERIWINKLE)),
137        ])
138        .alignment(Alignment::Center);
139
140        f.render_widget(input_line, layer_two[1]);
141
142        let text = Paragraph::new(vec![
143            Line::from(Span::styled(
144                format!(
145                    "Choose the start of the range of {} ports.",
146                    PORT_ALLOCATION + 1
147                ),
148                Style::default().fg(LIGHT_PERIWINKLE),
149            )),
150            Line::from(Span::styled(
151                format!("This must be between {} and {}.", PORT_MIN, PORT_MAX),
152                Style::default().fg(if self.can_save { LIGHT_PERIWINKLE } else { RED }),
153            )),
154        ])
155        .block(block::Block::default().padding(Padding::horizontal(2)))
156        .alignment(Alignment::Center)
157        .wrap(Wrap { trim: true });
158        f.render_widget(text.fg(GHOST_WHITE), layer_two[2]);
159
160        let dash = Block::new()
161            .borders(Borders::BOTTOM)
162            .border_style(Style::new().fg(GHOST_WHITE));
163        f.render_widget(dash, layer_two[3]);
164
165        let buttons_layer =
166            Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
167                .split(layer_two[4]);
168
169        let button_no = Line::from(vec![Span::styled(
170            "  Cancel [Esc]",
171            Style::default().fg(LIGHT_PERIWINKLE),
172        )]);
173        let button_yes_style = if self.can_save {
174            Style::default().fg(EUCALYPTUS)
175        } else {
176            Style::default().fg(LIGHT_PERIWINKLE)
177        };
178        f.render_widget(button_no, buttons_layer[0]);
179
180        let button_yes = Line::from(vec![
181            Span::styled("Save Port Range ", button_yes_style),
182            Span::styled("[Enter]", Style::default().fg(GHOST_WHITE)),
183        ]);
184        f.render_widget(button_yes, buttons_layer[1]);
185
186        pop_up_border
187    }
188
189    // Draws Confirmation screen
190    fn draw_confirm_and_reset(
191        &mut self,
192        f: &mut crate::tui::Frame<'_>,
193        layer_zero: Rect,
194        layer_one: Rc<[Rect]>,
195    ) -> Paragraph {
196        // layer zero
197        let pop_up_border = Paragraph::new("").block(
198            Block::default()
199                .borders(Borders::ALL)
200                .title(" Confirm & Reset ")
201                .bold()
202                .title_style(Style::new().fg(VIVID_SKY_BLUE))
203                .padding(Padding::uniform(2))
204                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
205        );
206        clear_area(f, layer_zero);
207
208        // split into 3 parts, paragraph, dash, buttons
209        let layer_two = Layout::new(
210            Direction::Vertical,
211            [
212                // for the text
213                Constraint::Length(8),
214                // gap
215                Constraint::Length(3),
216                // for the buttons
217                Constraint::Length(1),
218            ],
219        )
220        .split(layer_one[1]);
221
222        let paragraph_text = Paragraph::new(vec![
223            Line::from(Span::styled("\n\n", Style::default())),
224            Line::from(Span::styled("\n\n", Style::default())),
225            Line::from(vec![
226                Span::styled(
227                    "Changing connection mode will ",
228                    Style::default().fg(LIGHT_PERIWINKLE),
229                ),
230                Span::styled("reset all nodes.", Style::default().fg(GHOST_WHITE)),
231            ]),
232            Line::from(Span::styled("\n\n", Style::default())),
233            Line::from(Span::styled("\n\n", Style::default())),
234            Line::from(Span::styled("\n\n", Style::default())),
235            Line::from(vec![
236                Span::styled("You’ll need to ", Style::default().fg(LIGHT_PERIWINKLE)),
237                Span::styled("Add", Style::default().fg(GHOST_WHITE)),
238                Span::styled(" and ", Style::default().fg(LIGHT_PERIWINKLE)),
239                Span::styled("Start", Style::default().fg(GHOST_WHITE)),
240                Span::styled(
241                    " them again afterwards. Are you sure you want to continue?",
242                    Style::default().fg(LIGHT_PERIWINKLE),
243                ),
244            ]),
245        ])
246        .alignment(Alignment::Left)
247        .wrap(Wrap { trim: true })
248        .block(block::Block::default().padding(Padding::horizontal(2)));
249
250        f.render_widget(paragraph_text, layer_two[0]);
251
252        let dash = Block::new()
253            .borders(Borders::BOTTOM)
254            .border_style(Style::new().fg(GHOST_WHITE));
255        f.render_widget(dash, layer_two[1]);
256
257        let buttons_layer =
258            Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
259                .split(layer_two[2]);
260
261        let button_no = Line::from(vec![Span::styled(
262            "  Cancel [Esc]",
263            Style::default().fg(LIGHT_PERIWINKLE),
264        )]);
265        let button_yes_style = if self.can_save {
266            Style::default().fg(EUCALYPTUS)
267        } else {
268            Style::default().fg(LIGHT_PERIWINKLE)
269        };
270        f.render_widget(button_no, buttons_layer[0]);
271
272        let button_yes = Line::from(vec![
273            Span::styled("Yes, Change Mode ", button_yes_style),
274            Span::styled("[Enter]", Style::default().fg(GHOST_WHITE)),
275        ]);
276        f.render_widget(button_yes, buttons_layer[1]);
277
278        pop_up_border
279    }
280
281    // Draws info regarding router and ports
282    fn draw_info_port_forwarding(
283        &mut self,
284        f: &mut crate::tui::Frame<'_>,
285        layer_zero: Rect,
286        layer_one: Rc<[Rect]>,
287    ) -> Paragraph {
288        // layer zero
289        let pop_up_border = Paragraph::new("").block(
290            Block::default()
291                .borders(Borders::ALL)
292                .title(" Port Forwarding For Private IPs ")
293                .bold()
294                .title_style(Style::new().fg(VIVID_SKY_BLUE))
295                .padding(Padding::uniform(2))
296                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
297        );
298        clear_area(f, layer_zero);
299
300        // split into 3 parts, 1 paragraph, dash and buttons
301        let layer_two = Layout::new(
302            Direction::Vertical,
303            [
304                // for the text
305                Constraint::Length(8),
306                // gap
307                Constraint::Length(3),
308                // for the buttons
309                Constraint::Length(1),
310            ],
311        )
312        .split(layer_one[1]);
313
314        let paragraph_text = Paragraph::new(vec![
315            Line::from(Span::styled("\n\n",Style::default())),
316            Line::from(Span::styled("If you have a Private IP (which you probably do) you’ll now need to set your router to…\n\n", Style::default().fg(LIGHT_PERIWINKLE))),
317            Line::from(Span::styled("\n\n",Style::default())),
318            Line::from(Span::styled(
319                format!("Port Forward ports {}-{} ", self.port_from.value(), self.port_to.value()),
320                Style::default().fg(GHOST_WHITE),
321            )),
322            Line::from(Span::styled("\n\n",Style::default())),
323            Line::from(Span::styled("You can do this in your router’s admin panel.\n\n", Style::default().fg(LIGHT_PERIWINKLE))),
324        ])
325        .alignment(Alignment::Left)
326        .wrap(Wrap { trim: true })
327        .block(block::Block::default().padding(Padding::horizontal(2)));
328
329        f.render_widget(paragraph_text, layer_two[0]);
330
331        let dash = Block::new()
332            .borders(Borders::BOTTOM)
333            .border_style(Style::new().fg(GHOST_WHITE));
334        f.render_widget(dash, layer_two[1]);
335
336        let buttons_layer =
337            Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
338                .split(layer_two[2]);
339
340        let button_ok = Line::from(vec![
341            Span::styled("OK ", Style::default().fg(EUCALYPTUS)),
342            Span::styled("[Enter]   ", Style::default().fg(GHOST_WHITE)),
343        ])
344        .alignment(Alignment::Right);
345
346        f.render_widget(button_ok, buttons_layer[1]);
347
348        pop_up_border
349    }
350}
351
352impl Component for PortRangePopUp {
353    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
354        if !self.active {
355            return Ok(vec![]);
356        }
357        // while in entry mode, keybinds are not captured, so gotta exit entry mode from here
358        let send_back: Vec<Action> = match &self.state {
359            PortRangeState::Selection => {
360                match key.code {
361                    KeyCode::Enter => {
362                        if self.port_from_old_value
363                            == self.port_from.value().parse::<u32>().unwrap_or_default()
364                            && self.port_to_old_value
365                                == self.port_to.value().parse::<u32>().unwrap_or_default()
366                            && self.connection_mode_old_value != Some(ConnectionMode::CustomPorts)
367                            && self.can_save
368                        {
369                            self.state = PortRangeState::ConfirmChange;
370                            return Ok(vec![]);
371                        }
372                        let port_from = self.port_from.value();
373                        let port_to = self.port_to.value();
374
375                        if port_from.is_empty() || port_to.is_empty() || !self.can_save {
376                            debug!("Got Enter, but port_from or port_to is empty, ignoring.");
377                            return Ok(vec![]);
378                        }
379                        debug!("Got Enter, saving the ports and switching to Options Screen",);
380                        self.state = PortRangeState::ConfirmChange;
381                        vec![]
382                    }
383                    KeyCode::Esc => {
384                        debug!("Got Esc, restoring the old values and switching to actual screen");
385                        if let Some(connection_mode_old_value) = self.connection_mode_old_value {
386                            debug!("{:?}", connection_mode_old_value);
387                            vec![
388                                Action::OptionsActions(OptionsActions::UpdateConnectionMode(
389                                    connection_mode_old_value,
390                                )),
391                                Action::SwitchScene(Scene::Options),
392                            ]
393                        } else {
394                            // if the old values are 0 means that is the first time the user opens the app,
395                            // so we should set the connection mode to automatic.
396                            if self.port_from_old_value.to_string() == "0"
397                                && self.port_to_old_value.to_string() == "0"
398                            {
399                                self.connection_mode = self
400                                    .connection_mode_old_value
401                                    .unwrap_or(ConnectionMode::Automatic);
402                                return Ok(vec![
403                                    Action::StoreConnectionMode(self.connection_mode),
404                                    Action::OptionsActions(OptionsActions::UpdateConnectionMode(
405                                        self.connection_mode,
406                                    )),
407                                    Action::SwitchScene(Scene::Options),
408                                ]);
409                            }
410                            self.port_from = self
411                                .port_from
412                                .clone()
413                                .with_value(self.port_from_old_value.to_string());
414                            self.port_to = self
415                                .port_to
416                                .clone()
417                                .with_value(self.port_to_old_value.to_string());
418                            vec![Action::SwitchScene(Scene::Options)]
419                        }
420                    }
421                    KeyCode::Char(c) if !c.is_numeric() => vec![],
422                    KeyCode::Up => {
423                        if self.port_from.value().parse::<u32>().unwrap_or_default() < PORT_MAX {
424                            self.port_from = self.port_from.clone().with_value(
425                                (self.port_from.value().parse::<u32>().unwrap_or_default() + 1)
426                                    .to_string(),
427                            );
428                            let port_from_value: u32 =
429                                self.port_from.value().parse().unwrap_or_default();
430                            if port_from_value + PORT_ALLOCATION <= PORT_MAX {
431                                self.port_to = Input::default()
432                                    .with_value((port_from_value + PORT_ALLOCATION).to_string());
433                            } else {
434                                self.port_to = Input::default().with_value("-".to_string());
435                            }
436                        };
437                        self.validate();
438                        vec![]
439                    }
440                    KeyCode::Down => {
441                        if self.port_from.value().parse::<u32>().unwrap_or_default() > 0 {
442                            self.port_from = self.port_from.clone().with_value(
443                                (self.port_from.value().parse::<u32>().unwrap_or_default() - 1)
444                                    .to_string(),
445                            );
446                            let port_from_value: u32 =
447                                self.port_from.value().parse().unwrap_or_default();
448                            if port_from_value + PORT_ALLOCATION <= PORT_MAX {
449                                self.port_to = Input::default()
450                                    .with_value((port_from_value + PORT_ALLOCATION).to_string());
451                            } else {
452                                self.port_to = Input::default().with_value("-".to_string());
453                            }
454                        };
455                        self.validate();
456                        vec![]
457                    }
458                    KeyCode::Backspace => {
459                        self.port_from.handle_event(&Event::Key(key));
460                        let port_from_value: u32 =
461                            self.port_from.value().parse().unwrap_or_default();
462                        self.port_to = Input::default()
463                            .with_value((port_from_value + PORT_ALLOCATION).to_string());
464                        self.validate();
465                        vec![]
466                    }
467                    _ => {
468                        if self.first_stroke {
469                            self.first_stroke = false;
470                            self.port_from = Input::default().with_value("".to_string());
471                        }
472                        // if max limit reached, we should not allow any more inputs.
473                        if self.port_from.value().len() < INPUT_SIZE as usize {
474                            self.port_from.handle_event(&Event::Key(key));
475                            let port_from_value: u32 =
476                                self.port_from.value().parse().unwrap_or_default();
477                            if port_from_value + PORT_ALLOCATION <= PORT_MAX {
478                                self.port_to = Input::default()
479                                    .with_value((port_from_value + PORT_ALLOCATION).to_string());
480                            } else {
481                                self.port_to = Input::default().with_value("-".to_string());
482                            }
483                        };
484                        self.validate();
485                        vec![]
486                    }
487                }
488            }
489            PortRangeState::ConfirmChange => match key.code {
490                KeyCode::Enter => {
491                    self.state = PortRangeState::PortForwardingInfo;
492                    vec![
493                        Action::StoreConnectionMode(ConnectionMode::CustomPorts),
494                        Action::OptionsActions(OptionsActions::UpdateConnectionMode(
495                            ConnectionMode::CustomPorts,
496                        )),
497                        Action::StorePortRange(
498                            self.port_from.value().parse().unwrap_or_default(),
499                            self.port_to.value().parse().unwrap_or_default(),
500                        ),
501                        Action::OptionsActions(OptionsActions::UpdatePortRange(
502                            self.port_from.value().parse().unwrap_or_default(),
503                            self.port_to.value().parse().unwrap_or_default(),
504                        )),
505                    ]
506                }
507                KeyCode::Esc => {
508                    self.state = PortRangeState::Selection;
509                    if let Some(connection_mode_old_value) = self.connection_mode_old_value {
510                        if self.port_from_old_value != 0 && self.port_to_old_value != 0 {
511                            vec![
512                                Action::OptionsActions(OptionsActions::UpdateConnectionMode(
513                                    connection_mode_old_value,
514                                )),
515                                Action::OptionsActions(OptionsActions::UpdatePortRange(
516                                    self.port_from_old_value,
517                                    self.port_to_old_value,
518                                )),
519                                Action::SwitchScene(Scene::Options),
520                            ]
521                        } else {
522                            vec![
523                                Action::OptionsActions(OptionsActions::UpdateConnectionMode(
524                                    connection_mode_old_value,
525                                )),
526                                Action::SwitchScene(Scene::Options),
527                            ]
528                        }
529                    } else {
530                        vec![Action::SwitchScene(Scene::Options)]
531                    }
532                }
533                _ => vec![],
534            },
535            PortRangeState::PortForwardingInfo => match key.code {
536                KeyCode::Enter => {
537                    debug!("Got Enter, saving the ports and switching to Status Screen",);
538                    self.state = PortRangeState::Selection;
539                    vec![Action::SwitchScene(Scene::Status)]
540                }
541                _ => vec![],
542            },
543        };
544        Ok(send_back)
545    }
546
547    fn update(&mut self, action: Action) -> Result<Option<Action>> {
548        let send_back = match action {
549            Action::SwitchScene(scene) => match scene {
550                Scene::ChangePortsPopUp {
551                    connection_mode_old_value,
552                } => {
553                    if self.connection_mode == ConnectionMode::CustomPorts {
554                        self.active = true;
555                        self.first_stroke = true;
556                        self.connection_mode_old_value = connection_mode_old_value;
557                        self.validate();
558                        self.port_from_old_value =
559                            self.port_from.value().parse().unwrap_or_default();
560                        self.port_to_old_value = self.port_to.value().parse().unwrap_or_default();
561                        // Set to InputMode::Entry as we want to handle everything within our handle_key_events
562                        // so by default if this scene is active, we capture inputs.
563                        Some(Action::SwitchInputMode(InputMode::Entry))
564                    } else {
565                        self.active = false;
566                        Some(Action::SwitchScene(Scene::Options))
567                    }
568                }
569                _ => {
570                    self.active = false;
571                    None
572                }
573            },
574            // Useful when the user has selected a connection mode but didn't confirm it
575            Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => {
576                self.connection_mode = connection_mode;
577                None
578            }
579            _ => None,
580        };
581        Ok(send_back)
582    }
583
584    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
585        if !self.active {
586            return Ok(());
587        }
588
589        let layer_zero = centered_rect_fixed(52, 15, area);
590
591        let layer_one = Layout::new(
592            Direction::Vertical,
593            [
594                // for the pop_up_border
595                Constraint::Length(2),
596                // for the input field
597                Constraint::Min(1),
598                // for the pop_up_border
599                Constraint::Length(1),
600            ],
601        )
602        .split(layer_zero);
603
604        let pop_up_border: Paragraph = match self.state {
605            PortRangeState::Selection => self.draw_selection_state(f, layer_zero, layer_one),
606            PortRangeState::ConfirmChange => self.draw_confirm_and_reset(f, layer_zero, layer_one),
607            PortRangeState::PortForwardingInfo => {
608                self.draw_info_port_forwarding(f, layer_zero, layer_one)
609            }
610        };
611        // We render now so the borders are on top of the other widgets
612        f.render_widget(pop_up_border, layer_zero);
613
614        Ok(())
615    }
616}