Skip to main content

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 = "Show job messages for each job")]
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            // Display tool name - always show this field
84            match &job.tool {
85                Some(tool) => println!("  Tool:      {}", tool.yellow()),
86                None => println!("  Tool:      {}", "N/A".dimmed()),
87            }
88
89            // Display job stage - always show this field
90            match &job.job_stage {
91                Some(stage) => {
92                    let stage_icon = get_stage_icon(stage);
93                    println!("  Status:    {} {}", stage_icon, stage.bold());
94                }
95                None => println!("  Status:    {}", "Unknown".dimmed()),
96            }
97
98            // Display failed status - always show this field
99            if job.failed {
100                println!("  Failed:    {} YES", "✗".red().bold());
101            } else {
102                println!("  Failed:    {} No", "✓".green());
103            }
104
105            // Display submission date - always show this field
106            match &job.date_submitted {
107                Some(date) => println!("  Submitted: {}", format_timestamp(date)),
108                None => println!("  Submitted: {}", "N/A".dimmed()),
109            }
110
111            // Display completion date - always show this field
112            match &job.date_completed {
113                Some(date) => println!("  Completed: {}", format_timestamp(date).green()),
114                None => println!("  Completed: {}", "Not completed".dimmed()),
115            }
116
117            // The --detailed flag now shows job messages
118            if self.detailed {
119                match client.get_job_status(&job.url) {
120                    Ok(status) => {
121                        if !status.messages.is_empty() {
122                            println!("  Messages:");
123                            for msg in &status.messages {
124                                println!("    [{}] {}", msg.stage.cyan(), truncate(&msg.text, 80));
125                            }
126                        }
127                    }
128                    Err(_) => {
129                        println!("  Messages:  {} (failed to fetch)", "?".yellow());
130                    }
131                }
132            }
133
134            println!("  URL:       {}", job.url.dimmed());
135            println!("{}", "=".repeat(80));
136        }
137
138        println!();
139        println!("{}", "Commands:".bold());
140        println!("  Check job status:    {}", "nsg status <JOB_ID>".cyan());
141        println!("  Download results:    {}", "nsg download <JOB_ID>".cyan());
142
143        if showing_jobs < total_jobs {
144            println!();
145            println!("{}", "Tip:".bold());
146            println!("  Use {} to see all {} jobs", "--all".cyan(), total_jobs);
147            println!("  Use {} to see job messages", "--detailed".cyan());
148            println!("  Use {} to limit results", "--limit N".cyan());
149            println!("  Use {} to show N most recent jobs", "--recent N".cyan());
150        }
151        println!();
152
153        Ok(())
154    }
155}
156
157fn get_stage_icon(stage: &str) -> String {
158    match stage {
159        "COMPLETED" => "✓".green().bold().to_string(),
160        "RUNNING" | "RUN" => "⟳".yellow().bold().to_string(),
161        "QUEUE" | "SUBMITTED" => "⏳".cyan().to_string(),
162        "FAILED" => "✗".red().bold().to_string(),
163        _ => "?".dimmed().to_string(),
164    }
165}
166
167fn format_timestamp(ts: &str) -> String {
168    use chrono::{DateTime, Utc};
169    if let Ok(dt) = ts.parse::<DateTime<Utc>>() {
170        dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
171    } else {
172        ts.to_string()
173    }
174}
175
176fn truncate(s: &str, max_len: usize) -> String {
177    if s.len() <= max_len {
178        s.to_string()
179    } else {
180        format!("{}...", &s[..max_len])
181    }
182}