rusticity_term/ui/
lambda.rs

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