1use anyhow::Result;
7use colored::Colorize;
8use serde::Serialize;
9
10use raps_admin::{BulkOperationResult, ItemResult, OperationStatus, StateManager};
11
12use crate::output::OutputFormat;
13
14use super::OperationCommands;
15
16#[derive(Serialize)]
17struct OperationStatusOutput {
18 operation_id: String,
19 operation_type: String,
20 status: String,
21 total: usize,
22 completed: usize,
23 skipped: usize,
24 failed: usize,
25 created_at: String,
26 updated_at: String,
27}
28
29#[derive(Serialize)]
30struct OperationListOutput {
31 operation_id: String,
32 operation_type: String,
33 status: String,
34 progress: String,
35 updated_at: String,
36}
37
38pub(crate) fn format_status(status: &str) -> String {
39 match status.to_lowercase().as_str() {
40 "completed" => status.green().to_string(),
41 "failed" => status.red().to_string(),
42 "inprogress" | "in_progress" => status.yellow().to_string(),
43 "cancelled" => status.dimmed().to_string(),
44 _ => status.to_string(),
45 }
46}
47
48#[derive(Serialize)]
50struct BulkResultOutput {
51 operation_id: String,
52 total: usize,
53 completed: usize,
54 skipped: usize,
55 failed: usize,
56 duration_secs: f64,
57 details: Vec<BulkResultDetailOutput>,
58}
59
60#[derive(Serialize)]
61struct BulkResultDetailOutput {
62 project_id: String,
63 project_name: Option<String>,
64 status: String,
65 message: Option<String>,
66 attempts: u32,
67}
68
69pub(crate) fn display_bulk_result(
71 result: &BulkOperationResult,
72 output_format: OutputFormat,
73) -> Result<()> {
74 let details: Vec<BulkResultDetailOutput> = result
75 .details
76 .iter()
77 .map(|d| {
78 let (status, message) = match &d.result {
79 ItemResult::Success => ("success".to_string(), None),
80 ItemResult::Skipped { reason } => ("skipped".to_string(), Some(reason.clone())),
81 ItemResult::Failed { error, .. } => ("failed".to_string(), Some(error.clone())),
82 };
83 BulkResultDetailOutput {
84 project_id: d.project_id.clone(),
85 project_name: d.project_name.clone(),
86 status,
87 message,
88 attempts: d.attempts,
89 }
90 })
91 .collect();
92
93 let output = BulkResultOutput {
94 operation_id: result.operation_id.to_string(),
95 total: result.total,
96 completed: result.completed,
97 skipped: result.skipped,
98 failed: result.failed,
99 duration_secs: result.duration.as_secs_f64(),
100 details,
101 };
102
103 match output_format {
104 OutputFormat::Table => {
105 println!("\n{}", "Bulk Operation Results:".bold());
106 println!("{}", "\u{2500}".repeat(60));
107 println!("{:<15} {}", "Operation:".bold(), output.operation_id.cyan());
108 println!("{:<15} {}", "Total:".bold(), output.total);
109 println!(
110 "{:<15} {}",
111 "Completed:".bold(),
112 output.completed.to_string().green()
113 );
114 println!(
115 "{:<15} {}",
116 "Skipped:".bold(),
117 output.skipped.to_string().yellow()
118 );
119 println!(
120 "{:<15} {}",
121 "Failed:".bold(),
122 output.failed.to_string().red()
123 );
124 println!("{:<15} {:.2}s", "Duration:".bold(), output.duration_secs);
125 println!("{}", "\u{2500}".repeat(60));
126
127 if result.failed > 0 {
129 println!("\n{}", "Failed Projects:".red().bold());
130 for detail in &output.details {
131 if detail.status == "failed" {
132 let name = detail.project_name.as_deref().unwrap_or(&detail.project_id);
133 let msg = detail.message.as_deref().unwrap_or("Unknown error");
134 println!(" {} {} - {}", "\u{2717}".red(), name, msg.dimmed());
135 }
136 }
137 }
138
139 println!();
141 if result.failed == 0 && result.total > 0 {
142 println!(
143 "{} Operation completed successfully!",
144 "\u{2713}".green().bold()
145 );
146 } else if result.failed > 0 {
147 println!(
148 "{} Operation completed with {} failure(s)",
149 "\u{26A0}".yellow().bold(),
150 result.failed
151 );
152 }
153 }
154 _ => {
155 output_format.write(&output)?;
156 }
157 }
158
159 Ok(())
160}
161
162impl OperationCommands {
163 pub async fn execute(self, output_format: OutputFormat) -> Result<()> {
164 match self {
165 OperationCommands::Status { operation_id } => {
166 let state_manager = StateManager::new()?;
167
168 let op_id = match operation_id {
169 Some(id) => id,
170 None => {
171 let ops = state_manager.list_operations(None).await?;
173 if ops.is_empty() {
174 anyhow::bail!("No operations found");
175 }
176 ops[0].operation_id
177 }
178 };
179
180 let state = state_manager.load_operation(op_id).await?;
181
182 let output = OperationStatusOutput {
183 operation_id: state.operation_id.to_string(),
184 operation_type: format!("{:?}", state.operation_type),
185 status: format!("{:?}", state.status),
186 total: state.project_ids.len(),
187 completed: state
188 .results
189 .values()
190 .filter(|r| matches!(r.result, raps_admin::ItemResult::Success))
191 .count(),
192 skipped: state
193 .results
194 .values()
195 .filter(|r| matches!(r.result, raps_admin::ItemResult::Skipped { .. }))
196 .count(),
197 failed: state
198 .results
199 .values()
200 .filter(|r| matches!(r.result, raps_admin::ItemResult::Failed { .. }))
201 .count(),
202 created_at: state.created_at.to_rfc3339(),
203 updated_at: state.updated_at.to_rfc3339(),
204 };
205
206 match output_format {
207 OutputFormat::Table => {
208 println!("\n{}", "Operation Status:".bold());
209 println!("{}", "\u{2500}".repeat(60));
210 println!("{:<15} {}", "Operation:".bold(), output.operation_id.cyan());
211 println!("{:<15} {}", "Type:".bold(), output.operation_type);
212 println!("{:<15} {}", "Status:".bold(), format_status(&output.status));
213 println!(
214 "{:<15} {}/{} ({}%)",
215 "Progress:".bold(),
216 output.completed + output.skipped + output.failed,
217 output.total,
218 if output.total > 0 {
219 ((output.completed + output.skipped + output.failed) * 100)
220 / output.total
221 } else {
222 100
223 }
224 );
225 println!(
226 "{:<15} {}",
227 "Completed:".bold(),
228 output.completed.to_string().green()
229 );
230 println!(
231 "{:<15} {}",
232 "Skipped:".bold(),
233 output.skipped.to_string().yellow()
234 );
235 println!(
236 "{:<15} {}",
237 "Failed:".bold(),
238 output.failed.to_string().red()
239 );
240 println!("{:<15} {}", "Created:".bold(), output.created_at);
241 println!("{:<15} {}", "Updated:".bold(), output.updated_at);
242 println!("{}", "\u{2500}".repeat(60));
243 }
244 _ => {
245 output_format.write(&output)?;
246 }
247 }
248
249 Ok(())
250 }
251
252 OperationCommands::Resume {
253 operation_id,
254 concurrency,
255 } => {
256 let state_manager = StateManager::new()?;
257
258 let op_id = match operation_id {
260 Some(id) => id,
261 None => {
262 match state_manager.get_resumable_operation().await? {
264 Some(id) => id,
265 None => anyhow::bail!("No resumable operation found"),
266 }
267 }
268 };
269
270 let state = state_manager.load_operation(op_id).await?;
271
272 if state.status != OperationStatus::InProgress
274 && state.status != OperationStatus::Pending
275 {
276 anyhow::bail!(
277 "Operation cannot be resumed (current status: {:?})",
278 state.status
279 );
280 }
281
282 let pending = state_manager.get_pending_projects(&state);
283 if pending.is_empty() {
284 if output_format.supports_colors() {
285 println!(
286 "{} Operation {} is already complete",
287 "\u{2713}".green(),
288 op_id
289 );
290 }
291 return Ok(());
292 }
293
294 let concurrency_limit = concurrency.unwrap_or(10).min(50);
295
296 if output_format.supports_colors() {
297 println!(
298 "\n{} Resuming operation: {}",
299 "\u{2192}".cyan(),
300 op_id.to_string().cyan()
301 );
302 println!(" Type: {:?}", state.operation_type);
303 println!(
304 " Pending: {}/{} items",
305 pending.len(),
306 state.project_ids.len()
307 );
308 println!(" Concurrency: {}", concurrency_limit);
309 println!();
310
311 println!(
314 "{} Resume requires re-running with the original command and credentials.",
315 "\u{26A0}".yellow()
316 );
317 println!(" Pending projects:");
318 for (i, project_id) in pending.iter().take(10).enumerate() {
319 println!(" {}. {}", i + 1, project_id.dimmed());
320 }
321 if pending.len() > 10 {
322 println!(" ... and {} more", pending.len() - 10);
323 }
324 }
325
326 Ok(())
327 }
328
329 OperationCommands::Cancel {
330 operation_id,
331 yes: _,
332 } => {
333 let state_manager = StateManager::new()?;
334
335 let op_id = match operation_id {
337 Some(id) => id,
338 None => {
339 match state_manager.get_resumable_operation().await? {
341 Some(id) => id,
342 None => anyhow::bail!("No active operation found to cancel"),
343 }
344 }
345 };
346
347 let state = state_manager.load_operation(op_id).await?;
348
349 if output_format.supports_colors() {
350 println!(
351 "\n{} Cancelling operation: {}",
352 "\u{2192}".cyan(),
353 op_id.to_string().cyan()
354 );
355 println!(" Type: {:?}", state.operation_type);
356 println!(" Current status: {:?}", state.status);
357 }
358
359 state_manager.cancel_operation(op_id).await?;
361
362 if output_format.supports_colors() {
363 let processed = state.results.len();
364 let total = state.project_ids.len();
365 println!("\n{} Operation cancelled", "\u{2713}".green());
366 println!(
367 " Processed: {}/{} items before cancellation",
368 processed, total
369 );
370 }
371
372 Ok(())
373 }
374
375 OperationCommands::List { status, limit } => {
376 let state_manager = StateManager::new()?;
377
378 let status_filter = status
379 .as_ref()
380 .and_then(|s| match s.to_lowercase().as_str() {
381 "pending" => Some(OperationStatus::Pending),
382 "in_progress" | "in-progress" => Some(OperationStatus::InProgress),
383 "completed" => Some(OperationStatus::Completed),
384 "failed" => Some(OperationStatus::Failed),
385 "cancelled" => Some(OperationStatus::Cancelled),
386 _ => None,
387 });
388
389 let operations = state_manager.list_operations(status_filter).await?;
390 let operations: Vec<_> = operations.into_iter().take(limit).collect();
391
392 if operations.is_empty() {
393 match output_format {
394 OutputFormat::Table => println!("{}", "No operations found.".yellow()),
395 _ => output_format.write(&Vec::<OperationListOutput>::new())?,
396 }
397 return Ok(());
398 }
399
400 let outputs: Vec<OperationListOutput> = operations
401 .iter()
402 .map(|op| OperationListOutput {
403 operation_id: op.operation_id.to_string(),
404 operation_type: format!("{:?}", op.operation_type),
405 status: format!("{:?}", op.status),
406 progress: format!("{}/{}", op.completed + op.skipped + op.failed, op.total),
407 updated_at: op.updated_at.to_rfc3339(),
408 })
409 .collect();
410
411 match output_format {
412 OutputFormat::Table => {
413 println!("\n{}", "Operations:".bold());
414 println!("{}", "\u{2500}".repeat(100));
415 println!(
416 "{:<38} {:<15} {:<12} {:<12} {}",
417 "ID".bold(),
418 "Type".bold(),
419 "Status".bold(),
420 "Progress".bold(),
421 "Updated".bold()
422 );
423 println!("{}", "\u{2500}".repeat(100));
424
425 for op in &outputs {
426 println!(
427 "{:<38} {:<15} {:<12} {:<12} {}",
428 op.operation_id.cyan(),
429 op.operation_type,
430 format_status(&op.status),
431 op.progress,
432 op.updated_at.dimmed()
433 );
434 }
435
436 println!("{}", "\u{2500}".repeat(100));
437 println!("{} {} operation(s) found", "\u{2192}".cyan(), outputs.len());
438 }
439 _ => {
440 output_format.write(&outputs)?;
441 }
442 }
443
444 Ok(())
445 }
446 }
447 }
448}