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        popup::manage_nodes::{GB, GB_PER_NODE},
28        Component,
29    },
30    config::get_launchpad_nodes_data_dir_path,
31    mode::{InputMode, Scene},
32    style::{
33        clear_area, COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE,
34        VIVID_SKY_BLUE,
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            if let Some(i) = items.state.selected() {
94                items.items[i].status = DriveStatus::Selected;
95                self.drive_selection = items.items[i].clone();
96            }
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            if let Some(i) = items.state.selected() {
118                return items.items[i].clone();
119            }
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                            if items.items.len() > 1 {
407                                items.previous();
408                                let drive = self.return_selection();
409                                self.can_select = drive.mountpoint
410                                    != self.drive_selection.mountpoint
411                                    && drive.status != DriveStatus::NotAvailable
412                                    && drive.status != DriveStatus::NotEnoughSpace;
413                            }
414                        }
415                        vec![]
416                    }
417                    KeyCode::Down => {
418                        if let Some(ref mut items) = self.items {
419                            if items.items.len() > 1 {
420                                items.next();
421                                let drive = self.return_selection();
422                                self.can_select = drive.mountpoint
423                                    != self.drive_selection.mountpoint
424                                    && drive.status != DriveStatus::NotAvailable
425                                    && drive.status != DriveStatus::NotEnoughSpace;
426                            }
427                        }
428                        vec![]
429                    }
430                    _ => {
431                        vec![]
432                    }
433                }
434            }
435            ChangeDriveState::ConfirmChange => match key.code {
436                KeyCode::Enter => {
437                    debug!("Got Enter, storing value and switching to Options");
438                    // Let's create the data directory for the new drive
439                    self.drive_selection = self.return_selection();
440                    match get_launchpad_nodes_data_dir_path(&self.drive_selection.mountpoint, true)
441                    {
442                        Ok(_path) => {
443                            // TODO: probably delete the old data directory before switching
444                            // Taking in account if it's the default mountpoint
445                            // (were the executable is)
446                            vec![
447                                Action::StoreStorageDrive(
448                                    self.drive_selection.mountpoint.clone(),
449                                    self.drive_selection.name.clone(),
450                                ),
451                                Action::OptionsActions(OptionsActions::UpdateStorageDrive(
452                                    self.drive_selection.mountpoint.clone(),
453                                    self.drive_selection.name.clone(),
454                                )),
455                                Action::SwitchScene(Scene::Status),
456                            ]
457                        }
458                        Err(e) => {
459                            self.drive_selection = self.drive_selection_initial_state.clone();
460                            self.state = ChangeDriveState::Selection;
461                            error!(
462                                "Error creating folder {:?}: {}",
463                                self.drive_selection.mountpoint, e
464                            );
465                            vec![Action::SwitchScene(Scene::Options)]
466                        }
467                    }
468                }
469                KeyCode::Esc => {
470                    debug!("Got Esc, switching to Options");
471                    self.drive_selection = self.drive_selection_initial_state.clone();
472                    self.state = ChangeDriveState::Selection;
473                    vec![Action::SwitchScene(Scene::Options)]
474                }
475                _ => {
476                    vec![]
477                }
478            },
479        };
480        Ok(send_back)
481    }
482
483    fn update(&mut self, action: Action) -> Result<Option<Action>> {
484        let send_back = match action {
485            Action::SwitchScene(scene) => match scene {
486                Scene::ChangeDrivePopUp => {
487                    self.active = true;
488                    self.can_select = false;
489                    self.state = ChangeDriveState::Selection;
490                    let _ = self.update_drive_items();
491                    self.select_drive();
492                    Some(Action::SwitchInputMode(InputMode::Entry))
493                }
494                _ => {
495                    self.active = false;
496                    None
497                }
498            },
499            // Useful when the user has selected a drive but didn't confirm it
500            Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, drive_name)) => {
501                self.drive_selection.mountpoint = mountpoint;
502                self.drive_selection.name = drive_name;
503                self.select_drive();
504                None
505            }
506            // We need to refresh the list of available drives because of the space
507            Action::StoreNodesToStart(ref nodes_to_start) => {
508                self.nodes_to_start = *nodes_to_start;
509                let _ = self.update_drive_items();
510                None
511            }
512            Action::StoreStorageDrive(mountpoint, _drive_name) => {
513                self.storage_mountpoint = mountpoint;
514                let _ = self.update_drive_items();
515                self.select_drive();
516                None
517            }
518
519            _ => None,
520        };
521        Ok(send_back)
522    }
523
524    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
525        if !self.active {
526            return Ok(());
527        }
528
529        let layer_zero = centered_rect_fixed(52, 15, area);
530
531        let layer_one = Layout::new(
532            Direction::Vertical,
533            [
534                // Padding from title to the table
535                Constraint::Length(1),
536                // Table
537                Constraint::Min(1),
538                // for the pop_up_border
539                Constraint::Length(1),
540            ],
541        )
542        .split(layer_zero);
543
544        let pop_up_border: Paragraph = match self.state {
545            ChangeDriveState::Selection => self.draw_selection_state(f, layer_zero, layer_one),
546            ChangeDriveState::ConfirmChange => {
547                self.draw_confirm_change_state(f, layer_zero, layer_one)
548            }
549        };
550        // We render now so the borders are on top of the other widgets
551        f.render_widget(pop_up_border, layer_zero);
552
553        Ok(())
554    }
555}
556
557#[derive(Default, Clone)]
558struct StatefulList<T> {
559    state: ListState,
560    items: Vec<T>,
561    last_selected: Option<usize>,
562}
563
564impl<T> StatefulList<T> {
565    fn with_items(items: Vec<T>) -> Self {
566        StatefulList {
567            state: ListState::default(),
568            items,
569            last_selected: None,
570        }
571    }
572
573    fn next(&mut self) {
574        let i = match self.state.selected() {
575            Some(i) => {
576                if i >= self.items.len() - 1 {
577                    0
578                } else {
579                    i + 1
580                }
581            }
582            None => self.last_selected.unwrap_or(0),
583        };
584        self.state.select(Some(i));
585    }
586
587    fn previous(&mut self) {
588        let i = match self.state.selected() {
589            Some(i) => {
590                if i == 0 {
591                    self.items.len() - 1
592                } else {
593                    i - 1
594                }
595            }
596            None => self.last_selected.unwrap_or(0),
597        };
598        self.state.select(Some(i));
599    }
600}
601
602#[derive(Default, Debug, Copy, Clone, PartialEq)]
603enum DriveStatus {
604    Selected,
605    #[default]
606    NotSelected,
607    NotEnoughSpace,
608    NotAvailable,
609}
610
611#[derive(Default, Debug, Clone)]
612pub struct DriveItem {
613    name: String,
614    mountpoint: PathBuf,
615    size: String,
616    status: DriveStatus,
617}
618
619impl DriveItem {
620    fn to_list_item(&self, _index: usize, width: usize) -> ListItem {
621        let spaces = width - self.name.len() - self.size.len() - "   ".len() - 4;
622        let line = match self.status {
623            DriveStatus::NotSelected => Line::from(vec![
624                Span::raw("   "),
625                Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
626                Span::raw(" ".repeat(spaces)),
627                Span::styled(self.size.clone(), Style::default().fg(LIGHT_PERIWINKLE)),
628            ]),
629            DriveStatus::Selected => Line::from(vec![
630                Span::styled(" ►", Style::default().fg(EUCALYPTUS)),
631                Span::raw(" "),
632                Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
633                Span::raw(" ".repeat(spaces)),
634                Span::styled(self.size.clone(), Style::default().fg(GHOST_WHITE)),
635            ]),
636            DriveStatus::NotEnoughSpace => Line::from(vec![
637                Span::raw("   "),
638                Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
639                Span::raw(" ".repeat(spaces)),
640                Span::styled(self.size.clone(), Style::default().fg(COOL_GREY)),
641            ]),
642            DriveStatus::NotAvailable => {
643                let legend = "No Access";
644                let spaces = width - self.name.len() - legend.len() - "   ".len() - 4;
645                Line::from(vec![
646                    Span::raw("   "),
647                    Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
648                    Span::raw(" ".repeat(spaces)),
649                    Span::styled(legend, Style::default().fg(COOL_GREY)),
650                ])
651            }
652        };
653
654        ListItem::new(line)
655    }
656}