Skip to main content

rusticity_term/ui/
lambda.rs

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