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), Constraint::Length(3), Constraint::Min(0), ])
107 .split(area);
108
109 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 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 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 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 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 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), Constraint::Min(0), ])
217 .split(area);
218
219 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 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 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 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}