rusticity_term/ui/
ecr.rs

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