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), Constraint::Length(3), Constraint::Min(0), ])
110 .split(area);
111
112 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 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 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 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 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 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), Constraint::Min(0), ])
218 .split(area);
219
220 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 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 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 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}