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