1use crate::aws::Region;
2use crate::common::{render_dropdown, InputFocus};
3use crate::keymap::Mode::FilterInput;
4use crate::sqs::pipe::{Column as PipeColumn, EventBridgePipe};
5use crate::sqs::queue::{Column as SqsColumn, Queue};
6use crate::sqs::sub::{Column as SubscriptionColumn, SnsSubscription};
7use crate::sqs::tag::{Column as TagColumn, QueueTag};
8use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
9use crate::table::TableState;
10use crate::ui::filter::{
11 render_filter_bar, render_simple_filter, FilterConfig, FilterControl, SimpleFilterConfig,
12};
13use crate::ui::{labeled_field, render_tabs};
14use ratatui::widgets::*;
15
16pub const FILTER_CONTROLS: &[InputFocus] = &[InputFocus::Filter, InputFocus::Pagination];
17pub const SUBSCRIPTION_REGION: InputFocus = InputFocus::Dropdown("SubscriptionRegion");
18pub const SUBSCRIPTION_FILTER_CONTROLS: &[InputFocus] = &[
19 InputFocus::Filter,
20 SUBSCRIPTION_REGION,
21 InputFocus::Pagination,
22];
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum QueueDetailTab {
26 QueuePolicies,
27 Monitoring,
28 SnsSubscriptions,
29 LambdaTriggers,
30 EventBridgePipes,
31 DeadLetterQueue,
32 Tagging,
33 Encryption,
34 DeadLetterQueueRedriveTasks,
35}
36
37impl QueueDetailTab {
38 pub fn all() -> Vec<QueueDetailTab> {
39 vec![
40 QueueDetailTab::QueuePolicies,
41 QueueDetailTab::Monitoring,
42 QueueDetailTab::SnsSubscriptions,
43 QueueDetailTab::LambdaTriggers,
44 QueueDetailTab::EventBridgePipes,
45 QueueDetailTab::DeadLetterQueue,
46 QueueDetailTab::Tagging,
47 QueueDetailTab::Encryption,
48 QueueDetailTab::DeadLetterQueueRedriveTasks,
49 ]
50 }
51
52 pub fn name(&self) -> &'static str {
53 match self {
54 QueueDetailTab::QueuePolicies => "Queue policies",
55 QueueDetailTab::Monitoring => "Monitoring",
56 QueueDetailTab::SnsSubscriptions => "SNS subscriptions",
57 QueueDetailTab::LambdaTriggers => "Lambda triggers",
58 QueueDetailTab::EventBridgePipes => "EventBridge Pipes",
59 QueueDetailTab::Tagging => "Tagging",
60 QueueDetailTab::Encryption => "Encryption",
61 QueueDetailTab::DeadLetterQueueRedriveTasks => "Dead-letter queue redrive tasks",
62 QueueDetailTab::DeadLetterQueue => "Dead-letter queue",
63 }
64 }
65}
66
67impl crate::common::CyclicEnum for QueueDetailTab {
68 const ALL: &'static [Self] = &[
69 QueueDetailTab::QueuePolicies,
70 QueueDetailTab::Monitoring,
71 QueueDetailTab::SnsSubscriptions,
72 QueueDetailTab::LambdaTriggers,
73 QueueDetailTab::EventBridgePipes,
74 QueueDetailTab::DeadLetterQueue,
75 QueueDetailTab::Tagging,
76 QueueDetailTab::Encryption,
77 QueueDetailTab::DeadLetterQueueRedriveTasks,
78 ];
79}
80
81#[derive(Debug, Clone)]
82pub struct State {
83 pub queues: TableState<Queue>,
84 pub triggers: TableState<LambdaTrigger>,
85 pub trigger_visible_column_ids: Vec<String>,
86 pub trigger_column_ids: Vec<String>,
87 pub pipes: TableState<EventBridgePipe>,
88 pub pipe_visible_column_ids: Vec<String>,
89 pub pipe_column_ids: Vec<String>,
90 pub tags: TableState<QueueTag>,
91 pub tag_visible_column_ids: Vec<String>,
92 pub tag_column_ids: Vec<String>,
93 pub subscriptions: TableState<SnsSubscription>,
94 pub subscription_visible_column_ids: Vec<String>,
95 pub subscription_column_ids: Vec<String>,
96 pub subscription_region_filter: String,
97 pub subscription_region_selected: usize,
98 pub input_focus: InputFocus,
99 pub current_queue: Option<String>,
100 pub detail_tab: QueueDetailTab,
101 pub policy_scroll: usize,
102 pub policy_document: String,
103 pub metric_data: Vec<(i64, f64)>, pub metric_data_delayed: Vec<(i64, f64)>, pub metric_data_not_visible: Vec<(i64, f64)>, pub metric_data_visible: Vec<(i64, f64)>, pub metric_data_empty_receives: Vec<(i64, f64)>, pub metric_data_messages_deleted: Vec<(i64, f64)>, pub metric_data_messages_received: Vec<(i64, f64)>, pub metric_data_messages_sent: Vec<(i64, f64)>, pub metric_data_sent_message_size: Vec<(i64, f64)>, pub metrics_loading: bool,
113 pub monitoring_scroll: usize,
114}
115
116impl Default for State {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl State {
123 pub fn new() -> Self {
124 let trigger_column_ids: Vec<String> = TriggerColumn::ids()
125 .into_iter()
126 .map(|s| s.to_string())
127 .collect();
128 let pipe_column_ids: Vec<String> = PipeColumn::ids()
129 .into_iter()
130 .map(|s| s.to_string())
131 .collect();
132 let tag_column_ids: Vec<String> = TagColumn::ids()
133 .into_iter()
134 .map(|s| s.to_string())
135 .collect();
136 let subscription_column_ids: Vec<String> = SubscriptionColumn::ids()
137 .into_iter()
138 .map(|s| s.to_string())
139 .collect();
140 Self {
141 queues: TableState::new(),
142 triggers: TableState::new(),
143 trigger_visible_column_ids: trigger_column_ids.clone(),
144 trigger_column_ids,
145 pipes: TableState::new(),
146 pipe_visible_column_ids: pipe_column_ids.clone(),
147 pipe_column_ids,
148 tags: TableState::new(),
149 tag_visible_column_ids: tag_column_ids.clone(),
150 tag_column_ids,
151 subscriptions: TableState::new(),
152 subscription_visible_column_ids: subscription_column_ids.clone(),
153 subscription_column_ids,
154 subscription_region_filter: String::new(),
155 subscription_region_selected: 0,
156 input_focus: InputFocus::Filter,
157 current_queue: None,
158 detail_tab: QueueDetailTab::QueuePolicies,
159 policy_scroll: 0,
160 policy_document: r#"{
161 "Version": "2012-10-17",
162 "Statement": [
163 {
164 "Effect": "Allow",
165 "Principal": "*",
166 "Action": "sqs:*",
167 "Resource": "*"
168 }
169 ]
170}"#
171 .to_string(),
172 metric_data: Vec::new(),
173 metric_data_delayed: Vec::new(),
174 metric_data_not_visible: Vec::new(),
175 metric_data_visible: Vec::new(),
176 metric_data_empty_receives: Vec::new(),
177 metric_data_messages_deleted: Vec::new(),
178 metric_data_messages_received: Vec::new(),
179 metric_data_messages_sent: Vec::new(),
180 metric_data_sent_message_size: Vec::new(),
181 metrics_loading: false,
182 monitoring_scroll: 0,
183 }
184 }
185}
186
187use crate::ui::monitoring::MonitoringState;
188
189impl MonitoringState for State {
190 fn is_metrics_loading(&self) -> bool {
191 self.metrics_loading
192 }
193
194 fn set_metrics_loading(&mut self, loading: bool) {
195 self.metrics_loading = loading;
196 }
197
198 fn monitoring_scroll(&self) -> usize {
199 self.monitoring_scroll
200 }
201
202 fn set_monitoring_scroll(&mut self, scroll: usize) {
203 self.monitoring_scroll = scroll;
204 }
205
206 fn clear_metrics(&mut self) {
207 self.metric_data.clear();
208 self.metric_data_delayed.clear();
209 self.metric_data_not_visible.clear();
210 self.metric_data_visible.clear();
211 self.metric_data_empty_receives.clear();
212 self.metric_data_messages_deleted.clear();
213 self.metric_data_messages_received.clear();
214 self.metric_data_messages_sent.clear();
215 self.metric_data_sent_message_size.clear();
216 }
217}
218
219pub fn filtered_queues<'a>(queues: &'a [Queue], filter: &str) -> Vec<&'a Queue> {
220 queues
221 .iter()
222 .filter(|q| filter.is_empty() || q.name.to_lowercase().starts_with(&filter.to_lowercase()))
223 .collect()
224}
225
226pub fn filtered_lambda_triggers(app: &crate::App) -> Vec<&crate::sqs::LambdaTrigger> {
227 let mut filtered: Vec<_> = app
228 .sqs_state
229 .triggers
230 .items
231 .iter()
232 .filter(|t| {
233 app.sqs_state.triggers.filter.is_empty()
234 || t.uuid
235 .to_lowercase()
236 .contains(&app.sqs_state.triggers.filter.to_lowercase())
237 || t.arn
238 .to_lowercase()
239 .contains(&app.sqs_state.triggers.filter.to_lowercase())
240 })
241 .collect();
242
243 filtered.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
245 filtered
246}
247
248pub fn filtered_tags(app: &crate::App) -> Vec<&QueueTag> {
249 let mut filtered: Vec<_> = app
250 .sqs_state
251 .tags
252 .items
253 .iter()
254 .filter(|t| {
255 app.sqs_state.tags.filter.is_empty()
256 || t.key
257 .to_lowercase()
258 .contains(&app.sqs_state.tags.filter.to_lowercase())
259 || t.value
260 .to_lowercase()
261 .contains(&app.sqs_state.tags.filter.to_lowercase())
262 })
263 .collect();
264
265 filtered.sort_by(|a, b| a.value.cmp(&b.value));
267 filtered
268}
269
270pub fn filtered_subscriptions(app: &crate::App) -> Vec<&SnsSubscription> {
271 let region_filter = if app.sqs_state.subscription_region_filter.is_empty() {
272 &app.region
273 } else {
274 &app.sqs_state.subscription_region_filter
275 };
276
277 let mut filtered: Vec<_> = app
278 .sqs_state
279 .subscriptions
280 .items
281 .iter()
282 .filter(|s| {
283 let text_match = app.sqs_state.subscriptions.filter.is_empty()
284 || s.subscription_arn
285 .to_lowercase()
286 .contains(&app.sqs_state.subscriptions.filter.to_lowercase())
287 || s.topic_arn
288 .to_lowercase()
289 .contains(&app.sqs_state.subscriptions.filter.to_lowercase());
290
291 let region_match = s.subscription_arn.contains(region_filter);
292
293 text_match && region_match
294 })
295 .collect();
296
297 filtered.sort_by(|a, b| a.subscription_arn.cmp(&b.subscription_arn));
299 filtered
300}
301
302pub fn filtered_eventbridge_pipes(app: &crate::App) -> Vec<&crate::sqs::EventBridgePipe> {
303 let mut filtered: Vec<_> = app
304 .sqs_state
305 .pipes
306 .items
307 .iter()
308 .filter(|p| {
309 app.sqs_state.pipes.filter.is_empty()
310 || p.name
311 .to_lowercase()
312 .contains(&app.sqs_state.pipes.filter.to_lowercase())
313 || p.target
314 .to_lowercase()
315 .contains(&app.sqs_state.pipes.filter.to_lowercase())
316 })
317 .collect();
318
319 filtered.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
320 filtered
321}
322
323pub async fn load_sqs_queues(app: &mut crate::App) -> anyhow::Result<()> {
324 let queues = app.sqs_client.list_queues("").await?;
325 app.sqs_state.queues.items = queues
326 .into_iter()
327 .map(|q| Queue {
328 name: q.name,
329 url: q.url,
330 queue_type: q.queue_type,
331 created_timestamp: q.created_timestamp,
332 messages_available: q.messages_available,
333 messages_in_flight: q.messages_in_flight,
334 encryption: q.encryption,
335 content_based_deduplication: q.content_based_deduplication,
336 last_modified_timestamp: q.last_modified_timestamp,
337 visibility_timeout: q.visibility_timeout,
338 message_retention_period: q.message_retention_period,
339 maximum_message_size: q.maximum_message_size,
340 delivery_delay: q.delivery_delay,
341 receive_message_wait_time: q.receive_message_wait_time,
342 high_throughput_fifo: q.high_throughput_fifo,
343 deduplication_scope: q.deduplication_scope,
344 fifo_throughput_limit: q.fifo_throughput_limit,
345 dead_letter_queue: q.dead_letter_queue,
346 messages_delayed: q.messages_delayed,
347 redrive_allow_policy: q.redrive_allow_policy,
348 redrive_policy: q.redrive_policy,
349 redrive_task_id: q.redrive_task_id,
350 redrive_task_start_time: q.redrive_task_start_time,
351 redrive_task_status: q.redrive_task_status,
352 redrive_task_percent: q.redrive_task_percent,
353 redrive_task_destination: q.redrive_task_destination,
354 })
355 .collect();
356 Ok(())
357}
358
359pub async fn load_lambda_triggers(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
360 let queue_arn = app.sqs_client.get_queue_arn(queue_url).await?;
361 let triggers = app.sqs_client.list_lambda_triggers(&queue_arn).await?;
362
363 app.sqs_state.triggers.items = triggers
364 .into_iter()
365 .map(|t| LambdaTrigger {
366 uuid: t.uuid,
367 arn: t.arn,
368 status: t.status,
369 last_modified: t.last_modified,
370 })
371 .collect();
372
373 app.sqs_state
375 .triggers
376 .items
377 .sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
378
379 Ok(())
380}
381
382pub async fn load_metrics(app: &mut crate::App, queue_name: &str) -> anyhow::Result<()> {
383 let metrics = app.sqs_client.get_queue_metrics(queue_name).await?;
384 app.sqs_state.metric_data = metrics;
385
386 let delayed_metrics = app.sqs_client.get_queue_delayed_metrics(queue_name).await?;
387 app.sqs_state.metric_data_delayed = delayed_metrics;
388
389 let not_visible_metrics = app
390 .sqs_client
391 .get_queue_not_visible_metrics(queue_name)
392 .await?;
393 app.sqs_state.metric_data_not_visible = not_visible_metrics;
394
395 let visible_metrics = app.sqs_client.get_queue_visible_metrics(queue_name).await?;
396 app.sqs_state.metric_data_visible = visible_metrics;
397
398 let empty_receives_metrics = app
399 .sqs_client
400 .get_queue_empty_receives_metrics(queue_name)
401 .await?;
402 app.sqs_state.metric_data_empty_receives = empty_receives_metrics;
403
404 let messages_deleted_metrics = app
405 .sqs_client
406 .get_queue_messages_deleted_metrics(queue_name)
407 .await?;
408 app.sqs_state.metric_data_messages_deleted = messages_deleted_metrics;
409
410 let messages_received_metrics = app
411 .sqs_client
412 .get_queue_messages_received_metrics(queue_name)
413 .await?;
414 app.sqs_state.metric_data_messages_received = messages_received_metrics;
415
416 let messages_sent_metrics = app
417 .sqs_client
418 .get_queue_messages_sent_metrics(queue_name)
419 .await?;
420 app.sqs_state.metric_data_messages_sent = messages_sent_metrics;
421
422 let sent_message_size_metrics = app
423 .sqs_client
424 .get_queue_sent_message_size_metrics(queue_name)
425 .await?;
426 app.sqs_state.metric_data_sent_message_size = sent_message_size_metrics;
427
428 Ok(())
429}
430
431pub async fn load_pipes(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
432 let queue_arn = app.sqs_client.get_queue_arn(queue_url).await?;
433 let pipes = app.sqs_client.list_pipes(&queue_arn).await?;
434
435 app.sqs_state.pipes.items = pipes
436 .into_iter()
437 .map(|p| EventBridgePipe {
438 name: p.name,
439 status: p.status,
440 target: p.target,
441 last_modified: p.last_modified,
442 })
443 .collect();
444
445 app.sqs_state
446 .pipes
447 .items
448 .sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
449
450 Ok(())
451}
452
453pub async fn load_tags(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
454 let tags = app.sqs_client.list_tags(queue_url).await?;
455
456 app.sqs_state.tags.items = tags
457 .into_iter()
458 .map(|t| QueueTag {
459 key: t.key,
460 value: t.value,
461 })
462 .collect();
463
464 app.sqs_state
465 .tags
466 .items
467 .sort_by(|a, b| a.value.cmp(&b.value));
468
469 Ok(())
470}
471
472pub fn render_queues(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
473 use ratatui::widgets::Clear;
474
475 frame.render_widget(Clear, area);
476
477 if app.sqs_state.current_queue.is_some() {
478 render_queue_detail(frame, app, area);
479 } else {
480 render_queue_list(frame, app, area);
481 }
482}
483
484fn render_queue_detail(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
485 use ratatui::prelude::*;
486 use ratatui::widgets::{Clear, Paragraph};
487
488 frame.render_widget(Clear, area);
489
490 let queue = app
491 .sqs_state
492 .queues
493 .items
494 .iter()
495 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
496
497 let queue_name = queue.map(|q| q.name.as_str()).unwrap_or("Unknown");
498
499 let details_height = queue.map_or(3, |q| {
500 let field_count = render_details_fields(q).len();
501 field_count as u16 + 2 });
503
504 let chunks = Layout::default()
505 .direction(Direction::Vertical)
506 .constraints([
507 Constraint::Length(1), Constraint::Length(details_height), Constraint::Length(1), Constraint::Min(0), ])
512 .split(area);
513
514 let header = Paragraph::new(queue_name).style(
516 Style::default()
517 .fg(Color::Yellow)
518 .add_modifier(Modifier::BOLD),
519 );
520 frame.render_widget(header, chunks[0]);
521
522 if let Some(q) = queue {
524 render_details_pane(frame, q, chunks[1]);
525 }
526
527 let tabs: Vec<(&str, QueueDetailTab)> = QueueDetailTab::all()
530 .into_iter()
531 .map(|tab| (tab.name(), tab))
532 .collect();
533
534 render_tabs(frame, chunks[2], &tabs, &app.sqs_state.detail_tab);
535
536 match app.sqs_state.detail_tab {
538 QueueDetailTab::QueuePolicies => {
539 render_queue_policies_tab(frame, app, chunks[3]);
540 }
541 QueueDetailTab::Monitoring => {
542 if app.sqs_state.metrics_loading {
543 let loading_block = Block::default()
544 .title(" Monitoring ")
545 .borders(Borders::ALL)
546 .border_type(BorderType::Rounded);
547 let loading_text = Paragraph::new("Loading metrics...")
548 .block(loading_block)
549 .alignment(ratatui::layout::Alignment::Center);
550 frame.render_widget(loading_text, chunks[3]);
551 } else {
552 use crate::ui::monitoring::{render_monitoring_tab, MetricChart};
553
554 let age_max: f64 = app
555 .sqs_state
556 .metric_data
557 .iter()
558 .map(|(_, v)| v)
559 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
560 let age_label = format!(
561 "Age [max: {}]",
562 if age_max.is_finite() {
563 format!("{:.0}s", age_max)
564 } else {
565 "--".to_string()
566 }
567 );
568
569 render_monitoring_tab(
570 frame,
571 chunks[3],
572 &[
573 MetricChart {
574 title: "Approximate age of oldest message",
575 data: &app.sqs_state.metric_data,
576 y_axis_label: "Seconds",
577 x_axis_label: Some(age_label),
578 },
579 MetricChart {
580 title: "Approximate number of messages delayed",
581 data: &app.sqs_state.metric_data_delayed,
582 y_axis_label: "Count",
583 x_axis_label: None,
584 },
585 MetricChart {
586 title: "Approximate number of messages not visible",
587 data: &app.sqs_state.metric_data_not_visible,
588 y_axis_label: "Count",
589 x_axis_label: None,
590 },
591 MetricChart {
592 title: "Approximate number of messages visible",
593 data: &app.sqs_state.metric_data_visible,
594 y_axis_label: "Count",
595 x_axis_label: None,
596 },
597 MetricChart {
598 title: "Number of empty receives",
599 data: &app.sqs_state.metric_data_empty_receives,
600 y_axis_label: "Count",
601 x_axis_label: None,
602 },
603 MetricChart {
604 title: "Number of messages deleted",
605 data: &app.sqs_state.metric_data_messages_deleted,
606 y_axis_label: "Count",
607 x_axis_label: None,
608 },
609 MetricChart {
610 title: "Number of messages received",
611 data: &app.sqs_state.metric_data_messages_received,
612 y_axis_label: "Count",
613 x_axis_label: None,
614 },
615 MetricChart {
616 title: "Number of messages sent",
617 data: &app.sqs_state.metric_data_messages_sent,
618 y_axis_label: "Count",
619 x_axis_label: None,
620 },
621 MetricChart {
622 title: "Sent message size",
623 data: &app.sqs_state.metric_data_sent_message_size,
624 y_axis_label: "Bytes",
625 x_axis_label: None,
626 },
627 ],
628 &[],
629 &[],
630 &[],
631 app.sqs_state.monitoring_scroll,
632 );
633 }
634 }
635 QueueDetailTab::SnsSubscriptions => {
636 render_subscriptions_tab(frame, app, chunks[3]);
637 }
638 QueueDetailTab::LambdaTriggers => {
639 render_lambda_triggers_tab(frame, app, chunks[3]);
640 }
641 QueueDetailTab::EventBridgePipes => {
642 render_eventbridge_pipes_tab(frame, app, chunks[3]);
643 }
644 QueueDetailTab::DeadLetterQueue => {
645 render_dead_letter_queue_tab(frame, app, chunks[3]);
646 }
647 QueueDetailTab::Tagging => {
648 render_tags_tab(frame, app, chunks[3]);
649 }
650 QueueDetailTab::Encryption => {
651 render_encryption_tab(frame, app, chunks[3]);
652 }
653 QueueDetailTab::DeadLetterQueueRedriveTasks => {
654 render_dlq_redrive_tasks_tab(frame, app, chunks[3]);
655 }
656 }
657}
658
659fn render_details_fields(queue: &Queue) -> Vec<ratatui::text::Line<'static>> {
660 let max_msg_size = queue
661 .maximum_message_size
662 .split_whitespace()
663 .next()
664 .and_then(|s| s.parse::<i64>().ok())
665 .map(crate::common::format_bytes)
666 .unwrap_or_else(|| queue.maximum_message_size.clone());
667
668 let retention_period = queue
669 .message_retention_period
670 .parse::<i32>()
671 .ok()
672 .map(crate::common::format_duration_seconds)
673 .unwrap_or_else(|| queue.message_retention_period.clone());
674
675 let visibility_timeout = queue
676 .visibility_timeout
677 .parse::<i32>()
678 .ok()
679 .map(crate::common::format_duration_seconds)
680 .unwrap_or_else(|| queue.visibility_timeout.clone());
681
682 let delivery_delay = queue
683 .delivery_delay
684 .parse::<i32>()
685 .ok()
686 .map(crate::common::format_duration_seconds)
687 .unwrap_or_else(|| queue.delivery_delay.clone());
688
689 let receive_wait_time = queue
690 .receive_message_wait_time
691 .parse::<i32>()
692 .ok()
693 .map(crate::common::format_duration_seconds)
694 .unwrap_or_else(|| queue.receive_message_wait_time.clone());
695
696 vec![
697 labeled_field("Name", &queue.name),
698 labeled_field("Type", &queue.queue_type),
699 labeled_field(
700 "ARN",
701 format!(
702 "arn:aws:sqs:{}:{}:{}",
703 extract_region(&queue.url),
704 extract_account_id(&queue.url),
705 queue.name
706 ),
707 ),
708 labeled_field("Encryption", &queue.encryption),
709 labeled_field("URL", &queue.url),
710 labeled_field("Dead-letter queue", &queue.dead_letter_queue),
711 labeled_field(
712 "Created",
713 crate::common::format_unix_timestamp(&queue.created_timestamp),
714 ),
715 labeled_field("Maximum message size", max_msg_size),
716 labeled_field(
717 "Last updated",
718 crate::common::format_unix_timestamp(&queue.last_modified_timestamp),
719 ),
720 labeled_field("Message retention period", retention_period),
721 labeled_field("Default visibility timeout", visibility_timeout),
722 labeled_field("Messages available", &queue.messages_available),
723 labeled_field("Delivery delay", delivery_delay),
724 labeled_field(
725 "Messages in flight (not available to other consumers)",
726 &queue.messages_in_flight,
727 ),
728 labeled_field("Receive message wait time", receive_wait_time),
729 labeled_field("Messages delayed", &queue.messages_delayed),
730 labeled_field(
731 "Content-based deduplication",
732 &queue.content_based_deduplication,
733 ),
734 labeled_field("High throughput FIFO", &queue.high_throughput_fifo),
735 labeled_field("Deduplication scope", &queue.deduplication_scope),
736 labeled_field("FIFO throughput limit", &queue.fifo_throughput_limit),
737 labeled_field("Redrive allow policy", &queue.redrive_allow_policy),
738 ]
739}
740
741fn render_details_pane(frame: &mut ratatui::Frame, queue: &Queue, area: ratatui::prelude::Rect) {
742 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
743
744 let block = Block::default()
745 .title(" Details ")
746 .borders(Borders::ALL)
747 .border_type(BorderType::Rounded)
748 .border_style(crate::ui::active_border());
749
750 let inner = block.inner(area);
751 frame.render_widget(block, area);
752
753 let lines = render_details_fields(queue);
754 let paragraph = Paragraph::new(lines);
755 frame.render_widget(paragraph, inner);
756}
757
758fn render_queue_policies_tab(
759 frame: &mut ratatui::Frame,
760 app: &crate::App,
761 area: ratatui::prelude::Rect,
762) {
763 use ratatui::prelude::{Constraint, Direction, Layout};
764
765 let chunks = Layout::default()
766 .direction(Direction::Vertical)
767 .constraints([Constraint::Min(0)])
768 .split(area);
769
770 crate::ui::render_json_highlighted(
772 frame,
773 chunks[0],
774 &app.sqs_state.policy_document,
775 app.sqs_state.policy_scroll,
776 " Access policy ",
777 true,
778 );
779}
780
781fn render_lambda_triggers_tab(
782 frame: &mut ratatui::Frame,
783 app: &crate::App,
784 area: ratatui::prelude::Rect,
785) {
786 use crate::ui::table::{render_table, Column, TableConfig};
787 use ratatui::prelude::*;
788
789 let chunks = Layout::default()
790 .direction(Direction::Vertical)
791 .constraints([Constraint::Length(3), Constraint::Min(0)])
792 .split(area);
793
794 let filtered = filtered_lambda_triggers(app);
795
796 let columns: Vec<Box<dyn Column<crate::sqs::LambdaTrigger>>> = app
797 .sqs_state
798 .trigger_visible_column_ids
799 .iter()
800 .filter_map(|id| TriggerColumn::from_id(id))
801 .map(|col| Box::new(col) as Box<dyn Column<crate::sqs::LambdaTrigger>>)
802 .collect();
803
804 let page_size = app.sqs_state.triggers.page_size.value();
806 let total_pages = filtered.len().div_ceil(page_size.max(1));
807 let current_page = app.sqs_state.triggers.selected / page_size.max(1);
808 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
809
810 render_simple_filter(
812 frame,
813 chunks[0],
814 SimpleFilterConfig {
815 filter_text: &app.sqs_state.triggers.filter,
816 placeholder: "Search triggers",
817 pagination: &pagination,
818 mode: app.mode,
819 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
820 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
821 },
822 );
823
824 let start_idx = current_page * page_size;
825 let end_idx = (start_idx + page_size).min(filtered.len());
826 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
827
828 let expanded_index = app.sqs_state.triggers.expanded_item.and_then(|idx| {
829 if idx >= start_idx && idx < end_idx {
830 Some(idx - start_idx)
831 } else {
832 None
833 }
834 });
835
836 render_table(
837 frame,
838 TableConfig {
839 area: chunks[1],
840 columns: &columns,
841 items: paginated,
842 selected_index: app.sqs_state.triggers.selected % page_size.max(1),
843 is_active: app.mode != crate::keymap::Mode::FilterInput,
844 title: format!(" Lambda triggers ({}) ", filtered.len()),
845 sort_column: "last_modified",
846 sort_direction: crate::common::SortDirection::Asc,
847 expanded_index,
848 get_expanded_content: Some(Box::new(|trigger: &crate::sqs::LambdaTrigger| {
849 crate::ui::table::expanded_from_columns(&columns, trigger)
850 })),
851 },
852 );
853}
854
855pub fn extract_region(url: &str) -> &str {
856 url.split("sqs.")
857 .nth(1)
858 .and_then(|s| s.split('.').next())
859 .unwrap_or("unknown")
860}
861
862pub fn extract_account_id(url: &str) -> &str {
863 url.split('/').nth(3).unwrap_or("unknown")
864}
865
866fn render_eventbridge_pipes_tab(
867 frame: &mut ratatui::Frame,
868 app: &crate::App,
869 area: ratatui::prelude::Rect,
870) {
871 use crate::ui::table::{render_table, Column, TableConfig};
872 use ratatui::prelude::*;
873
874 let chunks = Layout::default()
875 .direction(Direction::Vertical)
876 .constraints([Constraint::Length(3), Constraint::Min(0)])
877 .split(area);
878
879 let filtered = filtered_eventbridge_pipes(app);
880
881 let columns: Vec<Box<dyn Column<crate::sqs::EventBridgePipe>>> = app
882 .sqs_state
883 .pipe_visible_column_ids
884 .iter()
885 .filter_map(|id| PipeColumn::from_id(id))
886 .map(|col| Box::new(col) as Box<dyn Column<crate::sqs::EventBridgePipe>>)
887 .collect();
888
889 let page_size = app.sqs_state.pipes.page_size.value();
890 let total_pages = filtered.len().div_ceil(page_size.max(1));
891 let current_page = app.sqs_state.pipes.selected / page_size.max(1);
892 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
893
894 render_simple_filter(
895 frame,
896 chunks[0],
897 SimpleFilterConfig {
898 filter_text: &app.sqs_state.pipes.filter,
899 placeholder: "Search pipes",
900 pagination: &pagination,
901 mode: app.mode,
902 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
903 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
904 },
905 );
906
907 let start_idx = current_page * page_size;
908 let end_idx = (start_idx + page_size).min(filtered.len());
909 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
910
911 let expanded_index = app.sqs_state.pipes.expanded_item.and_then(|idx| {
912 if idx >= start_idx && idx < end_idx {
913 Some(idx - start_idx)
914 } else {
915 None
916 }
917 });
918
919 render_table(
920 frame,
921 TableConfig {
922 area: chunks[1],
923 columns: &columns,
924 items: paginated,
925 selected_index: app.sqs_state.pipes.selected % page_size.max(1),
926 is_active: app.mode != crate::keymap::Mode::FilterInput,
927 title: format!(" EventBridge Pipes ({}) ", filtered.len()),
928 sort_column: "last_modified",
929 sort_direction: crate::common::SortDirection::Asc,
930 expanded_index,
931 get_expanded_content: Some(Box::new(|pipe: &crate::sqs::EventBridgePipe| {
932 crate::ui::table::expanded_from_columns(&columns, pipe)
933 })),
934 },
935 );
936}
937
938fn render_tags_tab(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
939 use crate::ui::table::{render_table, Column, TableConfig};
940 use ratatui::prelude::*;
941
942 let chunks = Layout::default()
943 .direction(Direction::Vertical)
944 .constraints([Constraint::Length(3), Constraint::Min(0)])
945 .split(area);
946
947 let filtered = filtered_tags(app);
948
949 let columns: Vec<Box<dyn Column<QueueTag>>> = app
950 .sqs_state
951 .tag_visible_column_ids
952 .iter()
953 .filter_map(|id| TagColumn::from_id(id))
954 .map(|col| Box::new(col) as Box<dyn Column<QueueTag>>)
955 .collect();
956
957 let page_size = app.sqs_state.tags.page_size.value();
958 let total_pages = filtered.len().div_ceil(page_size.max(1));
959 let current_page = app.sqs_state.tags.selected / page_size.max(1);
960 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
961
962 render_simple_filter(
963 frame,
964 chunks[0],
965 SimpleFilterConfig {
966 filter_text: &app.sqs_state.tags.filter,
967 placeholder: "Search tags",
968 pagination: &pagination,
969 mode: app.mode,
970 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
971 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
972 },
973 );
974
975 let start_idx = current_page * page_size;
976 let end_idx = (start_idx + page_size).min(filtered.len());
977 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
978
979 let expanded_index = app.sqs_state.tags.expanded_item.and_then(|idx| {
980 if idx >= start_idx && idx < end_idx {
981 Some(idx - start_idx)
982 } else {
983 None
984 }
985 });
986
987 render_table(
988 frame,
989 TableConfig {
990 area: chunks[1],
991 columns: &columns,
992 items: paginated,
993 selected_index: app.sqs_state.tags.selected % page_size.max(1),
994 is_active: app.mode != crate::keymap::Mode::FilterInput,
995 title: format!(" Tagging ({}) ", filtered.len()),
996 sort_column: "value",
997 sort_direction: crate::common::SortDirection::Asc,
998 expanded_index,
999 get_expanded_content: Some(Box::new(|tag: &QueueTag| {
1000 crate::ui::table::expanded_from_columns(&columns, tag)
1001 })),
1002 },
1003 );
1004}
1005
1006fn render_subscriptions_tab(
1007 frame: &mut ratatui::Frame,
1008 app: &crate::App,
1009 area: ratatui::prelude::Rect,
1010) {
1011 use crate::ui::table::{render_table, Column, TableConfig};
1012 use ratatui::prelude::*;
1013
1014 let chunks = Layout::default()
1015 .direction(Direction::Vertical)
1016 .constraints([Constraint::Length(3), Constraint::Min(0)])
1017 .split(area);
1018
1019 let filtered = filtered_subscriptions(app);
1020
1021 let columns: Vec<Box<dyn Column<SnsSubscription>>> = app
1022 .sqs_state
1023 .subscription_visible_column_ids
1024 .iter()
1025 .filter_map(|id| SubscriptionColumn::from_id(id))
1026 .map(|col| Box::new(col) as Box<dyn Column<SnsSubscription>>)
1027 .collect();
1028
1029 let page_size = app.sqs_state.subscriptions.page_size.value();
1030 let total_pages = filtered.len().div_ceil(page_size.max(1));
1031 let current_page = app.sqs_state.subscriptions.selected / page_size.max(1);
1032 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1033
1034 render_subscription_filter(frame, app, chunks[0], &pagination);
1036
1037 let start_idx = current_page * page_size;
1038 let end_idx = (start_idx + page_size).min(filtered.len());
1039 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1040
1041 let expanded_index = app.sqs_state.subscriptions.expanded_item.and_then(|idx| {
1042 if idx >= start_idx && idx < end_idx {
1043 Some(idx - start_idx)
1044 } else {
1045 None
1046 }
1047 });
1048
1049 render_table(
1050 frame,
1051 TableConfig {
1052 area: chunks[1],
1053 columns: &columns,
1054 items: paginated,
1055 selected_index: app.sqs_state.subscriptions.selected % page_size.max(1),
1056 is_active: app.mode != crate::keymap::Mode::FilterInput,
1057 title: format!(" SNS subscriptions ({}) ", filtered.len()),
1058 sort_column: "subscription_arn",
1059 sort_direction: crate::common::SortDirection::Asc,
1060 expanded_index,
1061 get_expanded_content: Some(Box::new(|sub: &SnsSubscription| {
1062 crate::ui::table::expanded_from_columns(&columns, sub)
1063 })),
1064 },
1065 );
1066
1067 if app.mode == FilterInput && app.sqs_state.input_focus == SUBSCRIPTION_REGION {
1069 let regions = Region::all();
1070 let region_codes: Vec<&str> = regions.iter().map(|r| r.code).collect();
1071 render_dropdown(
1072 frame,
1073 ®ion_codes,
1074 app.sqs_state.subscription_region_selected,
1075 chunks[0],
1076 pagination.len() as u16 + 3, );
1078 }
1079}
1080
1081fn render_subscription_filter(
1082 frame: &mut ratatui::Frame,
1083 app: &crate::App,
1084 area: ratatui::prelude::Rect,
1085 pagination: &str,
1086) {
1087 let region_text = if app.sqs_state.subscription_region_filter.is_empty() {
1088 format!("Subscription region: {}", app.region)
1089 } else {
1090 format!(
1091 "Subscription region: {}",
1092 app.sqs_state.subscription_region_filter
1093 )
1094 };
1095
1096 render_filter_bar(
1097 frame,
1098 FilterConfig {
1099 filter_text: &app.sqs_state.subscriptions.filter,
1100 placeholder: "Search subscriptions",
1101 mode: app.mode,
1102 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1103 controls: vec![
1104 FilterControl {
1105 text: region_text,
1106 is_focused: app.sqs_state.input_focus == SUBSCRIPTION_REGION,
1107 },
1108 FilterControl {
1109 text: pagination.to_string(),
1110 is_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1111 },
1112 ],
1113 area,
1114 },
1115 );
1116}
1117
1118fn render_dead_letter_queue_tab(
1119 frame: &mut ratatui::Frame,
1120 app: &crate::App,
1121 area: ratatui::prelude::Rect,
1122) {
1123 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
1124
1125 let queue = app
1126 .sqs_state
1127 .queues
1128 .items
1129 .iter()
1130 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1131
1132 let block = Block::default()
1133 .title(" Dead-letter queue ")
1134 .borders(Borders::ALL)
1135 .border_type(BorderType::Rounded)
1136 .border_style(crate::ui::active_border());
1137
1138 let inner = block.inner(area);
1139 frame.render_widget(block, area);
1140
1141 if let Some(q) = queue {
1142 if !q.redrive_policy.is_empty() {
1143 if let Ok(policy) = serde_json::from_str::<serde_json::Value>(&q.redrive_policy) {
1145 let dlq_arn = policy
1146 .get("deadLetterTargetArn")
1147 .and_then(|v| v.as_str())
1148 .unwrap_or("-");
1149 let max_receives = policy
1150 .get("maxReceiveCount")
1151 .and_then(|v| v.as_i64())
1152 .map(|n| n.to_string())
1153 .unwrap_or_else(|| "-".to_string());
1154
1155 let lines = vec![
1156 labeled_field("Queue", dlq_arn),
1157 labeled_field("Maximum receives", &max_receives),
1158 ];
1159
1160 let paragraph = Paragraph::new(lines);
1161 frame.render_widget(paragraph, inner);
1162 } else {
1163 let paragraph = Paragraph::new("No dead-letter queue configured");
1164 frame.render_widget(paragraph, inner);
1165 }
1166 } else {
1167 let paragraph = Paragraph::new("No dead-letter queue configured");
1168 frame.render_widget(paragraph, inner);
1169 }
1170 }
1171}
1172
1173fn render_encryption_tab(
1174 frame: &mut ratatui::Frame,
1175 app: &crate::App,
1176 area: ratatui::prelude::Rect,
1177) {
1178 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
1179
1180 let queue = app
1181 .sqs_state
1182 .queues
1183 .items
1184 .iter()
1185 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1186
1187 let block = Block::default()
1188 .title(" Encryption ")
1189 .borders(Borders::ALL)
1190 .border_type(BorderType::Rounded)
1191 .border_style(crate::ui::active_border());
1192
1193 let inner = block.inner(area);
1194 frame.render_widget(block, area);
1195
1196 if let Some(q) = queue {
1197 let encryption_text = if q.encryption.is_empty() || q.encryption == "-" {
1198 "Server-side encryption is not enabled".to_string()
1199 } else {
1200 format!("Server-side encryption is managed by {}", q.encryption)
1201 };
1202
1203 let paragraph = Paragraph::new(encryption_text);
1204 frame.render_widget(paragraph, inner);
1205 }
1206}
1207
1208fn render_dlq_redrive_tasks_tab(
1209 frame: &mut ratatui::Frame,
1210 app: &crate::App,
1211 area: ratatui::prelude::Rect,
1212) {
1213 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
1214
1215 let queue = app
1216 .sqs_state
1217 .queues
1218 .items
1219 .iter()
1220 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1221
1222 let block = Block::default()
1223 .title(" Dead-letter queue redrive status ")
1224 .borders(Borders::ALL)
1225 .border_type(BorderType::Rounded)
1226 .border_style(crate::ui::active_border());
1227
1228 let inner = block.inner(area);
1229 frame.render_widget(block, area);
1230
1231 if let Some(q) = queue {
1232 let lines = vec![
1233 labeled_field("Name", &q.redrive_task_id),
1234 labeled_field("Date started", &q.redrive_task_start_time),
1235 labeled_field("Percent processed", &q.redrive_task_percent),
1236 labeled_field("Status", &q.redrive_task_status),
1237 labeled_field("Redrive destination", &q.redrive_task_destination),
1238 ];
1239
1240 let paragraph = Paragraph::new(lines);
1241 frame.render_widget(paragraph, inner);
1242 }
1243}
1244
1245fn render_queue_list(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
1246 use crate::common::SortDirection;
1247 use crate::keymap::Mode;
1248 use ratatui::prelude::*;
1249 use ratatui::widgets::Clear;
1250
1251 frame.render_widget(Clear, area);
1252
1253 let chunks = Layout::default()
1254 .direction(Direction::Vertical)
1255 .constraints([
1256 Constraint::Length(3), Constraint::Min(0), ])
1259 .split(area);
1260
1261 let filtered_count =
1262 filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter).len();
1263 let page_size = app.sqs_state.queues.page_size.value();
1264 let total_pages = filtered_count.div_ceil(page_size);
1265 let current_page = app.sqs_state.queues.selected / page_size;
1266 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1267
1268 render_simple_filter(
1269 frame,
1270 chunks[0],
1271 SimpleFilterConfig {
1272 filter_text: &app.sqs_state.queues.filter,
1273 placeholder: "Search by queue name prefix",
1274 pagination: &pagination,
1275 mode: app.mode,
1276 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1277 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1278 },
1279 );
1280
1281 let filtered: Vec<_> =
1282 filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter);
1283
1284 let start_idx = current_page * page_size;
1285 let end_idx = (start_idx + page_size).min(filtered.len());
1286 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1287
1288 let title = format!(" Queues ({}) ", filtered.len());
1289
1290 let columns: Vec<Box<dyn crate::ui::table::Column<Queue>>> = app
1291 .sqs_visible_column_ids
1292 .iter()
1293 .filter_map(|col_id| {
1294 SqsColumn::from_id(col_id)
1295 .map(|col| Box::new(col) as Box<dyn crate::ui::table::Column<Queue>>)
1296 })
1297 .collect();
1298
1299 let expanded_index = app.sqs_state.queues.expanded_item.and_then(|idx| {
1300 if idx >= start_idx && idx < end_idx {
1301 Some(idx - start_idx)
1302 } else {
1303 None
1304 }
1305 });
1306
1307 let config = crate::ui::table::TableConfig {
1308 items: paginated,
1309 selected_index: app.sqs_state.queues.selected % page_size,
1310 expanded_index,
1311 columns: &columns,
1312 sort_column: "Name",
1313 sort_direction: SortDirection::Asc,
1314 title,
1315 area: chunks[1],
1316 get_expanded_content: Some(Box::new(|queue: &Queue| {
1317 crate::ui::table::expanded_from_columns(&columns, queue)
1318 })),
1319 is_active: app.mode != Mode::FilterInput,
1320 };
1321
1322 crate::ui::table::render_table(frame, config);
1323}
1324
1325#[cfg(test)]
1326mod tests {
1327 use super::*;
1328 use crate::common::CyclicEnum;
1329
1330 #[test]
1331 fn test_sqs_state_initialization() {
1332 let state = State::new();
1333 assert_eq!(state.queues.items.len(), 0);
1334 assert_eq!(state.queues.selected, 0);
1335 assert_eq!(state.queues.filter, "");
1336 assert_eq!(state.queues.page_size.value(), 50);
1337 assert_eq!(state.input_focus, InputFocus::Filter);
1338 }
1339
1340 #[test]
1341 fn test_filtered_queues_empty_filter() {
1342 let queues = vec![
1343 Queue {
1344 name: "queue1".to_string(),
1345 url: String::new(),
1346 queue_type: "Standard".to_string(),
1347 created_timestamp: String::new(),
1348 messages_available: "0".to_string(),
1349 messages_in_flight: "0".to_string(),
1350 encryption: "Disabled".to_string(),
1351 content_based_deduplication: "Disabled".to_string(),
1352 last_modified_timestamp: String::new(),
1353 visibility_timeout: String::new(),
1354 message_retention_period: String::new(),
1355 maximum_message_size: String::new(),
1356 delivery_delay: String::new(),
1357 receive_message_wait_time: String::new(),
1358 high_throughput_fifo: "N/A".to_string(),
1359 deduplication_scope: "N/A".to_string(),
1360 fifo_throughput_limit: "N/A".to_string(),
1361 dead_letter_queue: "-".to_string(),
1362 messages_delayed: "0".to_string(),
1363 redrive_allow_policy: "-".to_string(),
1364 redrive_policy: "".to_string(),
1365 redrive_task_id: "-".to_string(),
1366 redrive_task_start_time: "-".to_string(),
1367 redrive_task_status: "-".to_string(),
1368 redrive_task_percent: "-".to_string(),
1369 redrive_task_destination: "-".to_string(),
1370 },
1371 Queue {
1372 name: "queue2".to_string(),
1373 url: String::new(),
1374 queue_type: "Standard".to_string(),
1375 created_timestamp: String::new(),
1376 messages_available: "0".to_string(),
1377 messages_in_flight: "0".to_string(),
1378 encryption: "Disabled".to_string(),
1379 content_based_deduplication: "Disabled".to_string(),
1380 last_modified_timestamp: String::new(),
1381 visibility_timeout: String::new(),
1382 message_retention_period: String::new(),
1383 maximum_message_size: String::new(),
1384 delivery_delay: String::new(),
1385 receive_message_wait_time: String::new(),
1386 high_throughput_fifo: "N/A".to_string(),
1387 deduplication_scope: "N/A".to_string(),
1388 fifo_throughput_limit: "N/A".to_string(),
1389 dead_letter_queue: "-".to_string(),
1390 messages_delayed: "0".to_string(),
1391 redrive_allow_policy: "-".to_string(),
1392 redrive_policy: "".to_string(),
1393 redrive_task_id: "-".to_string(),
1394 redrive_task_start_time: "-".to_string(),
1395 redrive_task_status: "-".to_string(),
1396 redrive_task_percent: "-".to_string(),
1397 redrive_task_destination: "-".to_string(),
1398 },
1399 ];
1400
1401 let filtered = filtered_queues(&queues, "");
1402 assert_eq!(filtered.len(), 2);
1403 }
1404
1405 #[test]
1406 fn test_filtered_queues_with_prefix() {
1407 let queues = vec![
1408 Queue {
1409 name: "prod-orders".to_string(),
1410 url: String::new(),
1411 queue_type: "Standard".to_string(),
1412 created_timestamp: String::new(),
1413 messages_available: "0".to_string(),
1414 messages_in_flight: "0".to_string(),
1415 encryption: "Disabled".to_string(),
1416 content_based_deduplication: "Disabled".to_string(),
1417 last_modified_timestamp: String::new(),
1418 visibility_timeout: String::new(),
1419 message_retention_period: String::new(),
1420 maximum_message_size: String::new(),
1421 delivery_delay: String::new(),
1422 receive_message_wait_time: String::new(),
1423 high_throughput_fifo: "N/A".to_string(),
1424 deduplication_scope: "N/A".to_string(),
1425 fifo_throughput_limit: "N/A".to_string(),
1426 dead_letter_queue: "-".to_string(),
1427 messages_delayed: "0".to_string(),
1428 redrive_allow_policy: "-".to_string(),
1429 redrive_policy: "".to_string(),
1430 redrive_task_id: "-".to_string(),
1431 redrive_task_start_time: "-".to_string(),
1432 redrive_task_status: "-".to_string(),
1433 redrive_task_percent: "-".to_string(),
1434 redrive_task_destination: "-".to_string(),
1435 },
1436 Queue {
1437 name: "dev-orders".to_string(),
1438 url: String::new(),
1439 queue_type: "Standard".to_string(),
1440 created_timestamp: String::new(),
1441 messages_available: "0".to_string(),
1442 messages_in_flight: "0".to_string(),
1443 encryption: "Disabled".to_string(),
1444 content_based_deduplication: "Disabled".to_string(),
1445 last_modified_timestamp: String::new(),
1446 visibility_timeout: String::new(),
1447 message_retention_period: String::new(),
1448 maximum_message_size: String::new(),
1449 delivery_delay: String::new(),
1450 receive_message_wait_time: String::new(),
1451 high_throughput_fifo: "N/A".to_string(),
1452 deduplication_scope: "N/A".to_string(),
1453 fifo_throughput_limit: "N/A".to_string(),
1454 dead_letter_queue: "-".to_string(),
1455 messages_delayed: "0".to_string(),
1456 redrive_allow_policy: "-".to_string(),
1457 redrive_policy: "".to_string(),
1458 redrive_task_id: "-".to_string(),
1459 redrive_task_start_time: "-".to_string(),
1460 redrive_task_status: "-".to_string(),
1461 redrive_task_percent: "-".to_string(),
1462 redrive_task_destination: "-".to_string(),
1463 },
1464 ];
1465
1466 let filtered = filtered_queues(&queues, "prod");
1467 assert_eq!(filtered.len(), 1);
1468 assert_eq!(filtered[0].name, "prod-orders");
1469 }
1470
1471 #[test]
1472 fn test_filtered_queues_case_insensitive() {
1473 let queues = vec![Queue {
1474 name: "MyQueue".to_string(),
1475 url: String::new(),
1476 queue_type: "Standard".to_string(),
1477 created_timestamp: String::new(),
1478 messages_available: "0".to_string(),
1479 messages_in_flight: "0".to_string(),
1480 encryption: "Disabled".to_string(),
1481 content_based_deduplication: "Disabled".to_string(),
1482 last_modified_timestamp: String::new(),
1483 visibility_timeout: String::new(),
1484 message_retention_period: String::new(),
1485 maximum_message_size: String::new(),
1486 delivery_delay: String::new(),
1487 receive_message_wait_time: String::new(),
1488 high_throughput_fifo: "N/A".to_string(),
1489 deduplication_scope: "N/A".to_string(),
1490 fifo_throughput_limit: "N/A".to_string(),
1491 dead_letter_queue: "-".to_string(),
1492 messages_delayed: "0".to_string(),
1493 redrive_allow_policy: "-".to_string(),
1494 redrive_policy: "".to_string(),
1495 redrive_task_id: "-".to_string(),
1496 redrive_task_start_time: "-".to_string(),
1497 redrive_task_status: "-".to_string(),
1498 redrive_task_percent: "-".to_string(),
1499 redrive_task_destination: "-".to_string(),
1500 }];
1501
1502 let filtered = filtered_queues(&queues, "my");
1503 assert_eq!(filtered.len(), 1);
1504
1505 let filtered = filtered_queues(&queues, "MY");
1506 assert_eq!(filtered.len(), 1);
1507 }
1508
1509 #[test]
1510 fn test_pagination_page_size() {
1511 let state = State::new();
1512 assert_eq!(state.queues.page_size.value(), 50);
1513 }
1514
1515 #[test]
1516 fn test_state_initialization_with_policy() {
1517 let state = State::new();
1518 assert_eq!(state.policy_scroll, 0);
1519 assert_eq!(state.current_queue, None);
1520 assert!(state.policy_document.contains("Version"));
1521 assert!(state.policy_document.contains("2012-10-17"));
1522 }
1523
1524 #[test]
1525 fn test_extract_region() {
1526 let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1527 assert_eq!(extract_region(url), "us-east-1");
1528
1529 let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1530 assert_eq!(extract_region(url2), "eu-west-2");
1531 }
1532
1533 #[test]
1534 fn test_extract_account_id() {
1535 let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1536 assert_eq!(extract_account_id(url), "123456789012");
1537
1538 let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1539 assert_eq!(extract_account_id(url2), "987654321098");
1540 }
1541
1542 #[test]
1543 fn test_timestamp_column_width() {
1544 use crate::sqs::queue::Column;
1545 use crate::ui::table::Column as TableColumn;
1546 assert!(Column::Created.width() >= 27);
1548 assert!(Column::LastUpdated.width() >= 27);
1549 }
1550
1551 #[test]
1552 fn test_message_retention_period_formatting() {
1553 let seconds = 345600;
1555 let formatted = crate::common::format_duration_seconds(seconds);
1556 assert_eq!(formatted, "4d");
1558 }
1559
1560 #[test]
1561 fn test_queue_detail_tab_navigation() {
1562 let tab = QueueDetailTab::QueuePolicies;
1563 assert_eq!(tab.next(), QueueDetailTab::Monitoring);
1564 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1565
1566 let tab = QueueDetailTab::Monitoring;
1567 assert_eq!(tab.next(), QueueDetailTab::SnsSubscriptions);
1568 assert_eq!(tab.prev(), QueueDetailTab::QueuePolicies);
1569
1570 let tab = QueueDetailTab::SnsSubscriptions;
1571 assert_eq!(tab.next(), QueueDetailTab::LambdaTriggers);
1572 assert_eq!(tab.prev(), QueueDetailTab::Monitoring);
1573
1574 let tab = QueueDetailTab::LambdaTriggers;
1575 assert_eq!(tab.next(), QueueDetailTab::EventBridgePipes);
1576 assert_eq!(tab.prev(), QueueDetailTab::SnsSubscriptions);
1577
1578 let tab = QueueDetailTab::EventBridgePipes;
1579 assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueue);
1580 assert_eq!(tab.prev(), QueueDetailTab::LambdaTriggers);
1581
1582 let tab = QueueDetailTab::DeadLetterQueue;
1583 assert_eq!(tab.next(), QueueDetailTab::Tagging);
1584 assert_eq!(tab.prev(), QueueDetailTab::EventBridgePipes);
1585
1586 let tab = QueueDetailTab::Tagging;
1587 assert_eq!(tab.next(), QueueDetailTab::Encryption);
1588 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueue);
1589
1590 let tab = QueueDetailTab::Encryption;
1591 assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1592 assert_eq!(tab.prev(), QueueDetailTab::Tagging);
1593
1594 let tab = QueueDetailTab::DeadLetterQueueRedriveTasks;
1595 assert_eq!(tab.next(), QueueDetailTab::QueuePolicies);
1596 assert_eq!(tab.prev(), QueueDetailTab::Encryption);
1597
1598 let tab = QueueDetailTab::QueuePolicies;
1599 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1600 }
1601
1602 #[test]
1603 fn test_queue_detail_tab_all() {
1604 let tabs = QueueDetailTab::all();
1605 assert_eq!(tabs.len(), 9);
1606 assert_eq!(tabs[0], QueueDetailTab::QueuePolicies);
1607 assert_eq!(tabs[1], QueueDetailTab::Monitoring);
1608 assert_eq!(tabs[2], QueueDetailTab::SnsSubscriptions);
1609 assert_eq!(tabs[3], QueueDetailTab::LambdaTriggers);
1610 assert_eq!(tabs[4], QueueDetailTab::EventBridgePipes);
1611 assert_eq!(tabs[5], QueueDetailTab::DeadLetterQueue);
1612 assert_eq!(tabs[6], QueueDetailTab::Tagging);
1613 assert_eq!(tabs[7], QueueDetailTab::Encryption);
1614 assert_eq!(tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
1615 }
1616
1617 #[test]
1618 fn test_queue_detail_tab_names() {
1619 assert_eq!(QueueDetailTab::QueuePolicies.name(), "Queue policies");
1620 assert_eq!(QueueDetailTab::SnsSubscriptions.name(), "SNS subscriptions");
1621 assert_eq!(QueueDetailTab::LambdaTriggers.name(), "Lambda triggers");
1622 assert_eq!(QueueDetailTab::EventBridgePipes.name(), "EventBridge Pipes");
1623 assert_eq!(QueueDetailTab::Tagging.name(), "Tagging");
1624 assert_eq!(QueueDetailTab::DeadLetterQueue.name(), "Dead-letter queue");
1625 }
1626
1627 #[test]
1628 fn test_trigger_column_all() {
1629 use crate::sqs::trigger::Column as TriggerColumn;
1630 assert_eq!(TriggerColumn::all().len(), 4);
1631 }
1632
1633 #[test]
1634 fn test_trigger_column_ids() {
1635 use crate::sqs::trigger::Column as TriggerColumn;
1636 let ids = TriggerColumn::ids();
1637 assert_eq!(ids.len(), 4);
1638 assert!(ids.contains(&"column.sqs.trigger.uuid"));
1639 assert!(ids.contains(&"column.sqs.trigger.arn"));
1640 assert!(ids.contains(&"column.sqs.trigger.status"));
1641 assert!(ids.contains(&"column.sqs.trigger.last_modified"));
1642 }
1643
1644 #[test]
1645 fn test_trigger_column_from_id() {
1646 use crate::sqs::trigger::Column as TriggerColumn;
1647 assert_eq!(
1648 TriggerColumn::from_id("column.sqs.trigger.uuid"),
1649 Some(TriggerColumn::Uuid)
1650 );
1651 assert_eq!(
1652 TriggerColumn::from_id("column.sqs.trigger.arn"),
1653 Some(TriggerColumn::Arn)
1654 );
1655 assert_eq!(
1656 TriggerColumn::from_id("column.sqs.trigger.status"),
1657 Some(TriggerColumn::Status)
1658 );
1659 assert_eq!(
1660 TriggerColumn::from_id("column.sqs.trigger.last_modified"),
1661 Some(TriggerColumn::LastModified)
1662 );
1663 assert_eq!(TriggerColumn::from_id("invalid"), None);
1664 }
1665
1666 #[test]
1667 fn test_trigger_status_rendering() {
1668 use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
1669 use crate::ui::table::Column;
1670
1671 let trigger = LambdaTrigger {
1672 uuid: "test-uuid".to_string(),
1673 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1674 status: "Enabled".to_string(),
1675 last_modified: "1609459200".to_string(),
1676 };
1677
1678 let (text, style) = TriggerColumn::Status.render(&trigger);
1679 assert_eq!(text, "✅ Enabled");
1680 assert_eq!(style.fg, Some(ratatui::style::Color::Green));
1681 }
1682
1683 #[test]
1684 fn test_trigger_timestamp_rendering() {
1685 use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
1686 use crate::ui::table::Column;
1687
1688 let trigger = LambdaTrigger {
1689 uuid: "test-uuid".to_string(),
1690 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1691 status: "Enabled".to_string(),
1692 last_modified: "1609459200".to_string(),
1693 };
1694
1695 let (text, _) = TriggerColumn::LastModified.render(&trigger);
1696 assert!(text.contains("2021-01-01"));
1697 assert!(text.contains("(UTC)"));
1698 }
1699
1700 #[test]
1701 fn test_state_initializes_trigger_columns() {
1702 let state = State::new();
1703 assert_eq!(state.trigger_column_ids.len(), 4);
1704 assert_eq!(state.trigger_visible_column_ids.len(), 4);
1705 assert_eq!(state.trigger_column_ids, state.trigger_visible_column_ids);
1706 }
1707
1708 #[test]
1709 fn test_trigger_state_has_filter() {
1710 let mut state = State::new();
1711 state.detail_tab = QueueDetailTab::LambdaTriggers;
1712 state.triggers.filter = "test-filter".to_string();
1713
1714 assert_eq!(state.triggers.filter, "test-filter");
1716 assert_eq!(state.detail_tab, QueueDetailTab::LambdaTriggers);
1717 }
1718
1719 #[test]
1720 fn test_trigger_filtering() {
1721 use crate::sqs::trigger::LambdaTrigger;
1722
1723 let triggers = [
1724 LambdaTrigger {
1725 uuid: "uuid-123".to_string(),
1726 arn: "arn:aws:lambda:us-east-1:123:function:test1".to_string(),
1727 status: "Enabled".to_string(),
1728 last_modified: "1609459200".to_string(),
1729 },
1730 LambdaTrigger {
1731 uuid: "uuid-456".to_string(),
1732 arn: "arn:aws:lambda:us-east-1:123:function:test2".to_string(),
1733 status: "Enabled".to_string(),
1734 last_modified: "1609459200".to_string(),
1735 },
1736 ];
1737
1738 let filtered: Vec<_> = triggers.iter().filter(|t| t.uuid.contains("123")).collect();
1740 assert_eq!(filtered.len(), 1);
1741 assert_eq!(filtered[0].uuid, "uuid-123");
1742
1743 let filtered: Vec<_> = triggers
1745 .iter()
1746 .filter(|t| t.arn.contains("test2"))
1747 .collect();
1748 assert_eq!(filtered.len(), 1);
1749 assert_eq!(
1750 filtered[0].arn,
1751 "arn:aws:lambda:us-east-1:123:function:test2"
1752 );
1753 }
1754
1755 #[test]
1756 fn test_trigger_pagination() {
1757 let mut state = State::new();
1758 state.triggers.items = (0..10)
1759 .map(|i| crate::sqs::LambdaTrigger {
1760 uuid: format!("uuid-{}", i),
1761 arn: format!("arn:aws:lambda:us-east-1:123:function:test{}", i),
1762 status: "Enabled".to_string(),
1763 last_modified: "1609459200".to_string(),
1764 })
1765 .collect();
1766
1767 assert_eq!(state.triggers.items.len(), 10);
1768 assert_eq!(state.triggers.page_size.value(), 50); }
1770
1771 #[test]
1772 fn test_trigger_column_visibility() {
1773 let mut state = State::new();
1774
1775 assert_eq!(state.trigger_visible_column_ids.len(), 4);
1777
1778 state.trigger_visible_column_ids.remove(0);
1780 assert_eq!(state.trigger_visible_column_ids.len(), 3);
1781
1782 state
1784 .trigger_visible_column_ids
1785 .push(state.trigger_column_ids[0].clone());
1786 assert_eq!(state.trigger_visible_column_ids.len(), 4);
1787 }
1788
1789 #[test]
1790 fn test_trigger_page_size_options() {
1791 use crate::common::PageSize;
1792 let mut state = State::new();
1793
1794 assert_eq!(state.triggers.page_size, PageSize::Fifty);
1796
1797 state.triggers.page_size = PageSize::Ten;
1799 assert_eq!(state.triggers.page_size.value(), 10);
1800
1801 state.triggers.page_size = PageSize::TwentyFive;
1802 assert_eq!(state.triggers.page_size.value(), 25);
1803
1804 state.triggers.page_size = PageSize::OneHundred;
1805 assert_eq!(state.triggers.page_size.value(), 100);
1806 }
1807
1808 #[test]
1809 fn test_trigger_loading_state() {
1810 let mut state = State::new();
1811
1812 assert!(!state.triggers.loading);
1814
1815 state.triggers.loading = true;
1817 assert!(state.triggers.loading);
1818
1819 state.triggers.loading = false;
1821 assert!(!state.triggers.loading);
1822 }
1823
1824 #[test]
1825 fn test_trigger_sort_by_last_modified() {
1826 let mut triggers = [
1827 crate::sqs::LambdaTrigger {
1828 uuid: "uuid-2".to_string(),
1829 arn: "arn2".to_string(),
1830 status: "Enabled".to_string(),
1831 last_modified: "1609459300".to_string(), },
1833 crate::sqs::LambdaTrigger {
1834 uuid: "uuid-1".to_string(),
1835 arn: "arn1".to_string(),
1836 status: "Enabled".to_string(),
1837 last_modified: "1609459200".to_string(), },
1839 ];
1840
1841 triggers.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
1843
1844 assert_eq!(triggers[0].uuid, "uuid-1");
1845 assert_eq!(triggers[1].uuid, "uuid-2");
1846 }
1847
1848 #[test]
1849 fn test_trigger_pagination_calculation() {
1850 use crate::common::PageSize;
1851 let mut state = State::new();
1852
1853 state.triggers.items = (0..25)
1855 .map(|i| crate::sqs::LambdaTrigger {
1856 uuid: format!("uuid-{}", i),
1857 arn: format!("arn{}", i),
1858 status: "Enabled".to_string(),
1859 last_modified: "1609459200".to_string(),
1860 })
1861 .collect();
1862
1863 state.triggers.page_size = PageSize::Ten;
1865 let page_size = state.triggers.page_size.value();
1866 let total_pages = state.triggers.items.len().div_ceil(page_size);
1867 assert_eq!(total_pages, 3);
1868
1869 let current_page = 0;
1871 let start_idx = current_page * page_size;
1872 let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1873 assert_eq!(start_idx, 0);
1874 assert_eq!(end_idx, 10);
1875
1876 let current_page = 2;
1878 let start_idx = current_page * page_size;
1879 let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1880 assert_eq!(start_idx, 20);
1881 assert_eq!(end_idx, 25);
1882 }
1883
1884 #[test]
1885 fn test_monitoring_metric_data_with_values() {
1886 let mut state = State::new();
1887 state.metric_data = vec![
1889 (1700000000, 0.0),
1890 (1700000060, 5.0),
1891 (1700000120, 10.0),
1892 (1700000180, 0.0),
1893 ];
1894 assert_eq!(state.metric_data.len(), 4);
1895 assert_eq!(state.metric_data[0], (1700000000, 0.0));
1896 assert_eq!(state.metric_data[1], (1700000060, 5.0));
1897 }
1898
1899 #[test]
1900 fn test_monitoring_all_metrics_initialized() {
1901 let state = State::new();
1902 assert!(state.metric_data.is_empty());
1903 assert!(state.metric_data_delayed.is_empty());
1904 assert!(state.metric_data_not_visible.is_empty());
1905 assert!(state.metric_data_visible.is_empty());
1906 assert!(state.metric_data_empty_receives.is_empty());
1907 assert!(state.metric_data_messages_deleted.is_empty());
1908 assert!(state.metric_data_messages_received.is_empty());
1909 assert!(state.metric_data_messages_sent.is_empty());
1910 assert!(state.metric_data_sent_message_size.is_empty());
1911 assert_eq!(state.monitoring_scroll, 0);
1912 }
1913
1914 #[test]
1915 fn test_monitoring_scroll_pages() {
1916 let mut state = State::new();
1917 assert_eq!(state.monitoring_scroll, 0);
1918
1919 state.monitoring_scroll = 1;
1921 assert_eq!(state.monitoring_scroll, 1);
1922
1923 state.monitoring_scroll = 2;
1925 assert_eq!(state.monitoring_scroll, 2);
1926 }
1927
1928 #[test]
1929 fn test_monitoring_delayed_metrics() {
1930 let mut state = State::new();
1931 state.metric_data_delayed = vec![(1700000000, 1.0), (1700000060, 2.0)];
1932 assert_eq!(state.metric_data_delayed.len(), 2);
1933 assert_eq!(state.metric_data_delayed[0].1, 1.0);
1934 }
1935
1936 #[test]
1937 fn test_monitoring_not_visible_metrics() {
1938 let mut state = State::new();
1939 state.metric_data_not_visible = vec![(1700000000, 3.0), (1700000060, 4.0)];
1940 assert_eq!(state.metric_data_not_visible.len(), 2);
1941 assert_eq!(state.metric_data_not_visible[1].1, 4.0);
1942 }
1943
1944 #[test]
1945 fn test_monitoring_visible_metrics() {
1946 let mut state = State::new();
1947 state.metric_data_visible = vec![(1700000000, 5.0), (1700000060, 6.0)];
1948 assert_eq!(state.metric_data_visible.len(), 2);
1949 assert_eq!(state.metric_data_visible[0].1, 5.0);
1950 }
1951
1952 #[test]
1953 fn test_monitoring_empty_receives_metrics() {
1954 let mut state = State::new();
1955 state.metric_data_empty_receives = vec![(1700000000, 10.0), (1700000060, 15.0)];
1956 assert_eq!(state.metric_data_empty_receives.len(), 2);
1957 assert_eq!(state.metric_data_empty_receives[0].1, 10.0);
1958 }
1959
1960 #[test]
1961 fn test_monitoring_messages_deleted_metrics() {
1962 let mut state = State::new();
1963 state.metric_data_messages_deleted = vec![(1700000000, 20.0), (1700000060, 25.0)];
1964 assert_eq!(state.metric_data_messages_deleted.len(), 2);
1965 assert_eq!(state.metric_data_messages_deleted[0].1, 20.0);
1966 }
1967
1968 #[test]
1969 fn test_monitoring_messages_received_metrics() {
1970 let mut state = State::new();
1971 state.metric_data_messages_received = vec![(1700000000, 30.0), (1700000060, 35.0)];
1972 assert_eq!(state.metric_data_messages_received.len(), 2);
1973 assert_eq!(state.metric_data_messages_received[0].1, 30.0);
1974 }
1975
1976 #[test]
1977 fn test_monitoring_messages_sent_metrics() {
1978 let mut state = State::new();
1979 state.metric_data_messages_sent = vec![(1700000000, 40.0), (1700000060, 45.0)];
1980 assert_eq!(state.metric_data_messages_sent.len(), 2);
1981 assert_eq!(state.metric_data_messages_sent[0].1, 40.0);
1982 }
1983
1984 #[test]
1985 fn test_monitoring_sent_message_size_metrics() {
1986 let mut state = State::new();
1987 state.metric_data_sent_message_size = vec![(1700000000, 1024.0), (1700000060, 2048.0)];
1988 assert_eq!(state.metric_data_sent_message_size.len(), 2);
1989 assert_eq!(state.metric_data_sent_message_size[0].1, 1024.0);
1990 }
1991
1992 #[test]
1993 fn test_trigger_expand_collapse() {
1994 let mut state = State::new();
1995
1996 assert_eq!(state.triggers.expanded_item, None);
1998
1999 state.triggers.expanded_item = Some(0);
2001 assert_eq!(state.triggers.expanded_item, Some(0));
2002
2003 state.triggers.expanded_item = None;
2005 assert_eq!(state.triggers.expanded_item, None);
2006 }
2007
2008 #[test]
2009 fn test_trigger_filter_visibility() {
2010 let mut state = State::new();
2011
2012 assert!(state.triggers.filter.is_empty());
2014
2015 state.triggers.filter = "test".to_string();
2017 assert_eq!(state.triggers.filter, "test");
2018
2019 state.triggers.filter.clear();
2021 assert!(state.triggers.filter.is_empty());
2022 }
2023
2024 #[test]
2025 fn test_pipe_column_ids_have_correct_prefix() {
2026 for col in PipeColumn::all() {
2027 assert!(
2028 col.id().starts_with("column.sqs.pipe."),
2029 "PipeColumn ID '{}' should start with 'column.sqs.pipe.'",
2030 col.id()
2031 );
2032 }
2033 }
2034
2035 #[test]
2036 fn test_tags_sorted_by_value() {
2037 let mut state = State::new();
2038 state.tags.items = vec![
2039 QueueTag {
2040 key: "env".to_string(),
2041 value: "prod".to_string(),
2042 },
2043 QueueTag {
2044 key: "team".to_string(),
2045 value: "backend".to_string(),
2046 },
2047 QueueTag {
2048 key: "app".to_string(),
2049 value: "api".to_string(),
2050 },
2051 ];
2052
2053 let mut sorted = state.tags.items.clone();
2054 sorted.sort_by(|a, b| a.value.cmp(&b.value));
2055
2056 assert_eq!(sorted[0].value, "api");
2057 assert_eq!(sorted[1].value, "backend");
2058 assert_eq!(sorted[2].value, "prod");
2059 }
2060
2061 #[test]
2062 fn test_tags_initialization() {
2063 let state = State::new();
2064 assert_eq!(state.tags.items.len(), 0);
2065 assert_eq!(state.tag_column_ids.len(), 2);
2066 assert_eq!(state.tag_visible_column_ids.len(), 2);
2067 }
2068
2069 #[test]
2070 fn test_queue_tag_structure() {
2071 let tag = QueueTag {
2072 key: "Environment".to_string(),
2073 value: "Production".to_string(),
2074 };
2075 assert_eq!(tag.key, "Environment");
2076 assert_eq!(tag.value, "Production");
2077 }
2078
2079 #[test]
2080 fn test_tags_table_state() {
2081 let mut state = State::new();
2082 state.tags.items = vec![
2083 QueueTag {
2084 key: "Env".to_string(),
2085 value: "prod".to_string(),
2086 },
2087 QueueTag {
2088 key: "Team".to_string(),
2089 value: "backend".to_string(),
2090 },
2091 ];
2092 assert_eq!(state.tags.items.len(), 2);
2093 assert_eq!(state.tags.selected, 0);
2094 assert_eq!(state.tags.filter, "");
2095 }
2096
2097 #[test]
2098 fn test_tags_filtering() {
2099 let tags = [
2100 QueueTag {
2101 key: "Environment".to_string(),
2102 value: "production".to_string(),
2103 },
2104 QueueTag {
2105 key: "Team".to_string(),
2106 value: "backend".to_string(),
2107 },
2108 QueueTag {
2109 key: "Project".to_string(),
2110 value: "api".to_string(),
2111 },
2112 ];
2113
2114 let filtered: Vec<_> = tags
2116 .iter()
2117 .filter(|t| t.key.to_lowercase().contains("env"))
2118 .collect();
2119 assert_eq!(filtered.len(), 1);
2120 assert_eq!(filtered[0].key, "Environment");
2121
2122 let filtered: Vec<_> = tags
2124 .iter()
2125 .filter(|t| t.value.to_lowercase().contains("back"))
2126 .collect();
2127 assert_eq!(filtered.len(), 1);
2128 assert_eq!(filtered[0].value, "backend");
2129 }
2130
2131 #[test]
2132 fn test_tags_column_ids() {
2133 use crate::sqs::tag::Column as TagColumn;
2134 let ids = TagColumn::ids();
2135 assert_eq!(ids.len(), 2);
2136 assert_eq!(ids[0], "column.sqs.tag.key");
2137 assert_eq!(ids[1], "column.sqs.tag.value");
2138 }
2139
2140 #[test]
2141 fn test_tags_column_from_id() {
2142 use crate::sqs::tag::Column as TagColumn;
2143 assert!(TagColumn::from_id("column.sqs.tag.key").is_some());
2144 assert!(TagColumn::from_id("column.sqs.tag.value").is_some());
2145 assert!(TagColumn::from_id("invalid").is_none());
2146 }
2147
2148 #[test]
2149 fn test_subscriptions_initialization() {
2150 let state = State::new();
2151 assert_eq!(state.subscriptions.items.len(), 0);
2152 assert_eq!(state.subscription_column_ids.len(), 2);
2153 assert_eq!(state.subscription_visible_column_ids.len(), 2);
2154 assert_eq!(state.subscription_region_filter, "");
2155 }
2156
2157 #[test]
2158 fn test_subscription_column_ids() {
2159 use crate::sqs::sub::Column as SubscriptionColumn;
2160 let ids = SubscriptionColumn::ids();
2161 assert_eq!(ids.len(), 2);
2162 assert_eq!(ids[0], "column.sqs.subscription.subscription_arn");
2163 assert_eq!(ids[1], "column.sqs.subscription.topic_arn");
2164 }
2165
2166 #[test]
2167 fn test_subscription_column_from_id() {
2168 use crate::sqs::sub::Column as SubscriptionColumn;
2169 assert!(SubscriptionColumn::from_id("column.sqs.subscription.subscription_arn").is_some());
2170 assert!(SubscriptionColumn::from_id("column.sqs.subscription.topic_arn").is_some());
2171 assert!(SubscriptionColumn::from_id("invalid").is_none());
2172 }
2173
2174 #[test]
2175 fn test_subscription_region_filter_default() {
2176 let state = State::new();
2177 assert_eq!(state.subscription_region_filter, "");
2179 }
2180
2181 #[test]
2182 fn test_subscription_region_filter_display() {
2183 let mut state = State::new();
2184
2185 assert_eq!(state.subscription_region_filter, "");
2187
2188 state.subscription_region_filter = "us-west-2".to_string();
2190 assert_eq!(state.subscription_region_filter, "us-west-2");
2191 }
2192
2193 #[test]
2194 fn test_subscription_region_selected_index() {
2195 let state = State::new();
2196 assert_eq!(state.subscription_region_selected, 0);
2197 }
2198
2199 #[test]
2200 fn test_encryption_tab_in_all() {
2201 let tabs = QueueDetailTab::all();
2202 assert!(tabs.contains(&QueueDetailTab::Encryption));
2203 }
2204
2205 #[test]
2206 fn test_encryption_tab_name() {
2207 assert_eq!(QueueDetailTab::Encryption.name(), "Encryption");
2208 }
2209
2210 #[test]
2211 fn test_encryption_tab_order() {
2212 let tabs = QueueDetailTab::all();
2213 let dlq_idx = tabs
2214 .iter()
2215 .position(|t| *t == QueueDetailTab::DeadLetterQueue)
2216 .unwrap();
2217 let tagging_idx = tabs
2218 .iter()
2219 .position(|t| *t == QueueDetailTab::Tagging)
2220 .unwrap();
2221 let encryption_idx = tabs
2222 .iter()
2223 .position(|t| *t == QueueDetailTab::Encryption)
2224 .unwrap();
2225
2226 assert!(dlq_idx < tagging_idx);
2228 assert!(encryption_idx > tagging_idx);
2229 }
2230
2231 #[test]
2232 fn test_dlq_redrive_tasks_tab_in_all() {
2233 let tabs = QueueDetailTab::all();
2234 assert!(tabs.contains(&QueueDetailTab::DeadLetterQueueRedriveTasks));
2235 }
2236
2237 #[test]
2238 fn test_dlq_redrive_tasks_tab_name() {
2239 assert_eq!(
2240 QueueDetailTab::DeadLetterQueueRedriveTasks.name(),
2241 "Dead-letter queue redrive tasks"
2242 );
2243 }
2244
2245 #[test]
2246 fn test_dlq_redrive_tasks_tab_order() {
2247 let tabs = QueueDetailTab::all();
2248 let encryption_idx = tabs
2249 .iter()
2250 .position(|t| *t == QueueDetailTab::Encryption)
2251 .unwrap();
2252 let redrive_idx = tabs
2253 .iter()
2254 .position(|t| *t == QueueDetailTab::DeadLetterQueueRedriveTasks)
2255 .unwrap();
2256
2257 assert!(redrive_idx > encryption_idx);
2259 assert_eq!(redrive_idx, tabs.len() - 1);
2260 }
2261
2262 #[test]
2263 fn test_tab_strip_matches_enum_order() {
2264 let all_tabs = QueueDetailTab::all();
2266 assert_eq!(all_tabs.len(), 9);
2267
2268 assert_eq!(all_tabs[0], QueueDetailTab::QueuePolicies);
2270 assert_eq!(all_tabs[1], QueueDetailTab::Monitoring);
2271 assert_eq!(all_tabs[2], QueueDetailTab::SnsSubscriptions);
2272 assert_eq!(all_tabs[3], QueueDetailTab::LambdaTriggers);
2273 assert_eq!(all_tabs[4], QueueDetailTab::EventBridgePipes);
2274 assert_eq!(all_tabs[5], QueueDetailTab::DeadLetterQueue);
2275 assert_eq!(all_tabs[6], QueueDetailTab::Tagging);
2276 assert_eq!(all_tabs[7], QueueDetailTab::Encryption);
2277 assert_eq!(all_tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
2278 }
2279
2280 #[test]
2281 fn test_monitoring_tab_in_all() {
2282 let all_tabs = QueueDetailTab::all();
2283 assert!(all_tabs.contains(&QueueDetailTab::Monitoring));
2284 }
2285
2286 #[test]
2287 fn test_monitoring_tab_name() {
2288 assert_eq!(QueueDetailTab::Monitoring.name(), "Monitoring");
2289 }
2290
2291 #[test]
2292 fn test_monitoring_tab_order() {
2293 let all_tabs = QueueDetailTab::all();
2294 let monitoring_index = all_tabs
2295 .iter()
2296 .position(|t| *t == QueueDetailTab::Monitoring)
2297 .unwrap();
2298 assert_eq!(monitoring_index, 1); }
2300}