nsg_cli/commands/
list.rs

1use crate::client::NsgClient;
2use crate::config::Credentials;
3use anyhow::Result;
4use clap::Args;
5use colored::Colorize;
6
7#[derive(Debug, Args)]
8pub struct ListCommand {
9    #[arg(long, help = "Fetch detailed status for each job (slower)")]
10    detailed: bool,
11
12    #[arg(short, long, help = "Limit number of jobs to display")]
13    limit: Option<usize>,
14
15    #[arg(
16        long,
17        default_value = "20",
18        help = "Show only the N most recent jobs (default: 20, use --recent 0 to show all)"
19    )]
20    recent: usize,
21
22    #[arg(long, help = "Show all jobs (override default limit)")]
23    all: bool,
24}
25
26impl ListCommand {
27    pub fn execute(self) -> Result<()> {
28        let credentials = Credentials::load()?;
29        let client = NsgClient::new(credentials.clone())?;
30
31        println!("{}", "NSG Job List".bold().cyan());
32        println!("{}", "=".repeat(80).cyan());
33        println!();
34        println!(
35            "{} Fetching jobs for user: {}",
36            "→".cyan(),
37            credentials.username.bold()
38        );
39        println!();
40
41        let mut jobs = client.list_jobs()?;
42
43        if jobs.is_empty() {
44            println!("{}", "No jobs found".yellow());
45            println!();
46            println!("You can submit a test job with:");
47            println!("  {}", "nsg submit <zip_file> --tool PY_EXPANSE".cyan());
48            return Ok(());
49        }
50
51        let total_jobs = jobs.len();
52
53        // Apply limit/recent filters
54        if self.all {
55            // Show all jobs, no filtering
56        } else if let Some(limit) = self.limit {
57            // Explicit limit takes precedence
58            jobs.truncate(limit);
59        } else if self.recent > 0 && jobs.len() > self.recent {
60            // Default: show N most recent jobs
61            jobs.drain(0..jobs.len() - self.recent);
62        }
63
64        let showing_jobs = jobs.len();
65
66        if showing_jobs < total_jobs {
67            println!(
68                "Found {} job(s) total, showing {}",
69                total_jobs.to_string().bold(),
70                showing_jobs.to_string().bold()
71            );
72        } else {
73            println!("Found {} job(s)", jobs.len().to_string().bold());
74        }
75        println!();
76        println!("{}", "=".repeat(80));
77
78        for (i, job) in jobs.iter().enumerate() {
79            println!();
80            println!("Job #{}", (i + 1).to_string().bold());
81            println!("  ID:  {}", job.job_id.cyan());
82
83            if self.detailed {
84                println!("  {}", "Fetching details...".dimmed());
85                match client.get_job_status(&job.url) {
86                    Ok(status) => {
87                        let stage_icon = get_stage_icon(&status.job_stage);
88                        println!("  Status: {} {}", stage_icon, status.job_stage.bold());
89
90                        if status.failed {
91                            println!("  Failed: {} YES", "✗".red().bold());
92                        }
93
94                        if let Some(date) = &status.date_submitted {
95                            println!("  Submitted: {}", format_timestamp(date));
96                        }
97
98                        if !status.messages.is_empty() {
99                            if let Some(latest) = status.messages.last() {
100                                println!(
101                                    "  Latest: [{}] {}",
102                                    latest.stage,
103                                    truncate(&latest.text, 100)
104                                );
105                            }
106                        }
107                    }
108                    Err(_) => {
109                        println!("  Status: {} (failed to fetch)", "?".yellow());
110                    }
111                }
112            } else {
113                println!(
114                    "  Status: {} (use --detailed for full status)",
115                    "?".dimmed()
116                );
117            }
118
119            println!("  URL: {}", job.url.dimmed());
120            println!("{}", "=".repeat(80));
121        }
122
123        println!();
124        println!("{}", "Commands:".bold());
125        println!("  Check job status:    {}", "nsg status <JOB_ID>".cyan());
126        println!("  Download results:    {}", "nsg download <JOB_ID>".cyan());
127
128        if showing_jobs < total_jobs {
129            println!();
130            println!("{}", "Tip:".bold());
131            println!("  Use {} to see all {} jobs", "--all".cyan(), total_jobs);
132            println!("  Use {} to see detailed status", "--detailed".cyan());
133            println!("  Use {} to limit results", "--limit N".cyan());
134            println!("  Use {} to show N most recent jobs", "--recent N".cyan());
135        }
136        println!();
137
138        Ok(())
139    }
140}
141
142fn get_stage_icon(stage: &str) -> String {
143    match stage {
144        "COMPLETED" => "✓".green().bold().to_string(),
145        "RUNNING" | "RUN" => "⟳".yellow().bold().to_string(),
146        "QUEUE" | "SUBMITTED" => "⏳".cyan().to_string(),
147        "FAILED" => "✗".red().bold().to_string(),
148        _ => "?".dimmed().to_string(),
149    }
150}
151
152fn format_timestamp(ts: &str) -> String {
153    use chrono::{DateTime, Utc};
154    if let Ok(dt) = ts.parse::<DateTime<Utc>>() {
155        dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
156    } else {
157        ts.to_string()
158    }
159}
160
161fn truncate(s: &str, max_len: usize) -> String {
162    if s.len() <= max_len {
163        s.to_string()
164    } else {
165        format!("{}...", &s[..max_len])
166    }
167}