1use chrono::Local;
2use std::collections::HashMap;
3
4use crate::blocks::{TextAlign, TextSize};
5use crate::builder::CardBuilder;
6use crate::templates::{ColorTheme, GenericCardTemplate, LanguageCode, get_translation};
7
8pub struct TaskSetProgress {
10 pub complete: u32,
12 pub total: u32,
14}
15
16pub fn network_submission_start(
17 network_set_name: &str,
18 network_type: &str,
19 group: Option<&str>,
20 prefix: Option<&str>,
21 metadata: Option<&serde_json::Value>,
22 language: LanguageCode,
23) -> GenericCardTemplate {
24 let t = |k: &str| get_translation(k, language);
25 let metadata_text = format!(
26 "**{}:** {}\n**{}:** {}",
27 t("network_set_name"),
28 network_set_name,
29 t("network_type"),
30 network_type,
31 );
32 let status_text = t("running");
33 let mut builder = CardBuilder::new()
34 .header(
35 &t("network_submission_started"),
36 Some(&status_text),
37 Some(ColorTheme::Wathet),
38 None,
39 )
40 .markdown(&metadata_text, TextAlign::Left, TextSize::Normal);
41
42 if group.is_some() || prefix.is_some() {
43 builder = builder
44 .columns()
45 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
46 .column(
47 &t("storage_prefix"),
48 Some(prefix.unwrap_or("*N/A*")),
49 "weighted",
50 1,
51 )
52 .end_columns();
53 }
54 if let Some(meta) = metadata {
55 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
56 builder = builder.collapsible(
57 &t("metadata_overview"),
58 &format!("```json\n{s}\n```"),
59 false,
60 );
61 }
62 builder.build()
63}
64
65pub fn network_submission_complete(
66 network_set_name: &str,
67 submitted_count: Option<u32>,
68 group: Option<&str>,
69 prefix: Option<&str>,
70 duration: Option<&str>,
71 metadata: Option<&serde_json::Value>,
72 language: LanguageCode,
73) -> GenericCardTemplate {
74 let t = |k: &str| get_translation(k, language);
75 let mut lines = vec![format!(
76 "**{}:** {}",
77 t("network_set_name"),
78 network_set_name
79 )];
80 if let Some(c) = submitted_count {
81 lines.push(format!("**{}:** {c}", t("submitted_count")));
82 }
83 if let Some(d) = duration {
84 lines.push(format!("**{}:** {d}", t("duration")));
85 }
86
87 let status_text = t("success");
88 let mut builder = CardBuilder::new()
89 .header(
90 &t("network_submission_complete"),
91 Some(&status_text),
92 Some(ColorTheme::Green),
93 None,
94 )
95 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
96
97 if group.is_some() || prefix.is_some() {
98 builder = builder
99 .columns()
100 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
101 .column(
102 &t("storage_prefix"),
103 Some(prefix.unwrap_or("*N/A*")),
104 "weighted",
105 1,
106 )
107 .end_columns();
108 }
109 if let Some(meta) = metadata {
110 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
111 builder = builder.collapsible(
112 &t("metadata_overview"),
113 &format!("```json\n{s}\n```"),
114 false,
115 );
116 }
117 builder.build()
118}
119
120pub fn network_submission_failure(
121 network_set_name: &str,
122 error_message: &str,
123 submitted_count: Option<u32>,
124 group: Option<&str>,
125 language: LanguageCode,
126) -> GenericCardTemplate {
127 let t = |k: &str| get_translation(k, language);
128 let mut lines = vec![
129 format!("**{}:** {}", t("network_set_name"), network_set_name),
130 format!("**{}:** {}", t("group"), group.unwrap_or("*unknown*")),
131 ];
132 if let Some(c) = submitted_count {
133 lines.push(format!("**{}:** {c}", t("submitted_count")));
134 }
135
136 let status_text = t("failed");
137 CardBuilder::new()
138 .header(
139 &t("network_submission_failed"),
140 Some(&status_text),
141 Some(ColorTheme::Red),
142 None,
143 )
144 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal)
145 .collapsible(&t("error_details"), error_message, false)
146 .build()
147}
148
149pub fn config_upload_complete(
150 config_name: &str,
151 file_count: u32,
152 labels: Option<&[&str]>,
153 desc: Option<&str>,
154 language: LanguageCode,
155) -> GenericCardTemplate {
156 let t = |k: &str| get_translation(k, language);
157 let mut lines = vec![
158 format!("**{}:** {}", t("config_name"), config_name),
159 format!("**{}:** {}", t("files_uploaded"), file_count),
160 ];
161 if let Some(d) = desc {
162 lines.push(format!("**{}:** {d}", t("description")));
163 }
164
165 let status_text = t("success");
166 let mut builder = CardBuilder::new()
167 .header(
168 &t("config_uploaded"),
169 Some(&status_text),
170 Some(ColorTheme::Green),
171 None,
172 )
173 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
174
175 if let Some(lbls) = labels
176 && !lbls.is_empty()
177 {
178 builder = builder.collapsible(&t("uploaded_files"), &lbls.join(","), false);
179 }
180 builder.build()
181}
182
183pub fn job_submission_start(
184 job_title: &str,
185 desc: Option<&str>,
186 group: Option<&str>,
187 prefix: Option<&str>,
188 msg: Option<&str>,
189 metadata: Option<&serde_json::Value>,
190 language: LanguageCode,
191) -> GenericCardTemplate {
192 let t = |k: &str| get_translation(k, language);
193 let start_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
194 let running_status = format!(
195 "<font color='wathet-400'> :StatusInFlight: {}</font>",
196 t("running")
197 );
198 let mut lines = vec![
199 format!("**{}:** {}", t("task_name"), job_title),
200 format!("**{}:** {}", t("start_time"), start_time),
201 ];
202 if let Some(d) = desc {
203 lines.push(format!("**{}:** {d}", t("task_description")));
204 }
205 lines.push(format!("**{}:** {}", t("execution_status"), running_status));
206
207 let status_text = t("running");
208 let mut builder = CardBuilder::new()
209 .header(
210 &t("task_submission_started"),
211 Some(&status_text),
212 Some(ColorTheme::Wathet),
213 None,
214 )
215 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
216
217 if group.is_some() || prefix.is_some() {
218 builder = builder
219 .columns()
220 .column(
221 &t("result_storage"),
222 Some(group.unwrap_or("*unknown*")),
223 "auto",
224 1,
225 )
226 .column(
227 &t("storage_prefix"),
228 Some(prefix.unwrap_or("*N/A*")),
229 "weighted",
230 1,
231 )
232 .end_columns();
233 }
234 if let Some(m) = msg {
235 builder = builder.collapsible(&t("running_overview"), m, false);
236 }
237 if let Some(meta) = metadata {
238 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
239 builder = builder.collapsible(
240 &t("metadata_overview"),
241 &format!("```json\n{s}\n```"),
242 false,
243 );
244 }
245 builder.build()
246}
247
248#[allow(clippy::too_many_arguments)]
249pub fn job_submission_complete(
250 job_title: &str,
251 submitted_count: u32,
252 desc: Option<&str>,
253 group: Option<&str>,
254 prefix: Option<&str>,
255 duration: Option<&str>,
256 msg: Option<&str>,
257 metadata: Option<&serde_json::Value>,
258 language: LanguageCode,
259) -> GenericCardTemplate {
260 let t = |k: &str| get_translation(k, language);
261 let completion_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
262 let status_text = format!(
263 "<font color='wathet-400'> :StatusInFlight: {}, {}</font>",
264 t("task_submission_complete"),
265 t("running")
266 );
267 let mut lines = vec![
268 format!("**{}:** {}", t("task_name"), job_title),
269 format!("**{}:** {}", t("completion_time"), completion_time),
270 format!("**{}:** {}", t("submitted_count"), submitted_count),
271 ];
272 if let Some(d) = desc {
273 lines.push(format!("**{}:** {d}", t("task_description")));
274 }
275 if let Some(d) = duration {
276 lines.push(format!("**{}:** {d}", t("execution_duration")));
277 }
278 lines.push(format!("**{}:** {}", t("execution_status"), status_text));
279
280 let header_status_text = t("submitted");
281 let mut builder = CardBuilder::new()
282 .header(
283 &t("task_submission_complete"),
284 Some(&header_status_text),
285 Some(ColorTheme::Wathet),
286 None,
287 )
288 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
289
290 if group.is_some() || prefix.is_some() {
291 builder = builder
292 .columns()
293 .column(
294 &t("result_storage"),
295 Some(group.unwrap_or("*unknown*")),
296 "auto",
297 1,
298 )
299 .column(
300 &t("storage_prefix"),
301 Some(prefix.unwrap_or("*N/A*")),
302 "weighted",
303 1,
304 )
305 .end_columns();
306 }
307 if let Some(m) = msg {
308 builder = builder.collapsible(&t("running_overview"), m, false);
309 }
310 if let Some(meta) = metadata {
311 let s = serde_json::to_string_pretty(meta).unwrap_or_default();
312 builder = builder.collapsible(
313 &t("metadata_overview"),
314 &format!("```json\n{s}\n```"),
315 false,
316 );
317 }
318 builder.build()
319}
320
321pub fn job_submission_failure(
322 job_title: &str,
323 error_message: &str,
324 submitted_count: Option<u32>,
325 group: Option<&str>,
326 language: LanguageCode,
327) -> GenericCardTemplate {
328 let t = |k: &str| get_translation(k, language);
329 let mut lines = vec![
330 format!("**{}:** {}", t("task_name"), job_title),
331 format!("**{}:** {}", t("group"), group.unwrap_or("*unknown*")),
332 ];
333 if let Some(c) = submitted_count {
334 lines.push(format!("**{}:** {c}", t("submitted_before_failure")));
335 }
336
337 let status_text = t("failed");
338 CardBuilder::new()
339 .header(
340 &t("task_submission_failed"),
341 Some(&status_text),
342 Some(ColorTheme::Red),
343 None,
344 )
345 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal)
346 .collapsible(&t("error_details"), error_message, false)
347 .build()
348}
349
350#[allow(clippy::too_many_arguments)]
351pub fn job_complete(
352 job_title: &str,
353 success: bool,
354 status: i32,
355 group: Option<&str>,
356 prefix: Option<&str>,
357 desc: Option<&str>,
358 msg: Option<&str>,
359 duration: Option<&str>,
360 title: Option<&str>,
361 language: LanguageCode,
362) -> GenericCardTemplate {
363 let t = |k: &str| get_translation(k, language);
364 let completion_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
365
366 let (task_status, status_text, color, default_title) = if success {
367 (
368 format!("<font color='green'> :CheckMark: {}</font>", t("completed")),
369 t("success"),
370 ColorTheme::Green,
371 t("task_completion_notification"),
372 )
373 } else {
374 (
375 format!(
376 "<font color='red'> :CrossMark: {}: {status}</font>",
377 t("failed")
378 ),
379 t("failure"),
380 ColorTheme::Red,
381 t("task_failure_notification"),
382 )
383 };
384
385 let card_title = title.unwrap_or(&default_title);
386 let mut lines = vec![
387 format!("**{}:** {}", t("task_name"), job_title),
388 format!("**{}:** {}", t("completion_time"), completion_time),
389 ];
390 if let Some(d) = desc {
391 lines.push(format!("**{}:** {d}", t("task_description")));
392 }
393 if let Some(d) = duration {
394 lines.push(format!("**{}:** {d}", t("execution_duration")));
395 }
396 lines.push(format!("**{}:** {}", t("execution_status"), task_status));
397
398 let mut builder = CardBuilder::new()
399 .header(card_title, Some(&status_text), Some(color), None)
400 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
401
402 if group.is_some() || prefix.is_some() {
403 builder = builder
404 .columns()
405 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
406 .column(
407 &t("storage_prefix"),
408 Some(prefix.unwrap_or("*N/A*")),
409 "weighted",
410 1,
411 )
412 .end_columns();
413 }
414 if let Some(m) = msg {
415 builder = builder.collapsible(&t("result_overview"), m, false);
416 }
417 builder.build()
418}
419
420pub fn task_set_progress(
424 progress: &HashMap<&str, TaskSetProgress>,
425 overall_status: &str,
426 language: LanguageCode,
427) -> GenericCardTemplate {
428 let t = |k: &str| get_translation(k, language);
429 let mut table_lines = vec![
430 format!(
431 "| {} | Progress | {} | Total |",
432 t("task_set_name"),
433 t("completed")
434 ),
435 "|:---|:---|---:|---:|".to_owned(),
436 ];
437 for (name, p) in progress {
438 let pct = if p.total > 0 {
439 p.complete as f64 / p.total as f64 * 100.0
440 } else {
441 0.0
442 };
443 table_lines.push(format!(
444 "| {name} | {pct:.1}% | {} | {} |",
445 p.complete, p.total
446 ));
447 }
448
449 CardBuilder::new()
450 .header(
451 &t("task_set_progress"),
452 Some(overall_status),
453 Some(ColorTheme::Blue),
454 None,
455 )
456 .collapsible(&t("task_sets"), &table_lines.join("\n"), true)
457 .build()
458}
459
460pub fn result_collection_start(
461 task_set_names: &[&str],
462 job_title: Option<&str>,
463 group: Option<&str>,
464 msg: Option<&str>,
465 language: LanguageCode,
466) -> GenericCardTemplate {
467 let t = |k: &str| get_translation(k, language);
468 let resolved_title = job_title.map(|s| s.to_owned()).unwrap_or_else(|| {
469 if task_set_names.len() == 1 {
470 task_set_names[0].to_owned()
471 } else {
472 format!("Collection of {} Task Sets", task_set_names.len())
473 }
474 });
475 let mut lines = vec![
476 format!("**{}:** {}", t("task_name"), resolved_title),
477 format!("**{}:** {}", t("task_set_count"), task_set_names.len()),
478 ];
479 if let Some(g) = group {
480 lines.push(format!("**{}:** {g}", t("group")));
481 }
482
483 let status_text = t("running");
484 let mut builder = CardBuilder::new()
485 .header(
486 &t("result_collection_started"),
487 Some(&status_text),
488 Some(ColorTheme::Wathet),
489 None,
490 )
491 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
492
493 if let Some(m) = msg {
494 builder = builder.collapsible(&t("running_overview"), m, false);
495 }
496 builder.build()
497}
498
499pub fn result_collection_complete(
500 task_set_names: &[&str],
501 job_title: Option<&str>,
502 group: Option<&str>,
503 prefix: Option<&str>,
504 msg: Option<&str>,
505 language: LanguageCode,
506) -> GenericCardTemplate {
507 let t = |k: &str| get_translation(k, language);
508 let resolved_title = job_title.map(|s| s.to_owned()).unwrap_or_else(|| {
509 if task_set_names.len() == 1 {
510 task_set_names[0].to_owned()
511 } else {
512 format!("Collection of {} Task Sets", task_set_names.len())
513 }
514 });
515 let mut lines = vec![
516 format!("**{}:** {}", t("task_name"), resolved_title),
517 format!("**{}:** {}", t("task_set_count"), task_set_names.len()),
518 ];
519 if let Some(g) = group {
520 lines.push(format!("**{}:** {g}", t("group")));
521 }
522
523 let status_text = t("success");
524 let mut builder = CardBuilder::new()
525 .header(
526 &t("result_collection_complete"),
527 Some(&status_text),
528 Some(ColorTheme::Green),
529 None,
530 )
531 .markdown(&lines.join("\n"), TextAlign::Left, TextSize::Normal);
532
533 if group.is_some() || prefix.is_some() {
534 builder = builder
535 .columns()
536 .column(&t("group"), Some(group.unwrap_or("*unknown*")), "auto", 1)
537 .column(
538 &t("storage_prefix"),
539 Some(prefix.unwrap_or("*N/A*")),
540 "weighted",
541 1,
542 )
543 .end_columns();
544 }
545 if let Some(m) = msg {
546 builder = builder.collapsible(&t("state_overview"), m, false);
547 }
548 builder.build()
549}
550
551pub fn comparison_complete(
552 comparison_name: &str,
553 task_set_count: u32,
554 result_rows: u32,
555 result_columns: u32,
556 comparison_table: Option<&str>,
557 language: LanguageCode,
558) -> GenericCardTemplate {
559 let t = |k: &str| get_translation(k, language);
560 let metadata_text = format!(
561 "**{}:** {}\n**{}:** {}",
562 t("comparison_name"),
563 comparison_name,
564 t("task_sets_compared"),
565 task_set_count,
566 );
567
568 let status_text = t("success");
569 let mut builder = CardBuilder::new()
570 .header(
571 &t("comparison_complete"),
572 Some(&status_text),
573 Some(ColorTheme::Orange),
574 None,
575 )
576 .markdown(&metadata_text, TextAlign::Left, TextSize::Normal)
577 .columns()
578 .column(
579 &t("result_rows"),
580 Some(&result_rows.to_string()),
581 "weighted",
582 1,
583 )
584 .column(
585 &t("result_columns"),
586 Some(&result_columns.to_string()),
587 "weighted",
588 1,
589 )
590 .end_columns();
591
592 if let Some(tbl) = comparison_table {
593 builder = builder.collapsible(&t("comparison_results"), tbl, false);
594 }
595 builder.build()
596}
597
598pub fn create_custom_template(language: LanguageCode) -> CardBuilder {
599 CardBuilder::new().language(language)
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::templates::{LanguageCode, LarkTemplate};
606 use std::collections::HashMap;
607
608 fn zh() -> LanguageCode {
609 LanguageCode::Zh
610 }
611
612 #[test]
613 fn test_network_submission_start() {
614 let t = network_submission_start("netset1", "TCP", None, None, None, zh());
615 let card = t.generate();
616 assert_eq!(card["schema"], "2.0");
617 assert_eq!(card["header"]["template"], "wathet");
618 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
619 assert!(el0.contains("netset1"));
620 assert!(el0.contains("TCP"));
621 }
622
623 #[test]
624 fn test_network_submission_complete_green() {
625 let t = network_submission_complete("ns", Some(10), None, None, None, None, zh());
626 let card = t.generate();
627 assert_eq!(card["schema"], "2.0");
628 assert_eq!(card["header"]["template"], "green");
629 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
630 assert!(el0.contains("ns"));
631 }
632
633 #[test]
634 fn test_network_submission_failure_red() {
635 let t = network_submission_failure("ns", "timeout", None, None, zh());
636 let card = t.generate();
637 assert_eq!(card["schema"], "2.0");
638 assert_eq!(card["header"]["template"], "red");
639 let elements = card["body"]["elements"].as_array().unwrap();
640 assert!(elements.len() >= 2); }
642
643 #[test]
644 fn test_config_upload_complete() {
645 let t = config_upload_complete("cfg1", 5, None, None, zh());
646 let card = t.generate();
647 assert_eq!(card["schema"], "2.0");
648 assert_eq!(card["header"]["template"], "green");
649 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
650 assert!(el0.contains("cfg1"));
651 }
652
653 #[test]
654 fn test_job_submission_start() {
655 let t = job_submission_start("job1", None, None, None, None, None, zh());
656 let card = t.generate();
657 assert_eq!(card["schema"], "2.0");
658 assert_eq!(card["header"]["template"], "wathet");
659 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
660 assert!(el0.contains("job1"));
661 }
662
663 #[test]
664 fn test_job_submission_complete() {
665 let t = job_submission_complete("job1", 42, None, None, None, None, None, None, zh());
666 let card = t.generate();
667 assert_eq!(card["schema"], "2.0");
668 assert_eq!(card["header"]["template"], "wathet");
669 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
670 assert!(el0.contains("job1"));
671 assert!(el0.contains("42"));
672 }
673
674 #[test]
675 fn test_job_submission_failure_red() {
676 let t = job_submission_failure("job1", "oom", None, None, zh());
677 let card = t.generate();
678 assert_eq!(card["schema"], "2.0");
679 assert_eq!(card["header"]["template"], "red");
680 let elements = card["body"]["elements"].as_array().unwrap();
681 assert!(elements.len() >= 2);
682 }
683
684 #[test]
685 fn test_job_complete_success() {
686 let t = job_complete("job1", true, 0, None, None, None, None, None, None, zh());
687 let card = t.generate();
688 assert_eq!(card["schema"], "2.0");
689 assert_eq!(card["header"]["template"], "green");
690 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
691 assert!(el0.contains("job1"));
692 }
693
694 #[test]
695 fn test_job_complete_failure() {
696 let t = job_complete("job1", false, 1, None, None, None, None, None, None, zh());
697 let card = t.generate();
698 assert_eq!(card["schema"], "2.0");
699 assert_eq!(card["header"]["template"], "red");
700 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
701 assert!(el0.contains("1")); }
703
704 #[test]
705 fn test_task_set_progress() {
706 let mut progress = HashMap::new();
707 progress.insert(
708 "set_a",
709 TaskSetProgress {
710 complete: 5,
711 total: 10,
712 },
713 );
714 let t = task_set_progress(&progress, "running", zh());
715 let card = t.generate();
716 assert_eq!(card["schema"], "2.0");
717 assert_eq!(card["header"]["template"], "blue");
718 let elements = card["body"]["elements"].as_array().unwrap();
719 assert!(!elements.is_empty());
720 }
721
722 #[test]
723 fn test_result_collection_start() {
724 let t = result_collection_start(&["set_a", "set_b"], None, None, None, zh());
725 let card = t.generate();
726 assert_eq!(card["schema"], "2.0");
727 assert_eq!(card["header"]["template"], "wathet");
728 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
729 assert!(el0.contains("2")); }
731
732 #[test]
733 fn test_result_collection_complete() {
734 let t = result_collection_complete(&["set_a"], Some("my_job"), None, None, None, zh());
735 let card = t.generate();
736 assert_eq!(card["schema"], "2.0");
737 assert_eq!(card["header"]["template"], "green");
738 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
739 assert!(el0.contains("my_job"));
740 }
741
742 #[test]
743 fn test_comparison_complete_orange() {
744 let t = comparison_complete("cmp1", 3, 100, 5, None, zh());
745 let card = t.generate();
746 assert_eq!(card["schema"], "2.0");
747 assert_eq!(card["header"]["template"], "orange");
748 let el0 = card["body"]["elements"][0]["content"].as_str().unwrap();
749 assert!(el0.contains("cmp1"));
750 }
751
752 #[test]
753 fn test_task_set_progress_fields() {
754 let p = TaskSetProgress {
755 complete: 3,
756 total: 7,
757 };
758 assert_eq!(p.complete, 3);
759 assert_eq!(p.total, 7);
760 }
761
762 #[test]
763 fn test_create_custom_template() {
764 let t = create_custom_template(zh())
765 .header("Custom", Some("info"), None, None)
766 .markdown("content", TextAlign::Left, TextSize::Normal)
767 .build();
768 let card = t.generate();
769 assert_eq!(card["header"]["title"]["content"], "Custom");
770 }
771}