1use crate::app::App;
2use crate::common::{render_pagination_text, ColumnId, CyclicEnum, InputFocus, SortDirection};
3use crate::kms::key::{self, Key as KmsKey};
4use crate::table::TableState;
5use crate::ui::filter::{render_simple_filter, SimpleFilterConfig};
6use crate::ui::table::{expanded_from_columns, render_table, Column, TableConfig};
7use crate::ui::{format_title, render_tabs};
8use ratatui::prelude::*;
9
10pub const FILTER_CONTROLS: [InputFocus; 2] = [InputFocus::Filter, InputFocus::Pagination];
11
12pub struct State {
13 pub keys: TableState<KmsKey>,
14 pub tab: Tab,
15 pub input_focus: InputFocus,
16 pub aws_visible_column_ids: Vec<ColumnId>,
18 pub customer_visible_column_ids: Vec<ColumnId>,
20}
21
22impl Default for State {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl State {
29 pub fn new() -> Self {
30 use crate::kms::key::Column;
31 Self {
32 keys: TableState::new(),
33 tab: Tab::AwsManaged,
34 input_focus: InputFocus::Filter,
35 aws_visible_column_ids: Column::aws_managed_default_visible().to_vec(),
36 customer_visible_column_ids: Column::customer_managed_default_visible().to_vec(),
37 }
38 }
39
40 pub fn visible_column_ids(&self) -> &Vec<ColumnId> {
42 match self.tab {
43 Tab::AwsManaged => &self.aws_visible_column_ids,
44 Tab::CustomerManaged => &self.customer_visible_column_ids,
45 }
46 }
47
48 pub fn visible_column_ids_mut(&mut self) -> &mut Vec<ColumnId> {
50 match self.tab {
51 Tab::AwsManaged => &mut self.aws_visible_column_ids,
52 Tab::CustomerManaged => &mut self.customer_visible_column_ids,
53 }
54 }
55
56 pub fn tab_column_ids(&self) -> Vec<ColumnId> {
58 use crate::kms::key::Column;
59 match self.tab {
60 Tab::AwsManaged => Column::aws_managed_columns()
61 .iter()
62 .map(|c| c.id())
63 .collect(),
64 Tab::CustomerManaged => Column::customer_managed_columns()
65 .iter()
66 .map(|c| c.id())
67 .collect(),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub enum Tab {
74 AwsManaged,
75 CustomerManaged,
76}
77
78impl CyclicEnum for Tab {
79 const ALL: &'static [Self] = &[Self::AwsManaged, Self::CustomerManaged];
80}
81
82impl Tab {
83 pub fn name(&self) -> &'static str {
84 match self {
85 Tab::AwsManaged => "AWS managed keys",
86 Tab::CustomerManaged => "Customer managed keys",
87 }
88 }
89}
90
91pub fn filtered_kms_keys(app: &App) -> Vec<&KmsKey> {
92 let tab_filter: Box<dyn Fn(&&KmsKey) -> bool> = match app.kms_state.tab {
93 Tab::AwsManaged => Box::new(|k: &&KmsKey| k.key_manager == "Aws"),
94 Tab::CustomerManaged => Box::new(|k: &&KmsKey| k.key_manager == "Customer"),
95 };
96
97 let text_filter = &app.kms_state.keys.filter;
98 app.kms_state
99 .keys
100 .items
101 .iter()
102 .filter(|k| tab_filter(k))
103 .filter(|k| {
104 text_filter.is_empty()
105 || k.alias.to_lowercase().contains(&text_filter.to_lowercase())
106 || k.key_id
107 .to_lowercase()
108 .contains(&text_filter.to_lowercase())
109 || k.description
110 .to_lowercase()
111 .contains(&text_filter.to_lowercase())
112 })
113 .collect()
114}
115
116pub fn render_keys(frame: &mut Frame, app: &App, area: Rect) {
117 let chunks = Layout::default()
118 .direction(Direction::Vertical)
119 .constraints([
120 Constraint::Length(1), Constraint::Length(3), Constraint::Min(0), ])
124 .split(area);
125
126 let tabs: Vec<(&str, Tab)> = Tab::ALL.iter().map(|tab| (tab.name(), *tab)).collect();
128 render_tabs(frame, chunks[0], &tabs, &app.kms_state.tab);
129
130 let filtered = filtered_kms_keys(app);
132 let filtered_count = filtered.len();
133
134 let page_size = app.kms_state.keys.page_size.value();
135 let total_pages = filtered_count.div_ceil(page_size);
136 let current_page = app
137 .kms_state
138 .keys
139 .selected
140 .checked_div(page_size)
141 .unwrap_or(0);
142 let pagination = render_pagination_text(current_page, total_pages);
143
144 render_simple_filter(
146 frame,
147 chunks[1],
148 SimpleFilterConfig {
149 filter_text: &app.kms_state.keys.filter,
150 placeholder: "Search by alias, key ID, or description",
151 pagination: &pagination,
152 mode: app.mode,
153 is_input_focused: app.kms_state.input_focus == InputFocus::Filter,
154 is_pagination_focused: app.kms_state.input_focus == InputFocus::Pagination,
155 },
156 );
157
158 let start_idx = current_page * page_size;
160 let end_idx = (start_idx + page_size).min(filtered_count);
161 let paginated: Vec<_> = if start_idx < filtered_count {
162 filtered[start_idx..end_idx].to_vec()
163 } else {
164 vec![]
165 };
166
167 let title = format_title(&format!(
168 "{} ({})",
169 app.kms_state.tab.name(),
170 filtered_count
171 ));
172
173 let columns: Vec<Box<dyn Column<KmsKey>>> = app
174 .kms_state
175 .visible_column_ids()
176 .iter()
177 .filter_map(|col_id| {
178 key::Column::from_id(col_id).map(|col| Box::new(col) as Box<dyn Column<KmsKey>>)
179 })
180 .collect();
181
182 let expanded_index = app.kms_state.keys.expanded_item.and_then(|idx| {
183 if idx >= start_idx && idx < end_idx {
184 Some(idx - start_idx)
185 } else {
186 None
187 }
188 });
189
190 let selected_index = if page_size > 0 {
191 app.kms_state.keys.selected.saturating_sub(start_idx)
192 } else {
193 0
194 };
195
196 let config = TableConfig {
197 items: paginated,
198 selected_index,
199 expanded_index,
200 columns: &columns,
201 sort_column: "Alias",
202 sort_direction: SortDirection::Asc,
203 title,
204 area: chunks[2],
205 get_expanded_content: Some(Box::new(|k: &KmsKey| expanded_from_columns(&columns, k))),
206 is_active: app.mode != crate::keymap::Mode::FilterInput,
207 };
208
209 render_table(frame, config);
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_tab_names() {
218 assert_eq!(Tab::AwsManaged.name(), "AWS managed keys");
219 assert_eq!(Tab::CustomerManaged.name(), "Customer managed keys");
220 }
221
222 #[test]
223 fn test_tab_cycling() {
224 assert_eq!(Tab::AwsManaged.next(), Tab::CustomerManaged);
225 assert_eq!(Tab::CustomerManaged.next(), Tab::AwsManaged);
226 assert_eq!(Tab::CustomerManaged.prev(), Tab::AwsManaged);
227 assert_eq!(Tab::AwsManaged.prev(), Tab::CustomerManaged);
228 }
229
230 #[test]
231 fn test_state_default_tab() {
232 let state = State::new();
233 assert_eq!(state.tab, Tab::AwsManaged);
234 }
235
236 #[test]
237 fn test_filtered_kms_keys_aws_tab_only_returns_aws_keys() {
238 use crate::app::App;
239 let mut app = App::new_without_client("default".to_string(), None);
240 app.kms_state.tab = Tab::AwsManaged;
241 app.kms_state.keys.items = vec![
242 KmsKey {
243 key_id: "aws-key".to_string(),
244 key_arn: "arn:1".to_string(),
245 alias: "aws/s3".to_string(),
246 description: String::new(),
247 key_state: "Enabled".to_string(),
248 key_usage: "EncryptDecrypt".to_string(),
249 key_spec: "SymmetricDefault".to_string(),
250 key_manager: "Aws".to_string(),
251 creation_date: String::new(),
252 expiration_date: String::new(),
253 deletion_date: String::new(),
254 custom_key_store_id: String::new(),
255 origin: "AwsKms".to_string(),
256 multi_region: false,
257 enabled: true,
258 },
259 KmsKey {
260 key_id: "cust-key".to_string(),
261 key_arn: "arn:2".to_string(),
262 alias: "my-key".to_string(),
263 description: String::new(),
264 key_state: "Enabled".to_string(),
265 key_usage: "EncryptDecrypt".to_string(),
266 key_spec: "SymmetricDefault".to_string(),
267 key_manager: "Customer".to_string(),
268 creation_date: String::new(),
269 expiration_date: String::new(),
270 deletion_date: String::new(),
271 custom_key_store_id: String::new(),
272 origin: "AwsKms".to_string(),
273 multi_region: false,
274 enabled: true,
275 },
276 ];
277
278 let filtered = filtered_kms_keys(&app);
279 assert_eq!(filtered.len(), 1);
280 assert_eq!(filtered[0].key_id, "aws-key");
281 }
282
283 #[test]
284 fn test_filtered_kms_keys_customer_tab() {
285 use crate::app::App;
286 let mut app = App::new_without_client("default".to_string(), None);
287 app.kms_state.tab = Tab::CustomerManaged;
288 app.kms_state.keys.items = vec![
289 KmsKey {
290 key_id: "aws-key".to_string(),
291 key_arn: String::new(),
292 alias: "aws/s3".to_string(),
293 description: String::new(),
294 key_state: "Enabled".to_string(),
295 key_usage: String::new(),
296 key_spec: String::new(),
297 key_manager: "Aws".to_string(),
298 creation_date: String::new(),
299 origin: String::new(),
300 expiration_date: String::new(),
301 deletion_date: String::new(),
302 custom_key_store_id: String::new(),
303 multi_region: false,
304 enabled: true,
305 },
306 KmsKey {
307 key_id: "cust-key".to_string(),
308 key_arn: String::new(),
309 alias: "my-key".to_string(),
310 description: String::new(),
311 key_state: "Enabled".to_string(),
312 key_usage: String::new(),
313 key_spec: String::new(),
314 key_manager: "Customer".to_string(),
315 creation_date: String::new(),
316 expiration_date: String::new(),
317 deletion_date: String::new(),
318 custom_key_store_id: String::new(),
319 origin: String::new(),
320 multi_region: false,
321 enabled: true,
322 },
323 ];
324
325 let filtered = filtered_kms_keys(&app);
326 assert_eq!(filtered.len(), 1);
327 assert_eq!(filtered[0].key_id, "cust-key");
328 }
329
330 #[test]
331 fn test_filtered_kms_keys_text_filter() {
332 use crate::app::App;
333 let mut app = App::new_without_client("default".to_string(), None);
334 app.kms_state.tab = Tab::CustomerManaged;
335 app.kms_state.keys.filter = "prod".to_string();
336 app.kms_state.keys.items = vec![
337 KmsKey {
338 key_id: "key1".to_string(),
339 key_arn: String::new(),
340 alias: "prod-key".to_string(),
341 description: String::new(),
342 key_state: "Enabled".to_string(),
343 key_usage: String::new(),
344 key_spec: String::new(),
345 key_manager: "Customer".to_string(),
346 creation_date: String::new(),
347 expiration_date: String::new(),
348 deletion_date: String::new(),
349 custom_key_store_id: String::new(),
350 origin: String::new(),
351 multi_region: false,
352 enabled: true,
353 },
354 KmsKey {
355 key_id: "key2".to_string(),
356 key_arn: String::new(),
357 alias: "dev-key".to_string(),
358 description: String::new(),
359 key_state: "Enabled".to_string(),
360 key_usage: String::new(),
361 key_spec: String::new(),
362 key_manager: "Customer".to_string(),
363 creation_date: String::new(),
364 expiration_date: String::new(),
365 deletion_date: String::new(),
366 custom_key_store_id: String::new(),
367 origin: String::new(),
368 multi_region: false,
369 enabled: true,
370 },
371 ];
372
373 let filtered = filtered_kms_keys(&app);
374 assert_eq!(filtered.len(), 1);
375 assert_eq!(filtered[0].alias, "prod-key");
376 }
377}