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