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), Constraint::Min(0), ],
185 area,
186 );
187
188 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 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 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), Constraint::Min(0), ],
331 area,
332 );
333
334 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 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 if app.lambda_state.detail_tab == DetailTab::Code {
359 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), Constraint::Length(8), Constraint::Min(0), ])
375 .split(chunks[2]);
376
377 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 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 #[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 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 let version_chunks = vertical(
531 [
532 Constraint::Length(3), Constraint::Min(0), ],
535 chunks[2],
536 );
537
538 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 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 let alias_chunks = vertical(
639 [
640 Constraint::Length(3), Constraint::Min(0), ],
643 chunks[2],
644 );
645
646 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 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 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 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 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 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 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; let chunks = vertical(
840 [
841 Constraint::Length(overview_height),
842 Constraint::Length(config_height),
843 Constraint::Min(0), ],
845 area,
846 );
847
848 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 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 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), Constraint::Min(0), ],
935 area,
936 );
937
938 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 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 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 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 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 #[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 let chunks_content = Layout::default()
1082 .direction(Direction::Vertical)
1083 .constraints([
1084 Constraint::Length(5),
1085 Constraint::Length(3), Constraint::Min(0), ])
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 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 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 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 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
1290pub 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 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 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 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), Constraint::Length(1), Constraint::Min(0), ],
1442 area,
1443 );
1444
1445 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 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 if app.lambda_application_state.detail_tab == ApplicationDetailTab::Overview {
1464 let chunks_content = vertical(
1465 [
1466 Constraint::Length(3), Constraint::Min(0), ],
1469 chunks[2],
1470 );
1471
1472 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 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), Constraint::Min(0), ],
1539 chunks[2],
1540 );
1541
1542 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 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}