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), Constraint::Min(0), ],
196 area,
197 );
198
199 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 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 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), Constraint::Min(0), ],
342 area,
343 );
344
345 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 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 if app.lambda_state.detail_tab == DetailTab::Code {
368 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), Constraint::Length(8), Constraint::Min(0), ])
384 .split(chunks[2]);
385
386 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 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 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 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 let version_chunks = vertical(
506 [
507 Constraint::Length(3), Constraint::Min(0), ],
510 chunks[2],
511 );
512
513 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 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 let alias_chunks = vertical(
622 [
623 Constraint::Length(3), Constraint::Min(0), ],
626 chunks[2],
627 );
628
629 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 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 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 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 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 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 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; let chunks = vertical(
829 [
830 Constraint::Length(overview_height),
831 Constraint::Length(config_height),
832 Constraint::Min(0), ],
834 area,
835 );
836
837 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 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 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), Constraint::Min(0), ],
924 area,
925 );
926
927 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 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 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 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 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 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 let chunks_content = Layout::default()
1055 .direction(Direction::Vertical)
1056 .constraints([
1057 Constraint::Length(5),
1058 Constraint::Length(3), Constraint::Min(0), ])
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 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 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 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 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
1271pub 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 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 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 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), Constraint::Length(1), Constraint::Min(0), ],
1431 area,
1432 );
1433
1434 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 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 if app.lambda_application_state.detail_tab == ApplicationDetailTab::Overview {
1453 let chunks_content = vertical(
1454 [
1455 Constraint::Length(3), Constraint::Min(0), ],
1458 chunks[2],
1459 );
1460
1461 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 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 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), Constraint::Min(0), ],
1536 chunks[2],
1537 );
1538
1539 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 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}