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 },
525 MetricChart {
526 title: "Approximate number of messages delayed",
527 data: &app.sqs_state.metric_data_delayed,
528 y_axis_label: "Count",
529 x_axis_label: None,
530 },
531 MetricChart {
532 title: "Approximate number of messages not visible",
533 data: &app.sqs_state.metric_data_not_visible,
534 y_axis_label: "Count",
535 x_axis_label: None,
536 },
537 MetricChart {
538 title: "Approximate number of messages visible",
539 data: &app.sqs_state.metric_data_visible,
540 y_axis_label: "Count",
541 x_axis_label: None,
542 },
543 MetricChart {
544 title: "Number of empty receives",
545 data: &app.sqs_state.metric_data_empty_receives,
546 y_axis_label: "Count",
547 x_axis_label: None,
548 },
549 MetricChart {
550 title: "Number of messages deleted",
551 data: &app.sqs_state.metric_data_messages_deleted,
552 y_axis_label: "Count",
553 x_axis_label: None,
554 },
555 MetricChart {
556 title: "Number of messages received",
557 data: &app.sqs_state.metric_data_messages_received,
558 y_axis_label: "Count",
559 x_axis_label: None,
560 },
561 MetricChart {
562 title: "Number of messages sent",
563 data: &app.sqs_state.metric_data_messages_sent,
564 y_axis_label: "Count",
565 x_axis_label: None,
566 },
567 MetricChart {
568 title: "Sent message size",
569 data: &app.sqs_state.metric_data_sent_message_size,
570 y_axis_label: "Bytes",
571 x_axis_label: None,
572 },
573 ],
574 &[],
575 &[],
576 &[],
577 app.sqs_state.monitoring_scroll,
578 );
579 }
580 QueueDetailTab::SnsSubscriptions => {
581 render_subscriptions_tab(frame, app, chunks[2]);
582 }
583 QueueDetailTab::LambdaTriggers => {
584 render_lambda_triggers_tab(frame, app, chunks[2]);
585 }
586 QueueDetailTab::EventBridgePipes => {
587 render_eventbridge_pipes_tab(frame, app, chunks[2]);
588 }
589 QueueDetailTab::DeadLetterQueue => {
590 render_dead_letter_queue_tab(frame, app, chunks[2]);
591 }
592 QueueDetailTab::Tagging => {
593 render_tags_tab(frame, app, chunks[2]);
594 }
595 QueueDetailTab::Encryption => {
596 render_encryption_tab(frame, app, chunks[2]);
597 }
598 QueueDetailTab::DeadLetterQueueRedriveTasks => {
599 render_dlq_redrive_tasks_tab(frame, app, chunks[2]);
600 }
601 }
602}
603
604fn render_details_fields(queue: &Queue) -> Vec<ratatui::text::Line<'static>> {
605 let max_msg_size = queue
606 .maximum_message_size
607 .split_whitespace()
608 .next()
609 .and_then(|s| s.parse::<i64>().ok())
610 .map(format_bytes)
611 .unwrap_or_else(|| queue.maximum_message_size.clone());
612
613 let retention_period = queue
614 .message_retention_period
615 .parse::<i32>()
616 .ok()
617 .map(format_duration_seconds)
618 .unwrap_or_else(|| queue.message_retention_period.clone());
619
620 let visibility_timeout = queue
621 .visibility_timeout
622 .parse::<i32>()
623 .ok()
624 .map(format_duration_seconds)
625 .unwrap_or_else(|| queue.visibility_timeout.clone());
626
627 let delivery_delay = queue
628 .delivery_delay
629 .parse::<i32>()
630 .ok()
631 .map(format_duration_seconds)
632 .unwrap_or_else(|| queue.delivery_delay.clone());
633
634 let receive_wait_time = queue
635 .receive_message_wait_time
636 .parse::<i32>()
637 .ok()
638 .map(format_duration_seconds)
639 .unwrap_or_else(|| queue.receive_message_wait_time.clone());
640
641 vec![
642 labeled_field("Name", &queue.name),
643 labeled_field("Type", &queue.queue_type),
644 labeled_field(
645 "ARN",
646 format!(
647 "arn:aws:sqs:{}:{}:{}",
648 extract_region(&queue.url),
649 extract_account_id(&queue.url),
650 queue.name
651 ),
652 ),
653 labeled_field("Encryption", &queue.encryption),
654 labeled_field("URL", &queue.url),
655 labeled_field("Dead-letter queue", &queue.dead_letter_queue),
656 labeled_field("Created", format_unix_timestamp(&queue.created_timestamp)),
657 labeled_field("Maximum message size", max_msg_size),
658 labeled_field(
659 "Last updated",
660 format_unix_timestamp(&queue.last_modified_timestamp),
661 ),
662 labeled_field("Message retention period", retention_period),
663 labeled_field("Default visibility timeout", visibility_timeout),
664 labeled_field("Messages available", &queue.messages_available),
665 labeled_field("Delivery delay", delivery_delay),
666 labeled_field(
667 "Messages in flight (not available to other consumers)",
668 &queue.messages_in_flight,
669 ),
670 labeled_field("Receive message wait time", receive_wait_time),
671 labeled_field("Messages delayed", &queue.messages_delayed),
672 labeled_field(
673 "Content-based deduplication",
674 &queue.content_based_deduplication,
675 ),
676 labeled_field("High throughput FIFO", &queue.high_throughput_fifo),
677 labeled_field("Deduplication scope", &queue.deduplication_scope),
678 labeled_field("FIFO throughput limit", &queue.fifo_throughput_limit),
679 labeled_field("Redrive allow policy", &queue.redrive_allow_policy),
680 ]
681}
682
683fn render_queue_policies_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
684 use ratatui::prelude::{Constraint, Direction, Layout};
685
686 let chunks = Layout::default()
687 .direction(Direction::Vertical)
688 .constraints([Constraint::Min(0)])
689 .split(area);
690
691 render_json_highlighted(
693 frame,
694 chunks[0],
695 &app.sqs_state.policy_document,
696 app.sqs_state.policy_scroll,
697 "Access policy",
698 true,
699 );
700}
701
702fn render_lambda_triggers_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
703 use ratatui::prelude::*;
704
705 let chunks = Layout::default()
706 .direction(Direction::Vertical)
707 .constraints([Constraint::Length(3), Constraint::Min(0)])
708 .split(area);
709
710 let filtered = filtered_lambda_triggers(app);
711
712 let columns: Vec<Box<dyn Column<LambdaTrigger>>> = app
713 .sqs_state
714 .trigger_visible_column_ids
715 .iter()
716 .filter_map(|id| TriggerColumn::from_id(id))
717 .map(|col| Box::new(col) as Box<dyn Column<LambdaTrigger>>)
718 .collect();
719
720 let page_size = app.sqs_state.triggers.page_size.value();
722 let total_pages = filtered.len().div_ceil(page_size.max(1));
723 let current_page = app.sqs_state.triggers.selected / page_size.max(1);
724 let pagination = render_pagination_text(current_page, total_pages);
725
726 render_simple_filter(
728 frame,
729 chunks[0],
730 SimpleFilterConfig {
731 filter_text: &app.sqs_state.triggers.filter,
732 placeholder: "Search triggers",
733 pagination: &pagination,
734 mode: app.mode,
735 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
736 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
737 },
738 );
739
740 let start_idx = current_page * page_size;
741 let end_idx = (start_idx + page_size).min(filtered.len());
742 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
743
744 let expanded_index = app.sqs_state.triggers.expanded_item.and_then(|idx| {
745 if idx >= start_idx && idx < end_idx {
746 Some(idx - start_idx)
747 } else {
748 None
749 }
750 });
751
752 render_table(
753 frame,
754 TableConfig {
755 area: chunks[1],
756 columns: &columns,
757 items: paginated,
758 selected_index: app.sqs_state.triggers.selected % page_size.max(1),
759 is_active: app.mode != Mode::FilterInput,
760 title: format_title(&format!("Lambda triggers ({})", filtered.len())),
761 sort_column: "last_modified",
762 sort_direction: SortDirection::Asc,
763 expanded_index,
764 get_expanded_content: Some(Box::new(|trigger: &LambdaTrigger| {
765 expanded_from_columns(&columns, trigger)
766 })),
767 },
768 );
769}
770
771pub fn extract_region(url: &str) -> &str {
772 url.split("sqs.")
773 .nth(1)
774 .and_then(|s| s.split('.').next())
775 .unwrap_or("unknown")
776}
777
778pub fn extract_account_id(url: &str) -> &str {
779 url.split('/').nth(3).unwrap_or("unknown")
780}
781
782fn render_eventbridge_pipes_tab(
783 frame: &mut ratatui::Frame,
784 app: &App,
785 area: ratatui::prelude::Rect,
786) {
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_eventbridge_pipes(app);
795
796 let columns: Vec<Box<dyn Column<EventBridgePipe>>> = app
797 .sqs_state
798 .pipe_visible_column_ids
799 .iter()
800 .filter_map(|id| PipeColumn::from_id(id))
801 .map(|col| Box::new(col) as Box<dyn Column<EventBridgePipe>>)
802 .collect();
803
804 let page_size = app.sqs_state.pipes.page_size.value();
805 let total_pages = filtered.len().div_ceil(page_size.max(1));
806 let current_page = app.sqs_state.pipes.selected / page_size.max(1);
807 let pagination = render_pagination_text(current_page, total_pages);
808
809 render_simple_filter(
810 frame,
811 chunks[0],
812 SimpleFilterConfig {
813 filter_text: &app.sqs_state.pipes.filter,
814 placeholder: "Search pipes",
815 pagination: &pagination,
816 mode: app.mode,
817 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
818 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
819 },
820 );
821
822 let start_idx = current_page * page_size;
823 let end_idx = (start_idx + page_size).min(filtered.len());
824 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
825
826 let expanded_index = app.sqs_state.pipes.expanded_item.and_then(|idx| {
827 if idx >= start_idx && idx < end_idx {
828 Some(idx - start_idx)
829 } else {
830 None
831 }
832 });
833
834 render_table(
835 frame,
836 TableConfig {
837 area: chunks[1],
838 columns: &columns,
839 items: paginated,
840 selected_index: app.sqs_state.pipes.selected % page_size.max(1),
841 is_active: app.mode != Mode::FilterInput,
842 title: format_title(&format!("EventBridge Pipes ({})", filtered.len())),
843 sort_column: "last_modified",
844 sort_direction: SortDirection::Asc,
845 expanded_index,
846 get_expanded_content: Some(Box::new(|pipe: &EventBridgePipe| {
847 expanded_from_columns(&columns, pipe)
848 })),
849 },
850 );
851}
852
853fn render_tags_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
854 use ratatui::prelude::*;
855
856 let chunks = Layout::default()
857 .direction(Direction::Vertical)
858 .constraints([Constraint::Length(3), Constraint::Min(0)])
859 .split(area);
860
861 let filtered = filtered_tags(app);
862
863 let columns: Vec<Box<dyn Column<QueueTag>>> = app
864 .sqs_state
865 .tag_visible_column_ids
866 .iter()
867 .filter_map(|id| TagColumn::from_id(id))
868 .map(|col| Box::new(col) as Box<dyn Column<QueueTag>>)
869 .collect();
870
871 let page_size = app.sqs_state.tags.page_size.value();
872 let total_pages = filtered.len().div_ceil(page_size.max(1));
873 let current_page = app.sqs_state.tags.selected / page_size.max(1);
874 let pagination = render_pagination_text(current_page, total_pages);
875
876 render_simple_filter(
877 frame,
878 chunks[0],
879 SimpleFilterConfig {
880 filter_text: &app.sqs_state.tags.filter,
881 placeholder: "Search tags",
882 pagination: &pagination,
883 mode: app.mode,
884 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
885 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
886 },
887 );
888
889 let start_idx = current_page * page_size;
890 let end_idx = (start_idx + page_size).min(filtered.len());
891 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
892
893 let expanded_index = app.sqs_state.tags.expanded_item.and_then(|idx| {
894 if idx >= start_idx && idx < end_idx {
895 Some(idx - start_idx)
896 } else {
897 None
898 }
899 });
900
901 render_table(
902 frame,
903 TableConfig {
904 area: chunks[1],
905 columns: &columns,
906 items: paginated,
907 selected_index: app.sqs_state.tags.selected % page_size.max(1),
908 is_active: app.mode != Mode::FilterInput,
909 title: format_title(&format!("Tagging ({})", filtered.len())),
910 sort_column: "value",
911 sort_direction: SortDirection::Asc,
912 expanded_index,
913 get_expanded_content: Some(Box::new(|tag: &QueueTag| {
914 expanded_from_columns(&columns, tag)
915 })),
916 },
917 );
918}
919
920fn render_subscriptions_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
921 use ratatui::prelude::*;
922
923 let chunks = Layout::default()
924 .direction(Direction::Vertical)
925 .constraints([Constraint::Length(3), Constraint::Min(0)])
926 .split(area);
927
928 let filtered = filtered_subscriptions(app);
929
930 let columns: Vec<Box<dyn Column<SnsSubscription>>> = app
931 .sqs_state
932 .subscription_visible_column_ids
933 .iter()
934 .filter_map(|id| SubscriptionColumn::from_id(id))
935 .map(|col| Box::new(col) as Box<dyn Column<SnsSubscription>>)
936 .collect();
937
938 let page_size = app.sqs_state.subscriptions.page_size.value();
939 let total_pages = filtered.len().div_ceil(page_size.max(1));
940 let current_page = app.sqs_state.subscriptions.selected / page_size.max(1);
941 let pagination = render_pagination_text(current_page, total_pages);
942
943 render_subscription_filter(frame, app, chunks[0], &pagination);
945
946 let start_idx = current_page * page_size;
947 let end_idx = (start_idx + page_size).min(filtered.len());
948 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
949
950 let expanded_index = app.sqs_state.subscriptions.expanded_item.and_then(|idx| {
951 if idx >= start_idx && idx < end_idx {
952 Some(idx - start_idx)
953 } else {
954 None
955 }
956 });
957
958 render_table(
959 frame,
960 TableConfig {
961 area: chunks[1],
962 columns: &columns,
963 items: paginated,
964 selected_index: app.sqs_state.subscriptions.selected % page_size.max(1),
965 is_active: app.mode != Mode::FilterInput,
966 title: format_title(&format!("SNS subscriptions ({})", filtered.len())),
967 sort_column: "subscription_arn",
968 sort_direction: SortDirection::Asc,
969 expanded_index,
970 get_expanded_content: Some(Box::new(|sub: &SnsSubscription| {
971 expanded_from_columns(&columns, sub)
972 })),
973 },
974 );
975
976 if app.mode == FilterInput && app.sqs_state.input_focus == SUBSCRIPTION_REGION {
978 let regions = Region::all();
979 let region_codes: Vec<&str> = regions.iter().map(|r| r.code).collect();
980 render_dropdown(
981 frame,
982 ®ion_codes,
983 app.sqs_state.subscription_region_selected,
984 chunks[0],
985 pagination.len() as u16 + 3, );
987 }
988}
989
990fn render_subscription_filter(
991 frame: &mut ratatui::Frame,
992 app: &App,
993 area: ratatui::prelude::Rect,
994 pagination: &str,
995) {
996 let region_text = if app.sqs_state.subscription_region_filter.is_empty() {
997 format!("Subscription region: {}", app.region)
998 } else {
999 format!(
1000 "Subscription region: {}",
1001 app.sqs_state.subscription_region_filter
1002 )
1003 };
1004
1005 render_filter_bar(
1006 frame,
1007 FilterConfig {
1008 filter_text: &app.sqs_state.subscriptions.filter,
1009 placeholder: "Search subscriptions",
1010 mode: app.mode,
1011 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1012 controls: vec![
1013 FilterControl {
1014 text: region_text,
1015 is_focused: app.sqs_state.input_focus == SUBSCRIPTION_REGION,
1016 },
1017 FilterControl {
1018 text: pagination.to_string(),
1019 is_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1020 },
1021 ],
1022 area,
1023 },
1024 );
1025}
1026
1027fn render_dead_letter_queue_tab(
1028 frame: &mut ratatui::Frame,
1029 app: &App,
1030 area: ratatui::prelude::Rect,
1031) {
1032 use ratatui::prelude::*;
1033
1034 let queue = app
1035 .sqs_state
1036 .queues
1037 .items
1038 .iter()
1039 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1040
1041 if let Some(q) = queue {
1042 if !q.redrive_policy.is_empty() {
1043 if let Ok(policy) = serde_json::from_str::<serde_json::Value>(&q.redrive_policy) {
1044 let dlq_arn = policy
1045 .get("deadLetterTargetArn")
1046 .and_then(|v| v.as_str())
1047 .unwrap_or("-");
1048 let max_receives = policy
1049 .get("maxReceiveCount")
1050 .and_then(|v| v.as_i64())
1051 .map(|n| n.to_string())
1052 .unwrap_or_else(|| "-".to_string());
1053
1054 let lines = vec![
1055 labeled_field("Queue", dlq_arn),
1056 labeled_field("Maximum receives", &max_receives),
1057 ];
1058
1059 let height = calculate_dynamic_height(&lines, area.width.saturating_sub(4)) + 2;
1060
1061 let chunks = Layout::default()
1062 .direction(Direction::Vertical)
1063 .constraints([Constraint::Length(height), Constraint::Min(0)])
1064 .split(area);
1065
1066 let block = titled_block("Dead-letter queue");
1067 let inner = block.inner(chunks[0]);
1068 frame.render_widget(block, chunks[0]);
1069 render_fields_with_dynamic_columns(frame, inner, lines);
1070 return;
1071 }
1072 }
1073 }
1074
1075 let chunks = Layout::default()
1077 .direction(Direction::Vertical)
1078 .constraints([Constraint::Length(3), Constraint::Min(0)])
1079 .split(area);
1080
1081 let block = titled_block("Dead-letter queue");
1082 let inner = block.inner(chunks[0]);
1083 frame.render_widget(block, chunks[0]);
1084 frame.render_widget(Paragraph::new("No dead-letter queue configured"), inner);
1085}
1086
1087fn render_encryption_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
1088 use ratatui::prelude::*;
1089
1090 let queue = app
1091 .sqs_state
1092 .queues
1093 .items
1094 .iter()
1095 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1096
1097 let chunks = Layout::default()
1098 .direction(Direction::Vertical)
1099 .constraints([Constraint::Length(3), Constraint::Min(0)])
1100 .split(area);
1101
1102 let block = titled_block("Encryption");
1103 let inner = block.inner(chunks[0]);
1104 frame.render_widget(block, chunks[0]);
1105
1106 if let Some(q) = queue {
1107 let encryption_text = if q.encryption.is_empty() || q.encryption == "-" {
1108 "Server-side encryption is not enabled".to_string()
1109 } else {
1110 format!("Server-side encryption is managed by {}", q.encryption)
1111 };
1112
1113 frame.render_widget(Paragraph::new(encryption_text), inner);
1114 }
1115}
1116
1117fn render_dlq_redrive_tasks_tab(
1118 frame: &mut ratatui::Frame,
1119 app: &App,
1120 area: ratatui::prelude::Rect,
1121) {
1122 use ratatui::prelude::*;
1123
1124 let queue = app
1125 .sqs_state
1126 .queues
1127 .items
1128 .iter()
1129 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1130
1131 if let Some(q) = queue {
1132 let lines = vec![
1133 labeled_field("Name", &q.redrive_task_id),
1134 labeled_field("Date started", &q.redrive_task_start_time),
1135 labeled_field("Percent processed", &q.redrive_task_percent),
1136 labeled_field("Status", &q.redrive_task_status),
1137 labeled_field("Redrive destination", &q.redrive_task_destination),
1138 ];
1139
1140 let height = calculate_dynamic_height(&lines, area.width.saturating_sub(4)) + 2;
1141
1142 let chunks = Layout::default()
1143 .direction(Direction::Vertical)
1144 .constraints([Constraint::Length(height), Constraint::Min(0)])
1145 .split(area);
1146
1147 let block = titled_block("Dead-letter queue redrive status");
1148 let inner = block.inner(chunks[0]);
1149 frame.render_widget(block, chunks[0]);
1150 render_fields_with_dynamic_columns(frame, inner, lines);
1151 }
1152}
1153
1154fn render_queue_list(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
1155 use ratatui::prelude::*;
1156 use ratatui::widgets::Clear;
1157 use SortDirection;
1158
1159 frame.render_widget(Clear, area);
1160
1161 let chunks = Layout::default()
1162 .direction(Direction::Vertical)
1163 .constraints([
1164 Constraint::Length(3), Constraint::Min(0), ])
1167 .split(area);
1168
1169 let filtered_count =
1170 filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter).len();
1171 let page_size = app.sqs_state.queues.page_size.value();
1172 let total_pages = filtered_count.div_ceil(page_size);
1173 let current_page = app.sqs_state.queues.selected / page_size;
1174 let pagination = render_pagination_text(current_page, total_pages);
1175
1176 render_simple_filter(
1177 frame,
1178 chunks[0],
1179 SimpleFilterConfig {
1180 filter_text: &app.sqs_state.queues.filter,
1181 placeholder: "Search by queue name prefix",
1182 pagination: &pagination,
1183 mode: app.mode,
1184 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1185 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1186 },
1187 );
1188
1189 let filtered: Vec<_> =
1190 filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter);
1191
1192 let start_idx = current_page * page_size;
1193 let end_idx = (start_idx + page_size).min(filtered.len());
1194 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1195
1196 let title = format_title(&format!("Queues ({})", filtered.len()));
1197
1198 let columns: Vec<Box<dyn Column<Queue>>> = app
1199 .sqs_visible_column_ids
1200 .iter()
1201 .filter_map(|col_id| {
1202 SqsColumn::from_id(col_id).map(|col| Box::new(col) as Box<dyn Column<Queue>>)
1203 })
1204 .collect();
1205
1206 let expanded_index = app.sqs_state.queues.expanded_item.and_then(|idx| {
1207 if idx >= start_idx && idx < end_idx {
1208 Some(idx - start_idx)
1209 } else {
1210 None
1211 }
1212 });
1213
1214 let config = TableConfig {
1215 items: paginated,
1216 selected_index: app.sqs_state.queues.selected % page_size,
1217 expanded_index,
1218 columns: &columns,
1219 sort_column: "Name",
1220 sort_direction: SortDirection::Asc,
1221 title,
1222 area: chunks[1],
1223 get_expanded_content: Some(Box::new(|queue: &Queue| {
1224 expanded_from_columns(&columns, queue)
1225 })),
1226 is_active: app.mode != Mode::FilterInput,
1227 };
1228
1229 render_table(frame, config);
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235 use ratatui::prelude::Rect;
1236
1237 #[test]
1238 fn test_sqs_state_initialization() {
1239 let state = State::new();
1240 assert_eq!(state.queues.items.len(), 0);
1241 assert_eq!(state.queues.selected, 0);
1242 assert_eq!(state.queues.filter, "");
1243 assert_eq!(state.queues.page_size.value(), 50);
1244 assert_eq!(state.input_focus, InputFocus::Filter);
1245 }
1246
1247 #[test]
1248 fn test_filtered_queues_empty_filter() {
1249 let queues = vec![
1250 Queue {
1251 name: "queue1".to_string(),
1252 url: String::new(),
1253 queue_type: "Standard".to_string(),
1254 created_timestamp: String::new(),
1255 messages_available: "0".to_string(),
1256 messages_in_flight: "0".to_string(),
1257 encryption: "Disabled".to_string(),
1258 content_based_deduplication: "Disabled".to_string(),
1259 last_modified_timestamp: String::new(),
1260 visibility_timeout: String::new(),
1261 message_retention_period: String::new(),
1262 maximum_message_size: String::new(),
1263 delivery_delay: String::new(),
1264 receive_message_wait_time: String::new(),
1265 high_throughput_fifo: "N/A".to_string(),
1266 deduplication_scope: "N/A".to_string(),
1267 fifo_throughput_limit: "N/A".to_string(),
1268 dead_letter_queue: "-".to_string(),
1269 messages_delayed: "0".to_string(),
1270 redrive_allow_policy: "-".to_string(),
1271 redrive_policy: "".to_string(),
1272 redrive_task_id: "-".to_string(),
1273 redrive_task_start_time: "-".to_string(),
1274 redrive_task_status: "-".to_string(),
1275 redrive_task_percent: "-".to_string(),
1276 redrive_task_destination: "-".to_string(),
1277 },
1278 Queue {
1279 name: "queue2".to_string(),
1280 url: String::new(),
1281 queue_type: "Standard".to_string(),
1282 created_timestamp: String::new(),
1283 messages_available: "0".to_string(),
1284 messages_in_flight: "0".to_string(),
1285 encryption: "Disabled".to_string(),
1286 content_based_deduplication: "Disabled".to_string(),
1287 last_modified_timestamp: String::new(),
1288 visibility_timeout: String::new(),
1289 message_retention_period: String::new(),
1290 maximum_message_size: String::new(),
1291 delivery_delay: String::new(),
1292 receive_message_wait_time: String::new(),
1293 high_throughput_fifo: "N/A".to_string(),
1294 deduplication_scope: "N/A".to_string(),
1295 fifo_throughput_limit: "N/A".to_string(),
1296 dead_letter_queue: "-".to_string(),
1297 messages_delayed: "0".to_string(),
1298 redrive_allow_policy: "-".to_string(),
1299 redrive_policy: "".to_string(),
1300 redrive_task_id: "-".to_string(),
1301 redrive_task_start_time: "-".to_string(),
1302 redrive_task_status: "-".to_string(),
1303 redrive_task_percent: "-".to_string(),
1304 redrive_task_destination: "-".to_string(),
1305 },
1306 ];
1307
1308 let filtered = filtered_queues(&queues, "");
1309 assert_eq!(filtered.len(), 2);
1310 }
1311
1312 #[test]
1313 fn test_filtered_queues_with_prefix() {
1314 let queues = vec![
1315 Queue {
1316 name: "prod-orders".to_string(),
1317 url: String::new(),
1318 queue_type: "Standard".to_string(),
1319 created_timestamp: String::new(),
1320 messages_available: "0".to_string(),
1321 messages_in_flight: "0".to_string(),
1322 encryption: "Disabled".to_string(),
1323 content_based_deduplication: "Disabled".to_string(),
1324 last_modified_timestamp: String::new(),
1325 visibility_timeout: String::new(),
1326 message_retention_period: String::new(),
1327 maximum_message_size: String::new(),
1328 delivery_delay: String::new(),
1329 receive_message_wait_time: String::new(),
1330 high_throughput_fifo: "N/A".to_string(),
1331 deduplication_scope: "N/A".to_string(),
1332 fifo_throughput_limit: "N/A".to_string(),
1333 dead_letter_queue: "-".to_string(),
1334 messages_delayed: "0".to_string(),
1335 redrive_allow_policy: "-".to_string(),
1336 redrive_policy: "".to_string(),
1337 redrive_task_id: "-".to_string(),
1338 redrive_task_start_time: "-".to_string(),
1339 redrive_task_status: "-".to_string(),
1340 redrive_task_percent: "-".to_string(),
1341 redrive_task_destination: "-".to_string(),
1342 },
1343 Queue {
1344 name: "dev-orders".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 ];
1372
1373 let filtered = filtered_queues(&queues, "prod");
1374 assert_eq!(filtered.len(), 1);
1375 assert_eq!(filtered[0].name, "prod-orders");
1376 }
1377
1378 #[test]
1379 fn test_filtered_queues_case_insensitive() {
1380 let queues = vec![Queue {
1381 name: "MyQueue".to_string(),
1382 url: String::new(),
1383 queue_type: "Standard".to_string(),
1384 created_timestamp: String::new(),
1385 messages_available: "0".to_string(),
1386 messages_in_flight: "0".to_string(),
1387 encryption: "Disabled".to_string(),
1388 content_based_deduplication: "Disabled".to_string(),
1389 last_modified_timestamp: String::new(),
1390 visibility_timeout: String::new(),
1391 message_retention_period: String::new(),
1392 maximum_message_size: String::new(),
1393 delivery_delay: String::new(),
1394 receive_message_wait_time: String::new(),
1395 high_throughput_fifo: "N/A".to_string(),
1396 deduplication_scope: "N/A".to_string(),
1397 fifo_throughput_limit: "N/A".to_string(),
1398 dead_letter_queue: "-".to_string(),
1399 messages_delayed: "0".to_string(),
1400 redrive_allow_policy: "-".to_string(),
1401 redrive_policy: "".to_string(),
1402 redrive_task_id: "-".to_string(),
1403 redrive_task_start_time: "-".to_string(),
1404 redrive_task_status: "-".to_string(),
1405 redrive_task_percent: "-".to_string(),
1406 redrive_task_destination: "-".to_string(),
1407 }];
1408
1409 let filtered = filtered_queues(&queues, "my");
1410 assert_eq!(filtered.len(), 1);
1411
1412 let filtered = filtered_queues(&queues, "MY");
1413 assert_eq!(filtered.len(), 1);
1414 }
1415
1416 #[test]
1417 fn test_pagination_page_size() {
1418 let state = State::new();
1419 assert_eq!(state.queues.page_size.value(), 50);
1420 }
1421
1422 #[test]
1423 fn test_state_initialization_with_policy() {
1424 let state = State::new();
1425 assert_eq!(state.policy_scroll, 0);
1426 assert_eq!(state.current_queue, None);
1427 assert!(state.policy_document.contains("Version"));
1428 assert!(state.policy_document.contains("2012-10-17"));
1429 }
1430
1431 #[test]
1432 fn test_extract_region() {
1433 let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1434 assert_eq!(extract_region(url), "us-east-1");
1435
1436 let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1437 assert_eq!(extract_region(url2), "eu-west-2");
1438 }
1439
1440 #[test]
1441 fn test_extract_account_id() {
1442 let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1443 assert_eq!(extract_account_id(url), "123456789012");
1444
1445 let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1446 assert_eq!(extract_account_id(url2), "987654321098");
1447 }
1448
1449 #[test]
1450 fn test_timestamp_column_width() {
1451 use SqsColumn as Column;
1452 assert!(Column::Created.width() >= 27);
1454 assert!(Column::LastUpdated.width() >= 27);
1455 }
1456
1457 #[test]
1458 fn test_message_retention_period_formatting() {
1459 let seconds = 345600;
1461 let formatted = format_duration_seconds(seconds);
1462 assert_eq!(formatted, "4d");
1464 }
1465
1466 #[test]
1467 fn test_queue_detail_tab_navigation() {
1468 let tab = QueueDetailTab::QueuePolicies;
1469 assert_eq!(tab.next(), QueueDetailTab::Monitoring);
1470 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1471
1472 let tab = QueueDetailTab::Monitoring;
1473 assert_eq!(tab.next(), QueueDetailTab::SnsSubscriptions);
1474 assert_eq!(tab.prev(), QueueDetailTab::QueuePolicies);
1475
1476 let tab = QueueDetailTab::SnsSubscriptions;
1477 assert_eq!(tab.next(), QueueDetailTab::LambdaTriggers);
1478 assert_eq!(tab.prev(), QueueDetailTab::Monitoring);
1479
1480 let tab = QueueDetailTab::LambdaTriggers;
1481 assert_eq!(tab.next(), QueueDetailTab::EventBridgePipes);
1482 assert_eq!(tab.prev(), QueueDetailTab::SnsSubscriptions);
1483
1484 let tab = QueueDetailTab::EventBridgePipes;
1485 assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueue);
1486 assert_eq!(tab.prev(), QueueDetailTab::LambdaTriggers);
1487
1488 let tab = QueueDetailTab::DeadLetterQueue;
1489 assert_eq!(tab.next(), QueueDetailTab::Tagging);
1490 assert_eq!(tab.prev(), QueueDetailTab::EventBridgePipes);
1491
1492 let tab = QueueDetailTab::Tagging;
1493 assert_eq!(tab.next(), QueueDetailTab::Encryption);
1494 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueue);
1495
1496 let tab = QueueDetailTab::Encryption;
1497 assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1498 assert_eq!(tab.prev(), QueueDetailTab::Tagging);
1499
1500 let tab = QueueDetailTab::DeadLetterQueueRedriveTasks;
1501 assert_eq!(tab.next(), QueueDetailTab::QueuePolicies);
1502 assert_eq!(tab.prev(), QueueDetailTab::Encryption);
1503
1504 let tab = QueueDetailTab::QueuePolicies;
1505 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1506 }
1507
1508 #[test]
1509 fn test_queue_detail_tab_all() {
1510 let tabs = QueueDetailTab::all();
1511 assert_eq!(tabs.len(), 9);
1512 assert_eq!(tabs[0], QueueDetailTab::QueuePolicies);
1513 assert_eq!(tabs[1], QueueDetailTab::Monitoring);
1514 assert_eq!(tabs[2], QueueDetailTab::SnsSubscriptions);
1515 assert_eq!(tabs[3], QueueDetailTab::LambdaTriggers);
1516 assert_eq!(tabs[4], QueueDetailTab::EventBridgePipes);
1517 assert_eq!(tabs[5], QueueDetailTab::DeadLetterQueue);
1518 assert_eq!(tabs[6], QueueDetailTab::Tagging);
1519 assert_eq!(tabs[7], QueueDetailTab::Encryption);
1520 assert_eq!(tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
1521 }
1522
1523 #[test]
1524 fn test_queue_detail_tab_names() {
1525 assert_eq!(QueueDetailTab::QueuePolicies.name(), "Queue policies");
1526 assert_eq!(QueueDetailTab::SnsSubscriptions.name(), "SNS subscriptions");
1527 assert_eq!(QueueDetailTab::LambdaTriggers.name(), "Lambda triggers");
1528 assert_eq!(QueueDetailTab::EventBridgePipes.name(), "EventBridge Pipes");
1529 assert_eq!(QueueDetailTab::Tagging.name(), "Tagging");
1530 assert_eq!(QueueDetailTab::DeadLetterQueue.name(), "Dead-letter queue");
1531 }
1532
1533 #[test]
1534 fn test_trigger_column_all() {
1535 assert_eq!(TriggerColumn::all().len(), 4);
1536 }
1537
1538 #[test]
1539 fn test_trigger_column_ids() {
1540 let ids = TriggerColumn::ids();
1541 assert_eq!(ids.len(), 4);
1542 assert!(ids.contains(&"column.sqs.trigger.uuid"));
1543 assert!(ids.contains(&"column.sqs.trigger.arn"));
1544 assert!(ids.contains(&"column.sqs.trigger.status"));
1545 assert!(ids.contains(&"column.sqs.trigger.last_modified"));
1546 }
1547
1548 #[test]
1549 fn test_trigger_column_from_id() {
1550 assert_eq!(
1551 TriggerColumn::from_id("column.sqs.trigger.uuid"),
1552 Some(TriggerColumn::Uuid)
1553 );
1554 assert_eq!(
1555 TriggerColumn::from_id("column.sqs.trigger.arn"),
1556 Some(TriggerColumn::Arn)
1557 );
1558 assert_eq!(
1559 TriggerColumn::from_id("column.sqs.trigger.status"),
1560 Some(TriggerColumn::Status)
1561 );
1562 assert_eq!(
1563 TriggerColumn::from_id("column.sqs.trigger.last_modified"),
1564 Some(TriggerColumn::LastModified)
1565 );
1566 assert_eq!(TriggerColumn::from_id("invalid"), None);
1567 }
1568
1569 #[test]
1570 fn test_trigger_status_rendering() {
1571 use Column;
1572
1573 let trigger = LambdaTrigger {
1574 uuid: "test-uuid".to_string(),
1575 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1576 status: "Enabled".to_string(),
1577 last_modified: "1609459200".to_string(),
1578 };
1579
1580 let (text, style) = TriggerColumn::Status.render(&trigger);
1581 assert_eq!(text, "✅ Enabled");
1582 assert_eq!(style.fg, Some(ratatui::style::Color::Green));
1583 }
1584
1585 #[test]
1586 fn test_trigger_timestamp_rendering() {
1587 use Column;
1588
1589 let trigger = LambdaTrigger {
1590 uuid: "test-uuid".to_string(),
1591 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1592 status: "Enabled".to_string(),
1593 last_modified: "1609459200".to_string(),
1594 };
1595
1596 let (text, _) = TriggerColumn::LastModified.render(&trigger);
1597 assert!(text.contains("2021-01-01"));
1598 assert!(text.contains("(UTC)"));
1599 }
1600
1601 #[test]
1602 fn test_state_initializes_trigger_columns() {
1603 let state = State::new();
1604 assert_eq!(state.trigger_column_ids.len(), 4);
1605 assert_eq!(state.trigger_visible_column_ids.len(), 4);
1606 assert_eq!(state.trigger_column_ids, state.trigger_visible_column_ids);
1607 }
1608
1609 #[test]
1610 fn test_trigger_state_has_filter() {
1611 let mut state = State::new();
1612 state.detail_tab = QueueDetailTab::LambdaTriggers;
1613 state.triggers.filter = "test-filter".to_string();
1614
1615 assert_eq!(state.triggers.filter, "test-filter");
1617 assert_eq!(state.detail_tab, QueueDetailTab::LambdaTriggers);
1618 }
1619
1620 #[test]
1621 fn test_trigger_filtering() {
1622 let triggers = [
1623 LambdaTrigger {
1624 uuid: "uuid-123".to_string(),
1625 arn: "arn:aws:lambda:us-east-1:123:function:test1".to_string(),
1626 status: "Enabled".to_string(),
1627 last_modified: "1609459200".to_string(),
1628 },
1629 LambdaTrigger {
1630 uuid: "uuid-456".to_string(),
1631 arn: "arn:aws:lambda:us-east-1:123:function:test2".to_string(),
1632 status: "Enabled".to_string(),
1633 last_modified: "1609459200".to_string(),
1634 },
1635 ];
1636
1637 let filtered: Vec<_> = triggers.iter().filter(|t| t.uuid.contains("123")).collect();
1639 assert_eq!(filtered.len(), 1);
1640 assert_eq!(filtered[0].uuid, "uuid-123");
1641
1642 let filtered: Vec<_> = triggers
1644 .iter()
1645 .filter(|t| t.arn.contains("test2"))
1646 .collect();
1647 assert_eq!(filtered.len(), 1);
1648 assert_eq!(
1649 filtered[0].arn,
1650 "arn:aws:lambda:us-east-1:123:function:test2"
1651 );
1652 }
1653
1654 #[test]
1655 fn test_trigger_pagination() {
1656 let mut state = State::new();
1657 state.triggers.items = (0..10)
1658 .map(|i| LambdaTrigger {
1659 uuid: format!("uuid-{}", i),
1660 arn: format!("arn:aws:lambda:us-east-1:123:function:test{}", i),
1661 status: "Enabled".to_string(),
1662 last_modified: "1609459200".to_string(),
1663 })
1664 .collect();
1665
1666 assert_eq!(state.triggers.items.len(), 10);
1667 assert_eq!(state.triggers.page_size.value(), 50); }
1669
1670 #[test]
1671 fn test_trigger_column_visibility() {
1672 let mut state = State::new();
1673
1674 assert_eq!(state.trigger_visible_column_ids.len(), 4);
1676
1677 state.trigger_visible_column_ids.remove(0);
1679 assert_eq!(state.trigger_visible_column_ids.len(), 3);
1680
1681 state
1683 .trigger_visible_column_ids
1684 .push(state.trigger_column_ids[0].clone());
1685 assert_eq!(state.trigger_visible_column_ids.len(), 4);
1686 }
1687
1688 #[test]
1689 fn test_trigger_page_size_options() {
1690 let mut state = State::new();
1691
1692 assert_eq!(state.triggers.page_size, PageSize::Fifty);
1694
1695 state.triggers.page_size = PageSize::Ten;
1697 assert_eq!(state.triggers.page_size.value(), 10);
1698
1699 state.triggers.page_size = PageSize::TwentyFive;
1700 assert_eq!(state.triggers.page_size.value(), 25);
1701
1702 state.triggers.page_size = PageSize::OneHundred;
1703 assert_eq!(state.triggers.page_size.value(), 100);
1704 }
1705
1706 #[test]
1707 fn test_trigger_loading_state() {
1708 let mut state = State::new();
1709
1710 assert!(!state.triggers.loading);
1712
1713 state.triggers.loading = true;
1715 assert!(state.triggers.loading);
1716
1717 state.triggers.loading = false;
1719 assert!(!state.triggers.loading);
1720 }
1721
1722 #[test]
1723 fn test_trigger_sort_by_last_modified() {
1724 let mut triggers = [
1725 LambdaTrigger {
1726 uuid: "uuid-2".to_string(),
1727 arn: "arn2".to_string(),
1728 status: "Enabled".to_string(),
1729 last_modified: "1609459300".to_string(), },
1731 LambdaTrigger {
1732 uuid: "uuid-1".to_string(),
1733 arn: "arn1".to_string(),
1734 status: "Enabled".to_string(),
1735 last_modified: "1609459200".to_string(), },
1737 ];
1738
1739 triggers.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
1741
1742 assert_eq!(triggers[0].uuid, "uuid-1");
1743 assert_eq!(triggers[1].uuid, "uuid-2");
1744 }
1745
1746 #[test]
1747 fn test_trigger_pagination_calculation() {
1748 let mut state = State::new();
1749
1750 state.triggers.items = (0..25)
1752 .map(|i| LambdaTrigger {
1753 uuid: format!("uuid-{}", i),
1754 arn: format!("arn{}", i),
1755 status: "Enabled".to_string(),
1756 last_modified: "1609459200".to_string(),
1757 })
1758 .collect();
1759
1760 state.triggers.page_size = PageSize::Ten;
1762 let page_size = state.triggers.page_size.value();
1763 let total_pages = state.triggers.items.len().div_ceil(page_size);
1764 assert_eq!(total_pages, 3);
1765
1766 let current_page = 0;
1768 let start_idx = current_page * page_size;
1769 let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1770 assert_eq!(start_idx, 0);
1771 assert_eq!(end_idx, 10);
1772
1773 let current_page = 2;
1775 let start_idx = current_page * page_size;
1776 let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1777 assert_eq!(start_idx, 20);
1778 assert_eq!(end_idx, 25);
1779 }
1780
1781 #[test]
1782 fn test_monitoring_metric_data_with_values() {
1783 let mut state = State::new();
1784 state.metric_data = vec![
1786 (1700000000, 0.0),
1787 (1700000060, 5.0),
1788 (1700000120, 10.0),
1789 (1700000180, 0.0),
1790 ];
1791 assert_eq!(state.metric_data.len(), 4);
1792 assert_eq!(state.metric_data[0], (1700000000, 0.0));
1793 assert_eq!(state.metric_data[1], (1700000060, 5.0));
1794 }
1795
1796 #[test]
1797 fn test_monitoring_all_metrics_initialized() {
1798 let state = State::new();
1799 assert!(state.metric_data.is_empty());
1800 assert!(state.metric_data_delayed.is_empty());
1801 assert!(state.metric_data_not_visible.is_empty());
1802 assert!(state.metric_data_visible.is_empty());
1803 assert!(state.metric_data_empty_receives.is_empty());
1804 assert!(state.metric_data_messages_deleted.is_empty());
1805 assert!(state.metric_data_messages_received.is_empty());
1806 assert!(state.metric_data_messages_sent.is_empty());
1807 assert!(state.metric_data_sent_message_size.is_empty());
1808 assert_eq!(state.monitoring_scroll, 0);
1809 }
1810
1811 #[test]
1812 fn test_monitoring_scroll_pages() {
1813 let mut state = State::new();
1814 assert_eq!(state.monitoring_scroll, 0);
1815
1816 state.monitoring_scroll = 1;
1818 assert_eq!(state.monitoring_scroll, 1);
1819
1820 state.monitoring_scroll = 2;
1822 assert_eq!(state.monitoring_scroll, 2);
1823 }
1824
1825 #[test]
1826 fn test_monitoring_delayed_metrics() {
1827 let mut state = State::new();
1828 state.metric_data_delayed = vec![(1700000000, 1.0), (1700000060, 2.0)];
1829 assert_eq!(state.metric_data_delayed.len(), 2);
1830 assert_eq!(state.metric_data_delayed[0].1, 1.0);
1831 }
1832
1833 #[test]
1834 fn test_monitoring_not_visible_metrics() {
1835 let mut state = State::new();
1836 state.metric_data_not_visible = vec![(1700000000, 3.0), (1700000060, 4.0)];
1837 assert_eq!(state.metric_data_not_visible.len(), 2);
1838 assert_eq!(state.metric_data_not_visible[1].1, 4.0);
1839 }
1840
1841 #[test]
1842 fn test_monitoring_visible_metrics() {
1843 let mut state = State::new();
1844 state.metric_data_visible = vec![(1700000000, 5.0), (1700000060, 6.0)];
1845 assert_eq!(state.metric_data_visible.len(), 2);
1846 assert_eq!(state.metric_data_visible[0].1, 5.0);
1847 }
1848
1849 #[test]
1850 fn test_monitoring_empty_receives_metrics() {
1851 let mut state = State::new();
1852 state.metric_data_empty_receives = vec![(1700000000, 10.0), (1700000060, 15.0)];
1853 assert_eq!(state.metric_data_empty_receives.len(), 2);
1854 assert_eq!(state.metric_data_empty_receives[0].1, 10.0);
1855 }
1856
1857 #[test]
1858 fn test_monitoring_messages_deleted_metrics() {
1859 let mut state = State::new();
1860 state.metric_data_messages_deleted = vec![(1700000000, 20.0), (1700000060, 25.0)];
1861 assert_eq!(state.metric_data_messages_deleted.len(), 2);
1862 assert_eq!(state.metric_data_messages_deleted[0].1, 20.0);
1863 }
1864
1865 #[test]
1866 fn test_monitoring_messages_received_metrics() {
1867 let mut state = State::new();
1868 state.metric_data_messages_received = vec![(1700000000, 30.0), (1700000060, 35.0)];
1869 assert_eq!(state.metric_data_messages_received.len(), 2);
1870 assert_eq!(state.metric_data_messages_received[0].1, 30.0);
1871 }
1872
1873 #[test]
1874 fn test_monitoring_messages_sent_metrics() {
1875 let mut state = State::new();
1876 state.metric_data_messages_sent = vec![(1700000000, 40.0), (1700000060, 45.0)];
1877 assert_eq!(state.metric_data_messages_sent.len(), 2);
1878 assert_eq!(state.metric_data_messages_sent[0].1, 40.0);
1879 }
1880
1881 #[test]
1882 fn test_monitoring_sent_message_size_metrics() {
1883 let mut state = State::new();
1884 state.metric_data_sent_message_size = vec![(1700000000, 1024.0), (1700000060, 2048.0)];
1885 assert_eq!(state.metric_data_sent_message_size.len(), 2);
1886 assert_eq!(state.metric_data_sent_message_size[0].1, 1024.0);
1887 }
1888
1889 #[test]
1890 fn test_trigger_expand_collapse() {
1891 let mut state = State::new();
1892
1893 assert_eq!(state.triggers.expanded_item, None);
1895
1896 state.triggers.expanded_item = Some(0);
1898 assert_eq!(state.triggers.expanded_item, Some(0));
1899
1900 state.triggers.expanded_item = None;
1902 assert_eq!(state.triggers.expanded_item, None);
1903 }
1904
1905 #[test]
1906 fn test_trigger_filter_visibility() {
1907 let mut state = State::new();
1908
1909 assert!(state.triggers.filter.is_empty());
1911
1912 state.triggers.filter = "test".to_string();
1914 assert_eq!(state.triggers.filter, "test");
1915
1916 state.triggers.filter.clear();
1918 assert!(state.triggers.filter.is_empty());
1919 }
1920
1921 #[test]
1922 fn test_pipe_column_ids_have_correct_prefix() {
1923 for col in PipeColumn::all() {
1924 assert!(
1925 col.id().starts_with("column.sqs.pipe."),
1926 "PipeColumn ID '{}' should start with 'column.sqs.pipe.'",
1927 col.id()
1928 );
1929 }
1930 }
1931
1932 #[test]
1933 fn test_tags_sorted_by_value() {
1934 let mut state = State::new();
1935 state.tags.items = vec![
1936 QueueTag {
1937 key: "env".to_string(),
1938 value: "prod".to_string(),
1939 },
1940 QueueTag {
1941 key: "team".to_string(),
1942 value: "backend".to_string(),
1943 },
1944 QueueTag {
1945 key: "app".to_string(),
1946 value: "api".to_string(),
1947 },
1948 ];
1949
1950 let mut sorted = state.tags.items.clone();
1951 sorted.sort_by(|a, b| a.value.cmp(&b.value));
1952
1953 assert_eq!(sorted[0].value, "api");
1954 assert_eq!(sorted[1].value, "backend");
1955 assert_eq!(sorted[2].value, "prod");
1956 }
1957
1958 #[test]
1959 fn test_tags_initialization() {
1960 let state = State::new();
1961 assert_eq!(state.tags.items.len(), 0);
1962 assert_eq!(state.tag_column_ids.len(), 2);
1963 assert_eq!(state.tag_visible_column_ids.len(), 2);
1964 }
1965
1966 #[test]
1967 fn test_queue_tag_structure() {
1968 let tag = QueueTag {
1969 key: "Environment".to_string(),
1970 value: "Production".to_string(),
1971 };
1972 assert_eq!(tag.key, "Environment");
1973 assert_eq!(tag.value, "Production");
1974 }
1975
1976 #[test]
1977 fn test_tags_table_state() {
1978 let mut state = State::new();
1979 state.tags.items = vec![
1980 QueueTag {
1981 key: "Env".to_string(),
1982 value: "prod".to_string(),
1983 },
1984 QueueTag {
1985 key: "Team".to_string(),
1986 value: "backend".to_string(),
1987 },
1988 ];
1989 assert_eq!(state.tags.items.len(), 2);
1990 assert_eq!(state.tags.selected, 0);
1991 assert_eq!(state.tags.filter, "");
1992 }
1993
1994 #[test]
1995 fn test_tags_filtering() {
1996 let tags = [
1997 QueueTag {
1998 key: "Environment".to_string(),
1999 value: "production".to_string(),
2000 },
2001 QueueTag {
2002 key: "Team".to_string(),
2003 value: "backend".to_string(),
2004 },
2005 QueueTag {
2006 key: "Project".to_string(),
2007 value: "api".to_string(),
2008 },
2009 ];
2010
2011 let filtered: Vec<_> = tags
2013 .iter()
2014 .filter(|t| t.key.to_lowercase().contains("env"))
2015 .collect();
2016 assert_eq!(filtered.len(), 1);
2017 assert_eq!(filtered[0].key, "Environment");
2018
2019 let filtered: Vec<_> = tags
2021 .iter()
2022 .filter(|t| t.value.to_lowercase().contains("back"))
2023 .collect();
2024 assert_eq!(filtered.len(), 1);
2025 assert_eq!(filtered[0].value, "backend");
2026 }
2027
2028 #[test]
2029 fn test_tags_column_ids() {
2030 let ids = TagColumn::ids();
2031 assert_eq!(ids.len(), 2);
2032 assert_eq!(ids[0], "column.sqs.tag.key");
2033 assert_eq!(ids[1], "column.sqs.tag.value");
2034 }
2035
2036 #[test]
2037 fn test_tags_column_from_id() {
2038 assert!(TagColumn::from_id("column.sqs.tag.key").is_some());
2039 assert!(TagColumn::from_id("column.sqs.tag.value").is_some());
2040 assert!(TagColumn::from_id("invalid").is_none());
2041 }
2042
2043 #[test]
2044 fn test_subscriptions_initialization() {
2045 let state = State::new();
2046 assert_eq!(state.subscriptions.items.len(), 0);
2047 assert_eq!(state.subscription_column_ids.len(), 2);
2048 assert_eq!(state.subscription_visible_column_ids.len(), 2);
2049 assert_eq!(state.subscription_region_filter, "");
2050 }
2051
2052 #[test]
2053 fn test_subscription_column_ids() {
2054 let ids = SubscriptionColumn::ids();
2055 assert_eq!(ids.len(), 2);
2056 assert_eq!(ids[0], "column.sqs.subscription.subscription_arn");
2057 assert_eq!(ids[1], "column.sqs.subscription.topic_arn");
2058 }
2059
2060 #[test]
2061 fn test_subscription_column_from_id() {
2062 assert!(SubscriptionColumn::from_id("column.sqs.subscription.subscription_arn").is_some());
2063 assert!(SubscriptionColumn::from_id("column.sqs.subscription.topic_arn").is_some());
2064 assert!(SubscriptionColumn::from_id("invalid").is_none());
2065 }
2066
2067 #[test]
2068 fn test_subscription_region_filter_default() {
2069 let state = State::new();
2070 assert_eq!(state.subscription_region_filter, "");
2072 }
2073
2074 #[test]
2075 fn test_subscription_region_filter_display() {
2076 let mut state = State::new();
2077
2078 assert_eq!(state.subscription_region_filter, "");
2080
2081 state.subscription_region_filter = "us-west-2".to_string();
2083 assert_eq!(state.subscription_region_filter, "us-west-2");
2084 }
2085
2086 #[test]
2087 fn test_subscription_region_selected_index() {
2088 let state = State::new();
2089 assert_eq!(state.subscription_region_selected, 0);
2090 }
2091
2092 #[test]
2093 fn test_encryption_tab_in_all() {
2094 let tabs = QueueDetailTab::all();
2095 assert!(tabs.contains(&QueueDetailTab::Encryption));
2096 }
2097
2098 #[test]
2099 fn test_encryption_tab_name() {
2100 assert_eq!(QueueDetailTab::Encryption.name(), "Encryption");
2101 }
2102
2103 #[test]
2104 fn test_encryption_tab_order() {
2105 let tabs = QueueDetailTab::all();
2106 let dlq_idx = tabs
2107 .iter()
2108 .position(|t| *t == QueueDetailTab::DeadLetterQueue)
2109 .unwrap();
2110 let tagging_idx = tabs
2111 .iter()
2112 .position(|t| *t == QueueDetailTab::Tagging)
2113 .unwrap();
2114 let encryption_idx = tabs
2115 .iter()
2116 .position(|t| *t == QueueDetailTab::Encryption)
2117 .unwrap();
2118
2119 assert!(dlq_idx < tagging_idx);
2121 assert!(encryption_idx > tagging_idx);
2122 }
2123
2124 #[test]
2125 fn test_dlq_redrive_tasks_tab_in_all() {
2126 let tabs = QueueDetailTab::all();
2127 assert!(tabs.contains(&QueueDetailTab::DeadLetterQueueRedriveTasks));
2128 }
2129
2130 #[test]
2131 fn test_dlq_redrive_tasks_tab_name() {
2132 assert_eq!(
2133 QueueDetailTab::DeadLetterQueueRedriveTasks.name(),
2134 "Dead-letter queue redrive tasks"
2135 );
2136 }
2137
2138 #[test]
2139 fn test_dlq_redrive_tasks_tab_order() {
2140 let tabs = QueueDetailTab::all();
2141 let encryption_idx = tabs
2142 .iter()
2143 .position(|t| *t == QueueDetailTab::Encryption)
2144 .unwrap();
2145 let redrive_idx = tabs
2146 .iter()
2147 .position(|t| *t == QueueDetailTab::DeadLetterQueueRedriveTasks)
2148 .unwrap();
2149
2150 assert!(redrive_idx > encryption_idx);
2152 assert_eq!(redrive_idx, tabs.len() - 1);
2153 }
2154
2155 #[test]
2156 fn test_tab_strip_matches_enum_order() {
2157 let all_tabs = QueueDetailTab::all();
2159 assert_eq!(all_tabs.len(), 9);
2160
2161 assert_eq!(all_tabs[0], QueueDetailTab::QueuePolicies);
2163 assert_eq!(all_tabs[1], QueueDetailTab::Monitoring);
2164 assert_eq!(all_tabs[2], QueueDetailTab::SnsSubscriptions);
2165 assert_eq!(all_tabs[3], QueueDetailTab::LambdaTriggers);
2166 assert_eq!(all_tabs[4], QueueDetailTab::EventBridgePipes);
2167 assert_eq!(all_tabs[5], QueueDetailTab::DeadLetterQueue);
2168 assert_eq!(all_tabs[6], QueueDetailTab::Tagging);
2169 assert_eq!(all_tabs[7], QueueDetailTab::Encryption);
2170 assert_eq!(all_tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
2171 }
2172
2173 #[test]
2174 fn test_monitoring_tab_in_all() {
2175 let all_tabs = QueueDetailTab::all();
2176 assert!(all_tabs.contains(&QueueDetailTab::Monitoring));
2177 }
2178
2179 #[test]
2180 fn test_monitoring_tab_name() {
2181 assert_eq!(QueueDetailTab::Monitoring.name(), "Monitoring");
2182 }
2183
2184 #[test]
2185 fn test_monitoring_tab_order() {
2186 let all_tabs = QueueDetailTab::all();
2187 let monitoring_index = all_tabs
2188 .iter()
2189 .position(|t| *t == QueueDetailTab::Monitoring)
2190 .unwrap();
2191 assert_eq!(monitoring_index, 1); }
2193
2194 #[test]
2195 fn test_rounded_block_used_for_details_pane() {
2196 let block = titled_block("Details");
2198 let area = Rect::new(0, 0, 80, 10);
2199 let inner = block.inner(area);
2200 assert_eq!(inner.width, 78); assert_eq!(inner.height, 8); }
2203}