1use crate::common::{format_bytes, translate_column, ColumnId};
2use crate::ui::table::Column as TableColumn;
3use ratatui::prelude::*;
4use rusticity_core::dynamodb::DdbTable as Table;
5use std::collections::HashMap;
6
7pub fn init(i18n: &mut HashMap<String, String>) {
8 for col in Column::all() {
9 i18n.entry(col.id().to_string())
10 .or_insert_with(|| col.default_name().to_string());
11 }
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum Column {
16 Name,
17 Status,
18 PartitionKey,
19 SortKey,
20 Indexes,
21 ReplicationRegions,
22 DeletionProtection,
23 Favorite,
24 ReadCapacityMode,
25 WriteCapacityMode,
26 TotalSize,
27 TableClass,
28}
29
30impl Column {
31 const ID_NAME: &'static str = "column.dynamodb.table.name";
32 const ID_STATUS: &'static str = "column.dynamodb.table.status";
33 const ID_PARTITION_KEY: &'static str = "column.dynamodb.table.partition_key";
34 const ID_SORT_KEY: &'static str = "column.dynamodb.table.sort_key";
35 const ID_INDEXES: &'static str = "column.dynamodb.table.indexes";
36 const ID_REPLICATION_REGIONS: &'static str = "column.dynamodb.table.replication_regions";
37 const ID_DELETION_PROTECTION: &'static str = "column.dynamodb.table.deletion_protection";
38 const ID_FAVORITE: &'static str = "column.dynamodb.table.favorite";
39 const ID_READ_CAPACITY_MODE: &'static str = "column.dynamodb.table.read_capacity_mode";
40 const ID_WRITE_CAPACITY_MODE: &'static str = "column.dynamodb.table.write_capacity_mode";
41 const ID_TOTAL_SIZE: &'static str = "column.dynamodb.table.total_size";
42 const ID_TABLE_CLASS: &'static str = "column.dynamodb.table.table_class";
43
44 pub const fn id(&self) -> &'static str {
45 match self {
46 Column::Name => Self::ID_NAME,
47 Column::Status => Self::ID_STATUS,
48 Column::PartitionKey => Self::ID_PARTITION_KEY,
49 Column::SortKey => Self::ID_SORT_KEY,
50 Column::Indexes => Self::ID_INDEXES,
51 Column::ReplicationRegions => Self::ID_REPLICATION_REGIONS,
52 Column::DeletionProtection => Self::ID_DELETION_PROTECTION,
53 Column::Favorite => Self::ID_FAVORITE,
54 Column::ReadCapacityMode => Self::ID_READ_CAPACITY_MODE,
55 Column::WriteCapacityMode => Self::ID_WRITE_CAPACITY_MODE,
56 Column::TotalSize => Self::ID_TOTAL_SIZE,
57 Column::TableClass => Self::ID_TABLE_CLASS,
58 }
59 }
60
61 pub const fn default_name(&self) -> &'static str {
62 match self {
63 Column::Name => "Name",
64 Column::Status => "Status",
65 Column::PartitionKey => "Partition key",
66 Column::SortKey => "Sort key",
67 Column::Indexes => "Indexes",
68 Column::ReplicationRegions => "Replication Regions",
69 Column::DeletionProtection => "Deletion protection",
70 Column::Favorite => "Favorite",
71 Column::ReadCapacityMode => "Read capacity mode",
72 Column::WriteCapacityMode => "Write capacity mode",
73 Column::TotalSize => "Total size",
74 Column::TableClass => "Table class",
75 }
76 }
77
78 pub const fn all() -> [Column; 12] {
79 [
80 Column::Name,
81 Column::Status,
82 Column::PartitionKey,
83 Column::SortKey,
84 Column::Indexes,
85 Column::ReplicationRegions,
86 Column::DeletionProtection,
87 Column::Favorite,
88 Column::ReadCapacityMode,
89 Column::WriteCapacityMode,
90 Column::TotalSize,
91 Column::TableClass,
92 ]
93 }
94
95 pub fn ids() -> Vec<ColumnId> {
96 Self::all().iter().map(|c| c.id()).collect()
97 }
98
99 pub fn default_visible_ids() -> Vec<ColumnId> {
101 Self::ids()
102 }
103
104 pub fn from_id(id: &str) -> Option<Self> {
105 match id {
106 Self::ID_NAME => Some(Column::Name),
107 Self::ID_STATUS => Some(Column::Status),
108 Self::ID_PARTITION_KEY => Some(Column::PartitionKey),
109 Self::ID_SORT_KEY => Some(Column::SortKey),
110 Self::ID_INDEXES => Some(Column::Indexes),
111 Self::ID_REPLICATION_REGIONS => Some(Column::ReplicationRegions),
112 Self::ID_DELETION_PROTECTION => Some(Column::DeletionProtection),
113 Self::ID_FAVORITE => Some(Column::Favorite),
114 Self::ID_READ_CAPACITY_MODE => Some(Column::ReadCapacityMode),
115 Self::ID_WRITE_CAPACITY_MODE => Some(Column::WriteCapacityMode),
116 Self::ID_TOTAL_SIZE => Some(Column::TotalSize),
117 Self::ID_TABLE_CLASS => Some(Column::TableClass),
118 _ => None,
119 }
120 }
121
122 pub fn name(&self) -> String {
123 translate_column(self.id(), self.default_name())
124 }
125}
126
127impl TableColumn<Table> for Column {
128 fn name(&self) -> &str {
129 Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
130 }
131
132 fn width(&self) -> u16 {
133 let translated = translate_column(self.id(), self.default_name());
134 translated.len().max(match self {
135 Column::Name => 30,
136 Column::Status => 12,
137 Column::PartitionKey => 20,
138 Column::SortKey => 20,
139 Column::Indexes => 8,
140 Column::ReplicationRegions => 20,
141 Column::DeletionProtection => 19,
142 Column::Favorite => 8,
143 Column::ReadCapacityMode => 18,
144 Column::WriteCapacityMode => 19,
145 Column::TotalSize => 12,
146 Column::TableClass => 12,
147 }) as u16
148 }
149
150 fn render(&self, item: &Table) -> (String, Style) {
151 match self {
152 Column::Name => (item.name.clone(), Style::default()),
153 Column::Status => {
154 let (text, color) = format_status(&item.status);
155 (text.to_string(), Style::default().fg(color))
156 }
157 Column::PartitionKey => (item.partition_key.clone(), Style::default()),
158 Column::SortKey => (dash_if_empty(&item.sort_key), Style::default()),
159 Column::Indexes => (item.index_count().to_string(), Style::default()),
160 Column::ReplicationRegions => {
161 (dash_if_empty(&item.replication_regions()), Style::default())
162 }
163 Column::DeletionProtection => (
164 item.deletion_protection.clone(),
165 Style::default().fg(crate::ui::dynamodb::enabled_disabled_color(
166 &item.deletion_protection,
167 )),
168 ),
169 Column::Favorite => (
171 "☆".to_string(),
172 Style::default().fg(ratatui::style::Color::DarkGray),
173 ),
174 Column::ReadCapacityMode => (item.read_capacity_mode.clone(), Style::default()),
175 Column::WriteCapacityMode => (item.write_capacity_mode.clone(), Style::default()),
176 Column::TotalSize => (format_bytes(item.total_size_bytes), Style::default()),
177 Column::TableClass => (item.table_class.clone(), Style::default()),
178 }
179 }
180}
181
182fn dash_if_empty(value: &str) -> String {
184 if value.is_empty() {
185 "-".to_string()
186 } else {
187 value.to_string()
188 }
189}
190
191pub fn format_status(status: &str) -> (&'static str, Color) {
192 match status.to_uppercase().as_str() {
193 "ACTIVE" => ("✅ Active", Color::Green),
194 "CREATING" => ("⏳ Creating", Color::Yellow),
195 "UPDATING" => ("🔄 Updating", Color::Yellow),
196 "DELETING" => ("🗑 Deleting", Color::Red),
197 "ARCHIVING" => ("📦 Archiving", Color::Yellow),
198 "ARCHIVED" => ("📦 Archived", Color::DarkGray),
199 "INACCESSIBLE_ENCRYPTION_CREDENTIALS" => ("⚠ Inaccessible", Color::Red),
200 _ => ("Unknown", Color::White),
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_column_ids_have_prefix() {
210 for col in Column::all() {
211 assert!(col.id().starts_with("column.dynamodb.table."));
212 }
213 }
214
215 #[test]
216 fn test_from_id_roundtrip() {
217 for col in Column::all() {
218 assert_eq!(Column::from_id(col.id()), Some(col));
219 }
220 }
221
222 #[test]
223 fn test_all_twelve_columns() {
224 assert_eq!(Column::all().len(), 12);
225 }
226
227 #[test]
228 fn test_column_order_matches_console() {
229 assert_eq!(
230 Column::all()
231 .iter()
232 .map(|c| c.default_name())
233 .collect::<Vec<_>>(),
234 vec![
235 "Name",
236 "Status",
237 "Partition key",
238 "Sort key",
239 "Indexes",
240 "Replication Regions",
241 "Deletion protection",
242 "Favorite",
243 "Read capacity mode",
244 "Write capacity mode",
245 "Total size",
246 "Table class",
247 ]
248 );
249 }
250
251 #[test]
252 fn test_every_column_visible_by_default() {
253 assert_eq!(Column::default_visible_ids(), Column::ids());
254 assert_eq!(Column::default_visible_ids().len(), 12);
255 }
256
257 #[test]
258 fn test_render_indexes_sort_key_and_replication() {
259 use rusticity_core::dynamodb::{DdbIndex, DdbReplica};
260 let table = Table {
261 name: "orders".to_string(),
262 status: "ACTIVE".to_string(),
263 partition_key: "pk (S)".to_string(),
264 sort_key: String::new(),
265 global_secondary_indexes: vec![DdbIndex::default(), DdbIndex::default()],
266 total_size_bytes: 2048,
267 ..Default::default()
268 };
269
270 let (indexes, _) = TableColumn::render(&Column::Indexes, &table);
271 assert_eq!(indexes, "2");
272 let (sort, _) = TableColumn::render(&Column::SortKey, &table);
274 assert_eq!(sort, "-");
275 let (regions, _) = TableColumn::render(&Column::ReplicationRegions, &table);
276 assert_eq!(regions, "-");
277 let (status, style) = TableColumn::render(&Column::Status, &table);
278 assert_eq!(status, "✅ Active");
279 assert_eq!(style.fg, Some(Color::Green));
280
281 let replicated = Table {
282 replicas: vec![
283 DdbReplica {
284 region: "us-east-1".to_string(),
285 ..Default::default()
286 },
287 DdbReplica {
288 region: "eu-west-1".to_string(),
289 ..Default::default()
290 },
291 ],
292 ..table
293 };
294 let (regions, _) = TableColumn::render(&Column::ReplicationRegions, &replicated);
295 assert_eq!(regions, "us-east-1, eu-west-1");
296 }
297
298 #[test]
299 fn test_format_status() {
300 assert_eq!(format_status("ACTIVE").1, Color::Green);
301 assert_eq!(format_status("CREATING").1, Color::Yellow);
302 assert_eq!(format_status("DELETING").1, Color::Red);
303 assert_eq!(format_status("whatever").0, "Unknown");
304 }
305}