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