1pub mod applications;
2pub mod functions;
3
4use crate::common::{format_bytes, translate_column, ColumnId, UTC_TIMESTAMP_WIDTH};
5use crate::ui::lambda::{ApplicationDetailTab, DetailTab};
6use crate::ui::table;
7use ratatui::prelude::*;
8use std::collections::HashMap;
9
10pub fn parse_layer_arn(arn: &str) -> (String, String) {
11 let parts: Vec<&str> = arn.split(':').collect();
12 let name = parts.get(6).unwrap_or(&"").to_string();
13 let version = parts.get(7).unwrap_or(&"").to_string();
14 (name, version)
15}
16
17pub fn init(i18n: &mut HashMap<String, String>) {
18 for col in FunctionColumn::all() {
19 i18n.entry(col.id().to_string())
20 .or_insert_with(|| col.default_name().to_string());
21 }
22 for col in ApplicationColumn::all() {
23 i18n.entry(col.id().to_string())
24 .or_insert_with(|| col.default_name().to_string());
25 }
26 for col in DeploymentColumn::all() {
27 i18n.entry(col.id().to_string())
28 .or_insert_with(|| col.default_name().to_string());
29 }
30 for col in ResourceColumn::all() {
31 i18n.entry(col.id().to_string())
32 .or_insert_with(|| col.default_name().to_string());
33 }
34}
35
36pub fn format_runtime(runtime: &str) -> String {
37 let lower = runtime.to_lowercase();
38
39 if let Some(rest) = lower.strip_prefix("python") {
40 let version = if rest.contains('.') {
41 rest.to_string()
42 } else if rest.len() >= 2 {
43 format!("{}.{}", &rest[0..1], &rest[1..])
44 } else {
45 rest.to_string()
46 };
47 format!("Python {}", version)
48 } else if let Some(rest) = lower.strip_prefix("nodejs") {
49 let formatted = rest.replace("x", ".x");
50 format!("Node.js {}", formatted)
51 } else if let Some(rest) = lower.strip_prefix("java") {
52 format!("Java {}", rest)
53 } else if let Some(rest) = lower.strip_prefix("dotnet") {
54 format!(".NET {}", rest)
55 } else if let Some(rest) = lower.strip_prefix("go") {
56 format!("Go {}", rest)
57 } else if let Some(rest) = lower.strip_prefix("ruby") {
58 format!("Ruby {}", rest)
59 } else {
60 runtime.to_string()
61 }
62}
63
64pub fn format_architecture(arch: &str) -> String {
65 match arch {
66 "X86_64" => "x86-64".to_string(),
67 "X8664" => "x86-64".to_string(),
68 "Arm64" => "arm64".to_string(),
69 _ => arch.replace("X86", "x86").replace("Arm", "arm"),
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::common::InputFocus;
77 use crate::ui::lambda::FILTER_CONTROLS;
78 use crate::ui::table::Column as TableColumn;
79
80 #[test]
81 fn test_format_runtime() {
82 assert_eq!(format_runtime("Python39"), "Python 3.9");
84 assert_eq!(format_runtime("Python312"), "Python 3.12");
85 assert_eq!(format_runtime("Nodejs20x"), "Node.js 20.x");
86
87 assert_eq!(format_runtime("python3.12"), "Python 3.12");
89 assert_eq!(format_runtime("python3.11"), "Python 3.11");
90 assert_eq!(format_runtime("nodejs20x"), "Node.js 20.x");
91 assert_eq!(format_runtime("nodejs18x"), "Node.js 18.x");
92 assert_eq!(format_runtime("java21"), "Java 21");
93 assert_eq!(format_runtime("dotnet8"), ".NET 8");
94 assert_eq!(format_runtime("go1.x"), "Go 1.x");
95 assert_eq!(format_runtime("ruby3.3"), "Ruby 3.3");
96 assert_eq!(format_runtime("unknown"), "unknown");
97 }
98
99 #[test]
100 fn test_format_architecture() {
101 assert_eq!(format_architecture("X8664"), "x86-64");
102 assert_eq!(format_architecture("X86_64"), "x86-64");
103 assert_eq!(format_architecture("Arm64"), "arm64");
104 assert_eq!(format_architecture("arm64"), "arm64");
105 }
106
107 #[test]
108 fn test_runtime_formatter_in_table_column() {
109 let func = Function {
110 name: "test-func".to_string(),
111 arn: "arn".to_string(),
112 application: None,
113 description: "desc".to_string(),
114 package_type: "Zip".to_string(),
115 runtime: "python3.12".to_string(),
116 architecture: "X86_64".to_string(),
117 code_size: 1024,
118 code_sha256: "hash".to_string(),
119 memory_mb: 128,
120 timeout_seconds: 30,
121 last_modified: "2024-01-01".to_string(),
122 layers: vec![],
123 };
124
125 let runtime_col = FunctionColumn::Runtime;
126 let (text, _) = runtime_col.render(&func);
127 assert_eq!(text, "Python 3.12");
128 }
129
130 #[test]
131 fn test_architecture_formatter_in_table_column() {
132 let func = Function {
133 name: "test-func".to_string(),
134 arn: "arn".to_string(),
135 application: None,
136 description: "desc".to_string(),
137 package_type: "Zip".to_string(),
138 runtime: "python3.12".to_string(),
139 architecture: "X86_64".to_string(),
140 code_size: 1024,
141 code_sha256: "hash".to_string(),
142 memory_mb: 128,
143 timeout_seconds: 30,
144 last_modified: "2024-01-01".to_string(),
145 layers: vec![],
146 };
147
148 let arch_col = FunctionColumn::Architecture;
149 let (text, _) = arch_col.render(&func);
150 assert_eq!(text, "x86-64");
151 }
152
153 #[test]
154 fn test_nodejs_runtime_formatting() {
155 assert_eq!(format_runtime("nodejs16x"), "Node.js 16.x");
156 assert_eq!(format_runtime("nodejs18x"), "Node.js 18.x");
157 assert_eq!(format_runtime("nodejs20x"), "Node.js 20.x");
158 }
159
160 #[test]
161 fn test_python_runtime_formatting() {
162 assert_eq!(format_runtime("Python38"), "Python 3.8");
164 assert_eq!(format_runtime("Python39"), "Python 3.9");
165 assert_eq!(format_runtime("Python310"), "Python 3.10");
166 assert_eq!(format_runtime("Python311"), "Python 3.11");
167 assert_eq!(format_runtime("Python312"), "Python 3.12");
168
169 assert_eq!(format_runtime("python3.8"), "Python 3.8");
171 assert_eq!(format_runtime("python3.9"), "Python 3.9");
172 assert_eq!(format_runtime("python3.10"), "Python 3.10");
173 assert_eq!(format_runtime("python3.11"), "Python 3.11");
174 assert_eq!(format_runtime("python3.12"), "Python 3.12");
175 }
176
177 #[test]
178 fn test_timeout_formatting() {
179 assert_eq!(300 / 60, 5); assert_eq!(300 % 60, 0); assert_eq!(900 / 60, 15); assert_eq!(900 % 60, 0); assert_eq!(330 / 60, 5); assert_eq!(330 % 60, 30); }
187
188 #[test]
189 fn test_version_column_architecture_formatter() {
190 let version = Version {
191 version: "1".to_string(),
192 aliases: "prod".to_string(),
193 description: "Production version".to_string(),
194 last_modified: "2024-01-01".to_string(),
195 architecture: "X86_64".to_string(),
196 };
197
198 let arch_col = VersionColumn::Architecture.to_column();
199 let (text, _) = arch_col.render(&version);
200 assert_eq!(text, "x86-64");
201 }
202
203 #[test]
204 fn test_version_architecture_formatter_arm() {
205 let version = Version {
206 version: "1".to_string(),
207 aliases: "".to_string(),
208 description: "".to_string(),
209 last_modified: "".to_string(),
210 architecture: "Arm64".to_string(),
211 };
212
213 let arch_col = VersionColumn::Architecture.to_column();
214 let (text, _) = arch_col.render(&version);
215 assert_eq!(text, "arm64");
216 }
217
218 #[test]
219 fn test_version_column_all() {
220 let all = VersionColumn::all();
221 assert_eq!(all.len(), 5);
222 assert!(all.contains(&VersionColumn::Version));
223 assert!(all.contains(&VersionColumn::Aliases));
224 assert!(all.contains(&VersionColumn::Description));
225 assert!(all.contains(&VersionColumn::LastModified));
226 assert!(all.contains(&VersionColumn::Architecture));
227 }
228
229 #[test]
230 fn test_version_column_names() {
231 assert_eq!(VersionColumn::Version.name(), "Version");
232 assert_eq!(VersionColumn::Aliases.name(), "Aliases");
233 assert_eq!(VersionColumn::Description.name(), "Description");
234 assert_eq!(VersionColumn::LastModified.name(), "Last modified");
235 assert_eq!(VersionColumn::Architecture.name(), "Architecture");
236 }
237
238 #[test]
239 fn test_versions_table_sort_config() {
240 let sort_column = "Version";
244 let sort_direction = "DESC";
245 assert_eq!(sort_column, "Version");
246 assert_eq!(sort_direction, "DESC");
247 }
248
249 #[test]
250 fn test_input_focus_cycling() {
251 let focus = InputFocus::Filter;
252 assert_eq!(focus.next(&FILTER_CONTROLS), InputFocus::Pagination);
253
254 let focus = InputFocus::Pagination;
255 assert_eq!(focus.next(&FILTER_CONTROLS), InputFocus::Filter);
256
257 let focus = InputFocus::Filter;
258 assert_eq!(focus.prev(&FILTER_CONTROLS), InputFocus::Pagination);
259
260 let focus = InputFocus::Pagination;
261 assert_eq!(focus.prev(&FILTER_CONTROLS), InputFocus::Filter);
262 }
263
264 #[test]
265 #[allow(clippy::useless_vec)]
266 fn test_version_sorting_desc() {
267 let mut versions = vec![
268 Version {
269 version: "1".to_string(),
270 aliases: "".to_string(),
271 description: "".to_string(),
272 last_modified: "".to_string(),
273 architecture: "".to_string(),
274 },
275 Version {
276 version: "10".to_string(),
277 aliases: "".to_string(),
278 description: "".to_string(),
279 last_modified: "".to_string(),
280 architecture: "".to_string(),
281 },
282 Version {
283 version: "2".to_string(),
284 aliases: "".to_string(),
285 description: "".to_string(),
286 last_modified: "".to_string(),
287 architecture: "".to_string(),
288 },
289 ];
290
291 versions.sort_by(|a, b| {
293 let a_num = a.version.parse::<i32>().unwrap_or(0);
294 let b_num = b.version.parse::<i32>().unwrap_or(0);
295 b_num.cmp(&a_num)
296 });
297
298 assert_eq!(versions[0].version, "10");
299 assert_eq!(versions[1].version, "2");
300 assert_eq!(versions[2].version, "1");
301 }
302
303 #[test]
304 fn test_version_sorting_with_36_versions() {
305 let mut versions: Vec<Version> = (1..=36)
306 .map(|i| Version {
307 version: i.to_string(),
308 aliases: "".to_string(),
309 description: "".to_string(),
310 last_modified: "".to_string(),
311 architecture: "".to_string(),
312 })
313 .collect();
314
315 versions.sort_by(|a, b| {
317 let a_num = a.version.parse::<i32>().unwrap_or(0);
318 let b_num = b.version.parse::<i32>().unwrap_or(0);
319 b_num.cmp(&a_num)
320 });
321
322 assert_eq!(versions[0].version, "36");
324 assert_eq!(versions[1].version, "35");
325 assert_eq!(versions[35].version, "1");
326 assert_eq!(versions.len(), 36);
327 }
328
329 #[test]
330 fn test_column_id() {
331 assert_eq!(FunctionColumn::Name.id(), "column.lambda.function.name");
332 assert_eq!(
333 FunctionColumn::Description.id(),
334 "column.lambda.function.description"
335 );
336 assert_eq!(
337 FunctionColumn::PackageType.id(),
338 "column.lambda.function.package_type"
339 );
340 assert_eq!(
341 FunctionColumn::Runtime.id(),
342 "column.lambda.function.runtime"
343 );
344 assert_eq!(
345 FunctionColumn::Architecture.id(),
346 "column.lambda.function.architecture"
347 );
348 assert_eq!(
349 FunctionColumn::CodeSize.id(),
350 "column.lambda.function.code_size"
351 );
352 assert_eq!(
353 FunctionColumn::MemoryMb.id(),
354 "column.lambda.function.memory_mb"
355 );
356 assert_eq!(
357 FunctionColumn::TimeoutSeconds.id(),
358 "column.lambda.function.timeout_seconds"
359 );
360 assert_eq!(
361 FunctionColumn::LastModified.id(),
362 "column.lambda.function.last_modified"
363 );
364 }
365
366 #[test]
367 fn test_column_from_id() {
368 assert_eq!(
369 FunctionColumn::from_id("column.lambda.function.name"),
370 Some(FunctionColumn::Name)
371 );
372 assert_eq!(
373 FunctionColumn::from_id("column.lambda.function.runtime"),
374 Some(FunctionColumn::Runtime)
375 );
376 assert_eq!(FunctionColumn::from_id("invalid"), None);
377 }
378
379 #[test]
380 fn test_column_all_returns_ids() {
381 let all = FunctionColumn::ids();
382 assert_eq!(all.len(), 9);
383 assert!(all.contains(&"column.lambda.function.name"));
384 assert!(all.contains(&"column.lambda.function.runtime"));
385 assert!(all.contains(&"column.lambda.function.code_size"));
386 }
387
388 #[test]
389 fn test_column_visible_returns_ids() {
390 let visible = FunctionColumn::visible();
391 assert_eq!(visible.len(), 6);
392 assert!(visible.contains(&"column.lambda.function.name"));
393 assert!(visible.contains(&"column.lambda.function.runtime"));
394 assert!(!visible.contains(&"column.lambda.function.description"));
395 }
396
397 #[test]
398 fn test_application_column_id() {
399 assert_eq!(
400 ApplicationColumn::Name.id(),
401 "column.lambda.application.name"
402 );
403 assert_eq!(
404 ApplicationColumn::Description.id(),
405 "column.lambda.application.description"
406 );
407 assert_eq!(
408 ApplicationColumn::Status.id(),
409 "column.lambda.application.status"
410 );
411 assert_eq!(
412 ApplicationColumn::LastModified.id(),
413 "column.lambda.application.last_modified"
414 );
415 }
416
417 #[test]
418 fn test_application_column_from_id() {
419 assert_eq!(
420 ApplicationColumn::from_id("column.lambda.application.name"),
421 Some(ApplicationColumn::Name)
422 );
423 assert_eq!(
424 ApplicationColumn::from_id("column.lambda.application.status"),
425 Some(ApplicationColumn::Status)
426 );
427 assert_eq!(ApplicationColumn::from_id("invalid"), None);
428 }
429
430 #[test]
431 fn test_i18n_initialization() {
432 let mut i18n = std::collections::HashMap::new();
433 init(&mut i18n);
434 let name = crate::common::t("column.lambda.function.name");
437 assert!(!name.is_empty());
438 let nonexistent = crate::common::t("nonexistent.key");
439 assert_eq!(nonexistent, "nonexistent.key");
440 }
441
442 #[test]
443 fn test_column_width_uses_i18n() {
444 let mut i18n = std::collections::HashMap::new();
445 init(&mut i18n);
446 let col = FunctionColumn::Name;
447 let width = col.width();
448 assert!(width >= "Function name".len() as u16);
449 }
450}
451
452pub fn console_url_functions(region: &str) -> String {
453 format!(
454 "https://{}.console.aws.amazon.com/lambda/home?region={}#/functions",
455 region, region
456 )
457}
458
459pub fn console_url_function_detail(region: &str, function_name: &str) -> String {
460 format!(
461 "https://{}.console.aws.amazon.com/lambda/home?region={}#/functions/{}",
462 region, region, function_name
463 )
464}
465
466pub fn console_url_function_version(
467 region: &str,
468 function_name: &str,
469 version: &str,
470 detail_tab: &DetailTab,
471) -> String {
472 let tab = match detail_tab {
473 DetailTab::Code => "code",
474 DetailTab::Configuration => "configure",
475 _ => "code",
476 };
477 format!(
478 "https://{}.console.aws.amazon.com/lambda/home?region={}#/functions/{}/versions/{}?tab={}",
479 region, region, function_name, version, tab
480 )
481}
482
483pub fn console_url_applications(region: &str) -> String {
484 format!(
485 "https://{}.console.aws.amazon.com/lambda/home?region={}#/applications",
486 region, region
487 )
488}
489
490pub fn console_url_application_detail(
491 region: &str,
492 app_name: &str,
493 tab: &ApplicationDetailTab,
494) -> String {
495 let tab_param = match tab {
496 ApplicationDetailTab::Overview => "overview",
497 ApplicationDetailTab::Deployments => "deployments",
498 };
499 format!(
500 "https://{}.console.aws.amazon.com/lambda/home?region={}#/applications/{}?tab={}",
501 region, region, app_name, tab_param
502 )
503}
504
505#[derive(Debug, Clone)]
506pub struct Function {
507 pub name: String,
508 pub arn: String,
509 pub application: Option<String>,
510 pub description: String,
511 pub package_type: String,
512 pub runtime: String,
513 pub architecture: String,
514 pub code_size: i64,
515 pub code_sha256: String,
516 pub memory_mb: i32,
517 pub timeout_seconds: i32,
518 pub last_modified: String,
519 pub layers: Vec<Layer>,
520}
521
522#[derive(Debug, Clone)]
523pub struct Layer {
524 pub merge_order: String,
525 pub name: String,
526 pub layer_version: String,
527 pub compatible_runtimes: String,
528 pub compatible_architectures: String,
529 pub version_arn: String,
530}
531
532#[derive(Debug, Clone)]
533pub struct Version {
534 pub version: String,
535 pub aliases: String,
536 pub description: String,
537 pub last_modified: String,
538 pub architecture: String,
539}
540
541#[derive(Debug, Clone)]
542pub struct Alias {
543 pub name: String,
544 pub versions: String,
545 pub description: String,
546}
547
548#[derive(Debug, Clone)]
549pub struct Application {
550 pub name: String,
551 pub arn: String,
552 pub description: String,
553 pub status: String,
554 pub last_modified: String,
555}
556
557#[derive(Debug, Clone, Copy, PartialEq)]
558pub enum FunctionColumn {
559 Name,
560 Description,
561 PackageType,
562 Runtime,
563 Architecture,
564 CodeSize,
565 MemoryMb,
566 TimeoutSeconds,
567 LastModified,
568}
569
570impl FunctionColumn {
571 pub fn id(&self) -> ColumnId {
572 match self {
573 Self::Name => "column.lambda.function.name",
574 Self::Description => "column.lambda.function.description",
575 Self::PackageType => "column.lambda.function.package_type",
576 Self::Runtime => "column.lambda.function.runtime",
577 Self::Architecture => "column.lambda.function.architecture",
578 Self::CodeSize => "column.lambda.function.code_size",
579 Self::MemoryMb => "column.lambda.function.memory_mb",
580 Self::TimeoutSeconds => "column.lambda.function.timeout_seconds",
581 Self::LastModified => "column.lambda.function.last_modified",
582 }
583 }
584
585 pub fn default_name(&self) -> &'static str {
586 match self {
587 Self::Name => "Function name",
588 Self::Description => "Description",
589 Self::PackageType => "Package type",
590 Self::Runtime => "Runtime",
591 Self::Architecture => "Architecture",
592 Self::CodeSize => "Code size",
593 Self::MemoryMb => "Memory (MB)",
594 Self::TimeoutSeconds => "Timeout (s)",
595 Self::LastModified => "Last modified",
596 }
597 }
598
599 pub fn all() -> [Self; 9] {
600 [
601 Self::Name,
602 Self::Description,
603 Self::PackageType,
604 Self::Runtime,
605 Self::Architecture,
606 Self::CodeSize,
607 Self::MemoryMb,
608 Self::TimeoutSeconds,
609 Self::LastModified,
610 ]
611 }
612
613 pub fn ids() -> Vec<ColumnId> {
614 Self::all().iter().map(|c| c.id()).collect()
615 }
616
617 pub fn visible() -> Vec<ColumnId> {
618 [
619 Self::Name,
620 Self::Runtime,
621 Self::CodeSize,
622 Self::MemoryMb,
623 Self::TimeoutSeconds,
624 Self::LastModified,
625 ]
626 .iter()
627 .map(|c| c.id())
628 .collect()
629 }
630
631 pub fn from_id(id: ColumnId) -> Option<Self> {
632 match id {
633 "column.lambda.function.name" => Some(Self::Name),
634 "column.lambda.function.description" => Some(Self::Description),
635 "column.lambda.function.package_type" => Some(Self::PackageType),
636 "column.lambda.function.runtime" => Some(Self::Runtime),
637 "column.lambda.function.architecture" => Some(Self::Architecture),
638 "column.lambda.function.code_size" => Some(Self::CodeSize),
639 "column.lambda.function.memory_mb" => Some(Self::MemoryMb),
640 "column.lambda.function.timeout_seconds" => Some(Self::TimeoutSeconds),
641 "column.lambda.function.last_modified" => Some(Self::LastModified),
642 _ => None,
643 }
644 }
645}
646
647impl table::Column<Function> for FunctionColumn {
648 fn id(&self) -> &'static str {
649 Self::id(self)
650 }
651
652 fn default_name(&self) -> &'static str {
653 Self::default_name(self)
654 }
655
656 fn width(&self) -> u16 {
657 let translated = translate_column(self.id(), self.default_name());
658 translated.len().max(match self {
659 Self::Name => 30,
660 Self::Description => 40,
661 Self::Runtime => 20,
662 Self::LastModified => UTC_TIMESTAMP_WIDTH as usize,
663 _ => 0,
664 }) as u16
665 }
666
667 fn render(&self, item: &Function) -> (String, Style) {
668 let text = match self {
669 Self::Name => item.name.clone(),
670 Self::Description => item.description.clone(),
671 Self::PackageType => item.package_type.clone(),
672 Self::Runtime => format_runtime(&item.runtime),
673 Self::Architecture => format_architecture(&item.architecture),
674 Self::CodeSize => format_bytes(item.code_size),
675 Self::MemoryMb => item.memory_mb.to_string(),
676 Self::TimeoutSeconds => item.timeout_seconds.to_string(),
677 Self::LastModified => item.last_modified.clone(),
678 };
679 (text, Style::default())
680 }
681}
682
683#[derive(Debug, Clone, Copy, PartialEq)]
684pub enum ApplicationColumn {
685 Name,
686 Description,
687 Status,
688 LastModified,
689}
690
691impl ApplicationColumn {
692 pub fn id(&self) -> ColumnId {
693 match self {
694 Self::Name => "column.lambda.application.name",
695 Self::Description => "column.lambda.application.description",
696 Self::Status => "column.lambda.application.status",
697 Self::LastModified => "column.lambda.application.last_modified",
698 }
699 }
700
701 pub fn all() -> [Self; 4] {
702 [
703 Self::Name,
704 Self::Description,
705 Self::Status,
706 Self::LastModified,
707 ]
708 }
709
710 pub fn ids() -> Vec<ColumnId> {
711 Self::all().iter().map(|c| c.id()).collect()
712 }
713
714 pub fn visible() -> Vec<ColumnId> {
715 Self::ids()
716 }
717
718 pub fn from_id(id: ColumnId) -> Option<Self> {
719 match id {
720 "column.lambda.application.name" => Some(Self::Name),
721 "column.lambda.application.description" => Some(Self::Description),
722 "column.lambda.application.status" => Some(Self::Status),
723 "column.lambda.application.last_modified" => Some(Self::LastModified),
724 _ => None,
725 }
726 }
727
728 pub fn default_name(&self) -> &'static str {
729 match self {
730 Self::Name => "Name",
731 Self::Description => "Description",
732 Self::Status => "Status",
733 Self::LastModified => "Last modified",
734 }
735 }
736
737 pub fn name(&self) -> String {
738 translate_column(self.id(), self.default_name())
739 }
740}
741
742impl table::Column<Application> for ApplicationColumn {
743 fn id(&self) -> &'static str {
744 match self {
745 Self::Name => "column.lambda.application.name",
746 Self::Description => "column.lambda.application.description",
747 Self::Status => "column.lambda.application.status",
748 Self::LastModified => "column.lambda.application.last_modified",
749 }
750 }
751
752 fn default_name(&self) -> &'static str {
753 match self {
754 Self::Name => "Application name",
755 Self::Description => "Description",
756 Self::Status => "Status",
757 Self::LastModified => "Last modified",
758 }
759 }
760
761 fn width(&self) -> u16 {
762 self.name().len().max(match self {
763 Self::Name => 40,
764 Self::Description => 50,
765 Self::Status => 20,
766 Self::LastModified => UTC_TIMESTAMP_WIDTH as usize,
767 }) as u16
768 }
769
770 fn render(&self, item: &Application) -> (String, Style) {
771 match self {
772 Self::Name => (item.name.clone(), Style::default()),
773 Self::Description => (item.description.clone(), Style::default()),
774 Self::Status => {
775 let status_upper = item.status.to_uppercase();
776 let text = if status_upper.contains("COMPLETE") {
777 format!("✅ {}", item.status)
778 } else if status_upper == "UPDATE_IN_PROGRESS" {
779 format!("ℹ️ {}", item.status)
780 } else if status_upper.contains("ROLLBACK") || status_upper.contains("_FAILED") {
781 format!("❌ {}", item.status)
782 } else {
783 item.status.clone()
784 };
785 let color = if status_upper.contains("COMPLETE") {
786 Color::Green
787 } else if status_upper == "UPDATE_IN_PROGRESS" {
788 Color::LightBlue
789 } else if status_upper.contains("ROLLBACK") || status_upper.contains("_FAILED") {
790 Color::Red
791 } else {
792 Color::White
793 };
794 (text, Style::default().fg(color))
795 }
796 Self::LastModified => (item.last_modified.clone(), Style::default()),
797 }
798 }
799}
800
801#[derive(Debug, Clone, Copy, PartialEq)]
802pub enum VersionColumn {
803 Version,
804 Aliases,
805 Description,
806 LastModified,
807 Architecture,
808}
809
810impl VersionColumn {
811 pub fn name(&self) -> &'static str {
812 match self {
813 VersionColumn::Version => "Version",
814 VersionColumn::Aliases => "Aliases",
815 VersionColumn::Description => "Description",
816 VersionColumn::LastModified => "Last modified",
817 VersionColumn::Architecture => "Architecture",
818 }
819 }
820
821 pub fn all() -> [VersionColumn; 5] {
822 [
823 VersionColumn::Version,
824 VersionColumn::Aliases,
825 VersionColumn::Description,
826 VersionColumn::LastModified,
827 VersionColumn::Architecture,
828 ]
829 }
830
831 pub fn to_column(&self) -> Box<dyn table::Column<Version>> {
832 struct VersionCol {
833 variant: VersionColumn,
834 }
835
836 impl table::Column<Version> for VersionCol {
837 fn name(&self) -> &str {
838 self.variant.name()
839 }
840
841 fn width(&self) -> u16 {
842 match self.variant {
843 VersionColumn::Version => 10,
844 VersionColumn::Aliases => 20,
845 VersionColumn::Description => 40,
846 VersionColumn::LastModified => UTC_TIMESTAMP_WIDTH,
847 VersionColumn::Architecture => 15,
848 }
849 }
850
851 fn render(&self, item: &Version) -> (String, Style) {
852 let text = match self.variant {
853 VersionColumn::Version => item.version.clone(),
854 VersionColumn::Aliases => item.aliases.clone(),
855 VersionColumn::Description => item.description.clone(),
856 VersionColumn::LastModified => item.last_modified.clone(),
857 VersionColumn::Architecture => format_architecture(&item.architecture),
858 };
859 (text, Style::default())
860 }
861 }
862
863 Box::new(VersionCol { variant: *self })
864 }
865}
866
867#[derive(Debug, Clone, Copy, PartialEq)]
868pub enum AliasColumn {
869 Name,
870 Versions,
871 Description,
872}
873
874impl AliasColumn {
875 pub fn name(&self) -> &'static str {
876 match self {
877 AliasColumn::Name => "Name",
878 AliasColumn::Versions => "Versions",
879 AliasColumn::Description => "Description",
880 }
881 }
882
883 pub fn all() -> [AliasColumn; 3] {
884 [
885 AliasColumn::Name,
886 AliasColumn::Versions,
887 AliasColumn::Description,
888 ]
889 }
890
891 pub fn to_column(&self) -> Box<dyn table::Column<Alias>> {
892 struct AliasCol {
893 variant: AliasColumn,
894 }
895
896 impl table::Column<Alias> for AliasCol {
897 fn name(&self) -> &str {
898 self.variant.name()
899 }
900
901 fn width(&self) -> u16 {
902 match self.variant {
903 AliasColumn::Name => 20,
904 AliasColumn::Versions => 15,
905 AliasColumn::Description => 50,
906 }
907 }
908
909 fn render(&self, item: &Alias) -> (String, Style) {
910 let text = match self.variant {
911 AliasColumn::Name => item.name.clone(),
912 AliasColumn::Versions => item.versions.clone(),
913 AliasColumn::Description => item.description.clone(),
914 };
915 (text, Style::default())
916 }
917 }
918
919 Box::new(AliasCol { variant: *self })
920 }
921}
922
923#[derive(Debug, Clone, Copy, PartialEq)]
924pub enum LayerColumn {
925 MergeOrder,
926 Name,
927 LayerVersion,
928 CompatibleRuntimes,
929 CompatibleArchitectures,
930 VersionArn,
931}
932
933impl LayerColumn {
934 pub fn default_name(&self) -> &'static str {
935 match self {
936 LayerColumn::MergeOrder => "Merge order",
937 LayerColumn::Name => "Name",
938 LayerColumn::LayerVersion => "Layer version",
939 LayerColumn::CompatibleRuntimes => "Compatible runtimes",
940 LayerColumn::CompatibleArchitectures => "Compatible architectures",
941 LayerColumn::VersionArn => "Version ARN",
942 }
943 }
944
945 pub fn name(&self) -> String {
946 translate_column(self.id(), self.default_name())
947 }
948
949 pub fn id(&self) -> ColumnId {
950 match self {
951 Self::MergeOrder => "column.lambda.layer.merge_order",
952 Self::Name => "column.lambda.layer.name",
953 Self::LayerVersion => "column.lambda.layer.layer_version",
954 Self::CompatibleRuntimes => "column.lambda.layer.compatible_runtimes",
955 Self::CompatibleArchitectures => "column.lambda.layer.compatible_architectures",
956 Self::VersionArn => "column.lambda.layer.version_arn",
957 }
958 }
959
960 pub fn from_id(id: ColumnId) -> Option<Self> {
961 match id {
962 "column.lambda.layer.merge_order" => Some(Self::MergeOrder),
963 "column.lambda.layer.name" => Some(Self::Name),
964 "column.lambda.layer.layer_version" => Some(Self::LayerVersion),
965 "column.lambda.layer.compatible_runtimes" => Some(Self::CompatibleRuntimes),
966 "column.lambda.layer.compatible_architectures" => Some(Self::CompatibleArchitectures),
967 "column.lambda.layer.version_arn" => Some(Self::VersionArn),
968 _ => None,
969 }
970 }
971
972 pub fn all() -> [LayerColumn; 6] {
973 [
974 LayerColumn::MergeOrder,
975 LayerColumn::Name,
976 LayerColumn::LayerVersion,
977 LayerColumn::CompatibleRuntimes,
978 LayerColumn::CompatibleArchitectures,
979 LayerColumn::VersionArn,
980 ]
981 }
982
983 pub fn ids() -> Vec<ColumnId> {
984 Self::all().iter().map(|c| c.id()).collect()
985 }
986}
987
988impl table::Column<Layer> for LayerColumn {
989 fn id(&self) -> &'static str {
990 match self {
991 Self::MergeOrder => "column.lambda.layer.merge_order",
992 Self::Name => "column.lambda.layer.name",
993 Self::LayerVersion => "column.lambda.layer.layer_version",
994 Self::CompatibleRuntimes => "column.lambda.layer.compatible_runtimes",
995 Self::CompatibleArchitectures => "column.lambda.layer.compatible_architectures",
996 Self::VersionArn => "column.lambda.layer.version_arn",
997 }
998 }
999
1000 fn default_name(&self) -> &'static str {
1001 match self {
1002 Self::MergeOrder => "Merge order",
1003 Self::Name => "Layer name",
1004 Self::LayerVersion => "Version",
1005 Self::CompatibleRuntimes => "Compatible runtimes",
1006 Self::CompatibleArchitectures => "Compatible architectures",
1007 Self::VersionArn => "Version ARN",
1008 }
1009 }
1010
1011 fn width(&self) -> u16 {
1012 match self {
1013 Self::MergeOrder => 12,
1014 Self::Name => 20,
1015 Self::LayerVersion => 14,
1016 Self::CompatibleRuntimes => 20,
1017 Self::CompatibleArchitectures => 26,
1018 Self::VersionArn => 40,
1019 }
1020 }
1021
1022 fn render(&self, item: &Layer) -> (String, Style) {
1023 let text = match self {
1024 Self::MergeOrder => item.merge_order.clone(),
1025 Self::Name => item.name.clone(),
1026 Self::LayerVersion => item.layer_version.clone(),
1027 Self::CompatibleRuntimes => item.compatible_runtimes.clone(),
1028 Self::CompatibleArchitectures => item.compatible_architectures.clone(),
1029 Self::VersionArn => item.version_arn.clone(),
1030 };
1031 (text, Style::default())
1032 }
1033}
1034
1035#[derive(Debug, Clone, Copy, PartialEq)]
1036pub enum DeploymentColumn {
1037 Deployment,
1038 ResourceType,
1039 LastUpdated,
1040 Status,
1041}
1042
1043impl DeploymentColumn {
1044 pub fn id(&self) -> ColumnId {
1045 match self {
1046 Self::Deployment => "column.lambda.deployment.deployment",
1047 Self::ResourceType => "column.lambda.deployment.resource_type",
1048 Self::LastUpdated => "column.lambda.deployment.last_updated",
1049 Self::Status => "column.lambda.deployment.status",
1050 }
1051 }
1052
1053 pub fn default_name(&self) -> &'static str {
1054 match self {
1055 Self::Deployment => "Deployment",
1056 Self::ResourceType => "Resource type",
1057 Self::LastUpdated => "Last updated time",
1058 Self::Status => "Status",
1059 }
1060 }
1061
1062 pub fn name(&self) -> String {
1063 translate_column(self.id(), self.default_name())
1064 }
1065
1066 pub fn from_id(id: ColumnId) -> Option<Self> {
1067 match id {
1068 "column.lambda.deployment.deployment" => Some(Self::Deployment),
1069 "column.lambda.deployment.resource_type" => Some(Self::ResourceType),
1070 "column.lambda.deployment.last_updated" => Some(Self::LastUpdated),
1071 "column.lambda.deployment.status" => Some(Self::Status),
1072 _ => None,
1073 }
1074 }
1075
1076 pub fn all() -> [Self; 4] {
1077 [
1078 Self::Deployment,
1079 Self::ResourceType,
1080 Self::LastUpdated,
1081 Self::Status,
1082 ]
1083 }
1084
1085 pub fn ids() -> Vec<ColumnId> {
1086 Self::all().iter().map(|c| c.id()).collect()
1087 }
1088}
1089
1090impl table::Column<Deployment> for DeploymentColumn {
1091 fn id(&self) -> &'static str {
1092 match self {
1093 Self::Deployment => "column.lambda.deployment.deployment",
1094 Self::ResourceType => "column.lambda.deployment.resource_type",
1095 Self::LastUpdated => "column.lambda.deployment.last_updated",
1096 Self::Status => "column.lambda.deployment.status",
1097 }
1098 }
1099
1100 fn default_name(&self) -> &'static str {
1101 match self {
1102 Self::Deployment => "Deployment",
1103 Self::ResourceType => "Resource type",
1104 Self::LastUpdated => "Last updated",
1105 Self::Status => "Status",
1106 }
1107 }
1108
1109 fn width(&self) -> u16 {
1110 let translated = translate_column(self.id(), self.default_name());
1111 translated.len().max(match self {
1112 Self::Deployment => 30,
1113 Self::ResourceType => 20,
1114 Self::LastUpdated => UTC_TIMESTAMP_WIDTH as usize,
1115 Self::Status => 20,
1116 }) as u16
1117 }
1118
1119 fn render(&self, item: &Deployment) -> (String, Style) {
1120 match self {
1121 Self::Deployment => (item.deployment_id.clone(), Style::default()),
1122 Self::ResourceType => (item.resource_type.clone(), Style::default()),
1123 Self::LastUpdated => (item.last_updated.clone(), Style::default()),
1124 Self::Status => {
1125 if item.status == "Succeeded" {
1126 (
1127 format!("✅ {}", item.status),
1128 Style::default().fg(Color::Green),
1129 )
1130 } else {
1131 (item.status.clone(), Style::default())
1132 }
1133 }
1134 }
1135 }
1136}
1137#[derive(Clone, Debug)]
1138pub struct Resource {
1139 pub logical_id: String,
1140 pub physical_id: String,
1141 pub resource_type: String,
1142 pub last_modified: String,
1143}
1144
1145#[derive(Clone, Debug)]
1146pub struct Deployment {
1147 pub deployment_id: String,
1148 pub resource_type: String,
1149 pub last_updated: String,
1150 pub status: String,
1151}
1152
1153#[derive(Debug, Clone, Copy, PartialEq)]
1154pub enum ResourceColumn {
1155 LogicalId,
1156 PhysicalId,
1157 Type,
1158 LastModified,
1159}
1160
1161impl ResourceColumn {
1162 pub fn id(&self) -> ColumnId {
1163 match self {
1164 Self::LogicalId => "column.lambda.resource.logical_id",
1165 Self::PhysicalId => "column.lambda.resource.physical_id",
1166 Self::Type => "column.lambda.resource.type",
1167 Self::LastModified => "column.lambda.resource.last_modified",
1168 }
1169 }
1170
1171 pub fn default_name(&self) -> &'static str {
1172 match self {
1173 Self::LogicalId => "Logical ID",
1174 Self::PhysicalId => "Physical ID",
1175 Self::Type => "Type",
1176 Self::LastModified => "Last modified",
1177 }
1178 }
1179
1180 pub fn name(&self) -> String {
1181 translate_column(self.id(), self.default_name())
1182 }
1183
1184 pub fn from_id(id: ColumnId) -> Option<Self> {
1185 match id {
1186 "column.lambda.resource.logical_id" => Some(Self::LogicalId),
1187 "column.lambda.resource.physical_id" => Some(Self::PhysicalId),
1188 "column.lambda.resource.type" => Some(Self::Type),
1189 "column.lambda.resource.last_modified" => Some(Self::LastModified),
1190 _ => None,
1191 }
1192 }
1193
1194 pub fn all() -> [ResourceColumn; 4] {
1195 [
1196 Self::LogicalId,
1197 Self::PhysicalId,
1198 Self::Type,
1199 Self::LastModified,
1200 ]
1201 }
1202
1203 pub fn ids() -> Vec<ColumnId> {
1204 Self::all().iter().map(|c| c.id()).collect()
1205 }
1206}
1207
1208impl table::Column<Resource> for ResourceColumn {
1209 fn id(&self) -> &'static str {
1210 match self {
1211 Self::LogicalId => "column.lambda.resource.logical_id",
1212 Self::PhysicalId => "column.lambda.resource.physical_id",
1213 Self::Type => "column.lambda.resource.type",
1214 Self::LastModified => "column.lambda.resource.last_modified",
1215 }
1216 }
1217
1218 fn default_name(&self) -> &'static str {
1219 match self {
1220 Self::LogicalId => "Logical ID",
1221 Self::PhysicalId => "Physical ID",
1222 Self::Type => "Type",
1223 Self::LastModified => "Last Modified",
1224 }
1225 }
1226
1227 fn width(&self) -> u16 {
1228 match self {
1229 Self::LogicalId => 30,
1230 Self::PhysicalId => 40,
1231 Self::Type => 30,
1232 Self::LastModified => 27,
1233 }
1234 }
1235
1236 fn render(&self, item: &Resource) -> (String, Style) {
1237 let text = match self {
1238 Self::LogicalId => item.logical_id.clone(),
1239 Self::PhysicalId => item.physical_id.clone(),
1240 Self::Type => item.resource_type.clone(),
1241 Self::LastModified => item.last_modified.clone(),
1242 };
1243 (text, Style::default())
1244 }
1245}
1246
1247#[cfg(test)]
1248mod column_tests {
1249 use super::*;
1250
1251 #[test]
1252 fn test_function_column_id_returns_full_key() {
1253 let id = FunctionColumn::Name.id();
1254 assert_eq!(id, "column.lambda.function.name");
1255 assert!(id.starts_with("column."));
1256 }
1257
1258 #[test]
1259 fn test_application_column_id_returns_full_key() {
1260 let id = ApplicationColumn::Status.id();
1261 assert_eq!(id, "column.lambda.application.status");
1262 assert!(id.starts_with("column."));
1263 }
1264
1265 #[test]
1266 fn test_layer_column_id_returns_full_key() {
1267 let id = LayerColumn::Name.id();
1268 assert_eq!(id, "column.lambda.layer.name");
1269 assert!(id.starts_with("column."));
1270 }
1271
1272 #[test]
1273 fn test_deployment_column_id_returns_full_key() {
1274 let id = DeploymentColumn::Deployment.id();
1275 assert_eq!(id, "column.lambda.deployment.deployment");
1276 assert!(id.starts_with("column."));
1277 }
1278
1279 #[test]
1280 fn test_resource_column_id_returns_full_key() {
1281 let id = ResourceColumn::LogicalId.id();
1282 assert_eq!(id, "column.lambda.resource.logical_id");
1283 assert!(id.starts_with("column."));
1284 }
1285
1286 #[test]
1287 fn test_function_column_ids_have_correct_prefix() {
1288 for col in FunctionColumn::all() {
1289 assert!(
1290 col.id().starts_with("column.lambda.function."),
1291 "FunctionColumn ID '{}' should start with 'column.lambda.function.'",
1292 col.id()
1293 );
1294 }
1295 }
1296
1297 #[test]
1298 fn test_application_column_ids_have_correct_prefix() {
1299 for col in ApplicationColumn::all() {
1300 assert!(
1301 col.id().starts_with("column.lambda.application."),
1302 "ApplicationColumn ID '{}' should start with 'column.lambda.application.'",
1303 col.id()
1304 );
1305 }
1306 }
1307
1308 #[test]
1309 fn test_deployment_column_ids_have_correct_prefix() {
1310 for col in DeploymentColumn::all() {
1311 assert!(
1312 col.id().starts_with("column.lambda.deployment."),
1313 "DeploymentColumn ID '{}' should start with 'column.lambda.deployment.'",
1314 col.id()
1315 );
1316 }
1317 }
1318
1319 #[test]
1320 fn test_resource_column_ids_have_correct_prefix() {
1321 for col in ResourceColumn::all() {
1322 assert!(
1323 col.id().starts_with("column.lambda.resource."),
1324 "ResourceColumn ID '{}' should start with 'column.lambda.resource.'",
1325 col.id()
1326 );
1327 }
1328 }
1329
1330 #[test]
1331 fn test_resource_column_trait_id_and_name_do_not_panic() {
1332 use crate::ui::table::Column as TraitColumn;
1335 for col in ResourceColumn::all() {
1336 let _ = TraitColumn::id(&col);
1337 let _ = TraitColumn::default_name(&col);
1338 let _ = TraitColumn::name(&col);
1339 }
1340 }
1341
1342 #[test]
1343 fn test_deployment_column_trait_id_and_name_do_not_panic() {
1344 use crate::ui::table::Column as TraitColumn;
1345 for col in DeploymentColumn::all() {
1346 let _ = TraitColumn::id(&col);
1347 let _ = TraitColumn::default_name(&col);
1348 let _ = TraitColumn::name(&col);
1349 }
1350 }
1351}