diff --git a/oryx-tui/src/app.rs b/oryx-tui/src/app.rs index ae1cc1c..82e9eee 100644 --- a/oryx-tui/src/app.rs +++ b/oryx-tui/src/app.rs @@ -22,6 +22,7 @@ pub enum ActivePopup { Help, UpdateFilters, PacketInfos, + NewFirewallRule, } #[derive(Debug)] diff --git a/oryx-tui/src/handler.rs b/oryx-tui/src/handler.rs index aeafcf9..daf9145 100644 --- a/oryx-tui/src/handler.rs +++ b/oryx-tui/src/handler.rs @@ -6,6 +6,7 @@ use crate::{ export::export, filter::FocusedBlock, notification::{Notification, NotificationLevel}, + section::{FocusedSection, Section}, }; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -56,25 +57,41 @@ pub fn handle_key_events( match key_event.code { KeyCode::Esc => { app.active_popup = None; - if popup == ActivePopup::UpdateFilters { - app.filter.handle_key_events(key_event, true); + match popup { + ActivePopup::UpdateFilters => { + app.filter.handle_key_events(key_event, true); + } + ActivePopup::NewFirewallRule => { + app.section.firewall.handle_keys(key_event); + app.is_editing = false; + } + _ => {} } } - KeyCode::Enter => { - if popup == ActivePopup::UpdateFilters - && app.filter.focused_block == FocusedBlock::Apply - { - app.filter - .update(sender.clone(), app.data_channel_sender.clone())?; + KeyCode::Enter => match popup { + ActivePopup::UpdateFilters => { + if app.filter.focused_block == FocusedBlock::Apply { + app.filter + .update(sender.clone(), app.data_channel_sender.clone())?; - app.active_popup = None; + app.active_popup = None; + } } - } - _ => { - if popup == ActivePopup::UpdateFilters { + ActivePopup::NewFirewallRule => { + app.section.firewall.handle_keys(key_event); + } + _ => {} + }, + + _ => match popup { + ActivePopup::UpdateFilters => { app.filter.handle_key_events(key_event, true); } - } + ActivePopup::NewFirewallRule => { + app.section.firewall.handle_keys(key_event); + } + _ => {} + }, } return Ok(()); @@ -122,9 +139,19 @@ pub fn handle_key_events( } } - KeyCode::Char('/') | KeyCode::Char('n') => { - app.is_editing = true; - app.section.handle_keys(key_event); + KeyCode::Char('/') => { + if app.section.focused_section == FocusedSection::Inspection { + app.is_editing = true; + app.section.handle_keys(key_event); + } + } + + KeyCode::Char('n') => { + if app.section.focused_section == FocusedSection::Firewall { + app.is_editing = true; + app.section.handle_keys(key_event); + app.active_popup = Some(ActivePopup::NewFirewallRule); + } } KeyCode::Char('i') => { diff --git a/oryx-tui/src/section.rs b/oryx-tui/src/section.rs index 0f30efc..82373e7 100644 --- a/oryx-tui/src/section.rs +++ b/oryx-tui/src/section.rs @@ -31,7 +31,7 @@ pub enum FocusedSection { #[derive(Debug)] pub struct Section { - focused_section: FocusedSection, + pub focused_section: FocusedSection, pub inspection: Inspection, pub stats: Stats, pub alert: Alert, @@ -111,7 +111,7 @@ impl Section { FocusedSection::Inspection => self.inspection.render(frame, block), FocusedSection::Stats => self.stats.render(frame, block, network_interace), FocusedSection::Alerts => self.alert.render(frame, block), - FocusedSection::Firewall => self.alert.render(frame, block), + FocusedSection::Firewall => self.firewall.render(frame, block), } } @@ -135,6 +135,11 @@ impl Section { if self.focused_section == FocusedSection::Inspection { self.inspection.handle_keys(key_event); } + match self.focused_section { + FocusedSection::Inspection => self.inspection.handle_keys(key_event), + FocusedSection::Firewall => self.firewall.handle_keys(key_event), + _ => {} + } } } } diff --git a/oryx-tui/src/section/firewall.rs b/oryx-tui/src/section/firewall.rs index e035ca4..161bd9e 100644 --- a/oryx-tui/src/section/firewall.rs +++ b/oryx-tui/src/section/firewall.rs @@ -1,61 +1,23 @@ -use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::{ - layout::{Alignment, Constraint, Flex, Rect}, - style::{Style, Stylize}, - text::Line, - widgets::{Block, Borders, Padding, Row, Table}, + layout::{Constraint, Direction, Flex, Layout, Margin, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Text}, + widgets::{Block, Borders, Cell, Clear, HighlightSpacing, Padding, Row, Table, TableState}, Frame, }; -use std::net::IpAddr; -use std::str::FromStr; +use std::{net::IpAddr, str::FromStr}; use tui_input::{backend::crossterm::EventHandler, Input}; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct FirewallRule { name: String, enabled: bool, - ip: Option, - port: Option, + ip: IpAddr, + port: u16, } -impl FirewallRule { - pub fn new() -> Self { - Self { - name: "".to_string(), - enabled: false, - ip: None, - port: None, - } - } - pub fn update(&mut self, inputs: Inputs) { - match inputs.focus { - FocusedInput::Name => self.name = inputs.name.value().into(), - FocusedInput::Ip => { - let ip = IpAddr::from_str(inputs.ip.value()); - match ip { - Ok(ipaddr) => self.ip = Some(ipaddr), - _ => {} //TODO: error notif - } - } - FocusedInput::Port => { - let p = String::from(inputs.port).parse::(); - match p { - Ok(port) => self.port = Some(port), - _ => {} //TODO: error notif - } - } - } - } -} - -#[derive(Debug, Clone)] -pub struct Firewall { - rules: Vec, - is_editing: bool, - focused_rule: Option, - inputs: Inputs, -} -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum FocusedInput { Name, Ip, @@ -63,232 +25,325 @@ pub enum FocusedInput { } #[derive(Debug, Clone)] -struct Inputs { - pub name: Input, - pub ip: Input, - pub port: Input, - pub focus: FocusedInput, +struct UserInput { + pub name: UserInputField, + pub ip: UserInputField, + pub port: UserInputField, + focus_input: FocusedInput, } -impl Inputs { +#[derive(Debug, Clone, Default)] +struct UserInputField { + field: Input, + error: String, +} + +impl UserInput { pub fn new() -> Self { Self { - name: Input::new("".to_string()), - ip: Input::new("".to_string()), - port: Input::new("".to_string()), - focus: FocusedInput::Name, + name: UserInputField::default(), + ip: UserInputField::default(), + port: UserInputField::default(), + focus_input: FocusedInput::Name, } } - pub fn reset(&mut self) { - self.name.reset(); - self.ip.reset(); - self.port.reset(); + + fn validate_name(&mut self) { + self.name.error.clear(); + if self.name.field.value().is_empty() { + self.name.error = "Required field.".to_string(); + } + } + + fn validate_ip(&mut self) { + self.ip.error.clear(); + if self.ip.field.value().is_empty() { + self.ip.error = "Required field.".to_string(); + } else if IpAddr::from_str(self.ip.field.value()).is_err() { + self.ip.error = "Invalid IP Address.".to_string(); + } + } + + fn validate_port(&mut self) { + self.port.error.clear(); + if self.port.field.value().is_empty() { + self.port.error = "Required field.".to_string(); + } else if u16::from_str(self.port.field.value()).is_err() { + self.port.error = "Invalid Port number.".to_string(); + } } - pub fn handle_event(&mut self, event: &crossterm::event::Event) { - let _ = match self.focus { - FocusedInput::Name => self.name.handle_event(event), - FocusedInput::Ip => self.ip.handle_event(event), - FocusedInput::Port => self.port.handle_event(event), - }; + fn validate(&mut self) { + self.validate_name(); + self.validate_ip(); + self.validate_port(); } - pub fn render(&mut self, frame: &mut Frame, block: Rect) { - let edited_value = match self.focus { - FocusedInput::Name => self.name.value(), - FocusedInput::Ip => self.ip.value(), - FocusedInput::Port => self.port.value(), - }; - - Paragraph::new(format!("> {}", edited_value)) - .alignment(Alignment::Left) - .style(Style::default().white()) + + pub fn render(&mut self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Max(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let rows = [ + Row::new(vec![ + Cell::from(self.name.field.to_string()) + .bg({ + if self.focus_input == FocusedInput::Name { + Color::Gray + } else { + Color::DarkGray + } + }) + .fg(Color::White), + Cell::from(self.ip.field.to_string()) + .bg({ + if self.focus_input == FocusedInput::Ip { + Color::Gray + } else { + Color::DarkGray + } + }) + .fg(Color::White), + Cell::from(self.port.field.to_string()) + .bg({ + if self.focus_input == FocusedInput::Port { + Color::Gray + } else { + Color::DarkGray + } + }) + .fg(Color::White), + ]), + Row::new(vec![Cell::new(""), Cell::new(""), Cell::new("")]), + Row::new(vec![ + Cell::from(self.name.clone().error).red(), + Cell::from(self.ip.clone().error).red(), + Cell::from(self.port.clone().error).red(), + ]), + ]; + + let widths = [ + Constraint::Percentage(33), + Constraint::Percentage(33), + Constraint::Percentage(33), + ]; + + let table = Table::new(rows, widths) + .header( + Row::new(vec![ + Line::from("Name").centered(), + Line::from("IP").centered(), + Line::from("Port").centered(), + ]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .column_spacing(2) + .flex(Flex::SpaceBetween) + .highlight_spacing(HighlightSpacing::Always) .block( - Block::new() - .borders(Borders::TOP) - .title(" Search  ") - .padding(Padding::horizontal(1)) - .title_style({ Style::default().bold().yellow() }), + Block::default() + .title(" New Firewall Rule ") + .title_alignment(ratatui::layout::Alignment::Center) + .borders(Borders::all()) + .border_type(ratatui::widgets::BorderType::Thick) + .border_style(Style::default().green()) + .padding(Padding::uniform(1)), ); - frame.render_widget(fuzzy, fuzzy_block); + frame.render_widget(Clear, block); + frame.render_widget(table, block); } } -impl From for Inputs { - fn from(rule: FirewallRule) -> Self { - Self { - name: Input::new(rule.name), - ip: Input::new(match rule.ip { - Some(ip) => ip.to_string(), - None => "".to_string(), - }), - port: Input::new(match rule.port { - Some(port) => port.to_string(), - None => "".to_string(), - }), - focus: FocusedInput::Name, - } - } +#[derive(Debug, Clone, Default)] +pub struct Firewall { + rules: Vec, + state: TableState, + user_input: Option, } impl Firewall { pub fn new() -> Self { Self { rules: Vec::new(), - is_editing: false, - focused_rule: None, - inputs: Inputs::new(), + state: TableState::default(), + user_input: None, } } - pub fn add_rule(&mut self, rule: FirewallRule) { - if self.rules.iter().any(|r| r.name == rule.name) { - return; - } - self.rules.push(rule); + pub fn add_rule(&mut self) { + self.user_input = Some(UserInput::new()); } + pub fn remove_rule(&mut self, rule: &FirewallRule) { self.rules.retain(|r| r.name != rule.name); } + pub fn handle_keys(&mut self, key_event: KeyEvent) { - if self.is_editing { + if let Some(user_input) = &mut self.user_input { match key_event.code { KeyCode::Esc => { - self.is_editing = false; - self.inputs.reset() + self.user_input = None; } + KeyCode::Enter => { - self.is_editing = false; - self.focused_rule - .as_mut() - .unwrap() - .update(self.inputs.clone()); - self.inputs.reset(); + if let Some(user_input) = &mut self.user_input { + user_input.validate(); + } } - _ => { - self.inputs - .handle_event(&crossterm::event::Event::Key(key_event)); + + KeyCode::Tab => { + if let Some(user_input) = &mut self.user_input { + match user_input.focus_input { + FocusedInput::Name => user_input.focus_input = FocusedInput::Ip, + FocusedInput::Ip => user_input.focus_input = FocusedInput::Port, + FocusedInput::Port => user_input.focus_input = FocusedInput::Name, + } + } } + + _ => match user_input.focus_input { + FocusedInput::Name => { + user_input.name.field.handle_event(&Event::Key(key_event)); + } + FocusedInput::Ip => { + user_input.ip.field.handle_event(&Event::Key(key_event)); + } + FocusedInput::Port => { + user_input.port.field.handle_event(&Event::Key(key_event)); + } + }, } } else { match key_event.code { - KeyCode::Char('j') | KeyCode::Down => {} - KeyCode::Char('k') | KeyCode::Up => {} KeyCode::Char('n') => { - self.is_editing = true; - self.add_rule(FirewallRule::new()); + self.add_rule(); + } + + KeyCode::Char('j') | KeyCode::Down => { + let i = match self.state.selected() { + Some(i) => { + if i < self.rules.len() - 1 { + i + 1 + } else { + i + } + } + None => 0, + }; + + self.state.select(Some(i)); + } + + KeyCode::Char('k') | KeyCode::Up => { + let i = match self.state.selected() { + Some(i) => { + if i > 1 { + i - 1 + } else { + 0 + } + } + None => 0, + }; + + self.state.select(Some(i)); } _ => {} } } } + pub fn render(&self, frame: &mut Frame, block: Rect) { + if self.rules.is_empty() { + let text_block = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .margin(2) + .split(block)[1]; + + let text = Text::from("No Rules").bold().centered(); + frame.render_widget(text, text_block); + return; + } + let widths = [ - Constraint::Min(30), - Constraint::Min(20), + Constraint::Max(30), + Constraint::Max(20), Constraint::Length(10), Constraint::Length(10), ]; let rows = self.rules.iter().map(|rule| { - if self.is_editing && self.focused_rule.as_ref().unwrap().name == rule.name { - Row::new(vec![ - Line::from(rule.name.clone()).centered().bold(), - Line::from({ - if let Some(ip) = rule.ip { - ip.to_string() - } else { - "-".to_string() - } - }) + Row::new(vec![ + Line::from(rule.name.clone()).centered().bold(), + Line::from(rule.ip.to_string()).centered().centered().bold(), + Line::from(rule.port.to_string()) .centered() - .bold(), - Line::from({ - if let Some(port) = rule.port { - port.to_string() - } else { - "-".to_string() - } - }) - .centered(), - Line::from(rule.enabled.to_string()).centered(), - ]) - } else { - Row::new(vec![ - Line::from(rule.name.clone()).centered().bold(), - Line::from({ - if let Some(ip) = rule.ip { - ip.to_string() - } else { - "-".to_string() - } - }) .centered() .bold(), - Line::from({ - if let Some(port) = rule.port { - port.to_string() - } else { - "-".to_string() - } - }) - .centered(), - Line::from(rule.enabled.to_string()).centered(), - ]) - } + Line::from({ + if rule.enabled { + "Enabled".to_string() + } else { + "Disabled".to_string() + } + }) + .centered() + .centered() + .bold(), + ]) }); + let table = Table::new(rows, widths) .column_spacing(2) - .flex(Flex::SpaceBetween) + .flex(Flex::SpaceAround) + .highlight_style(Style::default().bg(Color::DarkGray)) .header( Row::new(vec![ Line::from("Name").centered(), - Line::from("IP Address").centered(), + Line::from("IP").centered(), Line::from("Port").centered(), - Line::from("Enabled?").centered(), + Line::from("Status").centered(), ]) .style(Style::new().bold()) .bottom_margin(1), - ) - .block( - Block::new() - .title(" Firewall Rules ") - .borders(Borders::all()) - .border_style(Style::new().yellow()) - .title_alignment(Alignment::Center) - .padding(Padding::uniform(2)), ); - frame.render_widget(table, block); + frame.render_widget( + table, + block.inner(Margin { + horizontal: 2, + vertical: 2, + }), + ); } -} -// Paragraph::new(format!("> {}", fuzzy.filter.value())) -// .alignment(Alignment::Left) -// .style(Style::default().white()) -// .block( -// Block::new() -// .borders(Borders::TOP) -// .title(" Search  ") -// .padding(Padding::horizontal(1)) -// .title_style({ -// if fuzzy.is_paused() { -// Style::default().bold().yellow() -// } else { -// Style::default().bold().green() -// } -// }) -// .border_type({ -// if fuzzy.is_paused() { -// BorderType::default() -// } else { -// BorderType::Thick -// } -// }) -// .border_style({ -// if fuzzy.is_paused() { -// Style::default().yellow() -// } else { -// Style::default().green() -// } -// }), + pub fn render_new_rule_popup(&self, frame: &mut Frame) { + if let Some(user_input) = &mut self.user_input.clone() { + user_input.render(frame); + } + } +} diff --git a/oryx-tui/src/ui.rs b/oryx-tui/src/ui.rs index d30b9ca..1453a73 100644 --- a/oryx-tui/src/ui.rs +++ b/oryx-tui/src/ui.rs @@ -10,6 +10,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { ActivePopup::Help => app.help.render(frame), ActivePopup::PacketInfos => app.section.inspection.render_packet_infos_popup(frame), ActivePopup::UpdateFilters => app.filter.render_update_popup(frame), + ActivePopup::NewFirewallRule => app.section.firewall.render_new_rule_popup(frame), } } for (index, notification) in app.notifications.iter().enumerate() {