1use crate::common::{render_pagination_text, CyclicEnum, InputFocus, SortDirection};
2use crate::ec2::{Column, Instance};
3use crate::keymap::Mode;
4use crate::table::TableState;
5use ratatui::prelude::*;
6
7pub const FILTER_CONTROLS: [InputFocus; 3] = [
8 InputFocus::Filter,
9 InputFocus::Checkbox("state"),
10 InputFocus::Pagination,
11];
12
13pub const STATE_FILTER: InputFocus = InputFocus::Checkbox("state");
14
15#[derive(Debug, Clone, Copy, PartialEq, Default)]
16pub enum StateFilter {
17 #[default]
18 AllStates,
19 Running,
20 Stopped,
21 Terminated,
22 Pending,
23 ShuttingDown,
24 Stopping,
25}
26
27impl StateFilter {
28 pub fn name(&self) -> &'static str {
29 match self {
30 StateFilter::AllStates => "All states",
31 StateFilter::Running => "Running",
32 StateFilter::Stopped => "Stopped",
33 StateFilter::Terminated => "Terminated",
34 StateFilter::Pending => "Pending",
35 StateFilter::ShuttingDown => "Shutting down",
36 StateFilter::Stopping => "Stopping",
37 }
38 }
39
40 pub fn matches(&self, state: &str) -> bool {
41 match self {
42 StateFilter::AllStates => true,
43 StateFilter::Running => state == "running",
44 StateFilter::Stopped => state == "stopped",
45 StateFilter::Terminated => state == "terminated",
46 StateFilter::Pending => state == "pending",
47 StateFilter::ShuttingDown => state == "shutting-down",
48 StateFilter::Stopping => state == "stopping",
49 }
50 }
51}
52
53impl CyclicEnum for StateFilter {
54 const ALL: &'static [Self] = &[
55 StateFilter::AllStates,
56 StateFilter::Running,
57 StateFilter::Stopped,
58 StateFilter::Terminated,
59 StateFilter::Pending,
60 StateFilter::ShuttingDown,
61 StateFilter::Stopping,
62 ];
63
64 fn next(&self) -> Self {
65 match self {
66 StateFilter::AllStates => StateFilter::Running,
67 StateFilter::Running => StateFilter::Stopped,
68 StateFilter::Stopped => StateFilter::Terminated,
69 StateFilter::Terminated => StateFilter::Pending,
70 StateFilter::Pending => StateFilter::ShuttingDown,
71 StateFilter::ShuttingDown => StateFilter::Stopping,
72 StateFilter::Stopping => StateFilter::AllStates,
73 }
74 }
75
76 fn prev(&self) -> Self {
77 match self {
78 StateFilter::AllStates => StateFilter::Stopping,
79 StateFilter::Running => StateFilter::AllStates,
80 StateFilter::Stopped => StateFilter::Running,
81 StateFilter::Terminated => StateFilter::Stopped,
82 StateFilter::Pending => StateFilter::Terminated,
83 StateFilter::ShuttingDown => StateFilter::Pending,
84 StateFilter::Stopping => StateFilter::ShuttingDown,
85 }
86 }
87}
88
89pub struct State {
90 pub table: TableState<Instance>,
91 pub state_filter: StateFilter,
92 pub sort_column: Column,
93 pub sort_direction: SortDirection,
94 pub input_focus: InputFocus,
95}
96
97impl Default for State {
98 fn default() -> Self {
99 Self {
100 table: TableState::default(),
101 state_filter: StateFilter::default(),
102 sort_column: Column::LaunchTime,
103 sort_direction: SortDirection::Desc,
104 input_focus: InputFocus::Filter,
105 }
106 }
107}
108
109pub const FILTER_HINT: &str = "Find Instance by attribute or tag (case-sensitive)";
110
111pub fn render_instances(
112 frame: &mut Frame,
113 area: Rect,
114 state: &State,
115 visible_columns: &[&str],
116 mode: Mode,
117) {
118 use crate::common::render_dropdown;
119 use crate::ui::filter::{render_filter_bar, FilterConfig, FilterControl};
120 use crate::ui::table::render_table;
121
122 let chunks = Layout::default()
123 .direction(Direction::Vertical)
124 .constraints([Constraint::Length(3), Constraint::Min(0)])
125 .split(area);
126
127 let filtered_items: Vec<&Instance> = state
128 .table
129 .items
130 .iter()
131 .filter(|i| state.state_filter.matches(&i.state))
132 .filter(|i| {
133 if state.table.filter.is_empty() {
134 return true;
135 }
136 i.name.contains(&state.table.filter)
137 || i.instance_id.contains(&state.table.filter)
138 || i.state.contains(&state.table.filter)
139 || i.instance_type.contains(&state.table.filter)
140 || i.availability_zone.contains(&state.table.filter)
141 || i.security_groups.contains(&state.table.filter)
142 || i.key_name.contains(&state.table.filter)
143 })
144 .collect();
145
146 let page_size = state.table.page_size.value();
147 let filtered_count = filtered_items.len();
148 let total_pages = filtered_count.div_ceil(page_size);
149 let current_page = state.table.selected / page_size;
150 let pagination = render_pagination_text(current_page, total_pages);
151
152 render_filter_bar(
153 frame,
154 FilterConfig {
155 filter_text: &state.table.filter,
156 placeholder: FILTER_HINT,
157 mode,
158 is_input_focused: state.input_focus == InputFocus::Filter,
159 controls: vec![
160 FilterControl {
161 text: state.state_filter.name().to_string(),
162 is_focused: state.input_focus == STATE_FILTER,
163 },
164 FilterControl {
165 text: pagination.clone(),
166 is_focused: state.input_focus == InputFocus::Pagination,
167 },
168 ],
169 area: chunks[0],
170 },
171 );
172
173 let columns: Vec<_> = visible_columns
174 .iter()
175 .filter_map(|id| Column::from_id(id).map(|c| c.to_column()))
176 .collect();
177
178 let title = format!("Instances ({})", filtered_count);
179
180 use crate::ui::table::TableConfig;
181 render_table(
182 frame,
183 TableConfig {
184 items: filtered_items,
185 selected_index: state.table.selected,
186 expanded_index: state.table.expanded_item,
187 columns: &columns,
188 sort_column: "",
189 sort_direction: state.sort_direction,
190 title,
191 area: chunks[1],
192 get_expanded_content: Some(Box::new(|instance: &Instance| {
193 crate::ui::table::expanded_from_columns(&columns, instance)
194 })),
195 is_active: mode == Mode::Normal,
196 },
197 );
198
199 if mode == Mode::FilterInput && state.input_focus == STATE_FILTER {
201 let filter_names: Vec<&str> = StateFilter::ALL.iter().map(|f| f.name()).collect();
202 let selected_idx = StateFilter::ALL
203 .iter()
204 .position(|f| *f == state.state_filter)
205 .unwrap_or(0);
206 let controls_after = pagination.len() as u16 + 3;
207 render_dropdown(
208 frame,
209 &filter_names,
210 selected_idx,
211 chunks[0],
212 controls_after,
213 );
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_state_filter_names() {
223 assert_eq!(StateFilter::AllStates.name(), "All states");
224 assert_eq!(StateFilter::Running.name(), "Running");
225 assert_eq!(StateFilter::Stopped.name(), "Stopped");
226 assert_eq!(StateFilter::Terminated.name(), "Terminated");
227 assert_eq!(StateFilter::Pending.name(), "Pending");
228 assert_eq!(StateFilter::ShuttingDown.name(), "Shutting down");
229 assert_eq!(StateFilter::Stopping.name(), "Stopping");
230 }
231
232 #[test]
233 fn test_state_filter_matches() {
234 assert!(StateFilter::AllStates.matches("running"));
235 assert!(StateFilter::AllStates.matches("stopped"));
236 assert!(StateFilter::Running.matches("running"));
237 assert!(!StateFilter::Running.matches("stopped"));
238 assert!(StateFilter::Stopped.matches("stopped"));
239 assert!(!StateFilter::Stopped.matches("running"));
240 }
241
242 #[test]
243 fn test_state_filter_next() {
244 assert_eq!(StateFilter::AllStates.next(), StateFilter::Running);
245 assert_eq!(StateFilter::Running.next(), StateFilter::Stopped);
246 assert_eq!(StateFilter::Stopping.next(), StateFilter::AllStates);
247 }
248
249 #[test]
250 fn test_state_filter_prev() {
251 assert_eq!(StateFilter::AllStates.prev(), StateFilter::Stopping);
252 assert_eq!(StateFilter::Running.prev(), StateFilter::AllStates);
253 assert_eq!(StateFilter::Stopped.prev(), StateFilter::Running);
254 }
255
256 #[test]
257 fn test_state_filter_all_constant() {
258 assert_eq!(StateFilter::ALL.len(), 7);
259 assert_eq!(StateFilter::ALL[0], StateFilter::AllStates);
260 assert_eq!(StateFilter::ALL[6], StateFilter::Stopping);
261 }
262
263 #[test]
264 fn test_state_default() {
265 let state = State::default();
266 assert_eq!(state.table.items.len(), 0);
267 assert_eq!(state.table.selected, 0);
268 assert!(!state.table.loading);
269 assert_eq!(state.table.filter, "");
270 assert_eq!(state.state_filter, StateFilter::AllStates);
271 assert_eq!(state.sort_column, Column::LaunchTime);
272 assert_eq!(state.sort_direction, SortDirection::Desc);
273 }
274
275 #[test]
276 fn test_state_filter_matches_all_states() {
277 let filter = StateFilter::AllStates;
278 assert!(filter.matches("running"));
279 assert!(filter.matches("stopped"));
280 assert!(filter.matches("terminated"));
281 assert!(filter.matches("pending"));
282 assert!(filter.matches("shutting-down"));
283 assert!(filter.matches("stopping"));
284 }
285
286 #[test]
287 fn test_state_filter_matches_specific_states() {
288 assert!(StateFilter::Running.matches("running"));
289 assert!(!StateFilter::Running.matches("stopped"));
290
291 assert!(StateFilter::Stopped.matches("stopped"));
292 assert!(!StateFilter::Stopped.matches("running"));
293
294 assert!(StateFilter::Terminated.matches("terminated"));
295 assert!(!StateFilter::Terminated.matches("running"));
296
297 assert!(StateFilter::Pending.matches("pending"));
298 assert!(!StateFilter::Pending.matches("running"));
299
300 assert!(StateFilter::ShuttingDown.matches("shutting-down"));
301 assert!(!StateFilter::ShuttingDown.matches("running"));
302
303 assert!(StateFilter::Stopping.matches("stopping"));
304 assert!(!StateFilter::Stopping.matches("running"));
305 }
306
307 #[test]
308 fn test_state_filter_cycle() {
309 let mut filter = StateFilter::AllStates;
310 filter = filter.next();
311 assert_eq!(filter, StateFilter::Running);
312 filter = filter.next();
313 assert_eq!(filter, StateFilter::Stopped);
314 filter = filter.next();
315 assert_eq!(filter, StateFilter::Terminated);
316 filter = filter.next();
317 assert_eq!(filter, StateFilter::Pending);
318 filter = filter.next();
319 assert_eq!(filter, StateFilter::ShuttingDown);
320 filter = filter.next();
321 assert_eq!(filter, StateFilter::Stopping);
322 filter = filter.next();
323 assert_eq!(filter, StateFilter::AllStates);
324 }
325
326 #[test]
327 fn test_filter_controls_constant() {
328 assert_eq!(FILTER_CONTROLS.len(), 3);
329 assert_eq!(FILTER_CONTROLS[0], InputFocus::Filter);
330 assert_eq!(FILTER_CONTROLS[1], STATE_FILTER);
331 assert_eq!(FILTER_CONTROLS[2], InputFocus::Pagination);
332 }
333
334 #[test]
335 fn test_input_focus_cycling() {
336 let mut focus = InputFocus::Filter;
337 focus = focus.next(&FILTER_CONTROLS);
338 assert_eq!(focus, STATE_FILTER);
339 focus = focus.next(&FILTER_CONTROLS);
340 assert_eq!(focus, InputFocus::Pagination);
341 focus = focus.next(&FILTER_CONTROLS);
342 assert_eq!(focus, InputFocus::Filter);
343 }
344
345 #[test]
346 fn test_input_focus_reverse_cycling() {
347 let mut focus = InputFocus::Filter;
348 focus = focus.prev(&FILTER_CONTROLS);
349 assert_eq!(focus, InputFocus::Pagination);
350 focus = focus.prev(&FILTER_CONTROLS);
351 assert_eq!(focus, STATE_FILTER);
352 focus = focus.prev(&FILTER_CONTROLS);
353 assert_eq!(focus, InputFocus::Filter);
354 }
355
356 #[test]
357 fn test_state_default_input_focus() {
358 let state = State::default();
359 assert_eq!(state.input_focus, InputFocus::Filter);
360 }
361
362 #[test]
363 fn test_filter_controls_includes_state_filter() {
364 assert_eq!(FILTER_CONTROLS.len(), 3);
365 assert_eq!(FILTER_CONTROLS[0], InputFocus::Filter);
366 assert_eq!(FILTER_CONTROLS[1], STATE_FILTER);
367 assert_eq!(FILTER_CONTROLS[2], InputFocus::Pagination);
368 }
369
370 #[test]
371 fn test_state_filter_constant() {
372 assert_eq!(STATE_FILTER, InputFocus::Checkbox("state"));
373 }
374
375 #[test]
376 fn test_state_filter_all_has_7_items() {
377 assert_eq!(StateFilter::ALL.len(), 7);
378 }
379
380 #[test]
381 fn test_dropdown_shows_when_state_filter_focused() {
382 let focus = STATE_FILTER;
385 assert_eq!(focus, InputFocus::Checkbox("state"));
386 }
387}