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;
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 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    // Filter bar
145    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    // Paginate
159    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}