Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions coman/src/app/ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ pub enum Id {
LoginPopup,
DownloadPopup,
SystemSelectPopup,
JobFilterPopup,
FileView,
}
11 changes: 10 additions & 1 deletion coman/src/app/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use crate::{
app::user_events::UserEvent,
cscs::api_client::types::{JobDetail, JobId, System},
cscs::api_client::types::{JobDetail, JobId, JobStatus, System},
};

#[derive(Debug, PartialEq)]
Expand All @@ -11,6 +11,7 @@ pub enum MenuMsg {
Opened,
Closed,
CscsLogin,
CscsShowFilterPopup,
CscsSwitchSystem,
Event(UserEvent),
}
Expand Down Expand Up @@ -45,6 +46,13 @@ pub enum SystemSelectMsg {
SystemSelected(String),
}

#[derive(Debug, PartialEq)]
pub enum JobFilterPopupMsg {
Opened,
Closed,
FilterSelected(Vec<JobStatus>),
}

#[derive(Debug, PartialEq)]
pub enum CscsMsg {
Login(String, String),
Expand Down Expand Up @@ -86,6 +94,7 @@ pub enum Msg {
LoginPopup(LoginPopupMsg),
DownloadPopup(DownloadPopupMsg),
SystemSelectPopup(SystemSelectMsg),
JobFilterPopup(JobFilterPopupMsg),
Error(String),
Info(String),
Cscs(CscsMsg),
Expand Down
52 changes: 48 additions & 4 deletions coman/src/app/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ use crate::{
app::{
ids::Id,
messages::{
CscsMsg, DownloadPopupMsg, ErrorPopupMsg, InfoPopupMsg, JobMsg, LoginPopupMsg, MenuMsg, Msg, StatusMsg,
SystemSelectMsg, View,
CscsMsg, DownloadPopupMsg, ErrorPopupMsg, InfoPopupMsg, JobFilterPopupMsg, JobMsg, LoginPopupMsg, MenuMsg,
Msg, StatusMsg, SystemSelectMsg, View,
},
user_events::{CscsEvent, StatusEvent, UserEvent},
},
components::{
context_menu::ContextMenu, download_popup::DownloadTargetInput, error_popup::ErrorPopup, info_popup::InfoPopup,
login_popup::LoginPopup, resource_usage::ResourceUsage, system_select_popup::SystemSelectPopup,
workload_details::WorkloadDetails, workload_list::WorkloadList, workload_log::WorkloadLog,
job_status_filter_popup::JobStatusFilterPopup, login_popup::LoginPopup, resource_usage::ResourceUsage,
system_select_popup::SystemSelectPopup, workload_details::WorkloadDetails, workload_list::WorkloadList,
workload_log::WorkloadLog,
},
config::Config,
cscs::{
api_client::types::JobStatus,
handlers::{cscs_login, cscs_system_set, get_available_compute_platforms},
ports::{BackgroundTask, JobLogAction, JobResourceUsageAction},
},
Expand Down Expand Up @@ -59,6 +61,9 @@ where
/// sending None stops watching
pub job_log_tx: mpsc::Sender<JobLogAction>,

/// Set to filter workload list
pub job_filter_tx: mpsc::Sender<Vec<JobStatus>>,

/// Triggers watching job logs
/// sending None stops watching
pub job_resource_usage_tx: mpsc::Sender<JobResourceUsageAction>,
Expand All @@ -81,6 +86,7 @@ where
error_tx: mpsc::Sender<String>,
select_system_tx: mpsc::Sender<()>,
job_log_tx: mpsc::Sender<JobLogAction>,
job_filter_tx: mpsc::Sender<Vec<JobStatus>>,
job_resource_usage_tx: mpsc::Sender<JobResourceUsageAction>,
user_event_tx: mpsc::Sender<UserEvent>,
background_task_tx: mpsc::Sender<BackgroundTask>,
Expand All @@ -94,6 +100,7 @@ where
error_tx,
select_system_tx,
job_log_tx,
job_filter_tx,
job_resource_usage_tx,
user_event_tx,
background_task_tx,
Expand Down Expand Up @@ -149,6 +156,10 @@ where
let popup = draw_area_in_absolute_fixed_height(f.area(), 10, 3);
f.render_widget(Clear, popup);
app.view(&Id::DownloadPopup, f, popup);
} else if app.mounted(&Id::JobFilterPopup) {
let popup = draw_area_in_absolute_fixed_height(f.area(), 10, 3);
f.render_widget(Clear, popup);
app.view(&Id::JobFilterPopup, f, popup);
}
})
.is_ok()
Expand Down Expand Up @@ -281,6 +292,34 @@ where
}
}
}
fn handle_job_filter_popup_msg(&mut self, msg: JobFilterPopupMsg) -> Option<Msg> {
match msg {
JobFilterPopupMsg::Opened => {
if self.app.mounted(&Id::JobFilterPopup) {
assert!(self.app.umount(&Id::JobFilterPopup).is_ok());
}
assert!(
self.app
.mount(Id::JobFilterPopup, Box::new(JobStatusFilterPopup::new()), vec![])
.is_ok()
);
assert!(self.app.active(&Id::JobFilterPopup).is_ok());
None
}
JobFilterPopupMsg::FilterSelected(filter) => {
assert!(self.app.umount(&Id::JobFilterPopup).is_ok());
let filter_tx = self.job_filter_tx.clone();
tokio::spawn(async move {
filter_tx.send(filter).await.unwrap();
});
None
}
JobFilterPopupMsg::Closed => {
assert!(self.app.umount(&Id::JobFilterPopup).is_ok());
None
}
}
}
fn handle_menu_msg(&mut self, msg: MenuMsg) -> Option<Msg> {
match msg {
MenuMsg::Opened => {
Expand All @@ -304,6 +343,10 @@ where
assert!(self.app.umount(&Id::Menu).is_ok());
Some(Msg::Cscs(CscsMsg::SelectSystem))
}
MenuMsg::CscsShowFilterPopup => {
assert!(self.app.umount(&Id::Menu).is_ok());
Some(Msg::JobFilterPopup(JobFilterPopupMsg::Opened))
}
MenuMsg::Event(event) => {
assert!(self.app.umount(&Id::Menu).is_ok());
Some(Msg::CreateEvent(event))
Expand Down Expand Up @@ -505,6 +548,7 @@ where
Msg::ErrorPopup(popup_msg) => self.handle_error_popup_msg(popup_msg),
Msg::InfoPopup(popup_msg) => self.handle_info_popup_msg(popup_msg),
Msg::DownloadPopup(popup_msg) => self.handle_download_popup_msg(popup_msg),
Msg::JobFilterPopup(popup_msg) => self.handle_job_filter_popup_msg(popup_msg),
Msg::Cscs(CscsMsg::Login(client_id, client_secret)) => {
let event_tx = self.user_event_tx.clone();
let error_tx = self.error_tx.clone();
Expand Down
11 changes: 7 additions & 4 deletions coman/src/cli/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
cscs::{
api_client::{
client::{EdfSpec as EdfSpecEnum, ScriptSpec as ScriptSpecEnum},
types::PathType,
types::{JobStatus, PathType},
},
handlers::{cscs_file_list, cscs_job_list, file_system_roots},
},
Expand Down Expand Up @@ -239,7 +239,10 @@ impl FromStr for JobIdOrName {
#[derive(Subcommand, Debug)]
pub enum CscsJobCommands {
#[clap(alias("ls"), about = "List all jobs [aliases: ls]")]
List,
List {
#[clap(short,long,help="filter by job status (separated by ',', [running, pending, finished, cancelled, failed, timeout, requeued])", value_delimiter=',', num_args=1.., value_enum)]
status: Option<Vec<JobStatus>>,
},
#[clap(alias("g"), about = "Get metadata for a specific job [aliases: g]")]
Get {
#[arg(help="id or name of the job (name uses newest job of that name)", add = ArgValueCompleter::new(job_id_or_name_completer))]
Expand Down Expand Up @@ -330,7 +333,7 @@ fn job_id_or_name_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidat
match jn {
JobIdOrName::Id(id) => {
tokio::spawn(async move {
let jobs = cscs_job_list(None, None).await.unwrap();
let jobs = cscs_job_list(None, None, None).await.unwrap();
let partial_id = id.to_string();
let ids: Vec<_> = jobs
.iter()
Expand All @@ -345,7 +348,7 @@ fn job_id_or_name_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidat
}
JobIdOrName::Name(name) => {
tokio::spawn(async move {
let jobs = cscs_job_list(None, None).await.unwrap();
let jobs = cscs_job_list(None, None, None).await.unwrap();
let names: Vec<_> = jobs
.into_iter()
.map(|j| j.name)
Expand Down
12 changes: 7 additions & 5 deletions coman/src/components/context_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,23 @@ pub struct ContextMenu {
impl ContextMenu {
fn workload_options() -> Table {
TableBuilder::default()
.add_col(TextSpan::from("Cancel Job").fg(Color::Cyan))
.add_row()
.add_col(TextSpan::from("Filter by Status").fg(Color::Cyan))
.add_row()
.add_col(TextSpan::from("Login to CSCS").fg(Color::Cyan))
.add_row()
.add_col(TextSpan::from("Switch System").fg(Color::Cyan))
.add_row()
.add_col(TextSpan::from("Cancel Job").fg(Color::Cyan))
.add_row()
.add_col(TextSpan::from("Quit").fg(Color::Cyan))
.add_row()
.build()
}
fn workload_actions(index: usize) -> Option<Msg> {
match index {
0 => Some(Msg::Menu(MenuMsg::CscsLogin)),
1 => Some(Msg::Menu(MenuMsg::CscsSwitchSystem)),
2 => Some(Msg::Menu(MenuMsg::Event(UserEvent::Job(JobEvent::Cancel)))),
0 => Some(Msg::Menu(MenuMsg::Event(UserEvent::Job(JobEvent::Cancel)))),
1 => Some(Msg::Menu(MenuMsg::CscsShowFilterPopup)),
2 => Some(Msg::Menu(MenuMsg::CscsSwitchSystem)),
3 => Some(Msg::AppClose),
_ => Some(Msg::Menu(MenuMsg::Closed)),
}
Expand Down
102 changes: 102 additions & 0 deletions coman/src/components/job_status_filter_popup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use strum::{VariantArray, VariantNames};
use tui_realm_stdlib::Checkbox;
use tuirealm::{
Attribute, Component, Event, MockComponent, State,
command::{Cmd, CmdResult, Direction},
event::{Key, KeyEvent},
props::{Alignment, BorderType, Borders, Color, PropValue},
};

use crate::{
app::{
messages::{JobFilterPopupMsg, Msg},
user_events::UserEvent,
},
cscs::api_client::types::JobStatus,
};

#[derive(MockComponent)]
pub struct JobStatusFilterPopup {
component: Checkbox,
values: Vec<usize>,
max_idx: usize,
}

impl JobStatusFilterPopup {
pub fn new() -> Self {
let mut variants: Vec<_> = <JobStatus as VariantNames>::VARIANTS
.iter()
.map(|v| v.to_owned())
.collect();
variants.insert(0, "All");
let max_idx = variants.len();
let values: Vec<_> = (0..max_idx).collect();
Self {
component: Checkbox::default()
.borders(Borders::default().modifiers(BorderType::Thick).color(Color::Green))
.title("Select Status to show", Alignment::Left)
.rewind(true)
.choices(variants)
.values(&values),
values,
max_idx,
}
}
}

impl Component<Msg, UserEvent> for JobStatusFilterPopup {
fn on(&mut self, ev: Event<UserEvent>) -> Option<Msg> {
let _ = match ev {
Event::Keyboard(KeyEvent { code: Key::Right, .. }) => self.perform(Cmd::Move(Direction::Right)),
Event::Keyboard(KeyEvent { code: Key::Left, .. }) => self.perform(Cmd::Move(Direction::Left)),
Event::Keyboard(KeyEvent {
code: Key::Char(' '), ..
}) => {
if let CmdResult::Changed(state) = self.perform(Cmd::Toggle)
&& let State::Vec(state) = state
{
let new_state: Vec<_> = state.into_iter().map(|s| s.unwrap_usize()).collect();
if new_state.contains(&0) && !self.values.contains(&0) {
// 'All' selected, activate all
self.values = (0..self.max_idx).collect();
} else if !new_state.contains(&0) && self.values.contains(&0) {
// 'All' deselected, deactivate all
self.values = vec![];
} else {
// check if all values except 'All' are selected
self.values = new_state;
if (1..self.max_idx).all(|s| self.values.contains(&s)) {
self.values.push(0);
} else if self.values.contains(&0) {
self.values.retain(|v| *v != 0);
}
}
self.attr(
Attribute::Value,
tuirealm::AttrValue::Payload(tuirealm::props::PropPayload::Vec(
self.values.iter().map(|v| PropValue::Usize(*v)).collect(),
)),
);
}
CmdResult::None
}
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
return Some(Msg::JobFilterPopup(JobFilterPopupMsg::Closed));
}
Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => {
let selected_statuses = self
.values
.iter()
.filter(|v| **v > 0)
.map(|v| <JobStatus as VariantArray>::VARIANTS[*v - 1].clone())
.collect();

return Some(Msg::JobFilterPopup(JobFilterPopupMsg::FilterSelected(
selected_statuses,
)));
}
_ => CmdResult::None,
};
Some(Msg::None)
}
}
1 change: 1 addition & 0 deletions coman/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub(crate) mod error_popup;
pub(crate) mod file_tree;
pub(crate) mod global_listener;
pub(crate) mod info_popup;
pub(crate) mod job_status_filter_popup;
pub(crate) mod login_popup;
pub(crate) mod resource_usage;
pub(crate) mod status_bar;
Expand Down
Loading