1use crate::common::{ColumnTrait, UTC_TIMESTAMP_WIDTH};
2use crate::ui::cfn::DetailTab;
3use crate::ui::table::Column as TableColumn;
4use ratatui::prelude::*;
5
6pub fn console_url_stacks(region: &str) -> String {
7 format!(
8 "https://{}.console.aws.amazon.com/cloudformation/home?region={}#/stacks",
9 region, region
10 )
11}
12
13pub fn console_url_stack_detail(region: &str, stack_name: &str, stack_id: &str) -> String {
14 format!(
15 "https://{}.console.aws.amazon.com/cloudformation/home?region={}#/stacks/{}?filteringText=&filteringStatus=active&viewNested=true&stackId={}",
16 region, region, stack_name, stack_id
17 )
18}
19
20pub fn console_url_stack_detail_with_tab(region: &str, stack_id: &str, tab: &DetailTab) -> String {
21 let tab_path = match tab {
22 DetailTab::StackInfo => "stackinfo",
23 DetailTab::Events => "events",
24 DetailTab::Resources => "resources",
25 DetailTab::Outputs => "outputs",
26 DetailTab::Parameters => "parameters",
27 DetailTab::Template => "template",
28 DetailTab::ChangeSets => "changesets",
29 DetailTab::GitSync => "gitsync",
30 };
31 let encoded_arn = urlencoding::encode(stack_id);
32 format!(
33 "https://{}.console.aws.amazon.com/cloudformation/home?region={}#/stacks/{}?filteringText=&filteringStatus=active&viewNested=true&stackId={}",
34 region, region, tab_path, encoded_arn
35 )
36}
37
38#[derive(Debug, Clone)]
39pub struct Stack {
40 pub name: String,
41 pub stack_id: String,
42 pub status: String,
43 pub created_time: String,
44 pub updated_time: String,
45 pub deleted_time: String,
46 pub drift_status: String,
47 pub last_drift_check_time: String,
48 pub status_reason: String,
49 pub description: String,
50 pub detailed_status: String,
51 pub root_stack: String,
52 pub parent_stack: String,
53 pub termination_protection: bool,
54 pub iam_role: String,
55 pub tags: Vec<(String, String)>,
56 pub stack_policy: String,
57 pub rollback_monitoring_time: String,
58 pub rollback_alarms: Vec<String>,
59 pub notification_arns: Vec<String>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub enum Column {
64 Name,
65 StackId,
66 Status,
67 CreatedTime,
68 UpdatedTime,
69 DeletedTime,
70 DriftStatus,
71 LastDriftCheckTime,
72 StatusReason,
73 Description,
74}
75
76impl Column {
77 pub fn name(&self) -> &'static str {
78 match self {
79 Column::Name => "Stack Name",
80 Column::StackId => "Stack ID",
81 Column::Status => "Status",
82 Column::CreatedTime => "Created Time",
83 Column::UpdatedTime => "Updated Time",
84 Column::DeletedTime => "Deleted Time",
85 Column::DriftStatus => "Drift Status",
86 Column::LastDriftCheckTime => "Last Drift Check Time",
87 Column::StatusReason => "Status Reason",
88 Column::Description => "Description",
89 }
90 }
91
92 pub fn all() -> Vec<Column> {
93 vec![
94 Column::Name,
95 Column::StackId,
96 Column::Status,
97 Column::CreatedTime,
98 Column::UpdatedTime,
99 Column::DeletedTime,
100 Column::DriftStatus,
101 Column::LastDriftCheckTime,
102 Column::StatusReason,
103 Column::Description,
104 ]
105 }
106
107 pub fn to_column(&self) -> Box<dyn TableColumn<&Stack>> {
108 struct StackColumn {
109 variant: Column,
110 }
111
112 impl TableColumn<&Stack> for StackColumn {
113 fn name(&self) -> &str {
114 self.variant.name()
115 }
116
117 fn width(&self) -> u16 {
118 self.variant.name().len().max(match self.variant {
119 Column::Name => 30,
120 Column::StackId => 20,
121 Column::Status => 35,
122 Column::CreatedTime
123 | Column::UpdatedTime
124 | Column::DeletedTime
125 | Column::LastDriftCheckTime => UTC_TIMESTAMP_WIDTH as usize,
126 Column::DriftStatus => 20,
127 Column::StatusReason | Column::Description => 50,
128 }) as u16
129 }
130
131 fn render(&self, item: &&Stack) -> (String, Style) {
132 match self.variant {
133 Column::Name => (item.name.clone(), Style::default()),
134 Column::StackId => (item.stack_id.clone(), Style::default()),
135 Column::Status => {
136 let (formatted, color) = format_status(&item.status);
137 (formatted, Style::default().fg(color))
138 }
139 Column::CreatedTime => (item.created_time.clone(), Style::default()),
140 Column::UpdatedTime => (item.updated_time.clone(), Style::default()),
141 Column::DeletedTime => (item.deleted_time.clone(), Style::default()),
142 Column::DriftStatus => (item.drift_status.clone(), Style::default()),
143 Column::LastDriftCheckTime => {
144 (item.last_drift_check_time.clone(), Style::default())
145 }
146 Column::StatusReason => (item.status_reason.clone(), Style::default()),
147 Column::Description => (item.description.clone(), Style::default()),
148 }
149 }
150 }
151
152 Box::new(StackColumn { variant: *self })
153 }
154}
155
156impl ColumnTrait for Column {
157 fn name(&self) -> &'static str {
158 self.name()
159 }
160}
161
162pub fn format_status(status: &str) -> (String, ratatui::style::Color) {
163 let (emoji, color) = match status {
164 "UPDATE_COMPLETE" | "CREATE_COMPLETE" | "DELETE_COMPLETE" | "IMPORT_COMPLETE" => {
165 ("✅ ", ratatui::style::Color::Green)
166 }
167 "ROLLBACK_COMPLETE"
168 | "UPDATE_ROLLBACK_COMPLETE"
169 | "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"
170 | "UPDATE_FAILED"
171 | "CREATE_FAILED"
172 | "DELETE_FAILED"
173 | "ROLLBACK_FAILED"
174 | "UPDATE_ROLLBACK_FAILED"
175 | "IMPORT_ROLLBACK_FAILED"
176 | "IMPORT_ROLLBACK_COMPLETE" => ("❌ ", ratatui::style::Color::Red),
177 "UPDATE_IN_PROGRESS"
178 | "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"
179 | "DELETE_IN_PROGRESS"
180 | "CREATE_IN_PROGRESS"
181 | "ROLLBACK_IN_PROGRESS"
182 | "UPDATE_ROLLBACK_IN_PROGRESS"
183 | "REVIEW_IN_PROGRESS"
184 | "IMPORT_IN_PROGRESS"
185 | "IMPORT_ROLLBACK_IN_PROGRESS" => ("ℹ️ ", ratatui::style::Color::Blue),
186 _ => ("", ratatui::style::Color::White),
187 };
188
189 (format!("{}{}", emoji, status), color)
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::common::{CyclicEnum, SortDirection};
196 use crate::ui::cfn::{DetailTab, State, StatusFilter};
197
198 #[test]
199 fn test_state_default() {
200 let state = State::default();
201 assert_eq!(state.table.items.len(), 0);
202 assert_eq!(state.table.selected, 0);
203 assert!(!state.table.loading);
204 assert_eq!(state.table.filter, "");
205 assert_eq!(state.status_filter, StatusFilter::All);
206 assert!(!state.view_nested);
207 assert_eq!(state.table.expanded_item, None);
208 assert_eq!(state.current_stack, None);
209 assert_eq!(state.detail_tab, DetailTab::StackInfo);
210 assert_eq!(state.overview_scroll, 0);
211 assert_eq!(state.sort_column, Column::CreatedTime);
212 assert_eq!(state.sort_direction, SortDirection::Desc);
213 }
214
215 #[test]
216 fn test_status_filter_names() {
217 assert_eq!(StatusFilter::Active.name(), "Active");
218 assert_eq!(StatusFilter::Complete.name(), "Complete");
219 assert_eq!(StatusFilter::Failed.name(), "Failed");
220 assert_eq!(StatusFilter::Deleted.name(), "Deleted");
221 assert_eq!(StatusFilter::InProgress.name(), "In progress");
222 }
223
224 #[test]
225 fn test_status_filter_next() {
226 assert_eq!(StatusFilter::All.next(), StatusFilter::Active);
227 assert_eq!(StatusFilter::Active.next(), StatusFilter::Complete);
228 assert_eq!(StatusFilter::Complete.next(), StatusFilter::Failed);
229 assert_eq!(StatusFilter::Failed.next(), StatusFilter::Deleted);
230 assert_eq!(StatusFilter::Deleted.next(), StatusFilter::InProgress);
231 assert_eq!(StatusFilter::InProgress.next(), StatusFilter::All);
232 }
233
234 #[test]
235 fn test_status_filter_matches_active() {
236 let filter = StatusFilter::Active;
237 assert!(filter.matches("CREATE_IN_PROGRESS"));
238 assert!(filter.matches("UPDATE_IN_PROGRESS"));
239 assert!(!filter.matches("CREATE_COMPLETE"));
240 assert!(!filter.matches("DELETE_COMPLETE"));
241 assert!(!filter.matches("CREATE_FAILED"));
242 }
243
244 #[test]
245 fn test_status_filter_matches_complete() {
246 let filter = StatusFilter::Complete;
247 assert!(filter.matches("CREATE_COMPLETE"));
248 assert!(filter.matches("UPDATE_COMPLETE"));
249 assert!(!filter.matches("DELETE_COMPLETE"));
250 assert!(!filter.matches("CREATE_FAILED"));
251 assert!(!filter.matches("CREATE_IN_PROGRESS"));
252 }
253
254 #[test]
255 fn test_status_filter_matches_failed() {
256 let filter = StatusFilter::Failed;
257 assert!(filter.matches("CREATE_FAILED"));
258 assert!(filter.matches("UPDATE_FAILED"));
259 assert!(filter.matches("ROLLBACK_FAILED"));
260 assert!(!filter.matches("CREATE_COMPLETE"));
261 assert!(!filter.matches("DELETE_COMPLETE"));
262 }
263
264 #[test]
265 fn test_status_filter_matches_deleted() {
266 let filter = StatusFilter::Deleted;
267 assert!(filter.matches("DELETE_COMPLETE"));
268 assert!(filter.matches("DELETE_IN_PROGRESS"));
269 assert!(filter.matches("DELETE_FAILED"));
270 assert!(!filter.matches("CREATE_COMPLETE"));
271 assert!(!filter.matches("UPDATE_FAILED"));
272 }
273
274 #[test]
275 fn test_status_filter_matches_in_progress() {
276 let filter = StatusFilter::InProgress;
277 assert!(filter.matches("CREATE_IN_PROGRESS"));
278 assert!(filter.matches("UPDATE_IN_PROGRESS"));
279 assert!(filter.matches("DELETE_IN_PROGRESS"));
280 assert!(!filter.matches("CREATE_COMPLETE"));
281 assert!(!filter.matches("CREATE_FAILED"));
282 }
283
284 #[test]
285 fn test_detail_tab_names() {
286 assert_eq!(DetailTab::StackInfo.name(), "Stack info");
287 assert_eq!(DetailTab::Events.name(), "Events");
288 assert_eq!(DetailTab::Resources.name(), "Resources");
289 assert_eq!(DetailTab::Outputs.name(), "Outputs");
290 assert_eq!(DetailTab::Parameters.name(), "Parameters");
291 assert_eq!(DetailTab::Template.name(), "Template");
292 assert_eq!(DetailTab::ChangeSets.name(), "Change sets");
293 assert_eq!(DetailTab::GitSync.name(), "Git sync");
294 }
295
296 #[test]
297 fn test_detail_tab_next() {
298 assert_eq!(DetailTab::StackInfo.next(), DetailTab::StackInfo);
299 }
300
301 #[test]
302 fn test_column_names() {
303 assert_eq!(Column::Name.name(), "Stack Name");
304 assert_eq!(Column::StackId.name(), "Stack ID");
305 assert_eq!(Column::Status.name(), "Status");
306 assert_eq!(Column::CreatedTime.name(), "Created Time");
307 assert_eq!(Column::UpdatedTime.name(), "Updated Time");
308 assert_eq!(Column::DeletedTime.name(), "Deleted Time");
309 assert_eq!(Column::DriftStatus.name(), "Drift Status");
310 assert_eq!(Column::LastDriftCheckTime.name(), "Last Drift Check Time");
311 assert_eq!(Column::StatusReason.name(), "Status Reason");
312 assert_eq!(Column::Description.name(), "Description");
313 }
314
315 #[test]
316 fn test_column_all() {
317 let columns = Column::all();
318 assert_eq!(columns.len(), 10);
319 assert_eq!(columns[0], Column::Name);
320 assert_eq!(columns[9], Column::Description);
321 }
322
323 #[test]
324 fn test_column_trait() {
325 let column = Column::Name;
326 assert_eq!(ColumnTrait::name(&column), "Stack Name");
327 }
328
329 #[test]
330 fn test_format_status_complete_green() {
331 let (formatted, color) = format_status("UPDATE_COMPLETE");
332 assert_eq!(formatted, "✅ UPDATE_COMPLETE");
333 assert_eq!(color, ratatui::style::Color::Green);
334
335 let (formatted, color) = format_status("CREATE_COMPLETE");
336 assert_eq!(formatted, "✅ CREATE_COMPLETE");
337 assert_eq!(color, ratatui::style::Color::Green);
338
339 let (formatted, color) = format_status("DELETE_COMPLETE");
340 assert_eq!(formatted, "✅ DELETE_COMPLETE");
341 assert_eq!(color, ratatui::style::Color::Green);
342 }
343
344 #[test]
345 fn test_format_status_failed_red() {
346 let (formatted, color) = format_status("UPDATE_FAILED");
347 assert_eq!(formatted, "❌ UPDATE_FAILED");
348 assert_eq!(color, ratatui::style::Color::Red);
349
350 let (formatted, color) = format_status("CREATE_FAILED");
351 assert_eq!(formatted, "❌ CREATE_FAILED");
352 assert_eq!(color, ratatui::style::Color::Red);
353
354 let (formatted, color) = format_status("DELETE_FAILED");
355 assert_eq!(formatted, "❌ DELETE_FAILED");
356 assert_eq!(color, ratatui::style::Color::Red);
357
358 let (formatted, color) = format_status("ROLLBACK_FAILED");
359 assert_eq!(formatted, "❌ ROLLBACK_FAILED");
360 assert_eq!(color, ratatui::style::Color::Red);
361 }
362
363 #[test]
364 fn test_format_status_rollback_red() {
365 let (formatted, color) = format_status("ROLLBACK_COMPLETE");
366 assert_eq!(formatted, "❌ ROLLBACK_COMPLETE");
367 assert_eq!(color, ratatui::style::Color::Red);
368
369 let (formatted, color) = format_status("UPDATE_ROLLBACK_COMPLETE");
370 assert_eq!(formatted, "❌ UPDATE_ROLLBACK_COMPLETE");
371 assert_eq!(color, ratatui::style::Color::Red);
372
373 let (formatted, color) = format_status("UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS");
374 assert_eq!(formatted, "❌ UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS");
375 assert_eq!(color, ratatui::style::Color::Red);
376 }
377
378 #[test]
379 fn test_format_status_in_progress_blue() {
380 let (formatted, color) = format_status("UPDATE_IN_PROGRESS");
381 assert_eq!(formatted, "ℹ️ UPDATE_IN_PROGRESS");
382 assert_eq!(color, ratatui::style::Color::Blue);
383
384 let (formatted, color) = format_status("CREATE_IN_PROGRESS");
385 assert_eq!(formatted, "ℹ️ CREATE_IN_PROGRESS");
386 assert_eq!(color, ratatui::style::Color::Blue);
387
388 let (formatted, color) = format_status("DELETE_IN_PROGRESS");
389 assert_eq!(formatted, "ℹ️ DELETE_IN_PROGRESS");
390 assert_eq!(color, ratatui::style::Color::Blue);
391
392 let (formatted, color) = format_status("UPDATE_COMPLETE_CLEANUP_IN_PROGRESS");
393 assert_eq!(formatted, "ℹ️ UPDATE_COMPLETE_CLEANUP_IN_PROGRESS");
394 assert_eq!(color, ratatui::style::Color::Blue);
395
396 let (formatted, color) = format_status("ROLLBACK_IN_PROGRESS");
397 assert_eq!(formatted, "ℹ️ ROLLBACK_IN_PROGRESS");
398 assert_eq!(color, ratatui::style::Color::Blue);
399
400 let (formatted, color) = format_status("UPDATE_ROLLBACK_IN_PROGRESS");
401 assert_eq!(formatted, "ℹ️ UPDATE_ROLLBACK_IN_PROGRESS");
402 assert_eq!(color, ratatui::style::Color::Blue);
403 }
404
405 #[test]
406 fn test_format_status_unknown() {
407 let (formatted, color) = format_status("UNKNOWN_STATUS");
408 assert_eq!(formatted, "UNKNOWN_STATUS");
409 assert_eq!(color, ratatui::style::Color::White);
410 }
411
412 #[test]
413 fn test_format_status_emoji_spacing() {
414 let (formatted, _) = format_status("CREATE_IN_PROGRESS");
416 assert!(formatted.starts_with("ℹ️ ")); let (formatted, _) = format_status("CREATE_COMPLETE");
419 assert!(formatted.starts_with("✅ ")); let (formatted, _) = format_status("CREATE_FAILED");
422 assert!(formatted.starts_with("❌ ")); }
424
425 #[test]
426 fn test_all_aws_statuses_covered() {
427 let statuses = vec![
429 "CREATE_IN_PROGRESS",
430 "CREATE_FAILED",
431 "CREATE_COMPLETE",
432 "ROLLBACK_IN_PROGRESS",
433 "ROLLBACK_FAILED",
434 "ROLLBACK_COMPLETE",
435 "DELETE_IN_PROGRESS",
436 "DELETE_FAILED",
437 "DELETE_COMPLETE",
438 "UPDATE_IN_PROGRESS",
439 "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
440 "UPDATE_COMPLETE",
441 "UPDATE_FAILED",
442 "UPDATE_ROLLBACK_IN_PROGRESS",
443 "UPDATE_ROLLBACK_FAILED",
444 "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
445 "UPDATE_ROLLBACK_COMPLETE",
446 "REVIEW_IN_PROGRESS",
447 "IMPORT_IN_PROGRESS",
448 "IMPORT_COMPLETE",
449 "IMPORT_ROLLBACK_IN_PROGRESS",
450 "IMPORT_ROLLBACK_FAILED",
451 "IMPORT_ROLLBACK_COMPLETE",
452 ];
453
454 for status in statuses {
455 let (formatted, _) = format_status(status);
456 assert!(!formatted.is_empty());
458 assert!(formatted.len() > 2); }
460 }
461}