1use crate::app::App;
2use crate::common::{
3 filter_by_fields, format_bytes, format_duration_seconds, format_memory_mb,
4 render_pagination_text, ColumnId, CyclicEnum, InputFocus, SortDirection,
5};
6use crate::keymap::Mode;
7use crate::lambda::{
8 format_architecture, format_runtime, parse_layer_arn, Alias, AliasColumn,
9 Application as LambdaApplication, ApplicationColumn, Deployment, DeploymentColumn,
10 Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer, LayerColumn, Resource,
11 ResourceColumn, Version, VersionColumn,
12};
13use crate::table::TableState;
14use crate::ui::filter::{render_simple_filter, SimpleFilterConfig};
15use crate::ui::monitoring::MonitoringState;
16use crate::ui::table::{
17 expanded_from_columns, plain_expanded_content, render_table, Column as TableColumn, TableConfig,
18};
19use crate::ui::{
20 calculate_dynamic_height, format_expansion_text, format_title, labeled_field,
21 render_fields_with_dynamic_columns, render_tabs, rounded_block, section_header, vertical,
22};
23use ratatui::{prelude::*, widgets::*};
24
25pub const FILTER_CONTROLS: [InputFocus; 2] = [InputFocus::Filter, InputFocus::Pagination];
26
27pub struct State {
28 pub table: TableState<LambdaFunction>,
29 pub current_function: Option<String>,
30 pub current_version: Option<String>,
31 pub current_alias: Option<String>,
32 pub detail_tab: DetailTab,
33 pub version_detail_tab: VersionDetailTab,
34 pub function_visible_column_ids: Vec<ColumnId>,
35 pub function_column_ids: Vec<ColumnId>,
36 pub version_table: TableState<Version>,
37 pub version_visible_column_ids: Vec<String>,
38 pub version_column_ids: Vec<String>,
39 pub alias_table: TableState<Alias>,
40 pub alias_visible_column_ids: Vec<String>,
41 pub alias_column_ids: Vec<String>,
42 pub layer_visible_column_ids: Vec<String>,
43 pub layer_column_ids: Vec<String>,
44 pub input_focus: InputFocus,
45 pub version_input_focus: InputFocus,
46 pub alias_input_focus: InputFocus,
47 pub layer_selected: usize,
48 pub layer_expanded: Option<usize>,
49 pub monitoring_scroll: usize,
50 pub metric_data_invocations: Vec<(i64, f64)>,
51 pub metric_data_duration_min: Vec<(i64, f64)>,
52 pub metric_data_duration_avg: Vec<(i64, f64)>,
53 pub metric_data_duration_max: Vec<(i64, f64)>,
54 pub metric_data_errors: Vec<(i64, f64)>,
55 pub metric_data_success_rate: Vec<(i64, f64)>,
56 pub metric_data_throttles: Vec<(i64, f64)>,
57 pub metric_data_concurrent_executions: Vec<(i64, f64)>,
58 pub metric_data_recursive_invocations_dropped: Vec<(i64, f64)>,
59 pub metric_data_async_event_age_min: Vec<(i64, f64)>,
60 pub metric_data_async_event_age_avg: Vec<(i64, f64)>,
61 pub metric_data_async_event_age_max: Vec<(i64, f64)>,
62 pub metric_data_async_events_received: Vec<(i64, f64)>,
63 pub metric_data_async_events_dropped: Vec<(i64, f64)>,
64 pub metric_data_destination_delivery_failures: Vec<(i64, f64)>,
65 pub metric_data_dead_letter_errors: Vec<(i64, f64)>,
66 pub metric_data_iterator_age: Vec<(i64, f64)>,
67 pub metrics_loading: bool,
68}
69
70impl Default for State {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl State {
77 pub fn new() -> Self {
78 Self {
79 table: TableState::new(),
80 current_function: None,
81 current_version: None,
82 current_alias: None,
83 detail_tab: DetailTab::Code,
84 version_detail_tab: VersionDetailTab::Code,
85 function_visible_column_ids: LambdaColumn::visible(),
86 function_column_ids: LambdaColumn::ids(),
87 version_table: TableState::new(),
88 version_visible_column_ids: VersionColumn::all()
89 .iter()
90 .map(|c| c.name().to_string())
91 .collect(),
92 version_column_ids: VersionColumn::all()
93 .iter()
94 .map(|c| c.name().to_string())
95 .collect(),
96 alias_table: TableState::new(),
97 alias_visible_column_ids: AliasColumn::all()
98 .iter()
99 .map(|c| c.name().to_string())
100 .collect(),
101 alias_column_ids: AliasColumn::all()
102 .iter()
103 .map(|c| c.name().to_string())
104 .collect(),
105 layer_visible_column_ids: LayerColumn::all()
106 .iter()
107 .map(|c| c.name().to_string())
108 .collect(),
109 layer_column_ids: LayerColumn::all()
110 .iter()
111 .map(|c| c.name().to_string())
112 .collect(),
113 input_focus: InputFocus::Filter,
114 version_input_focus: InputFocus::Filter,
115 alias_input_focus: InputFocus::Filter,
116 layer_selected: 0,
117 layer_expanded: None,
118 monitoring_scroll: 0,
119 metric_data_invocations: Vec::new(),
120 metric_data_duration_min: Vec::new(),
121 metric_data_duration_avg: Vec::new(),
122 metric_data_duration_max: Vec::new(),
123 metric_data_errors: Vec::new(),
124 metric_data_success_rate: Vec::new(),
125 metric_data_throttles: Vec::new(),
126 metric_data_concurrent_executions: Vec::new(),
127 metric_data_recursive_invocations_dropped: Vec::new(),
128 metric_data_async_event_age_min: Vec::new(),
129 metric_data_async_event_age_avg: Vec::new(),
130 metric_data_async_event_age_max: Vec::new(),
131 metric_data_async_events_received: Vec::new(),
132 metric_data_async_events_dropped: Vec::new(),
133 metric_data_destination_delivery_failures: Vec::new(),
134 metric_data_dead_letter_errors: Vec::new(),
135 metric_data_iterator_age: Vec::new(),
136 metrics_loading: false,
137 }
138 }
139}
140
141impl MonitoringState for State {
142 fn is_metrics_loading(&self) -> bool {
143 self.metrics_loading
144 }
145
146 fn set_metrics_loading(&mut self, loading: bool) {
147 self.metrics_loading = loading;
148 }
149
150 fn monitoring_scroll(&self) -> usize {
151 self.monitoring_scroll
152 }
153
154 fn set_monitoring_scroll(&mut self, scroll: usize) {
155 self.monitoring_scroll = scroll;
156 }
157
158 fn clear_metrics(&mut self) {
159 self.metric_data_invocations.clear();
160 self.metric_data_duration_min.clear();
161 self.metric_data_duration_avg.clear();
162 self.metric_data_duration_max.clear();
163 self.metric_data_errors.clear();
164 self.metric_data_success_rate.clear();
165 self.metric_data_throttles.clear();
166 self.metric_data_concurrent_executions.clear();
167 self.metric_data_recursive_invocations_dropped.clear();
168 self.metric_data_async_event_age_min.clear();
169 self.metric_data_async_event_age_avg.clear();
170 self.metric_data_async_event_age_max.clear();
171 self.metric_data_async_events_received.clear();
172 self.metric_data_async_events_dropped.clear();
173 self.metric_data_destination_delivery_failures.clear();
174 self.metric_data_dead_letter_errors.clear();
175 self.metric_data_iterator_age.clear();
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq)]
180pub enum DetailTab {
181 Code,
182 Monitor,
183 Configuration,
184 Aliases,
185 Versions,
186}
187
188impl CyclicEnum for DetailTab {
189 const ALL: &'static [Self] = &[
190 Self::Code,
191 Self::Monitor,
192 Self::Configuration,
193 Self::Aliases,
194 Self::Versions,
195 ];
196}
197
198impl DetailTab {
199 pub const VERSION_TABS: &'static [Self] = &[Self::Code, Self::Monitor, Self::Configuration];
200
201 pub fn name(&self) -> &'static str {
202 match self {
203 DetailTab::Code => "Code",
204 DetailTab::Monitor => "Monitor",
205 DetailTab::Configuration => "Configuration",
206 DetailTab::Aliases => "Aliases",
207 DetailTab::Versions => "Versions",
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq)]
213pub enum VersionDetailTab {
214 Code,
215 Monitor,
216 Configuration,
217}
218
219impl CyclicEnum for VersionDetailTab {
220 const ALL: &'static [Self] = &[Self::Code, Self::Monitor, Self::Configuration];
221}
222
223impl VersionDetailTab {
224 pub fn name(&self) -> &'static str {
225 match self {
226 VersionDetailTab::Code => "Code",
227 VersionDetailTab::Monitor => "Monitor",
228 VersionDetailTab::Configuration => "Configuration",
229 }
230 }
231
232 pub fn to_detail_tab(&self) -> DetailTab {
233 match self {
234 VersionDetailTab::Code => DetailTab::Code,
235 VersionDetailTab::Monitor => DetailTab::Monitor,
236 VersionDetailTab::Configuration => DetailTab::Configuration,
237 }
238 }
239
240 pub fn from_detail_tab(tab: DetailTab) -> Self {
241 match tab {
242 DetailTab::Code => VersionDetailTab::Code,
243 DetailTab::Monitor => VersionDetailTab::Monitor,
244 _ => VersionDetailTab::Configuration,
245 }
246 }
247}
248
249pub struct ApplicationState {
250 pub table: TableState<LambdaApplication>,
251 pub input_focus: InputFocus,
252 pub current_application: Option<String>,
253 pub detail_tab: ApplicationDetailTab,
254 pub deployments: TableState<Deployment>,
255 pub deployment_input_focus: InputFocus,
256 pub resources: TableState<Resource>,
257 pub resource_input_focus: InputFocus,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq)]
261pub enum ApplicationDetailTab {
262 Overview,
263 Deployments,
264}
265
266impl CyclicEnum for ApplicationDetailTab {
267 const ALL: &'static [Self] = &[Self::Overview, Self::Deployments];
268}
269
270impl ApplicationDetailTab {
271 pub fn name(&self) -> &'static str {
272 match self {
273 Self::Overview => "Overview",
274 Self::Deployments => "Deployments",
275 }
276 }
277}
278
279impl Default for ApplicationState {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285impl ApplicationState {
286 pub fn new() -> Self {
287 Self {
288 table: TableState::new(),
289 input_focus: InputFocus::Filter,
290 current_application: None,
291 detail_tab: ApplicationDetailTab::Overview,
292 deployments: TableState::new(),
293 deployment_input_focus: InputFocus::Filter,
294 resources: TableState::new(),
295 resource_input_focus: InputFocus::Filter,
296 }
297 }
298}
299
300pub fn render_functions(frame: &mut Frame, app: &App, area: Rect) {
301 frame.render_widget(Clear, area);
302
303 if app.lambda_state.current_alias.is_some() {
304 render_alias_detail(frame, app, area);
305 return;
306 }
307
308 if app.lambda_state.current_version.is_some() {
309 render_version_detail(frame, app, area);
310 return;
311 }
312
313 if app.lambda_state.current_function.is_some() {
314 render_detail(frame, app, area);
315 return;
316 }
317
318 let chunks = vertical(
319 [
320 Constraint::Length(3), Constraint::Min(0), ],
323 area,
324 );
325
326 let page_size = app.lambda_state.table.page_size.value();
328 let filtered_count: usize = app
329 .lambda_state
330 .table
331 .items
332 .iter()
333 .filter(|f| {
334 app.lambda_state.table.filter.is_empty()
335 || f.name
336 .to_lowercase()
337 .contains(&app.lambda_state.table.filter.to_lowercase())
338 || f.description
339 .to_lowercase()
340 .contains(&app.lambda_state.table.filter.to_lowercase())
341 || f.runtime
342 .to_lowercase()
343 .contains(&app.lambda_state.table.filter.to_lowercase())
344 })
345 .count();
346
347 let total_pages = filtered_count.div_ceil(page_size);
348 let current_page = app.lambda_state.table.selected / page_size;
349 let pagination = render_pagination_text(current_page, total_pages);
350
351 render_simple_filter(
352 frame,
353 chunks[0],
354 SimpleFilterConfig {
355 filter_text: &app.lambda_state.table.filter,
356 placeholder: "Filter by attributes or search by keyword",
357 pagination: &pagination,
358 mode: app.mode,
359 is_input_focused: app.lambda_state.input_focus == InputFocus::Filter,
360 is_pagination_focused: app.lambda_state.input_focus == InputFocus::Pagination,
361 },
362 );
363
364 let filtered: Vec<_> = app
366 .lambda_state
367 .table
368 .items
369 .iter()
370 .filter(|f| {
371 app.lambda_state.table.filter.is_empty()
372 || f.name
373 .to_lowercase()
374 .contains(&app.lambda_state.table.filter.to_lowercase())
375 || f.description
376 .to_lowercase()
377 .contains(&app.lambda_state.table.filter.to_lowercase())
378 || f.runtime
379 .to_lowercase()
380 .contains(&app.lambda_state.table.filter.to_lowercase())
381 })
382 .collect();
383
384 let start_idx = current_page * page_size;
385 let end_idx = (start_idx + page_size).min(filtered.len());
386 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
387
388 let title = format_title(&format!("Lambda functions ({})", filtered.len()));
389
390 let mut columns: Vec<Box<dyn TableColumn<LambdaFunction>>> = vec![];
391 for col_id in &app.lambda_state.function_visible_column_ids {
392 if let Some(column) = LambdaColumn::from_id(col_id) {
393 columns.push(Box::new(column));
394 }
395 }
396
397 let expanded_index = if let Some(expanded) = app.lambda_state.table.expanded_item {
398 if expanded >= start_idx && expanded < end_idx {
399 Some(expanded - start_idx)
400 } else {
401 None
402 }
403 } else {
404 None
405 };
406
407 let config = TableConfig {
408 items: paginated,
409 selected_index: app.lambda_state.table.selected % page_size,
410 expanded_index,
411 columns: &columns,
412 sort_column: "Last modified",
413 sort_direction: SortDirection::Desc,
414 title,
415 area: chunks[1],
416 get_expanded_content: Some(Box::new(|func: &LambdaFunction| {
417 expanded_from_columns(&columns, func)
418 })),
419 is_active: app.mode != Mode::FilterInput,
420 };
421
422 render_table(frame, config);
423}
424
425pub fn render_detail(frame: &mut Frame, app: &App, area: Rect) {
426 frame.render_widget(Clear, area);
427
428 let overview_lines = if let Some(func_name) = &app.lambda_state.current_function {
430 if let Some(func) = app
431 .lambda_state
432 .table
433 .items
434 .iter()
435 .find(|f| f.name == *func_name)
436 {
437 vec![
438 labeled_field(
439 "Description",
440 if func.description.is_empty() {
441 "-"
442 } else {
443 &func.description
444 },
445 ),
446 labeled_field("Last modified", &func.last_modified),
447 labeled_field("Function ARN", &func.arn),
448 labeled_field("Application", func.application.as_deref().unwrap_or("-")),
449 ]
450 } else {
451 vec![]
452 }
453 } else {
454 vec![]
455 };
456
457 let overview_height = if overview_lines.is_empty() {
458 0
459 } else {
460 calculate_dynamic_height(&overview_lines, area.width.saturating_sub(4)) + 2
461 };
462
463 let chunks = vertical(
464 [
465 Constraint::Length(overview_height),
466 Constraint::Length(1), Constraint::Min(0), ],
469 area,
470 );
471
472 if !overview_lines.is_empty() {
474 let overview_block = Block::default()
475 .title(format_title("Function overview"))
476 .borders(Borders::ALL)
477 .border_type(BorderType::Rounded)
478 .border_style(Style::default());
479
480 let overview_inner = overview_block.inner(chunks[0]);
481 frame.render_widget(overview_block, chunks[0]);
482 render_fields_with_dynamic_columns(frame, overview_inner, overview_lines);
483 }
484
485 let tabs: Vec<(&str, DetailTab)> = DetailTab::ALL
487 .iter()
488 .map(|tab| (tab.name(), *tab))
489 .collect();
490
491 render_tabs(frame, chunks[1], &tabs, &app.lambda_state.detail_tab);
492
493 if app.lambda_state.detail_tab == DetailTab::Code {
495 if let Some(func_name) = &app.lambda_state.current_function {
497 if let Some(func) = app
498 .lambda_state
499 .table
500 .items
501 .iter()
502 .find(|f| f.name == *func_name)
503 {
504 let code_lines = vec![
506 labeled_field("Package size", format_bytes(func.code_size)),
507 labeled_field("SHA256 hash", &func.code_sha256),
508 labeled_field("Last modified", &func.last_modified),
509 section_header(
510 "Encryption with AWS KMS customer managed KMS key",
511 chunks[2].width.saturating_sub(2),
512 ),
513 Line::from(Span::styled(
514 "To edit customer managed key encryption, you must upload a new .zip deployment package.",
515 Style::default().fg(Color::DarkGray),
516 )),
517 labeled_field("AWS KMS key ARN", ""),
518 labeled_field("Key alias", ""),
519 labeled_field("Status", ""),
520 ];
521
522 let runtime_lines = vec![
523 labeled_field("Runtime", format_runtime(&func.runtime)),
524 labeled_field("Handler", ""),
525 labeled_field("Architecture", format_architecture(&func.architecture)),
526 section_header(
527 "Runtime management configuration",
528 chunks[2].width.saturating_sub(2),
529 ),
530 labeled_field("Runtime version ARN", ""),
531 labeled_field("Update runtime version", "Auto"),
532 ];
533
534 let chunks_content = Layout::default()
535 .direction(Direction::Vertical)
536 .constraints([
537 Constraint::Length(
538 calculate_dynamic_height(
539 &code_lines,
540 chunks[2].width.saturating_sub(4),
541 ) + 2,
542 ),
543 Constraint::Length(
544 calculate_dynamic_height(
545 &runtime_lines,
546 chunks[2].width.saturating_sub(4),
547 ) + 2,
548 ),
549 Constraint::Min(0), ])
551 .split(chunks[2]);
552
553 let code_block = Block::default()
555 .title(format_title("Code properties"))
556 .borders(Borders::ALL)
557 .border_type(BorderType::Rounded);
558
559 let code_inner = code_block.inner(chunks_content[0]);
560 frame.render_widget(code_block, chunks_content[0]);
561
562 render_fields_with_dynamic_columns(frame, code_inner, code_lines);
563
564 let runtime_block = Block::default()
566 .title(format_title("Runtime settings"))
567 .borders(Borders::ALL)
568 .border_type(BorderType::Rounded);
569
570 let runtime_inner = runtime_block.inner(chunks_content[1]);
571 frame.render_widget(runtime_block, chunks_content[1]);
572
573 render_fields_with_dynamic_columns(frame, runtime_inner, runtime_lines);
574
575 let layer_refs: Vec<&Layer> = func.layers.iter().collect();
577 let title = format_title(&format!("Layers ({})", layer_refs.len()));
578
579 let columns: Vec<Box<dyn TableColumn<Layer>>> = vec![
580 Box::new(LayerColumn::MergeOrder),
581 Box::new(LayerColumn::Name),
582 Box::new(LayerColumn::LayerVersion),
583 Box::new(LayerColumn::CompatibleRuntimes),
584 Box::new(LayerColumn::CompatibleArchitectures),
585 Box::new(LayerColumn::VersionArn),
586 ];
587
588 let config = TableConfig {
589 items: layer_refs,
590 selected_index: app.lambda_state.layer_selected,
591 expanded_index: app.lambda_state.layer_expanded,
592 columns: &columns,
593 sort_column: "",
594 sort_direction: SortDirection::Asc,
595 title,
596 area: chunks_content[2],
597 get_expanded_content: Some(Box::new(|layer: &Layer| {
598 format_expansion_text(&[
599 ("Merge order", layer.merge_order.clone()),
600 ("Name", layer.name.clone()),
601 ("Layer version", layer.layer_version.clone()),
602 ("Compatible runtimes", layer.compatible_runtimes.clone()),
603 (
604 "Compatible architectures",
605 layer.compatible_architectures.clone(),
606 ),
607 ("Version ARN", layer.version_arn.clone()),
608 ])
609 })),
610 is_active: app.lambda_state.detail_tab == DetailTab::Code,
611 };
612
613 render_table(frame, config);
614 }
615 }
616 } else if app.lambda_state.detail_tab == DetailTab::Monitor {
617 render_lambda_monitoring_charts(frame, app, chunks[2]);
618 } else if app.lambda_state.detail_tab == DetailTab::Configuration {
619 if let Some(func_name) = &app.lambda_state.current_function {
621 if let Some(func) = app
622 .lambda_state
623 .table
624 .items
625 .iter()
626 .find(|f| f.name == *func_name)
627 {
628 let config_lines = vec![
629 labeled_field("Description", &func.description),
630 labeled_field("Revision", &func.last_modified),
631 labeled_field("Memory", format_memory_mb(func.memory_mb)),
632 labeled_field("Ephemeral storage", format_memory_mb(512)),
633 labeled_field("Timeout", format_duration_seconds(func.timeout_seconds)),
634 labeled_field("SnapStart", "None"),
635 ];
636
637 let config_chunks = vertical(
638 [
639 Constraint::Length(
640 calculate_dynamic_height(
641 &config_lines,
642 chunks[2].width.saturating_sub(4),
643 ) + 2,
644 ),
645 Constraint::Min(0),
646 ],
647 chunks[2],
648 );
649
650 let config_block = Block::default()
651 .title(format_title("General configuration"))
652 .borders(Borders::ALL)
653 .border_type(BorderType::Rounded)
654 .border_style(Style::default());
655
656 let config_inner = config_block.inner(config_chunks[0]);
657 frame.render_widget(config_block, config_chunks[0]);
658
659 render_fields_with_dynamic_columns(frame, config_inner, config_lines);
660 }
661 }
662 } else if app.lambda_state.detail_tab == DetailTab::Versions {
663 let version_chunks = vertical(
665 [
666 Constraint::Length(3), Constraint::Min(0), ],
669 chunks[2],
670 );
671
672 let page_size = app.lambda_state.version_table.page_size.value();
674 let filtered_count: usize = app
675 .lambda_state
676 .version_table
677 .items
678 .iter()
679 .filter(|v| {
680 app.lambda_state.version_table.filter.is_empty()
681 || v.version
682 .to_lowercase()
683 .contains(&app.lambda_state.version_table.filter.to_lowercase())
684 || v.aliases
685 .to_lowercase()
686 .contains(&app.lambda_state.version_table.filter.to_lowercase())
687 || v.description
688 .to_lowercase()
689 .contains(&app.lambda_state.version_table.filter.to_lowercase())
690 })
691 .count();
692
693 let total_pages = filtered_count.div_ceil(page_size);
694 let current_page = app.lambda_state.version_table.selected / page_size;
695 let pagination = render_pagination_text(current_page, total_pages);
696
697 render_simple_filter(
698 frame,
699 version_chunks[0],
700 SimpleFilterConfig {
701 filter_text: &app.lambda_state.version_table.filter,
702 placeholder: "Filter by attributes or search by keyword",
703 pagination: &pagination,
704 mode: app.mode,
705 is_input_focused: app.lambda_state.version_input_focus == InputFocus::Filter,
706 is_pagination_focused: app.lambda_state.version_input_focus
707 == InputFocus::Pagination,
708 },
709 );
710
711 let filtered: Vec<_> = app
713 .lambda_state
714 .version_table
715 .items
716 .iter()
717 .filter(|v| {
718 app.lambda_state.version_table.filter.is_empty()
719 || v.version
720 .to_lowercase()
721 .contains(&app.lambda_state.version_table.filter.to_lowercase())
722 || v.aliases
723 .to_lowercase()
724 .contains(&app.lambda_state.version_table.filter.to_lowercase())
725 || v.description
726 .to_lowercase()
727 .contains(&app.lambda_state.version_table.filter.to_lowercase())
728 })
729 .collect();
730
731 let start_idx = current_page * page_size;
732 let end_idx = (start_idx + page_size).min(filtered.len());
733 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
734
735 let title = format_title(&format!("Versions ({})", filtered.len()));
736
737 let mut columns: Vec<Box<dyn TableColumn<Version>>> = vec![];
738 for col_name in &app.lambda_state.version_visible_column_ids {
739 let column = match col_name.as_str() {
740 "Version" => Some(VersionColumn::Version),
741 "Aliases" => Some(VersionColumn::Aliases),
742 "Description" => Some(VersionColumn::Description),
743 "Last modified" => Some(VersionColumn::LastModified),
744 "Architecture" => Some(VersionColumn::Architecture),
745 _ => None,
746 };
747 if let Some(c) = column {
748 columns.push(c.to_column());
749 }
750 }
751
752 let expanded_index = if let Some(expanded) = app.lambda_state.version_table.expanded_item {
753 if expanded >= start_idx && expanded < end_idx {
754 Some(expanded - start_idx)
755 } else {
756 None
757 }
758 } else {
759 None
760 };
761
762 let config = TableConfig {
763 items: paginated,
764 selected_index: app.lambda_state.version_table.selected % page_size,
765 expanded_index,
766 columns: &columns,
767 sort_column: "Version",
768 sort_direction: SortDirection::Desc,
769 title,
770 area: version_chunks[1],
771 get_expanded_content: Some(Box::new(|ver: &Version| {
772 expanded_from_columns(&columns, ver)
773 })),
774 is_active: app.mode != Mode::FilterInput,
775 };
776
777 render_table(frame, config);
778 } else if app.lambda_state.detail_tab == DetailTab::Aliases {
779 let alias_chunks = vertical(
781 [
782 Constraint::Length(3), Constraint::Min(0), ],
785 chunks[2],
786 );
787
788 let page_size = app.lambda_state.alias_table.page_size.value();
790 let filtered_count: usize = app
791 .lambda_state
792 .alias_table
793 .items
794 .iter()
795 .filter(|a| {
796 app.lambda_state.alias_table.filter.is_empty()
797 || a.name
798 .to_lowercase()
799 .contains(&app.lambda_state.alias_table.filter.to_lowercase())
800 || a.versions
801 .to_lowercase()
802 .contains(&app.lambda_state.alias_table.filter.to_lowercase())
803 || a.description
804 .to_lowercase()
805 .contains(&app.lambda_state.alias_table.filter.to_lowercase())
806 })
807 .count();
808
809 let total_pages = filtered_count.div_ceil(page_size);
810 let current_page = app.lambda_state.alias_table.selected / page_size;
811 let pagination = render_pagination_text(current_page, total_pages);
812
813 render_simple_filter(
814 frame,
815 alias_chunks[0],
816 SimpleFilterConfig {
817 filter_text: &app.lambda_state.alias_table.filter,
818 placeholder: "Filter by attributes or search by keyword",
819 pagination: &pagination,
820 mode: app.mode,
821 is_input_focused: app.lambda_state.alias_input_focus == InputFocus::Filter,
822 is_pagination_focused: app.lambda_state.alias_input_focus == InputFocus::Pagination,
823 },
824 );
825
826 let filtered: Vec<_> = app
828 .lambda_state
829 .alias_table
830 .items
831 .iter()
832 .filter(|a| {
833 app.lambda_state.alias_table.filter.is_empty()
834 || a.name
835 .to_lowercase()
836 .contains(&app.lambda_state.alias_table.filter.to_lowercase())
837 || a.versions
838 .to_lowercase()
839 .contains(&app.lambda_state.alias_table.filter.to_lowercase())
840 || a.description
841 .to_lowercase()
842 .contains(&app.lambda_state.alias_table.filter.to_lowercase())
843 })
844 .collect();
845
846 let start_idx = current_page * page_size;
847 let end_idx = (start_idx + page_size).min(filtered.len());
848 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
849
850 let title = format_title(&format!("Aliases ({})", filtered.len()));
851
852 let mut columns: Vec<Box<dyn TableColumn<Alias>>> = vec![];
853 for col_name in &app.lambda_state.alias_visible_column_ids {
854 let column = match col_name.as_str() {
855 "Name" => Some(AliasColumn::Name),
856 "Versions" => Some(AliasColumn::Versions),
857 "Description" => Some(AliasColumn::Description),
858 _ => None,
859 };
860 if let Some(c) = column {
861 columns.push(c.to_column());
862 }
863 }
864
865 let expanded_index = if let Some(expanded) = app.lambda_state.alias_table.expanded_item {
866 if expanded >= start_idx && expanded < end_idx {
867 Some(expanded - start_idx)
868 } else {
869 None
870 }
871 } else {
872 None
873 };
874
875 let config = TableConfig {
876 items: paginated,
877 selected_index: app.lambda_state.alias_table.selected % page_size,
878 expanded_index,
879 columns: &columns,
880 sort_column: "Name",
881 sort_direction: SortDirection::Asc,
882 title,
883 area: alias_chunks[1],
884 get_expanded_content: Some(Box::new(|alias: &Alias| {
885 expanded_from_columns(&columns, alias)
886 })),
887 is_active: app.mode != Mode::FilterInput,
888 };
889
890 render_table(frame, config);
891 } else {
892 let content = Paragraph::new(format!(
894 "{} tab content (coming soon)",
895 app.lambda_state.detail_tab.name()
896 ))
897 .block(rounded_block());
898 frame.render_widget(content, chunks[2]);
899 }
900}
901
902pub fn render_alias_detail(frame: &mut Frame, app: &App, area: Rect) {
903 frame.render_widget(Clear, area);
904
905 let mut overview_lines = vec![];
907 if let Some(func_name) = &app.lambda_state.current_function {
908 if let Some(func) = app
909 .lambda_state
910 .table
911 .items
912 .iter()
913 .find(|f| f.name == *func_name)
914 {
915 if let Some(alias_name) = &app.lambda_state.current_alias {
916 if let Some(alias) = app
917 .lambda_state
918 .alias_table
919 .items
920 .iter()
921 .find(|a| a.name == *alias_name)
922 {
923 overview_lines.push(labeled_field("Description", &alias.description));
924
925 let versions_parts: Vec<&str> =
927 alias.versions.split(',').map(|s| s.trim()).collect();
928 if let Some(first_version) = versions_parts.first() {
929 overview_lines.push(labeled_field("Version", *first_version));
930 }
931 if versions_parts.len() > 1 {
932 if let Some(second_version) = versions_parts.get(1) {
933 overview_lines
934 .push(labeled_field("Additional version", *second_version));
935 }
936 }
937
938 overview_lines.push(labeled_field("Function ARN", &func.arn));
939
940 if let Some(app) = &func.application {
941 overview_lines.push(labeled_field("Application", app));
942 }
943
944 overview_lines.push(labeled_field("Function URL", "-"));
945 }
946 }
947 }
948 }
949
950 let mut config_lines = vec![];
952 if let Some(_func_name) = &app.lambda_state.current_function {
953 if let Some(alias_name) = &app.lambda_state.current_alias {
954 if let Some(alias) = app
955 .lambda_state
956 .alias_table
957 .items
958 .iter()
959 .find(|a| a.name == *alias_name)
960 {
961 config_lines.push(labeled_field("Name", &alias.name));
962 config_lines.push(labeled_field("Description", &alias.description));
963
964 let versions_parts: Vec<&str> =
966 alias.versions.split(',').map(|s| s.trim()).collect();
967 if let Some(first_version) = versions_parts.first() {
968 config_lines.push(labeled_field("Version", *first_version));
969 }
970 if versions_parts.len() > 1 {
971 if let Some(second_version) = versions_parts.get(1) {
972 config_lines.push(labeled_field("Additional version", *second_version));
973 }
974 }
975 }
976 }
977 }
978
979 let config_height = if config_lines.is_empty() {
980 0
981 } else {
982 calculate_dynamic_height(&config_lines, area.width.saturating_sub(4)) + 2
983 };
984
985 let overview_height =
986 calculate_dynamic_height(&overview_lines, area.width.saturating_sub(4)) + 2;
987
988 let chunks = vertical(
989 [
990 Constraint::Length(overview_height),
991 Constraint::Length(config_height),
992 Constraint::Min(0), ],
994 area,
995 );
996
997 if let Some(func_name) = &app.lambda_state.current_function {
999 if let Some(_func) = app
1000 .lambda_state
1001 .table
1002 .items
1003 .iter()
1004 .find(|f| f.name == *func_name)
1005 {
1006 if let Some(alias_name) = &app.lambda_state.current_alias {
1007 if let Some(_alias) = app
1008 .lambda_state
1009 .alias_table
1010 .items
1011 .iter()
1012 .find(|a| a.name == *alias_name)
1013 {
1014 let overview_block = Block::default()
1015 .title(format_title("Function overview"))
1016 .borders(Borders::ALL)
1017 .border_type(BorderType::Rounded)
1018 .border_style(Style::default());
1019
1020 let overview_inner = overview_block.inner(chunks[0]);
1021 frame.render_widget(overview_block, chunks[0]);
1022
1023 render_fields_with_dynamic_columns(frame, overview_inner, overview_lines);
1024 }
1025 }
1026 }
1027 }
1028
1029 if !config_lines.is_empty() {
1031 let config_block = Block::default()
1032 .title(format_title("General configuration"))
1033 .borders(Borders::ALL)
1034 .border_type(BorderType::Rounded);
1035
1036 let config_inner = config_block.inner(chunks[1]);
1037 frame.render_widget(config_block, chunks[1]);
1038 render_fields_with_dynamic_columns(frame, config_inner, config_lines);
1039 }
1040}
1041
1042pub fn render_version_detail(frame: &mut Frame, app: &App, area: Rect) {
1043 frame.render_widget(Clear, area);
1044
1045 let mut overview_lines = vec![];
1047 if let Some(func_name) = &app.lambda_state.current_function {
1048 if let Some(func) = app
1049 .lambda_state
1050 .table
1051 .items
1052 .iter()
1053 .find(|f| f.name == *func_name)
1054 {
1055 if let Some(version_num) = &app.lambda_state.current_version {
1056 let version_arn = format!("{}:{}", func.arn, version_num);
1057
1058 overview_lines.push(labeled_field("Name", &func.name));
1059
1060 if let Some(app) = &func.application {
1061 overview_lines.push(labeled_field("Application", app));
1062 }
1063
1064 overview_lines.extend(vec![
1065 labeled_field("ARN", version_arn),
1066 labeled_field("Version", version_num),
1067 ]);
1068 }
1069 }
1070 }
1071
1072 let overview_height = if overview_lines.is_empty() {
1073 0
1074 } else {
1075 calculate_dynamic_height(&overview_lines, area.width.saturating_sub(4)) + 2
1076 };
1077
1078 let chunks = vertical(
1079 [
1080 Constraint::Length(overview_height),
1081 Constraint::Length(1), Constraint::Min(0), ],
1084 area,
1085 );
1086
1087 if !overview_lines.is_empty() {
1089 let overview_block = Block::default()
1090 .title(format_title("Function overview"))
1091 .borders(Borders::ALL)
1092 .border_type(BorderType::Rounded)
1093 .border_style(Style::default());
1094
1095 let overview_inner = overview_block.inner(chunks[0]);
1096 frame.render_widget(overview_block, chunks[0]);
1097 render_fields_with_dynamic_columns(frame, overview_inner, overview_lines);
1098 }
1099
1100 let tabs: Vec<(&str, VersionDetailTab)> = VersionDetailTab::ALL
1102 .iter()
1103 .map(|tab| (tab.name(), *tab))
1104 .collect();
1105
1106 render_tabs(
1107 frame,
1108 chunks[1],
1109 &tabs,
1110 &app.lambda_state.version_detail_tab,
1111 );
1112
1113 if app.lambda_state.detail_tab == DetailTab::Code {
1115 if let Some(func_name) = &app.lambda_state.current_function {
1116 if let Some(func) = app
1117 .lambda_state
1118 .table
1119 .items
1120 .iter()
1121 .find(|f| f.name == *func_name)
1122 {
1123 let code_lines = vec![
1125 labeled_field("Package size", format_bytes(func.code_size)),
1126 labeled_field("SHA256 hash", &func.code_sha256),
1127 labeled_field("Last modified", &func.last_modified),
1128 ];
1129
1130 let runtime_lines = vec![
1131 labeled_field("Runtime", format_runtime(&func.runtime)),
1132 labeled_field("Handler", ""),
1133 labeled_field("Architecture", format_architecture(&func.architecture)),
1134 ];
1135
1136 let chunks_content = Layout::default()
1137 .direction(Direction::Vertical)
1138 .constraints([
1139 Constraint::Length(
1140 calculate_dynamic_height(
1141 &code_lines,
1142 chunks[2].width.saturating_sub(4),
1143 ) + 2,
1144 ),
1145 Constraint::Length(
1146 calculate_dynamic_height(
1147 &runtime_lines,
1148 chunks[2].width.saturating_sub(4),
1149 ) + 2,
1150 ),
1151 Constraint::Min(0),
1152 ])
1153 .split(chunks[2]);
1154
1155 let code_block = Block::default()
1157 .title(format_title("Code properties"))
1158 .borders(Borders::ALL)
1159 .border_type(BorderType::Rounded);
1160
1161 let code_inner = code_block.inner(chunks_content[0]);
1162 frame.render_widget(code_block, chunks_content[0]);
1163
1164 render_fields_with_dynamic_columns(frame, code_inner, code_lines);
1165
1166 let runtime_block = Block::default()
1168 .title(format_title("Runtime settings"))
1169 .borders(Borders::ALL)
1170 .border_type(BorderType::Rounded);
1171
1172 let runtime_inner = runtime_block.inner(chunks_content[1]);
1173 frame.render_widget(runtime_block, chunks_content[1]);
1174
1175 render_fields_with_dynamic_columns(frame, runtime_inner, runtime_lines);
1176
1177 let layers: Vec<Layer> = vec![];
1179 let layer_refs: Vec<&Layer> = layers.iter().collect();
1180 let title = format_title(&format!("Layers ({})", layer_refs.len()));
1181
1182 let columns: Vec<Box<dyn TableColumn<Layer>>> = vec![
1183 Box::new(LayerColumn::MergeOrder),
1184 Box::new(LayerColumn::Name),
1185 Box::new(LayerColumn::LayerVersion),
1186 Box::new(LayerColumn::CompatibleRuntimes),
1187 Box::new(LayerColumn::CompatibleArchitectures),
1188 Box::new(LayerColumn::VersionArn),
1189 ];
1190
1191 let config = TableConfig {
1192 items: layer_refs,
1193 selected_index: 0,
1194 expanded_index: None,
1195 columns: &columns,
1196 sort_column: "",
1197 sort_direction: SortDirection::Asc,
1198 title,
1199 area: chunks_content[2],
1200 get_expanded_content: Some(Box::new(|layer: &Layer| {
1201 format_expansion_text(&[
1202 ("Merge order", layer.merge_order.clone()),
1203 ("Name", layer.name.clone()),
1204 ("Layer version", layer.layer_version.clone()),
1205 ("Compatible runtimes", layer.compatible_runtimes.clone()),
1206 (
1207 "Compatible architectures",
1208 layer.compatible_architectures.clone(),
1209 ),
1210 ("Version ARN", layer.version_arn.clone()),
1211 ])
1212 })),
1213 is_active: app.lambda_state.detail_tab == DetailTab::Code,
1214 };
1215
1216 render_table(frame, config);
1217 }
1218 }
1219 } else if app.lambda_state.detail_tab == DetailTab::Monitor {
1220 if app.lambda_state.metrics_loading {
1222 let loading_block = Block::default()
1223 .title(format_title("Monitor"))
1224 .borders(Borders::ALL)
1225 .border_type(BorderType::Rounded);
1226 let loading_text = Paragraph::new("Loading metrics...")
1227 .block(loading_block)
1228 .alignment(ratatui::layout::Alignment::Center);
1229 frame.render_widget(loading_text, chunks[2]);
1230 return;
1231 }
1232
1233 render_lambda_monitoring_charts(frame, app, chunks[2]);
1235 } else if app.lambda_state.detail_tab == DetailTab::Configuration {
1236 if let Some(func_name) = &app.lambda_state.current_function {
1237 if let Some(func) = app
1238 .lambda_state
1239 .table
1240 .items
1241 .iter()
1242 .find(|f| f.name == *func_name)
1243 {
1244 if let Some(version_num) = &app.lambda_state.current_version {
1245 let config_lines = vec![
1247 labeled_field("Description", &func.description),
1248 labeled_field("Memory", format_memory_mb(func.memory_mb)),
1249 labeled_field("Timeout", format_duration_seconds(func.timeout_seconds)),
1250 ];
1251
1252 let chunks_content = Layout::default()
1253 .direction(Direction::Vertical)
1254 .constraints([
1255 Constraint::Length(
1256 calculate_dynamic_height(
1257 &config_lines,
1258 chunks[2].width.saturating_sub(4),
1259 ) + 2,
1260 ),
1261 Constraint::Length(3), Constraint::Min(0), ])
1264 .split(chunks[2]);
1265
1266 let config_block = Block::default()
1267 .title(format_title("General configuration"))
1268 .borders(Borders::ALL)
1269 .border_type(BorderType::Rounded)
1270 .border_style(Style::default());
1271
1272 let config_inner = config_block.inner(chunks_content[0]);
1273 frame.render_widget(config_block, chunks_content[0]);
1274
1275 render_fields_with_dynamic_columns(frame, config_inner, config_lines);
1276
1277 let page_size = app.lambda_state.alias_table.page_size.value();
1279 let filtered_count: usize = app
1280 .lambda_state
1281 .alias_table
1282 .items
1283 .iter()
1284 .filter(|a| {
1285 a.versions.contains(version_num)
1286 && (app.lambda_state.alias_table.filter.is_empty()
1287 || a.name.to_lowercase().contains(
1288 &app.lambda_state.alias_table.filter.to_lowercase(),
1289 )
1290 || a.versions.to_lowercase().contains(
1291 &app.lambda_state.alias_table.filter.to_lowercase(),
1292 )
1293 || a.description.to_lowercase().contains(
1294 &app.lambda_state.alias_table.filter.to_lowercase(),
1295 ))
1296 })
1297 .count();
1298
1299 let total_pages = filtered_count.div_ceil(page_size);
1300 let current_page = app.lambda_state.alias_table.selected / page_size;
1301 let pagination = render_pagination_text(current_page, total_pages);
1302
1303 render_simple_filter(
1304 frame,
1305 chunks_content[1],
1306 SimpleFilterConfig {
1307 filter_text: &app.lambda_state.alias_table.filter,
1308 placeholder: "Filter by attributes or search by keyword",
1309 pagination: &pagination,
1310 mode: app.mode,
1311 is_input_focused: app.lambda_state.alias_input_focus
1312 == InputFocus::Filter,
1313 is_pagination_focused: app.lambda_state.alias_input_focus
1314 == InputFocus::Pagination,
1315 },
1316 );
1317
1318 let filtered: Vec<_> = app
1320 .lambda_state
1321 .alias_table
1322 .items
1323 .iter()
1324 .filter(|a| {
1325 a.versions.contains(version_num)
1326 && (app.lambda_state.alias_table.filter.is_empty()
1327 || a.name.to_lowercase().contains(
1328 &app.lambda_state.alias_table.filter.to_lowercase(),
1329 )
1330 || a.versions.to_lowercase().contains(
1331 &app.lambda_state.alias_table.filter.to_lowercase(),
1332 )
1333 || a.description.to_lowercase().contains(
1334 &app.lambda_state.alias_table.filter.to_lowercase(),
1335 ))
1336 })
1337 .collect();
1338
1339 let start_idx = current_page * page_size;
1340 let end_idx = (start_idx + page_size).min(filtered.len());
1341 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1342
1343 let title = format_title(&format!("Aliases ({})", filtered.len()));
1344
1345 let mut columns: Vec<Box<dyn TableColumn<Alias>>> = vec![];
1346 for col_name in &app.lambda_state.alias_visible_column_ids {
1347 let column = match col_name.as_str() {
1348 "Name" => Some(AliasColumn::Name),
1349 "Versions" => Some(AliasColumn::Versions),
1350 "Description" => Some(AliasColumn::Description),
1351 _ => None,
1352 };
1353 if let Some(c) = column {
1354 columns.push(c.to_column());
1355 }
1356 }
1357
1358 let expanded_index =
1359 if let Some(expanded) = app.lambda_state.alias_table.expanded_item {
1360 if expanded >= start_idx && expanded < end_idx {
1361 Some(expanded - start_idx)
1362 } else {
1363 None
1364 }
1365 } else {
1366 None
1367 };
1368
1369 let config = TableConfig {
1370 items: paginated,
1371 selected_index: app.lambda_state.alias_table.selected % page_size,
1372 expanded_index,
1373 columns: &columns,
1374 sort_column: "Name",
1375 sort_direction: SortDirection::Asc,
1376 title,
1377 area: chunks_content[2],
1378 get_expanded_content: Some(Box::new(|alias: &Alias| {
1379 expanded_from_columns(&columns, alias)
1380 })),
1381 is_active: app.mode != Mode::FilterInput,
1382 };
1383
1384 render_table(frame, config);
1385 }
1386 }
1387 }
1388 }
1389}
1390
1391pub fn render_applications(frame: &mut Frame, app: &App, area: Rect) {
1392 frame.render_widget(Clear, area);
1393
1394 if app.lambda_application_state.current_application.is_some() {
1395 render_application_detail(frame, app, area);
1396 return;
1397 }
1398
1399 let chunks = Layout::default()
1400 .direction(Direction::Vertical)
1401 .constraints([Constraint::Length(3), Constraint::Min(0)])
1402 .split(area);
1403
1404 let page_size = app.lambda_application_state.table.page_size.value();
1406 let filtered_count = filtered_lambda_applications(app).len();
1407 let total_pages = filtered_count.div_ceil(page_size);
1408 let current_page = app.lambda_application_state.table.selected / page_size;
1409 let pagination = render_pagination_text(current_page, total_pages);
1410
1411 render_simple_filter(
1412 frame,
1413 chunks[0],
1414 SimpleFilterConfig {
1415 filter_text: &app.lambda_application_state.table.filter,
1416 placeholder: "Filter by attributes or search by keyword",
1417 pagination: &pagination,
1418 mode: app.mode,
1419 is_input_focused: app.lambda_application_state.input_focus == InputFocus::Filter,
1420 is_pagination_focused: app.lambda_application_state.input_focus
1421 == InputFocus::Pagination,
1422 },
1423 );
1424
1425 let filtered: Vec<_> = filtered_lambda_applications(app);
1427 let start_idx = current_page * page_size;
1428 let end_idx = (start_idx + page_size).min(filtered.len());
1429 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1430
1431 let title = format_title(&format!("Applications ({})", filtered.len()));
1432
1433 let mut columns: Vec<Box<dyn TableColumn<LambdaApplication>>> = vec![];
1434 for col_id in &app.lambda_application_visible_column_ids {
1435 if let Some(column) = ApplicationColumn::from_id(col_id) {
1436 columns.push(Box::new(column));
1437 }
1438 }
1439
1440 let expanded_index = if let Some(expanded) = app.lambda_application_state.table.expanded_item {
1441 if expanded >= start_idx && expanded < end_idx {
1442 Some(expanded - start_idx)
1443 } else {
1444 None
1445 }
1446 } else {
1447 None
1448 };
1449
1450 let config = TableConfig {
1451 items: paginated,
1452 selected_index: app.lambda_application_state.table.selected % page_size,
1453 expanded_index,
1454 columns: &columns,
1455 sort_column: "Last modified",
1456 sort_direction: SortDirection::Desc,
1457 title,
1458 area: chunks[1],
1459 get_expanded_content: Some(Box::new(|app: &LambdaApplication| {
1460 expanded_from_columns(&columns, app)
1461 })),
1462 is_active: app.mode != Mode::FilterInput,
1463 };
1464
1465 render_table(frame, config);
1466}
1467
1468pub fn filtered_lambda_functions(app: &App) -> Vec<&LambdaFunction> {
1470 filter_by_fields(
1471 &app.lambda_state.table.items,
1472 &app.lambda_state.table.filter,
1473 |f| vec![&f.name, &f.description, &f.runtime],
1474 )
1475}
1476
1477pub fn filtered_lambda_applications(app: &App) -> Vec<&LambdaApplication> {
1478 filter_by_fields(
1479 &app.lambda_application_state.table.items,
1480 &app.lambda_application_state.table.filter,
1481 |a| vec![&a.name, &a.description, &a.status],
1482 )
1483}
1484
1485pub async fn load_lambda_functions(app: &mut App) -> anyhow::Result<()> {
1486 let functions = app.lambda_client.list_functions().await?;
1487
1488 let mut functions: Vec<LambdaFunction> = functions
1489 .into_iter()
1490 .map(|f| LambdaFunction {
1491 name: f.name,
1492 arn: f.arn,
1493 application: f.application,
1494 description: f.description,
1495 package_type: f.package_type,
1496 runtime: f.runtime,
1497 architecture: f.architecture,
1498 code_size: f.code_size,
1499 code_sha256: f.code_sha256,
1500 memory_mb: f.memory_mb,
1501 timeout_seconds: f.timeout_seconds,
1502 last_modified: f.last_modified,
1503 layers: f
1504 .layers
1505 .into_iter()
1506 .enumerate()
1507 .map(|(i, l)| {
1508 let (name, version) = parse_layer_arn(&l.arn);
1509 Layer {
1510 merge_order: (i + 1).to_string(),
1511 name,
1512 layer_version: version,
1513 compatible_runtimes: "-".to_string(),
1514 compatible_architectures: "-".to_string(),
1515 version_arn: l.arn,
1516 }
1517 })
1518 .collect(),
1519 })
1520 .collect();
1521
1522 functions.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
1524
1525 app.lambda_state.table.items = functions;
1526
1527 Ok(())
1528}
1529
1530pub async fn load_lambda_applications(app: &mut App) -> anyhow::Result<()> {
1531 let applications = app.lambda_client.list_applications().await?;
1532 let mut applications: Vec<LambdaApplication> = applications
1533 .into_iter()
1534 .map(|a| LambdaApplication {
1535 name: a.name,
1536 arn: a.arn,
1537 description: a.description,
1538 status: a.status,
1539 last_modified: a.last_modified,
1540 })
1541 .collect();
1542 applications.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
1543 app.lambda_application_state.table.items = applications;
1544 Ok(())
1545}
1546
1547pub async fn load_lambda_versions(app: &mut App, function_name: &str) -> anyhow::Result<()> {
1548 let versions = app.lambda_client.list_versions(function_name).await?;
1549 let mut versions: Vec<Version> = versions
1550 .into_iter()
1551 .map(|v| Version {
1552 version: v.version,
1553 aliases: v.aliases,
1554 description: v.description,
1555 last_modified: v.last_modified,
1556 architecture: v.architecture,
1557 })
1558 .collect();
1559
1560 versions.sort_by(|a, b| {
1562 let a_num = a.version.parse::<i32>().unwrap_or(0);
1563 let b_num = b.version.parse::<i32>().unwrap_or(0);
1564 b_num.cmp(&a_num)
1565 });
1566
1567 app.lambda_state.version_table.items = versions;
1568 Ok(())
1569}
1570
1571pub async fn load_lambda_aliases(app: &mut App, function_name: &str) -> anyhow::Result<()> {
1572 let aliases = app.lambda_client.list_aliases(function_name).await?;
1573 let mut aliases: Vec<Alias> = aliases
1574 .into_iter()
1575 .map(|a| Alias {
1576 name: a.name,
1577 versions: a.versions,
1578 description: a.description,
1579 })
1580 .collect();
1581
1582 aliases.sort_by(|a, b| a.name.cmp(&b.name));
1584
1585 app.lambda_state.alias_table.items = aliases;
1586 Ok(())
1587}
1588
1589pub async fn load_lambda_metrics(
1590 app: &mut App,
1591 function_name: &str,
1592 version: Option<&str>,
1593) -> anyhow::Result<()> {
1594 use rusticity_core::lambda::Statistic;
1595
1596 let resource = version.map(|v| format!("{}:{}", function_name, v));
1598 let resource_ref = resource.as_deref();
1599
1600 let invocations = app
1601 .lambda_client
1602 .get_invocations_metric(function_name, resource_ref)
1603 .await?;
1604 app.lambda_state.metric_data_invocations = invocations.clone();
1605
1606 let duration_min = app
1607 .lambda_client
1608 .get_duration_metric(function_name, Statistic::Minimum)
1609 .await?;
1610 app.lambda_state.metric_data_duration_min = duration_min;
1611
1612 let duration_avg = app
1613 .lambda_client
1614 .get_duration_metric(function_name, Statistic::Average)
1615 .await?;
1616 app.lambda_state.metric_data_duration_avg = duration_avg;
1617
1618 let duration_max = app
1619 .lambda_client
1620 .get_duration_metric(function_name, Statistic::Maximum)
1621 .await?;
1622 app.lambda_state.metric_data_duration_max = duration_max;
1623
1624 let errors = app.lambda_client.get_errors_metric(function_name).await?;
1625 app.lambda_state.metric_data_errors = errors.clone();
1626
1627 let mut success_rate = Vec::new();
1628 for (timestamp, error_count) in &errors {
1629 if let Some((_, invocation_count)) = invocations.iter().find(|(ts, _)| ts == timestamp) {
1630 let max_val = error_count.max(*invocation_count);
1631 if max_val > 0.0 {
1632 let rate = 100.0 - 100.0 * error_count / max_val;
1633 success_rate.push((*timestamp, rate));
1634 }
1635 }
1636 }
1637 app.lambda_state.metric_data_success_rate = success_rate;
1638
1639 let throttles = app
1640 .lambda_client
1641 .get_throttles_metric(function_name)
1642 .await?;
1643 app.lambda_state.metric_data_throttles = throttles;
1644
1645 let concurrent_executions = app
1646 .lambda_client
1647 .get_concurrent_executions_metric(function_name)
1648 .await?;
1649 app.lambda_state.metric_data_concurrent_executions = concurrent_executions;
1650
1651 let recursive_invocations_dropped = app
1652 .lambda_client
1653 .get_recursive_invocations_dropped_metric(function_name)
1654 .await?;
1655 app.lambda_state.metric_data_recursive_invocations_dropped = recursive_invocations_dropped;
1656
1657 let async_event_age_min = app
1658 .lambda_client
1659 .get_async_event_age_metric(function_name, Statistic::Minimum)
1660 .await?;
1661 app.lambda_state.metric_data_async_event_age_min = async_event_age_min;
1662
1663 let async_event_age_avg = app
1664 .lambda_client
1665 .get_async_event_age_metric(function_name, Statistic::Average)
1666 .await?;
1667 app.lambda_state.metric_data_async_event_age_avg = async_event_age_avg;
1668
1669 let async_event_age_max = app
1670 .lambda_client
1671 .get_async_event_age_metric(function_name, Statistic::Maximum)
1672 .await?;
1673 app.lambda_state.metric_data_async_event_age_max = async_event_age_max;
1674
1675 let async_events_received = app
1676 .lambda_client
1677 .get_async_events_received_metric(function_name)
1678 .await?;
1679 app.lambda_state.metric_data_async_events_received = async_events_received;
1680
1681 let async_events_dropped = app
1682 .lambda_client
1683 .get_async_events_dropped_metric(function_name)
1684 .await?;
1685 app.lambda_state.metric_data_async_events_dropped = async_events_dropped;
1686
1687 let destination_delivery_failures = app
1688 .lambda_client
1689 .get_destination_delivery_failures_metric(function_name)
1690 .await?;
1691 app.lambda_state.metric_data_destination_delivery_failures = destination_delivery_failures;
1692
1693 let dead_letter_errors = app
1694 .lambda_client
1695 .get_dead_letter_errors_metric(function_name)
1696 .await?;
1697 app.lambda_state.metric_data_dead_letter_errors = dead_letter_errors;
1698
1699 let iterator_age = app
1700 .lambda_client
1701 .get_iterator_age_metric(function_name)
1702 .await?;
1703 app.lambda_state.metric_data_iterator_age = iterator_age;
1704
1705 Ok(())
1706}
1707
1708pub fn render_application_detail(frame: &mut Frame, app: &App, area: Rect) {
1709 frame.render_widget(Clear, area);
1710
1711 let chunks = vertical(
1712 [
1713 Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ],
1717 area,
1718 );
1719
1720 if let Some(app_name) = &app.lambda_application_state.current_application {
1722 frame.render_widget(Paragraph::new(app_name.as_str()), chunks[0]);
1723 }
1724
1725 let tabs: Vec<(&str, ApplicationDetailTab)> = ApplicationDetailTab::ALL
1727 .iter()
1728 .map(|tab| (tab.name(), *tab))
1729 .collect();
1730 render_tabs(
1731 frame,
1732 chunks[1],
1733 &tabs,
1734 &app.lambda_application_state.detail_tab,
1735 );
1736
1737 if app.lambda_application_state.detail_tab == ApplicationDetailTab::Overview {
1739 let chunks_content = vertical(
1740 [
1741 Constraint::Length(3), Constraint::Min(0), ],
1744 chunks[2],
1745 );
1746
1747 let page_size = app.lambda_application_state.resources.page_size.value();
1749 let filtered_count = app.lambda_application_state.resources.items.len();
1750 let total_pages = filtered_count.div_ceil(page_size);
1751 let current_page = app.lambda_application_state.resources.selected / page_size;
1752 let pagination = render_pagination_text(current_page, total_pages);
1753
1754 render_simple_filter(
1755 frame,
1756 chunks_content[0],
1757 SimpleFilterConfig {
1758 filter_text: &app.lambda_application_state.resources.filter,
1759 placeholder: "Filter by attributes or search by keyword",
1760 pagination: &pagination,
1761 mode: app.mode,
1762 is_input_focused: app.lambda_application_state.resource_input_focus
1763 == InputFocus::Filter,
1764 is_pagination_focused: app.lambda_application_state.resource_input_focus
1765 == InputFocus::Pagination,
1766 },
1767 );
1768
1769 let title = format!(
1771 " Resources ({}) ",
1772 app.lambda_application_state.resources.items.len()
1773 );
1774
1775 let columns: Vec<Box<dyn TableColumn<Resource>>> = app
1776 .lambda_resource_visible_column_ids
1777 .iter()
1778 .filter_map(|col_id| {
1779 ResourceColumn::from_id(col_id)
1780 .map(|col| Box::new(col) as Box<dyn TableColumn<Resource>>)
1781 })
1782 .collect();
1783 let start_idx = current_page * page_size;
1791 let end_idx = (start_idx + page_size).min(filtered_count);
1792 let paginated: Vec<&Resource> = app.lambda_application_state.resources.items
1793 [start_idx..end_idx]
1794 .iter()
1795 .collect();
1796
1797 let config = TableConfig {
1798 items: paginated,
1799 selected_index: app.lambda_application_state.resources.selected,
1800 expanded_index: app.lambda_application_state.resources.expanded_item,
1801 columns: &columns,
1802 sort_column: "Logical ID",
1803 sort_direction: SortDirection::Asc,
1804 title,
1805 area: chunks_content[1],
1806 get_expanded_content: Some(Box::new(|res: &Resource| {
1807 plain_expanded_content(format!(
1808 "Logical ID: {}\nPhysical ID: {}\nType: {}\nLast modified: {}",
1809 res.logical_id, res.physical_id, res.resource_type, res.last_modified
1810 ))
1811 })),
1812 is_active: true,
1813 };
1814
1815 render_table(frame, config);
1816 } else if app.lambda_application_state.detail_tab == ApplicationDetailTab::Deployments {
1817 let chunks_content = vertical(
1818 [
1819 Constraint::Length(3), Constraint::Min(0), ],
1822 chunks[2],
1823 );
1824
1825 let page_size = app.lambda_application_state.deployments.page_size.value();
1827 let filtered_count = app.lambda_application_state.deployments.items.len();
1828 let total_pages = filtered_count.div_ceil(page_size);
1829 let current_page = app.lambda_application_state.deployments.selected / page_size;
1830 let pagination = render_pagination_text(current_page, total_pages);
1831
1832 render_simple_filter(
1833 frame,
1834 chunks_content[0],
1835 SimpleFilterConfig {
1836 filter_text: &app.lambda_application_state.deployments.filter,
1837 placeholder: "Filter by attributes or search by keyword",
1838 pagination: &pagination,
1839 mode: app.mode,
1840 is_input_focused: app.lambda_application_state.deployment_input_focus
1841 == InputFocus::Filter,
1842 is_pagination_focused: app.lambda_application_state.deployment_input_focus
1843 == InputFocus::Pagination,
1844 },
1845 );
1846
1847 let title = format!(
1849 " Deployment history ({}) ",
1850 app.lambda_application_state.deployments.items.len()
1851 );
1852
1853 let columns: Vec<Box<dyn TableColumn<Deployment>>> = vec![
1854 Box::new(DeploymentColumn::Deployment),
1855 Box::new(DeploymentColumn::ResourceType),
1856 Box::new(DeploymentColumn::LastUpdated),
1857 Box::new(DeploymentColumn::Status),
1858 ];
1859
1860 let start_idx = current_page * page_size;
1861 let end_idx = (start_idx + page_size).min(filtered_count);
1862 let paginated: Vec<&Deployment> = app.lambda_application_state.deployments.items
1863 [start_idx..end_idx]
1864 .iter()
1865 .collect();
1866
1867 let config = TableConfig {
1868 items: paginated,
1869 selected_index: app.lambda_application_state.deployments.selected,
1870 expanded_index: app.lambda_application_state.deployments.expanded_item,
1871 columns: &columns,
1872 sort_column: "",
1873 sort_direction: SortDirection::Asc,
1874 title,
1875 area: chunks_content[1],
1876 get_expanded_content: Some(Box::new(|dep: &Deployment| {
1877 plain_expanded_content(format!(
1878 "Deployment: {}\nResource type: {}\nLast updated: {}\nStatus: {}",
1879 dep.deployment_id, dep.resource_type, dep.last_updated, dep.status
1880 ))
1881 })),
1882 is_active: true,
1883 };
1884
1885 render_table(frame, config);
1886 }
1887}
1888
1889fn render_lambda_monitoring_charts(frame: &mut Frame, app: &App, area: Rect) {
1890 use crate::ui::monitoring::{
1891 render_monitoring_tab, DualAxisChart, MetricChart, MultiDatasetChart,
1892 };
1893
1894 let invocations_sum: f64 = app
1896 .lambda_state
1897 .metric_data_invocations
1898 .iter()
1899 .map(|(_, v)| v)
1900 .sum();
1901 let invocations_label = format!("Invocations [sum: {:.0}]", invocations_sum);
1902
1903 let duration_min: f64 = app
1904 .lambda_state
1905 .metric_data_duration_min
1906 .iter()
1907 .map(|(_, v)| v)
1908 .fold(f64::INFINITY, |a, &b| a.min(b));
1909 let duration_avg: f64 = if !app.lambda_state.metric_data_duration_avg.is_empty() {
1910 app.lambda_state
1911 .metric_data_duration_avg
1912 .iter()
1913 .map(|(_, v)| v)
1914 .sum::<f64>()
1915 / app.lambda_state.metric_data_duration_avg.len() as f64
1916 } else {
1917 0.0
1918 };
1919 let duration_max: f64 = app
1920 .lambda_state
1921 .metric_data_duration_max
1922 .iter()
1923 .map(|(_, v)| v)
1924 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1925 let duration_label = format!(
1926 "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
1927 if duration_min.is_finite() {
1928 duration_min
1929 } else {
1930 0.0
1931 },
1932 duration_avg,
1933 if duration_max.is_finite() {
1934 duration_max
1935 } else {
1936 0.0
1937 }
1938 );
1939
1940 let async_event_age_min: f64 = app
1941 .lambda_state
1942 .metric_data_async_event_age_min
1943 .iter()
1944 .map(|(_, v)| v)
1945 .fold(f64::INFINITY, |a, &b| a.min(b));
1946 let async_event_age_avg: f64 = if !app.lambda_state.metric_data_async_event_age_avg.is_empty() {
1947 app.lambda_state
1948 .metric_data_async_event_age_avg
1949 .iter()
1950 .map(|(_, v)| v)
1951 .sum::<f64>()
1952 / app.lambda_state.metric_data_async_event_age_avg.len() as f64
1953 } else {
1954 0.0
1955 };
1956 let async_event_age_max: f64 = app
1957 .lambda_state
1958 .metric_data_async_event_age_max
1959 .iter()
1960 .map(|(_, v)| v)
1961 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1962 let async_event_age_label = format!(
1963 "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
1964 if async_event_age_min.is_finite() {
1965 async_event_age_min
1966 } else {
1967 0.0
1968 },
1969 async_event_age_avg,
1970 if async_event_age_max.is_finite() {
1971 async_event_age_max
1972 } else {
1973 0.0
1974 }
1975 );
1976
1977 let async_events_received_sum: f64 = app
1978 .lambda_state
1979 .metric_data_async_events_received
1980 .iter()
1981 .map(|(_, v)| v)
1982 .sum();
1983 let async_events_dropped_sum: f64 = app
1984 .lambda_state
1985 .metric_data_async_events_dropped
1986 .iter()
1987 .map(|(_, v)| v)
1988 .sum();
1989 let async_events_label = format!(
1990 "Received [sum: {:.0}], Dropped [sum: {:.0}]",
1991 async_events_received_sum, async_events_dropped_sum
1992 );
1993
1994 let destination_delivery_failures_sum: f64 = app
1995 .lambda_state
1996 .metric_data_destination_delivery_failures
1997 .iter()
1998 .map(|(_, v)| v)
1999 .sum();
2000 let dead_letter_errors_sum: f64 = app
2001 .lambda_state
2002 .metric_data_dead_letter_errors
2003 .iter()
2004 .map(|(_, v)| v)
2005 .sum();
2006 let async_delivery_failures_label = format!(
2007 "Destination delivery failures [sum: {:.0}], Dead letter queue failures [sum: {:.0}]",
2008 destination_delivery_failures_sum, dead_letter_errors_sum
2009 );
2010
2011 let iterator_age_max: f64 = app
2012 .lambda_state
2013 .metric_data_iterator_age
2014 .iter()
2015 .map(|(_, v)| v)
2016 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2017 let iterator_age_label = format!(
2018 "Maximum [{}]",
2019 if iterator_age_max.is_finite() {
2020 format!("{:.0}", iterator_age_max)
2021 } else {
2022 "--".to_string()
2023 }
2024 );
2025
2026 let error_max: f64 = app
2027 .lambda_state
2028 .metric_data_errors
2029 .iter()
2030 .map(|(_, v)| v)
2031 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2032 let success_rate_min: f64 = app
2033 .lambda_state
2034 .metric_data_success_rate
2035 .iter()
2036 .map(|(_, v)| v)
2037 .fold(f64::INFINITY, |a, &b| a.min(b));
2038 let error_label = format!(
2039 "Errors [max: {:.0}] and Success rate [min: {:.0}%]",
2040 if error_max.is_finite() {
2041 error_max
2042 } else {
2043 0.0
2044 },
2045 if success_rate_min.is_finite() {
2046 success_rate_min
2047 } else {
2048 0.0
2049 }
2050 );
2051
2052 let throttles_max: f64 = app
2053 .lambda_state
2054 .metric_data_throttles
2055 .iter()
2056 .map(|(_, v)| v)
2057 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2058 let throttles_label = format!(
2059 "Throttles [max: {:.0}]",
2060 if throttles_max.is_finite() {
2061 throttles_max
2062 } else {
2063 0.0
2064 }
2065 );
2066
2067 let concurrent_max: f64 = app
2068 .lambda_state
2069 .metric_data_concurrent_executions
2070 .iter()
2071 .map(|(_, v)| v)
2072 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2073 let concurrent_label = format!(
2074 "Concurrent executions [max: {}]",
2075 if concurrent_max.is_finite() {
2076 format!("{:.0}", concurrent_max)
2077 } else {
2078 "--".to_string()
2079 }
2080 );
2081
2082 let recursive_sum: f64 = app
2083 .lambda_state
2084 .metric_data_recursive_invocations_dropped
2085 .iter()
2086 .map(|(_, v)| v)
2087 .sum();
2088 let recursive_label = format!(
2089 "Dropped [sum: {}]",
2090 if recursive_sum > 0.0 {
2091 format!("{:.0}", recursive_sum)
2092 } else {
2093 "--".to_string()
2094 }
2095 );
2096
2097 render_monitoring_tab(
2098 frame,
2099 area,
2100 &[MetricChart {
2101 title: "Invocations",
2102 data: &app.lambda_state.metric_data_invocations,
2103 y_axis_label: "Count",
2104 x_axis_label: Some(invocations_label),
2105 }],
2106 &[MultiDatasetChart {
2107 title: "Duration",
2108 datasets: vec![
2109 ("Minimum", &app.lambda_state.metric_data_duration_min),
2110 ("Average", &app.lambda_state.metric_data_duration_avg),
2111 ("Maximum", &app.lambda_state.metric_data_duration_max),
2112 ],
2113 y_axis_label: "Milliseconds",
2114 y_axis_step: 1000,
2115 x_axis_label: Some(duration_label),
2116 }],
2117 &[DualAxisChart {
2118 title: "Error count and success rate",
2119 left_dataset: ("Errors", &app.lambda_state.metric_data_errors),
2120 right_dataset: ("Success rate", &app.lambda_state.metric_data_success_rate),
2121 left_y_label: "Count",
2122 right_y_label: "%",
2123 x_axis_label: Some(error_label),
2124 }],
2125 &[
2126 MetricChart {
2127 title: "Throttles",
2128 data: &app.lambda_state.metric_data_throttles,
2129 y_axis_label: "Count",
2130 x_axis_label: Some(throttles_label),
2131 },
2132 MetricChart {
2133 title: "Total concurrent executions",
2134 data: &app.lambda_state.metric_data_concurrent_executions,
2135 y_axis_label: "Count",
2136 x_axis_label: Some(concurrent_label),
2137 },
2138 MetricChart {
2139 title: "Recursive invocations",
2140 data: &app.lambda_state.metric_data_recursive_invocations_dropped,
2141 y_axis_label: "Count",
2142 x_axis_label: Some(recursive_label),
2143 },
2144 MetricChart {
2145 title: "Async event age",
2146 data: &app.lambda_state.metric_data_async_event_age_avg,
2147 y_axis_label: "Milliseconds",
2148 x_axis_label: Some(async_event_age_label),
2149 },
2150 MetricChart {
2151 title: "Async events",
2152 data: &app.lambda_state.metric_data_async_events_received,
2153 y_axis_label: "Count",
2154 x_axis_label: Some(async_events_label),
2155 },
2156 MetricChart {
2157 title: "Async delivery failures",
2158 data: &app.lambda_state.metric_data_destination_delivery_failures,
2159 y_axis_label: "Count",
2160 x_axis_label: Some(async_delivery_failures_label),
2161 },
2162 MetricChart {
2163 title: "Iterator age",
2164 data: &app.lambda_state.metric_data_iterator_age,
2165 y_axis_label: "Milliseconds",
2166 x_axis_label: Some(iterator_age_label),
2167 },
2168 ],
2169 app.lambda_state.monitoring_scroll,
2170 );
2171}
2172
2173#[cfg(test)]
2174mod tests {
2175 use super::*;
2176
2177 #[test]
2178 fn test_detail_tab_monitoring_in_all() {
2179 let tabs = DetailTab::ALL;
2180 assert_eq!(tabs.len(), 5);
2181 assert_eq!(tabs[0], DetailTab::Code);
2182 assert_eq!(tabs[1], DetailTab::Monitor);
2183 assert_eq!(tabs[2], DetailTab::Configuration);
2184 assert_eq!(tabs[3], DetailTab::Aliases);
2185 assert_eq!(tabs[4], DetailTab::Versions);
2186 }
2187
2188 #[test]
2189 fn test_detail_tab_monitoring_name() {
2190 assert_eq!(DetailTab::Monitor.name(), "Monitor");
2191 }
2192
2193 #[test]
2194 fn test_detail_tab_monitoring_navigation() {
2195 let tab = DetailTab::Code;
2196 assert_eq!(tab.next(), DetailTab::Monitor);
2197
2198 let tab = DetailTab::Monitor;
2199 assert_eq!(tab.next(), DetailTab::Configuration);
2200 assert_eq!(tab.prev(), DetailTab::Code);
2201 }
2202
2203 #[test]
2204 fn test_state_monitoring_fields_initialized() {
2205 let state = State::new();
2206 assert_eq!(state.monitoring_scroll, 0);
2207 assert!(state.metric_data_invocations.is_empty());
2208 assert!(state.metric_data_duration_min.is_empty());
2209 assert!(state.metric_data_duration_avg.is_empty());
2210 assert!(state.metric_data_duration_max.is_empty());
2211 assert!(state.metric_data_errors.is_empty());
2212 assert!(state.metric_data_success_rate.is_empty());
2213 assert!(state.metric_data_throttles.is_empty());
2214 assert!(state.metric_data_concurrent_executions.is_empty());
2215 assert!(state.metric_data_recursive_invocations_dropped.is_empty());
2216 }
2217
2218 #[test]
2219 fn test_state_monitoring_scroll() {
2220 let mut state = State::new();
2221 assert_eq!(state.monitoring_scroll, 0);
2222
2223 state.monitoring_scroll = 1;
2224 assert_eq!(state.monitoring_scroll, 1);
2225
2226 state.monitoring_scroll = 2;
2227 assert_eq!(state.monitoring_scroll, 2);
2228 }
2229
2230 #[test]
2231 fn test_state_metric_data() {
2232 let mut state = State::new();
2233 state.metric_data_invocations = vec![(1700000000, 10.0), (1700000060, 15.0)];
2234 state.metric_data_duration_min = vec![(1700000000, 100.0), (1700000060, 150.0)];
2235 state.metric_data_duration_avg = vec![(1700000000, 200.0), (1700000060, 250.0)];
2236 state.metric_data_duration_max = vec![(1700000000, 300.0), (1700000060, 350.0)];
2237 state.metric_data_errors = vec![(1700000000, 1.0), (1700000060, 2.0)];
2238 state.metric_data_success_rate = vec![(1700000000, 90.0), (1700000060, 85.0)];
2239 state.metric_data_throttles = vec![(1700000000, 0.0), (1700000060, 1.0)];
2240 state.metric_data_concurrent_executions = vec![(1700000000, 5.0), (1700000060, 10.0)];
2241 state.metric_data_recursive_invocations_dropped =
2242 vec![(1700000000, 0.0), (1700000060, 0.0)];
2243
2244 assert_eq!(state.metric_data_invocations.len(), 2);
2245 assert_eq!(state.metric_data_duration_min.len(), 2);
2246 assert_eq!(state.metric_data_duration_avg.len(), 2);
2247 assert_eq!(state.metric_data_duration_max.len(), 2);
2248 assert_eq!(state.metric_data_errors.len(), 2);
2249 assert_eq!(state.metric_data_success_rate.len(), 2);
2250 assert_eq!(state.metric_data_throttles.len(), 2);
2251 assert_eq!(state.metric_data_concurrent_executions.len(), 2);
2252 assert_eq!(state.metric_data_recursive_invocations_dropped.len(), 2);
2253 }
2254
2255 #[test]
2256 fn test_invocations_sum_calculation() {
2257 let data = [(1700000000, 10.0), (1700000060, 15.0), (1700000120, 5.0)];
2258 let sum: f64 = data.iter().map(|(_, v)| v).sum();
2259 assert_eq!(sum, 30.0);
2260 }
2261
2262 #[test]
2263 fn test_invocations_label_format() {
2264 let sum = 1234.5;
2265 let label = format!("Invocations [sum: {:.0}]", sum);
2266 assert_eq!(label, "Invocations [sum: 1234]");
2267 }
2268
2269 #[test]
2270 fn test_invocations_sum_empty() {
2271 let data: Vec<(i64, f64)> = vec![];
2272 let sum: f64 = data.iter().map(|(_, v)| v).sum();
2273 assert_eq!(sum, 0.0);
2274 }
2275
2276 #[test]
2277 fn test_duration_label_formatting() {
2278 let min = 100.5;
2279 let avg = 250.7;
2280 let max = 450.2;
2281 let label = format!(
2282 "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
2283 min, avg, max
2284 );
2285 assert_eq!(label, "Minimum [100], Average [251], Maximum [450]");
2286 }
2287
2288 #[test]
2289 fn test_duration_min_with_infinity() {
2290 let data: Vec<(i64, f64)> = vec![];
2291 let min: f64 = data
2292 .iter()
2293 .map(|(_, v)| v)
2294 .fold(f64::INFINITY, |a, &b| a.min(b));
2295 assert!(min.is_infinite());
2296 let result = if min.is_finite() { min } else { 0.0 };
2297 assert_eq!(result, 0.0);
2298 }
2299
2300 #[test]
2301 fn test_duration_max_with_neg_infinity() {
2302 let data: Vec<(i64, f64)> = vec![];
2303 let max: f64 = data
2304 .iter()
2305 .map(|(_, v)| v)
2306 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2307 assert!(max.is_infinite());
2308 let result = if max.is_finite() { max } else { 0.0 };
2309 assert_eq!(result, 0.0);
2310 }
2311
2312 #[test]
2313 fn test_duration_avg_empty_data() {
2314 let data: Vec<(i64, f64)> = vec![];
2315 let avg: f64 = if !data.is_empty() {
2316 data.iter().map(|(_, v)| v).sum::<f64>() / data.len() as f64
2317 } else {
2318 0.0
2319 };
2320 assert_eq!(avg, 0.0);
2321 }
2322
2323 #[test]
2324 fn test_duration_metrics_with_data() {
2325 let min_data = [(1700000000, 100.0), (1700000060, 90.0), (1700000120, 110.0)];
2326 let avg_data = [
2327 (1700000000, 200.0),
2328 (1700000060, 210.0),
2329 (1700000120, 190.0),
2330 ];
2331 let max_data = [
2332 (1700000000, 300.0),
2333 (1700000060, 320.0),
2334 (1700000120, 310.0),
2335 ];
2336
2337 let min: f64 = min_data
2338 .iter()
2339 .map(|(_, v)| v)
2340 .fold(f64::INFINITY, |a, &b| a.min(b));
2341 let avg: f64 = avg_data.iter().map(|(_, v)| v).sum::<f64>() / avg_data.len() as f64;
2342 let max: f64 = max_data
2343 .iter()
2344 .map(|(_, v)| v)
2345 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2346
2347 assert_eq!(min, 90.0);
2348 assert_eq!(avg, 200.0);
2349 assert_eq!(max, 320.0);
2350 }
2351
2352 #[test]
2353 fn test_success_rate_calculation() {
2354 let errors: f64 = 5.0;
2355 let invocations: f64 = 100.0;
2356 let max_val = errors.max(invocations);
2357 let success_rate = 100.0 - 100.0 * errors / max_val;
2358 assert_eq!(success_rate, 95.0);
2359 }
2360
2361 #[test]
2362 fn test_success_rate_with_zero_invocations() {
2363 let errors: f64 = 0.0;
2364 let invocations: f64 = 0.0;
2365 let max_val = errors.max(invocations);
2366 assert_eq!(max_val, 0.0);
2367 }
2368
2369 #[test]
2370 fn test_error_label_format() {
2371 let error_max = 10.0;
2372 let success_rate_min = 85.5;
2373 let label = format!(
2374 "Errors [max: {:.0}] and Success rate [min: {:.0}%]",
2375 error_max, success_rate_min
2376 );
2377 assert_eq!(label, "Errors [max: 10] and Success rate [min: 86%]");
2378 }
2379
2380 #[test]
2381 fn test_load_lambda_metrics_builds_resource_string() {
2382 let function_name = "test-function";
2384 let version = Some("1");
2385 let resource = version.map(|v| format!("{}:{}", function_name, v));
2386 assert_eq!(resource, Some("test-function:1".to_string()));
2387
2388 let version: Option<&str> = None;
2390 let resource = version.map(|v| format!("{}:{}", function_name, v));
2391 assert_eq!(resource, None);
2392 }
2393
2394 #[test]
2395 fn test_detail_tab_next_version_tab() {
2396 assert_eq!(VersionDetailTab::Code.next(), VersionDetailTab::Monitor);
2397 assert_eq!(
2398 VersionDetailTab::Monitor.next(),
2399 VersionDetailTab::Configuration
2400 );
2401 assert_eq!(
2402 VersionDetailTab::Configuration.next(),
2403 VersionDetailTab::Code
2404 );
2405 }
2406
2407 #[test]
2408 fn test_detail_tab_prev_version_tab() {
2409 assert_eq!(
2410 VersionDetailTab::Code.prev(),
2411 VersionDetailTab::Configuration
2412 );
2413 assert_eq!(
2414 VersionDetailTab::Configuration.prev(),
2415 VersionDetailTab::Monitor
2416 );
2417 assert_eq!(VersionDetailTab::Monitor.prev(), VersionDetailTab::Code);
2418 }
2419
2420 #[test]
2421 fn test_rounded_block_helper_usage() {
2422 use crate::ui::titled_block;
2423 use ratatui::prelude::Rect;
2424 let block = titled_block("Function overview");
2426 let area = Rect::new(0, 0, 100, 20);
2427 let inner = block.inner(area);
2428 assert_eq!(inner.width, 98); assert_eq!(inner.height, 18); }
2431
2432 #[test]
2433 fn test_overview_height_uses_dynamic_calculation() {
2434 use crate::ui::labeled_field;
2435 let fields = vec![
2437 labeled_field("Name", "test-function"),
2438 labeled_field("Runtime", "python3.11"),
2439 labeled_field("Handler", "lambda_function.lambda_handler"),
2440 labeled_field("Memory", "128 MB"),
2441 ];
2442 let width = 200; let height = calculate_dynamic_height(&fields, width);
2444 assert!(height <= 2, "Expected 2 rows or less, got {}", height);
2446 }
2447
2448 #[test]
2449 fn test_code_properties_uses_dynamic_height() {
2450 use crate::ui::labeled_field;
2451 let code_lines = vec![
2453 labeled_field("Package size", "17.86 MB"),
2454 labeled_field("SHA256 hash", "abc123"),
2455 labeled_field("Last modified", "2025-12-11"),
2456 ];
2457 let width = 200;
2458 let height = calculate_dynamic_height(&code_lines, width);
2459 assert!(
2461 height <= 2,
2462 "Expected 2 rows or less for code properties, got {}",
2463 height
2464 );
2465 }
2466}