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