Skip to main content

rusticity_term/ui/
kms.rs

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    /// Visible column IDs for the AWS managed keys tab
17    pub aws_visible_column_ids: Vec<ColumnId>,
18    /// Visible column IDs for the Customer managed keys tab
19    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    /// Returns the currently active visible column IDs based on the tab.
41    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    /// Returns the mutable currently active visible column IDs.
49    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    /// Returns the full column list for the current tab.
57    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), // Tabs
121            Constraint::Length(3), // Filter
122            Constraint::Min(0),    // Table
123        ])
124        .split(area);
125
126    // Tabs
127    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    // Filtered keys for current tab
131    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    // Filter bar
138    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}