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