From f34c2a9e46a8fb73cc20adeb3aabec86fd518ab6 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Wed, 2 Oct 2024 17:58:39 +0100 Subject: [PATCH 01/35] chore: utility script for finding pr numbers --- resources/scripts/find_prs.py | 132 ++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100755 resources/scripts/find_prs.py diff --git a/resources/scripts/find_prs.py b/resources/scripts/find_prs.py new file mode 100755 index 0000000000..dbfc3e8c03 --- /dev/null +++ b/resources/scripts/find_prs.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import requests +import argparse +import os +from typing import List +from datetime import datetime + +class GitHubPRFinder: + def __init__(self, token: str): + self.owner = "maidsafe" + self.repo = "safe_network" + self.token = token + self.api_url = f"https://api.github.com/repos/{self.owner}/{self.repo}/commits" + + def get_pr_for_commit(self, commit_sha: str) -> List[dict]: + """ + Retrieves the list of pull requests that include the given commit SHA. + + Args: + commit_sha (str): The commit hash to search for. + + Returns: + List[dict]: A list of pull request data dictionaries. + """ + headers = { + 'Accept': 'application/vnd.github.groot-preview+json', + 'Authorization': f'token {self.token}' + } + url = f"{self.api_url}/{commit_sha}/pulls" + response = requests.get(url, headers=headers) + + if response.status_code == 200: + return response.json() + else: + return [] + +def parse_arguments() -> argparse.Namespace: + """ + Parses command-line arguments. + + Returns: + argparse.Namespace: The parsed arguments. + """ + parser = argparse.ArgumentParser(description="Find merged PRs for commit hashes listed in a file.") + parser.add_argument('--path', required=True, help='Path to the file containing commit hashes, one per line.') + parser.add_argument('--token', help='GitHub personal access token. Can also be set via GITHUB_PAT_SAFE_NETWORK_PR_LIST environment variable.') + return parser.parse_args() + +def read_commits_from_file(file_path: str) -> List[str]: + """ + Reads commit hashes from a file, one per line. + + Args: + file_path (str): The path to the file containing commit hashes. + + Returns: + List[str]: A list of commit hashes. + """ + try: + with open(file_path, 'r') as file: + commits = [line.strip() for line in file if line.strip()] + return commits + except FileNotFoundError: + return [] + except Exception: + return [] + +def format_date(iso_date_str: str) -> str: + """ + Formats an ISO 8601 date string to 'YYYY-MM-DD'. + + Args: + iso_date_str (str): The ISO 8601 date string. + + Returns: + str: The formatted date string. + """ + try: + date_obj = datetime.strptime(iso_date_str, "%Y-%m-%dT%H:%M:%SZ") + return date_obj.strftime("%Y-%m-%d") + except ValueError: + return iso_date_str.split('T')[0] if 'T' in iso_date_str else iso_date_str + +def main(): + args = parse_arguments() + token = args.token or os.getenv('GITHUB_PAT_SAFE_NETWORK_PR_LIST') + if not token: + print("GitHub token not provided. Use --token argument or set GITHUB_PAT_SAFE_NETWORK_PR_LIST environment variable.") + return + + commits = read_commits_from_file(args.path) + if not commits: + print("No commit hashes to process.") + return + + finder = GitHubPRFinder(token=token) + + pr_entries = [] + no_pr_entries = [] + + for commit in commits: + prs = finder.get_pr_for_commit(commit) + if prs: + pr_found = False + for pr in prs: + merged_at = pr.get('merged_at') + if merged_at: + pr_found = True + formatted_date = format_date(merged_at) + pr_entry = { + 'date': formatted_date, + 'commit': commit, + 'pr_number': pr['number'], + 'pr_title': pr['title'] + } + pr_entries.append(pr_entry) + if not pr_found: + no_pr_entries.append(f"No merged PR found for commit {commit}.") + else: + no_pr_entries.append(f"No merged PR found for commit {commit}.") + + pr_entries_sorted = sorted(pr_entries, key=lambda x: x['date']) + + for entry in pr_entries_sorted: + print(f"{entry['date']} - {entry['commit']} #{entry['pr_number']}: {entry['pr_title']}") + + for entry in no_pr_entries: + print(entry) + +if __name__ == "__main__": + main() \ No newline at end of file From 0f5dca082c4007e453c48a8e3ced9902d08d29cf Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 3 Sep 2024 17:49:39 +0200 Subject: [PATCH 02/35] fix(launchpad): port selection screen styling --- node-launchpad/src/components/popup/port_range.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index 491294a96a..eb08bb5e68 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -119,7 +119,6 @@ impl PortRangePopUp { f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]); let spaces_from = " ".repeat((INPUT_AREA - 1) as usize - self.port_from.value().len()); - let spaces_to = " ".repeat((INPUT_AREA - 1) as usize - self.port_to.value().len()); let input_line = Line::from(vec![ Span::styled( @@ -130,10 +129,7 @@ impl PortRangePopUp { .underlined(), ), Span::styled(" to ", Style::default().fg(GHOST_WHITE)), - Span::styled( - format!("{}{} ", spaces_to, self.port_to.value()), - Style::default().fg(VIVID_SKY_BLUE), - ), + Span::styled(self.port_to.value(), Style::default().fg(LIGHT_PERIWINKLE)), ]) .alignment(Alignment::Center); @@ -145,11 +141,11 @@ impl PortRangePopUp { "Choose the start of the range of {} ports.", PORT_ALLOCATION + 1 ), - Style::default().fg(GHOST_WHITE), + Style::default().fg(LIGHT_PERIWINKLE), )), Line::from(Span::styled( format!("This must be between {} and {}.", PORT_MIN, PORT_MAX), - Style::default().fg(if self.can_save { GHOST_WHITE } else { RED }), + Style::default().fg(if self.can_save { LIGHT_PERIWINKLE } else { RED }), )), ]) .block(block::Block::default().padding(Padding::horizontal(2))) From e70bc2908bd708075512c92c61d31c388e363e96 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 3 Sep 2024 17:44:09 +0200 Subject: [PATCH 03/35] fix(launchpad): first stroke on port edition --- node-launchpad/src/components/popup/port_range.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index eb08bb5e68..c1db0bf96b 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -45,6 +45,7 @@ pub struct PortRangePopUp { port_from_old_value: u32, port_to_old_value: u32, can_save: bool, + first_stroke: bool, } impl PortRangePopUp { @@ -59,6 +60,7 @@ impl PortRangePopUp { port_from_old_value: Default::default(), port_to_old_value: Default::default(), can_save: false, + first_stroke: true, } } @@ -366,6 +368,10 @@ impl Component for PortRangePopUp { vec![] } _ => { + if self.first_stroke { + self.first_stroke = false; + self.port_from = Input::default().with_value("".to_string()); + } // if max limit reached, we should not allow any more inputs. if self.port_from.value().len() < INPUT_SIZE as usize { self.port_from.handle_event(&Event::Key(key)); @@ -403,6 +409,7 @@ impl Component for PortRangePopUp { } => { if self.connection_mode == ConnectionMode::CustomPorts { self.active = true; + self.first_stroke = true; self.connection_mode_old_value = connection_mode_old_value; self.validate(); self.port_from_old_value = From 9b753f1c12285b9bac28ed49c4d2229b1c28c76b Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 3 Sep 2024 17:27:43 +0200 Subject: [PATCH 04/35] fix(launchpad): connection mode styling --- .../src/components/popup/connection_mode.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/node-launchpad/src/components/popup/connection_mode.rs b/node-launchpad/src/components/popup/connection_mode.rs index 987d46e017..e233557520 100644 --- a/node-launchpad/src/components/popup/connection_mode.rs +++ b/node-launchpad/src/components/popup/connection_mode.rs @@ -14,7 +14,7 @@ use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Modifier, Style, Stylize}, + style::{Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph}, }; @@ -27,7 +27,7 @@ use crate::{ mode::{InputMode, Scene}, style::{ clear_area, COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, - SPACE_CADET, VIVID_SKY_BLUE, + VIVID_SKY_BLUE, }, }; @@ -226,8 +226,7 @@ impl Component for ChangeConnectionModePopUp { .title(" Connection Mode ") .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) - .border_style(Style::new().fg(VIVID_SKY_BLUE)) - .bg(DARK_GUNMETAL), + .border_style(Style::new().fg(VIVID_SKY_BLUE)), ); clear_area(f, layer_zero); @@ -257,12 +256,7 @@ impl Component for ChangeConnectionModePopUp { let items = List::new(items) .block(Block::default().padding(Padding::uniform(1))) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED) - .fg(INDIGO), - ) + .highlight_style(Style::default().bg(INDIGO)) .highlight_spacing(HighlightSpacing::Always); f.render_stateful_widget(items, layer_two[0], &mut self.items.state); @@ -395,6 +389,6 @@ impl ConnectionModeItem { ]), }; - ListItem::new(line).style(Style::default().bg(SPACE_CADET)) + ListItem::new(line).style(Style::default().bg(DARK_GUNMETAL)) } } From 3139cd77013f8a74c4740cdc85978ac28f205a62 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 3 Sep 2024 17:14:48 +0200 Subject: [PATCH 05/35] fix(launchpad): drive selection screen styling --- .../src/components/popup/change_drive.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index 0bb424d673..0fff53336f 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -14,7 +14,7 @@ use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Modifier, Style, Stylize}, + style::{Style, Stylize}, text::{Line, Span}, widgets::{ Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, Wrap, @@ -28,7 +28,7 @@ use crate::{ mode::{InputMode, Scene}, style::{ clear_area, COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, - SPACE_CADET, VIVID_SKY_BLUE, + VIVID_SKY_BLUE, }, system, }; @@ -151,8 +151,7 @@ impl ChangeDrivePopup { .title(" Select a Drive ") .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) - .border_style(Style::new().fg(VIVID_SKY_BLUE)) - .bg(DARK_GUNMETAL), + .border_style(Style::new().fg(VIVID_SKY_BLUE)), ); clear_area(f, layer_zero); @@ -180,12 +179,7 @@ impl ChangeDrivePopup { let items = List::new(items) .block(Block::default().padding(Padding::uniform(1))) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED) - .fg(INDIGO), - ) + .highlight_style(Style::default().bg(INDIGO)) .highlight_spacing(HighlightSpacing::Always); f.render_stateful_widget(items, layer_two[0], &mut self.items.state); @@ -602,6 +596,6 @@ impl DriveItem { } }; - ListItem::new(line).style(Style::default().bg(SPACE_CADET)) + ListItem::new(line) } } From 2f553489b3b6dfb9700eafeb8e39c3a929428080 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 3 Sep 2024 16:40:46 +0200 Subject: [PATCH 06/35] fix(launchpad): styling fixes --- node-launchpad/src/components/footer.rs | 4 +- node-launchpad/src/components/header.rs | 8 +- node-launchpad/src/components/help.rs | 150 +----------------- node-launchpad/src/components/options.rs | 6 +- .../src/components/popup/beta_programme.rs | 1 + .../src/components/popup/change_drive.rs | 2 + .../src/components/popup/connection_mode.rs | 1 + .../src/components/popup/manage_nodes.rs | 1 + .../src/components/popup/port_range.rs | 2 + .../src/components/popup/reset_nodes.rs | 1 + node-launchpad/src/components/status.rs | 22 ++- node-launchpad/src/error.rs | 1 + 12 files changed, 43 insertions(+), 156 deletions(-) diff --git a/node-launchpad/src/components/footer.rs b/node-launchpad/src/components/footer.rs index 6401766850..ff5a8eb045 100644 --- a/node-launchpad/src/components/footer.rs +++ b/node-launchpad/src/components/footer.rs @@ -6,7 +6,7 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use crate::style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE}; +use crate::style::{COOL_GREY, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE}; use ratatui::{prelude::*, widgets::*}; pub enum NodesToStart { @@ -28,7 +28,7 @@ impl StatefulWidget for Footer { ) } else { ( - Style::default().fg(LIGHT_PERIWINKLE), + Style::default().fg(COOL_GREY), Style::default().fg(LIGHT_PERIWINKLE), ) }; diff --git a/node-launchpad/src/components/header.rs b/node-launchpad/src/components/header.rs index 030dbcc6c0..d503db6213 100644 --- a/node-launchpad/src/components/header.rs +++ b/node-launchpad/src/components/header.rs @@ -74,7 +74,13 @@ impl StatefulWidget for Header { let help = Span::styled("[H]elp", Style::default().fg(help_color)); // Combine the menu parts with separators - let menu = vec![status, Span::raw(" | "), options, Span::raw(" | "), help]; + let menu = vec![ + status, + Span::raw(" | ").fg(VIVID_SKY_BLUE), + options, + Span::raw(" | ").fg(VIVID_SKY_BLUE), + help, + ]; // Calculate spacing between title and menu items let total_width = (layout[0].width - 1) as usize; diff --git a/node-launchpad/src/components/help.rs b/node-launchpad/src/components/help.rs index 3bc293ad0a..7bb94db50d 100644 --- a/node-launchpad/src/components/help.rs +++ b/node-launchpad/src/components/help.rs @@ -14,7 +14,7 @@ use crate::{ action::Action, components::header::Header, mode::{InputMode, Scene}, - style::{EUCALYPTUS, GHOST_WHITE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE}, + style::{COOL_GREY, GHOST_WHITE}, widgets::hyperlink::Hyperlink, }; use ansi_to_tui::IntoText; @@ -46,11 +46,7 @@ impl Component for Help { // We define a layout, top and down box. let layout = Layout::default() .direction(Direction::Vertical) - .constraints(vec![ - Constraint::Length(1), - Constraint::Min(7), - Constraint::Max(13), - ]) + .constraints(vec![Constraint::Length(1), Constraint::Length(9)]) .split(area); // ==== Header ===== @@ -132,151 +128,15 @@ impl Component for Help { .block( Block::new() .borders(Borders::ALL) + .border_style(Style::default().fg(COOL_GREY)) .padding(Padding::uniform(1)) .title(" Get Help & Support ") - .title_style(Style::default().bold()), + .bold() + .title_style(Style::default().bold().fg(GHOST_WHITE)), ); f.render_widget(table_help_and_support, layout[1]); - // ---- Keyboard shortcuts ---- - let rows_keyboard_shortcuts = vec![ - Row::new(vec![ - Cell::from(Line::from(vec![ - Span::styled("[S] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Status", Style::default().fg(VIVID_SKY_BLUE)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+G] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Manage Nodes", Style::default().fg(EUCALYPTUS)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+D] ", Style::default().fg(GHOST_WHITE)), - Span::styled( - "Change Storage Drive", - Style::default().fg(VERY_LIGHT_AZURE), - ), - ])), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - Cell::from(Line::from(vec![ - Span::styled("[O] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Options", Style::default().fg(VIVID_SKY_BLUE)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+S] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Start All Nodes", Style::default().fg(EUCALYPTUS)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+K] ", Style::default().fg(GHOST_WHITE)), - Span::styled( - "Switch Connection Mode", - Style::default().fg(VERY_LIGHT_AZURE), - ), - ])), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - Cell::from(Line::from(vec![ - Span::styled("[H] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Help", Style::default().fg(VIVID_SKY_BLUE)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+X] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Stop All Nodes", Style::default().fg(EUCALYPTUS)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+P] ", Style::default().fg(GHOST_WHITE)), - Span::styled( - "Edit Custom Port Range", - Style::default().fg(VERY_LIGHT_AZURE), - ), - ])), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - Cell::from(Line::from(vec![ - Span::styled("[Q] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Quit", Style::default().fg(VIVID_SKY_BLUE)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+R] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Reset All Nodes", Style::default().fg(EUCALYPTUS)), - ])), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+B] ", Style::default().fg(GHOST_WHITE)), - Span::styled( - "Edit Discord Username", - Style::default().fg(VERY_LIGHT_AZURE), - ), - ])), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+L] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Open Logs Folder", Style::default().fg(VERY_LIGHT_AZURE)), - ])), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Line::from(vec![ - Span::styled("[Ctrl+L] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Open Logs Folder", Style::default().fg(VERY_LIGHT_AZURE)), - ])), - ]), - ]; - - let table_keyboard_shortcuts = Table::new( - rows_keyboard_shortcuts, - vec![ - Constraint::Percentage(33), - Constraint::Percentage(33), - Constraint::Percentage(33), - ], - ) - .block( - Block::new() - .borders(Borders::ALL) - .padding(Padding::uniform(1)) - .title(" Keyboard Shortcuts ") - .title_style(Style::default().bold()), - ); - - f.render_widget(table_keyboard_shortcuts, layout[2]); - Ok(()) } diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index 3a2c8658fc..2e234726f9 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -90,7 +90,7 @@ impl Component for Options { .title_style(Style::default().bold().fg(GHOST_WHITE)) .style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) - .border_style(Style::default().fg(VIVID_SKY_BLUE)); + .border_style(Style::default().fg(VERY_LIGHT_AZURE)); let storage_drivename = Table::new( vec![ Row::new(vec![ @@ -217,7 +217,7 @@ impl Component for Options { .title_style(Style::default().bold().fg(GHOST_WHITE)) .style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) - .border_style(Style::default().fg(VIVID_SKY_BLUE)); + .border_style(Style::default().fg(VERY_LIGHT_AZURE)); let beta_rewards = Table::new( vec![ Row::new(vec![ @@ -268,7 +268,7 @@ impl Component for Options { .title_style(Style::default().bold().fg(GHOST_WHITE)) .style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) - .border_style(Style::default().fg(VIVID_SKY_BLUE)); + .border_style(Style::default().fg(VERY_LIGHT_AZURE)); let logs_folder = Table::new( vec![ Row::new(vec![ diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index bcfbe45acd..bb64865c19 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -187,6 +187,7 @@ impl Component for BetaProgramme { Block::default() .borders(Borders::ALL) .title(" Beta Rewards Program ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(VIVID_SKY_BLUE)), diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index 0fff53336f..3c0d22906d 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -149,6 +149,7 @@ impl ChangeDrivePopup { Block::default() .borders(Borders::ALL) .title(" Select a Drive ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(VIVID_SKY_BLUE)), @@ -241,6 +242,7 @@ impl ChangeDrivePopup { Block::default() .borders(Borders::ALL) .title(" Confirm & Reset ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(VIVID_SKY_BLUE)) diff --git a/node-launchpad/src/components/popup/connection_mode.rs b/node-launchpad/src/components/popup/connection_mode.rs index e233557520..34440af4fb 100644 --- a/node-launchpad/src/components/popup/connection_mode.rs +++ b/node-launchpad/src/components/popup/connection_mode.rs @@ -224,6 +224,7 @@ impl Component for ChangeConnectionModePopUp { Block::default() .borders(Borders::ALL) .title(" Connection Mode ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(VIVID_SKY_BLUE)), diff --git a/node-launchpad/src/components/popup/manage_nodes.rs b/node-launchpad/src/components/popup/manage_nodes.rs index fd0e285660..2ad7674730 100644 --- a/node-launchpad/src/components/popup/manage_nodes.rs +++ b/node-launchpad/src/components/popup/manage_nodes.rs @@ -219,6 +219,7 @@ impl Component for ManageNodes { Block::default() .borders(Borders::ALL) .title(" Manage Nodes ") + .bold() .title_style(Style::new().fg(GHOST_WHITE)) .title_style(Style::new().fg(EUCALYPTUS)) .padding(Padding::uniform(2)) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index c1db0bf96b..dd6fa90b39 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -90,6 +90,7 @@ impl PortRangePopUp { Block::default() .borders(Borders::ALL) .title(" Custom Ports ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(VIVID_SKY_BLUE)), @@ -196,6 +197,7 @@ impl PortRangePopUp { Block::default() .borders(Borders::ALL) .title(" Port Forwarding For Private IPs ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(VIVID_SKY_BLUE)), diff --git a/node-launchpad/src/components/popup/reset_nodes.rs b/node-launchpad/src/components/popup/reset_nodes.rs index 7389b8e472..c7117519f9 100644 --- a/node-launchpad/src/components/popup/reset_nodes.rs +++ b/node-launchpad/src/components/popup/reset_nodes.rs @@ -118,6 +118,7 @@ impl Component for ResetNodesPopup { Block::default() .borders(Borders::ALL) .title(" Reset Nodes ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(VIVID_SKY_BLUE)), diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index c047a39249..74a71f98cd 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -532,7 +532,7 @@ impl Component for Status { )]); let line2 = Line::from(vec![ Span::styled("Press ", Style::default().fg(VERY_LIGHT_AZURE)), - Span::styled("[Ctrl+B]", Style::default().fg(GHOST_WHITE)), + Span::styled("[Ctrl+B]", Style::default().fg(GHOST_WHITE).bold()), Span::styled(" to add your ", Style::default().fg(VERY_LIGHT_AZURE)), Span::styled( "Discord Username", @@ -543,6 +543,7 @@ impl Component for Status { Paragraph::new(vec![Line::raw(""), Line::raw(""), line1, line2]).block( Block::default() .title(" Device Status ") + .bold() .title_style(Style::new().fg(GHOST_WHITE)) .borders(Borders::ALL) .padding(Padding::horizontal(1)) @@ -636,6 +637,7 @@ impl Component for Status { .block( Block::default() .title(" Device Status ") + .bold() .title_style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) .padding(Padding::horizontal(1)) @@ -672,9 +674,9 @@ impl Component for Status { if node_rows.is_empty() { let line1 = Line::from(vec![ Span::styled("Press ", Style::default().fg(LIGHT_PERIWINKLE)), - Span::styled("[Ctrl+G] ", Style::default().fg(GHOST_WHITE)), + Span::styled("[Ctrl+G] ", Style::default().fg(GHOST_WHITE).bold()), Span::styled("to Add and ", Style::default().fg(LIGHT_PERIWINKLE)), - Span::styled("Start Nodes ", Style::default().fg(GHOST_WHITE)), + Span::styled("Start Nodes ", Style::default().fg(GHOST_WHITE).bold()), Span::styled("on this device", Style::default().fg(LIGHT_PERIWINKLE)), ]); @@ -694,7 +696,10 @@ impl Component for Status { .fg(LIGHT_PERIWINKLE) .block( Block::default() - .title(" Nodes (0) ".to_string()) + .title(Line::from(vec![ + Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()), + Span::styled(" (0) ", Style::default().fg(LIGHT_PERIWINKLE)), + ])) .title_style(Style::default().fg(LIGHT_PERIWINKLE)) .borders(Borders::ALL) .border_style(style::Style::default().fg(EUCALYPTUS)) @@ -714,7 +719,13 @@ impl Component for Status { .highlight_style(Style::new().reversed()) .block( Block::default() - .title(format!(" Nodes ({}) ", self.nodes_to_start)) + .title(Line::from(vec![ + Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()), + Span::styled( + format!(" ({}) ", self.nodes_to_start), + Style::default().fg(LIGHT_PERIWINKLE), + ), + ])) .padding(Padding::new(2, 2, 1, 1)) .title_style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) @@ -754,6 +765,7 @@ impl Component for Status { Block::default() .borders(Borders::ALL) .title(" Manage Nodes ") + .bold() .title_style(Style::new().fg(VIVID_SKY_BLUE)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(GHOST_WHITE)), diff --git a/node-launchpad/src/error.rs b/node-launchpad/src/error.rs index 1f487ee558..14005a87a6 100644 --- a/node-launchpad/src/error.rs +++ b/node-launchpad/src/error.rs @@ -120,6 +120,7 @@ impl ErrorPopup { Block::default() .borders(Borders::ALL) .title(format!(" {} ", self.title)) + .bold() .title_style(Style::new().fg(RED)) .padding(Padding::uniform(2)) .border_style(Style::new().fg(RED)), From 0444e46093a546ec10d8315e208b59de219a4c8f Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 11 Sep 2024 17:09:48 +0200 Subject: [PATCH 07/35] feat(launchpad): disable disk with not enough space --- .../src/components/popup/change_drive.rs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index 3c0d22906d..448c09a507 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -23,7 +23,10 @@ use ratatui::{ use crate::{ action::{Action, OptionsActions}, - components::Component, + components::{ + popup::manage_nodes::{GB, GB_PER_NODE}, + Component, + }, config::get_launchpad_nodes_data_dir_path, mode::{InputMode, Scene}, style::{ @@ -61,6 +64,7 @@ impl ChangeDrivePopup { .map(|(drive_name, mountpoint, space, available)| { let size_str = format!("{:.2} GB", *space as f64 / 1e9); let size_str_cloned = size_str.clone(); + let has_enough_space = *space >= (GB_PER_NODE * GB) as u64; DriveItem { name: drive_name.to_string(), mountpoint: mountpoint.clone(), @@ -75,6 +79,8 @@ impl ChangeDrivePopup { DriveStatus::Selected } else if !available { DriveStatus::NotAvailable + } else if !has_enough_space { + DriveStatus::NotEnoughSpace } else { DriveStatus::NotSelected }, @@ -101,7 +107,9 @@ impl ChangeDrivePopup { /// fn deselect_all(&mut self) { for item in &mut self.items.items { - if item.status != DriveStatus::NotAvailable { + if item.status != DriveStatus::NotAvailable + && item.status != DriveStatus::NotEnoughSpace + { item.status = DriveStatus::NotSelected; } } @@ -382,7 +390,8 @@ impl Component for ChangeDrivePopup { self.items.previous(); let drive = self.return_selection(); self.can_select = drive.mountpoint != self.drive_selection.mountpoint - && drive.status != DriveStatus::NotAvailable; + && drive.status != DriveStatus::NotAvailable + && drive.status != DriveStatus::NotEnoughSpace; } vec![] } @@ -391,7 +400,8 @@ impl Component for ChangeDrivePopup { self.items.next(); let drive = self.return_selection(); self.can_select = drive.mountpoint != self.drive_selection.mountpoint - && drive.status != DriveStatus::NotAvailable; + && drive.status != DriveStatus::NotAvailable + && drive.status != DriveStatus::NotEnoughSpace; } vec![] } @@ -558,6 +568,7 @@ enum DriveStatus { Selected, #[default] NotSelected, + NotEnoughSpace, NotAvailable, } @@ -586,6 +597,12 @@ impl DriveItem { Span::raw(" ".repeat(spaces)), Span::styled(self.size.clone(), Style::default().fg(GHOST_WHITE)), ]), + DriveStatus::NotEnoughSpace => Line::from(vec![ + Span::raw(" "), + Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)), + Span::raw(" ".repeat(spaces)), + Span::styled(self.size.clone(), Style::default().fg(COOL_GREY)), + ]), DriveStatus::NotAvailable => { let legend = "No Access"; let spaces = width - self.name.len() - legend.len() - " ".len() - 4; From 8e776c14c90fcb17cb787de2e0081cdbcdfa70a1 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 11 Sep 2024 18:05:28 +0200 Subject: [PATCH 08/35] feat(launchpad): avaialble disk space considers amount of nodes --- node-launchpad/src/app.rs | 3 +- .../src/components/popup/change_drive.rs | 168 ++++++++++-------- 2 files changed, 100 insertions(+), 71 deletions(-) diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index f5163a5837..f456fdb6b9 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -115,7 +115,8 @@ impl App { // Popups let reset_nodes = ResetNodesPopup::default(); let manage_nodes = ManageNodes::new(app_data.nodes_to_start, storage_mountpoint.clone())?; - let change_drive = ChangeDrivePopup::new(storage_mountpoint.clone())?; + let change_drive = + ChangeDrivePopup::new(storage_mountpoint.clone(), app_data.nodes_to_start)?; let change_connection_mode = ChangeConnectionModePopUp::new(connection_mode)?; let port_range = PortRangePopUp::new(connection_mode, port_from, port_to); let beta_programme = BetaProgramme::new(app_data.discord_username.clone()); diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index 448c09a507..8efeeaed88 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -47,56 +47,25 @@ enum ChangeDriveState { pub struct ChangeDrivePopup { active: bool, state: ChangeDriveState, - items: StatefulList, + items: Option>, drive_selection: DriveItem, drive_selection_initial_state: DriveItem, + nodes_to_start: usize, + storage_mountpoint: PathBuf, can_select: bool, // Used to enable the "Change Drive" button based on conditions } impl ChangeDrivePopup { - pub fn new(storage_mountpoint: PathBuf) -> Result { - let drives_and_space = system::get_list_of_available_drives_and_available_space()?; - - let mut selected_connection_mode: DriveItem = DriveItem::default(); - // Create a vector of DriveItem from drives_and_space - let drives_items: Vec = drives_and_space - .iter() - .map(|(drive_name, mountpoint, space, available)| { - let size_str = format!("{:.2} GB", *space as f64 / 1e9); - let size_str_cloned = size_str.clone(); - let has_enough_space = *space >= (GB_PER_NODE * GB) as u64; - DriveItem { - name: drive_name.to_string(), - mountpoint: mountpoint.clone(), - size: size_str, - status: if mountpoint == &storage_mountpoint { - selected_connection_mode = DriveItem { - name: drive_name.to_string(), - mountpoint: mountpoint.clone(), - size: size_str_cloned, - status: DriveStatus::Selected, - }; - DriveStatus::Selected - } else if !available { - DriveStatus::NotAvailable - } else if !has_enough_space { - DriveStatus::NotEnoughSpace - } else { - DriveStatus::NotSelected - }, - } - }) - .collect::>(); + pub fn new(storage_mountpoint: PathBuf, nodes_to_start: usize) -> Result { debug!("Drive Mountpoint in Config: {:?}", storage_mountpoint); - debug!("Drives and space: {:?}", drives_and_space); - debug!("Drives items: {:?}", drives_items); - let items = StatefulList::with_items(drives_items); - Ok(Self { + Ok(ChangeDrivePopup { active: false, state: ChangeDriveState::Selection, - items, - drive_selection: selected_connection_mode.clone(), - drive_selection_initial_state: selected_connection_mode.clone(), + items: None, + drive_selection: DriveItem::default(), + drive_selection_initial_state: DriveItem::default(), + nodes_to_start, + storage_mountpoint, can_select: false, }) } @@ -106,11 +75,13 @@ impl ChangeDrivePopup { /// Deselects all drives in the list of items /// fn deselect_all(&mut self) { - for item in &mut self.items.items { - if item.status != DriveStatus::NotAvailable - && item.status != DriveStatus::NotEnoughSpace - { - item.status = DriveStatus::NotSelected; + if let Some(ref mut items) = self.items { + for item in &mut items.items { + if item.status != DriveStatus::NotAvailable + && item.status != DriveStatus::NotEnoughSpace + { + item.status = DriveStatus::NotSelected; + } } } } @@ -118,32 +89,74 @@ impl ChangeDrivePopup { /// fn assign_drive_selection(&mut self) { self.deselect_all(); - if let Some(i) = self.items.state.selected() { - self.items.items[i].status = DriveStatus::Selected; - self.drive_selection = self.items.items[i].clone(); + if let Some(ref mut items) = self.items { + if let Some(i) = items.state.selected() { + items.items[i].status = DriveStatus::Selected; + self.drive_selection = items.items[i].clone(); + } } } /// Highlights the drive that is currently selected in the list of items. /// fn select_drive(&mut self) { self.deselect_all(); - for (index, item) in self.items.items.iter_mut().enumerate() { - if item.mountpoint == self.drive_selection.mountpoint { - item.status = DriveStatus::Selected; - self.items.state.select(Some(index)); - break; + if let Some(ref mut items) = self.items { + for (index, item) in items.items.iter_mut().enumerate() { + if item.mountpoint == self.drive_selection.mountpoint { + item.status = DriveStatus::Selected; + items.state.select(Some(index)); + break; + } } } } /// Returns the highlighted drive in the list of items. /// fn return_selection(&mut self) -> DriveItem { - if let Some(i) = self.items.state.selected() { - return self.items.items[i].clone(); + if let Some(ref mut items) = self.items { + if let Some(i) = items.state.selected() { + return items.items[i].clone(); + } } DriveItem::default() } + /// Updates the drive items based on the current nodes_to_start value. + fn update_drive_items(&mut self) -> Result<()> { + let drives_and_space = system::get_list_of_available_drives_and_available_space()?; + let drives_items: Vec = drives_and_space + .iter() + .map(|(drive_name, mountpoint, space, available)| { + let size_str = format!("{:.2} GB", *space as f64 / 1e9); + let has_enough_space = *space >= (GB_PER_NODE * GB * self.nodes_to_start) as u64; + DriveItem { + name: drive_name.to_string(), + mountpoint: mountpoint.clone(), + size: size_str.clone(), + status: if *mountpoint == self.storage_mountpoint { + self.drive_selection = DriveItem { + name: drive_name.to_string(), + mountpoint: mountpoint.clone(), + size: size_str.clone(), + status: DriveStatus::Selected, + }; + DriveStatus::Selected + } else if !available { + DriveStatus::NotAvailable + } else if !has_enough_space { + DriveStatus::NotEnoughSpace + } else { + DriveStatus::NotSelected + }, + } + }) + .collect(); + self.items = Some(StatefulList::with_items(drives_items.clone())); + debug!("Drives and space: {:?}", drives_and_space); + debug!("Drives items: {:?}", drives_items); + Ok(()) + } + // -- Draw functions -- // Draws the Drive Selection screen @@ -180,6 +193,8 @@ impl ChangeDrivePopup { // Drive selector let items: Vec = self .items + .as_ref() + .unwrap() .items .iter() .enumerate() @@ -191,7 +206,7 @@ impl ChangeDrivePopup { .highlight_style(Style::default().bg(INDIGO)) .highlight_spacing(HighlightSpacing::Always); - f.render_stateful_widget(items, layer_two[0], &mut self.items.state); + f.render_stateful_widget(items, layer_two[0], &mut self.items.clone().unwrap().state); // Dash let dash = Block::new() @@ -386,22 +401,28 @@ impl Component for ChangeDrivePopup { vec![Action::SwitchScene(Scene::Options)] } KeyCode::Up => { - if self.items.items.len() > 1 { - self.items.previous(); - let drive = self.return_selection(); - self.can_select = drive.mountpoint != self.drive_selection.mountpoint - && drive.status != DriveStatus::NotAvailable - && drive.status != DriveStatus::NotEnoughSpace; + if let Some(ref mut items) = self.items { + if items.items.len() > 1 { + items.previous(); + let drive = self.return_selection(); + self.can_select = drive.mountpoint + != self.drive_selection.mountpoint + && drive.status != DriveStatus::NotAvailable + && drive.status != DriveStatus::NotEnoughSpace; + } } vec![] } KeyCode::Down => { - if self.items.items.len() > 1 { - self.items.next(); - let drive = self.return_selection(); - self.can_select = drive.mountpoint != self.drive_selection.mountpoint - && drive.status != DriveStatus::NotAvailable - && drive.status != DriveStatus::NotEnoughSpace; + if let Some(ref mut items) = self.items { + if items.items.len() > 1 { + items.next(); + let drive = self.return_selection(); + self.can_select = drive.mountpoint + != self.drive_selection.mountpoint + && drive.status != DriveStatus::NotAvailable + && drive.status != DriveStatus::NotEnoughSpace; + } } vec![] } @@ -465,6 +486,7 @@ impl Component for ChangeDrivePopup { self.active = true; self.can_select = false; self.state = ChangeDriveState::Selection; + let _ = self.update_drive_items(); self.select_drive(); Some(Action::SwitchInputMode(InputMode::Entry)) } @@ -480,6 +502,12 @@ impl Component for ChangeDrivePopup { self.select_drive(); None } + // We need to refresh the list of available drives because of the space + Action::StoreNodesToStart(ref nodes_to_start) => { + self.nodes_to_start = *nodes_to_start; + let _ = self.update_drive_items(); + None + } _ => None, }; Ok(send_back) @@ -518,7 +546,7 @@ impl Component for ChangeDrivePopup { } } -#[derive(Default)] +#[derive(Default, Clone)] struct StatefulList { state: ListState, items: Vec, From 20a8ea1fd9014aeea99a81dfff40912fc4c093ff Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 12 Sep 2024 09:10:54 +0200 Subject: [PATCH 09/35] fix(launchpad): underline style in beta programme makes the screen flicker --- node-launchpad/src/components/popup/beta_programme.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index bb64865c19..7dc70f7576 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -226,11 +226,7 @@ impl Component for BetaProgramme { ); let input = Paragraph::new(Span::styled( format!("{}{} ", spaces, self.discord_input_filed.value()), - Style::default() - .fg(VIVID_SKY_BLUE) - .bg(INDIGO) - .underlined() - .underline_color(VIVID_SKY_BLUE), + Style::default().fg(VIVID_SKY_BLUE).bg(INDIGO).underlined(), )) .alignment(Alignment::Center); f.render_widget(input, layer_two[1]); From fcdc10d2ce0a0ae4ca68ce58d34c0e9ef354f577 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 12 Sep 2024 10:12:43 +0200 Subject: [PATCH 10/35] feat(launchpad): adding quit to options screen --- node-launchpad/src/components/options.rs | 215 ++++++++++++----------- 1 file changed, 113 insertions(+), 102 deletions(-) diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index 2e234726f9..2be6af49d5 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -71,10 +71,11 @@ impl Component for Options { .constraints( [ Constraint::Length(1), - Constraint::Length(9), - Constraint::Length(5), - Constraint::Length(5), - Constraint::Length(5), + Constraint::Length(7), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), ] .as_ref(), ) @@ -85,6 +86,8 @@ impl Component for Options { f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Options); // Storage Drive + let port_legend = " Edit Port Range "; + let port_key = " [Ctrl+P] "; let block1 = Block::default() .title(" Device Options ") .title_style(Style::default().bold().fg(GHOST_WHITE)) @@ -93,11 +96,7 @@ impl Component for Options { .border_style(Style::default().fg(VERY_LIGHT_AZURE)); let storage_drivename = Table::new( vec![ - Row::new(vec![ - Cell::from(Span::raw(" ")), // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), + Row::new(vec![Line::from(vec![])]), Row::new(vec![ Cell::from( Line::from(vec![Span::styled( @@ -121,11 +120,6 @@ impl Component for Options { .alignment(Alignment::Right), ), ]), - Row::new(vec![ - Cell::from(Span::raw(" ")), // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), Row::new(vec![ Cell::from( Line::from(vec![Span::styled( @@ -149,11 +143,6 @@ impl Component for Options { .alignment(Alignment::Right), ), ]), - Row::new(vec![ - Cell::from(Span::raw(" ")), // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), Row::new(vec![ Cell::from( Line::from(vec![Span::styled( @@ -182,7 +171,7 @@ impl Component for Options { Cell::from( Line::from(vec![ Span::styled( - " Edit Port Range ", + port_legend, if self.connection_mode == ConnectionMode::CustomPorts { Style::default().fg(VERY_LIGHT_AZURE) } else { @@ -190,7 +179,7 @@ impl Component for Options { }, ), Span::styled( - " [Ctrl+P] ", + port_key, if self.connection_mode == ConnectionMode::CustomPorts { Style::default().fg(GHOST_WHITE) } else { @@ -201,17 +190,20 @@ impl Component for Options { .alignment(Alignment::Right), ), ]), + Row::new(vec![Line::from(vec![])]), ], &[ Constraint::Length(18), - Constraint::Percentage(25), Constraint::Fill(1), + Constraint::Length((port_legend.len() + port_key.len()) as u16), ], ) .block(block1) .style(Style::default().fg(GHOST_WHITE)); // Beta Rewards Program + let beta_legend = " Edit Discord Username "; + let beta_key = " [Ctrl+B] "; let block2 = Block::default() .title(" Beta Rewards Program ") .title_style(Style::default().bold().fg(GHOST_WHITE)) @@ -219,50 +211,41 @@ impl Component for Options { .borders(Borders::ALL) .border_style(Style::default().fg(VERY_LIGHT_AZURE)); let beta_rewards = Table::new( - vec![ - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - Cell::from( - Line::from(vec![Span::styled( - " Discord Username: ", - Style::default().fg(LIGHT_PERIWINKLE), - )]) - .alignment(Alignment::Left), - ), - Cell::from( - Line::from(vec![Span::styled( - format!(" {} ", self.discord_username), - Style::default().fg(VIVID_SKY_BLUE), - )]) - .alignment(Alignment::Left), - ), - Cell::from( - Line::from(vec![ - Span::styled( - " Edit Discord Username ", - Style::default().fg(VERY_LIGHT_AZURE), - ), - Span::styled(" [Ctrl+B] ", Style::default().fg(GHOST_WHITE)), - ]) - .alignment(Alignment::Right), - ), - ]), - ], + vec![Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Discord Username: ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![Span::styled( + format!(" {} ", self.discord_username), + Style::default().fg(VIVID_SKY_BLUE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![ + Span::styled(beta_legend, Style::default().fg(VERY_LIGHT_AZURE)), + Span::styled(beta_key, Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right), + ), + ])], &[ Constraint::Length(18), - Constraint::Percentage(25), Constraint::Fill(1), + Constraint::Length((beta_legend.len() + beta_key.len()) as u16), ], ) .block(block2) .style(Style::default().fg(GHOST_WHITE)); // Access Logs + let logs_legend = " Access Logs "; + let logs_key = " [Ctrl+L] "; let block3 = Block::default() .title(" Access Logs ") .title_style(Style::default().bold().fg(GHOST_WHITE)) @@ -270,35 +253,33 @@ impl Component for Options { .borders(Borders::ALL) .border_style(Style::default().fg(VERY_LIGHT_AZURE)); let logs_folder = Table::new( - vec![ - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - Cell::from( - Line::from(vec![Span::styled( - " Open the Logs folder on this device ", - Style::default().fg(LIGHT_PERIWINKLE), - )]) - .alignment(Alignment::Left), - ), - Cell::from( - Line::from(vec![ - Span::styled(" Access Logs ", Style::default().fg(VERY_LIGHT_AZURE)), - Span::styled(" [Ctrl+L] ", Style::default().fg(GHOST_WHITE)), - ]) - .alignment(Alignment::Right), - ), - ]), + vec![Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Open the Logs folder on this device ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![ + Span::styled(logs_legend, Style::default().fg(VERY_LIGHT_AZURE)), + Span::styled(logs_key, Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right), + ), + ])], + &[ + Constraint::Fill(1), + Constraint::Length((logs_legend.len() + logs_key.len()) as u16), ], - &[Constraint::Percentage(50), Constraint::Percentage(50)], ) .block(block3) .style(Style::default().fg(GHOST_WHITE)); // Reset All Nodes + let reset_legend = " Begin Reset "; + let reset_key = " [Ctrl+ R] "; let block4 = Block::default() .title(" Reset All Nodes ") .title_style(Style::default().bold().fg(GHOST_WHITE)) @@ -306,38 +287,68 @@ impl Component for Options { .borders(Borders::ALL) .border_style(Style::default().fg(EUCALYPTUS)); let reset_nodes = Table::new( - vec![ - Row::new(vec![ - Cell::from(Span::raw(" ")), // Empty row for padding - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - Cell::from( - Line::from(vec![Span::styled( - " Remove and Reset all Nodes on this device ", - Style::default().fg(LIGHT_PERIWINKLE), - )]) - .alignment(Alignment::Left), - ), - Cell::from( - Line::from(vec![ - Span::styled(" Begin Reset ", Style::default().fg(EUCALYPTUS)), - Span::styled(" [Ctrl+R] ", Style::default().fg(GHOST_WHITE)), - ]) - .alignment(Alignment::Right), - ), - ]), + vec![Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Remove and Reset all Nodes on this device ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![ + Span::styled(reset_legend, Style::default().fg(EUCALYPTUS)), + Span::styled(reset_key, Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right), + ), + ])], + &[ + Constraint::Fill(1), + Constraint::Length((reset_legend.len() + reset_key.len()) as u16), ], - &[Constraint::Percentage(50), Constraint::Percentage(50)], ) .block(block4) .style(Style::default().fg(GHOST_WHITE)); + // Quit + let quit_legend = " Quit "; + let quit_key = " [Q] "; + let block5 = Block::default() + .style(Style::default().fg(GHOST_WHITE)) + .borders(Borders::ALL) + .border_style(Style::default().fg(VIVID_SKY_BLUE)); + let quit = Table::new( + vec![Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Close Launchpad (your nodes will keep running in the background) ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![ + Span::styled(quit_legend, Style::default().fg(VIVID_SKY_BLUE)), + Span::styled(quit_key, Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right), + ), + ])], + &[ + Constraint::Fill(1), + Constraint::Length((quit_legend.len() + quit_key.len()) as u16), + ], + ) + .block(block5) + .style(Style::default().fg(GHOST_WHITE)); + // Render the tables in their respective sections f.render_widget(storage_drivename, layout[1]); f.render_widget(beta_rewards, layout[2]); f.render_widget(logs_folder, layout[3]); f.render_widget(reset_nodes, layout[4]); + f.render_widget(quit, layout[5]); Ok(()) } From 2f0f00ef7884d3908dbdbff4a99a1fdeabd78025 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 12 Sep 2024 10:30:15 +0200 Subject: [PATCH 11/35] feat(launchpad): we hide change port hotkey when not in custom ports --- node-launchpad/src/components/options.rs | 26 ++++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index 2be6af49d5..1cdd4cf9a6 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -169,24 +169,14 @@ impl Component for Options { .alignment(Alignment::Left), ), Cell::from( - Line::from(vec![ - Span::styled( - port_legend, - if self.connection_mode == ConnectionMode::CustomPorts { - Style::default().fg(VERY_LIGHT_AZURE) - } else { - Style::default().fg(COOL_GREY) - }, - ), - Span::styled( - port_key, - if self.connection_mode == ConnectionMode::CustomPorts { - Style::default().fg(GHOST_WHITE) - } else { - Style::default().fg(COOL_GREY) - }, - ), - ]) + Line::from(if self.connection_mode == ConnectionMode::CustomPorts { + vec![ + Span::styled(port_legend, Style::default().fg(VERY_LIGHT_AZURE)), + Span::styled(port_key, Style::default().fg(GHOST_WHITE)), + ] + } else { + vec![] + }) .alignment(Alignment::Right), ), ]), From aa2782ab8f762b9efd87343589b3a584e55c45a9 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 12 Sep 2024 10:37:52 +0200 Subject: [PATCH 12/35] feat(launchpad): forward to status screen after changing options --- node-launchpad/src/components/popup/beta_programme.rs | 2 +- node-launchpad/src/components/popup/change_drive.rs | 2 +- node-launchpad/src/components/popup/connection_mode.rs | 2 +- node-launchpad/src/components/popup/port_range.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index 7dc70f7576..8940438eec 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -69,7 +69,7 @@ impl BetaProgramme { vec![ Action::StoreDiscordUserName(self.discord_input_filed.value().to_string()), Action::OptionsActions(OptionsActions::UpdateBetaProgrammeUsername(username)), - Action::SwitchScene(Scene::Options), + Action::SwitchScene(Scene::Status), ] } KeyCode::Esc => { diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index 8efeeaed88..ad58e21dcc 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -451,7 +451,7 @@ impl Component for ChangeDrivePopup { self.drive_selection.mountpoint.clone(), self.drive_selection.name.clone(), )), - Action::SwitchScene(Scene::Options), + Action::SwitchScene(Scene::Status), ] } Err(e) => { diff --git a/node-launchpad/src/components/popup/connection_mode.rs b/node-launchpad/src/components/popup/connection_mode.rs index 34440af4fb..0cff9bfbb3 100644 --- a/node-launchpad/src/components/popup/connection_mode.rs +++ b/node-launchpad/src/components/popup/connection_mode.rs @@ -138,7 +138,7 @@ impl Component for ChangeConnectionModePopUp { ), }) } else { - Action::SwitchScene(Scene::Options) + Action::SwitchScene(Scene::Status) }, ] } else { diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index dd6fa90b39..b3267969b2 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -393,9 +393,9 @@ impl Component for PortRangePopUp { } PortRangeState::ConfirmChange => match key.code { KeyCode::Enter => { - debug!("Got Enter, saving the ports and switching to Options Screen",); + debug!("Got Enter, saving the ports and switching to Status Screen",); self.state = PortRangeState::Selection; - vec![Action::SwitchScene(Scene::Options)] + vec![Action::SwitchScene(Scene::Status)] } _ => vec![], }, From 622ea769e42ef29073cfe8cb564e5581776185e9 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 12 Sep 2024 11:49:32 +0200 Subject: [PATCH 13/35] fix(launchpad): default mountpoint not available on disks list --- node-launchpad/src/components/popup/change_drive.rs | 7 +++++++ node-launchpad/src/system.rs | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index ad58e21dcc..a2cfd0e024 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -508,6 +508,13 @@ impl Component for ChangeDrivePopup { let _ = self.update_drive_items(); None } + Action::StoreStorageDrive(mountpoint, _drive_name) => { + self.storage_mountpoint = mountpoint; + let _ = self.update_drive_items(); + self.select_drive(); + None + } + _ => None, }; Ok(send_back) diff --git a/node-launchpad/src/system.rs b/node-launchpad/src/system.rs index 7d57ae91e6..d1691e0d80 100644 --- a/node-launchpad/src/system.rs +++ b/node-launchpad/src/system.rs @@ -67,6 +67,11 @@ pub fn get_list_of_available_drives_and_available_space( let disks = Disks::new_with_refreshed_list(); let mut drives: Vec<(String, PathBuf, u64, bool)> = Vec::new(); + let default_mountpoint = match get_default_mount_point() { + Ok((_name, mountpoint)) => mountpoint, + Err(_) => PathBuf::new(), + }; + for disk in disks.list() { let disk_info = ( disk.name() @@ -76,7 +81,8 @@ pub fn get_list_of_available_drives_and_available_space( .to_string(), disk.mount_point().to_path_buf(), disk.available_space(), - has_read_write_access(disk.mount_point().to_path_buf()), + has_read_write_access(disk.mount_point().to_path_buf()) + || default_mountpoint == disk.mount_point().to_path_buf(), ); // We avoid adding the same disk multiple times if it's mounted in multiple places From 19b1dcac74bd3d94d422fd711d7c7104f677ef87 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 16 Sep 2024 17:48:28 +0200 Subject: [PATCH 14/35] feat(launchpad): flexible status layout --- node-launchpad/src/components/footer.rs | 31 ++++++----- node-launchpad/src/components/status.rs | 72 +++++++++++++++---------- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/node-launchpad/src/components/footer.rs b/node-launchpad/src/components/footer.rs index ff5a8eb045..c1d74db1a1 100644 --- a/node-launchpad/src/components/footer.rs +++ b/node-launchpad/src/components/footer.rs @@ -12,6 +12,7 @@ use ratatui::{prelude::*, widgets::*}; pub enum NodesToStart { Configured, NotConfigured, + Running, } #[derive(Default)] @@ -33,23 +34,26 @@ impl StatefulWidget for Footer { ) }; - let command1 = vec![ + let commands = vec![ Span::styled("[Ctrl+G] ", Style::default().fg(GHOST_WHITE)), Span::styled("Manage Nodes", Style::default().fg(EUCALYPTUS)), - ]; - let command2 = vec![ + Span::styled(" ", Style::default()), Span::styled("[Ctrl+S] ", command_style), Span::styled("Start Nodes", text_style), - ]; - let command3 = vec![ + Span::styled(" ", Style::default()), Span::styled("[Ctrl+X] ", command_style), - Span::styled("Stop Nodes", text_style), + Span::styled( + "Stop Nodes", + if matches!(state, NodesToStart::Running) { + Style::default().fg(EUCALYPTUS) + } else { + Style::default().fg(COOL_GREY) + }, + ), ]; - let cell1 = Cell::from(Line::from(command1)); - let cell2 = Cell::from(Line::from(command2)); - let cell3 = Cell::from(Line::from(command3)); - let row = Row::new(vec![cell1, cell2, cell3]); + let cell1 = Cell::from(Line::from(commands)); + let row = Row::new(vec![cell1]); let table = Table::new(vec![row], vec![Constraint::Max(1)]) .block( @@ -58,12 +62,7 @@ impl StatefulWidget for Footer { .border_style(Style::default().fg(EUCALYPTUS)) .padding(Padding::horizontal(1)), ) - .widths(vec![ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - ]); + .widths(vec![Constraint::Fill(1)]); StatefulWidget::render(table, area, buf, &mut TableState::default()); } diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 74a71f98cd..4fbe03cff2 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -552,7 +552,18 @@ impl Component for Status { layout[1], ); } else { - // Device Status as a table + // Device Status as a block with two tables so we can shrink the screen + // and preserve as much as we can information + + let combined_block = Block::default() + .title(" Device Status ") + .bold() + .title_style(Style::default().fg(GHOST_WHITE)) + .borders(Borders::ALL) + .padding(Padding::horizontal(1)) + .style(Style::default().fg(VERY_LIGHT_AZURE)); + + f.render_widget(combined_block.clone(), layout[1]); let storage_allocated_row = Row::new(vec![ Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE), @@ -585,11 +596,16 @@ impl Component for Status { let connection_mode_row = Row::new(vec![ Cell::new("Connection".to_string()).fg(GHOST_WHITE), - Cell::new(connection_mode_string).fg(GHOST_WHITE), + Cell::new(connection_mode_string).fg(LIGHT_PERIWINKLE), ]); - // Combine "Nanos Earned" and "Discord Username" into a single row - let discord_username_placeholder = "Discord Username: "; // Used to calculate the width of the username column + let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row]; + let stats_width = [Constraint::Length(5)]; + let column_constraints = [Constraint::Length(23), Constraint::Fill(1)]; + let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints); + + // Combine "Nanos Earned" and "Username" into a single row + let discord_username_placeholder = "Username: "; // Used to calculate the width of the username column let discord_username_title = Span::styled( discord_username_placeholder, Style::default().fg(VIVID_SKY_BLUE), @@ -619,13 +635,8 @@ impl Component for Status { ), ]); - let stats_rows = vec![ - storage_allocated_row, - memory_use_row, - connection_mode_row, - total_nanos_earned_and_discord_row, - ]; - let stats_width = [Constraint::Length(5)]; + let nanos_discord_rows = vec![total_nanos_earned_and_discord_row]; + let nanos_discord_width = [Constraint::Length(5)]; let column_constraints = [ Constraint::Length(23), Constraint::Fill(1), @@ -633,18 +644,19 @@ impl Component for Status { (discord_username_placeholder.len() + self.discord_username.len()) as u16, ), ]; - let stats_table = Table::new(stats_rows, stats_width) - .block( - Block::default() - .title(" Device Status ") - .bold() - .title_style(Style::default().fg(GHOST_WHITE)) - .borders(Borders::ALL) - .padding(Padding::horizontal(1)) - .style(Style::default().fg(VERY_LIGHT_AZURE)), - ) - .widths(column_constraints); - f.render_widget(stats_table, layout[1]); + let nanos_discord_table = + Table::new(nanos_discord_rows, nanos_discord_width).widths(column_constraints); + + let inner_area = combined_block.inner(layout[1]); + let device_layout = Layout::new( + Direction::Vertical, + vec![Constraint::Length(5), Constraint::Length(1)], + ) + .split(inner_area); + + // Render both tables inside the combined block + f.render_widget(stats_table, device_layout[0]); + f.render_widget(nanos_discord_table, device_layout[1]); }; // ==== Node Status ===== @@ -709,10 +721,10 @@ impl Component for Status { ); } else { let node_widths = [ - Constraint::Max(15), - Constraint::Min(40), - Constraint::Max(10), - Constraint::Max(10), + Constraint::Length(11), + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Length(8), ]; let table = Table::new(node_rows.clone(), node_widths) .column_spacing(2) @@ -739,7 +751,11 @@ impl Component for Status { let footer = Footer::default(); let footer_state = if !node_rows.is_empty() { - &mut NodesToStart::Configured + if !self.get_running_nodes().is_empty() { + &mut NodesToStart::Running + } else { + &mut NodesToStart::Configured + } } else { &mut NodesToStart::NotConfigured }; From 2d537b999a0279cf83e868d30ab13be98cf6110b Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 17 Sep 2024 17:12:19 +0200 Subject: [PATCH 15/35] feat(launchpad): discord name set or not set in device status --- node-launchpad/src/components/status.rs | 230 +++++++++++------------- 1 file changed, 104 insertions(+), 126 deletions(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 4fbe03cff2..35c2e6c85c 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -525,140 +525,118 @@ impl Component for Status { // ==== Device Status ===== - if self.discord_username.is_empty() { - let line1 = Line::from(vec![Span::styled( - "Add this device to the Beta Rewards Program", - Style::default().fg(VERY_LIGHT_AZURE), - )]); - let line2 = Line::from(vec![ - Span::styled("Press ", Style::default().fg(VERY_LIGHT_AZURE)), - Span::styled("[Ctrl+B]", Style::default().fg(GHOST_WHITE).bold()), - Span::styled(" to add your ", Style::default().fg(VERY_LIGHT_AZURE)), - Span::styled( - "Discord Username", - Style::default().fg(VERY_LIGHT_AZURE).bold(), - ), - ]); - f.render_widget( - Paragraph::new(vec![Line::raw(""), Line::raw(""), line1, line2]).block( - Block::default() - .title(" Device Status ") - .bold() - .title_style(Style::new().fg(GHOST_WHITE)) - .borders(Borders::ALL) - .padding(Padding::horizontal(1)) - .border_style(Style::new().fg(VERY_LIGHT_AZURE)), - ), - layout[1], - ); + // Device Status as a block with two tables so we can shrink the screen + // and preserve as much as we can information + + let combined_block = Block::default() + .title(" Device Status ") + .bold() + .title_style(Style::default().fg(GHOST_WHITE)) + .borders(Borders::ALL) + .padding(Padding::horizontal(1)) + .style(Style::default().fg(VERY_LIGHT_AZURE)); + + f.render_widget(combined_block.clone(), layout[1]); + + let storage_allocated_row = Row::new(vec![ + Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE), + Cell::new(format!("{} GB", self.nodes_to_start * GB_PER_NODE)).fg(GHOST_WHITE), + ]); + let memory_use_val = if self.node_stats.memory_usage_mb as f64 / 1024_f64 > 1.0 { + format!( + "{:.2} GB", + self.node_stats.memory_usage_mb as f64 / 1024_f64 + ) } else { - // Device Status as a block with two tables so we can shrink the screen - // and preserve as much as we can information - - let combined_block = Block::default() - .title(" Device Status ") - .bold() - .title_style(Style::default().fg(GHOST_WHITE)) - .borders(Borders::ALL) - .padding(Padding::horizontal(1)) - .style(Style::default().fg(VERY_LIGHT_AZURE)); - - f.render_widget(combined_block.clone(), layout[1]); - - let storage_allocated_row = Row::new(vec![ - Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE), - Cell::new(format!("{} GB", self.nodes_to_start * GB_PER_NODE)).fg(GHOST_WHITE), - ]); - let memory_use_val = if self.node_stats.memory_usage_mb as f64 / 1024_f64 > 1.0 { - format!( - "{:.2} GB", - self.node_stats.memory_usage_mb as f64 / 1024_f64 - ) - } else { - format!("{} MB", self.node_stats.memory_usage_mb) - }; - - let memory_use_row = Row::new(vec![ - Cell::new("Memory Use".to_string()).fg(GHOST_WHITE), - Cell::new(memory_use_val).fg(GHOST_WHITE), - ]); - - let connection_mode_string = match self.connection_mode { - ConnectionMode::HomeNetwork => "Home Network", - ConnectionMode::UPnP => "UPnP", - ConnectionMode::CustomPorts => &format!( - "Custom Ports {}-{}", - self.port_from.unwrap_or(PORT_MIN), - self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION) - ), - ConnectionMode::Automatic => "Automatic", - }; + format!("{} MB", self.node_stats.memory_usage_mb) + }; - let connection_mode_row = Row::new(vec![ - Cell::new("Connection".to_string()).fg(GHOST_WHITE), - Cell::new(connection_mode_string).fg(LIGHT_PERIWINKLE), - ]); + let memory_use_row = Row::new(vec![ + Cell::new("Memory Use".to_string()).fg(GHOST_WHITE), + Cell::new(memory_use_val).fg(GHOST_WHITE), + ]); + + let connection_mode_string = match self.connection_mode { + ConnectionMode::HomeNetwork => "Home Network", + ConnectionMode::UPnP => "UPnP", + ConnectionMode::CustomPorts => &format!( + "Custom Ports {}-{}", + self.port_from.unwrap_or(PORT_MIN), + self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION) + ), + ConnectionMode::Automatic => "Automatic", + }; - let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row]; - let stats_width = [Constraint::Length(5)]; - let column_constraints = [Constraint::Length(23), Constraint::Fill(1)]; - let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints); + let connection_mode_row = Row::new(vec![ + Cell::new("Connection".to_string()).fg(GHOST_WHITE), + Cell::new(connection_mode_string).fg(LIGHT_PERIWINKLE), + ]); + + let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row]; + let stats_width = [Constraint::Length(5)]; + let column_constraints = [Constraint::Length(23), Constraint::Fill(1)]; + let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints); + + // Combine "Nanos Earned" and "Username" into a single row + let discord_username_placeholder = "Username: "; // Used to calculate the width of the username column + let discord_username_no_username = "[Ctrl+B] to set"; + let discord_username_title = Span::styled( + discord_username_placeholder, + Style::default().fg(VIVID_SKY_BLUE), + ); - // Combine "Nanos Earned" and "Username" into a single row - let discord_username_placeholder = "Username: "; // Used to calculate the width of the username column - let discord_username_title = Span::styled( - discord_username_placeholder, + let discord_username = if !self.discord_username.is_empty() { + Span::styled( + self.discord_username.clone(), Style::default().fg(VIVID_SKY_BLUE), - ); - - let discord_username = if !self.discord_username.is_empty() { - Span::styled( - self.discord_username.clone(), - Style::default().fg(VIVID_SKY_BLUE), - ) - .bold() - } else { - Span::styled( - "[Ctrl+B] to set".to_string(), - Style::default().fg(GHOST_WHITE), - ) - }; - - let total_nanos_earned_and_discord_row = Row::new(vec![ - Cell::new("Nanos Earned".to_string()).fg(VIVID_SKY_BLUE), - Cell::new(self.node_stats.forwarded_rewards.to_string()) - .fg(VIVID_SKY_BLUE) - .bold(), - Cell::new( - Line::from(vec![discord_username_title, discord_username]) - .alignment(Alignment::Right), - ), - ]); - - let nanos_discord_rows = vec![total_nanos_earned_and_discord_row]; - let nanos_discord_width = [Constraint::Length(5)]; - let column_constraints = [ - Constraint::Length(23), - Constraint::Fill(1), - Constraint::Length( - (discord_username_placeholder.len() + self.discord_username.len()) as u16, - ), - ]; - let nanos_discord_table = - Table::new(nanos_discord_rows, nanos_discord_width).widths(column_constraints); - - let inner_area = combined_block.inner(layout[1]); - let device_layout = Layout::new( - Direction::Vertical, - vec![Constraint::Length(5), Constraint::Length(1)], ) - .split(inner_area); - - // Render both tables inside the combined block - f.render_widget(stats_table, device_layout[0]); - f.render_widget(nanos_discord_table, device_layout[1]); + .bold() + } else { + Span::styled( + discord_username_no_username, + Style::default().fg(GHOST_WHITE), + ) }; + let total_nanos_earned_and_discord_row = Row::new(vec![ + Cell::new("Nanos Earned".to_string()).fg(VIVID_SKY_BLUE), + Cell::new(self.node_stats.forwarded_rewards.to_string()) + .fg(VIVID_SKY_BLUE) + .bold(), + Cell::new( + Line::from(vec![discord_username_title, discord_username]) + .alignment(Alignment::Right), + ), + ]); + + let nanos_discord_rows = vec![total_nanos_earned_and_discord_row]; + let nanos_discord_width = [Constraint::Length(5)]; + let column_constraints = [ + Constraint::Length(23), + Constraint::Fill(1), + Constraint::Length( + discord_username_placeholder.len() as u16 + + if !self.discord_username.is_empty() { + self.discord_username.len() as u16 + } else { + discord_username_no_username.len() as u16 + }, + ), + ]; + let nanos_discord_table = + Table::new(nanos_discord_rows, nanos_discord_width).widths(column_constraints); + + let inner_area = combined_block.inner(layout[1]); + let device_layout = Layout::new( + Direction::Vertical, + vec![Constraint::Length(5), Constraint::Length(1)], + ) + .split(inner_area); + + // Render both tables inside the combined block + f.render_widget(stats_table, device_layout[0]); + f.render_widget(nanos_discord_table, device_layout[1]); + // ==== Node Status ===== let node_rows: Vec<_> = self From 802d0cd8a3cf2f4aa32a5b45ad460d7a4bf780b8 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 17 Sep 2024 12:18:16 +0200 Subject: [PATCH 16/35] fix(launchpad): some error handling when sn node manager fails --- node-launchpad/src/components/options.rs | 6 ++-- node-launchpad/src/node_mgmt.rs | 38 ++++++++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index 1cdd4cf9a6..36a3f666e9 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -269,7 +269,7 @@ impl Component for Options { // Reset All Nodes let reset_legend = " Begin Reset "; - let reset_key = " [Ctrl+ R] "; + let reset_key = " [Ctrl+R] "; let block4 = Block::default() .title(" Reset All Nodes ") .title_style(Style::default().bold().fg(GHOST_WHITE)) @@ -302,8 +302,8 @@ impl Component for Options { .style(Style::default().fg(GHOST_WHITE)); // Quit - let quit_legend = " Quit "; - let quit_key = " [Q] "; + let quit_legend = "Quit "; + let quit_key = "[Q] "; let block5 = Block::default() .style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index 9030247e32..f5b9786bc6 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -17,7 +17,7 @@ use sn_releases::{self, ReleaseType, SafeReleaseRepoActions}; pub const PORT_MAX: u32 = 65535; pub const PORT_MIN: u32 = 1024; -const PORT_ASSIGNMENT_MAX_RETRIES: u32 = 5; +const NODE_ADD_MAX_RETRIES: u32 = 5; /// Stop the specified services pub fn stop_nodes(services: Vec, action_sender: UnboundedSender) { @@ -332,7 +332,7 @@ async fn add_nodes( ) { let mut retry_count = 0; - while nodes_to_add > 0 && retry_count < PORT_ASSIGNMENT_MAX_RETRIES { + while nodes_to_add > 0 && retry_count < NODE_ADD_MAX_RETRIES { // Find the next available port while used_ports.contains(current_port) && *current_port <= max_port { *current_port += 1; @@ -397,16 +397,42 @@ async fn add_nodes( "Port {} is being used, retrying with a different port. Attempt {}/{}", current_port, retry_count + 1, - PORT_ASSIGNMENT_MAX_RETRIES + NODE_ADD_MAX_RETRIES ); - *current_port += 1; - retry_count += 1; + } else if err + .to_string() + .contains("Failed to add one or more services") + && retry_count >= NODE_ADD_MAX_RETRIES + { + if let Err(err) = action_sender.send(Action::StatusActions( + StatusActions::ErrorScalingUpNodes { + raw_error: "When trying to add a node, we failed.\n\n\ + Maybe you ran out of disk space?\n\n\ + Maybe you need to change the port range\n\n" + .to_string(), + }, + )) { + error!("Error while sending action: {err:?}"); + } } else { error!("Range of ports to be used {:?}", *current_port..max_port); error!("Error while adding node on port {}: {err:?}", current_port); - retry_count += 1; } + // In case of error, we increase the port and the retry count + *current_port += 1; + retry_count += 1; } } } + if retry_count >= NODE_ADD_MAX_RETRIES { + if let Err(err) = action_sender.send(Action::StatusActions(StatusActions::ErrorScalingUpNodes { + raw_error: format!( + "When trying to assign an available port to run a node, we reached the maximum amount of retries ({}).", + NODE_ADD_MAX_RETRIES + ), + })) + { + error!("Error while sending action: {err:?}"); + } + } } From 028035ad97eea3373575895e31594b07b0543f76 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 17 Sep 2024 13:17:54 +0200 Subject: [PATCH 17/35] fix(launchpad): avoid overflow when computing needed space --- .../src/components/popup/change_drive.rs | 3 ++- node-launchpad/src/node_mgmt.rs | 15 ++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index a2cfd0e024..c73dbffee7 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -128,7 +128,8 @@ impl ChangeDrivePopup { .iter() .map(|(drive_name, mountpoint, space, available)| { let size_str = format!("{:.2} GB", *space as f64 / 1e9); - let has_enough_space = *space >= (GB_PER_NODE * GB * self.nodes_to_start) as u64; + let has_enough_space = *space as u128 + >= (GB_PER_NODE as u128 * GB as u128 * self.nodes_to_start as u128); DriveItem { name: drive_name.to_string(), mountpoint: mountpoint.clone(), diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index f5b9786bc6..1b591e5a95 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -408,7 +408,7 @@ async fn add_nodes( StatusActions::ErrorScalingUpNodes { raw_error: "When trying to add a node, we failed.\n\n\ Maybe you ran out of disk space?\n\n\ - Maybe you need to change the port range\n\n" + Maybe you need to change the port range?\n\n" .to_string(), }, )) { @@ -425,12 +425,13 @@ async fn add_nodes( } } if retry_count >= NODE_ADD_MAX_RETRIES { - if let Err(err) = action_sender.send(Action::StatusActions(StatusActions::ErrorScalingUpNodes { - raw_error: format!( - "When trying to assign an available port to run a node, we reached the maximum amount of retries ({}).", - NODE_ADD_MAX_RETRIES - ), - })) + if let Err(err) = + action_sender.send(Action::StatusActions(StatusActions::ErrorScalingUpNodes { + raw_error: format!( + "When trying run a node, we reached the maximum amount of retries ({}).", + NODE_ADD_MAX_RETRIES + ), + })) { error!("Error while sending action: {err:?}"); } From fa5e8b24d1a2670295cbc1a92955b6343a705203 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 17 Sep 2024 16:29:07 +0200 Subject: [PATCH 18/35] fix(launchpad): spacing on error titles --- node-launchpad/src/components/status.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 35c2e6c85c..6f017c415b 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -368,7 +368,7 @@ impl Component for Status { StatusActions::ErrorLoadingNodeRegistry { raw_error } | StatusActions::ErrorGettingNodeRegistryPath { raw_error } => { self.error_popup = Some(ErrorPopup::new( - " Error ".to_string(), + "Error".to_string(), "Error getting node registry path".to_string(), raw_error, )); @@ -380,7 +380,7 @@ impl Component for Status { } StatusActions::ErrorScalingUpNodes { raw_error } => { self.error_popup = Some(ErrorPopup::new( - " Error ".to_string(), + "Erro ".to_string(), "Error adding new nodes".to_string(), raw_error, )); @@ -392,7 +392,7 @@ impl Component for Status { } StatusActions::ErrorStoppingNodes { raw_error } => { self.error_popup = Some(ErrorPopup::new( - " Error ".to_string(), + "Error".to_string(), "Error stopping nodes".to_string(), raw_error, )); @@ -404,7 +404,7 @@ impl Component for Status { } StatusActions::ErrorResettingNodes { raw_error } => { self.error_popup = Some(ErrorPopup::new( - " Error ".to_string(), + "Error".to_string(), "Error resetting nodes".to_string(), raw_error, )); From a4af25d7e049f8f42631cb707a50858fa7943942 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 12 Sep 2024 16:01:34 +0200 Subject: [PATCH 19/35] fix(launchpad): beta programm can be opened from status --- node-launchpad/.config/config.json5 | 3 +++ node-launchpad/src/action.rs | 1 + node-launchpad/src/components/options.rs | 4 ++-- .../src/components/popup/beta_programme.rs | 15 +++++++++++---- node-launchpad/src/components/status.rs | 5 ++++- node-launchpad/src/mode.rs | 3 ++- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/node-launchpad/.config/config.json5 b/node-launchpad/.config/config.json5 index 58db17d7bb..049f9de82b 100644 --- a/node-launchpad/.config/config.json5 +++ b/node-launchpad/.config/config.json5 @@ -14,6 +14,9 @@ "": {"StatusActions":"StopNodes"}, "": {"StatusActions":"StopNodes"}, "": {"StatusActions":"StopNodes"}, + "": {"StatusActions":"TriggerBetaProgramme"}, + "": {"StatusActions":"TriggerBetaProgramme"}, + "": {"StatusActions":"TriggerBetaProgramme"}, "up" : {"StatusActions":"PreviousTableItem"}, "down": {"StatusActions":"NextTableItem"}, diff --git a/node-launchpad/src/action.rs b/node-launchpad/src/action.rs index 19b8bc7125..37a0226323 100644 --- a/node-launchpad/src/action.rs +++ b/node-launchpad/src/action.rs @@ -58,6 +58,7 @@ pub enum StatusActions { NodesStatsObtained(NodeStats), TriggerManageNodes, + TriggerBetaProgramme, PreviousTableItem, NextTableItem, diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index 36a3f666e9..90a626ec33 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -350,7 +350,7 @@ impl Component for Options { | Scene::ChangeDrivePopUp | Scene::ChangeConnectionModePopUp | Scene::ChangePortsPopUp { .. } - | Scene::BetaProgrammePopUp + | Scene::OptionsBetaProgrammePopUp | Scene::ResetNodesPopUp => { self.active = true; // make sure we're in navigation mode @@ -382,7 +382,7 @@ impl Component for Options { self.port_to = Some(to); } OptionsActions::TriggerBetaProgramme => { - return Ok(Some(Action::SwitchScene(Scene::BetaProgrammePopUp))); + return Ok(Some(Action::SwitchScene(Scene::OptionsBetaProgrammePopUp))); } OptionsActions::UpdateBetaProgrammeUsername(username) => { self.discord_username = username; diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index 8940438eec..de3a4b3dcb 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -29,6 +29,7 @@ pub struct BetaProgramme { discord_input_filed: Input, // cache the old value incase user presses Esc. old_value: String, + back_to: Scene, } enum BetaProgrammeState { @@ -50,6 +51,7 @@ impl BetaProgramme { state, discord_input_filed: Input::default().with_value(username), old_value: Default::default(), + back_to: Scene::Status, } } @@ -69,7 +71,7 @@ impl BetaProgramme { vec![ Action::StoreDiscordUserName(self.discord_input_filed.value().to_string()), Action::OptionsActions(OptionsActions::UpdateBetaProgrammeUsername(username)), - Action::SwitchScene(Scene::Status), + Action::SwitchScene(self.back_to), ] } KeyCode::Esc => { @@ -82,7 +84,7 @@ impl BetaProgramme { .discord_input_filed .clone() .with_value(self.old_value.clone()); - vec![Action::SwitchScene(Scene::Options)] + vec![Action::SwitchScene(self.back_to)] } KeyCode::Char(' ') => vec![], KeyCode::Backspace => { @@ -135,7 +137,7 @@ impl Component for BetaProgramme { debug!("RejectTCs msg closed. Switching to Status scene."); self.state = BetaProgrammeState::ShowTCs; } - vec![Action::SwitchScene(Scene::Status)] + vec![Action::SwitchScene(self.back_to)] } BetaProgrammeState::AcceptTCsAndEnterDiscordId => self.capture_inputs(key), }; @@ -145,9 +147,14 @@ impl Component for BetaProgramme { fn update(&mut self, action: Action) -> Result> { let send_back = match action { Action::SwitchScene(scene) => match scene { - Scene::BetaProgrammePopUp => { + Scene::StatusBetaProgrammePopUp | Scene::OptionsBetaProgrammePopUp => { self.active = true; self.old_value = self.discord_input_filed.value().to_string(); + if scene == Scene::StatusBetaProgrammePopUp { + self.back_to = Scene::Status; + } else if scene == Scene::OptionsBetaProgrammePopUp { + self.back_to = Scene::Options; + } // Set to InputMode::Entry as we want to handle everything within our handle_key_events // so by default if this scene is active, we capture inputs. Some(Action::SwitchInputMode(InputMode::Entry)) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 6f017c415b..54b9268ab1 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -275,7 +275,7 @@ impl Component for Status { self.try_update_node_stats(false)?; } Action::SwitchScene(scene) => match scene { - Scene::Status => { + Scene::Status | Scene::StatusBetaProgrammePopUp => { self.active = true; // make sure we're in navigation mode return Ok(Some(Action::SwitchInputMode(InputMode::Navigation))); @@ -479,6 +479,9 @@ impl Component for Status { stop_nodes(running_nodes, action_sender); } + StatusActions::TriggerBetaProgramme => { + return Ok(Some(Action::SwitchScene(Scene::StatusBetaProgrammePopUp))); + } }, Action::OptionsActions(OptionsActions::ResetNodes) => { debug!("Got action to reset nodes"); diff --git a/node-launchpad/src/mode.rs b/node-launchpad/src/mode.rs index 2f0d356599..3f4871302e 100644 --- a/node-launchpad/src/mode.rs +++ b/node-launchpad/src/mode.rs @@ -21,7 +21,8 @@ pub enum Scene { ChangePortsPopUp { connection_mode_old_value: Option, }, - BetaProgrammePopUp, + StatusBetaProgrammePopUp, + OptionsBetaProgrammePopUp, ManageNodesPopUp, ResetNodesPopUp, } From 3ceb003bc8e256e978f4191b175cadb6f013e35d Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 12 Sep 2024 16:03:14 +0200 Subject: [PATCH 20/35] fix(launchpad): after setting up the name forward to status --- node-launchpad/src/components/popup/beta_programme.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index de3a4b3dcb..ab1e9ee739 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -71,7 +71,7 @@ impl BetaProgramme { vec![ Action::StoreDiscordUserName(self.discord_input_filed.value().to_string()), Action::OptionsActions(OptionsActions::UpdateBetaProgrammeUsername(username)), - Action::SwitchScene(self.back_to), + Action::SwitchScene(Scene::Status), ] } KeyCode::Esc => { From 7c2c3905eca02b87f94a5bb8073065baaeea1936 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 16 Sep 2024 09:25:31 +0200 Subject: [PATCH 21/35] fix(launchpad): remove underline color to avoid flickering --- node-launchpad/src/components/popup/beta_programme.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index ab1e9ee739..f6a4f8463f 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -407,11 +407,7 @@ impl Component for BetaProgramme { ); let input = Paragraph::new(Span::styled( format!("{}{} ", spaces, self.discord_input_filed.value()), - Style::default() - .fg(VIVID_SKY_BLUE) - .bg(INDIGO) - .underlined() - .underline_color(VIVID_SKY_BLUE), + Style::default().fg(VIVID_SKY_BLUE).bg(INDIGO).underlined(), )) .alignment(Alignment::Center); f.render_widget(input, layer_two[1]); From 7388cb2948b16c6e295fca228e3c928e1ee62070 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 16 Sep 2024 10:48:51 +0200 Subject: [PATCH 22/35] fix(launchpad): discord username can be empty --- .../src/components/popup/beta_programme.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index f6a4f8463f..8f0a547fe9 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -60,10 +60,6 @@ impl BetaProgramme { KeyCode::Enter => { let username = self.discord_input_filed.value().to_string(); - if username.is_empty() { - debug!("Got Enter, but username is empty, ignoring."); - return vec![]; - } debug!( "Got Enter, saving the discord username {username:?} and switching to DiscordIdAlreadySet, and Home Scene", ); @@ -268,14 +264,10 @@ impl Component for BetaProgramme { )]); f.render_widget(button_no, buttons_layer[0]); - let button_yes_style = if self.discord_input_filed.value().is_empty() { - Style::default().fg(LIGHT_PERIWINKLE) - } else { - Style::default().fg(EUCALYPTUS) - }; + let button_yes = Line::from(vec![Span::styled( "Save Username [Enter]", - button_yes_style, + Style::default().fg(EUCALYPTUS), )]); f.render_widget(button_yes, buttons_layer[1]); } @@ -443,15 +435,10 @@ impl Component for BetaProgramme { " No, Cancel [Esc]", Style::default().fg(LIGHT_PERIWINKLE), )]); - let button_yes_style = if self.discord_input_filed.value().is_empty() { - Style::default().fg(LIGHT_PERIWINKLE) - } else { - Style::default().fg(EUCALYPTUS) - }; f.render_widget(button_no, buttons_layer[0]); let button_yes = Line::from(vec![Span::styled( "Submit Username [Enter]", - button_yes_style, + Style::default().fg(EUCALYPTUS), )]); f.render_widget(button_yes, buttons_layer[1]); } From daeb836f6deb4cfe8382347934d90107b6a64b4d Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Fri, 20 Sep 2024 17:26:01 +0200 Subject: [PATCH 23/35] chore(launchpad): ratataui 0.28.1 --- Cargo.lock | 156 ++++++++++++++------ node-launchpad/Cargo.toml | 2 +- node-launchpad/src/app.rs | 10 +- node-launchpad/src/components/help.rs | 185 ++++++++++++++---------- node-launchpad/src/widgets/hyperlink.rs | 19 +-- 5 files changed, 224 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0db3eced7..adf3205abd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,7 +156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3bf628a79452df9614d933012dc500f8cb6ddad8c897ff8122ea1c0b187ff7" dependencies = [ "nom", - "ratatui", + "ratatui 0.26.3", "simdutf8", "smallvec", "thiserror", @@ -213,9 +213,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "arc-swap" @@ -225,9 +225,9 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "arrayref" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" @@ -888,9 +888,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" dependencies = [ "serde", ] @@ -989,9 +989,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.18" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" dependencies = [ "jobserver", "libc", @@ -1216,6 +1216,20 @@ dependencies = [ "cfg-if", "itoa", "ryu", + "static_assertions", +] + +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", "serde", "static_assertions", ] @@ -1465,6 +1479,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio 1.0.2", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -3398,7 +3428,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.0", "tower-service", - "webpki-roots 0.26.5", + "webpki-roots 0.26.6", ] [[package]] @@ -3435,9 +3465,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3614,6 +3644,16 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn 2.0.77", +] + [[package]] name = "instant" version = "0.1.13" @@ -4367,9 +4407,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] @@ -4456,6 +4496,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -4718,7 +4759,7 @@ dependencies = [ "clap", "color-eyre", "config", - "crossterm", + "crossterm 0.27.0", "derive_deref", "directories", "dirs-next", @@ -4732,7 +4773,7 @@ dependencies = [ "log", "pretty_assertions", "prometheus-parse", - "ratatui", + "ratatui 0.28.1", "reqwest 0.12.7", "serde", "serde_json", @@ -5221,9 +5262,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.12" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c73c26c01b8c87956cea613c907c9d6ecffd8d18a2a5908e5de0adfaa185cea" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" dependencies = [ "memchr", "thiserror", @@ -5232,9 +5273,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.12" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664d22978e2815783adbdd2c588b455b1bd625299ce36b2a99881ac9627e6d8d" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" dependencies = [ "pest", "pest_generator", @@ -5242,9 +5283,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.12" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d5487022d5d33f4c30d91c22afa240ce2a644e87fe08caad974d4eab6badbe" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" dependencies = [ "pest", "pest_meta", @@ -5255,9 +5296,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.12" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0091754bbd0ea592c4deb3a122ce8ecbb0753b738aa82bc055fcc2eccc8d8174" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" dependencies = [ "once_cell", "pest", @@ -5501,9 +5542,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", @@ -6029,12 +6070,10 @@ checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ "bitflags 2.6.0", "cassowary", - "compact_str", - "crossterm", + "compact_str 0.7.1", "itertools 0.12.1", "lru", "paste", - "serde", "stability", "strum", "unicode-segmentation", @@ -6042,6 +6081,28 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "ratatui" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str 0.8.0", + "crossterm 0.28.1", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "serde", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + [[package]] name = "rayon" version = "1.10.0" @@ -6226,7 +6287,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.26.5", + "webpki-roots 0.26.6", "windows-registry", ] @@ -6905,6 +6966,7 @@ checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio 0.8.11", + "mio 1.0.2", "signal-hook", ] @@ -7648,9 +7710,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" -version = "12.11.0" +version = "12.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1db5ac243c7d7f8439eb3b8f0357888b37cf3732957e91383b0ad61756374e" +checksum = "9fdf97c441f18a4f92425b896a4ec7a27e03631a0b1047ec4e34e9916a9a167e" dependencies = [ "debugid", "memmap2", @@ -7660,9 +7722,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.11.0" +version = "12.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26e430c27d4a8a5dea4c4b81440606c7c1a415bd611451ef6af8c81416afc3" +checksum = "bc8ece6b129e97e53d1fbb3f61d33a6a9e5369b11d01228c068094d6d134eaea" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -8099,9 +8161,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ "indexmap 2.5.0", "serde", @@ -8392,7 +8454,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3e785f863a3af4c800a2a669d0b64c879b538738e352607e2624d03f868dc01" dependencies = [ - "crossterm", + "crossterm 0.27.0", "unicode-width", ] @@ -8483,9 +8545,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" @@ -8500,15 +8562,15 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" @@ -8853,9 +8915,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] @@ -9282,9 +9344,9 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yasna" diff --git a/node-launchpad/Cargo.toml b/node-launchpad/Cargo.toml index 19511b1c9e..d4fa995e98 100644 --- a/node-launchpad/Cargo.toml +++ b/node-launchpad/Cargo.toml @@ -41,7 +41,7 @@ libc = "0.2.148" log = "0.4.20" pretty_assertions = "1.4.0" prometheus-parse = "0.2.5" -ratatui = { version = "0.26.0", features = ["serde", "macros", "unstable-widget-ref"] } +ratatui = { version = "0.28.1", features = ["serde", "macros", "unstable-widget-ref"] } reqwest = { version = "0.12.2", default-features = false, features = [ "rustls-tls-manual-roots", ] } diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index f456fdb6b9..e8531ab825 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -167,7 +167,9 @@ impl App { for component in self.components.iter_mut() { component.register_action_handler(action_tx.clone())?; component.register_config_handler(self.config.clone())?; - component.init(tui.size()?)?; + let size = tui.size()?; + let rect = Rect::new(0, 0, size.width, size.height); + component.init(rect)?; } loop { @@ -223,7 +225,7 @@ impl App { tui.resize(Rect::new(0, 0, w, h))?; tui.draw(|f| { for component in self.components.iter_mut() { - let r = component.draw(f, f.size()); + let r = component.draw(f, f.area()); if let Err(e) = r { action_tx .send(Action::Error(format!("Failed to draw: {:?}", e))) @@ -236,10 +238,10 @@ impl App { tui.draw(|f| { f.render_widget( Block::new().style(Style::new().bg(SPACE_CADET)), - f.size(), + f.area(), ); for component in self.components.iter_mut() { - let r = component.draw(f, f.size()); + let r = component.draw(f, f.area()); if let Err(e) = r { action_tx .send(Action::Error(format!("Failed to draw: {:?}", e))) diff --git a/node-launchpad/src/components/help.rs b/node-launchpad/src/components/help.rs index 7bb94db50d..9270616d27 100644 --- a/node-launchpad/src/components/help.rs +++ b/node-launchpad/src/components/help.rs @@ -3,8 +3,8 @@ use color_eyre::eyre::Result; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Style, Stylize}, - text::{Line, Span}, - widgets::{Block, Borders, Cell, Padding, Row, Table}, + text::Span, + widgets::{Block, Borders, Padding}, Frame, }; use tokio::sync::mpsc::UnboundedSender; @@ -14,10 +14,9 @@ use crate::{ action::Action, components::header::Header, mode::{InputMode, Scene}, - style::{COOL_GREY, GHOST_WHITE}, + style::{COOL_GREY, GHOST_WHITE, VIVID_SKY_BLUE}, widgets::hyperlink::Hyperlink, }; -use ansi_to_tui::IntoText; #[derive(Clone)] pub struct Help { @@ -56,86 +55,114 @@ impl Component for Help { // ---- Get Help & Support ---- // Links + // Create a new layout as a table, so we can render hyperlinks + let columns_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layout[1]); + + let padded_area_left = Rect { + x: columns_layout[0].x + 2, + y: columns_layout[0].y + 2, + width: columns_layout[0].width - 2, + height: columns_layout[0].height - 2, + }; + + let left_column = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Max(1), + Constraint::Max(2), + Constraint::Max(1), + Constraint::Max(2), + ]) + .split(padded_area_left); + + let padded_area_right = Rect { + x: columns_layout[1].x + 2, + y: columns_layout[1].y + 2, + width: columns_layout[1].width - 2, + height: columns_layout[1].height - 2, + }; + let right_column = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Max(1), + Constraint::Max(2), + Constraint::Max(1), + Constraint::Max(2), + ]) + .split(padded_area_right); + let quickstart_guide_link = Hyperlink::new( - "docs.autonomi.com/getstarted", + Span::styled( + "docs.autonomi.com/getstarted", + Style::default().fg(VIVID_SKY_BLUE).underlined(), + ), "https://docs.autonomi.com/getstarted", ); - let beta_rewards_link = Hyperlink::new("autonomi.com/beta", "https://autonomi.com/beta"); - let get_direct_support_link = - Hyperlink::new("autonomi.com/support", "https://autonomi.com/support"); - let download_latest_link = - Hyperlink::new("autonomi.com/downloads", "https://autonomi.com/downloads"); - - // Content - let rows_help_and_support = vec![ - Row::new(vec![ - Cell::from(Line::from(vec![Span::styled( - "See the quick start guides:", - Style::default().fg(GHOST_WHITE), - )])), - Cell::from(Line::from(vec![Span::styled( - "To join the Beta Rewards Program:", - Style::default().fg(GHOST_WHITE), - )])), - ]), - Row::new(vec![ - Cell::from( - quickstart_guide_link - .to_string() - .into_text() - .unwrap() - .clone(), - ), - Cell::from(beta_rewards_link.to_string().into_text().unwrap().clone()), - ]), - Row::new(vec![ - // Empty row for padding - Cell::from(Span::raw(" ")), - Cell::from(Span::raw(" ")), - ]), - Row::new(vec![ - Cell::from(Line::from(vec![Span::styled( - "Get Direct Support:", - Style::default().fg(GHOST_WHITE), - )])), - Cell::from(Line::from(vec![Span::styled( - "Download the latest launchpad:", - Style::default().fg(GHOST_WHITE), - )])), - ]), - Row::new(vec![ - Cell::from( - get_direct_support_link - .to_string() - .into_text() - .unwrap() - .clone(), - ), - Cell::from( - download_latest_link - .to_string() - .into_text() - .unwrap() - .clone(), - ), - ]), - ]; - - let table_help_and_support = Table::new( - rows_help_and_support, - vec![Constraint::Percentage(50), Constraint::Percentage(50)], - ) - .block( - Block::new() - .borders(Borders::ALL) - .border_style(Style::default().fg(COOL_GREY)) - .padding(Padding::uniform(1)) - .title(" Get Help & Support ") - .bold() - .title_style(Style::default().bold().fg(GHOST_WHITE)), + let beta_rewards_link = Hyperlink::new( + Span::styled( + "autonomi.com/beta", + Style::default().fg(VIVID_SKY_BLUE).underlined(), + ), + "https://autonomi.com/beta", + ); + let get_direct_support_link = Hyperlink::new( + Span::styled( + "autonomi.com/support", + Style::default().fg(VIVID_SKY_BLUE).underlined(), + ), + "https://autonomi.com/support", + ); + let download_latest_link = Hyperlink::new( + Span::styled( + "autonomi.com/downloads", + Style::default().fg(VIVID_SKY_BLUE).underlined(), + ), + "https://autonomi.com/downloads", + ); + + let block = Block::new() + .borders(Borders::ALL) + .border_style(Style::default().fg(COOL_GREY)) + .padding(Padding::uniform(1)) + .title(" Get Help & Support ") + .bold() + .title_style(Style::default().bold().fg(GHOST_WHITE)); + + // Render hyperlinks in the new area + f.render_widget( + Span::styled( + "See the quick start guides:", + Style::default().fg(GHOST_WHITE), + ), + left_column[0], + ); + f.render_widget_ref(quickstart_guide_link, left_column[1]); + f.render_widget( + Span::styled("Get Direct Support:", Style::default().fg(GHOST_WHITE)), + left_column[2], + ); + f.render_widget_ref(get_direct_support_link, left_column[3]); + f.render_widget( + Span::styled( + "To join the Beta Rewards Program:", + Style::default().fg(GHOST_WHITE), + ), + right_column[0], + ); + f.render_widget_ref(beta_rewards_link, right_column[1]); + f.render_widget( + Span::styled( + "Download the latest launchpad:", + Style::default().fg(GHOST_WHITE), + ), + right_column[2], ); + f.render_widget_ref(download_latest_link, right_column[3]); - f.render_widget(table_help_and_support, layout[1]); + f.render_widget(block, layout[1]); Ok(()) } diff --git a/node-launchpad/src/widgets/hyperlink.rs b/node-launchpad/src/widgets/hyperlink.rs index 0798811ae0..2d78ed312e 100644 --- a/node-launchpad/src/widgets/hyperlink.rs +++ b/node-launchpad/src/widgets/hyperlink.rs @@ -8,7 +8,6 @@ use itertools::Itertools; use ratatui::{prelude::*, widgets::WidgetRef}; -use std::fmt; /// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8]. /// @@ -27,20 +26,6 @@ impl<'content> Hyperlink<'content> { } } -// Displays the hyperlink in the terminal using OSC 8. -// Underline solid \x1b[4m -// Foreground color 45 \x1b[38;5;45m -impl fmt::Display for Hyperlink<'_> { - //TODO: Parameterize the color, underline, bold, etc. Use ratatui::Style. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "\x1b[4m\x1b[38;5;45m\x1B]8;;{}\x07{}\x1B]8;;\x07\x1b[0m", - self.url, self.text - ) - } -} - impl WidgetRef for Hyperlink<'_> { fn render_ref(&self, area: Rect, buffer: &mut Buffer) { self.text.render_ref(area, buffer); @@ -60,8 +45,8 @@ impl WidgetRef for Hyperlink<'_> { let text = two_chars.collect::(); let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text); buffer - .get_mut(area.x + i as u16 * 2, area.y) - .set_symbol(hyperlink.as_str()); + .cell_mut(Position::new(area.x + i as u16 * 2, area.y)) + .map(|cell| cell.set_symbol(hyperlink.as_str())); } } } From eb3055c6e318696aead26169b4b3832979a52ff7 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Fri, 20 Sep 2024 17:33:11 +0200 Subject: [PATCH 24/35] chore(launchpad): ansi-to-tui removed --- Cargo.lock | 66 ++------------------------------------- node-launchpad/Cargo.toml | 1 - 2 files changed, 2 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index adf3205abd..1015a95c8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,19 +149,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "ansi-to-tui" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3bf628a79452df9614d933012dc500f8cb6ddad8c897ff8122ea1c0b187ff7" -dependencies = [ - "nom", - "ratatui 0.26.3", - "simdutf8", - "smallvec", - "thiserror", -] - [[package]] name = "anstream" version = "0.6.15" @@ -1206,19 +1193,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] - [[package]] name = "compact_str" version = "0.8.0" @@ -4752,7 +4726,6 @@ dependencies = [ name = "node-launchpad" version = "0.3.17" dependencies = [ - "ansi-to-tui", "atty", "better-panic", "chrono", @@ -4773,7 +4746,7 @@ dependencies = [ "log", "pretty_assertions", "prometheus-parse", - "ratatui 0.28.1", + "ratatui", "reqwest 0.12.7", "serde", "serde_json", @@ -6062,25 +6035,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "ratatui" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" -dependencies = [ - "bitflags 2.6.0", - "cassowary", - "compact_str 0.7.1", - "itertools 0.12.1", - "lru", - "paste", - "stability", - "strum", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - [[package]] name = "ratatui" version = "0.28.1" @@ -6089,7 +6043,7 @@ checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" dependencies = [ "bitflags 2.6.0", "cassowary", - "compact_str 0.8.0", + "compact_str", "crossterm 0.28.1", "instability", "itertools 0.13.0", @@ -6998,12 +6952,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simdutf8" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" - [[package]] name = "slab" version = "0.4.9" @@ -7637,16 +7585,6 @@ dependencies = [ "der 0.7.9", ] -[[package]] -name = "stability" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" -dependencies = [ - "quote", - "syn 2.0.77", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/node-launchpad/Cargo.toml b/node-launchpad/Cargo.toml index d4fa995e98..cbd061cfdc 100644 --- a/node-launchpad/Cargo.toml +++ b/node-launchpad/Cargo.toml @@ -62,7 +62,6 @@ tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] } tui-input = "0.8.0" which = "6.0.1" -ansi-to-tui = "4.1.0" faccess = "0.2.4" [build-dependencies] From 5979c715d95ce1ec8e529a7675705d6987f42b29 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 24 Sep 2024 14:42:32 +0200 Subject: [PATCH 25/35] feat(launchpad): new stats with layout and interaction --- Cargo.lock | 11 + node-launchpad/Cargo.toml | 1 + node-launchpad/src/components/status.rs | 302 ++++++++++++++++++------ node-launchpad/src/node_stats.rs | 136 ++++++++++- 4 files changed, 367 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1015a95c8d..f74b921cab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4759,6 +4759,7 @@ dependencies = [ "strum", "sysinfo", "tempfile", + "throbber-widgets-tui", "tokio", "tokio-util 0.7.12", "tracing", @@ -7861,6 +7862,16 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "throbber-widgets-tui" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad9e055cadd9da8b4a67662b962e3e67e96af491ae9cec7e88aaff92e7c3666" +dependencies = [ + "rand 0.8.5", + "ratatui", +] + [[package]] name = "time" version = "0.3.36" diff --git a/node-launchpad/Cargo.toml b/node-launchpad/Cargo.toml index cbd061cfdc..c25a818373 100644 --- a/node-launchpad/Cargo.toml +++ b/node-launchpad/Cargo.toml @@ -63,6 +63,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] } tui-input = "0.8.0" which = "6.0.1" faccess = "0.2.4" +throbber-widgets-tui = "0.7.0" [build-dependencies] vergen = { version = "8.2.6", features = ["build", "git", "gitoxide", "cargo"] } diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 54b9268ab1..b55831acca 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -19,6 +19,7 @@ use crate::connection_mode::ConnectionMode; use crate::error::ErrorPopup; use crate::node_mgmt::MaintainNodesArgs; use crate::node_mgmt::{PORT_MAX, PORT_MIN}; +use crate::style::{COOL_GREY, INDIGO}; use crate::tui::Event; use crate::{ action::{Action, StatusActions}, @@ -39,6 +40,7 @@ use sn_peers_acquisition::PeersArgs; use sn_service_management::{ control::ServiceController, NodeRegistry, NodeServiceData, ServiceStatus, }; +use std::collections::HashMap; use std::{ path::PathBuf, time::{Duration, Instant}, @@ -48,6 +50,8 @@ use tokio::sync::mpsc::UnboundedSender; use super::super::node_mgmt::{maintain_n_running_nodes, reset_nodes, stop_nodes}; +use throbber_widgets_tui::{self, ThrobberState}; + const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5); /// If nat detection fails for more than 3 times, we don't want to waste time running during every node start. const MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION: usize = 3; @@ -60,6 +64,7 @@ pub struct Status { config: Config, // state node_services: Vec, + node_services_throttle_state: HashMap, is_nat_status_determined: bool, error_while_running_nat_detection: usize, node_stats: NodeStats, @@ -111,6 +116,7 @@ impl Status { config: Default::default(), active: true, node_services: Default::default(), + node_services_throttle_state: HashMap::new(), is_nat_status_determined: false, error_while_running_nat_detection: 0, node_stats: NodeStats::default(), @@ -273,6 +279,9 @@ impl Component for Status { match action { Action::Tick => { self.try_update_node_stats(false)?; + for (_spinner_key, spinner_state) in self.node_services_throttle_state.iter_mut() { + spinner_state.calc_next(); // Assuming calc_next() is a method of ThrobberState + } } Action::SwitchScene(scene) => match scene { Scene::Status | Scene::StatusBetaProgrammePopUp => { @@ -545,13 +554,13 @@ impl Component for Status { Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE), Cell::new(format!("{} GB", self.nodes_to_start * GB_PER_NODE)).fg(GHOST_WHITE), ]); - let memory_use_val = if self.node_stats.memory_usage_mb as f64 / 1024_f64 > 1.0 { + let memory_use_val = if self.node_stats.total_memory_usage_mb as f64 / 1024_f64 > 1.0 { format!( "{:.2} GB", - self.node_stats.memory_usage_mb as f64 / 1024_f64 + self.node_stats.total_memory_usage_mb as f64 / 1024_f64 ) } else { - format!("{} MB", self.node_stats.memory_usage_mb) + format!("{} MB", self.node_stats.total_memory_usage_mb) }; let memory_use_row = Row::new(vec![ @@ -603,7 +612,7 @@ impl Component for Status { let total_nanos_earned_and_discord_row = Row::new(vec![ Cell::new("Nanos Earned".to_string()).fg(VIVID_SKY_BLUE), - Cell::new(self.node_stats.forwarded_rewards.to_string()) + Cell::new(self.node_stats.total_forwarded_rewards.to_string()) .fg(VIVID_SKY_BLUE) .bold(), Cell::new( @@ -642,24 +651,98 @@ impl Component for Status { // ==== Node Status ===== + // Widths + const NODE_WIDTH: usize = 10; + const VERSION_WIDTH: usize = 7; + const NANOS_WIDTH: usize = 5; + const MEMORY_WIDTH: usize = 7; + const MBPS_WIDTH: usize = 15; + const RECORDS_WIDTH: usize = 4; + const PEERS_WIDTH: usize = 5; + const CONNS_WIDTH: usize = 5; + const STATUS_WIDTH: usize = 8; + const SPINNER_WIDTH: usize = 1; + let node_rows: Vec<_> = self .node_services .iter() .filter_map(|n| { - let peer_id = n.peer_id; if n.status == ServiceStatus::Removed { return None; } - let peer_id = peer_id.map(|p| p.to_string()).unwrap_or("-".to_string()); - let status = format!("{:?}", n.status); - let version = format!("v{}", n.version); - let row = vec![n.service_name.clone(), peer_id, version, status]; + let mut status = format!("{:?}", n.status); + if let Some(LockRegistryState::StartingNodes) = self.lock_registry { + status = "Starting".to_string(); + } + let connected_peers = match n.connected_peers { + Some(ref peers) => format!("{:?}", peers.len()), + None => "0".to_string(), + }; + + let mut nanos = "-".to_string(); + let mut memory = "-".to_string(); + let mut mbps = " -".to_string(); + let mut records = "-".to_string(); + let mut connections = "-".to_string(); + + let individual_stats = self + .node_stats + .individual_stats + .iter() + .find(|s| s.service_name == n.service_name); + if let Some(stats) = individual_stats { + nanos = stats.forwarded_rewards.to_string(); + memory = stats.memory_usage_mb.to_string(); + mbps = format!( + "↓{:06.2} ↑{:06.2}", + stats.bandwidth_inbound as f64 / (1024_f64 * 1024_f64), + stats.bandwidth_outbound as f64 / (1024_f64 * 1024_f64) + ); + records = stats.max_records.to_string(); + connections = stats.connections.to_string(); + } + + // Create a row vector + let row = vec![ + n.service_name.clone().to_string(), + n.version.to_string(), + format!( + "{}{}", + " ".repeat(NANOS_WIDTH.saturating_sub(nanos.len())), + nanos.to_string() + ), + format!( + "{}{} MB", + " ".repeat(MEMORY_WIDTH.saturating_sub(memory.len() + 4)), + memory.to_string() + ), + mbps.to_string(), + format!( + "{}{}", + " ".repeat(RECORDS_WIDTH.saturating_sub(records.len())), + records.to_string() + ), + format!( + "{}{}", + " ".repeat(PEERS_WIDTH.saturating_sub(connected_peers.len())), + connected_peers.to_string() + ), + format!( + "{}{}", + " ".repeat(CONNS_WIDTH.saturating_sub(connections.len())), + connections.to_string() + ), + status.to_string(), + ]; + + // Create a styled row let row_style = if n.status == ServiceStatus::Running { Style::default().fg(EUCALYPTUS) } else { Style::default().fg(GHOST_WHITE) }; + Some(Row::new(row).style(row_style)) }) .collect(); @@ -701,31 +784,112 @@ impl Component for Status { layout[2], ); } else { + // Node/s block + let block_nodes = Block::default() + .title(Line::from(vec![ + Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()), + Span::styled( + format!(" ({}) ", self.nodes_to_start), + Style::default().fg(LIGHT_PERIWINKLE), + ), + ])) + .padding(Padding::new(1, 1, 0, 0)) + .title_style(Style::default().fg(GHOST_WHITE)) + .borders(Borders::ALL) + .border_style(Style::default().fg(EUCALYPTUS)); + + // Create a layout to arrange the header and table vertically + let inner_layout = Layout::new( + Direction::Vertical, + vec![Constraint::Length(1), Constraint::Min(0)], + ); + + // Split the inner area of the combined block + let inner_area = block_nodes.inner(layout[2]); + let inner_chunks = inner_layout.split(inner_area); + + // Column Widths let node_widths = [ - Constraint::Length(11), - Constraint::Fill(1), - Constraint::Length(9), - Constraint::Length(8), + Constraint::Min(NODE_WIDTH as u16), + Constraint::Min(VERSION_WIDTH as u16), + Constraint::Min(NANOS_WIDTH as u16), + Constraint::Min(MEMORY_WIDTH as u16), + Constraint::Min(MBPS_WIDTH as u16), + Constraint::Min(RECORDS_WIDTH as u16), + Constraint::Min(PEERS_WIDTH as u16), + Constraint::Min(CONNS_WIDTH as u16), + Constraint::Min(STATUS_WIDTH as u16), + Constraint::Max(SPINNER_WIDTH as u16), ]; + + // Header + let header_row = Row::new(vec![ + Cell::new("Node").fg(COOL_GREY), + Cell::new("Version").fg(COOL_GREY), + Cell::new("Nanos").fg(COOL_GREY), + Cell::new("Memory").fg(COOL_GREY), + Cell::new( + format!("{}{}", " ".repeat(MBPS_WIDTH - "Mbps".len()), "Mbps").fg(COOL_GREY), + ), + Cell::new("Recs").fg(COOL_GREY), + Cell::new("Peers").fg(COOL_GREY), + Cell::new("Conns").fg(COOL_GREY), + Cell::new("Status").fg(COOL_GREY), + Cell::new(" ").fg(COOL_GREY), // Spinner + ]); + + let header = Table::new(vec![header_row.clone()], node_widths) + .style(Style::default().add_modifier(Modifier::BOLD)); + + // Table items let table = Table::new(node_rows.clone(), node_widths) - .column_spacing(2) - .highlight_style(Style::new().reversed()) - .block( - Block::default() - .title(Line::from(vec![ - Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()), - Span::styled( - format!(" ({}) ", self.nodes_to_start), - Style::default().fg(LIGHT_PERIWINKLE), - ), - ])) - .padding(Padding::new(2, 2, 1, 1)) - .title_style(Style::default().fg(GHOST_WHITE)) - .borders(Borders::ALL) - .border_style(Style::default().fg(EUCALYPTUS)), - ) - .highlight_symbol("*"); - f.render_stateful_widget(table, layout[2], &mut self.node_table_state); + .column_spacing(1) + .highlight_style(Style::default().bg(INDIGO)) + .highlight_spacing(HighlightSpacing::Always); + + f.render_stateful_widget(header, inner_chunks[0], &mut self.node_table_state); + f.render_stateful_widget(table, inner_chunks[1], &mut self.node_table_state); + + // Render the throbber in the last column for running nodes + for (i, node) in self.node_services.iter().enumerate() { + let mut throbber = throbber_widgets_tui::Throbber::default() + .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE); + match node.status { + ServiceStatus::Running => { + throbber = throbber + .throbber_style( + Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD), + ) + .use_type(throbber_widgets_tui::WhichUse::Spin); + } + ServiceStatus::Stopped => { + throbber = throbber + .throbber_style( + Style::default() + .fg(GHOST_WHITE) + .add_modifier(Modifier::BOLD), + ) + .use_type(throbber_widgets_tui::WhichUse::Full); + } + _ => {} + } + if let Some(LockRegistryState::StartingNodes) = self.lock_registry { + throbber = throbber + .throbber_style( + Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD), + ) + .throbber_set(throbber_widgets_tui::BOX_DRAWING) + .use_type(throbber_widgets_tui::WhichUse::Spin); + } + let throbber_area = + Rect::new(inner_chunks[1].width, inner_chunks[1].y + i as u16, 1, 1); + let throttle_state = self + .node_services_throttle_state + .entry(node.service_name.clone()) + .or_default(); + f.render_stateful_widget(throbber, throbber_area, throttle_state); + } + f.render_widget(block_nodes, layout[2]); } // ==== Footer ===== @@ -755,19 +919,6 @@ impl Component for Status { // Status Popup if let Some(registry_state) = &self.lock_registry { - let popup_area = centered_rect_fixed(50, 12, area); - clear_area(f, popup_area); - - let popup_border = Paragraph::new("").block( - Block::default() - .borders(Borders::ALL) - .title(" Manage Nodes ") - .bold() - .title_style(Style::new().fg(VIVID_SKY_BLUE)) - .padding(Padding::uniform(2)) - .border_style(Style::new().fg(GHOST_WHITE)), - ); - let popup_text = match registry_state { LockRegistryState::StartingNodes => { if self.should_we_run_nat_detection() { @@ -779,12 +930,8 @@ impl Component for Status { Line::raw("This may take a couple minutes."), ] } else { - vec![ - Line::raw(""), - Line::raw(""), - Line::raw(""), - Line::raw("Starting nodes..."), - ] + // We avoid rendering the popup as we have status lines now + return Ok(()); } } LockRegistryState::StoppingNodes => { @@ -804,26 +951,41 @@ impl Component for Status { ] } }; - let centred_area = Layout::new( - Direction::Vertical, - vec![ - // border - Constraint::Length(2), - // our text goes here - Constraint::Min(5), - // border - Constraint::Length(1), - ], - ) - .split(popup_area); - let text = Paragraph::new(popup_text) - .block(Block::default().padding(Padding::horizontal(2))) - .wrap(Wrap { trim: false }) - .alignment(Alignment::Center) - .fg(EUCALYPTUS); - f.render_widget(text, centred_area[1]); - - f.render_widget(popup_border, popup_area); + if !popup_text.is_empty() { + let popup_area = centered_rect_fixed(50, 12, area); + clear_area(f, popup_area); + + let popup_border = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title(" Manage Nodes ") + .bold() + .title_style(Style::new().fg(VIVID_SKY_BLUE)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(GHOST_WHITE)), + ); + + let centred_area = Layout::new( + Direction::Vertical, + vec![ + // border + Constraint::Length(2), + // our text goes here + Constraint::Min(5), + // border + Constraint::Length(1), + ], + ) + .split(popup_area); + let text = Paragraph::new(popup_text) + .block(Block::default().padding(Padding::horizontal(2))) + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center) + .fg(EUCALYPTUS); + f.render_widget(text, centred_area[1]); + + f.render_widget(popup_border, popup_area); + } } Ok(()) diff --git a/node-launchpad/src/node_stats.rs b/node-launchpad/src/node_stats.rs index c43d868eef..0cbebcc815 100644 --- a/node-launchpad/src/node_stats.rs +++ b/node-launchpad/src/node_stats.rs @@ -16,17 +16,49 @@ use tokio::sync::mpsc::UnboundedSender; use crate::action::{Action, StatusActions}; #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct NodeStats { +pub struct IndividualNodeStats { + pub service_name: String, pub forwarded_rewards: u64, pub memory_usage_mb: usize, + pub bandwidth_inbound: usize, + pub bandwidth_outbound: usize, + pub max_records: usize, + pub peers: usize, + pub connections: usize, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NodeStats { + pub total_forwarded_rewards: u64, + pub total_memory_usage_mb: usize, + pub individual_stats: Vec, } impl NodeStats { - fn merge(&mut self, other: &NodeStats) { - self.forwarded_rewards += other.forwarded_rewards; - self.memory_usage_mb += other.memory_usage_mb; + fn merge(&mut self, other: &IndividualNodeStats) { + self.total_forwarded_rewards += other.forwarded_rewards; + self.total_memory_usage_mb += other.memory_usage_mb; + self.individual_stats.push(other.clone()); // Store individual stats } + /// Fetches statistics from all running nodes and sends the aggregated stats via the action sender. + /// + /// This method iterates over the provided list of `NodeServiceData` instances, filters out nodes that are not running, + /// and for each running node, it checks if a metrics port is available. If a metrics port is found, the node's details + /// (service name, metrics port, and data directory path) are collected. If no metrics port is found, a debug message + /// is logged indicating that the node's stats will not be fetched. + /// + /// If there are any nodes with available metrics ports, this method spawns a local task to asynchronously fetch + /// statistics from these nodes using `fetch_all_node_stats_inner`. The aggregated statistics are then sent via the + /// provided `action_sender`. + /// + /// If no running nodes with metrics ports are found, a debug message is logged indicating that there are no running nodes + /// to fetch stats from. + /// + /// # Parameters + /// + /// * `nodes`: A slice of `NodeServiceData` instances representing the nodes to fetch statistics from. + /// * `action_sender`: An unbounded sender of `Action` instances used to send the aggregated node statistics. pub fn fetch_all_node_stats(nodes: &[NodeServiceData], action_sender: UnboundedSender) { let node_details = nodes .iter() @@ -60,6 +92,24 @@ impl NodeStats { } } + /// This method is an inner function used to fetch statistics from all nodes. + /// It takes a vector of node details (service name, metrics port, and data directory path) and an unbounded sender of `Action` instances. + /// The method iterates over the provided list of `NodeServiceData` instances, filters out nodes that are not running, + /// and for each running node, it checks if a metrics port is available. If a metrics port is found, the node's details + /// (service name, metrics port, and data directory path) are collected. If no metrics port is found, a debug message + /// is logged indicating that the node's stats will not be fetched. + /// + /// If there are any nodes with available metrics ports, this method spawns a local task to asynchronously fetch + /// statistics from these nodes using `fetch_all_node_stats_inner`. The aggregated statistics are then sent via the + /// provided `action_sender`. + /// + /// If no running nodes with metrics ports are found, a debug message is logged indicating that there are no running nodes + /// to fetch stats from. + /// + /// # Parameters + /// + /// * `node_details`: A vector of tuples, each containing the service name, metrics port, and data directory path of a node. + /// * `action_sender`: An unbounded sender of `Action` instances used to send the aggregated node statistics. async fn fetch_all_node_stats_inner( node_details: Vec<(String, u16, PathBuf)>, action_sender: UnboundedSender, @@ -78,7 +128,17 @@ impl NodeStats { while let Some((result, service_name)) = stream.next().await { match result { Ok(stats) => { - all_node_stats.merge(&stats); + let individual_stats = IndividualNodeStats { + service_name: service_name.clone(), + forwarded_rewards: stats.forwarded_rewards, + memory_usage_mb: stats.memory_usage_mb, + bandwidth_inbound: stats.bandwidth_inbound, + bandwidth_outbound: stats.bandwidth_outbound, + max_records: stats.max_records, + peers: stats.peers, + connections: stats.connections, + }; + all_node_stats.merge(&individual_stats); } Err(err) => { error!("Error while fetching stats from {service_name:?}: {err:?}"); @@ -93,7 +153,10 @@ impl NodeStats { } } - async fn fetch_stat_per_node(metrics_port: u16, _data_dir: PathBuf) -> Result { + async fn fetch_stat_per_node( + metrics_port: u16, + _data_dir: PathBuf, + ) -> Result { let now = Instant::now(); let body = reqwest::get(&format!("http://localhost:{metrics_port}/metrics")) @@ -103,12 +166,22 @@ impl NodeStats { let lines: Vec<_> = body.lines().map(|s| Ok(s.to_owned())).collect(); let all_metrics = prometheus_parse::Scrape::parse(lines.into_iter())?; - let mut stats = NodeStats { - memory_usage_mb: 0, - forwarded_rewards: 0, - }; + let mut stats = IndividualNodeStats::default(); + for sample in all_metrics.samples.iter() { - if sample.metric == "sn_networking_process_memory_used_mb" { + debug!(sample.metric); + if sample.metric == "sn_node_total_forwarded_rewards" { + // Nanos + match sample.value { + prometheus_parse::Value::Counter(val) + | prometheus_parse::Value::Gauge(val) + | prometheus_parse::Value::Untyped(val) => { + stats.forwarded_rewards = val as u64; + } + _ => {} + } + } else if sample.metric == "sn_networking_process_memory_used_mb" { + // Memory match sample.value { prometheus_parse::Value::Counter(val) | prometheus_parse::Value::Gauge(val) @@ -117,12 +190,49 @@ impl NodeStats { } _ => {} } - } else if sample.metric == "sn_node_total_forwarded_rewards" { + } else if sample.metric == "libp2p_bandwidth_bytes_total" { + // Mbps match sample.value { prometheus_parse::Value::Counter(val) | prometheus_parse::Value::Gauge(val) | prometheus_parse::Value::Untyped(val) => { - stats.forwarded_rewards = val as u64; + if let Some(direction) = sample.labels.get("direction") { + if direction == "Inbound" { + stats.bandwidth_inbound += val as usize; + } else if direction == "Outbound" { + stats.bandwidth_outbound += val as usize; + } + } + } + _ => {} + } + } else if sample.metric == "sn_networking_records_stored" { + // Records + match sample.value { + prometheus_parse::Value::Counter(val) + | prometheus_parse::Value::Gauge(val) + | prometheus_parse::Value::Untyped(val) => { + stats.max_records = val as usize; + } + _ => {} + } + } else if sample.metric == "sn_networking_peers_in_routing_table" { + // Peers + match sample.value { + prometheus_parse::Value::Counter(val) + | prometheus_parse::Value::Gauge(val) + | prometheus_parse::Value::Untyped(val) => { + stats.peers = val as usize; + } + _ => {} + } + } else if sample.metric == "sn_networking_open_connections" { + // Connections + match sample.value { + prometheus_parse::Value::Counter(val) + | prometheus_parse::Value::Gauge(val) + | prometheus_parse::Value::Untyped(val) => { + stats.connections = val as usize; } _ => {} } From 7ff9680b3529a0b3c31f0b0d78aa15379298d60e Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 25 Sep 2024 16:10:07 +0200 Subject: [PATCH 26/35] feat(launchpad): mbps calculation and display --- node-launchpad/src/components/status.rs | 6 +++--- node-launchpad/src/node_stats.rs | 28 ++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index b55831acca..b653946563 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -52,7 +52,7 @@ use super::super::node_mgmt::{maintain_n_running_nodes, reset_nodes, stop_nodes} use throbber_widgets_tui::{self, ThrobberState}; -const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5); +pub const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5); /// If nat detection fails for more than 3 times, we don't want to waste time running during every node start. const MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION: usize = 3; @@ -656,7 +656,7 @@ impl Component for Status { const VERSION_WIDTH: usize = 7; const NANOS_WIDTH: usize = 5; const MEMORY_WIDTH: usize = 7; - const MBPS_WIDTH: usize = 15; + const MBPS_WIDTH: usize = 13; const RECORDS_WIDTH: usize = 4; const PEERS_WIDTH: usize = 5; const CONNS_WIDTH: usize = 5; @@ -695,7 +695,7 @@ impl Component for Status { nanos = stats.forwarded_rewards.to_string(); memory = stats.memory_usage_mb.to_string(); mbps = format!( - "↓{:06.2} ↑{:06.2}", + "↓{:05.2} ↑{:05.2}", stats.bandwidth_inbound as f64 / (1024_f64 * 1024_f64), stats.bandwidth_outbound as f64 / (1024_f64 * 1024_f64) ); diff --git a/node-launchpad/src/node_stats.rs b/node-launchpad/src/node_stats.rs index 0cbebcc815..788ccfb94b 100644 --- a/node-launchpad/src/node_stats.rs +++ b/node-launchpad/src/node_stats.rs @@ -13,6 +13,8 @@ use sn_service_management::{NodeServiceData, ServiceStatus}; use std::{path::PathBuf, time::Instant}; use tokio::sync::mpsc::UnboundedSender; +use super::components::status::NODE_STAT_UPDATE_INTERVAL; + use crate::action::{Action, StatusActions}; #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -22,6 +24,10 @@ pub struct IndividualNodeStats { pub memory_usage_mb: usize, pub bandwidth_inbound: usize, pub bandwidth_outbound: usize, + pub prev_bandwidth_inbound: usize, + pub prev_bandwidth_outbound: usize, + pub bandwidth_inbound_rate: usize, + pub bandwidth_outbound_rate: usize, pub max_records: usize, pub peers: usize, pub connections: usize, @@ -137,6 +143,10 @@ impl NodeStats { max_records: stats.max_records, peers: stats.peers, connections: stats.connections, + prev_bandwidth_inbound: stats.prev_bandwidth_inbound, + prev_bandwidth_outbound: stats.prev_bandwidth_outbound, + bandwidth_inbound_rate: stats.bandwidth_inbound_rate, + bandwidth_outbound_rate: stats.bandwidth_outbound_rate, }; all_node_stats.merge(&individual_stats); } @@ -169,7 +179,6 @@ impl NodeStats { let mut stats = IndividualNodeStats::default(); for sample in all_metrics.samples.iter() { - debug!(sample.metric); if sample.metric == "sn_node_total_forwarded_rewards" { // Nanos match sample.value { @@ -198,9 +207,22 @@ impl NodeStats { | prometheus_parse::Value::Untyped(val) => { if let Some(direction) = sample.labels.get("direction") { if direction == "Inbound" { - stats.bandwidth_inbound += val as usize; + let current_inbound = val as usize; + stats.bandwidth_inbound = current_inbound; + let rate = (current_inbound as f64 + - stats.prev_bandwidth_inbound as f64) + / NODE_STAT_UPDATE_INTERVAL.as_secs_f64(); + stats.bandwidth_inbound_rate = rate as usize; + stats.prev_bandwidth_inbound = current_inbound; } else if direction == "Outbound" { - stats.bandwidth_outbound += val as usize; + let current_outbound = val as usize; + stats.bandwidth_outbound = current_outbound; + let rate = (current_outbound as f64 + - stats.prev_bandwidth_outbound as f64) + / NODE_STAT_UPDATE_INTERVAL.as_secs_f64(); + stats.bandwidth_outbound_rate = rate as usize; + stats.prev_bandwidth_outbound = current_outbound; + // Update previous value } } } From e0aa8e37387f5c92062c1311d028dcdbf2adeb66 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 26 Sep 2024 11:11:54 +0200 Subject: [PATCH 27/35] feat(launchpad): less variables to calculate rate --- node-launchpad/src/node_stats.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/node-launchpad/src/node_stats.rs b/node-launchpad/src/node_stats.rs index 788ccfb94b..a68d0d1404 100644 --- a/node-launchpad/src/node_stats.rs +++ b/node-launchpad/src/node_stats.rs @@ -24,8 +24,6 @@ pub struct IndividualNodeStats { pub memory_usage_mb: usize, pub bandwidth_inbound: usize, pub bandwidth_outbound: usize, - pub prev_bandwidth_inbound: usize, - pub prev_bandwidth_outbound: usize, pub bandwidth_inbound_rate: usize, pub bandwidth_outbound_rate: usize, pub max_records: usize, @@ -143,8 +141,6 @@ impl NodeStats { max_records: stats.max_records, peers: stats.peers, connections: stats.connections, - prev_bandwidth_inbound: stats.prev_bandwidth_inbound, - prev_bandwidth_outbound: stats.prev_bandwidth_outbound, bandwidth_inbound_rate: stats.bandwidth_inbound_rate, bandwidth_outbound_rate: stats.bandwidth_outbound_rate, }; @@ -208,21 +204,18 @@ impl NodeStats { if let Some(direction) = sample.labels.get("direction") { if direction == "Inbound" { let current_inbound = val as usize; - stats.bandwidth_inbound = current_inbound; let rate = (current_inbound as f64 - - stats.prev_bandwidth_inbound as f64) + - stats.bandwidth_inbound as f64) / NODE_STAT_UPDATE_INTERVAL.as_secs_f64(); stats.bandwidth_inbound_rate = rate as usize; - stats.prev_bandwidth_inbound = current_inbound; + stats.bandwidth_inbound = current_inbound; } else if direction == "Outbound" { let current_outbound = val as usize; - stats.bandwidth_outbound = current_outbound; let rate = (current_outbound as f64 - - stats.prev_bandwidth_outbound as f64) + - stats.bandwidth_outbound as f64) / NODE_STAT_UPDATE_INTERVAL.as_secs_f64(); stats.bandwidth_outbound_rate = rate as usize; - stats.prev_bandwidth_outbound = current_outbound; - // Update previous value + stats.bandwidth_outbound = current_outbound; } } } From 03f006d0a1bf77a807b469d121f6036a47519e79 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 26 Sep 2024 10:17:24 +0200 Subject: [PATCH 28/35] fix(launchpad): nat mode only when first time automatic --- node-launchpad/src/components/status.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index b653946563..418f4a5eb7 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -190,7 +190,8 @@ impl Status { /// Only run NAT detection if we haven't determined the status yet and we haven't failed more than 3 times. fn should_we_run_nat_detection(&self) -> bool { - !self.is_nat_status_determined + self.connection_mode == ConnectionMode::Automatic + && !self.is_nat_status_determined && self.error_while_running_nat_detection < MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION } From 6c8fd91372a33da7f5b372ddd4472f5568b0dfa9 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 1 Oct 2024 09:00:50 +0200 Subject: [PATCH 29/35] fix(launchpad): typo on error popup --- node-launchpad/src/components/status.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 418f4a5eb7..86a25b4228 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -390,7 +390,7 @@ impl Component for Status { } StatusActions::ErrorScalingUpNodes { raw_error } => { self.error_popup = Some(ErrorPopup::new( - "Erro ".to_string(), + "Error".to_string(), "Error adding new nodes".to_string(), raw_error, )); From 13396f56606302e8ffd3883207db7026a1bf621d Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 30 Sep 2024 11:28:56 +0200 Subject: [PATCH 30/35] fix(launchapd): cancelling changing to custom port mode restores old values --- .../src/components/popup/port_range.rs | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index b3267969b2..23b79870e5 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -297,31 +297,40 @@ impl Component for PortRangePopUp { } KeyCode::Esc => { debug!("Got Esc, restoring the old values and switching to actual screen"); - // if the old values are 0 means that is the first time the user opens the app, - // so we should set the connection mode to automatic. - if self.port_from_old_value.to_string() == "0" - && self.port_to_old_value.to_string() == "0" - { - self.connection_mode = self - .connection_mode_old_value - .unwrap_or(ConnectionMode::Automatic); - return Ok(vec![ - Action::StoreConnectionMode(self.connection_mode), + if let Some(connection_mode_old_value) = self.connection_mode_old_value { + vec![ Action::OptionsActions(OptionsActions::UpdateConnectionMode( - self.connection_mode, + connection_mode_old_value, )), Action::SwitchScene(Scene::Options), - ]); + ] + } else { + // if the old values are 0 means that is the first time the user opens the app, + // so we should set the connection mode to automatic. + if self.port_from_old_value.to_string() == "0" + && self.port_to_old_value.to_string() == "0" + { + self.connection_mode = self + .connection_mode_old_value + .unwrap_or(ConnectionMode::Automatic); + return Ok(vec![ + Action::StoreConnectionMode(self.connection_mode), + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + self.connection_mode, + )), + Action::SwitchScene(Scene::Options), + ]); + } + self.port_from = self + .port_from + .clone() + .with_value(self.port_from_old_value.to_string()); + self.port_to = self + .port_to + .clone() + .with_value(self.port_to_old_value.to_string()); + vec![Action::SwitchScene(Scene::Options)] } - self.port_from = self - .port_from - .clone() - .with_value(self.port_from_old_value.to_string()); - self.port_to = self - .port_to - .clone() - .with_value(self.port_to_old_value.to_string()); - vec![Action::SwitchScene(Scene::Options)] } KeyCode::Char(c) if !c.is_numeric() => vec![], KeyCode::Up => { From 6e32976d5450c731788db5a050af128b5794dfd9 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 30 Sep 2024 18:36:39 +0200 Subject: [PATCH 31/35] feat(launchpad): reset confirmation when changing connection mode --- .../src/components/popup/connection_mode.rs | 377 ++++++++++++------ .../src/components/popup/port_range.rs | 167 +++++++- 2 files changed, 412 insertions(+), 132 deletions(-) diff --git a/node-launchpad/src/components/popup/connection_mode.rs b/node-launchpad/src/components/popup/connection_mode.rs index 0cff9bfbb3..71906a12a4 100644 --- a/node-launchpad/src/components/popup/connection_mode.rs +++ b/node-launchpad/src/components/popup/connection_mode.rs @@ -6,7 +6,7 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use std::default::Default; +use std::{default::Default, rc::Rc}; use super::super::utils::centered_rect_fixed; @@ -16,7 +16,9 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Style, Stylize}, text::{Line, Span}, - widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph}, + widgets::{ + Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, Wrap, + }, }; use strum::IntoEnumIterator; @@ -31,9 +33,17 @@ use crate::{ }, }; +#[derive(Default)] +enum ChangeConnectionModeState { + #[default] + Selection, + ConfirmChange, +} + #[derive(Default)] pub struct ChangeConnectionModePopUp { active: bool, + state: ChangeConnectionModeState, items: StatefulList, connection_mode_selection: ConnectionModeItem, connection_mode_initial_state: ConnectionModeItem, @@ -61,6 +71,7 @@ impl ChangeConnectionModePopUp { let items = StatefulList::with_items(connection_modes_items); Ok(Self { active: false, + state: ChangeConnectionModeState::Selection, items, connection_mode_selection: selected_connection_mode.clone(), connection_mode_initial_state: selected_connection_mode.clone(), @@ -106,120 +117,15 @@ impl ChangeConnectionModePopUp { } ConnectionModeItem::default() } -} -impl Component for ChangeConnectionModePopUp { - fn handle_key_events(&mut self, key: KeyEvent) -> Result> { - if !self.active { - return Ok(vec![]); - } - let send_back: Vec = match key.code { - KeyCode::Enter => { - // We allow action if we have more than one connection mode and the action is not - // over the connection mode already selected - let connection_mode = self.return_selection(); - if connection_mode.connection_mode != self.connection_mode_selection.connection_mode - { - debug!( - "Got Enter and there's a new selection, storing value and switching to Options" - ); - debug!("Connection Mode selected: {:?}", connection_mode); - self.connection_mode_initial_state = self.connection_mode_selection.clone(); - self.assign_connection_mode_selection(); - vec![ - Action::StoreConnectionMode(self.connection_mode_selection.connection_mode), - Action::OptionsActions(OptionsActions::UpdateConnectionMode( - connection_mode.clone().connection_mode, - )), - if connection_mode.connection_mode == ConnectionMode::CustomPorts { - Action::SwitchScene(Scene::ChangePortsPopUp { - connection_mode_old_value: Some( - self.connection_mode_initial_state.connection_mode, - ), - }) - } else { - Action::SwitchScene(Scene::Status) - }, - ] - } else { - debug!("Got Enter, but no new selection. We should not do anything"); - vec![Action::SwitchScene(Scene::ChangeConnectionModePopUp)] - } - } - KeyCode::Esc => { - debug!("Got Esc, switching to Options"); - vec![Action::SwitchScene(Scene::Options)] - } - KeyCode::Up => { - if self.items.items.len() > 1 { - self.items.previous(); - let connection_mode = self.return_selection(); - self.can_select = connection_mode.connection_mode - != self.connection_mode_selection.connection_mode; - } - vec![] - } - KeyCode::Down => { - if self.items.items.len() > 1 { - self.items.next(); - let connection_mode = self.return_selection(); - self.can_select = connection_mode.connection_mode - != self.connection_mode_selection.connection_mode; - } - vec![] - } - _ => { - vec![] - } - }; - Ok(send_back) - } - - fn update(&mut self, action: Action) -> Result> { - let send_back = match action { - Action::SwitchScene(scene) => match scene { - Scene::ChangeConnectionModePopUp => { - self.active = true; - self.can_select = false; - self.select_connection_mode(); - Some(Action::SwitchInputMode(InputMode::Entry)) - } - _ => { - self.active = false; - None - } - }, - // Useful when the user has selected a connection mode but didn't confirm it - Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { - self.connection_mode_selection.connection_mode = connection_mode; - self.select_connection_mode(); - None - } - _ => None, - }; - Ok(send_back) - } - - fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { - if !self.active { - return Ok(()); - } - - let layer_zero = centered_rect_fixed(52, 15, area); - - let layer_one = Layout::new( - Direction::Vertical, - [ - // Padding from title to the table - Constraint::Length(1), - // Table - Constraint::Min(1), - // for the pop_up_border - Constraint::Length(1), - ], - ) - .split(layer_zero); + // Draw functions + fn draw_selection_state( + &mut self, + f: &mut crate::tui::Frame<'_>, + layer_zero: Rect, + layer_one: Rc<[Rect]>, + ) -> Paragraph { let pop_up_border: Paragraph = Paragraph::new("").block( Block::default() .borders(Borders::ALL) @@ -305,7 +211,248 @@ impl Component for ChangeConnectionModePopUp { buttons_layer[1], ); - // We render now so the borders are on top of the other widgets + pop_up_border + } + + fn draw_confirm_change( + &mut self, + f: &mut crate::tui::Frame<'_>, + layer_zero: Rect, + layer_one: Rc<[Rect]>, + ) -> Paragraph { + // layer zero + let pop_up_border = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title(" Confirm & Reset ") + .bold() + .title_style(Style::new().fg(VIVID_SKY_BLUE)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(VIVID_SKY_BLUE)), + ); + clear_area(f, layer_zero); + + // split into 3 parts, paragraph, dash, buttons + let layer_two = Layout::new( + Direction::Vertical, + [ + // for the text + Constraint::Length(9), + // gap + Constraint::Length(3), + // for the buttons + Constraint::Length(1), + ], + ) + .split(layer_one[1]); + + let paragraph_text = Paragraph::new(vec![ + Line::from(Span::styled("\n\n", Style::default())), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(vec![ + Span::styled( + "Changing connection mode will ", + Style::default().fg(LIGHT_PERIWINKLE), + ), + Span::styled("reset all nodes.", Style::default().fg(GHOST_WHITE)), + ]), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(vec![ + Span::styled("You’ll need to ", Style::default().fg(LIGHT_PERIWINKLE)), + Span::styled("Add", Style::default().fg(GHOST_WHITE)), + Span::styled(" and ", Style::default().fg(LIGHT_PERIWINKLE)), + Span::styled("Start", Style::default().fg(GHOST_WHITE)), + Span::styled( + " them again afterwards. Are you sure you want to continue?", + Style::default().fg(LIGHT_PERIWINKLE), + ), + ]), + ]) + .alignment(Alignment::Left) + .wrap(Wrap { trim: true }) + .block(Block::default().padding(Padding::horizontal(2))); + + f.render_widget(paragraph_text, layer_two[0]); + + let dash = Block::new() + .borders(Borders::BOTTOM) + .border_style(Style::new().fg(GHOST_WHITE)); + f.render_widget(dash, layer_two[1]); + + let buttons_layer = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layer_two[2]); + + let button_no = Line::from(vec![Span::styled( + " Cancel [Esc]", + Style::default().fg(LIGHT_PERIWINKLE), + )]); + let button_yes_style = if self.can_select { + Style::default().fg(EUCALYPTUS) + } else { + Style::default().fg(LIGHT_PERIWINKLE) + }; + f.render_widget(button_no, buttons_layer[0]); + + let button_yes = Line::from(vec![ + Span::styled("Yes, Change Mode ", button_yes_style), + Span::styled("[Enter]", Style::default().fg(GHOST_WHITE)), + ]); + f.render_widget(button_yes, buttons_layer[1]); + + pop_up_border + } +} + +impl Component for ChangeConnectionModePopUp { + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + if !self.active { + return Ok(vec![]); + } + let send_back: Vec = match &self.state { + ChangeConnectionModeState::Selection => match key.code { + KeyCode::Enter => { + let connection_mode = self.return_selection(); + self.connection_mode_initial_state = self.connection_mode_selection.clone(); + if connection_mode.connection_mode == ConnectionMode::CustomPorts { + vec![ + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + ConnectionMode::CustomPorts, + )), + Action::SwitchScene(Scene::ChangePortsPopUp { + connection_mode_old_value: Some( + self.connection_mode_initial_state.connection_mode, + ), + }), + ] + } else { + self.state = ChangeConnectionModeState::ConfirmChange; + vec![] + } + } + KeyCode::Esc => { + debug!("Got Esc, switching to Options"); + vec![Action::SwitchScene(Scene::Options)] + } + KeyCode::Up => { + if self.items.items.len() > 1 { + self.items.previous(); + let connection_mode = self.return_selection(); + self.can_select = connection_mode.connection_mode + != self.connection_mode_selection.connection_mode; + } + vec![] + } + KeyCode::Down => { + if self.items.items.len() > 1 { + self.items.next(); + let connection_mode = self.return_selection(); + self.can_select = connection_mode.connection_mode + != self.connection_mode_selection.connection_mode; + } + vec![] + } + _ => { + vec![] + } + }, + ChangeConnectionModeState::ConfirmChange => match key.code { + KeyCode::Enter => { + self.state = ChangeConnectionModeState::Selection; + // We allow action if we have more than one connection mode and the action is not + // over the connection mode already selected + let connection_mode = self.return_selection(); + if connection_mode.connection_mode + != self.connection_mode_selection.connection_mode + { + debug!( + "Got Enter and there's a new selection, storing value and switching to Options" + ); + debug!("Connection Mode selected: {:?}", connection_mode); + self.connection_mode_initial_state = self.connection_mode_selection.clone(); + self.assign_connection_mode_selection(); + vec![ + Action::StoreConnectionMode( + self.connection_mode_selection.connection_mode, + ), + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + connection_mode.clone().connection_mode, + )), + Action::SwitchScene(Scene::Status), + ] + } else { + debug!("Got Enter, but no new selection. We should not do anything"); + vec![Action::SwitchScene(Scene::ChangeConnectionModePopUp)] + } + } + KeyCode::Esc => { + self.state = ChangeConnectionModeState::Selection; + vec![Action::SwitchScene(Scene::Options)] + } + _ => { + vec![] + } + }, + }; + Ok(send_back) + } + + fn update(&mut self, action: Action) -> Result> { + let send_back = match action { + Action::SwitchScene(scene) => match scene { + Scene::ChangeConnectionModePopUp => { + self.active = true; + self.can_select = false; + self.select_connection_mode(); + Some(Action::SwitchInputMode(InputMode::Entry)) + } + _ => { + self.active = false; + None + } + }, + // Useful when the user has selected a connection mode but didn't confirm it + Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { + self.connection_mode_selection.connection_mode = connection_mode; + self.select_connection_mode(); + None + } + _ => None, + }; + Ok(send_back) + } + + fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { + if !self.active { + return Ok(()); + } + + let layer_zero = centered_rect_fixed(52, 15, area); + + let layer_one = Layout::new( + Direction::Vertical, + [ + // Padding from title to the table + Constraint::Length(1), + // Table + Constraint::Min(1), + // for the pop_up_border + Constraint::Length(1), + ], + ) + .split(layer_zero); + + let pop_up_border: Paragraph = match self.state { + ChangeConnectionModeState::Selection => { + self.draw_selection_state(f, layer_zero, layer_one) + } + ChangeConnectionModeState::ConfirmChange => { + self.draw_confirm_change(f, layer_zero, layer_one) + } + }; + f.render_widget(pop_up_border, layer_zero); Ok(()) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index 23b79870e5..da426b8e7b 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -33,6 +33,7 @@ enum PortRangeState { #[default] Selection, ConfirmChange, + PortForwardingInfo, } pub struct PortRangePopUp { @@ -185,8 +186,100 @@ impl PortRangePopUp { pop_up_border } - // Draws the Confirmation screen - fn draw_confirm_change_state( + // Draws Confirmation screen + fn draw_confirm_and_reset( + &mut self, + f: &mut crate::tui::Frame<'_>, + layer_zero: Rect, + layer_one: Rc<[Rect]>, + ) -> Paragraph { + // layer zero + let pop_up_border = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title(" Confirm & Reset ") + .bold() + .title_style(Style::new().fg(VIVID_SKY_BLUE)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(VIVID_SKY_BLUE)), + ); + clear_area(f, layer_zero); + + // split into 3 parts, paragraph, dash, buttons + let layer_two = Layout::new( + Direction::Vertical, + [ + // for the text + Constraint::Length(8), + // gap + Constraint::Length(3), + // for the buttons + Constraint::Length(1), + ], + ) + .split(layer_one[1]); + + let paragraph_text = Paragraph::new(vec![ + Line::from(Span::styled("\n\n", Style::default())), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(vec![ + Span::styled( + "Changing connection mode will ", + Style::default().fg(LIGHT_PERIWINKLE), + ), + Span::styled("reset all nodes.", Style::default().fg(GHOST_WHITE)), + ]), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(vec![ + Span::styled("You’ll need to ", Style::default().fg(LIGHT_PERIWINKLE)), + Span::styled("Add", Style::default().fg(GHOST_WHITE)), + Span::styled(" and ", Style::default().fg(LIGHT_PERIWINKLE)), + Span::styled("Start", Style::default().fg(GHOST_WHITE)), + Span::styled( + " them again afterwards. Are you sure you want to continue?", + Style::default().fg(LIGHT_PERIWINKLE), + ), + ]), + ]) + .alignment(Alignment::Left) + .wrap(Wrap { trim: true }) + .block(block::Block::default().padding(Padding::horizontal(2))); + + f.render_widget(paragraph_text, layer_two[0]); + + let dash = Block::new() + .borders(Borders::BOTTOM) + .border_style(Style::new().fg(GHOST_WHITE)); + f.render_widget(dash, layer_two[1]); + + let buttons_layer = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layer_two[2]); + + let button_no = Line::from(vec![Span::styled( + " Cancel [Esc]", + Style::default().fg(LIGHT_PERIWINKLE), + )]); + let button_yes_style = if self.can_save { + Style::default().fg(EUCALYPTUS) + } else { + Style::default().fg(LIGHT_PERIWINKLE) + }; + f.render_widget(button_no, buttons_layer[0]); + + let button_yes = Line::from(vec![ + Span::styled("Yes, Change Mode ", button_yes_style), + Span::styled("[Enter]", Style::default().fg(GHOST_WHITE)), + ]); + f.render_widget(button_yes, buttons_layer[1]); + + pop_up_border + } + + // Draws info regarding router and ports + fn draw_info_port_forwarding( &mut self, f: &mut crate::tui::Frame<'_>, layer_zero: Rect, @@ -204,7 +297,7 @@ impl PortRangePopUp { ); clear_area(f, layer_zero); - // split into 4 parts, for the prompt, input, text, dash , and buttons + // split into 3 parts, 1 paragraph, dash and buttons let layer_two = Layout::new( Direction::Vertical, [ @@ -270,10 +363,11 @@ impl Component for PortRangePopUp { == self.port_from.value().parse::().unwrap_or_default() && self.port_to_old_value == self.port_to.value().parse::().unwrap_or_default() + && self.connection_mode_old_value != Some(ConnectionMode::CustomPorts) && self.can_save { - debug!("Got Enter, but nothing changed, ignoring."); - return Ok(vec![Action::SwitchScene(Scene::Options)]); + self.state = PortRangeState::ConfirmChange; + return Ok(vec![]); } let port_from = self.port_from.value(); let port_to = self.port_to.value(); @@ -284,20 +378,12 @@ impl Component for PortRangePopUp { } debug!("Got Enter, saving the ports and switching to Options Screen",); self.state = PortRangeState::ConfirmChange; - vec![ - Action::StorePortRange( - self.port_from.value().parse().unwrap_or_default(), - self.port_to.value().parse().unwrap_or_default(), - ), - Action::OptionsActions(OptionsActions::UpdatePortRange( - self.port_from.value().parse().unwrap_or_default(), - self.port_to.value().parse().unwrap_or_default(), - )), - ] + vec![] } KeyCode::Esc => { debug!("Got Esc, restoring the old values and switching to actual screen"); if let Some(connection_mode_old_value) = self.connection_mode_old_value { + debug!("{:?}", connection_mode_old_value); vec![ Action::OptionsActions(OptionsActions::UpdateConnectionMode( connection_mode_old_value, @@ -401,6 +487,52 @@ impl Component for PortRangePopUp { } } PortRangeState::ConfirmChange => match key.code { + KeyCode::Enter => { + self.state = PortRangeState::PortForwardingInfo; + vec![ + Action::StoreConnectionMode(ConnectionMode::CustomPorts), + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + ConnectionMode::CustomPorts, + )), + Action::StorePortRange( + self.port_from.value().parse().unwrap_or_default(), + self.port_to.value().parse().unwrap_or_default(), + ), + Action::OptionsActions(OptionsActions::UpdatePortRange( + self.port_from.value().parse().unwrap_or_default(), + self.port_to.value().parse().unwrap_or_default(), + )), + ] + } + KeyCode::Esc => { + self.state = PortRangeState::Selection; + if let Some(connection_mode_old_value) = self.connection_mode_old_value { + if self.port_from_old_value != 0 && self.port_to_old_value != 0 { + vec![ + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + connection_mode_old_value, + )), + Action::OptionsActions(OptionsActions::UpdatePortRange( + self.port_from_old_value, + self.port_to_old_value, + )), + Action::SwitchScene(Scene::Options), + ] + } else { + vec![ + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + connection_mode_old_value, + )), + Action::SwitchScene(Scene::Options), + ] + } + } else { + vec![Action::SwitchScene(Scene::Options)] + } + } + _ => vec![], + }, + PortRangeState::PortForwardingInfo => match key.code { KeyCode::Enter => { debug!("Got Enter, saving the ports and switching to Status Screen",); self.state = PortRangeState::Selection; @@ -471,8 +603,9 @@ impl Component for PortRangePopUp { let pop_up_border: Paragraph = match self.state { PortRangeState::Selection => self.draw_selection_state(f, layer_zero, layer_one), - PortRangeState::ConfirmChange => { - self.draw_confirm_change_state(f, layer_zero, layer_one) + PortRangeState::ConfirmChange => self.draw_confirm_and_reset(f, layer_zero, layer_one), + PortRangeState::PortForwardingInfo => { + self.draw_info_port_forwarding(f, layer_zero, layer_one) } }; // We render now so the borders are on top of the other widgets From 74a3556e13d5dd0277983ce63ef57f1f7c895446 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 2 Oct 2024 12:59:06 +0200 Subject: [PATCH 32/35] fix(launchpad): we change discord username to lowercase --- node-launchpad/src/components/popup/beta_programme.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index 8f0a547fe9..615c20bcf4 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -58,14 +58,15 @@ impl BetaProgramme { fn capture_inputs(&mut self, key: KeyEvent) -> Vec { let send_back = match key.code { KeyCode::Enter => { - let username = self.discord_input_filed.value().to_string(); + let username = self.discord_input_filed.value().to_string().to_lowercase(); + self.discord_input_filed = username.clone().into(); debug!( "Got Enter, saving the discord username {username:?} and switching to DiscordIdAlreadySet, and Home Scene", ); self.state = BetaProgrammeState::DiscordIdAlreadySet; vec![ - Action::StoreDiscordUserName(self.discord_input_filed.value().to_string()), + Action::StoreDiscordUserName(username.clone()), Action::OptionsActions(OptionsActions::UpdateBetaProgrammeUsername(username)), Action::SwitchScene(Scene::Status), ] From f94f632c281c564e7feff10d2b52a779b8b62478 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 2 Oct 2024 12:59:33 +0200 Subject: [PATCH 33/35] chore(launchpad): we reset but do not start nodes after username change --- node-launchpad/src/components/status.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 86a25b4228..0f6dea98b5 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -315,7 +315,7 @@ impl Component for Status { self.lock_registry = Some(LockRegistryState::ResettingNodes); info!("Resetting safenode services because the Discord Username was reset."); let action_sender = self.get_actions_sender()?; - reset_nodes(action_sender, true); + reset_nodes(action_sender, false); } } Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => { From e21b35ca1d86abd25bb5b074eb51e502ec7e4c5e Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Wed, 2 Oct 2024 21:08:35 +0100 Subject: [PATCH 34/35] chore(release): stable release 2024.10.1.3 ================== Crate Versions ================== autonomi: 0.1.1 sn_auditor: 0.3.2 sn_build_info: 0.1.14 sn_cli: 0.95.2 sn_client: 0.110.2 sn_faucet: 0.5.2 sn_logging: 0.2.35 sn_metrics: 0.1.15 nat-detection: 0.2.6 sn_networking: 0.18.3 sn_node: 0.111.3 node-launchpad: 0.3.18 sn_node_manager: 0.10.5 sn_node_rpc_client: 0.6.30 sn_peers_acquisition: 0.5.2 sn_protocol: 0.17.10 sn_registers: 0.3.20 sn_service_management: 0.3.13 sn_transfers: 0.19.2 test_utils: 0.4.6 token_supplies: 0.1.53 =================== Binary Versions =================== faucet: 0.5.2 nat-detection: 0.2.6 node-launchpad: 0.3.18 safe: 0.95.2 safenode: 0.111.3 safenode-manager: 0.10.5 safenode_rpc_client: 0.6.30 safenodemand: 0.10.5 sn_auditor: 0.3.2 --- Cargo.lock | 2 +- node-launchpad/Cargo.toml | 2 +- release-cycle-info | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f74b921cab..bf85dd8399 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4724,7 +4724,7 @@ dependencies = [ [[package]] name = "node-launchpad" -version = "0.3.17" +version = "0.3.18" dependencies = [ "atty", "better-panic", diff --git a/node-launchpad/Cargo.toml b/node-launchpad/Cargo.toml index c25a818373..c92f499ab4 100644 --- a/node-launchpad/Cargo.toml +++ b/node-launchpad/Cargo.toml @@ -2,7 +2,7 @@ authors = ["MaidSafe Developers "] description = "Node Launchpad" name = "node-launchpad" -version = "0.3.17" +version = "0.3.18" edition = "2021" license = "GPL-3.0" homepage = "https://maidsafe.net" diff --git a/release-cycle-info b/release-cycle-info index 1bc2281ec8..b7fbab14d3 100644 --- a/release-cycle-info +++ b/release-cycle-info @@ -15,4 +15,4 @@ release-year: 2024 release-month: 10 release-cycle: 1 -release-cycle-counter: 2 +release-cycle-counter: 3 From 03b9bb2cf53d8ca8f882e094560f4b89b7166bc5 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Thu, 3 Oct 2024 13:52:20 +0100 Subject: [PATCH 35/35] docs: changelog for hotfix `2024.10.1.3` --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7eabe0d6d..26c2b893ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 *When editing this file, please respect a line length of 100.* +## 2024-10-03 + +### Launchpad + +### Changed + +- Upgrade to `Ratatui` v0.28.1 +- Styling and layout fixes + +#### Added + +- Drives that don't have enough space are being shown and flagged +- Error handling and generic error popup +- New metrics in the `Status` section +- Confirmation needed when changing connection mode + +### Fixed + +- NAT mode only on first start in `Automatic Connection Mode` +- Force Discord username to be in lowercase + ## 2024-10-01 ### Launchpad