Skip to main content

rusticity_term/ui/
ecr.rs

1use crate::app::App;
2use crate::common::{
3    filter_by_field, render_pagination_text, CyclicEnum, InputFocus, SortDirection,
4};
5use crate::ecr::image::{self, Image as EcrImage};
6use crate::ecr::repo::{self, Repository as EcrRepository};
7use crate::keymap::Mode;
8use crate::table::TableState;
9use crate::ui::filter::{render_simple_filter, SimpleFilterConfig};
10use crate::ui::table::{expanded_from_columns, render_table, Column, TableConfig};
11use crate::ui::{format_title, render_tabs};
12use ratatui::{prelude::*, widgets::*};
13
14pub const FILTER_CONTROLS: [InputFocus; 2] = [InputFocus::Filter, InputFocus::Pagination];
15
16pub struct State {
17    pub repositories: TableState<EcrRepository>,
18    pub tab: Tab,
19    pub current_repository: Option<String>,
20    pub current_repository_uri: Option<String>,
21    pub images: TableState<EcrImage>,
22    pub input_focus: InputFocus,
23}
24
25impl Default for State {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl State {
32    pub fn new() -> Self {
33        Self {
34            repositories: TableState::new(),
35            tab: Tab::Private,
36            current_repository: None,
37            current_repository_uri: None,
38            images: TableState::new(),
39            input_focus: InputFocus::Filter,
40        }
41    }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub enum Tab {
46    Private,
47    Public,
48}
49
50impl CyclicEnum for Tab {
51    const ALL: &'static [Self] = &[Self::Private, Self::Public];
52}
53
54impl Tab {
55    pub fn name(&self) -> &'static str {
56        match self {
57            Tab::Private => "Private",
58            Tab::Public => "Public",
59        }
60    }
61}
62
63pub fn filtered_ecr_repositories(app: &App) -> Vec<&EcrRepository> {
64    filter_by_field(
65        &app.ecr_state.repositories.items,
66        &app.ecr_state.repositories.filter,
67        |r| &r.name,
68    )
69}
70
71pub fn filtered_ecr_images(app: &App) -> Vec<&EcrImage> {
72    if app.ecr_state.images.filter.is_empty() {
73        app.ecr_state.images.items.iter().collect()
74    } else {
75        app.ecr_state
76            .images
77            .items
78            .iter()
79            .filter(|img| {
80                img.tag
81                    .to_lowercase()
82                    .contains(&app.ecr_state.images.filter.to_lowercase())
83                    || img
84                        .digest
85                        .to_lowercase()
86                        .contains(&app.ecr_state.images.filter.to_lowercase())
87            })
88            .collect()
89    }
90}
91
92pub fn render_repositories(frame: &mut Frame, app: &App, area: Rect) {
93    frame.render_widget(Clear, area);
94
95    if app.ecr_state.current_repository.is_some() {
96        render_images(frame, app, area);
97    } else {
98        render_repository_list(frame, app, area);
99    }
100}
101
102pub fn render_repository_list(frame: &mut Frame, app: &App, area: Rect) {
103    let chunks = Layout::default()
104        .direction(Direction::Vertical)
105        .constraints([
106            Constraint::Length(1), // Tabs
107            Constraint::Length(3), // Filter
108            Constraint::Min(0),    // Table
109        ])
110        .split(area);
111
112    // Tabs
113    let tabs: Vec<(&str, Tab)> = Tab::ALL.iter().map(|tab| (tab.name(), *tab)).collect();
114    render_tabs(frame, chunks[0], &tabs, &app.ecr_state.tab);
115
116    // Calculate pagination
117    let filtered_count: usize = app
118        .ecr_state
119        .repositories
120        .items
121        .iter()
122        .filter(|r| {
123            app.ecr_state.repositories.filter.is_empty()
124                || r.name
125                    .to_lowercase()
126                    .contains(&app.ecr_state.repositories.filter.to_lowercase())
127        })
128        .count();
129
130    let page_size = app.ecr_state.repositories.page_size.value();
131    let total_pages = filtered_count.div_ceil(page_size);
132    let current_page = app.ecr_state.repositories.selected / page_size;
133    let pagination = render_pagination_text(current_page, total_pages);
134
135    // Filter
136    render_simple_filter(
137        frame,
138        chunks[1],
139        SimpleFilterConfig {
140            filter_text: &app.ecr_state.repositories.filter,
141            placeholder: "Search by repository substring",
142            pagination: &pagination,
143            mode: app.mode,
144            is_input_focused: app.ecr_state.input_focus == InputFocus::Filter,
145            is_pagination_focused: app.ecr_state.input_focus == InputFocus::Pagination,
146        },
147    );
148
149    // Table
150    let filtered: Vec<_> = app
151        .ecr_state
152        .repositories
153        .items
154        .iter()
155        .filter(|r| {
156            app.ecr_state.repositories.filter.is_empty()
157                || r.name
158                    .to_lowercase()
159                    .contains(&app.ecr_state.repositories.filter.to_lowercase())
160        })
161        .collect();
162
163    // Apply pagination
164    let page_size = app.ecr_state.repositories.page_size.value();
165    let current_page = app.ecr_state.repositories.selected / page_size;
166    let start_idx = current_page * page_size;
167    let end_idx = (start_idx + page_size).min(filtered.len());
168    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
169
170    let tab_label = match app.ecr_state.tab {
171        Tab::Private => "Private",
172        Tab::Public => "Public",
173    };
174    let title = format_title(&format!("{} repositories ({})", tab_label, filtered.len()));
175
176    // Define columns
177    let columns: Vec<Box<dyn Column<EcrRepository>>> = app
178        .ecr_repo_visible_column_ids
179        .iter()
180        .filter_map(|col_id| {
181            repo::Column::from_id(col_id).map(|col| Box::new(col) as Box<dyn Column<EcrRepository>>)
182        })
183        .collect();
184
185    let expanded_index = app.ecr_state.repositories.expanded_item.and_then(|idx| {
186        if idx >= start_idx && idx < end_idx {
187            Some(idx - start_idx)
188        } else {
189            None
190        }
191    });
192
193    let config = TableConfig {
194        items: paginated,
195        selected_index: app.ecr_state.repositories.selected % page_size,
196        expanded_index,
197        columns: &columns,
198        sort_column: "Repository name",
199        sort_direction: SortDirection::Asc,
200        title,
201        area: chunks[2],
202        get_expanded_content: Some(Box::new(|repo: &EcrRepository| {
203            expanded_from_columns(&columns, repo)
204        })),
205        is_active: app.mode != Mode::FilterInput,
206    };
207
208    render_table(frame, config);
209}
210
211pub fn render_images(frame: &mut Frame, app: &App, area: Rect) {
212    let chunks = Layout::default()
213        .direction(Direction::Vertical)
214        .constraints([
215            Constraint::Length(3), // Filter
216            Constraint::Min(0),    // Table
217        ])
218        .split(area);
219
220    // Calculate pagination
221    let filtered_count: usize = app
222        .ecr_state
223        .images
224        .items
225        .iter()
226        .filter(|img| {
227            app.ecr_state.images.filter.is_empty()
228                || img
229                    .tag
230                    .to_lowercase()
231                    .contains(&app.ecr_state.images.filter.to_lowercase())
232                || img
233                    .digest
234                    .to_lowercase()
235                    .contains(&app.ecr_state.images.filter.to_lowercase())
236        })
237        .count();
238
239    let page_size = 50;
240    let total_pages = filtered_count.div_ceil(page_size);
241    let current_page = app.ecr_state.images.selected / page_size;
242    let pagination = render_pagination_text(current_page, total_pages);
243
244    render_simple_filter(
245        frame,
246        chunks[0],
247        SimpleFilterConfig {
248            filter_text: &app.ecr_state.images.filter,
249            placeholder: "Search artifacts",
250            pagination: &pagination,
251            mode: app.mode,
252            is_input_focused: true,
253            is_pagination_focused: false,
254        },
255    );
256
257    // Table
258    let filtered: Vec<_> = app
259        .ecr_state
260        .images
261        .items
262        .iter()
263        .filter(|img| {
264            app.ecr_state.images.filter.is_empty()
265                || img
266                    .tag
267                    .to_lowercase()
268                    .contains(&app.ecr_state.images.filter.to_lowercase())
269                || img
270                    .digest
271                    .to_lowercase()
272                    .contains(&app.ecr_state.images.filter.to_lowercase())
273        })
274        .collect();
275
276    // Apply pagination
277    let page_size = app.ecr_state.repositories.page_size.value();
278    let current_page = app.ecr_state.images.selected / page_size;
279    let start_idx = current_page * page_size;
280    let end_idx = (start_idx + page_size).min(filtered.len());
281    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
282
283    let title = format_title(&format!("Images ({})", filtered.len()));
284
285    // Define columns
286    let columns: Vec<Box<dyn Column<EcrImage>>> = app
287        .ecr_image_visible_column_ids
288        .iter()
289        .filter_map(|col_id| {
290            image::Column::from_id(col_id).map(|col| Box::new(col) as Box<dyn Column<EcrImage>>)
291        })
292        .collect();
293
294    let config = TableConfig {
295        items: paginated,
296        selected_index: app.ecr_state.images.selected - app.ecr_state.images.scroll_offset,
297        expanded_index: app
298            .ecr_state
299            .images
300            .expanded_item
301            .map(|idx| idx - app.ecr_state.images.scroll_offset),
302        columns: &columns,
303        sort_column: "Pushed at",
304        sort_direction: SortDirection::Desc,
305        title,
306        area: chunks[1],
307        get_expanded_content: Some(Box::new(|img: &EcrImage| {
308            expanded_from_columns(&columns, img)
309        })),
310        is_active: app.mode != Mode::FilterInput,
311    };
312
313    render_table(frame, config);
314}