rusticity_term/ui/
lambda.rs

1use crate::app::App;
2use crate::column;
3use crate::common::CyclicEnum;
4use crate::common::{
5    format_bytes, format_duration_seconds, format_memory_mb, render_pagination_text, InputFocus,
6    SortDirection,
7};
8use crate::keymap::Mode;
9use crate::lambda::{
10    format_architecture, format_runtime, Application as LambdaApplication, Deployment,
11    Function as LambdaFunction, Layer, Resource,
12};
13use crate::table::TableState;
14use crate::ui::table::{expanded_from_columns, render_table, Column as TableColumn, TableConfig};
15use crate::ui::{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 visible_columns: Vec<crate::lambda::Column>,
27    pub all_columns: Vec<crate::lambda::Column>,
28    pub version_table: TableState<crate::lambda::Version>,
29    pub visible_version_columns: Vec<crate::lambda::VersionColumn>,
30    pub all_version_columns: Vec<crate::lambda::VersionColumn>,
31    pub alias_table: TableState<crate::lambda::Alias>,
32    pub visible_alias_columns: Vec<crate::lambda::AliasColumn>,
33    pub all_alias_columns: Vec<crate::lambda::AliasColumn>,
34    pub visible_layer_columns: Vec<crate::lambda::LayerColumn>,
35    pub all_layer_columns: Vec<crate::lambda::LayerColumn>,
36    pub input_focus: InputFocus,
37    pub version_input_focus: InputFocus,
38    pub alias_input_focus: InputFocus,
39    pub layer_selected: usize,
40    pub layer_expanded: Option<usize>,
41}
42
43impl Default for State {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl State {
50    pub fn new() -> Self {
51        Self {
52            table: TableState::new(),
53            current_function: None,
54            current_version: None,
55            current_alias: None,
56            detail_tab: DetailTab::Code,
57            visible_columns: vec![
58                crate::lambda::Column::Name,
59                crate::lambda::Column::Runtime,
60                crate::lambda::Column::CodeSize,
61                crate::lambda::Column::MemoryMb,
62                crate::lambda::Column::TimeoutSeconds,
63                crate::lambda::Column::LastModified,
64            ],
65            all_columns: crate::lambda::Column::all(),
66            version_table: TableState::new(),
67            visible_version_columns: crate::lambda::VersionColumn::all(),
68            all_version_columns: crate::lambda::VersionColumn::all(),
69            alias_table: TableState::new(),
70            visible_alias_columns: crate::lambda::AliasColumn::all(),
71            all_alias_columns: crate::lambda::AliasColumn::all(),
72            visible_layer_columns: crate::lambda::LayerColumn::all(),
73            all_layer_columns: crate::lambda::LayerColumn::all(),
74            input_focus: InputFocus::Filter,
75            version_input_focus: InputFocus::Filter,
76            alias_input_focus: InputFocus::Filter,
77            layer_selected: 0,
78            layer_expanded: None,
79        }
80    }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq)]
84pub enum DetailTab {
85    Code,
86    Configuration,
87    Aliases,
88    Versions,
89}
90
91impl CyclicEnum for DetailTab {
92    const ALL: &'static [Self] = &[
93        Self::Code,
94        Self::Configuration,
95        Self::Aliases,
96        Self::Versions,
97    ];
98}
99
100impl DetailTab {
101    pub fn name(&self) -> &'static str {
102        match self {
103            DetailTab::Code => "Code",
104            DetailTab::Configuration => "Configuration",
105            DetailTab::Aliases => "Aliases",
106            DetailTab::Versions => "Versions",
107        }
108    }
109}
110
111pub struct ApplicationState {
112    pub table: TableState<LambdaApplication>,
113    pub input_focus: InputFocus,
114    pub current_application: Option<String>,
115    pub detail_tab: ApplicationDetailTab,
116    pub deployments: TableState<Deployment>,
117    pub deployment_input_focus: InputFocus,
118    pub resources: TableState<Resource>,
119    pub resource_input_focus: InputFocus,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub enum ApplicationDetailTab {
124    Overview,
125    Deployments,
126}
127
128impl CyclicEnum for ApplicationDetailTab {
129    const ALL: &'static [Self] = &[Self::Overview, Self::Deployments];
130}
131
132impl ApplicationDetailTab {
133    pub fn name(&self) -> &str {
134        match self {
135            Self::Overview => "Overview",
136            Self::Deployments => "Deployments",
137        }
138    }
139}
140
141impl Default for ApplicationState {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147impl ApplicationState {
148    pub fn new() -> Self {
149        Self {
150            table: TableState::new(),
151            input_focus: InputFocus::Filter,
152            current_application: None,
153            detail_tab: ApplicationDetailTab::Overview,
154            deployments: TableState::new(),
155            deployment_input_focus: InputFocus::Filter,
156            resources: TableState::new(),
157            resource_input_focus: InputFocus::Filter,
158        }
159    }
160}
161
162pub fn render_functions(frame: &mut Frame, app: &App, area: Rect) {
163    frame.render_widget(Clear, area);
164
165    if app.lambda_state.current_alias.is_some() {
166        render_alias_detail(frame, app, area);
167        return;
168    }
169
170    if app.lambda_state.current_version.is_some() {
171        render_version_detail(frame, app, area);
172        return;
173    }
174
175    if app.lambda_state.current_function.is_some() {
176        render_detail(frame, app, area);
177        return;
178    }
179
180    let chunks = vertical(
181        [
182            Constraint::Length(3), // Filter
183            Constraint::Min(0),    // Table
184        ],
185        area,
186    );
187
188    // Filter
189    let page_size = app.lambda_state.table.page_size.value();
190    let filtered_count: usize = app
191        .lambda_state
192        .table
193        .items
194        .iter()
195        .filter(|f| {
196            app.lambda_state.table.filter.is_empty()
197                || f.name
198                    .to_lowercase()
199                    .contains(&app.lambda_state.table.filter.to_lowercase())
200                || f.description
201                    .to_lowercase()
202                    .contains(&app.lambda_state.table.filter.to_lowercase())
203                || f.runtime
204                    .to_lowercase()
205                    .contains(&app.lambda_state.table.filter.to_lowercase())
206        })
207        .count();
208
209    let total_pages = filtered_count.div_ceil(page_size);
210    let current_page = app.lambda_state.table.selected / page_size;
211    let pagination = render_pagination_text(current_page, total_pages);
212
213    crate::ui::filter::render_simple_filter(
214        frame,
215        chunks[0],
216        crate::ui::filter::SimpleFilterConfig {
217            filter_text: &app.lambda_state.table.filter,
218            placeholder: "Filter by attributes or search by keyword",
219            pagination: &pagination,
220            mode: app.mode,
221            is_input_focused: app.lambda_state.input_focus == InputFocus::Filter,
222            is_pagination_focused: app.lambda_state.input_focus == InputFocus::Pagination,
223        },
224    );
225
226    // Table
227    let filtered: Vec<_> = app
228        .lambda_state
229        .table
230        .items
231        .iter()
232        .filter(|f| {
233            app.lambda_state.table.filter.is_empty()
234                || f.name
235                    .to_lowercase()
236                    .contains(&app.lambda_state.table.filter.to_lowercase())
237                || f.description
238                    .to_lowercase()
239                    .contains(&app.lambda_state.table.filter.to_lowercase())
240                || f.runtime
241                    .to_lowercase()
242                    .contains(&app.lambda_state.table.filter.to_lowercase())
243        })
244        .collect();
245
246    let start_idx = current_page * page_size;
247    let end_idx = (start_idx + page_size).min(filtered.len());
248    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
249
250    let title = format!(" Lambda functions ({}) ", filtered.len());
251
252    let columns: Vec<Box<dyn TableColumn<LambdaFunction>>> = app
253        .lambda_state
254        .visible_columns
255        .iter()
256        .map(|col| col.to_column())
257        .collect();
258
259    let expanded_index = if let Some(expanded) = app.lambda_state.table.expanded_item {
260        if expanded >= start_idx && expanded < end_idx {
261            Some(expanded - start_idx)
262        } else {
263            None
264        }
265    } else {
266        None
267    };
268
269    let config = TableConfig {
270        items: paginated,
271        selected_index: app.lambda_state.table.selected % page_size,
272        expanded_index,
273        columns: &columns,
274        sort_column: "Last modified",
275        sort_direction: SortDirection::Desc,
276        title,
277        area: chunks[1],
278        get_expanded_content: Some(Box::new(|func: &LambdaFunction| {
279            expanded_from_columns(&columns, func)
280        })),
281        is_active: app.mode != Mode::FilterInput,
282    };
283
284    render_table(frame, config);
285}
286
287pub fn render_detail(frame: &mut Frame, app: &App, area: Rect) {
288    frame.render_widget(Clear, area);
289
290    // Build overview lines first to calculate height
291    let overview_lines = if let Some(func_name) = &app.lambda_state.current_function {
292        if let Some(func) = app
293            .lambda_state
294            .table
295            .items
296            .iter()
297            .find(|f| f.name == *func_name)
298        {
299            vec![
300                labeled_field(
301                    "Description",
302                    if func.description.is_empty() {
303                        "-"
304                    } else {
305                        &func.description
306                    },
307                ),
308                labeled_field("Last modified", &func.last_modified),
309                labeled_field("Function ARN", &func.arn),
310                labeled_field("Application", func.application.as_deref().unwrap_or("-")),
311            ]
312        } else {
313            vec![]
314        }
315    } else {
316        vec![]
317    };
318
319    let overview_height = if overview_lines.is_empty() {
320        0
321    } else {
322        overview_lines.len() as u16 + 2
323    };
324
325    let chunks = vertical(
326        [
327            Constraint::Length(overview_height),
328            Constraint::Length(1), // Tabs
329            Constraint::Min(0),    // Content
330        ],
331        area,
332    );
333
334    // Function overview
335    if !overview_lines.is_empty() {
336        let overview_block = Block::default()
337            .title(" Function overview ")
338            .borders(Borders::ALL)
339            .border_type(BorderType::Plain)
340            .border_style(Style::default());
341
342        let overview_inner = overview_block.inner(chunks[0]);
343        frame.render_widget(overview_block, chunks[0]);
344        frame.render_widget(Paragraph::new(overview_lines), overview_inner);
345    }
346
347    // Tabs
348    let tabs = [
349        ("Code", DetailTab::Code),
350        ("Configuration", DetailTab::Configuration),
351        ("Aliases", DetailTab::Aliases),
352        ("Versions", DetailTab::Versions),
353    ];
354
355    render_tabs(frame, chunks[1], &tabs, &app.lambda_state.detail_tab);
356
357    // Content area
358    if app.lambda_state.detail_tab == DetailTab::Code {
359        // Show Code properties
360        if let Some(func_name) = &app.lambda_state.current_function {
361            if let Some(func) = app
362                .lambda_state
363                .table
364                .items
365                .iter()
366                .find(|f| f.name == *func_name)
367            {
368                let chunks_content = Layout::default()
369                    .direction(Direction::Vertical)
370                    .constraints([
371                        Constraint::Length(10), // Code properties (includes KMS)
372                        Constraint::Length(8),  // Runtime settings (includes Runtime management)
373                        Constraint::Min(0),     // Layers
374                    ])
375                    .split(chunks[2]);
376
377                // Code properties section
378                let code_block = Block::default()
379                    .title(" Code properties ")
380                    .borders(Borders::ALL)
381                    .border_type(BorderType::Plain);
382
383                let code_inner = code_block.inner(chunks_content[0]);
384                frame.render_widget(code_block, chunks_content[0]);
385
386                let code_lines = vec![
387                    labeled_field("Package size", format_bytes(func.code_size)),
388                    labeled_field("SHA256 hash", &func.code_sha256),
389                    labeled_field("Last modified", &func.last_modified),
390                    section_header("Encryption with AWS KMS customer managed KMS key", code_inner.width),
391                    Line::from(Span::styled(
392                        "To edit customer managed key encryption, you must upload a new .zip deployment package.",
393                        Style::default().fg(Color::DarkGray),
394                    )),
395                    labeled_field("AWS KMS key ARN", ""),
396                    labeled_field("Key alias", ""),
397                    labeled_field("Status", ""),
398                ];
399
400                frame.render_widget(Paragraph::new(code_lines), code_inner);
401
402                // Runtime settings section
403                let runtime_block = Block::default()
404                    .title(" Runtime settings ")
405                    .borders(Borders::ALL)
406                    .border_type(BorderType::Plain);
407
408                let runtime_inner = runtime_block.inner(chunks_content[1]);
409                frame.render_widget(runtime_block, chunks_content[1]);
410
411                let runtime_lines = vec![
412                    labeled_field("Runtime", format_runtime(&func.runtime)),
413                    labeled_field("Handler", ""),
414                    labeled_field("Architecture", format_architecture(&func.architecture)),
415                    section_header("Runtime management configuration", runtime_inner.width),
416                    labeled_field("Runtime version ARN", ""),
417                    labeled_field("Update runtime version", "Auto"),
418                ];
419
420                frame.render_widget(Paragraph::new(runtime_lines), runtime_inner);
421
422                // Layers section
423                #[derive(Clone)]
424                struct Layer {
425                    merge_order: String,
426                    name: String,
427                    layer_version: String,
428                    compatible_runtimes: String,
429                    compatible_architectures: String,
430                    version_arn: String,
431                }
432
433                let layers: Vec<Layer> = func
434                    .layers
435                    .iter()
436                    .enumerate()
437                    .map(|(i, l)| {
438                        let parts: Vec<&str> = l.arn.split(':').collect();
439                        let name = parts.get(6).unwrap_or(&"").to_string();
440                        let version = parts.get(7).unwrap_or(&"").to_string();
441                        Layer {
442                            merge_order: (i + 1).to_string(),
443                            name,
444                            layer_version: version,
445                            compatible_runtimes: "-".to_string(),
446                            compatible_architectures: "-".to_string(),
447                            version_arn: l.arn.clone(),
448                        }
449                    })
450                    .collect();
451                let layer_refs: Vec<&Layer> = layers.iter().collect();
452                let title = format!(" Layers ({}) ", layer_refs.len());
453
454                let columns: Vec<Box<dyn TableColumn<Layer>>> = vec![
455                    Box::new(column!(name="Merge order", width=12, type=Layer, field=merge_order)),
456                    Box::new(column!(name="Name", width=20, type=Layer, field=name)),
457                    Box::new(
458                        column!(name="Layer version", width=14, type=Layer, field=layer_version),
459                    ),
460                    Box::new(
461                        column!(name="Compatible runtimes", width=20, type=Layer, field=compatible_runtimes),
462                    ),
463                    Box::new(
464                        column!(name="Compatible architectures", width=26, type=Layer, field=compatible_architectures),
465                    ),
466                    Box::new(column!(name="Version ARN", width=40, type=Layer, field=version_arn)),
467                ];
468
469                let config = TableConfig {
470                    items: layer_refs,
471                    selected_index: app.lambda_state.layer_selected,
472                    expanded_index: app.lambda_state.layer_expanded,
473                    columns: &columns,
474                    sort_column: "",
475                    sort_direction: SortDirection::Asc,
476                    title,
477                    area: chunks_content[2],
478                    get_expanded_content: Some(Box::new(|layer: &Layer| {
479                        crate::ui::format_expansion_text(&[
480                            ("Merge order", layer.merge_order.clone()),
481                            ("Name", layer.name.clone()),
482                            ("Layer version", layer.layer_version.clone()),
483                            ("Compatible runtimes", layer.compatible_runtimes.clone()),
484                            (
485                                "Compatible architectures",
486                                layer.compatible_architectures.clone(),
487                            ),
488                            ("Version ARN", layer.version_arn.clone()),
489                        ])
490                    })),
491                    is_active: app.lambda_state.detail_tab == DetailTab::Code,
492                };
493
494                render_table(frame, config);
495            }
496        }
497    } else if app.lambda_state.detail_tab == DetailTab::Configuration {
498        // Configuration tab
499        if let Some(func_name) = &app.lambda_state.current_function {
500            if let Some(func) = app
501                .lambda_state
502                .table
503                .items
504                .iter()
505                .find(|f| f.name == *func_name)
506            {
507                let config_block = Block::default()
508                    .title(" General configuration ")
509                    .borders(Borders::ALL)
510                    .border_type(BorderType::Plain)
511                    .border_style(Style::default());
512
513                let config_inner = config_block.inner(chunks[2]);
514                frame.render_widget(config_block, chunks[2]);
515
516                let config_lines = vec![
517                    labeled_field("Description", &func.description),
518                    labeled_field("Revision", &func.last_modified),
519                    labeled_field("Memory", format_memory_mb(func.memory_mb)),
520                    labeled_field("Ephemeral storage", format_memory_mb(512)),
521                    labeled_field("Timeout", format_duration_seconds(func.timeout_seconds)),
522                    labeled_field("SnapStart", "None"),
523                ];
524
525                frame.render_widget(Paragraph::new(config_lines), config_inner);
526            }
527        }
528    } else if app.lambda_state.detail_tab == DetailTab::Versions {
529        // Versions tab
530        let version_chunks = vertical(
531            [
532                Constraint::Length(3), // Filter
533                Constraint::Min(0),    // Table
534            ],
535            chunks[2],
536        );
537
538        // Filter
539        let page_size = app.lambda_state.version_table.page_size.value();
540        let filtered_count: usize = app
541            .lambda_state
542            .version_table
543            .items
544            .iter()
545            .filter(|v| {
546                app.lambda_state.version_table.filter.is_empty()
547                    || v.version
548                        .to_lowercase()
549                        .contains(&app.lambda_state.version_table.filter.to_lowercase())
550                    || v.aliases
551                        .to_lowercase()
552                        .contains(&app.lambda_state.version_table.filter.to_lowercase())
553                    || v.description
554                        .to_lowercase()
555                        .contains(&app.lambda_state.version_table.filter.to_lowercase())
556            })
557            .count();
558
559        let total_pages = filtered_count.div_ceil(page_size);
560        let current_page = app.lambda_state.version_table.selected / page_size;
561        let pagination = render_pagination_text(current_page, total_pages);
562
563        crate::ui::filter::render_simple_filter(
564            frame,
565            version_chunks[0],
566            crate::ui::filter::SimpleFilterConfig {
567                filter_text: &app.lambda_state.version_table.filter,
568                placeholder: "Filter by attributes or search by keyword",
569                pagination: &pagination,
570                mode: app.mode,
571                is_input_focused: app.lambda_state.version_input_focus == InputFocus::Filter,
572                is_pagination_focused: app.lambda_state.version_input_focus
573                    == InputFocus::Pagination,
574            },
575        );
576
577        // Table
578        let filtered: Vec<_> = app
579            .lambda_state
580            .version_table
581            .items
582            .iter()
583            .filter(|v| {
584                app.lambda_state.version_table.filter.is_empty()
585                    || v.version
586                        .to_lowercase()
587                        .contains(&app.lambda_state.version_table.filter.to_lowercase())
588                    || v.aliases
589                        .to_lowercase()
590                        .contains(&app.lambda_state.version_table.filter.to_lowercase())
591                    || v.description
592                        .to_lowercase()
593                        .contains(&app.lambda_state.version_table.filter.to_lowercase())
594            })
595            .collect();
596
597        let start_idx = current_page * page_size;
598        let end_idx = (start_idx + page_size).min(filtered.len());
599        let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
600
601        let title = format!(" Versions ({}) ", filtered.len());
602
603        let columns: Vec<Box<dyn TableColumn<crate::lambda::Version>>> = app
604            .lambda_state
605            .visible_version_columns
606            .iter()
607            .map(|col| col.to_column())
608            .collect();
609
610        let expanded_index = if let Some(expanded) = app.lambda_state.version_table.expanded_item {
611            if expanded >= start_idx && expanded < end_idx {
612                Some(expanded - start_idx)
613            } else {
614                None
615            }
616        } else {
617            None
618        };
619
620        let config = TableConfig {
621            items: paginated,
622            selected_index: app.lambda_state.version_table.selected % page_size,
623            expanded_index,
624            columns: &columns,
625            sort_column: "Version",
626            sort_direction: SortDirection::Desc,
627            title,
628            area: version_chunks[1],
629            get_expanded_content: Some(Box::new(|ver: &crate::lambda::Version| {
630                expanded_from_columns(&columns, ver)
631            })),
632            is_active: app.mode != Mode::FilterInput,
633        };
634
635        render_table(frame, config);
636    } else if app.lambda_state.detail_tab == DetailTab::Aliases {
637        // Aliases tab
638        let alias_chunks = vertical(
639            [
640                Constraint::Length(3), // Filter
641                Constraint::Min(0),    // Table
642            ],
643            chunks[2],
644        );
645
646        // Filter
647        let page_size = app.lambda_state.alias_table.page_size.value();
648        let filtered_count: usize = app
649            .lambda_state
650            .alias_table
651            .items
652            .iter()
653            .filter(|a| {
654                app.lambda_state.alias_table.filter.is_empty()
655                    || a.name
656                        .to_lowercase()
657                        .contains(&app.lambda_state.alias_table.filter.to_lowercase())
658                    || a.versions
659                        .to_lowercase()
660                        .contains(&app.lambda_state.alias_table.filter.to_lowercase())
661                    || a.description
662                        .to_lowercase()
663                        .contains(&app.lambda_state.alias_table.filter.to_lowercase())
664            })
665            .count();
666
667        let total_pages = filtered_count.div_ceil(page_size);
668        let current_page = app.lambda_state.alias_table.selected / page_size;
669        let pagination = render_pagination_text(current_page, total_pages);
670
671        crate::ui::filter::render_simple_filter(
672            frame,
673            alias_chunks[0],
674            crate::ui::filter::SimpleFilterConfig {
675                filter_text: &app.lambda_state.alias_table.filter,
676                placeholder: "Filter by attributes or search by keyword",
677                pagination: &pagination,
678                mode: app.mode,
679                is_input_focused: app.lambda_state.alias_input_focus == InputFocus::Filter,
680                is_pagination_focused: app.lambda_state.alias_input_focus == InputFocus::Pagination,
681            },
682        );
683
684        // Table
685        let filtered: Vec<_> = app
686            .lambda_state
687            .alias_table
688            .items
689            .iter()
690            .filter(|a| {
691                app.lambda_state.alias_table.filter.is_empty()
692                    || a.name
693                        .to_lowercase()
694                        .contains(&app.lambda_state.alias_table.filter.to_lowercase())
695                    || a.versions
696                        .to_lowercase()
697                        .contains(&app.lambda_state.alias_table.filter.to_lowercase())
698                    || a.description
699                        .to_lowercase()
700                        .contains(&app.lambda_state.alias_table.filter.to_lowercase())
701            })
702            .collect();
703
704        let start_idx = current_page * page_size;
705        let end_idx = (start_idx + page_size).min(filtered.len());
706        let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
707
708        let title = format!(" Aliases ({}) ", filtered.len());
709
710        let columns: Vec<Box<dyn TableColumn<crate::lambda::Alias>>> = app
711            .lambda_state
712            .visible_alias_columns
713            .iter()
714            .map(|col| col.to_column())
715            .collect();
716
717        let expanded_index = if let Some(expanded) = app.lambda_state.alias_table.expanded_item {
718            if expanded >= start_idx && expanded < end_idx {
719                Some(expanded - start_idx)
720            } else {
721                None
722            }
723        } else {
724            None
725        };
726
727        let config = TableConfig {
728            items: paginated,
729            selected_index: app.lambda_state.alias_table.selected % page_size,
730            expanded_index,
731            columns: &columns,
732            sort_column: "Name",
733            sort_direction: SortDirection::Asc,
734            title,
735            area: alias_chunks[1],
736            get_expanded_content: Some(Box::new(|alias: &crate::lambda::Alias| {
737                expanded_from_columns(&columns, alias)
738            })),
739            is_active: app.mode != Mode::FilterInput,
740        };
741
742        render_table(frame, config);
743    } else {
744        // Placeholder for other tabs
745        let content = Paragraph::new(format!(
746            "{} tab content (coming soon)",
747            app.lambda_state.detail_tab.name()
748        ))
749        .block(Block::default().borders(Borders::ALL));
750        frame.render_widget(content, chunks[2]);
751    }
752}
753
754pub fn render_alias_detail(frame: &mut Frame, app: &App, area: Rect) {
755    frame.render_widget(Clear, area);
756
757    // Build overview lines first to calculate height
758    let mut overview_lines = vec![];
759    if let Some(func_name) = &app.lambda_state.current_function {
760        if let Some(func) = app
761            .lambda_state
762            .table
763            .items
764            .iter()
765            .find(|f| f.name == *func_name)
766        {
767            if let Some(alias_name) = &app.lambda_state.current_alias {
768                if let Some(alias) = app
769                    .lambda_state
770                    .alias_table
771                    .items
772                    .iter()
773                    .find(|a| a.name == *alias_name)
774                {
775                    overview_lines.push(labeled_field("Description", &alias.description));
776
777                    // Parse versions
778                    let versions_parts: Vec<&str> =
779                        alias.versions.split(',').map(|s| s.trim()).collect();
780                    if let Some(first_version) = versions_parts.first() {
781                        overview_lines.push(labeled_field("Version", *first_version));
782                    }
783                    if versions_parts.len() > 1 {
784                        if let Some(second_version) = versions_parts.get(1) {
785                            overview_lines
786                                .push(labeled_field("Additional version", *second_version));
787                        }
788                    }
789
790                    overview_lines.push(labeled_field("Function ARN", &func.arn));
791
792                    if let Some(app) = &func.application {
793                        overview_lines.push(labeled_field("Application", app));
794                    }
795
796                    overview_lines.push(labeled_field("Function URL", "-"));
797                }
798            }
799        }
800    }
801
802    // Build config lines to calculate height
803    let mut config_lines = vec![];
804    if let Some(_func_name) = &app.lambda_state.current_function {
805        if let Some(alias_name) = &app.lambda_state.current_alias {
806            if let Some(alias) = app
807                .lambda_state
808                .alias_table
809                .items
810                .iter()
811                .find(|a| a.name == *alias_name)
812            {
813                config_lines.push(labeled_field("Name", &alias.name));
814                config_lines.push(labeled_field("Description", &alias.description));
815
816                // Parse versions
817                let versions_parts: Vec<&str> =
818                    alias.versions.split(',').map(|s| s.trim()).collect();
819                if let Some(first_version) = versions_parts.first() {
820                    config_lines.push(labeled_field("Version", *first_version));
821                }
822                if versions_parts.len() > 1 {
823                    if let Some(second_version) = versions_parts.get(1) {
824                        config_lines.push(labeled_field("Additional version", *second_version));
825                    }
826                }
827            }
828        }
829    }
830
831    let config_height = if config_lines.is_empty() {
832        0
833    } else {
834        config_lines.len() as u16 + 2
835    };
836
837    let overview_height = overview_lines.len() as u16 + 2; // +2 for borders
838
839    let chunks = vertical(
840        [
841            Constraint::Length(overview_height),
842            Constraint::Length(config_height),
843            Constraint::Min(0), // Empty space
844        ],
845        area,
846    );
847
848    // Function overview
849    if let Some(func_name) = &app.lambda_state.current_function {
850        if let Some(_func) = app
851            .lambda_state
852            .table
853            .items
854            .iter()
855            .find(|f| f.name == *func_name)
856        {
857            if let Some(alias_name) = &app.lambda_state.current_alias {
858                if let Some(_alias) = app
859                    .lambda_state
860                    .alias_table
861                    .items
862                    .iter()
863                    .find(|a| a.name == *alias_name)
864                {
865                    let overview_block = Block::default()
866                        .title(" Function overview ")
867                        .borders(Borders::ALL)
868                        .border_type(BorderType::Plain)
869                        .border_style(Style::default());
870
871                    let overview_inner = overview_block.inner(chunks[0]);
872                    frame.render_widget(overview_block, chunks[0]);
873
874                    frame.render_widget(Paragraph::new(overview_lines), overview_inner);
875                }
876            }
877        }
878    }
879
880    // General configuration
881    if !config_lines.is_empty() {
882        let config_block = Block::default()
883            .title(" General configuration ")
884            .borders(Borders::ALL)
885            .border_type(BorderType::Plain);
886
887        let config_inner = config_block.inner(chunks[1]);
888        frame.render_widget(config_block, chunks[1]);
889        frame.render_widget(Paragraph::new(config_lines), config_inner);
890    }
891}
892
893pub fn render_version_detail(frame: &mut Frame, app: &App, area: Rect) {
894    frame.render_widget(Clear, area);
895
896    // Build overview lines first to calculate height
897    let mut overview_lines = vec![];
898    if let Some(func_name) = &app.lambda_state.current_function {
899        if let Some(func) = app
900            .lambda_state
901            .table
902            .items
903            .iter()
904            .find(|f| f.name == *func_name)
905        {
906            if let Some(version_num) = &app.lambda_state.current_version {
907                let version_arn = format!("{}:{}", func.arn, version_num);
908
909                overview_lines.push(labeled_field("Name", &func.name));
910
911                if let Some(app) = &func.application {
912                    overview_lines.push(labeled_field("Application", app));
913                }
914
915                overview_lines.extend(vec![
916                    labeled_field("ARN", version_arn),
917                    labeled_field("Version", version_num),
918                ]);
919            }
920        }
921    }
922
923    let overview_height = if overview_lines.is_empty() {
924        0
925    } else {
926        overview_lines.len() as u16 + 2
927    };
928
929    let chunks = vertical(
930        [
931            Constraint::Length(overview_height),
932            Constraint::Length(1), // Tabs (Code, Configuration only)
933            Constraint::Min(0),    // Content
934        ],
935        area,
936    );
937
938    // Function overview
939    if !overview_lines.is_empty() {
940        let overview_block = Block::default()
941            .title(" Function overview ")
942            .borders(Borders::ALL)
943            .border_type(BorderType::Plain)
944            .border_style(Style::default());
945
946        let overview_inner = overview_block.inner(chunks[0]);
947        frame.render_widget(overview_block, chunks[0]);
948        frame.render_widget(Paragraph::new(overview_lines), overview_inner);
949    }
950
951    // Tabs - only Code and Configuration
952    let tabs = [
953        ("Code", DetailTab::Code),
954        ("Configuration", DetailTab::Configuration),
955    ];
956
957    render_tabs(frame, chunks[1], &tabs, &app.lambda_state.detail_tab);
958
959    // Content area - reuse same rendering as function detail
960    if app.lambda_state.detail_tab == DetailTab::Code {
961        if let Some(func_name) = &app.lambda_state.current_function {
962            if let Some(func) = app
963                .lambda_state
964                .table
965                .items
966                .iter()
967                .find(|f| f.name == *func_name)
968            {
969                let chunks_content = Layout::default()
970                    .direction(Direction::Vertical)
971                    .constraints([
972                        Constraint::Length(5),
973                        Constraint::Length(5),
974                        Constraint::Min(0),
975                    ])
976                    .split(chunks[2]);
977
978                // Code properties section
979                let code_block = Block::default()
980                    .title(" Code properties ")
981                    .borders(Borders::ALL)
982                    .border_type(BorderType::Plain);
983
984                let code_inner = code_block.inner(chunks_content[0]);
985                frame.render_widget(code_block, chunks_content[0]);
986
987                let code_lines = vec![
988                    labeled_field("Package size", format_bytes(func.code_size)),
989                    labeled_field("SHA256 hash", &func.code_sha256),
990                    labeled_field("Last modified", &func.last_modified),
991                ];
992
993                frame.render_widget(Paragraph::new(code_lines), code_inner);
994
995                // Runtime settings section
996                let runtime_block = Block::default()
997                    .title(" Runtime settings ")
998                    .borders(Borders::ALL)
999                    .border_type(BorderType::Plain);
1000
1001                let runtime_inner = runtime_block.inner(chunks_content[1]);
1002                frame.render_widget(runtime_block, chunks_content[1]);
1003
1004                let runtime_lines = vec![
1005                    labeled_field("Runtime", format_runtime(&func.runtime)),
1006                    labeled_field("Handler", ""),
1007                    labeled_field("Architecture", format_architecture(&func.architecture)),
1008                ];
1009
1010                frame.render_widget(Paragraph::new(runtime_lines), runtime_inner);
1011
1012                // Layers section (empty table)
1013                #[derive(Clone)]
1014                struct Layer {
1015                    merge_order: String,
1016                    name: String,
1017                    layer_version: String,
1018                    compatible_runtimes: String,
1019                    compatible_architectures: String,
1020                    version_arn: String,
1021                }
1022
1023                let layers: Vec<Layer> = vec![];
1024                let layer_refs: Vec<&Layer> = layers.iter().collect();
1025                let title = format!(" Layers ({}) ", layer_refs.len());
1026
1027                let columns: Vec<Box<dyn TableColumn<Layer>>> = vec![
1028                    Box::new(column!(name="Merge order", width=12, type=Layer, field=merge_order)),
1029                    Box::new(column!(name="Name", width=20, type=Layer, field=name)),
1030                    Box::new(
1031                        column!(name="Layer version", width=14, type=Layer, field=layer_version),
1032                    ),
1033                    Box::new(
1034                        column!(name="Compatible runtimes", width=20, type=Layer, field=compatible_runtimes),
1035                    ),
1036                    Box::new(
1037                        column!(name="Compatible architectures", width=26, type=Layer, field=compatible_architectures),
1038                    ),
1039                    Box::new(column!(name="Version ARN", width=40, type=Layer, field=version_arn)),
1040                ];
1041
1042                let config = TableConfig {
1043                    items: layer_refs,
1044                    selected_index: 0,
1045                    expanded_index: None,
1046                    columns: &columns,
1047                    sort_column: "",
1048                    sort_direction: SortDirection::Asc,
1049                    title,
1050                    area: chunks_content[2],
1051                    get_expanded_content: Some(Box::new(|layer: &Layer| {
1052                        crate::ui::format_expansion_text(&[
1053                            ("Merge order", layer.merge_order.clone()),
1054                            ("Name", layer.name.clone()),
1055                            ("Layer version", layer.layer_version.clone()),
1056                            ("Compatible runtimes", layer.compatible_runtimes.clone()),
1057                            (
1058                                "Compatible architectures",
1059                                layer.compatible_architectures.clone(),
1060                            ),
1061                            ("Version ARN", layer.version_arn.clone()),
1062                        ])
1063                    })),
1064                    is_active: app.lambda_state.detail_tab == DetailTab::Code,
1065                };
1066
1067                render_table(frame, config);
1068            }
1069        }
1070    } else if app.lambda_state.detail_tab == DetailTab::Configuration {
1071        if let Some(func_name) = &app.lambda_state.current_function {
1072            if let Some(func) = app
1073                .lambda_state
1074                .table
1075                .items
1076                .iter()
1077                .find(|f| f.name == *func_name)
1078            {
1079                if let Some(version_num) = &app.lambda_state.current_version {
1080                    // Version Configuration: show config + aliases for this version
1081                    let chunks_content = Layout::default()
1082                        .direction(Direction::Vertical)
1083                        .constraints([
1084                            Constraint::Length(5),
1085                            Constraint::Length(3), // Filter
1086                            Constraint::Min(0),    // Aliases table
1087                        ])
1088                        .split(chunks[2]);
1089
1090                    let config_block = Block::default()
1091                        .title(" General configuration ")
1092                        .borders(Borders::ALL)
1093                        .border_type(BorderType::Plain)
1094                        .border_style(Style::default());
1095
1096                    let config_inner = config_block.inner(chunks_content[0]);
1097                    frame.render_widget(config_block, chunks_content[0]);
1098
1099                    let config_lines = vec![
1100                        labeled_field("Description", &func.description),
1101                        labeled_field("Memory", format_memory_mb(func.memory_mb)),
1102                        labeled_field("Timeout", format_duration_seconds(func.timeout_seconds)),
1103                    ];
1104
1105                    frame.render_widget(Paragraph::new(config_lines), config_inner);
1106
1107                    // Filter for aliases
1108                    let page_size = app.lambda_state.alias_table.page_size.value();
1109                    let filtered_count: usize = app
1110                        .lambda_state
1111                        .alias_table
1112                        .items
1113                        .iter()
1114                        .filter(|a| {
1115                            a.versions.contains(version_num)
1116                                && (app.lambda_state.alias_table.filter.is_empty()
1117                                    || a.name.to_lowercase().contains(
1118                                        &app.lambda_state.alias_table.filter.to_lowercase(),
1119                                    )
1120                                    || a.versions.to_lowercase().contains(
1121                                        &app.lambda_state.alias_table.filter.to_lowercase(),
1122                                    )
1123                                    || a.description.to_lowercase().contains(
1124                                        &app.lambda_state.alias_table.filter.to_lowercase(),
1125                                    ))
1126                        })
1127                        .count();
1128
1129                    let total_pages = filtered_count.div_ceil(page_size);
1130                    let current_page = app.lambda_state.alias_table.selected / page_size;
1131                    let pagination = render_pagination_text(current_page, total_pages);
1132
1133                    crate::ui::filter::render_simple_filter(
1134                        frame,
1135                        chunks_content[1],
1136                        crate::ui::filter::SimpleFilterConfig {
1137                            filter_text: &app.lambda_state.alias_table.filter,
1138                            placeholder: "Filter by attributes or search by keyword",
1139                            pagination: &pagination,
1140                            mode: app.mode,
1141                            is_input_focused: app.lambda_state.alias_input_focus
1142                                == InputFocus::Filter,
1143                            is_pagination_focused: app.lambda_state.alias_input_focus
1144                                == InputFocus::Pagination,
1145                        },
1146                    );
1147
1148                    // Aliases table - filter to show only aliases pointing to this version
1149                    let filtered: Vec<_> = app
1150                        .lambda_state
1151                        .alias_table
1152                        .items
1153                        .iter()
1154                        .filter(|a| {
1155                            a.versions.contains(version_num)
1156                                && (app.lambda_state.alias_table.filter.is_empty()
1157                                    || a.name.to_lowercase().contains(
1158                                        &app.lambda_state.alias_table.filter.to_lowercase(),
1159                                    )
1160                                    || a.versions.to_lowercase().contains(
1161                                        &app.lambda_state.alias_table.filter.to_lowercase(),
1162                                    )
1163                                    || a.description.to_lowercase().contains(
1164                                        &app.lambda_state.alias_table.filter.to_lowercase(),
1165                                    ))
1166                        })
1167                        .collect();
1168
1169                    let start_idx = current_page * page_size;
1170                    let end_idx = (start_idx + page_size).min(filtered.len());
1171                    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1172
1173                    let title = format!(" Aliases ({}) ", filtered.len());
1174
1175                    let columns: Vec<Box<dyn TableColumn<crate::lambda::Alias>>> = app
1176                        .lambda_state
1177                        .visible_alias_columns
1178                        .iter()
1179                        .map(|col| col.to_column())
1180                        .collect();
1181
1182                    let expanded_index =
1183                        if let Some(expanded) = app.lambda_state.alias_table.expanded_item {
1184                            if expanded >= start_idx && expanded < end_idx {
1185                                Some(expanded - start_idx)
1186                            } else {
1187                                None
1188                            }
1189                        } else {
1190                            None
1191                        };
1192
1193                    let config = TableConfig {
1194                        items: paginated,
1195                        selected_index: app.lambda_state.alias_table.selected % page_size,
1196                        expanded_index,
1197                        columns: &columns,
1198                        sort_column: "Name",
1199                        sort_direction: SortDirection::Asc,
1200                        title,
1201                        area: chunks_content[2],
1202                        get_expanded_content: Some(Box::new(|alias: &crate::lambda::Alias| {
1203                            expanded_from_columns(&columns, alias)
1204                        })),
1205                        is_active: app.mode != Mode::FilterInput,
1206                    };
1207
1208                    render_table(frame, config);
1209                }
1210            }
1211        }
1212    }
1213}
1214
1215pub fn render_applications(frame: &mut Frame, app: &App, area: Rect) {
1216    frame.render_widget(Clear, area);
1217
1218    if app.lambda_application_state.current_application.is_some() {
1219        render_application_detail(frame, app, area);
1220        return;
1221    }
1222
1223    let chunks = Layout::default()
1224        .direction(Direction::Vertical)
1225        .constraints([Constraint::Length(3), Constraint::Min(0)])
1226        .split(area);
1227
1228    // Filter with pagination
1229    let page_size = app.lambda_application_state.table.page_size.value();
1230    let filtered_count = filtered_lambda_applications(app).len();
1231    let total_pages = filtered_count.div_ceil(page_size);
1232    let current_page = app.lambda_application_state.table.selected / page_size;
1233    let pagination = render_pagination_text(current_page, total_pages);
1234
1235    crate::ui::filter::render_simple_filter(
1236        frame,
1237        chunks[0],
1238        crate::ui::filter::SimpleFilterConfig {
1239            filter_text: &app.lambda_application_state.table.filter,
1240            placeholder: "Filter by attributes or search by keyword",
1241            pagination: &pagination,
1242            mode: app.mode,
1243            is_input_focused: app.lambda_application_state.input_focus == InputFocus::Filter,
1244            is_pagination_focused: app.lambda_application_state.input_focus
1245                == InputFocus::Pagination,
1246        },
1247    );
1248
1249    // Table
1250    let filtered: Vec<_> = filtered_lambda_applications(app);
1251    let start_idx = current_page * page_size;
1252    let end_idx = (start_idx + page_size).min(filtered.len());
1253    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1254
1255    let title = format!(" Applications ({}) ", filtered.len());
1256
1257    let mut columns: Vec<Box<dyn TableColumn<LambdaApplication>>> = vec![];
1258    for col in &app.visible_lambda_application_columns {
1259        columns.push(col.to_column());
1260    }
1261
1262    let expanded_index = if let Some(expanded) = app.lambda_application_state.table.expanded_item {
1263        if expanded >= start_idx && expanded < end_idx {
1264            Some(expanded - start_idx)
1265        } else {
1266            None
1267        }
1268    } else {
1269        None
1270    };
1271
1272    let config = TableConfig {
1273        items: paginated,
1274        selected_index: app.lambda_application_state.table.selected % page_size,
1275        expanded_index,
1276        columns: &columns,
1277        sort_column: "Last modified",
1278        sort_direction: SortDirection::Desc,
1279        title,
1280        area: chunks[1],
1281        get_expanded_content: Some(Box::new(|app: &LambdaApplication| {
1282            expanded_from_columns(&columns, app)
1283        })),
1284        is_active: app.mode != Mode::FilterInput,
1285    };
1286
1287    render_table(frame, config);
1288}
1289
1290// Lambda-specific helper functions
1291pub fn filtered_lambda_functions(app: &App) -> Vec<&LambdaFunction> {
1292    if app.lambda_state.table.filter.is_empty() {
1293        app.lambda_state.table.items.iter().collect()
1294    } else {
1295        app.lambda_state
1296            .table
1297            .items
1298            .iter()
1299            .filter(|f| {
1300                f.name
1301                    .to_lowercase()
1302                    .contains(&app.lambda_state.table.filter.to_lowercase())
1303                    || f.description
1304                        .to_lowercase()
1305                        .contains(&app.lambda_state.table.filter.to_lowercase())
1306                    || f.runtime
1307                        .to_lowercase()
1308                        .contains(&app.lambda_state.table.filter.to_lowercase())
1309            })
1310            .collect()
1311    }
1312}
1313
1314pub fn filtered_lambda_applications(app: &App) -> Vec<&LambdaApplication> {
1315    if app.lambda_application_state.table.filter.is_empty() {
1316        app.lambda_application_state.table.items.iter().collect()
1317    } else {
1318        app.lambda_application_state
1319            .table
1320            .items
1321            .iter()
1322            .filter(|a| {
1323                a.name
1324                    .to_lowercase()
1325                    .contains(&app.lambda_application_state.table.filter.to_lowercase())
1326                    || a.description
1327                        .to_lowercase()
1328                        .contains(&app.lambda_application_state.table.filter.to_lowercase())
1329                    || a.status
1330                        .to_lowercase()
1331                        .contains(&app.lambda_application_state.table.filter.to_lowercase())
1332            })
1333            .collect()
1334    }
1335}
1336
1337pub async fn load_lambda_functions(app: &mut App) -> anyhow::Result<()> {
1338    let functions = app.lambda_client.list_functions().await?;
1339
1340    let mut functions: Vec<LambdaFunction> = functions
1341        .into_iter()
1342        .map(|f| LambdaFunction {
1343            name: f.name,
1344            arn: f.arn,
1345            application: f.application,
1346            description: f.description,
1347            package_type: f.package_type,
1348            runtime: f.runtime,
1349            architecture: f.architecture,
1350            code_size: f.code_size,
1351            code_sha256: f.code_sha256,
1352            memory_mb: f.memory_mb,
1353            timeout_seconds: f.timeout_seconds,
1354            last_modified: f.last_modified,
1355            layers: f
1356                .layers
1357                .into_iter()
1358                .map(|l| Layer {
1359                    arn: l.arn,
1360                    code_size: l.code_size,
1361                })
1362                .collect(),
1363        })
1364        .collect();
1365
1366    // Sort by last_modified DESC
1367    functions.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
1368
1369    app.lambda_state.table.items = functions;
1370
1371    Ok(())
1372}
1373
1374pub async fn load_lambda_applications(app: &mut App) -> anyhow::Result<()> {
1375    let applications = app.lambda_client.list_applications().await?;
1376    let mut applications: Vec<LambdaApplication> = applications
1377        .into_iter()
1378        .map(|a| LambdaApplication {
1379            name: a.name,
1380            arn: a.arn,
1381            description: a.description,
1382            status: a.status,
1383            last_modified: a.last_modified,
1384        })
1385        .collect();
1386    applications.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
1387    app.lambda_application_state.table.items = applications;
1388    Ok(())
1389}
1390
1391pub async fn load_lambda_versions(app: &mut App, function_name: &str) -> anyhow::Result<()> {
1392    let versions = app.lambda_client.list_versions(function_name).await?;
1393    let mut versions: Vec<crate::lambda::Version> = versions
1394        .into_iter()
1395        .map(|v| crate::lambda::Version {
1396            version: v.version,
1397            aliases: v.aliases,
1398            description: v.description,
1399            last_modified: v.last_modified,
1400            architecture: v.architecture,
1401        })
1402        .collect();
1403
1404    // Sort by version DESC (numeric sort)
1405    versions.sort_by(|a, b| {
1406        let a_num = a.version.parse::<i32>().unwrap_or(0);
1407        let b_num = b.version.parse::<i32>().unwrap_or(0);
1408        b_num.cmp(&a_num)
1409    });
1410
1411    app.lambda_state.version_table.items = versions;
1412    Ok(())
1413}
1414
1415pub async fn load_lambda_aliases(app: &mut App, function_name: &str) -> anyhow::Result<()> {
1416    let aliases = app.lambda_client.list_aliases(function_name).await?;
1417    let mut aliases: Vec<crate::lambda::Alias> = aliases
1418        .into_iter()
1419        .map(|a| crate::lambda::Alias {
1420            name: a.name,
1421            versions: a.versions,
1422            description: a.description,
1423        })
1424        .collect();
1425
1426    // Sort by name ASC
1427    aliases.sort_by(|a, b| a.name.cmp(&b.name));
1428
1429    app.lambda_state.alias_table.items = aliases;
1430    Ok(())
1431}
1432
1433pub fn render_application_detail(frame: &mut Frame, app: &App, area: Rect) {
1434    frame.render_widget(Clear, area);
1435
1436    let chunks = vertical(
1437        [
1438            Constraint::Length(1), // Application name
1439            Constraint::Length(1), // Tabs
1440            Constraint::Min(0),    // Content
1441        ],
1442        area,
1443    );
1444
1445    // Application name
1446    if let Some(app_name) = &app.lambda_application_state.current_application {
1447        frame.render_widget(Paragraph::new(app_name.as_str()), chunks[0]);
1448    }
1449
1450    // Tabs
1451    let tabs = [
1452        ("Overview", ApplicationDetailTab::Overview),
1453        ("Deployments", ApplicationDetailTab::Deployments),
1454    ];
1455    render_tabs(
1456        frame,
1457        chunks[1],
1458        &tabs,
1459        &app.lambda_application_state.detail_tab,
1460    );
1461
1462    // Content
1463    if app.lambda_application_state.detail_tab == ApplicationDetailTab::Overview {
1464        let chunks_content = vertical(
1465            [
1466                Constraint::Length(3), // Filter
1467                Constraint::Min(0),    // Table
1468            ],
1469            chunks[2],
1470        );
1471
1472        // Filter with pagination
1473        let page_size = app.lambda_application_state.resources.page_size.value();
1474        let filtered_count = app.lambda_application_state.resources.items.len();
1475        let total_pages = filtered_count.div_ceil(page_size);
1476        let current_page = app.lambda_application_state.resources.selected / page_size;
1477        let pagination = render_pagination_text(current_page, total_pages);
1478
1479        crate::ui::filter::render_simple_filter(
1480            frame,
1481            chunks_content[0],
1482            crate::ui::filter::SimpleFilterConfig {
1483                filter_text: &app.lambda_application_state.resources.filter,
1484                placeholder: "Filter by attributes or search by keyword",
1485                pagination: &pagination,
1486                mode: app.mode,
1487                is_input_focused: app.lambda_application_state.resource_input_focus
1488                    == InputFocus::Filter,
1489                is_pagination_focused: app.lambda_application_state.resource_input_focus
1490                    == InputFocus::Pagination,
1491            },
1492        );
1493
1494        // Resources table
1495        let title = format!(
1496            " Resources ({}) ",
1497            app.lambda_application_state.resources.items.len()
1498        );
1499
1500        let columns: Vec<Box<dyn TableColumn<Resource>>> = vec![
1501            Box::new(column!(name="Logical ID", width=30, type=Resource, field=logical_id)),
1502            Box::new(column!(name="Physical ID", width=40, type=Resource, field=physical_id)),
1503            Box::new(column!(name="Type", width=30, type=Resource, field=resource_type)),
1504            Box::new(column!(name="Last modified", width=27, type=Resource, field=last_modified)),
1505        ];
1506
1507        let start_idx = current_page * page_size;
1508        let end_idx = (start_idx + page_size).min(filtered_count);
1509        let paginated: Vec<&Resource> = app.lambda_application_state.resources.items
1510            [start_idx..end_idx]
1511            .iter()
1512            .collect();
1513
1514        let config = TableConfig {
1515            items: paginated,
1516            selected_index: app.lambda_application_state.resources.selected,
1517            expanded_index: app.lambda_application_state.resources.expanded_item,
1518            columns: &columns,
1519            sort_column: "Logical ID",
1520            sort_direction: SortDirection::Asc,
1521            title,
1522            area: chunks_content[1],
1523            get_expanded_content: Some(Box::new(|res: &Resource| {
1524                crate::ui::table::plain_expanded_content(format!(
1525                    "Logical ID: {}\nPhysical ID: {}\nType: {}\nLast modified: {}",
1526                    res.logical_id, res.physical_id, res.resource_type, res.last_modified
1527                ))
1528            })),
1529            is_active: true,
1530        };
1531
1532        render_table(frame, config);
1533    } else if app.lambda_application_state.detail_tab == ApplicationDetailTab::Deployments {
1534        let chunks_content = vertical(
1535            [
1536                Constraint::Length(3), // Filter
1537                Constraint::Min(0),    // Table
1538            ],
1539            chunks[2],
1540        );
1541
1542        // Filter with pagination
1543        let page_size = app.lambda_application_state.deployments.page_size.value();
1544        let filtered_count = app.lambda_application_state.deployments.items.len();
1545        let total_pages = filtered_count.div_ceil(page_size);
1546        let current_page = app.lambda_application_state.deployments.selected / page_size;
1547        let pagination = render_pagination_text(current_page, total_pages);
1548
1549        crate::ui::filter::render_simple_filter(
1550            frame,
1551            chunks_content[0],
1552            crate::ui::filter::SimpleFilterConfig {
1553                filter_text: &app.lambda_application_state.deployments.filter,
1554                placeholder: "Filter by attributes or search by keyword",
1555                pagination: &pagination,
1556                mode: app.mode,
1557                is_input_focused: app.lambda_application_state.deployment_input_focus
1558                    == InputFocus::Filter,
1559                is_pagination_focused: app.lambda_application_state.deployment_input_focus
1560                    == InputFocus::Pagination,
1561            },
1562        );
1563
1564        // Table
1565        let title = format!(
1566            " Deployment history ({}) ",
1567            app.lambda_application_state.deployments.items.len()
1568        );
1569
1570        use crate::lambda::DeploymentColumn;
1571        let columns: Vec<Box<dyn TableColumn<Deployment>>> = vec![
1572            DeploymentColumn::Deployment.as_table_column(),
1573            DeploymentColumn::ResourceType.as_table_column(),
1574            DeploymentColumn::LastUpdated.as_table_column(),
1575            DeploymentColumn::Status.as_table_column(),
1576        ];
1577
1578        let start_idx = current_page * page_size;
1579        let end_idx = (start_idx + page_size).min(filtered_count);
1580        let paginated: Vec<&Deployment> = app.lambda_application_state.deployments.items
1581            [start_idx..end_idx]
1582            .iter()
1583            .collect();
1584
1585        let config = TableConfig {
1586            items: paginated,
1587            selected_index: app.lambda_application_state.deployments.selected,
1588            expanded_index: app.lambda_application_state.deployments.expanded_item,
1589            columns: &columns,
1590            sort_column: "",
1591            sort_direction: SortDirection::Asc,
1592            title,
1593            area: chunks_content[1],
1594            get_expanded_content: Some(Box::new(|dep: &Deployment| {
1595                crate::ui::table::plain_expanded_content(format!(
1596                    "Deployment: {}\nResource type: {}\nLast updated: {}\nStatus: {}",
1597                    dep.deployment_id, dep.resource_type, dep.last_updated, dep.status
1598                ))
1599            })),
1600            is_active: true,
1601        };
1602
1603        render_table(frame, config);
1604    }
1605}