From c8dc747bccd76c00eade2a4240878393cf4fc4ed Mon Sep 17 00:00:00 2001 From: plazmoid Date: Tue, 26 Oct 2021 12:03:43 +0500 Subject: [PATCH] some ui progress --- bin/u_panel/Cargo.toml | 2 + bin/u_panel/src/argparse.rs | 28 +++-- bin/u_panel/src/main.rs | 3 + bin/u_panel/src/tui/impls.rs | 63 +++++++++++ bin/u_panel/src/tui/mod.rs | 29 ++++-- bin/u_panel/src/tui/state.rs | 195 ++++++++++++++++++++++++++++++++++- bin/u_panel/src/tui/ui.rs | 23 ++++- 7 files changed, 318 insertions(+), 25 deletions(-) create mode 100644 bin/u_panel/src/tui/impls.rs diff --git a/bin/u_panel/Cargo.toml b/bin/u_panel/Cargo.toml index c357f5a..50395a0 100644 --- a/bin/u_panel/Cargo.toml +++ b/bin/u_panel/Cargo.toml @@ -20,3 +20,5 @@ tui = { version = "0.16", default-features = false, features = ['crossterm'] } crossterm = "0.22.1" anyhow = "1.0.44" strum = { version = "0.22.0", features = ["derive"] } +async-trait = "0.1.51" +once_cell = "1.8.0" diff --git a/bin/u_panel/src/argparse.rs b/bin/u_panel/src/argparse.rs index b3df203..dec2ccb 100644 --- a/bin/u_panel/src/argparse.rs +++ b/bin/u_panel/src/argparse.rs @@ -1,3 +1,4 @@ +use once_cell::sync::Lazy; use std::env; use std::fmt; use structopt::StructOpt; @@ -74,6 +75,11 @@ enum LD { }, } +pub static CLIENT: Lazy = Lazy::new(|| { + let token = env::var("ADMIN_AUTH_TOKEN").expect("access token is not set"); + ClientHandler::new(None).password(token.clone()) +}); + fn parse_uuid(src: &str) -> Result { Uuid::parse_str(src).map_err(|e| e.to_string()) } @@ -100,13 +106,11 @@ pub async fn process_cmd(args: Args) -> UResult<()> { } } - let token = env::var("ADMIN_AUTH_TOKEN").map_err(|_| UError::WrongToken)?; - let cli_handler = ClientHandler::new(None).password(token.clone()); let printer = Printer { json: args.json }; match args.cmd { Cmd::Agents(action) => match action { - LD::List { uid } => printer.print(cli_handler.get_agents(uid).await), - LD::Delete { uid } => printer.print(cli_handler.del(Some(uid)).await), + LD::List { uid } => printer.print(CLIENT.get_agents(uid).await), + LD::Delete { uid } => printer.print(CLIENT.del(Some(uid)).await), }, Cmd::Jobs(action) => match action { JobALD::Add { @@ -118,21 +122,23 @@ pub async fn process_cmd(args: Args) -> UResult<()> { .with_shell(cmd.join(" ")) .with_alias(alias) .build()?; - printer.print(cli_handler.upload_jobs(&[job]).await); + printer.print(CLIENT.upload_jobs(&[job]).await); } - JobALD::LD(LD::List { uid }) => printer.print(cli_handler.get_jobs(uid).await), - JobALD::LD(LD::Delete { uid }) => printer.print(cli_handler.del(Some(uid)).await), + JobALD::LD(LD::List { uid }) => printer.print(CLIENT.get_jobs(uid).await), + JobALD::LD(LD::Delete { uid }) => printer.print(CLIENT.del(Some(uid)).await), }, Cmd::Jobmap(action) => match action { JobMapALD::Add { agent_uid, job_idents, - } => printer.print(cli_handler.set_jobs(Some(agent_uid), &job_idents).await), - JobMapALD::List { uid } => printer.print(cli_handler.get_agent_jobs(uid).await), - JobMapALD::Delete { uid } => printer.print(cli_handler.del(Some(uid)).await), + } => printer.print(CLIENT.set_jobs(Some(agent_uid), &job_idents).await), + JobMapALD::List { uid } => printer.print(CLIENT.get_agent_jobs(uid).await), + JobMapALD::Delete { uid } => printer.print(CLIENT.del(Some(uid)).await), }, //Cmd::Server => be::serve().unwrap(), - Cmd::TUI => crate::tui::init_tui(token).map_err(|e| UError::TUIError(e.to_string()))?, + Cmd::TUI => crate::tui::init_tui() + .await + .map_err(|e| UError::TUIError(e.to_string()))?, } Ok(()) } diff --git a/bin/u_panel/src/main.rs b/bin/u_panel/src/main.rs index 65fe81d..b48a2c1 100644 --- a/bin/u_panel/src/main.rs +++ b/bin/u_panel/src/main.rs @@ -1,6 +1,9 @@ mod argparse; mod tui; +#[macro_use] +extern crate async_trait; + use argparse::{process_cmd, Args}; use std::process; use structopt::StructOpt; diff --git a/bin/u_panel/src/tui/impls.rs b/bin/u_panel/src/tui/impls.rs new file mode 100644 index 0000000..5b660d1 --- /dev/null +++ b/bin/u_panel/src/tui/impls.rs @@ -0,0 +1,63 @@ +use crate::argparse::CLIENT; +use u_lib::models::{Agent, AssignedJob, JobMeta}; +use u_lib::UResult; +use uuid::Uuid; + +pub trait Id { + fn id(&self) -> Uuid; +} + +impl Id for Agent { + fn id(&self) -> Uuid { + self.id + } +} + +impl Id for JobMeta { + fn id(&self) -> Uuid { + self.id + } +} + +impl Id for AssignedJob { + fn id(&self) -> Uuid { + self.id + } +} + +#[async_trait] +pub trait CRUD: Id +where + Self: Sized, +{ + async fn read() -> UResult>; + + async fn delete(uid: Uuid) -> UResult { + CLIENT.del(Some(uid)).await + } + //TODO: other crud +} + +#[async_trait] +impl CRUD for Agent { + async fn read() -> UResult> { + CLIENT.get_agents(None).await.map(|r| r.into_builtin_vec()) + } +} + +#[async_trait] +impl CRUD for AssignedJob { + async fn read() -> UResult> { + CLIENT + .get_agent_jobs(None) + .await + .map(|r| r.into_builtin_vec()) + } +} + +#[async_trait] +impl CRUD for JobMeta { + async fn read() -> UResult> { + CLIENT.get_jobs(None).await.map(|r| r.into_builtin_vec()) + } +} diff --git a/bin/u_panel/src/tui/mod.rs b/bin/u_panel/src/tui/mod.rs index 2cd7c6b..5742407 100644 --- a/bin/u_panel/src/tui/mod.rs +++ b/bin/u_panel/src/tui/mod.rs @@ -1,3 +1,4 @@ +mod impls; mod state; mod ui; @@ -20,22 +21,27 @@ use tui::{backend::CrosstermBackend, Terminal}; type Frame<'f> = tui::Frame<'f, CrosstermBackend>; -pub fn init_tui(token: String) -> Result<()> { +enum InputEvent { + Key(I), + Tick, +} + +pub async fn init_tui() -> Result<()> { //TODO: fix this set_hook(Box::new(|p| { teardown().unwrap(); eprintln!("{}", p); exit(254); })); - let mut state = State::new(token); - if let Err(e) = init(&mut state) { + let mut state = State::default(); + if let Err(e) = init(&mut state).await { teardown()?; return Err(e); } Ok(()) } -fn init(state: &mut State) -> Result<()> { +async fn init(state: &mut State) -> Result<()> { let mut stdout = stdout(); enable_raw_mode()?; execute!(&mut stdout, EnterAlternateScreen, EnableMouseCapture)?; @@ -47,18 +53,21 @@ fn init(state: &mut State) -> Result<()> { thread::spawn(move || loop { if event::poll(Duration::from_millis(10)).unwrap() { match event::read().unwrap() { - key @ Event::Key(_) => tx.send(key).unwrap(), + key @ Event::Key(_) => tx.send(InputEvent::Key(key)).unwrap(), _ => (), } + } else { + tx.send(InputEvent::Tick).unwrap() } }); terminal.clear()?; loop { + state.check_updates().await; terminal.draw(|f| ui::draw(f, state))?; match rx.recv()? { - Event::Key(key) => match key.code { + InputEvent::Key(Event::Key(key)) => match key.code { KeyCode::Esc => { teardown()?; terminal.show_cursor()?; @@ -66,8 +75,16 @@ fn init(state: &mut State) -> Result<()> { } KeyCode::Left => state.prev_tab(), KeyCode::Right => state.next_tab(), + KeyCode::Up => state.on_up(), + KeyCode::Down => state.on_down(), + KeyCode::Delete => { + state.delete().await; + state.update_tab(); + } + KeyCode::F(5) => state.update_tab(), _ => (), }, + InputEvent::Tick => (), _ => unreachable!(), } } diff --git a/bin/u_panel/src/tui/state.rs b/bin/u_panel/src/tui/state.rs index d72fbb4..57b1109 100644 --- a/bin/u_panel/src/tui/state.rs +++ b/bin/u_panel/src/tui/state.rs @@ -1,5 +1,11 @@ -use std::str::FromStr; +use super::impls::CRUD; +use anyhow::Result as AResult; +use std::{fmt::Display, str::FromStr}; use strum::VariantNames; +use tokio::join; +use tui::widgets::ListState; +use u_lib::models::{Agent, AssignedJob, JobMeta}; +use uuid::Uuid; #[derive(strum::Display, strum::EnumVariantNames, strum::EnumString)] pub enum UiTabs { @@ -30,19 +36,73 @@ impl UiTabs { } } +pub struct StatefulList { + pub updated: bool, + pub inner: Vec, + pub state: ListState, +} + +impl StatefulList { + pub async fn update(&mut self) -> AResult<()> { + if !self.updated { + let new_values = ::read().await?; + self.inner = new_values; + self.updated = true; + } + Ok(()) + } + + pub async fn delete(&mut self) -> AResult<()> { + if let Some(s) = self.state.selected() { + let uid = self.inner[s].id(); + ::delete(uid).await?; + } + Ok(()) + } + + pub fn get(&self, id: Uuid) -> Option<&T> { + for item in self.inner.iter() { + if item.id() == id { + return Some(item); + } + } + None + } +} + +impl Default for StatefulList { + fn default() -> Self { + let mut state = ListState::default(); + state.select(Some(0)); + StatefulList { + updated: false, + inner: vec![], + state, + } + } +} + pub struct State { - pub token: String, pub active_tab: UiTabs, + pub last_error: Option, + pub agents: StatefulList, + pub jobs: StatefulList, + pub map: StatefulList, } -impl State { - pub fn new(token: String) -> Self { +impl Default for State { + fn default() -> Self { State { - token, active_tab: UiTabs::Agents, + last_error: None, + agents: Default::default(), + jobs: Default::default(), + map: Default::default(), } } +} +impl State { pub fn next_tab(&mut self) { self.active_tab = self.active_tab.next() } @@ -50,4 +110,129 @@ impl State { pub fn prev_tab(&mut self) { self.active_tab = self.active_tab.prev() } + + fn check_err(&mut self, res: AResult<()>) -> bool { + if let Err(e) = res { + self.last_error = Some(e.to_string()); + true + } else { + false + } + } + + pub async fn check_updates(&mut self) { + if !self.agents.updated || !self.jobs.updated || !self.map.updated { + let state = self.tab_list_state(); + if let None = state.selected() { + state.select(Some(0)) + } + + let (a, j, m) = join! { + self.agents.update(), + self.jobs.update(), + self.map.update() + }; + for res in [a, j, m] { + self.check_err(res); + } + } + } + + pub fn tab_data(&self) -> Vec { + match self.active_tab { + UiTabs::Agents => self + .agents + .inner + .iter() + .map(|i| format!("{}: {}-{}", crop(i.id, 6), i.username, i.hostname)) + .collect(), + UiTabs::Jobs => self + .jobs + .inner + .iter() + .map(|i| format!("{}: {}", crop(i.id, 6), i.alias.clone().unwrap_or_default())) + .collect(), + UiTabs::Map => self + .map + .inner + .iter() + .map(|i| { + let job = self.jobs.get(i.job_id).unwrap(); + let job_id = crop(i.job_id, 6); + let job_ident = if let Some(alias) = job.alias.as_ref() { + format!("{} ({})", alias, job_id) + } else { + format!("{}", job_id) + }; + let agent = self.agents.get(i.agent_id).unwrap(); + let agent_id = crop(i.agent_id, 6); + let agent_ident = if let Some(alias) = agent.alias.as_ref() { + format!("{} ({})", alias, agent_id) + } else { + format!("{}-{} ({})", agent.username, agent.hostname, agent_id) + }; + format!("{}: {} for {}", crop(i.id, 6), job_ident, agent_ident) + }) + .collect(), + } + } + + pub fn tab_list_state(&mut self) -> &mut ListState { + match self.active_tab { + UiTabs::Agents => &mut self.agents.state, + UiTabs::Jobs => &mut self.jobs.state, + UiTabs::Map => &mut self.map.state, + } + } + + pub fn update_tab(&mut self) { + match self.active_tab { + UiTabs::Agents => self.agents.updated = false, + UiTabs::Jobs => self.jobs.updated = false, + UiTabs::Map => self.map.updated = false, + } + } + + pub fn on_down(&mut self) { + let (list_len, list_state) = match self.active_tab { + UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), + UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), + UiTabs::Map => (self.map.inner.len(), &mut self.map.state), + }; + if list_len == 0 { + list_state.select(None); + } else { + let selected = list_state.selected().unwrap_or(0); + list_state.select(Some((selected + 1) % list_len)); + } + } + + pub fn on_up(&mut self) { + let (list_len, list_state) = match self.active_tab { + UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), + UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), + UiTabs::Map => (self.map.inner.len(), &mut self.map.state), + }; + if list_len == 0 { + list_state.select(None); + } else { + let selected = list_state.selected().unwrap_or(1); + list_state.select(Some((selected + list_len - 1) % list_len)); + } + } + + pub async fn delete(&mut self) { + let res = match self.active_tab { + UiTabs::Agents => self.agents.delete().await, + UiTabs::Jobs => self.jobs.delete().await, + UiTabs::Map => self.map.delete().await, + }; + if !self.check_err(res) { + self.on_up(); + } + } +} + +fn crop(data: T, retain: usize) -> String { + data.to_string()[..retain].to_string() } diff --git a/bin/u_panel/src/tui/ui.rs b/bin/u_panel/src/tui/ui.rs index 0341afe..8a97e65 100644 --- a/bin/u_panel/src/tui/ui.rs +++ b/bin/u_panel/src/tui/ui.rs @@ -2,11 +2,18 @@ use super::{ state::{State, UiTabs}, Frame, }; -use tui::style::{Color, Style}; +use tui::layout::{Constraint, Direction, Layout}; +use tui::style::{Color, Modifier, Style}; use tui::text::Spans; -use tui::widgets::{Block, Borders, Tabs}; +use tui::widgets::{Block, Borders, List, ListItem, Tabs}; pub fn draw(f: &mut Frame, s: &mut State) { + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(size); let titles = UiTabs::variants() .iter() .cloned() @@ -22,5 +29,15 @@ pub fn draw(f: &mut Frame, s: &mut State) { .highlight_style(Style::default().fg(Color::Yellow)) .divider("-") .select(s.active_tab.index()); - f.render_widget(tabs, f.size()) + f.render_widget(tabs, chunks[0]); + + let tab_data = s + .tab_data() + .into_iter() + .map(ListItem::new) + .collect::>(); + let list = List::new(tab_data) + .block(Block::default().borders(Borders::ALL)) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)); + f.render_stateful_widget(list, chunks[1], s.tab_list_state()); }