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, TableWindow};
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 view = app
132 .kms_state
133 .keys
134 .prepare_view(filtered_kms_keys(app), TableWindow::SelectedPage);
135 let pagination = render_pagination_text(view.current_page, view.total_pages);
136
137 render_simple_filter(
139 frame,
140 chunks[1],
141 SimpleFilterConfig {
142 filter_text: &app.kms_state.keys.filter,
143 placeholder: "Search by alias, key ID, or description",
144 pagination: &pagination,
145 mode: app.mode,
146 is_input_focused: app.kms_state.input_focus == InputFocus::Filter,
147 is_pagination_focused: app.kms_state.input_focus == InputFocus::Pagination,
148 },
149 );
150
151 let title = format_title(&format!(
152 "{} ({})",
153 app.kms_state.tab.name(),
154 view.total_items
155 ));
156
157 let columns: Vec<Box<dyn Column<KmsKey>>> = app
158 .kms_state
159 .visible_column_ids()
160 .iter()
161 .filter_map(|col_id| {
162 key::Column::from_id(col_id).map(|col| Box::new(col) as Box<dyn Column<KmsKey>>)
163 })
164 .collect();
165
166 let config = TableConfig {
167 items: view.items,
168 selected_index: view.selected_index,
169 expanded_index: view.expanded_index,
170 columns: &columns,
171 sort_column: "Alias",
172 sort_direction: SortDirection::Asc,
173 title,
174 area: chunks[2],
175 get_expanded_content: Some(Box::new(|k: &KmsKey| expanded_from_columns(&columns, k))),
176 is_active: app.mode != crate::keymap::Mode::FilterInput,
177 };
178
179 render_table(frame, config);
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_tab_names() {
188 assert_eq!(Tab::AwsManaged.name(), "AWS managed keys");
189 assert_eq!(Tab::CustomerManaged.name(), "Customer managed keys");
190 }
191
192 #[test]
193 fn test_tab_cycling() {
194 assert_eq!(Tab::AwsManaged.next(), Tab::CustomerManaged);
195 assert_eq!(Tab::CustomerManaged.next(), Tab::AwsManaged);
196 assert_eq!(Tab::CustomerManaged.prev(), Tab::AwsManaged);
197 assert_eq!(Tab::AwsManaged.prev(), Tab::CustomerManaged);
198 }
199
200 #[test]
201 fn test_state_default_tab() {
202 let state = State::new();
203 assert_eq!(state.tab, Tab::AwsManaged);
204 }
205
206 #[test]
207 fn test_filtered_kms_keys_aws_tab_only_returns_aws_keys() {
208 use crate::app::App;
209 let mut app = App::new_without_client("default".to_string(), None);
210 app.kms_state.tab = Tab::AwsManaged;
211 app.kms_state.keys.items = vec![
212 KmsKey {
213 key_id: "aws-key".to_string(),
214 key_arn: "arn:1".to_string(),
215 alias: "aws/s3".to_string(),
216 description: String::new(),
217 key_state: "Enabled".to_string(),
218 key_usage: "EncryptDecrypt".to_string(),
219 key_spec: "SymmetricDefault".to_string(),
220 key_manager: "Aws".to_string(),
221 creation_date: String::new(),
222 expiration_date: String::new(),
223 deletion_date: String::new(),
224 custom_key_store_id: String::new(),
225 origin: "AwsKms".to_string(),
226 multi_region: false,
227 enabled: true,
228 },
229 KmsKey {
230 key_id: "cust-key".to_string(),
231 key_arn: "arn:2".to_string(),
232 alias: "my-key".to_string(),
233 description: String::new(),
234 key_state: "Enabled".to_string(),
235 key_usage: "EncryptDecrypt".to_string(),
236 key_spec: "SymmetricDefault".to_string(),
237 key_manager: "Customer".to_string(),
238 creation_date: String::new(),
239 expiration_date: String::new(),
240 deletion_date: String::new(),
241 custom_key_store_id: String::new(),
242 origin: "AwsKms".to_string(),
243 multi_region: false,
244 enabled: true,
245 },
246 ];
247
248 let filtered = filtered_kms_keys(&app);
249 assert_eq!(filtered.len(), 1);
250 assert_eq!(filtered[0].key_id, "aws-key");
251 }
252
253 #[test]
254 fn test_filtered_kms_keys_customer_tab() {
255 use crate::app::App;
256 let mut app = App::new_without_client("default".to_string(), None);
257 app.kms_state.tab = Tab::CustomerManaged;
258 app.kms_state.keys.items = vec![
259 KmsKey {
260 key_id: "aws-key".to_string(),
261 key_arn: String::new(),
262 alias: "aws/s3".to_string(),
263 description: String::new(),
264 key_state: "Enabled".to_string(),
265 key_usage: String::new(),
266 key_spec: String::new(),
267 key_manager: "Aws".to_string(),
268 creation_date: String::new(),
269 origin: String::new(),
270 expiration_date: String::new(),
271 deletion_date: String::new(),
272 custom_key_store_id: String::new(),
273 multi_region: false,
274 enabled: true,
275 },
276 KmsKey {
277 key_id: "cust-key".to_string(),
278 key_arn: String::new(),
279 alias: "my-key".to_string(),
280 description: String::new(),
281 key_state: "Enabled".to_string(),
282 key_usage: String::new(),
283 key_spec: String::new(),
284 key_manager: "Customer".to_string(),
285 creation_date: String::new(),
286 expiration_date: String::new(),
287 deletion_date: String::new(),
288 custom_key_store_id: String::new(),
289 origin: String::new(),
290 multi_region: false,
291 enabled: true,
292 },
293 ];
294
295 let filtered = filtered_kms_keys(&app);
296 assert_eq!(filtered.len(), 1);
297 assert_eq!(filtered[0].key_id, "cust-key");
298 }
299
300 #[test]
301 fn test_filtered_kms_keys_text_filter() {
302 use crate::app::App;
303 let mut app = App::new_without_client("default".to_string(), None);
304 app.kms_state.tab = Tab::CustomerManaged;
305 app.kms_state.keys.filter = "prod".to_string();
306 app.kms_state.keys.items = vec![
307 KmsKey {
308 key_id: "key1".to_string(),
309 key_arn: String::new(),
310 alias: "prod-key".to_string(),
311 description: String::new(),
312 key_state: "Enabled".to_string(),
313 key_usage: String::new(),
314 key_spec: String::new(),
315 key_manager: "Customer".to_string(),
316 creation_date: String::new(),
317 expiration_date: String::new(),
318 deletion_date: String::new(),
319 custom_key_store_id: String::new(),
320 origin: String::new(),
321 multi_region: false,
322 enabled: true,
323 },
324 KmsKey {
325 key_id: "key2".to_string(),
326 key_arn: String::new(),
327 alias: "dev-key".to_string(),
328 description: String::new(),
329 key_state: "Enabled".to_string(),
330 key_usage: String::new(),
331 key_spec: String::new(),
332 key_manager: "Customer".to_string(),
333 creation_date: String::new(),
334 expiration_date: String::new(),
335 deletion_date: String::new(),
336 custom_key_store_id: String::new(),
337 origin: String::new(),
338 multi_region: false,
339 enabled: true,
340 },
341 ];
342
343 let filtered = filtered_kms_keys(&app);
344 assert_eq!(filtered.len(), 1);
345 assert_eq!(filtered[0].alias, "prod-key");
346 }
347}