nsg_cli/commands/
list.rs

1use anyhow::Result;
2use clap::Args;
3use colored::Colorize;
4use crate::client::NsgClient;
5use crate::config::Credentials;
6
7#[derive(Debug, Args)]
8pub struct ListCommand {
9    #[arg(long, help = "Fetch detailed status for each job (slower)")]
10    detailed: bool,
11}
12
13impl ListCommand {
14    pub fn execute(self) -> Result<()> {
15        let credentials = Credentials::load()?;
16        let client = NsgClient::new(credentials.clone())?;
17
18        println!("{}", "NSG Job List".bold().cyan());
19        println!("{}", "=".repeat(80).cyan());
20        println!();
21        println!("{} Fetching jobs for user: {}", "→".cyan(), credentials.username.bold());
22        println!();
23
24        let jobs = client.list_jobs()?;
25
26        if jobs.is_empty() {
27            println!("{}", "No jobs found".yellow());
28            println!();
29            println!("You can submit a test job with:");
30            println!("  {}", "nsg submit <zip_file> --tool PY_EXPANSE".cyan());
31            return Ok(());
32        }
33
34        println!("Found {} job(s)", jobs.len().to_string().bold());
35        println!();
36        println!("{}", "=".repeat(80));
37
38        for (i, job) in jobs.iter().enumerate() {
39            println!();
40            println!("Job #{}", (i + 1).to_string().bold());
41            println!("  ID:  {}", job.job_id.cyan());
42
43            if self.detailed {
44                println!("  {}", "Fetching details...".dimmed());
45                match client.get_job_status(&job.url) {
46                    Ok(status) => {
47                        let stage_icon = get_stage_icon(&status.job_stage);
48                        println!("  Status: {} {}", stage_icon, status.job_stage.bold());
49
50                        if status.failed {
51                            println!("  Failed: {} YES", "✗".red().bold());
52                        }
53
54                        if let Some(date) = &status.date_submitted {
55                            println!("  Submitted: {}", format_timestamp(date));
56                        }
57
58                        if !status.messages.is_empty() {
59                            if let Some(latest) = status.messages.last() {
60                                println!("  Latest: [{}] {}", latest.stage, truncate(&latest.text, 100));
61                            }
62                        }
63                    }
64                    Err(_) => {
65                        println!("  Status: {} (failed to fetch)", "?".yellow());
66                    }
67                }
68            } else {
69                println!("  Status: {} (use --detailed for full status)", "?".dimmed());
70            }
71
72            println!("  URL: {}", job.url.dimmed());
73            println!("{}", "=".repeat(80));
74        }
75
76        println!();
77        println!("{}", "Commands:".bold());
78        println!("  Check job status:    {}", "nsg status <JOB_ID>".cyan());
79        println!("  Download results:    {}", "nsg download <JOB_ID>".cyan());
80        println!();
81
82        Ok(())
83    }
84}
85
86fn get_stage_icon(stage: &str) -> String {
87    match stage {
88        "COMPLETED" => "✓".green().bold().to_string(),
89        "RUNNING" | "RUN" => "⟳".yellow().bold().to_string(),
90        "QUEUE" | "SUBMITTED" => "⏳".cyan().to_string(),
91        "FAILED" => "✗".red().bold().to_string(),
92        _ => "?".dimmed().to_string(),
93    }
94}
95
96fn format_timestamp(ts: &str) -> String {
97    use chrono::{DateTime, Utc};
98    if let Ok(dt) = ts.parse::<DateTime<Utc>>() {
99        dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
100    } else {
101        ts.to_string()
102    }
103}
104
105fn truncate(s: &str, max_len: usize) -> String {
106    if s.len() <= max_len {
107        s.to_string()
108    } else {
109        format!("{}...", &s[..max_len])
110    }
111}