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), Constraint::Length(3), Constraint::Min(0), ])
116 .split(area);
117
118 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 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 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 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 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 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), Constraint::Min(0), ])
225 .split(area);
226
227 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 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 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 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}