Skip to content

Commit 0b48057

Browse files
authored
minor fixes (#33)
* allow collapsing parent node if current node collapsed * allow target directory for download * allow target directory for upload * support getting stderr
1 parent 32d7d94 commit 0b48057

11 files changed

Lines changed: 123 additions & 40 deletions

File tree

coman/src/app/messages.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ pub enum CscsMsg {
4949
}
5050
#[derive(Debug, PartialEq)]
5151
pub enum JobMsg {
52-
ShowLog(usize),
53-
CloseLog,
52+
Show(usize),
53+
Switch,
54+
Close,
5455
}
5556
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord, strum::Display)]
5657
pub enum View {

coman/src/app/model.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::{
2626
},
2727
cscs::{
2828
handlers::{cscs_login, cscs_system_set},
29-
ports::TreeAction,
29+
ports::{JobLogAction, TreeAction},
3030
},
3131
trace_dbg,
3232
util::ui::{draw_area_in_absolute, draw_area_in_absolute_fixed_height},
@@ -56,7 +56,7 @@ where
5656

5757
/// Triggers watching job logs
5858
/// sending None stops watching
59-
pub job_log_tx: mpsc::Sender<Option<usize>>,
59+
pub job_log_tx: mpsc::Sender<JobLogAction>,
6060

6161
/// Allows creating user events based on messages
6262
pub user_event_tx: mpsc::Sender<UserEvent>,
@@ -74,7 +74,7 @@ where
7474
bridge: TerminalBridge<T>,
7575
error_tx: mpsc::Sender<String>,
7676
select_system_tx: mpsc::Sender<()>,
77-
job_log_tx: mpsc::Sender<Option<usize>>,
77+
job_log_tx: mpsc::Sender<JobLogAction>,
7878
user_event_tx: mpsc::Sender<UserEvent>,
7979
file_tree_tx: mpsc::Sender<TreeAction>,
8080
) -> Self {
@@ -305,7 +305,7 @@ where
305305
}
306306
fn handle_job_msg(&mut self, msg: JobMsg) -> Option<Msg> {
307307
match msg {
308-
JobMsg::ShowLog(jobid) => {
308+
JobMsg::Show(jobid) => {
309309
if self.app.mounted(&Id::WorkloadList) {
310310
assert!(self.app.umount(&Id::WorkloadList).is_ok());
311311
}
@@ -319,11 +319,18 @@ where
319319
assert!(self.app.active(&Id::WorkloadLogs).is_ok());
320320
let job_log_tx = self.job_log_tx.clone();
321321
tokio::spawn(async move {
322-
job_log_tx.send(Some(jobid)).await.unwrap();
322+
job_log_tx.send(JobLogAction::Job(jobid)).await.unwrap();
323323
});
324324
None
325325
}
326-
JobMsg::CloseLog => {
326+
JobMsg::Switch => {
327+
let job_log_tx = self.job_log_tx.clone();
328+
tokio::spawn(async move {
329+
job_log_tx.send(JobLogAction::SwitchLog).await.unwrap();
330+
});
331+
None
332+
}
333+
JobMsg::Close => {
327334
if self.app.mounted(&Id::WorkloadLogs) {
328335
assert!(self.app.umount(&Id::WorkloadLogs).is_ok());
329336
}
@@ -338,7 +345,7 @@ where
338345
let job_log_tx = self.job_log_tx.clone();
339346
tokio::spawn(async move {
340347
// stopp polling for logs
341-
job_log_tx.send(None).await.unwrap();
348+
job_log_tx.send(JobLogAction::Stop).await.unwrap();
342349
});
343350
None
344351
}

coman/src/cli.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ pub enum CscsJobCommands {
6363
#[clap(alias("g"), about = "Get metadata for a specific job [aliases: g]")]
6464
Get { job_id: i64 },
6565
#[clap(about = "Get the stdout of a job")]
66-
Log { job_id: i64 },
66+
Log {
67+
#[clap(short, long, action, help = "whether to get stderr instead of stdout")]
68+
stderr: bool,
69+
job_id: i64,
70+
},
6771

6872
#[clap(alias("s"), about = "Submit a new compute job [aliases: s]")]
6973
Submit {

coman/src/components/file_tree.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::{iter, path::PathBuf};
22

33
use tokio::sync::mpsc;
4-
use tui_realm_treeview::{Node, NodeValue, TREE_CMD_CLOSE, TREE_CMD_OPEN, Tree, TreeView};
4+
use tui_realm_treeview::{Node, NodeValue, TREE_CMD_CLOSE, TREE_CMD_OPEN, TREE_INITIAL_NODE, Tree, TreeView};
55
use tuirealm::{
6-
Component, Event, MockComponent, State, StateValue,
6+
AttrValue, Attribute, Component, Event, MockComponent, State, StateValue,
77
command::{Cmd, CmdResult, Direction, Position},
88
event::{Key, KeyEvent, KeyModifiers},
99
props::{Alignment, BorderType, Borders, Color, Style},
@@ -84,7 +84,20 @@ impl Component<Msg, UserEvent> for FileTree {
8484
Event::Keyboard(KeyEvent {
8585
code: Key::Left,
8686
modifiers: KeyModifiers::NONE,
87-
}) => self.perform(Cmd::Custom(TREE_CMD_CLOSE)),
87+
}) => {
88+
let current_id = self.state().unwrap_one().unwrap_string();
89+
let node = self.component.tree().root().query(&current_id).unwrap();
90+
if self.component.tree_state().is_closed(node) {
91+
// current node is already closed, so we select and close the parent
92+
if let Some(parent) = self.component.tree().root().parent(node.id()) {
93+
self.attr(
94+
Attribute::Custom(TREE_INITIAL_NODE),
95+
AttrValue::String(parent.id().clone()),
96+
);
97+
}
98+
}
99+
self.perform(Cmd::Custom(TREE_CMD_CLOSE))
100+
}
88101
Event::Keyboard(KeyEvent {
89102
code: Key::Right,
90103
modifiers: KeyModifiers::NONE,

coman/src/components/toolbar.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
},
99
trace_dbg,
1010
};
11-
const WORKLOAD_TOOLTIP: &str = "q: quit, Esc: close/back, l: logs, f: File view, x: menu, ?: help";
11+
const WORKLOAD_TOOLTIP: &str = "q: quit, Esc: close/back, l: logs, f: File view, x: menu, tab: switch view, ?: help";
1212
const FILETREE_TOOLTIP: &str = "q: quit, ↑↓: navigate,←→: collapse/expand, x: menu, ?: help";
1313

1414
#[derive(MockComponent)]

coman/src/components/workload_list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ impl Component<Msg, UserEvent> for WorkloadList {
7777
&& !self.jobs.is_empty()
7878
{
7979
let job = self.jobs[index].clone();
80-
return Some(Msg::Job(JobMsg::ShowLog(job.id)));
80+
return Some(Msg::Job(JobMsg::Show(job.id)));
8181
}
8282
CmdResult::None
8383
}

coman/src/components/workload_log.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,32 @@ use tuirealm::{
66
props::{Alignment, BorderType, Borders, Color, PropPayload, PropValue, TextSpan},
77
};
88

9-
use crate::{
10-
app::{
11-
messages::{JobMsg, Msg},
12-
user_events::{CscsEvent, UserEvent},
13-
},
14-
trace_dbg,
9+
use crate::app::{
10+
messages::{JobMsg, Msg},
11+
user_events::{CscsEvent, UserEvent},
1512
};
1613

1714
#[derive(MockComponent)]
1815
pub struct WorkloadLog {
1916
component: Textarea,
17+
stderr: bool,
2018
}
2119

2220
impl WorkloadLog {
2321
pub fn new() -> Self {
2422
Self {
2523
component: Textarea::default()
2624
.borders(Borders::default().modifiers(BorderType::Rounded).color(Color::Yellow))
27-
.title("Workload Log", Alignment::Center)
25+
.title("Workload Log (stdout)", Alignment::Center)
2826
.step(4),
27+
stderr: false,
2928
}
3029
}
3130
}
3231
impl Component<Msg, UserEvent> for WorkloadLog {
3332
fn on(&mut self, ev: tuirealm::Event<UserEvent>) -> Option<Msg> {
3433
let _ = match ev {
3534
Event::User(UserEvent::Cscs(CscsEvent::GotJobLog(log))) => {
36-
let _ = trace_dbg!("got log component");
37-
let log = trace_dbg!(log);
3835
self.attr(
3936
Attribute::Text,
4037
AttrValue::Payload(PropPayload::Vec(
@@ -51,8 +48,25 @@ impl Component<Msg, UserEvent> for WorkloadLog {
5148
Event::Keyboard(KeyEvent { code: Key::PageUp, .. }) => self.perform(Cmd::Scroll(Direction::Up)),
5249
Event::Keyboard(KeyEvent { code: Key::Home, .. }) => self.perform(Cmd::GoTo(Position::Begin)),
5350
Event::Keyboard(KeyEvent { code: Key::End, .. }) => self.perform(Cmd::GoTo(Position::End)),
51+
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
52+
self.stderr = !self.stderr;
53+
if self.stderr {
54+
self.attr(
55+
Attribute::Title,
56+
AttrValue::Title(("Workload Log (stderr)".to_owned(), Alignment::Center)),
57+
);
58+
} else {
59+
self.attr(
60+
Attribute::Title,
61+
AttrValue::Title(("Workload Log (stdout)".to_owned(), Alignment::Center)),
62+
);
63+
}
64+
// empty log view
65+
self.attr(Attribute::Text, AttrValue::Payload(PropPayload::Vec(vec![])));
66+
return Some(Msg::Job(JobMsg::Switch));
67+
}
5468
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
55-
return Some(Msg::Job(JobMsg::CloseLog));
69+
return Some(Msg::Job(JobMsg::Close));
5670
}
5771
_ => CmdResult::None,
5872
};

coman/src/cscs/cli.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ pub(crate) async fn cli_cscs_job_detail(
8787

8888
pub(crate) async fn cli_cscs_job_log(
8989
job_id: i64,
90+
stderr: bool,
9091
system: Option<String>,
9192
platform: Option<ComputePlatform>,
9293
) -> Result<()> {
93-
match cscs_job_log(job_id, system, platform).await {
94+
match cscs_job_log(job_id, stderr, system, platform).await {
9495
Ok(content) => {
9596
println!("{}", content);
9697
Ok(())
@@ -171,6 +172,11 @@ pub(crate) async fn cli_cscs_file_download(
171172
system: Option<String>,
172173
platform: Option<ComputePlatform>,
173174
) -> Result<()> {
175+
let local = if local.is_dir() {
176+
local.join(remote.file_name().ok_or(eyre!("couldn't get name of remote file"))?)
177+
} else {
178+
local
179+
};
174180
match cscs_file_download(remote, local.clone(), account, system.clone(), platform.clone()).await {
175181
Ok(None) => {
176182
println!("File successfully downloaded");

coman/src/cscs/handlers.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use reqwest::Url;
1010
use crate::{
1111
config::{ComputePlatform, Config},
1212
cscs::{
13-
api_client::{CscsApi, FileStat, FileSystemType, Job, JobDetail, PathEntry, S3Upload, System, UserInfo},
13+
api_client::{
14+
CscsApi, FileStat, FileSystemType, Job, JobDetail, PathEntry, PathType, S3Upload, System, UserInfo,
15+
},
1416
oauth2::{
1517
CLIENT_ID_SECRET_NAME, CLIENT_SECRET_SECRET_NAME, client_credentials_login, finish_cscs_device_login,
1618
start_cscs_device_login,
@@ -115,7 +117,12 @@ pub async fn cscs_job_details(
115117
}
116118
}
117119

118-
pub async fn cscs_job_log(job_id: i64, system: Option<String>, platform: Option<ComputePlatform>) -> Result<String> {
120+
pub async fn cscs_job_log(
121+
job_id: i64,
122+
stderr: bool,
123+
system: Option<String>,
124+
platform: Option<ComputePlatform>,
125+
) -> Result<String> {
119126
match get_access_token().await {
120127
Ok(access_token) => {
121128
let api_client = CscsApi::new(access_token.0, platform).unwrap();
@@ -125,9 +132,12 @@ pub async fn cscs_job_log(job_id: i64, system: Option<String>, platform: Option<
125132
if job.is_none() {
126133
return Err(eyre!("couldn't find job {}", job_id));
127134
}
128-
api_client
129-
.tail(current_system, PathBuf::from(job.unwrap().stdout), 100)
130-
.await
135+
let path = if stderr {
136+
PathBuf::from(job.unwrap().stderr)
137+
} else {
138+
PathBuf::from(job.unwrap().stdout)
139+
};
140+
api_client.tail(current_system, path, 100).await
131141
}
132142
Err(e) => Err(e),
133143
}
@@ -321,6 +331,16 @@ pub async fn cscs_file_upload(
321331
Ok(access_token) => {
322332
let api_client = CscsApi::new(access_token.0, platform).unwrap();
323333
let config = Config::new().unwrap();
334+
let current_system = &system.unwrap_or(config.cscs.current_system);
335+
let existing = api_client.list_path(current_system, remote.clone()).await?;
336+
let remote = if !existing.is_empty() {
337+
if existing.len() == 1 && existing[0].path_type == PathType::File {
338+
return Err(eyre!("remote file already exists"));
339+
}
340+
remote.join(local.file_name().ok_or(eyre!("couldn't get filename for local file"))?)
341+
} else {
342+
remote
343+
};
324344

325345
let file_meta = std::fs::metadata(local.clone())?;
326346

@@ -333,15 +353,13 @@ pub async fn cscs_file_upload(
333353
if size < CSCS_MAX_DIRECT_SIZE {
334354
// upload directly
335355
let contents = std::fs::read(local)?;
336-
api_client
337-
.upload(&system.unwrap_or(config.cscs.current_system), remote, contents)
338-
.await?;
356+
api_client.upload(current_system, remote, contents).await?;
339357
Ok(None)
340358
} else {
341359
// upload via s3
342360
let account = account.or(config.cscs.account);
343361
let transfer_data = api_client
344-
.transfer_upload(&config.cscs.current_system, account, remote, size as i64)
362+
.transfer_upload(current_system, account, remote, size as i64)
345363
.await?;
346364

347365
Ok(Some(transfer_data))

coman/src/cscs/ports.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,25 @@ impl PollAsync<UserEvent> for AsyncSelectSystemPort {
154154
}
155155
}
156156

157+
pub enum JobLogAction {
158+
Job(usize),
159+
SwitchLog,
160+
Stop,
161+
}
162+
157163
/// This port handles polling the logs of a CSCS job
158164
pub(crate) struct AsyncJobLogPort {
159-
receiver: mpsc::Receiver<Option<usize>>,
165+
receiver: mpsc::Receiver<JobLogAction>,
160166
current_job: Option<usize>,
167+
stderr: bool,
161168
}
162169

163170
impl AsyncJobLogPort {
164-
pub fn new(receiver: mpsc::Receiver<Option<usize>>) -> Self {
171+
pub fn new(receiver: mpsc::Receiver<JobLogAction>) -> Self {
165172
Self {
166173
receiver,
167174
current_job: None,
175+
stderr: false,
168176
}
169177
}
170178
}
@@ -176,11 +184,21 @@ impl PollAsync<UserEvent> for AsyncJobLogPort {
176184
}
177185
if !self.receiver.is_empty() {
178186
if let Some(val) = self.receiver.recv().await {
179-
self.current_job = val;
187+
match val {
188+
JobLogAction::Job(jobid) => {
189+
self.current_job = Some(jobid);
190+
}
191+
JobLogAction::SwitchLog => {
192+
self.stderr = !self.stderr;
193+
}
194+
JobLogAction::Stop => {
195+
self.current_job = None;
196+
}
197+
}
180198
}
181199
Ok(Some(Event::None))
182200
} else if let Some(job_id) = self.current_job {
183-
match cscs_job_log(job_id as i64, None, None).await {
201+
match cscs_job_log(job_id as i64, self.stderr, None, None).await {
184202
Ok(log) => {
185203
let log = trace_dbg!(log);
186204
Ok(Some(Event::User(UserEvent::Cscs(CscsEvent::GotJobLog(log)))))

0 commit comments

Comments
 (0)