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