node_launchpad/components/popup/
change_drive.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::{default::Default, path::PathBuf, rc::Rc};
10
11use super::super::utils::centered_rect_fixed;
12
13use color_eyre::Result;
14use crossterm::event::{KeyCode, KeyEvent};
15use ratatui::{
16    layout::{Alignment, Constraint, Direction, Layout, Rect},
17    style::{Style, Stylize},
18    text::{Line, Span},
19    widgets::{
20        Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, Wrap,
21    },
22};
23
24use crate::{
25    action::{Action, OptionsActions},
26    components::{
27        Component,
28        popup::manage_nodes::{GB, GB_PER_NODE},
29    },
30    config::get_launchpad_nodes_data_dir_path,
31    mode::{InputMode, Scene},
32    style::{
33        COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE,
34        VIVID_SKY_BLUE, clear_area,
35    },
36    system,
37};
38
39#[derive(Default)]
40enum ChangeDriveState {
41    #[default]
42    Selection,
43    ConfirmChange,
44}
45
46#[derive(Default)]
47pub struct ChangeDrivePopup {
48    active: bool,
49    state: ChangeDriveState,
50    items: Option<StatefulList<DriveItem>>,
51    drive_selection: DriveItem,
52    drive_selection_initial_state: DriveItem,
53    nodes_to_start: usize,
54    storage_mountpoint: PathBuf,
55    can_select: bool, // Used to enable the "Change Drive" button based on conditions
56}
57
58impl ChangeDrivePopup {
59    pub fn new(storage_mountpoint: PathBuf, nodes_to_start: usize) -> Result<Self> {
60        debug!("Drive Mountpoint in Config: {:?}", storage_mountpoint);
61        Ok(ChangeDrivePopup {
62            active: false,
63            state: ChangeDriveState::Selection,
64            items: None,
65            drive_selection: DriveItem::default(),
66            drive_selection_initial_state: DriveItem::default(),
67            nodes_to_start,
68            storage_mountpoint,
69            can_select: false,
70        })
71    }
72
73    // --- Interactions with the List of drives ---
74
75    /// Deselects all drives in the list of items
76    ///
77    fn deselect_all(&mut self) {
78        if let Some(ref mut items) = self.items {
79            for item in &mut items.items {
80                if item.status != DriveStatus::NotAvailable
81                    && item.status != DriveStatus::NotEnoughSpace
82                {
83                    item.status = DriveStatus::NotSelected;
84                }
85            }
86        }
87    }
88    /// Assigns to self.drive_selection the selected drive in the list
89    ///
90    fn assign_drive_selection(&mut self) {
91        self.deselect_all();
92        if let Some(ref mut items) = self.items
93            && let Some(i) = items.state.selected()
94        {
95            items.items[i].status = DriveStatus::Selected;
96            self.drive_selection = items.items[i].clone();
97        }
98    }
99    /// Highlights the drive that is currently selected in the list of items.
100    ///
101    fn select_drive(&mut self) {
102        self.deselect_all();
103        if let Some(ref mut items) = self.items {
104            for (index, item) in items.items.iter_mut().enumerate() {
105                if item.mountpoint == self.drive_selection.mountpoint {
106                    item.status = DriveStatus::Selected;
107                    items.state.select(Some(index));
108                    break;
109                }
110            }
111        }
112    }
113    /// Returns the highlighted drive in the list of items.
114    ///
115    fn return_selection(&mut self) -> DriveItem {
116        if let Some(ref mut items) = self.items
117            && let Some(i) = items.state.selected()
118        {
119            return items.items[i].clone();
120        }
121        DriveItem::default()
122    }
123
124    /// Updates the drive items based on the current nodes_to_start value.
125    fn update_drive_items(&mut self) -> Result<()> {
126        let drives_and_space = system::get_list_of_available_drives_and_available_space()?;
127        let drives_items: Vec<DriveItem> = drives_and_space
128            .iter()
129            .map(|(drive_name, mountpoint, space, available)| {
130                let size_str = format!("{:.2} GB", *space as f64 / 1e9);
131                let has_enough_space = *space as u128
132                    >= (GB_PER_NODE as u128 * GB as u128 * self.nodes_to_start as u128);
133                DriveItem {
134                    name: drive_name.to_string(),
135                    mountpoint: mountpoint.clone(),
136                    size: size_str.clone(),
137                    status: if *mountpoint == self.storage_mountpoint {
138                        self.drive_selection = DriveItem {
139                            name: drive_name.to_string(),
140                            mountpoint: mountpoint.clone(),
141                            size: size_str.clone(),
142                            status: DriveStatus::Selected,
143                        };
144                        DriveStatus::Selected
145                    } else if !available {
146                        DriveStatus::NotAvailable
147                    } else if !has_enough_space {
148                        DriveStatus::NotEnoughSpace
149                    } else {
150                        DriveStatus::NotSelected
151                    },
152                }
153            })
154            .collect();
155        self.items = Some(StatefulList::with_items(drives_items.clone()));
156        debug!("Drives and space: {:?}", drives_and_space);
157        debug!("Drives items: {:?}", drives_items);
158        Ok(())
159    }
160
161    // -- Draw functions --
162
163    // Draws the Drive Selection screen
164    fn draw_selection_state(
165        &mut self,
166        f: &mut crate::tui::Frame<'_>,
167        layer_zero: Rect,
168        layer_one: Rc<[Rect]>,
169    ) -> Paragraph<'_> {
170        let pop_up_border = Paragraph::new("").block(
171            Block::default()
172                .borders(Borders::ALL)
173                .title(" Select a Drive ")
174                .bold()
175                .title_style(Style::new().fg(VIVID_SKY_BLUE))
176                .padding(Padding::uniform(2))
177                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
178        );
179        clear_area(f, layer_zero);
180
181        let layer_two = Layout::new(
182            Direction::Vertical,
183            [
184                // for the table
185                Constraint::Length(10),
186                // gap
187                Constraint::Length(3),
188                // for the buttons
189                Constraint::Length(1),
190            ],
191        )
192        .split(layer_one[1]);
193
194        // Drive selector
195        let items: Vec<ListItem> = self
196            .items
197            .as_ref()
198            .unwrap()
199            .items
200            .iter()
201            .enumerate()
202            .map(|(i, drive_item)| drive_item.to_list_item(i, layer_two[0].width as usize))
203            .collect();
204
205        let items = List::new(items)
206            .block(Block::default().padding(Padding::uniform(1)))
207            .highlight_style(Style::default().bg(INDIGO))
208            .highlight_spacing(HighlightSpacing::Always);
209
210        f.render_stateful_widget(items, layer_two[0], &mut self.items.clone().unwrap().state);
211
212        // Dash
213        let dash = Block::new()
214            .borders(Borders::BOTTOM)
215            .border_style(Style::new().fg(GHOST_WHITE));
216        f.render_widget(dash, layer_two[1]);
217
218        // Buttons
219        let buttons_layer =
220            Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
221                .split(layer_two[2]);
222
223        let button_no = Line::from(vec![Span::styled(
224            "Cancel [Esc]",
225            Style::default().fg(LIGHT_PERIWINKLE),
226        )]);
227
228        f.render_widget(
229            Paragraph::new(button_no)
230                .block(Block::default().padding(Padding::horizontal(2)))
231                .alignment(Alignment::Left),
232            buttons_layer[0],
233        );
234
235        let button_yes = Line::from(vec![
236            Span::styled(
237                "Change Drive ",
238                if self.can_select {
239                    Style::default().fg(EUCALYPTUS)
240                } else {
241                    Style::default().fg(COOL_GREY)
242                },
243            ),
244            Span::styled("[Enter]", Style::default().fg(LIGHT_PERIWINKLE).bold()),
245        ])
246        .alignment(Alignment::Right);
247
248        f.render_widget(
249            Paragraph::new(button_yes)
250                .block(Block::default().padding(Padding::horizontal(2)))
251                .alignment(Alignment::Right),
252            buttons_layer[1],
253        );
254
255        pop_up_border
256    }
257
258    // Draws the Confirmation screen
259    fn draw_confirm_change_state(
260        &mut self,
261        f: &mut crate::tui::Frame<'_>,
262        layer_zero: Rect,
263        layer_one: Rc<[Rect]>,
264    ) -> Paragraph<'_> {
265        let pop_up_border = Paragraph::new("").block(
266            Block::default()
267                .borders(Borders::ALL)
268                .title(" Confirm & Reset ")
269                .bold()
270                .title_style(Style::new().fg(VIVID_SKY_BLUE))
271                .padding(Padding::uniform(2))
272                .border_style(Style::new().fg(VIVID_SKY_BLUE))
273                .bg(DARK_GUNMETAL),
274        );
275        clear_area(f, layer_zero);
276
277        let layer_two = Layout::new(
278            Direction::Vertical,
279            [
280                // for the table
281                Constraint::Length(10),
282                // gap
283                Constraint::Length(3),
284                // for the buttons
285                Constraint::Length(1),
286            ],
287        )
288        .split(layer_one[1]);
289
290        // Text
291        let text = vec![
292            Line::from(vec![]), // Empty line
293            Line::from(vec![]), // Empty line
294            Line::from(vec![
295                Span::styled("Changing storage to ", Style::default().fg(GHOST_WHITE)),
296                Span::styled(
297                    format!("{} ", self.drive_selection.name),
298                    Style::default().fg(VIVID_SKY_BLUE),
299                ),
300                Span::styled("will ", Style::default().fg(GHOST_WHITE)),
301            ])
302            .alignment(Alignment::Center),
303            Line::from(vec![Span::styled(
304                "reset all nodes.",
305                Style::default().fg(GHOST_WHITE),
306            )])
307            .alignment(Alignment::Center),
308            Line::from(vec![]), // Empty line
309            Line::from(vec![]), // Empty line
310            Line::from(vec![
311                Span::styled("You’ll need to ", Style::default().fg(GHOST_WHITE)),
312                Span::styled("Add ", Style::default().fg(GHOST_WHITE).bold()),
313                Span::styled("and ", Style::default().fg(GHOST_WHITE)),
314                Span::styled("Start ", Style::default().fg(GHOST_WHITE).bold()),
315                Span::styled(
316                    "them again afterwards. Are you sure you want to continue?",
317                    Style::default().fg(GHOST_WHITE),
318                ),
319            ])
320            .alignment(Alignment::Center),
321        ];
322        let paragraph = Paragraph::new(text)
323            .wrap(Wrap { trim: false })
324            .block(
325                Block::default()
326                    .borders(Borders::NONE)
327                    .padding(Padding::horizontal(2)),
328            )
329            .alignment(Alignment::Center)
330            .style(Style::default().fg(GHOST_WHITE).bg(DARK_GUNMETAL));
331
332        f.render_widget(paragraph, layer_two[0]);
333
334        // Dash
335        let dash = Block::new()
336            .borders(Borders::BOTTOM)
337            .border_style(Style::new().fg(GHOST_WHITE));
338        f.render_widget(dash, layer_two[1]);
339
340        // Buttons
341        let buttons_layer =
342            Layout::horizontal(vec![Constraint::Percentage(30), Constraint::Percentage(70)])
343                .split(layer_two[2]);
344
345        let button_no = Line::from(vec![Span::styled(
346            "Back [Esc]",
347            Style::default().fg(LIGHT_PERIWINKLE),
348        )]);
349
350        f.render_widget(
351            Paragraph::new(button_no)
352                .block(Block::default().padding(Padding::horizontal(2)))
353                .alignment(Alignment::Left),
354            buttons_layer[0],
355        );
356
357        let button_yes = Line::from(vec![
358            Span::styled("Yes, change drive ", Style::default().fg(EUCALYPTUS)),
359            Span::styled("[Enter]", Style::default().fg(LIGHT_PERIWINKLE).bold()),
360        ])
361        .alignment(Alignment::Right);
362
363        f.render_widget(
364            Paragraph::new(button_yes)
365                .block(Block::default().padding(Padding::horizontal(2)))
366                .alignment(Alignment::Right),
367            buttons_layer[1],
368        );
369
370        pop_up_border
371    }
372}
373
374impl Component for ChangeDrivePopup {
375    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
376        if !self.active {
377            return Ok(vec![]);
378        }
379        let send_back: Vec<Action> = match &self.state {
380            ChangeDriveState::Selection => {
381                match key.code {
382                    KeyCode::Enter => {
383                        // We allow action if we have more than one drive and the action is not
384                        // over the drive already selected
385                        let drive = self.return_selection();
386                        if self.can_select {
387                            debug!(
388                                "Got Enter and there's a new selection, storing value and switching to Options"
389                            );
390                            debug!("Drive selected: {:?}", drive.name);
391                            self.drive_selection_initial_state = self.drive_selection.clone();
392                            self.assign_drive_selection();
393                            self.state = ChangeDriveState::ConfirmChange;
394                            vec![]
395                        } else {
396                            debug!("Got Enter, but no new selection. We should not do anything");
397                            vec![]
398                        }
399                    }
400                    KeyCode::Esc => {
401                        debug!("Got Esc, switching to Options");
402                        vec![Action::SwitchScene(Scene::Options)]
403                    }
404                    KeyCode::Up => {
405                        if let Some(ref mut items) = self.items
406                            && items.items.len() > 1
407                        {
408                            items.previous();
409                            let drive = self.return_selection();
410                            self.can_select = drive.mountpoint != self.drive_selection.mountpoint
411                                && drive.status != DriveStatus::NotAvailable
412                                && drive.status != DriveStatus::NotEnoughSpace;
413                        }
414                        vec![]
415                    }
416                    KeyCode::Down => {
417                        if let Some(ref mut items) = self.items
418                            && items.items.len() > 1
419                        {
420                            items.next();
421                            let drive = self.return_selection();
422                            self.can_select = drive.mountpoint != self.drive_selection.mountpoint
423                                && drive.status != DriveStatus::NotAvailable
424                                && drive.status != DriveStatus::NotEnoughSpace;
425                        }
426                        vec![]
427                    }
428                    _ => {
429                        vec![]
430                    }
431                }
432            }
433            ChangeDriveState::ConfirmChange => match key.code {
434                KeyCode::Enter => {
435                    debug!("Got Enter, storing value and switching to Options");
436                    // Let's create the data directory for the new drive
437                    self.drive_selection = self.return_selection();
438                    match get_launchpad_nodes_data_dir_path(&self.drive_selection.mountpoint, true)
439                    {
440                        Ok(_path) => {
441                            // TODO: probably delete the old data directory before switching
442                            // Taking in account if it's the default mountpoint
443                            // (were the executable is)
444                            vec![
445                                Action::StoreStorageDrive(
446                                    self.drive_selection.mountpoint.clone(),
447                                    self.drive_selection.name.clone(),
448                                ),
449                                Action::OptionsActions(OptionsActions::UpdateStorageDrive(
450                                    self.drive_selection.mountpoint.clone(),
451                                    self.drive_selection.name.clone(),
452                                )),
453                                Action::SwitchScene(Scene::Status),
454                            ]
455                        }
456                        Err(e) => {
457                            self.drive_selection = self.drive_selection_initial_state.clone();
458                            self.state = ChangeDriveState::Selection;
459                            error!(
460                                "Error creating folder {:?}: {}",
461                                self.drive_selection.mountpoint, e
462                            );
463                            vec![Action::SwitchScene(Scene::Options)]
464                        }
465                    }
466                }
467                KeyCode::Esc => {
468                    debug!("Got Esc, switching to Options");
469                    self.drive_selection = self.drive_selection_initial_state.clone();
470                    self.state = ChangeDriveState::Selection;
471                    vec![Action::SwitchScene(Scene::Options)]
472                }
473                _ => {
474                    vec![]
475                }
476            },
477        };
478        Ok(send_back)
479    }
480
481    fn update(&mut self, action: Action) -> Result<Option<Action>> {
482        let send_back = match action {
483            Action::SwitchScene(scene) => match scene {
484                Scene::ChangeDrivePopUp => {
485                    self.active = true;
486                    self.can_select = false;
487                    self.state = ChangeDriveState::Selection;
488                    let _ = self.update_drive_items();
489                    self.select_drive();
490                    Some(Action::SwitchInputMode(InputMode::Entry))
491                }
492                _ => {
493                    self.active = false;
494                    None
495                }
496            },
497            // Useful when the user has selected a drive but didn't confirm it
498            Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, drive_name)) => {
499                self.drive_selection.mountpoint = mountpoint;
500                self.drive_selection.name = drive_name;
501                self.select_drive();
502                None
503            }
504            // We need to refresh the list of available drives because of the space
505            Action::StoreNodesToStart(ref nodes_to_start) => {
506                self.nodes_to_start = *nodes_to_start;
507                let _ = self.update_drive_items();
508                None
509            }
510            Action::StoreStorageDrive(mountpoint, _drive_name) => {
511                self.storage_mountpoint = mountpoint;
512                let _ = self.update_drive_items();
513                self.select_drive();
514                None
515            }
516
517            _ => None,
518        };
519        Ok(send_back)
520    }
521
522    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
523        if !self.active {
524            return Ok(());
525        }
526
527        let layer_zero = centered_rect_fixed(52, 15, area);
528
529        let layer_one = Layout::new(
530            Direction::Vertical,
531            [
532                // Padding from title to the table
533                Constraint::Length(1),
534                // Table
535                Constraint::Min(1),
536                // for the pop_up_border
537                Constraint::Length(1),
538            ],
539        )
540        .split(layer_zero);
541
542        let pop_up_border: Paragraph = match self.state {
543            ChangeDriveState::Selection => self.draw_selection_state(f, layer_zero, layer_one),
544            ChangeDriveState::ConfirmChange => {
545                self.draw_confirm_change_state(f, layer_zero, layer_one)
546            }
547        };
548        // We render now so the borders are on top of the other widgets
549        f.render_widget(pop_up_border, layer_zero);
550
551        Ok(())
552    }
553}
554
555#[derive(Default, Clone)]
556struct StatefulList<T> {
557    state: ListState,
558    items: Vec<T>,
559    last_selected: Option<usize>,
560}
561
562impl<T> StatefulList<T> {
563    fn with_items(items: Vec<T>) -> Self {
564        StatefulList {
565            state: ListState::default(),
566            items,
567            last_selected: None,
568        }
569    }
570
571    fn next(&mut self) {
572        let i = match self.state.selected() {
573            Some(i) => {
574                if i >= self.items.len() - 1 {
575                    0
576                } else {
577                    i + 1
578                }
579            }
580            None => self.last_selected.unwrap_or(0),
581        };
582        self.state.select(Some(i));
583    }
584
585    fn previous(&mut self) {
586        let i = match self.state.selected() {
587            Some(i) => {
588                if i == 0 {
589                    self.items.len() - 1
590                } else {
591                    i - 1
592                }
593            }
594            None => self.last_selected.unwrap_or(0),
595        };
596        self.state.select(Some(i));
597    }
598}
599
600#[derive(Default, Debug, Copy, Clone, PartialEq)]
601enum DriveStatus {
602    Selected,
603    #[default]
604    NotSelected,
605    NotEnoughSpace,
606    NotAvailable,
607}
608
609#[derive(Default, Debug, Clone)]
610pub struct DriveItem {
611    name: String,
612    mountpoint: PathBuf,
613    size: String,
614    status: DriveStatus,
615}
616
617impl DriveItem {
618    fn to_list_item(&self, _index: usize, width: usize) -> ListItem<'_> {
619        let spaces = width - self.name.len() - self.size.len() - "   ".len() - 4;
620        let line = match self.status {
621            DriveStatus::NotSelected => Line::from(vec![
622                Span::raw("   "),
623                Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
624                Span::raw(" ".repeat(spaces)),
625                Span::styled(self.size.clone(), Style::default().fg(LIGHT_PERIWINKLE)),
626            ]),
627            DriveStatus::Selected => Line::from(vec![
628                Span::styled(" ►", Style::default().fg(EUCALYPTUS)),
629                Span::raw(" "),
630                Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
631                Span::raw(" ".repeat(spaces)),
632                Span::styled(self.size.clone(), Style::default().fg(GHOST_WHITE)),
633            ]),
634            DriveStatus::NotEnoughSpace => Line::from(vec![
635                Span::raw("   "),
636                Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
637                Span::raw(" ".repeat(spaces)),
638                Span::styled(self.size.clone(), Style::default().fg(COOL_GREY)),
639            ]),
640            DriveStatus::NotAvailable => {
641                let legend = "No Access";
642                let spaces = width - self.name.len() - legend.len() - "   ".len() - 4;
643                Line::from(vec![
644                    Span::raw("   "),
645                    Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
646                    Span::raw(" ".repeat(spaces)),
647                    Span::styled(legend, Style::default().fg(COOL_GREY)),
648                ])
649            }
650        };
651
652        ListItem::new(line)
653    }
654}