From b91d6a2afd02a1665d24bd55932f232a5709b752 Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Thu, 19 Sep 2024 23:12:21 +0200 Subject: [PATCH] feat: notifications (#10) --- edc-connector-tui/src/app.rs | 32 ++++++-- edc-connector-tui/src/app/action.rs | 8 ++ edc-connector-tui/src/app/msg.rs | 3 +- edc-connector-tui/src/components.rs | 76 ++++++++++++++++++- edc-connector-tui/src/components/footer.rs | 43 +++++++++-- edc-connector-tui/src/components/header.rs | 2 +- edc-connector-tui/src/components/resources.rs | 13 +++- .../src/components/resources/msg.rs | 1 + .../src/components/resources/resource.rs | 6 +- edc-connector-tui/src/components/table.rs | 2 +- edc-connector-tui/src/config.rs | 2 +- edc-connector-tui/src/runner.rs | 75 +++++++++++++----- edc-connector-tui/src/types/connector.rs | 2 +- edc-connector-tui/src/types/nav.rs | 12 +-- 14 files changed, 229 insertions(+), 48 deletions(-) diff --git a/edc-connector-tui/src/app.rs b/edc-connector-tui/src/app.rs index 8104dd8..47ec09f 100644 --- a/edc-connector-tui/src/app.rs +++ b/edc-connector-tui/src/app.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{rc::Rc, time::Duration}; mod action; mod fetch; pub mod model; @@ -17,7 +17,8 @@ use crate::{ assets::AssetsComponent, connectors::ConnectorsComponent, contract_definitions::ContractDefinitionsComponent, footer::Footer, header::HeaderComponent, launch_bar::LaunchBar, policies::PolicyDefinitionsComponent, - Component, ComponentEvent, ComponentMsg, ComponentReturn, + Action, Component, ComponentEvent, ComponentMsg, ComponentReturn, Notification, + NotificationMsg, }, config::{AuthKind, Config, ConnectorConfig}, types::{ @@ -29,6 +30,8 @@ use crate::{ use self::{model::AppFocus, msg::AppMsg}; +const SERVICE: &str = "edc-connector-tui"; + pub struct App { connectors: ConnectorsComponent, policies: PolicyDefinitionsComponent, @@ -46,8 +49,7 @@ impl App { match cfg.auth() { AuthKind::NoAuth => (ConnectorStatus::Connected, Auth::NoAuth), AuthKind::Token { token_alias } => { - let entry = - Entry::new("edc-tui", &token_alias).and_then(|entry| entry.get_password()); + let entry = Entry::new(SERVICE, token_alias).and_then(|entry| entry.get_password()); match entry { Ok(pwd) => (ConnectorStatus::Connected, Auth::api_token(pwd)), @@ -105,6 +107,25 @@ impl App { .key_binding("<:q>", "Quit") } + pub fn show_notification( + &mut self, + noty: Notification, + ) -> anyhow::Result> { + let timeout = noty.timeout(); + self.footer.show_notification(noty); + + let action = Action::spawn(async move { + tokio::time::sleep(Duration::from_secs(timeout)).await; + Ok(Action::ClearNotification) + }); + Ok(ComponentReturn::action(action)) + } + + pub fn clear_notification(&mut self) -> anyhow::Result> { + self.footer.clear_notification(); + Ok(ComponentReturn::empty()) + } + pub fn change_sheet(&mut self) -> anyhow::Result> { let component_sheet = match self.header.selected_menu() { Menu::Connectors => InfoSheet::default(), @@ -127,7 +148,6 @@ impl App { self.launch_bar.clear(); self.header.set_selected_menu(nav); self.change_sheet()?; - match self.header.selected_menu() { Menu::Connectors => { self.focus = AppFocus::ConnectorList; @@ -240,6 +260,8 @@ impl Component for App { } AppMsg::RoutingMsg(nav) => self.handle_routing(nav).await, AppMsg::ChangeSheet => self.change_sheet(), + AppMsg::NontificationMsg(NotificationMsg::Show(noty)) => self.show_notification(noty), + AppMsg::NontificationMsg(NotificationMsg::Clear) => self.clear_notification(), } } diff --git a/edc-connector-tui/src/app/action.rs b/edc-connector-tui/src/app/action.rs index 339fdaa..8a340a6 100644 --- a/edc-connector-tui/src/app/action.rs +++ b/edc-connector-tui/src/app/action.rs @@ -12,6 +12,14 @@ impl ActionHandler for App { (AppFocus::LaunchBar, Action::Esc) => Ok(vec![AppMsg::HideLaunchBar.into()]), (_, Action::NavTo(nav)) => Ok(vec![AppMsg::RoutingMsg(nav).into()]), (_, Action::ChangeSheet) => Ok(vec![AppMsg::ChangeSheet.into()]), + (_, Action::Notification(noty)) => Ok(vec![AppMsg::NontificationMsg( + crate::components::NotificationMsg::Show(noty), + ) + .into()]), + (_, Action::ClearNotification) => Ok(vec![AppMsg::NontificationMsg( + crate::components::NotificationMsg::Clear, + ) + .into()]), _ => Ok(vec![]), } } diff --git a/edc-connector-tui/src/app/msg.rs b/edc-connector-tui/src/app/msg.rs index 0a666b7..9dc7fb4 100644 --- a/edc-connector-tui/src/app/msg.rs +++ b/edc-connector-tui/src/app/msg.rs @@ -2,7 +2,7 @@ use crate::{ components::{ assets::AssetsMsg, connectors::msg::ConnectorsMsg, contract_definitions::ContractDefinitionsMsg, header::msg::HeaderMsg, - launch_bar::msg::LaunchBarMsg, policies::PoliciesMsg, + launch_bar::msg::LaunchBarMsg, policies::PoliciesMsg, NotificationMsg, }, types::nav::Nav, }; @@ -18,5 +18,6 @@ pub enum AppMsg { ContractDefinitions(ContractDefinitionsMsg), HeaderMsg(HeaderMsg), RoutingMsg(Nav), + NontificationMsg(NotificationMsg), ChangeSheet, } diff --git a/edc-connector-tui/src/components.rs b/edc-connector-tui/src/components.rs index 4b23177..8ad15d3 100644 --- a/edc-connector-tui/src/components.rs +++ b/edc-connector-tui/src/components.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, future::Future, sync::Arc}; use crossterm::event::Event; use futures::{future::BoxFuture, FutureExt}; @@ -107,12 +107,83 @@ impl<'a, T: Debug> Debug for ComponentReturn<'a, T> { } } -#[derive(Debug, Clone)] pub enum Action { Quit, Esc, NavTo(Nav), ChangeSheet, + Notification(Notification), + ClearNotification, + Spawn(BoxFuture<'static, anyhow::Result>), +} + +impl Action { + pub fn spawn(fut: impl Future> + Send + 'static) -> Action { + Action::Spawn(fut.boxed()) + } +} + +impl Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Quit => write!(f, "Quit"), + Self::Esc => write!(f, "Esc"), + Self::NavTo(arg0) => f.debug_tuple("NavTo").field(arg0).finish(), + Self::ChangeSheet => write!(f, "ChangeSheet"), + Self::Notification(arg0) => f.debug_tuple("Notification").field(arg0).finish(), + Self::Spawn(_arg0) => f.debug_tuple("Spawn").finish(), + Self::ClearNotification => f.debug_tuple("ClearNotification").finish(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Notification { + msg: String, + kind: NotificationKind, + timeout: u64, +} + +#[derive(Debug, Clone)] +pub enum NotificationMsg { + Show(Notification), + Clear, +} + +impl Notification { + pub fn error(msg: String) -> Notification { + Notification { + msg, + kind: NotificationKind::Error, + timeout: 5, + } + } + + pub fn info(msg: String) -> Notification { + Notification { + msg, + kind: NotificationKind::Info, + timeout: 5, + } + } + + pub fn msg(&self) -> &str { + &self.msg + } + + pub fn kind(&self) -> &NotificationKind { + &self.kind + } + + pub fn timeout(&self) -> u64 { + self.timeout + } +} + +#[derive(Debug, Clone)] +pub enum NotificationKind { + Error, + Info, } impl ComponentMsg { @@ -136,6 +207,7 @@ impl<'a, T: 'a> ComponentReturn<'a, T> { actions: vec![], } } + pub fn empty() -> ComponentReturn<'a, T> { ComponentReturn { msgs: vec![], diff --git a/edc-connector-tui/src/components/footer.rs b/edc-connector-tui/src/components/footer.rs index 71f0992..38d03cc 100644 --- a/edc-connector-tui/src/components/footer.rs +++ b/edc-connector-tui/src/components/footer.rs @@ -1,15 +1,19 @@ use ratatui::{ - layout::Rect, - widgets::{Block, Borders}, + layout::{Alignment, Rect}, + style::{Color, Style}, + text::{Span, Text}, + widgets::{Block, Borders, Paragraph}, Frame, }; -use super::Component; +use super::{Component, Notification, NotificationKind}; pub mod msg; #[derive(Default)] -pub struct Footer {} +pub struct Footer { + noty: Option, +} #[async_trait::async_trait] impl Component for Footer { @@ -18,6 +22,35 @@ impl Component for Footer { fn view(&mut self, f: &mut Frame, rect: Rect) { let block = Block::default().borders(Borders::all()); - f.render_widget(block, rect) + + let content = self.noty.as_ref().map(Notification::msg).unwrap_or(""); + + let style = self + .noty + .as_ref() + .map(Footer::map_color) + .unwrap_or_default(); + + let text = Text::from(Span::styled(content, style)); + let p = Paragraph::new(text) + .block(block) + .alignment(Alignment::Center); + f.render_widget(p, rect) + } +} + +impl Footer { + pub fn show_notification(&mut self, noty: Notification) { + self.noty = Some(noty); + } + pub fn clear_notification(&mut self) { + self.noty = None; + } + + fn map_color(noty: &Notification) -> Style { + match noty.kind() { + NotificationKind::Error => Style::default().fg(Color::Red), + NotificationKind::Info => Style::default().fg(Color::Cyan), + } } } diff --git a/edc-connector-tui/src/components/header.rs b/edc-connector-tui/src/components/header.rs index 8e95963..8bf8a0c 100644 --- a/edc-connector-tui/src/components/header.rs +++ b/edc-connector-tui/src/components/header.rs @@ -75,7 +75,7 @@ impl Component for HeaderComponent { HeaderMsg::NextTab => { let current = self.menu.clone(); let idx = (self.menu.ordinal() + 1) % Menu::VALUES.len(); - self.menu = Menu::from_ordinal(idx).unwrap_or_else(|| current); + self.menu = Menu::from_ordinal(idx).unwrap_or(current); Ok(ComponentReturn::action(super::Action::NavTo( self.menu.clone().into(), ))) diff --git a/edc-connector-tui/src/components/resources.rs b/edc-connector-tui/src/components/resources.rs index f5de2bd..eaf6dbe 100644 --- a/edc-connector-tui/src/components/resources.rs +++ b/edc-connector-tui/src/components/resources.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, sync::Arc}; use self::{msg::ResourcesMsg, resource::ResourceComponent}; use super::{ table::{msg::TableMsg, TableEntry, UiTable}, - Action, Component, ComponentEvent, ComponentMsg, ComponentReturn, + Action, Component, ComponentEvent, ComponentMsg, ComponentReturn, Notification, }; use crate::types::{connector::Connector, info::InfoSheet}; use crossterm::event::{Event, KeyCode}; @@ -84,8 +84,12 @@ impl Component for ResourcesComp if let Some(on_fetch) = self.on_fetch.as_ref() { Ok(ComponentReturn::cmd( async move { - let elements = on_fetch(&connector).await?; - Ok(vec![ResourcesMsg::ResourcesFetched(elements).into()]) + match on_fetch(&connector).await { + Ok(elements) => Ok(vec![ResourcesMsg::ResourcesFetched(elements).into()]), + Err(err) => Ok(vec![ + ResourcesMsg::ResourcesFetchFailed(err.to_string()).into() + ]), + } } .boxed(), )) @@ -126,6 +130,9 @@ impl Component for ResourcesComp Self::forward_update(&mut self.resource, msg.into(), ResourcesMsg::ResourceMsg) .await } + ResourcesMsg::ResourcesFetchFailed(error) => Ok(ComponentReturn::action( + Action::Notification(Notification::error(error)), + )), } } diff --git a/edc-connector-tui/src/components/resources/msg.rs b/edc-connector-tui/src/components/resources/msg.rs index 39d04c6..737def7 100644 --- a/edc-connector-tui/src/components/resources/msg.rs +++ b/edc-connector-tui/src/components/resources/msg.rs @@ -9,4 +9,5 @@ pub enum ResourcesMsg { TableEvent(TableMsg>>), ResourceMsg(ResourceMsg), ResourcesFetched(Vec), + ResourcesFetchFailed(String), } diff --git a/edc-connector-tui/src/components/resources/resource.rs b/edc-connector-tui/src/components/resources/resource.rs index fa0ddba..25b10c1 100644 --- a/edc-connector-tui/src/components/resources/resource.rs +++ b/edc-connector-tui/src/components/resources/resource.rs @@ -81,9 +81,9 @@ impl ResourceComponent { fn handle_key(&self, key: KeyEvent) -> Vec> { match key.code { - KeyCode::Char('j') => vec![(ComponentMsg(ResourceMsg::MoveDown.into()))], - KeyCode::Char('k') => vec![(ComponentMsg(ResourceMsg::MoveUp.into()))], - KeyCode::Char('y') => vec![(ComponentMsg(ResourceMsg::Yank.into()))], + KeyCode::Char('j') => vec![(ComponentMsg(ResourceMsg::MoveDown))], + KeyCode::Char('k') => vec![(ComponentMsg(ResourceMsg::MoveUp))], + KeyCode::Char('y') => vec![(ComponentMsg(ResourceMsg::Yank))], _ => vec![], } } diff --git a/edc-connector-tui/src/components/table.rs b/edc-connector-tui/src/components/table.rs index e807a42..56a1e16 100644 --- a/edc-connector-tui/src/components/table.rs +++ b/edc-connector-tui/src/components/table.rs @@ -132,7 +132,7 @@ impl UiTable { if let Some(cb) = self.on_select.as_ref() { if let Some(idx) = self.table_state.selected() { if let Some(element) = self.elements.get(idx) { - vec![ComponentMsg(TableMsg::Outer(cb(&element)))] + vec![ComponentMsg(TableMsg::Outer(cb(element)))] } else { vec![] } diff --git a/edc-connector-tui/src/config.rs b/edc-connector-tui/src/config.rs index 4f95a45..80a1507 100644 --- a/edc-connector-tui/src/config.rs +++ b/edc-connector-tui/src/config.rs @@ -33,7 +33,7 @@ impl Config { let config: Result = toml::from_str(&contents); match config { - Ok(config) => return Ok(config), + Ok(config) => Ok(config), Err(e) => panic!("fail to parse config file: {}", e), } } diff --git a/edc-connector-tui/src/runner.rs b/edc-connector-tui/src/runner.rs index c1a02b7..69f06b7 100644 --- a/edc-connector-tui/src/runner.rs +++ b/edc-connector-tui/src/runner.rs @@ -1,9 +1,10 @@ -use std::{collections::VecDeque, time::Duration}; +use std::{collections::VecDeque, sync::Arc, time::Duration}; use crossterm::event; use ratatui::{backend::Backend, Terminal}; +use tokio::sync::Mutex; -use crate::components::{Action, ActionHandler, Component, ComponentEvent}; +use crate::components::{Action, ActionHandler, Component, ComponentEvent, ComponentMsg}; pub struct Runner { tick_rate: Duration, @@ -22,45 +23,81 @@ impl::Msg> + Send> Runner terminal.clear()?; let mut should_quit = false; + let action_queue = Arc::new(Mutex::new(VecDeque::::new())); + let async_msgs = Arc::new(Mutex::new( + VecDeque::::Msg>>::new(), + )); loop { if should_quit { break; } - terminal.draw(|frame| self.component.view(frame, frame.size()))?; + terminal.draw(|frame| self.component.view(frame, frame.area()))?; + + let mut msgs = VecDeque::new(); + let mut guard = async_msgs.lock().await; + + while let Some(m) = guard.pop_front() { + msgs.push_front(m); + } + drop(guard); if event::poll(self.tick_rate)? { let evt = event::read()?; - let mut msgs = self + let event_msgs = self .component .handle_event(ComponentEvent::Event(evt))? .into_iter() - .collect::>(); + .collect::>(); - while let Some(msg) = msgs.pop_front() { - let actions = { - let ret = self.component.update(msg).await?; + for m in event_msgs { + msgs.push_back(m); + } + }; - for m in ret.msgs { - msgs.push_back(m); - } + while let Some(msg) = msgs.pop_front() { + let actions = { + let ret = self.component.update(msg).await?; - for c in ret.cmds { - for m in c.await.unwrap() { - msgs.push_back(m); - } + for m in ret.msgs { + msgs.push_back(m); + } + + for c in ret.cmds { + for m in c.await.unwrap() { + msgs.push_back(m); } + } - ret.actions - }; + ret.actions + }; - for a in actions { + for a in actions { + if let Action::Spawn(handler) = a { + let inner_action_queue = action_queue.clone(); + tokio::task::spawn(async move { + if let Ok(action) = handler.await { + inner_action_queue.lock().await.push_back(action) + } + }); + } else { should_quit = should_quit || matches!(a, Action::Quit); for m in self.component.handle_action(a)? { msgs.push_back(m) } } } - }; + } + let mut guard = action_queue.lock().await; + let mut msg_guard = async_msgs.lock().await; + while let Some(a) = guard.pop_front() { + if let Action::Spawn(_) = a { + } else { + should_quit = should_quit || matches!(a, Action::Quit); + for m in self.component.handle_action(a)? { + msg_guard.push_back(m) + } + } + } } Ok(()) diff --git a/edc-connector-tui/src/types/connector.rs b/edc-connector-tui/src/types/connector.rs index 6e56b38..5a97816 100644 --- a/edc-connector-tui/src/types/connector.rs +++ b/edc-connector-tui/src/types/connector.rs @@ -21,7 +21,7 @@ impl ConnectorStatus { pub fn as_str(&self) -> &str { match self { ConnectorStatus::Connected => "connected", - ConnectorStatus::Custom(msg) => &msg, + ConnectorStatus::Custom(msg) => msg, } } } diff --git a/edc-connector-tui/src/types/nav.rs b/edc-connector-tui/src/types/nav.rs index 3f0cc79..19cd109 100644 --- a/edc-connector-tui/src/types/nav.rs +++ b/edc-connector-tui/src/types/nav.rs @@ -45,9 +45,9 @@ impl Menu { } } -impl Into for Nav { - fn into(self) -> Menu { - match self { +impl From