1use crate::aws::Region;
2use crate::common::{render_dropdown, render_vertical_scrollbar, InputFocus};
3use crate::keymap::Mode::FilterInput;
4use crate::sqs::pipe::{Column as PipeColumn, EventBridgePipe};
5use crate::sqs::queue::{Column as SqsColumn, Queue};
6use crate::sqs::sub::{Column as SubscriptionColumn, SnsSubscription};
7use crate::sqs::tag::{Column as TagColumn, QueueTag};
8use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
9use crate::table::TableState;
10use crate::ui::filter::{
11 render_filter_bar, render_simple_filter, FilterConfig, FilterControl, SimpleFilterConfig,
12};
13use crate::ui::{labeled_field, render_tabs};
14
15pub const FILTER_CONTROLS: &[InputFocus] = &[InputFocus::Filter, InputFocus::Pagination];
16pub const SUBSCRIPTION_REGION: InputFocus = InputFocus::Dropdown("SubscriptionRegion");
17pub const SUBSCRIPTION_FILTER_CONTROLS: &[InputFocus] = &[
18 InputFocus::Filter,
19 SUBSCRIPTION_REGION,
20 InputFocus::Pagination,
21];
22
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum QueueDetailTab {
25 QueuePolicies,
26 Monitoring,
27 SnsSubscriptions,
28 LambdaTriggers,
29 EventBridgePipes,
30 DeadLetterQueue,
31 Tagging,
32 Encryption,
33 DeadLetterQueueRedriveTasks,
34}
35
36impl QueueDetailTab {
37 pub fn all() -> Vec<QueueDetailTab> {
38 vec![
39 QueueDetailTab::QueuePolicies,
40 QueueDetailTab::Monitoring,
41 QueueDetailTab::SnsSubscriptions,
42 QueueDetailTab::LambdaTriggers,
43 QueueDetailTab::EventBridgePipes,
44 QueueDetailTab::DeadLetterQueue,
45 QueueDetailTab::Tagging,
46 QueueDetailTab::Encryption,
47 QueueDetailTab::DeadLetterQueueRedriveTasks,
48 ]
49 }
50
51 pub fn name(&self) -> &'static str {
52 match self {
53 QueueDetailTab::QueuePolicies => "Queue policies",
54 QueueDetailTab::Monitoring => "Monitoring",
55 QueueDetailTab::SnsSubscriptions => "SNS subscriptions",
56 QueueDetailTab::LambdaTriggers => "Lambda triggers",
57 QueueDetailTab::EventBridgePipes => "EventBridge Pipes",
58 QueueDetailTab::Tagging => "Tagging",
59 QueueDetailTab::Encryption => "Encryption",
60 QueueDetailTab::DeadLetterQueueRedriveTasks => "Dead-letter queue redrive tasks",
61 QueueDetailTab::DeadLetterQueue => "Dead-letter queue",
62 }
63 }
64}
65
66impl crate::common::CyclicEnum for QueueDetailTab {
67 const ALL: &'static [Self] = &[
68 QueueDetailTab::QueuePolicies,
69 QueueDetailTab::Monitoring,
70 QueueDetailTab::SnsSubscriptions,
71 QueueDetailTab::LambdaTriggers,
72 QueueDetailTab::EventBridgePipes,
73 QueueDetailTab::DeadLetterQueue,
74 QueueDetailTab::Tagging,
75 QueueDetailTab::Encryption,
76 QueueDetailTab::DeadLetterQueueRedriveTasks,
77 ];
78}
79
80#[derive(Debug, Clone)]
81pub struct State {
82 pub queues: TableState<Queue>,
83 pub triggers: TableState<LambdaTrigger>,
84 pub trigger_visible_column_ids: Vec<String>,
85 pub trigger_column_ids: Vec<String>,
86 pub pipes: TableState<EventBridgePipe>,
87 pub pipe_visible_column_ids: Vec<String>,
88 pub pipe_column_ids: Vec<String>,
89 pub tags: TableState<QueueTag>,
90 pub tag_visible_column_ids: Vec<String>,
91 pub tag_column_ids: Vec<String>,
92 pub subscriptions: TableState<SnsSubscription>,
93 pub subscription_visible_column_ids: Vec<String>,
94 pub subscription_column_ids: Vec<String>,
95 pub subscription_region_filter: String,
96 pub subscription_region_selected: usize,
97 pub input_focus: InputFocus,
98 pub current_queue: Option<String>,
99 pub detail_tab: QueueDetailTab,
100 pub policy_scroll: usize,
101 pub policy_document: String,
102 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,
112 pub monitoring_scroll: usize,
113}
114
115impl Default for State {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl State {
122 pub fn new() -> Self {
123 let trigger_column_ids: Vec<String> = TriggerColumn::ids()
124 .into_iter()
125 .map(|s| s.to_string())
126 .collect();
127 let pipe_column_ids: Vec<String> = PipeColumn::ids()
128 .into_iter()
129 .map(|s| s.to_string())
130 .collect();
131 let tag_column_ids: Vec<String> = TagColumn::ids()
132 .into_iter()
133 .map(|s| s.to_string())
134 .collect();
135 let subscription_column_ids: Vec<String> = SubscriptionColumn::ids()
136 .into_iter()
137 .map(|s| s.to_string())
138 .collect();
139 Self {
140 queues: TableState::new(),
141 triggers: TableState::new(),
142 trigger_visible_column_ids: trigger_column_ids.clone(),
143 trigger_column_ids,
144 pipes: TableState::new(),
145 pipe_visible_column_ids: pipe_column_ids.clone(),
146 pipe_column_ids,
147 tags: TableState::new(),
148 tag_visible_column_ids: tag_column_ids.clone(),
149 tag_column_ids,
150 subscriptions: TableState::new(),
151 subscription_visible_column_ids: subscription_column_ids.clone(),
152 subscription_column_ids,
153 subscription_region_filter: String::new(),
154 subscription_region_selected: 0,
155 input_focus: InputFocus::Filter,
156 current_queue: None,
157 detail_tab: QueueDetailTab::QueuePolicies,
158 policy_scroll: 0,
159 policy_document: r#"{
160 "Version": "2012-10-17",
161 "Statement": [
162 {
163 "Effect": "Allow",
164 "Principal": "*",
165 "Action": "sqs:*",
166 "Resource": "*"
167 }
168 ]
169}"#
170 .to_string(),
171 metric_data: Vec::new(),
172 metric_data_delayed: Vec::new(),
173 metric_data_not_visible: Vec::new(),
174 metric_data_visible: Vec::new(),
175 metric_data_empty_receives: Vec::new(),
176 metric_data_messages_deleted: Vec::new(),
177 metric_data_messages_received: Vec::new(),
178 metric_data_messages_sent: Vec::new(),
179 metric_data_sent_message_size: Vec::new(),
180 metrics_loading: false,
181 monitoring_scroll: 0,
182 }
183 }
184}
185
186pub fn filtered_queues<'a>(queues: &'a [Queue], filter: &str) -> Vec<&'a Queue> {
187 queues
188 .iter()
189 .filter(|q| filter.is_empty() || q.name.to_lowercase().starts_with(&filter.to_lowercase()))
190 .collect()
191}
192
193pub fn filtered_lambda_triggers(app: &crate::App) -> Vec<&crate::sqs::LambdaTrigger> {
194 let mut filtered: Vec<_> = app
195 .sqs_state
196 .triggers
197 .items
198 .iter()
199 .filter(|t| {
200 app.sqs_state.triggers.filter.is_empty()
201 || t.uuid
202 .to_lowercase()
203 .contains(&app.sqs_state.triggers.filter.to_lowercase())
204 || t.arn
205 .to_lowercase()
206 .contains(&app.sqs_state.triggers.filter.to_lowercase())
207 })
208 .collect();
209
210 filtered.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
212 filtered
213}
214
215pub fn filtered_tags(app: &crate::App) -> Vec<&QueueTag> {
216 let mut filtered: Vec<_> = app
217 .sqs_state
218 .tags
219 .items
220 .iter()
221 .filter(|t| {
222 app.sqs_state.tags.filter.is_empty()
223 || t.key
224 .to_lowercase()
225 .contains(&app.sqs_state.tags.filter.to_lowercase())
226 || t.value
227 .to_lowercase()
228 .contains(&app.sqs_state.tags.filter.to_lowercase())
229 })
230 .collect();
231
232 filtered.sort_by(|a, b| a.value.cmp(&b.value));
234 filtered
235}
236
237pub fn filtered_subscriptions(app: &crate::App) -> Vec<&SnsSubscription> {
238 let region_filter = if app.sqs_state.subscription_region_filter.is_empty() {
239 &app.region
240 } else {
241 &app.sqs_state.subscription_region_filter
242 };
243
244 let mut filtered: Vec<_> = app
245 .sqs_state
246 .subscriptions
247 .items
248 .iter()
249 .filter(|s| {
250 let text_match = app.sqs_state.subscriptions.filter.is_empty()
251 || s.subscription_arn
252 .to_lowercase()
253 .contains(&app.sqs_state.subscriptions.filter.to_lowercase())
254 || s.topic_arn
255 .to_lowercase()
256 .contains(&app.sqs_state.subscriptions.filter.to_lowercase());
257
258 let region_match = s.subscription_arn.contains(region_filter);
259
260 text_match && region_match
261 })
262 .collect();
263
264 filtered.sort_by(|a, b| a.subscription_arn.cmp(&b.subscription_arn));
266 filtered
267}
268
269pub fn filtered_eventbridge_pipes(app: &crate::App) -> Vec<&crate::sqs::EventBridgePipe> {
270 let mut filtered: Vec<_> = app
271 .sqs_state
272 .pipes
273 .items
274 .iter()
275 .filter(|p| {
276 app.sqs_state.pipes.filter.is_empty()
277 || p.name
278 .to_lowercase()
279 .contains(&app.sqs_state.pipes.filter.to_lowercase())
280 || p.target
281 .to_lowercase()
282 .contains(&app.sqs_state.pipes.filter.to_lowercase())
283 })
284 .collect();
285
286 filtered.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
287 filtered
288}
289
290pub async fn load_sqs_queues(app: &mut crate::App) -> anyhow::Result<()> {
291 let queues = app.sqs_client.list_queues("").await?;
292 app.sqs_state.queues.items = queues
293 .into_iter()
294 .map(|q| Queue {
295 name: q.name,
296 url: q.url,
297 queue_type: q.queue_type,
298 created_timestamp: q.created_timestamp,
299 messages_available: q.messages_available,
300 messages_in_flight: q.messages_in_flight,
301 encryption: q.encryption,
302 content_based_deduplication: q.content_based_deduplication,
303 last_modified_timestamp: q.last_modified_timestamp,
304 visibility_timeout: q.visibility_timeout,
305 message_retention_period: q.message_retention_period,
306 maximum_message_size: q.maximum_message_size,
307 delivery_delay: q.delivery_delay,
308 receive_message_wait_time: q.receive_message_wait_time,
309 high_throughput_fifo: q.high_throughput_fifo,
310 deduplication_scope: q.deduplication_scope,
311 fifo_throughput_limit: q.fifo_throughput_limit,
312 dead_letter_queue: q.dead_letter_queue,
313 messages_delayed: q.messages_delayed,
314 redrive_allow_policy: q.redrive_allow_policy,
315 redrive_policy: q.redrive_policy,
316 redrive_task_id: q.redrive_task_id,
317 redrive_task_start_time: q.redrive_task_start_time,
318 redrive_task_status: q.redrive_task_status,
319 redrive_task_percent: q.redrive_task_percent,
320 redrive_task_destination: q.redrive_task_destination,
321 })
322 .collect();
323 Ok(())
324}
325
326pub async fn load_lambda_triggers(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
327 let queue_arn = app.sqs_client.get_queue_arn(queue_url).await?;
328 let triggers = app.sqs_client.list_lambda_triggers(&queue_arn).await?;
329
330 app.sqs_state.triggers.items = triggers
331 .into_iter()
332 .map(|t| LambdaTrigger {
333 uuid: t.uuid,
334 arn: t.arn,
335 status: t.status,
336 last_modified: t.last_modified,
337 })
338 .collect();
339
340 app.sqs_state
342 .triggers
343 .items
344 .sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
345
346 Ok(())
347}
348
349pub async fn load_metrics(app: &mut crate::App, queue_name: &str) -> anyhow::Result<()> {
350 let metrics = app.sqs_client.get_queue_metrics(queue_name).await?;
351 app.sqs_state.metric_data = metrics;
352
353 let delayed_metrics = app.sqs_client.get_queue_delayed_metrics(queue_name).await?;
354 app.sqs_state.metric_data_delayed = delayed_metrics;
355
356 let not_visible_metrics = app
357 .sqs_client
358 .get_queue_not_visible_metrics(queue_name)
359 .await?;
360 app.sqs_state.metric_data_not_visible = not_visible_metrics;
361
362 let visible_metrics = app.sqs_client.get_queue_visible_metrics(queue_name).await?;
363 app.sqs_state.metric_data_visible = visible_metrics;
364
365 let empty_receives_metrics = app
366 .sqs_client
367 .get_queue_empty_receives_metrics(queue_name)
368 .await?;
369 app.sqs_state.metric_data_empty_receives = empty_receives_metrics;
370
371 let messages_deleted_metrics = app
372 .sqs_client
373 .get_queue_messages_deleted_metrics(queue_name)
374 .await?;
375 app.sqs_state.metric_data_messages_deleted = messages_deleted_metrics;
376
377 let messages_received_metrics = app
378 .sqs_client
379 .get_queue_messages_received_metrics(queue_name)
380 .await?;
381 app.sqs_state.metric_data_messages_received = messages_received_metrics;
382
383 let messages_sent_metrics = app
384 .sqs_client
385 .get_queue_messages_sent_metrics(queue_name)
386 .await?;
387 app.sqs_state.metric_data_messages_sent = messages_sent_metrics;
388
389 let sent_message_size_metrics = app
390 .sqs_client
391 .get_queue_sent_message_size_metrics(queue_name)
392 .await?;
393 app.sqs_state.metric_data_sent_message_size = sent_message_size_metrics;
394
395 Ok(())
396}
397
398pub async fn load_pipes(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
399 let queue_arn = app.sqs_client.get_queue_arn(queue_url).await?;
400 let pipes = app.sqs_client.list_pipes(&queue_arn).await?;
401
402 app.sqs_state.pipes.items = pipes
403 .into_iter()
404 .map(|p| EventBridgePipe {
405 name: p.name,
406 status: p.status,
407 target: p.target,
408 last_modified: p.last_modified,
409 })
410 .collect();
411
412 app.sqs_state
413 .pipes
414 .items
415 .sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
416
417 Ok(())
418}
419
420pub async fn load_tags(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
421 let tags = app.sqs_client.list_tags(queue_url).await?;
422
423 app.sqs_state.tags.items = tags
424 .into_iter()
425 .map(|t| QueueTag {
426 key: t.key,
427 value: t.value,
428 })
429 .collect();
430
431 app.sqs_state
432 .tags
433 .items
434 .sort_by(|a, b| a.value.cmp(&b.value));
435
436 Ok(())
437}
438
439pub fn render_queues(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
440 use ratatui::widgets::Clear;
441
442 frame.render_widget(Clear, area);
443
444 if app.sqs_state.current_queue.is_some() {
445 render_queue_detail(frame, app, area);
446 } else {
447 render_queue_list(frame, app, area);
448 }
449}
450
451fn render_queue_detail(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
452 use ratatui::prelude::*;
453 use ratatui::widgets::{Clear, Paragraph};
454
455 frame.render_widget(Clear, area);
456
457 let queue = app
458 .sqs_state
459 .queues
460 .items
461 .iter()
462 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
463
464 let queue_name = queue.map(|q| q.name.as_str()).unwrap_or("Unknown");
465
466 let details_height = queue.map_or(3, |q| {
467 let field_count = render_details_fields(q).len();
468 field_count as u16 + 2 });
470
471 let chunks = Layout::default()
472 .direction(Direction::Vertical)
473 .constraints([
474 Constraint::Length(1), Constraint::Length(details_height), Constraint::Length(1), Constraint::Min(0), ])
479 .split(area);
480
481 let header = Paragraph::new(queue_name).style(
483 Style::default()
484 .fg(Color::Yellow)
485 .add_modifier(Modifier::BOLD),
486 );
487 frame.render_widget(header, chunks[0]);
488
489 if let Some(q) = queue {
491 render_details_pane(frame, q, chunks[1]);
492 }
493
494 let tabs: Vec<(&str, QueueDetailTab)> = QueueDetailTab::all()
497 .into_iter()
498 .map(|tab| (tab.name(), tab))
499 .collect();
500
501 render_tabs(frame, chunks[2], &tabs, &app.sqs_state.detail_tab);
502
503 match app.sqs_state.detail_tab {
505 QueueDetailTab::QueuePolicies => {
506 render_queue_policies_tab(frame, app, chunks[3]);
507 }
508 QueueDetailTab::Monitoring => {
509 render_monitoring_tab(frame, app, chunks[3]);
510 }
511 QueueDetailTab::SnsSubscriptions => {
512 render_subscriptions_tab(frame, app, chunks[3]);
513 }
514 QueueDetailTab::LambdaTriggers => {
515 render_lambda_triggers_tab(frame, app, chunks[3]);
516 }
517 QueueDetailTab::EventBridgePipes => {
518 render_eventbridge_pipes_tab(frame, app, chunks[3]);
519 }
520 QueueDetailTab::DeadLetterQueue => {
521 render_dead_letter_queue_tab(frame, app, chunks[3]);
522 }
523 QueueDetailTab::Tagging => {
524 render_tags_tab(frame, app, chunks[3]);
525 }
526 QueueDetailTab::Encryption => {
527 render_encryption_tab(frame, app, chunks[3]);
528 }
529 QueueDetailTab::DeadLetterQueueRedriveTasks => {
530 render_dlq_redrive_tasks_tab(frame, app, chunks[3]);
531 }
532 }
533}
534
535fn render_details_fields(queue: &Queue) -> Vec<ratatui::text::Line<'static>> {
536 let max_msg_size = queue
537 .maximum_message_size
538 .split_whitespace()
539 .next()
540 .and_then(|s| s.parse::<i64>().ok())
541 .map(crate::common::format_bytes)
542 .unwrap_or_else(|| queue.maximum_message_size.clone());
543
544 let retention_period = queue
545 .message_retention_period
546 .parse::<i32>()
547 .ok()
548 .map(crate::common::format_duration_seconds)
549 .unwrap_or_else(|| queue.message_retention_period.clone());
550
551 let visibility_timeout = queue
552 .visibility_timeout
553 .parse::<i32>()
554 .ok()
555 .map(crate::common::format_duration_seconds)
556 .unwrap_or_else(|| queue.visibility_timeout.clone());
557
558 let delivery_delay = queue
559 .delivery_delay
560 .parse::<i32>()
561 .ok()
562 .map(crate::common::format_duration_seconds)
563 .unwrap_or_else(|| queue.delivery_delay.clone());
564
565 let receive_wait_time = queue
566 .receive_message_wait_time
567 .parse::<i32>()
568 .ok()
569 .map(crate::common::format_duration_seconds)
570 .unwrap_or_else(|| queue.receive_message_wait_time.clone());
571
572 vec![
573 labeled_field("Name", &queue.name),
574 labeled_field("Type", &queue.queue_type),
575 labeled_field(
576 "ARN",
577 format!(
578 "arn:aws:sqs:{}:{}:{}",
579 extract_region(&queue.url),
580 extract_account_id(&queue.url),
581 queue.name
582 ),
583 ),
584 labeled_field("Encryption", &queue.encryption),
585 labeled_field("URL", &queue.url),
586 labeled_field("Dead-letter queue", &queue.dead_letter_queue),
587 labeled_field(
588 "Created",
589 crate::common::format_unix_timestamp(&queue.created_timestamp),
590 ),
591 labeled_field("Maximum message size", max_msg_size),
592 labeled_field(
593 "Last updated",
594 crate::common::format_unix_timestamp(&queue.last_modified_timestamp),
595 ),
596 labeled_field("Message retention period", retention_period),
597 labeled_field("Default visibility timeout", visibility_timeout),
598 labeled_field("Messages available", &queue.messages_available),
599 labeled_field("Delivery delay", delivery_delay),
600 labeled_field(
601 "Messages in flight (not available to other consumers)",
602 &queue.messages_in_flight,
603 ),
604 labeled_field("Receive message wait time", receive_wait_time),
605 labeled_field("Messages delayed", &queue.messages_delayed),
606 labeled_field(
607 "Content-based deduplication",
608 &queue.content_based_deduplication,
609 ),
610 labeled_field("High throughput FIFO", &queue.high_throughput_fifo),
611 labeled_field("Deduplication scope", &queue.deduplication_scope),
612 labeled_field("FIFO throughput limit", &queue.fifo_throughput_limit),
613 labeled_field("Redrive allow policy", &queue.redrive_allow_policy),
614 ]
615}
616
617fn render_details_pane(frame: &mut ratatui::Frame, queue: &Queue, area: ratatui::prelude::Rect) {
618 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
619
620 let block = Block::default()
621 .title(" Details ")
622 .borders(Borders::ALL)
623 .border_type(BorderType::Rounded)
624 .border_style(crate::ui::active_border());
625
626 let inner = block.inner(area);
627 frame.render_widget(block, area);
628
629 let lines = render_details_fields(queue);
630 let paragraph = Paragraph::new(lines);
631 frame.render_widget(paragraph, inner);
632}
633
634fn render_queue_policies_tab(
635 frame: &mut ratatui::Frame,
636 app: &crate::App,
637 area: ratatui::prelude::Rect,
638) {
639 use ratatui::prelude::{Constraint, Direction, Layout};
640
641 let chunks = Layout::default()
642 .direction(Direction::Vertical)
643 .constraints([Constraint::Min(0)])
644 .split(area);
645
646 crate::ui::render_json_highlighted(
648 frame,
649 chunks[0],
650 &app.sqs_state.policy_document,
651 app.sqs_state.policy_scroll,
652 " Access policy ",
653 );
654}
655
656fn render_monitoring_tab(
657 frame: &mut ratatui::Frame,
658 app: &crate::App,
659 area: ratatui::prelude::Rect,
660) {
661 use ratatui::prelude::*;
662
663 let available_height = area.height as usize;
664
665 let current_page = app.sqs_state.monitoring_scroll;
667
668 if current_page == 0 {
669 let chart1_rect = Rect {
671 x: area.x,
672 y: area.y,
673 width: area.width,
674 height: 20.min(available_height as u16),
675 };
676 render_age_chart(frame, app, chart1_rect);
677
678 let remaining = available_height.saturating_sub(20);
680 if remaining >= 14 {
681 let chart2_height = remaining.min(20);
682 let chart2_rect = Rect {
683 x: area.x,
684 y: area.y + 20,
685 width: area.width,
686 height: chart2_height as u16,
687 };
688 render_delayed_chart(frame, app, chart2_rect);
689 }
690 } else if current_page == 1 {
691 let chart2_rect = Rect {
693 x: area.x,
694 y: area.y,
695 width: area.width,
696 height: 20.min(available_height as u16),
697 };
698 render_delayed_chart(frame, app, chart2_rect);
699
700 let remaining = available_height.saturating_sub(20);
702 if remaining >= 14 {
703 let chart3_height = remaining.min(20);
704 let chart3_rect = Rect {
705 x: area.x,
706 y: area.y + 20,
707 width: area.width,
708 height: chart3_height as u16,
709 };
710 render_not_visible_chart(frame, app, chart3_rect);
711 }
712 } else if current_page == 2 {
713 let chart3_rect = Rect {
715 x: area.x,
716 y: area.y,
717 width: area.width,
718 height: 20.min(available_height as u16),
719 };
720 render_not_visible_chart(frame, app, chart3_rect);
721
722 let remaining = available_height.saturating_sub(20);
724 if remaining >= 14 {
725 let chart4_height = remaining.min(20);
726 let chart4_rect = Rect {
727 x: area.x,
728 y: area.y + 20,
729 width: area.width,
730 height: chart4_height as u16,
731 };
732 render_visible_chart(frame, app, chart4_rect);
733 }
734 } else if current_page == 3 {
735 let chart4_rect = Rect {
737 x: area.x,
738 y: area.y,
739 width: area.width,
740 height: 20.min(available_height as u16),
741 };
742 render_visible_chart(frame, app, chart4_rect);
743
744 let remaining = available_height.saturating_sub(20);
746 if remaining >= 14 {
747 let chart5_height = remaining.min(20);
748 let chart5_rect = Rect {
749 x: area.x,
750 y: area.y + 20,
751 width: area.width,
752 height: chart5_height as u16,
753 };
754 render_empty_receives_chart(frame, app, chart5_rect);
755 }
756 } else if current_page == 4 {
757 let chart5_rect = Rect {
759 x: area.x,
760 y: area.y,
761 width: area.width,
762 height: 20.min(available_height as u16),
763 };
764 render_empty_receives_chart(frame, app, chart5_rect);
765
766 let remaining = available_height.saturating_sub(20);
768 if remaining >= 14 {
769 let chart6_height = remaining.min(20);
770 let chart6_rect = Rect {
771 x: area.x,
772 y: area.y + 20,
773 width: area.width,
774 height: chart6_height as u16,
775 };
776 render_messages_deleted_chart(frame, app, chart6_rect);
777 }
778 } else if current_page == 5 {
779 let chart6_rect = Rect {
781 x: area.x,
782 y: area.y,
783 width: area.width,
784 height: 20.min(available_height as u16),
785 };
786 render_messages_deleted_chart(frame, app, chart6_rect);
787
788 let remaining = available_height.saturating_sub(20);
790 if remaining >= 14 {
791 let chart7_height = remaining.min(20);
792 let chart7_rect = Rect {
793 x: area.x,
794 y: area.y + 20,
795 width: area.width,
796 height: chart7_height as u16,
797 };
798 render_messages_received_chart(frame, app, chart7_rect);
799 }
800 } else if current_page == 6 {
801 let chart7_rect = Rect {
803 x: area.x,
804 y: area.y,
805 width: area.width,
806 height: 20.min(available_height as u16),
807 };
808 render_messages_received_chart(frame, app, chart7_rect);
809
810 let remaining = available_height.saturating_sub(20);
812 if remaining >= 14 {
813 let chart8_height = remaining.min(20);
814 let chart8_rect = Rect {
815 x: area.x,
816 y: area.y + 20,
817 width: area.width,
818 height: chart8_height as u16,
819 };
820 render_messages_sent_chart(frame, app, chart8_rect);
821 }
822 } else if current_page == 7 {
823 let chart8_rect = Rect {
825 x: area.x,
826 y: area.y,
827 width: area.width,
828 height: 20.min(available_height as u16),
829 };
830 render_messages_sent_chart(frame, app, chart8_rect);
831
832 let remaining = available_height.saturating_sub(20);
834 if remaining >= 14 {
835 let chart9_height = remaining.min(20);
836 let chart9_rect = Rect {
837 x: area.x,
838 y: area.y + 20,
839 width: area.width,
840 height: chart9_height as u16,
841 };
842 render_sent_message_size_chart(frame, app, chart9_rect);
843 }
844 } else if current_page == 8 {
845 let chart9_rect = Rect {
847 x: area.x,
848 y: area.y,
849 width: area.width,
850 height: 20.min(available_height as u16),
851 };
852 render_sent_message_size_chart(frame, app, chart9_rect);
853 }
854
855 let total_charts = 9;
857 let total_height = total_charts * 20;
858 let scroll_offset = current_page * 20;
859 render_vertical_scrollbar(frame, area, total_height, scroll_offset);
860}
861
862fn render_age_chart(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
863 use ratatui::prelude::*;
864 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
865
866 let block = Block::default()
867 .title(" Approximate Age Of Oldest Message ")
868 .borders(Borders::ALL)
869 .border_type(BorderType::Rounded)
870 .border_style(Style::default().fg(Color::Gray));
871
872 if app.sqs_state.metric_data.is_empty() {
873 let inner = block.inner(area);
874 frame.render_widget(block, area);
875 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
876 frame.render_widget(paragraph, inner);
877 return;
878 }
879
880 let data: Vec<(f64, f64)> = app
882 .sqs_state
883 .metric_data
884 .iter()
885 .map(|(timestamp, value)| (*timestamp as f64, *value))
886 .collect();
887
888 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
890 let max_x = data
891 .iter()
892 .map(|(x, _)| *x)
893 .fold(f64::NEG_INFINITY, f64::max);
894 let max_y = data
895 .iter()
896 .map(|(_, y)| *y)
897 .fold(0.0_f64, f64::max)
898 .max(1.0); let dataset = Dataset::default()
901 .name("ApproximateAgeOfOldestMessage")
902 .marker(symbols::Marker::Braille)
903 .graph_type(GraphType::Line)
904 .style(Style::default().fg(Color::Cyan))
905 .data(&data);
906
907 let x_labels: Vec<ratatui::text::Span> = {
909 let mut labels = Vec::new();
910 let step = 1800; let mut current = (min_x as i64 / step) * step;
912 while current <= max_x as i64 {
913 let time = chrono::DateTime::from_timestamp(current, 0)
914 .unwrap_or_default()
915 .format("%H:%M")
916 .to_string();
917 labels.push(ratatui::text::Span::raw(time));
918 current += step;
919 }
920 labels
921 };
922
923 let x_axis = Axis::default()
924 .style(Style::default().fg(Color::Gray))
925 .bounds([min_x, max_x])
926 .labels(x_labels);
927
928 let y_labels: Vec<ratatui::text::Span> = {
930 let mut labels = Vec::new();
931 let mut current = 0.0;
932 let step = 0.5;
933 let max = (max_y * 1.1).ceil();
934 while current <= max {
935 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
936 current += step;
937 }
938 labels
939 };
940
941 let y_axis = Axis::default()
942 .title("Seconds")
943 .style(Style::default().fg(Color::Gray))
944 .bounds([0.0, max_y * 1.1])
945 .labels(y_labels);
946
947 let chart = Chart::new(vec![dataset])
948 .block(block)
949 .x_axis(x_axis)
950 .y_axis(y_axis);
951
952 frame.render_widget(chart, area);
953}
954
955fn render_delayed_chart(
956 frame: &mut ratatui::Frame,
957 app: &crate::App,
958 area: ratatui::prelude::Rect,
959) {
960 use ratatui::prelude::*;
961 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
962
963 let block = Block::default()
964 .title(" Approximate Number Of Messages Delayed ")
965 .borders(Borders::ALL)
966 .border_type(BorderType::Rounded)
967 .border_style(Style::default().fg(Color::Gray));
968
969 if app.sqs_state.metric_data_delayed.is_empty() {
970 let inner = block.inner(area);
971 frame.render_widget(block, area);
972 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
973 frame.render_widget(paragraph, inner);
974 return;
975 }
976
977 let data: Vec<(f64, f64)> = app
979 .sqs_state
980 .metric_data_delayed
981 .iter()
982 .map(|(timestamp, value)| (*timestamp as f64, *value))
983 .collect();
984
985 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
987 let max_x = data
988 .iter()
989 .map(|(x, _)| *x)
990 .fold(f64::NEG_INFINITY, f64::max);
991 let max_y = data
992 .iter()
993 .map(|(_, y)| *y)
994 .fold(0.0_f64, f64::max)
995 .max(1.0);
996
997 let dataset = Dataset::default()
998 .name("ApproximateNumberOfMessagesDelayed")
999 .marker(symbols::Marker::Braille)
1000 .graph_type(GraphType::Line)
1001 .style(Style::default().fg(Color::Cyan))
1002 .data(&data);
1003
1004 let x_labels: Vec<ratatui::text::Span> = {
1006 let mut labels = Vec::new();
1007 let step = 1800;
1008 let mut current = (min_x as i64 / step) * step;
1009 while current <= max_x as i64 {
1010 let time = chrono::DateTime::from_timestamp(current, 0)
1011 .unwrap_or_default()
1012 .format("%H:%M")
1013 .to_string();
1014 labels.push(ratatui::text::Span::raw(time));
1015 current += step;
1016 }
1017 labels
1018 };
1019
1020 let x_axis = Axis::default()
1021 .style(Style::default().fg(Color::Gray))
1022 .bounds([min_x, max_x])
1023 .labels(x_labels);
1024
1025 let y_labels: Vec<ratatui::text::Span> = {
1027 let mut labels = Vec::new();
1028 let mut current = 0.0;
1029 let step = 0.5;
1030 let max = (max_y * 1.1).ceil();
1031 while current <= max {
1032 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1033 current += step;
1034 }
1035 labels
1036 };
1037
1038 let y_axis = Axis::default()
1039 .title("Count")
1040 .style(Style::default().fg(Color::Gray))
1041 .bounds([0.0, max_y * 1.1])
1042 .labels(y_labels);
1043
1044 let chart = Chart::new(vec![dataset])
1045 .block(block)
1046 .x_axis(x_axis)
1047 .y_axis(y_axis);
1048
1049 frame.render_widget(chart, area);
1050}
1051
1052fn render_not_visible_chart(
1053 frame: &mut ratatui::Frame,
1054 app: &crate::App,
1055 area: ratatui::prelude::Rect,
1056) {
1057 use ratatui::prelude::*;
1058 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
1059
1060 let block = Block::default()
1061 .title(" Approximate Number Of Messages Not Visible ")
1062 .borders(Borders::ALL)
1063 .border_type(BorderType::Rounded)
1064 .border_style(Style::default().fg(Color::Gray));
1065
1066 if app.sqs_state.metric_data_not_visible.is_empty() {
1067 let inner = block.inner(area);
1068 frame.render_widget(block, area);
1069 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
1070 frame.render_widget(paragraph, inner);
1071 return;
1072 }
1073
1074 let data: Vec<(f64, f64)> = app
1076 .sqs_state
1077 .metric_data_not_visible
1078 .iter()
1079 .map(|(timestamp, value)| (*timestamp as f64, *value))
1080 .collect();
1081
1082 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
1084 let max_x = data
1085 .iter()
1086 .map(|(x, _)| *x)
1087 .fold(f64::NEG_INFINITY, f64::max);
1088 let max_y = data
1089 .iter()
1090 .map(|(_, y)| *y)
1091 .fold(0.0_f64, f64::max)
1092 .max(1.0);
1093
1094 let dataset = Dataset::default()
1095 .name("ApproximateNumberOfMessagesNotVisible")
1096 .marker(symbols::Marker::Braille)
1097 .graph_type(GraphType::Line)
1098 .style(Style::default().fg(Color::Cyan))
1099 .data(&data);
1100
1101 let x_labels: Vec<ratatui::text::Span> = {
1103 let mut labels = Vec::new();
1104 let step = 1800;
1105 let mut current = (min_x as i64 / step) * step;
1106 while current <= max_x as i64 {
1107 let time = chrono::DateTime::from_timestamp(current, 0)
1108 .unwrap_or_default()
1109 .format("%H:%M")
1110 .to_string();
1111 labels.push(ratatui::text::Span::raw(time));
1112 current += step;
1113 }
1114 labels
1115 };
1116
1117 let x_axis = Axis::default()
1118 .style(Style::default().fg(Color::Gray))
1119 .bounds([min_x, max_x])
1120 .labels(x_labels);
1121
1122 let y_labels: Vec<ratatui::text::Span> = {
1124 let mut labels = Vec::new();
1125 let mut current = 0.0;
1126 let step = 0.5;
1127 let max = (max_y * 1.1).ceil();
1128 while current <= max {
1129 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1130 current += step;
1131 }
1132 labels
1133 };
1134
1135 let y_axis = Axis::default()
1136 .title("Count")
1137 .style(Style::default().fg(Color::Gray))
1138 .bounds([0.0, max_y * 1.1])
1139 .labels(y_labels);
1140
1141 let chart = Chart::new(vec![dataset])
1142 .block(block)
1143 .x_axis(x_axis)
1144 .y_axis(y_axis);
1145
1146 frame.render_widget(chart, area);
1147}
1148
1149fn render_visible_chart(
1150 frame: &mut ratatui::Frame,
1151 app: &crate::App,
1152 area: ratatui::prelude::Rect,
1153) {
1154 use ratatui::prelude::*;
1155 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
1156
1157 let block = Block::default()
1158 .title(" Approximate Number Of Messages Visible ")
1159 .borders(Borders::ALL)
1160 .border_type(BorderType::Rounded)
1161 .border_style(Style::default().fg(Color::Gray));
1162
1163 if app.sqs_state.metric_data_visible.is_empty() {
1164 let inner = block.inner(area);
1165 frame.render_widget(block, area);
1166 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
1167 frame.render_widget(paragraph, inner);
1168 return;
1169 }
1170
1171 let data: Vec<(f64, f64)> = app
1172 .sqs_state
1173 .metric_data_visible
1174 .iter()
1175 .map(|(timestamp, value)| (*timestamp as f64, *value))
1176 .collect();
1177
1178 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
1179 let max_x = data
1180 .iter()
1181 .map(|(x, _)| *x)
1182 .fold(f64::NEG_INFINITY, f64::max);
1183 let max_y = data
1184 .iter()
1185 .map(|(_, y)| *y)
1186 .fold(0.0_f64, f64::max)
1187 .max(1.0);
1188
1189 let dataset = Dataset::default()
1190 .name("ApproximateNumberOfMessagesVisible")
1191 .marker(symbols::Marker::Braille)
1192 .graph_type(GraphType::Line)
1193 .style(Style::default().fg(Color::Cyan))
1194 .data(&data);
1195
1196 let x_labels: Vec<ratatui::text::Span> = {
1197 let mut labels = Vec::new();
1198 let step = 1800;
1199 let mut current = (min_x as i64 / step) * step;
1200 while current <= max_x as i64 {
1201 let time = chrono::DateTime::from_timestamp(current, 0)
1202 .unwrap_or_default()
1203 .format("%H:%M")
1204 .to_string();
1205 labels.push(ratatui::text::Span::raw(time));
1206 current += step;
1207 }
1208 labels
1209 };
1210
1211 let x_axis = Axis::default()
1212 .style(Style::default().fg(Color::Gray))
1213 .bounds([min_x, max_x])
1214 .labels(x_labels);
1215
1216 let y_labels: Vec<ratatui::text::Span> = {
1217 let mut labels = Vec::new();
1218 let mut current = 0.0;
1219 let step = 0.5;
1220 let max = (max_y * 1.1).ceil();
1221 while current <= max {
1222 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1223 current += step;
1224 }
1225 labels
1226 };
1227
1228 let y_axis = Axis::default()
1229 .title("Count")
1230 .style(Style::default().fg(Color::Gray))
1231 .bounds([0.0, max_y * 1.1])
1232 .labels(y_labels);
1233
1234 let chart = Chart::new(vec![dataset])
1235 .block(block)
1236 .x_axis(x_axis)
1237 .y_axis(y_axis);
1238
1239 frame.render_widget(chart, area);
1240}
1241
1242fn render_empty_receives_chart(
1243 frame: &mut ratatui::Frame,
1244 app: &crate::App,
1245 area: ratatui::prelude::Rect,
1246) {
1247 use ratatui::prelude::*;
1248 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
1249
1250 let block = Block::default()
1251 .title(" Number Of Empty Receives ")
1252 .borders(Borders::ALL)
1253 .border_type(BorderType::Rounded)
1254 .border_style(Style::default().fg(Color::Gray));
1255
1256 if app.sqs_state.metric_data_empty_receives.is_empty() {
1257 let inner = block.inner(area);
1258 frame.render_widget(block, area);
1259 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
1260 frame.render_widget(paragraph, inner);
1261 return;
1262 }
1263
1264 let data: Vec<(f64, f64)> = app
1265 .sqs_state
1266 .metric_data_empty_receives
1267 .iter()
1268 .map(|(timestamp, value)| (*timestamp as f64, *value))
1269 .collect();
1270
1271 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
1272 let max_x = data
1273 .iter()
1274 .map(|(x, _)| *x)
1275 .fold(f64::NEG_INFINITY, f64::max);
1276 let max_y = data
1277 .iter()
1278 .map(|(_, y)| *y)
1279 .fold(0.0_f64, f64::max)
1280 .max(1.0);
1281
1282 let dataset = Dataset::default()
1283 .name("NumberOfEmptyReceives")
1284 .marker(symbols::Marker::Braille)
1285 .graph_type(GraphType::Line)
1286 .style(Style::default().fg(Color::Cyan))
1287 .data(&data);
1288
1289 let x_labels: Vec<ratatui::text::Span> = {
1290 let mut labels = Vec::new();
1291 let step = 1800;
1292 let mut current = (min_x as i64 / step) * step;
1293 while current <= max_x as i64 {
1294 let time = chrono::DateTime::from_timestamp(current, 0)
1295 .unwrap_or_default()
1296 .format("%H:%M")
1297 .to_string();
1298 labels.push(ratatui::text::Span::raw(time));
1299 current += step;
1300 }
1301 labels
1302 };
1303
1304 let x_axis = Axis::default()
1305 .style(Style::default().fg(Color::Gray))
1306 .bounds([min_x, max_x])
1307 .labels(x_labels);
1308
1309 let y_labels: Vec<ratatui::text::Span> = {
1310 let mut labels = Vec::new();
1311 let mut current = 0.0;
1312 let max = max_y * 1.1;
1313 let step = if max <= 10.0 {
1314 1.0
1315 } else {
1316 (max / 10.0).ceil()
1317 };
1318 while current <= max {
1319 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1320 current += step;
1321 }
1322 labels
1323 };
1324
1325 let y_axis = Axis::default()
1326 .title("Count")
1327 .style(Style::default().fg(Color::Gray))
1328 .bounds([0.0, max_y * 1.1])
1329 .labels(y_labels);
1330
1331 let chart = Chart::new(vec![dataset])
1332 .block(block)
1333 .x_axis(x_axis)
1334 .y_axis(y_axis);
1335
1336 frame.render_widget(chart, area);
1337}
1338
1339fn render_messages_deleted_chart(
1340 frame: &mut ratatui::Frame,
1341 app: &crate::App,
1342 area: ratatui::prelude::Rect,
1343) {
1344 use ratatui::prelude::*;
1345 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
1346
1347 let block = Block::default()
1348 .title(" Number Of Messages Deleted ")
1349 .borders(Borders::ALL)
1350 .border_type(BorderType::Rounded)
1351 .border_style(Style::default().fg(Color::Gray));
1352
1353 if app.sqs_state.metric_data_messages_deleted.is_empty() {
1354 let inner = block.inner(area);
1355 frame.render_widget(block, area);
1356 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
1357 frame.render_widget(paragraph, inner);
1358 return;
1359 }
1360
1361 let data: Vec<(f64, f64)> = app
1362 .sqs_state
1363 .metric_data_messages_deleted
1364 .iter()
1365 .map(|(timestamp, value)| (*timestamp as f64, *value))
1366 .collect();
1367
1368 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
1369 let max_x = data
1370 .iter()
1371 .map(|(x, _)| *x)
1372 .fold(f64::NEG_INFINITY, f64::max);
1373 let max_y = data
1374 .iter()
1375 .map(|(_, y)| *y)
1376 .fold(0.0_f64, f64::max)
1377 .max(1.0);
1378
1379 let dataset = Dataset::default()
1380 .name("NumberOfMessagesDeleted")
1381 .marker(symbols::Marker::Braille)
1382 .graph_type(GraphType::Line)
1383 .style(Style::default().fg(Color::Cyan))
1384 .data(&data);
1385
1386 let x_labels: Vec<ratatui::text::Span> = {
1387 let mut labels = Vec::new();
1388 let step = 1800;
1389 let mut current = (min_x as i64 / step) * step;
1390 while current <= max_x as i64 {
1391 let time = chrono::DateTime::from_timestamp(current, 0)
1392 .unwrap_or_default()
1393 .format("%H:%M")
1394 .to_string();
1395 labels.push(ratatui::text::Span::raw(time));
1396 current += step;
1397 }
1398 labels
1399 };
1400
1401 let x_axis = Axis::default()
1402 .style(Style::default().fg(Color::Gray))
1403 .bounds([min_x, max_x])
1404 .labels(x_labels);
1405
1406 let y_labels: Vec<ratatui::text::Span> = {
1407 let mut labels = Vec::new();
1408 let mut current = 0.0;
1409 let max = max_y * 1.1;
1410 let step = if max <= 10.0 {
1411 1.0
1412 } else {
1413 (max / 10.0).ceil()
1414 };
1415 while current <= max {
1416 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1417 current += step;
1418 }
1419 labels
1420 };
1421
1422 let y_axis = Axis::default()
1423 .title("Count")
1424 .style(Style::default().fg(Color::Gray))
1425 .bounds([0.0, max_y * 1.1])
1426 .labels(y_labels);
1427
1428 let chart = Chart::new(vec![dataset])
1429 .block(block)
1430 .x_axis(x_axis)
1431 .y_axis(y_axis);
1432
1433 frame.render_widget(chart, area);
1434}
1435
1436fn render_messages_received_chart(
1437 frame: &mut ratatui::Frame,
1438 app: &crate::App,
1439 area: ratatui::prelude::Rect,
1440) {
1441 use ratatui::prelude::*;
1442 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
1443
1444 let block = Block::default()
1445 .title(" Number Of Messages Received ")
1446 .borders(Borders::ALL)
1447 .border_type(BorderType::Rounded)
1448 .border_style(Style::default().fg(Color::Gray));
1449
1450 if app.sqs_state.metric_data_messages_received.is_empty() {
1451 let inner = block.inner(area);
1452 frame.render_widget(block, area);
1453 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
1454 frame.render_widget(paragraph, inner);
1455 return;
1456 }
1457
1458 let data: Vec<(f64, f64)> = app
1459 .sqs_state
1460 .metric_data_messages_received
1461 .iter()
1462 .map(|(timestamp, value)| (*timestamp as f64, *value))
1463 .collect();
1464
1465 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
1466 let max_x = data
1467 .iter()
1468 .map(|(x, _)| *x)
1469 .fold(f64::NEG_INFINITY, f64::max);
1470 let max_y = data
1471 .iter()
1472 .map(|(_, y)| *y)
1473 .fold(0.0_f64, f64::max)
1474 .max(1.0);
1475
1476 let dataset = Dataset::default()
1477 .name("NumberOfMessagesReceived")
1478 .marker(symbols::Marker::Braille)
1479 .graph_type(GraphType::Line)
1480 .style(Style::default().fg(Color::Cyan))
1481 .data(&data);
1482
1483 let x_labels: Vec<ratatui::text::Span> = {
1484 let mut labels = Vec::new();
1485 let step = 1800;
1486 let mut current = (min_x as i64 / step) * step;
1487 while current <= max_x as i64 {
1488 let time = chrono::DateTime::from_timestamp(current, 0)
1489 .unwrap_or_default()
1490 .format("%H:%M")
1491 .to_string();
1492 labels.push(ratatui::text::Span::raw(time));
1493 current += step;
1494 }
1495 labels
1496 };
1497
1498 let x_axis = Axis::default()
1499 .style(Style::default().fg(Color::Gray))
1500 .bounds([min_x, max_x])
1501 .labels(x_labels);
1502
1503 let y_labels: Vec<ratatui::text::Span> = {
1504 let mut labels = Vec::new();
1505 let mut current = 0.0;
1506 let max = max_y * 1.1;
1507 let step = if max <= 10.0 {
1508 1.0
1509 } else {
1510 (max / 10.0).ceil()
1511 };
1512 while current <= max {
1513 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1514 current += step;
1515 }
1516 labels
1517 };
1518
1519 let y_axis = Axis::default()
1520 .title("Count")
1521 .style(Style::default().fg(Color::Gray))
1522 .bounds([0.0, max_y * 1.1])
1523 .labels(y_labels);
1524
1525 let chart = Chart::new(vec![dataset])
1526 .block(block)
1527 .x_axis(x_axis)
1528 .y_axis(y_axis);
1529
1530 frame.render_widget(chart, area);
1531}
1532
1533fn render_messages_sent_chart(
1534 frame: &mut ratatui::Frame,
1535 app: &crate::App,
1536 area: ratatui::prelude::Rect,
1537) {
1538 use ratatui::prelude::*;
1539 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
1540
1541 let block = Block::default()
1542 .title(" Number Of Messages Sent ")
1543 .borders(Borders::ALL)
1544 .border_type(BorderType::Rounded)
1545 .border_style(Style::default().fg(Color::Gray));
1546
1547 if app.sqs_state.metric_data_messages_sent.is_empty() {
1548 let inner = block.inner(area);
1549 frame.render_widget(block, area);
1550 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
1551 frame.render_widget(paragraph, inner);
1552 return;
1553 }
1554
1555 let data: Vec<(f64, f64)> = app
1556 .sqs_state
1557 .metric_data_messages_sent
1558 .iter()
1559 .map(|(timestamp, value)| (*timestamp as f64, *value))
1560 .collect();
1561
1562 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
1563 let max_x = data
1564 .iter()
1565 .map(|(x, _)| *x)
1566 .fold(f64::NEG_INFINITY, f64::max);
1567 let max_y = data
1568 .iter()
1569 .map(|(_, y)| *y)
1570 .fold(0.0_f64, f64::max)
1571 .max(1.0);
1572
1573 let dataset = Dataset::default()
1574 .name("NumberOfMessagesSent")
1575 .marker(symbols::Marker::Braille)
1576 .graph_type(GraphType::Line)
1577 .style(Style::default().fg(Color::Cyan))
1578 .data(&data);
1579
1580 let x_labels: Vec<ratatui::text::Span> = {
1581 let mut labels = Vec::new();
1582 let step = 1800;
1583 let mut current = (min_x as i64 / step) * step;
1584 while current <= max_x as i64 {
1585 let time = chrono::DateTime::from_timestamp(current, 0)
1586 .unwrap_or_default()
1587 .format("%H:%M")
1588 .to_string();
1589 labels.push(ratatui::text::Span::raw(time));
1590 current += step;
1591 }
1592 labels
1593 };
1594
1595 let x_axis = Axis::default()
1596 .style(Style::default().fg(Color::Gray))
1597 .bounds([min_x, max_x])
1598 .labels(x_labels);
1599
1600 let y_labels: Vec<ratatui::text::Span> = {
1601 let mut labels = Vec::new();
1602 let mut current = 0.0;
1603 let max = max_y * 1.1;
1604 let step = if max <= 10.0 {
1605 1.0
1606 } else {
1607 (max / 10.0).ceil()
1608 };
1609 while current <= max {
1610 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1611 current += step;
1612 }
1613 labels
1614 };
1615
1616 let y_axis = Axis::default()
1617 .title("Count")
1618 .style(Style::default().fg(Color::Gray))
1619 .bounds([0.0, max_y * 1.1])
1620 .labels(y_labels);
1621
1622 let chart = Chart::new(vec![dataset])
1623 .block(block)
1624 .x_axis(x_axis)
1625 .y_axis(y_axis);
1626
1627 frame.render_widget(chart, area);
1628}
1629
1630fn render_sent_message_size_chart(
1631 frame: &mut ratatui::Frame,
1632 app: &crate::App,
1633 area: ratatui::prelude::Rect,
1634) {
1635 use ratatui::prelude::*;
1636 use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};
1637
1638 let block = Block::default()
1639 .title(" Sent Message Size ")
1640 .borders(Borders::ALL)
1641 .border_type(BorderType::Rounded)
1642 .border_style(Style::default().fg(Color::Gray));
1643
1644 if app.sqs_state.metric_data_sent_message_size.is_empty() {
1645 let inner = block.inner(area);
1646 frame.render_widget(block, area);
1647 let paragraph = ratatui::widgets::Paragraph::new("No data available.");
1648 frame.render_widget(paragraph, inner);
1649 return;
1650 }
1651
1652 let data: Vec<(f64, f64)> = app
1653 .sqs_state
1654 .metric_data_sent_message_size
1655 .iter()
1656 .map(|(timestamp, value)| (*timestamp as f64, *value))
1657 .collect();
1658
1659 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
1660 let max_x = data
1661 .iter()
1662 .map(|(x, _)| *x)
1663 .fold(f64::NEG_INFINITY, f64::max);
1664 let max_y = data
1665 .iter()
1666 .map(|(_, y)| *y)
1667 .fold(0.0_f64, f64::max)
1668 .max(1.0);
1669
1670 let dataset = Dataset::default()
1671 .name("SentMessageSize")
1672 .marker(symbols::Marker::Braille)
1673 .graph_type(GraphType::Line)
1674 .style(Style::default().fg(Color::Cyan))
1675 .data(&data);
1676
1677 let x_labels: Vec<ratatui::text::Span> = {
1678 let mut labels = Vec::new();
1679 let step = 1800;
1680 let mut current = (min_x as i64 / step) * step;
1681 while current <= max_x as i64 {
1682 let time = chrono::DateTime::from_timestamp(current, 0)
1683 .unwrap_or_default()
1684 .format("%H:%M")
1685 .to_string();
1686 labels.push(ratatui::text::Span::raw(time));
1687 current += step;
1688 }
1689 labels
1690 };
1691
1692 let x_axis = Axis::default()
1693 .style(Style::default().fg(Color::Gray))
1694 .bounds([min_x, max_x])
1695 .labels(x_labels);
1696
1697 let y_labels: Vec<ratatui::text::Span> = {
1698 let mut labels = Vec::new();
1699 let mut current = 0.0;
1700 let max = max_y * 1.1;
1701 let step = if max <= 10.0 {
1702 1.0
1703 } else {
1704 (max / 10.0).ceil()
1705 };
1706 while current <= max {
1707 labels.push(ratatui::text::Span::raw(format!("{:.1}", current)));
1708 current += step;
1709 }
1710 labels
1711 };
1712
1713 let y_axis = Axis::default()
1714 .title("Bytes")
1715 .style(Style::default().fg(Color::Gray))
1716 .bounds([0.0, max_y * 1.1])
1717 .labels(y_labels);
1718
1719 let chart = Chart::new(vec![dataset])
1720 .block(block)
1721 .x_axis(x_axis)
1722 .y_axis(y_axis);
1723
1724 frame.render_widget(chart, area);
1725}
1726
1727fn render_lambda_triggers_tab(
1728 frame: &mut ratatui::Frame,
1729 app: &crate::App,
1730 area: ratatui::prelude::Rect,
1731) {
1732 use crate::ui::table::{render_table, Column, TableConfig};
1733 use ratatui::prelude::*;
1734
1735 let chunks = Layout::default()
1736 .direction(Direction::Vertical)
1737 .constraints([Constraint::Length(3), Constraint::Min(0)])
1738 .split(area);
1739
1740 let filtered = filtered_lambda_triggers(app);
1741
1742 let columns: Vec<Box<dyn Column<crate::sqs::LambdaTrigger>>> = app
1743 .sqs_state
1744 .trigger_visible_column_ids
1745 .iter()
1746 .filter_map(|id| TriggerColumn::from_id(id))
1747 .map(|col| Box::new(col) as Box<dyn Column<crate::sqs::LambdaTrigger>>)
1748 .collect();
1749
1750 let page_size = app.sqs_state.triggers.page_size.value();
1752 let total_pages = filtered.len().div_ceil(page_size.max(1));
1753 let current_page = app.sqs_state.triggers.selected / page_size.max(1);
1754 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1755
1756 render_simple_filter(
1758 frame,
1759 chunks[0],
1760 SimpleFilterConfig {
1761 filter_text: &app.sqs_state.triggers.filter,
1762 placeholder: "Search triggers",
1763 pagination: &pagination,
1764 mode: app.mode,
1765 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1766 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1767 },
1768 );
1769
1770 let start_idx = current_page * page_size;
1771 let end_idx = (start_idx + page_size).min(filtered.len());
1772 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1773
1774 let expanded_index = app.sqs_state.triggers.expanded_item.and_then(|idx| {
1775 if idx >= start_idx && idx < end_idx {
1776 Some(idx - start_idx)
1777 } else {
1778 None
1779 }
1780 });
1781
1782 render_table(
1783 frame,
1784 TableConfig {
1785 area: chunks[1],
1786 columns: &columns,
1787 items: paginated,
1788 selected_index: app.sqs_state.triggers.selected % page_size.max(1),
1789 is_active: app.mode != crate::keymap::Mode::FilterInput,
1790 title: format!(" Lambda triggers ({}) ", filtered.len()),
1791 sort_column: "last_modified",
1792 sort_direction: crate::common::SortDirection::Asc,
1793 expanded_index,
1794 get_expanded_content: Some(Box::new(|trigger: &crate::sqs::LambdaTrigger| {
1795 crate::ui::table::expanded_from_columns(&columns, trigger)
1796 })),
1797 },
1798 );
1799}
1800
1801pub fn extract_region(url: &str) -> &str {
1802 url.split("sqs.")
1803 .nth(1)
1804 .and_then(|s| s.split('.').next())
1805 .unwrap_or("unknown")
1806}
1807
1808pub fn extract_account_id(url: &str) -> &str {
1809 url.split('/').nth(3).unwrap_or("unknown")
1810}
1811
1812fn render_eventbridge_pipes_tab(
1813 frame: &mut ratatui::Frame,
1814 app: &crate::App,
1815 area: ratatui::prelude::Rect,
1816) {
1817 use crate::ui::table::{render_table, Column, TableConfig};
1818 use ratatui::prelude::*;
1819
1820 let chunks = Layout::default()
1821 .direction(Direction::Vertical)
1822 .constraints([Constraint::Length(3), Constraint::Min(0)])
1823 .split(area);
1824
1825 let filtered = filtered_eventbridge_pipes(app);
1826
1827 let columns: Vec<Box<dyn Column<crate::sqs::EventBridgePipe>>> = app
1828 .sqs_state
1829 .pipe_visible_column_ids
1830 .iter()
1831 .filter_map(|id| PipeColumn::from_id(id))
1832 .map(|col| Box::new(col) as Box<dyn Column<crate::sqs::EventBridgePipe>>)
1833 .collect();
1834
1835 let page_size = app.sqs_state.pipes.page_size.value();
1836 let total_pages = filtered.len().div_ceil(page_size.max(1));
1837 let current_page = app.sqs_state.pipes.selected / page_size.max(1);
1838 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1839
1840 render_simple_filter(
1841 frame,
1842 chunks[0],
1843 SimpleFilterConfig {
1844 filter_text: &app.sqs_state.pipes.filter,
1845 placeholder: "Search pipes",
1846 pagination: &pagination,
1847 mode: app.mode,
1848 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1849 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1850 },
1851 );
1852
1853 let start_idx = current_page * page_size;
1854 let end_idx = (start_idx + page_size).min(filtered.len());
1855 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1856
1857 let expanded_index = app.sqs_state.pipes.expanded_item.and_then(|idx| {
1858 if idx >= start_idx && idx < end_idx {
1859 Some(idx - start_idx)
1860 } else {
1861 None
1862 }
1863 });
1864
1865 render_table(
1866 frame,
1867 TableConfig {
1868 area: chunks[1],
1869 columns: &columns,
1870 items: paginated,
1871 selected_index: app.sqs_state.pipes.selected % page_size.max(1),
1872 is_active: app.mode != crate::keymap::Mode::FilterInput,
1873 title: format!(" EventBridge Pipes ({}) ", filtered.len()),
1874 sort_column: "last_modified",
1875 sort_direction: crate::common::SortDirection::Asc,
1876 expanded_index,
1877 get_expanded_content: Some(Box::new(|pipe: &crate::sqs::EventBridgePipe| {
1878 crate::ui::table::expanded_from_columns(&columns, pipe)
1879 })),
1880 },
1881 );
1882}
1883
1884fn render_tags_tab(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
1885 use crate::ui::table::{render_table, Column, TableConfig};
1886 use ratatui::prelude::*;
1887
1888 let chunks = Layout::default()
1889 .direction(Direction::Vertical)
1890 .constraints([Constraint::Length(3), Constraint::Min(0)])
1891 .split(area);
1892
1893 let filtered = filtered_tags(app);
1894
1895 let columns: Vec<Box<dyn Column<QueueTag>>> = app
1896 .sqs_state
1897 .tag_visible_column_ids
1898 .iter()
1899 .filter_map(|id| TagColumn::from_id(id))
1900 .map(|col| Box::new(col) as Box<dyn Column<QueueTag>>)
1901 .collect();
1902
1903 let page_size = app.sqs_state.tags.page_size.value();
1904 let total_pages = filtered.len().div_ceil(page_size.max(1));
1905 let current_page = app.sqs_state.tags.selected / page_size.max(1);
1906 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1907
1908 render_simple_filter(
1909 frame,
1910 chunks[0],
1911 SimpleFilterConfig {
1912 filter_text: &app.sqs_state.tags.filter,
1913 placeholder: "Search tags",
1914 pagination: &pagination,
1915 mode: app.mode,
1916 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1917 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1918 },
1919 );
1920
1921 let start_idx = current_page * page_size;
1922 let end_idx = (start_idx + page_size).min(filtered.len());
1923 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1924
1925 let expanded_index = app.sqs_state.tags.expanded_item.and_then(|idx| {
1926 if idx >= start_idx && idx < end_idx {
1927 Some(idx - start_idx)
1928 } else {
1929 None
1930 }
1931 });
1932
1933 render_table(
1934 frame,
1935 TableConfig {
1936 area: chunks[1],
1937 columns: &columns,
1938 items: paginated,
1939 selected_index: app.sqs_state.tags.selected % page_size.max(1),
1940 is_active: app.mode != crate::keymap::Mode::FilterInput,
1941 title: format!(" Tagging ({}) ", filtered.len()),
1942 sort_column: "value",
1943 sort_direction: crate::common::SortDirection::Asc,
1944 expanded_index,
1945 get_expanded_content: Some(Box::new(|tag: &QueueTag| {
1946 crate::ui::table::expanded_from_columns(&columns, tag)
1947 })),
1948 },
1949 );
1950}
1951
1952fn render_subscriptions_tab(
1953 frame: &mut ratatui::Frame,
1954 app: &crate::App,
1955 area: ratatui::prelude::Rect,
1956) {
1957 use crate::ui::table::{render_table, Column, TableConfig};
1958 use ratatui::prelude::*;
1959
1960 let chunks = Layout::default()
1961 .direction(Direction::Vertical)
1962 .constraints([Constraint::Length(3), Constraint::Min(0)])
1963 .split(area);
1964
1965 let filtered = filtered_subscriptions(app);
1966
1967 let columns: Vec<Box<dyn Column<SnsSubscription>>> = app
1968 .sqs_state
1969 .subscription_visible_column_ids
1970 .iter()
1971 .filter_map(|id| SubscriptionColumn::from_id(id))
1972 .map(|col| Box::new(col) as Box<dyn Column<SnsSubscription>>)
1973 .collect();
1974
1975 let page_size = app.sqs_state.subscriptions.page_size.value();
1976 let total_pages = filtered.len().div_ceil(page_size.max(1));
1977 let current_page = app.sqs_state.subscriptions.selected / page_size.max(1);
1978 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1979
1980 render_subscription_filter(frame, app, chunks[0], &pagination);
1982
1983 let start_idx = current_page * page_size;
1984 let end_idx = (start_idx + page_size).min(filtered.len());
1985 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1986
1987 let expanded_index = app.sqs_state.subscriptions.expanded_item.and_then(|idx| {
1988 if idx >= start_idx && idx < end_idx {
1989 Some(idx - start_idx)
1990 } else {
1991 None
1992 }
1993 });
1994
1995 render_table(
1996 frame,
1997 TableConfig {
1998 area: chunks[1],
1999 columns: &columns,
2000 items: paginated,
2001 selected_index: app.sqs_state.subscriptions.selected % page_size.max(1),
2002 is_active: app.mode != crate::keymap::Mode::FilterInput,
2003 title: format!(" SNS subscriptions ({}) ", filtered.len()),
2004 sort_column: "subscription_arn",
2005 sort_direction: crate::common::SortDirection::Asc,
2006 expanded_index,
2007 get_expanded_content: Some(Box::new(|sub: &SnsSubscription| {
2008 crate::ui::table::expanded_from_columns(&columns, sub)
2009 })),
2010 },
2011 );
2012
2013 if app.mode == FilterInput && app.sqs_state.input_focus == SUBSCRIPTION_REGION {
2015 let regions = Region::all();
2016 let region_codes: Vec<&str> = regions.iter().map(|r| r.code).collect();
2017 render_dropdown(
2018 frame,
2019 ®ion_codes,
2020 app.sqs_state.subscription_region_selected,
2021 chunks[0],
2022 pagination.len() as u16 + 3, );
2024 }
2025}
2026
2027fn render_subscription_filter(
2028 frame: &mut ratatui::Frame,
2029 app: &crate::App,
2030 area: ratatui::prelude::Rect,
2031 pagination: &str,
2032) {
2033 let region_text = if app.sqs_state.subscription_region_filter.is_empty() {
2034 format!("Subscription region: {}", app.region)
2035 } else {
2036 format!(
2037 "Subscription region: {}",
2038 app.sqs_state.subscription_region_filter
2039 )
2040 };
2041
2042 render_filter_bar(
2043 frame,
2044 FilterConfig {
2045 filter_text: &app.sqs_state.subscriptions.filter,
2046 placeholder: "Search subscriptions",
2047 mode: app.mode,
2048 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
2049 controls: vec![
2050 FilterControl {
2051 text: region_text,
2052 is_focused: app.sqs_state.input_focus == SUBSCRIPTION_REGION,
2053 },
2054 FilterControl {
2055 text: pagination.to_string(),
2056 is_focused: app.sqs_state.input_focus == InputFocus::Pagination,
2057 },
2058 ],
2059 area,
2060 },
2061 );
2062}
2063
2064fn render_dead_letter_queue_tab(
2065 frame: &mut ratatui::Frame,
2066 app: &crate::App,
2067 area: ratatui::prelude::Rect,
2068) {
2069 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
2070
2071 let queue = app
2072 .sqs_state
2073 .queues
2074 .items
2075 .iter()
2076 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
2077
2078 let block = Block::default()
2079 .title(" Dead-letter queue ")
2080 .borders(Borders::ALL)
2081 .border_type(BorderType::Rounded)
2082 .border_style(crate::ui::active_border());
2083
2084 let inner = block.inner(area);
2085 frame.render_widget(block, area);
2086
2087 if let Some(q) = queue {
2088 if !q.redrive_policy.is_empty() {
2089 if let Ok(policy) = serde_json::from_str::<serde_json::Value>(&q.redrive_policy) {
2091 let dlq_arn = policy
2092 .get("deadLetterTargetArn")
2093 .and_then(|v| v.as_str())
2094 .unwrap_or("-");
2095 let max_receives = policy
2096 .get("maxReceiveCount")
2097 .and_then(|v| v.as_i64())
2098 .map(|n| n.to_string())
2099 .unwrap_or_else(|| "-".to_string());
2100
2101 let lines = vec![
2102 labeled_field("Queue", dlq_arn),
2103 labeled_field("Maximum receives", &max_receives),
2104 ];
2105
2106 let paragraph = Paragraph::new(lines);
2107 frame.render_widget(paragraph, inner);
2108 } else {
2109 let paragraph = Paragraph::new("No dead-letter queue configured");
2110 frame.render_widget(paragraph, inner);
2111 }
2112 } else {
2113 let paragraph = Paragraph::new("No dead-letter queue configured");
2114 frame.render_widget(paragraph, inner);
2115 }
2116 }
2117}
2118
2119fn render_encryption_tab(
2120 frame: &mut ratatui::Frame,
2121 app: &crate::App,
2122 area: ratatui::prelude::Rect,
2123) {
2124 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
2125
2126 let queue = app
2127 .sqs_state
2128 .queues
2129 .items
2130 .iter()
2131 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
2132
2133 let block = Block::default()
2134 .title(" Encryption ")
2135 .borders(Borders::ALL)
2136 .border_type(BorderType::Rounded)
2137 .border_style(crate::ui::active_border());
2138
2139 let inner = block.inner(area);
2140 frame.render_widget(block, area);
2141
2142 if let Some(q) = queue {
2143 let encryption_text = if q.encryption.is_empty() || q.encryption == "-" {
2144 "Server-side encryption is not enabled".to_string()
2145 } else {
2146 format!("Server-side encryption is managed by {}", q.encryption)
2147 };
2148
2149 let paragraph = Paragraph::new(encryption_text);
2150 frame.render_widget(paragraph, inner);
2151 }
2152}
2153
2154fn render_dlq_redrive_tasks_tab(
2155 frame: &mut ratatui::Frame,
2156 app: &crate::App,
2157 area: ratatui::prelude::Rect,
2158) {
2159 use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
2160
2161 let queue = app
2162 .sqs_state
2163 .queues
2164 .items
2165 .iter()
2166 .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
2167
2168 let block = Block::default()
2169 .title(" Dead-letter queue redrive status ")
2170 .borders(Borders::ALL)
2171 .border_type(BorderType::Rounded)
2172 .border_style(crate::ui::active_border());
2173
2174 let inner = block.inner(area);
2175 frame.render_widget(block, area);
2176
2177 if let Some(q) = queue {
2178 let lines = vec![
2179 labeled_field("Name", &q.redrive_task_id),
2180 labeled_field("Date started", &q.redrive_task_start_time),
2181 labeled_field("Percent processed", &q.redrive_task_percent),
2182 labeled_field("Status", &q.redrive_task_status),
2183 labeled_field("Redrive destination", &q.redrive_task_destination),
2184 ];
2185
2186 let paragraph = Paragraph::new(lines);
2187 frame.render_widget(paragraph, inner);
2188 }
2189}
2190
2191fn render_queue_list(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
2192 use crate::common::SortDirection;
2193 use crate::keymap::Mode;
2194 use ratatui::prelude::*;
2195 use ratatui::widgets::Clear;
2196
2197 frame.render_widget(Clear, area);
2198
2199 let chunks = Layout::default()
2200 .direction(Direction::Vertical)
2201 .constraints([
2202 Constraint::Length(3), Constraint::Min(0), ])
2205 .split(area);
2206
2207 let filtered_count =
2208 filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter).len();
2209 let page_size = app.sqs_state.queues.page_size.value();
2210 let total_pages = filtered_count.div_ceil(page_size);
2211 let current_page = app.sqs_state.queues.selected / page_size;
2212 let pagination = crate::ui::render_pagination_text(current_page, total_pages);
2213
2214 render_simple_filter(
2215 frame,
2216 chunks[0],
2217 SimpleFilterConfig {
2218 filter_text: &app.sqs_state.queues.filter,
2219 placeholder: "Search by queue name prefix",
2220 pagination: &pagination,
2221 mode: app.mode,
2222 is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
2223 is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
2224 },
2225 );
2226
2227 let filtered: Vec<_> =
2228 filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter);
2229
2230 let start_idx = current_page * page_size;
2231 let end_idx = (start_idx + page_size).min(filtered.len());
2232 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
2233
2234 let title = format!(" Queues ({}) ", filtered.len());
2235
2236 let columns: Vec<Box<dyn crate::ui::table::Column<Queue>>> = app
2237 .sqs_visible_column_ids
2238 .iter()
2239 .filter_map(|col_id| {
2240 SqsColumn::from_id(col_id)
2241 .map(|col| Box::new(col) as Box<dyn crate::ui::table::Column<Queue>>)
2242 })
2243 .collect();
2244
2245 let expanded_index = app.sqs_state.queues.expanded_item.and_then(|idx| {
2246 if idx >= start_idx && idx < end_idx {
2247 Some(idx - start_idx)
2248 } else {
2249 None
2250 }
2251 });
2252
2253 let config = crate::ui::table::TableConfig {
2254 items: paginated,
2255 selected_index: app.sqs_state.queues.selected % page_size,
2256 expanded_index,
2257 columns: &columns,
2258 sort_column: "Name",
2259 sort_direction: SortDirection::Asc,
2260 title,
2261 area: chunks[1],
2262 get_expanded_content: Some(Box::new(|queue: &Queue| {
2263 crate::ui::table::expanded_from_columns(&columns, queue)
2264 })),
2265 is_active: app.mode != Mode::FilterInput,
2266 };
2267
2268 crate::ui::table::render_table(frame, config);
2269}
2270
2271#[cfg(test)]
2272mod tests {
2273 use super::*;
2274 use crate::common::CyclicEnum;
2275
2276 #[test]
2277 fn test_sqs_state_initialization() {
2278 let state = State::new();
2279 assert_eq!(state.queues.items.len(), 0);
2280 assert_eq!(state.queues.selected, 0);
2281 assert_eq!(state.queues.filter, "");
2282 assert_eq!(state.queues.page_size.value(), 50);
2283 assert_eq!(state.input_focus, InputFocus::Filter);
2284 }
2285
2286 #[test]
2287 fn test_filtered_queues_empty_filter() {
2288 let queues = vec![
2289 Queue {
2290 name: "queue1".to_string(),
2291 url: String::new(),
2292 queue_type: "Standard".to_string(),
2293 created_timestamp: String::new(),
2294 messages_available: "0".to_string(),
2295 messages_in_flight: "0".to_string(),
2296 encryption: "Disabled".to_string(),
2297 content_based_deduplication: "Disabled".to_string(),
2298 last_modified_timestamp: String::new(),
2299 visibility_timeout: String::new(),
2300 message_retention_period: String::new(),
2301 maximum_message_size: String::new(),
2302 delivery_delay: String::new(),
2303 receive_message_wait_time: String::new(),
2304 high_throughput_fifo: "N/A".to_string(),
2305 deduplication_scope: "N/A".to_string(),
2306 fifo_throughput_limit: "N/A".to_string(),
2307 dead_letter_queue: "-".to_string(),
2308 messages_delayed: "0".to_string(),
2309 redrive_allow_policy: "-".to_string(),
2310 redrive_policy: "".to_string(),
2311 redrive_task_id: "-".to_string(),
2312 redrive_task_start_time: "-".to_string(),
2313 redrive_task_status: "-".to_string(),
2314 redrive_task_percent: "-".to_string(),
2315 redrive_task_destination: "-".to_string(),
2316 },
2317 Queue {
2318 name: "queue2".to_string(),
2319 url: String::new(),
2320 queue_type: "Standard".to_string(),
2321 created_timestamp: String::new(),
2322 messages_available: "0".to_string(),
2323 messages_in_flight: "0".to_string(),
2324 encryption: "Disabled".to_string(),
2325 content_based_deduplication: "Disabled".to_string(),
2326 last_modified_timestamp: String::new(),
2327 visibility_timeout: String::new(),
2328 message_retention_period: String::new(),
2329 maximum_message_size: String::new(),
2330 delivery_delay: String::new(),
2331 receive_message_wait_time: String::new(),
2332 high_throughput_fifo: "N/A".to_string(),
2333 deduplication_scope: "N/A".to_string(),
2334 fifo_throughput_limit: "N/A".to_string(),
2335 dead_letter_queue: "-".to_string(),
2336 messages_delayed: "0".to_string(),
2337 redrive_allow_policy: "-".to_string(),
2338 redrive_policy: "".to_string(),
2339 redrive_task_id: "-".to_string(),
2340 redrive_task_start_time: "-".to_string(),
2341 redrive_task_status: "-".to_string(),
2342 redrive_task_percent: "-".to_string(),
2343 redrive_task_destination: "-".to_string(),
2344 },
2345 ];
2346
2347 let filtered = filtered_queues(&queues, "");
2348 assert_eq!(filtered.len(), 2);
2349 }
2350
2351 #[test]
2352 fn test_filtered_queues_with_prefix() {
2353 let queues = vec![
2354 Queue {
2355 name: "prod-orders".to_string(),
2356 url: String::new(),
2357 queue_type: "Standard".to_string(),
2358 created_timestamp: String::new(),
2359 messages_available: "0".to_string(),
2360 messages_in_flight: "0".to_string(),
2361 encryption: "Disabled".to_string(),
2362 content_based_deduplication: "Disabled".to_string(),
2363 last_modified_timestamp: String::new(),
2364 visibility_timeout: String::new(),
2365 message_retention_period: String::new(),
2366 maximum_message_size: String::new(),
2367 delivery_delay: String::new(),
2368 receive_message_wait_time: String::new(),
2369 high_throughput_fifo: "N/A".to_string(),
2370 deduplication_scope: "N/A".to_string(),
2371 fifo_throughput_limit: "N/A".to_string(),
2372 dead_letter_queue: "-".to_string(),
2373 messages_delayed: "0".to_string(),
2374 redrive_allow_policy: "-".to_string(),
2375 redrive_policy: "".to_string(),
2376 redrive_task_id: "-".to_string(),
2377 redrive_task_start_time: "-".to_string(),
2378 redrive_task_status: "-".to_string(),
2379 redrive_task_percent: "-".to_string(),
2380 redrive_task_destination: "-".to_string(),
2381 },
2382 Queue {
2383 name: "dev-orders".to_string(),
2384 url: String::new(),
2385 queue_type: "Standard".to_string(),
2386 created_timestamp: String::new(),
2387 messages_available: "0".to_string(),
2388 messages_in_flight: "0".to_string(),
2389 encryption: "Disabled".to_string(),
2390 content_based_deduplication: "Disabled".to_string(),
2391 last_modified_timestamp: String::new(),
2392 visibility_timeout: String::new(),
2393 message_retention_period: String::new(),
2394 maximum_message_size: String::new(),
2395 delivery_delay: String::new(),
2396 receive_message_wait_time: String::new(),
2397 high_throughput_fifo: "N/A".to_string(),
2398 deduplication_scope: "N/A".to_string(),
2399 fifo_throughput_limit: "N/A".to_string(),
2400 dead_letter_queue: "-".to_string(),
2401 messages_delayed: "0".to_string(),
2402 redrive_allow_policy: "-".to_string(),
2403 redrive_policy: "".to_string(),
2404 redrive_task_id: "-".to_string(),
2405 redrive_task_start_time: "-".to_string(),
2406 redrive_task_status: "-".to_string(),
2407 redrive_task_percent: "-".to_string(),
2408 redrive_task_destination: "-".to_string(),
2409 },
2410 ];
2411
2412 let filtered = filtered_queues(&queues, "prod");
2413 assert_eq!(filtered.len(), 1);
2414 assert_eq!(filtered[0].name, "prod-orders");
2415 }
2416
2417 #[test]
2418 fn test_filtered_queues_case_insensitive() {
2419 let queues = vec![Queue {
2420 name: "MyQueue".to_string(),
2421 url: String::new(),
2422 queue_type: "Standard".to_string(),
2423 created_timestamp: String::new(),
2424 messages_available: "0".to_string(),
2425 messages_in_flight: "0".to_string(),
2426 encryption: "Disabled".to_string(),
2427 content_based_deduplication: "Disabled".to_string(),
2428 last_modified_timestamp: String::new(),
2429 visibility_timeout: String::new(),
2430 message_retention_period: String::new(),
2431 maximum_message_size: String::new(),
2432 delivery_delay: String::new(),
2433 receive_message_wait_time: String::new(),
2434 high_throughput_fifo: "N/A".to_string(),
2435 deduplication_scope: "N/A".to_string(),
2436 fifo_throughput_limit: "N/A".to_string(),
2437 dead_letter_queue: "-".to_string(),
2438 messages_delayed: "0".to_string(),
2439 redrive_allow_policy: "-".to_string(),
2440 redrive_policy: "".to_string(),
2441 redrive_task_id: "-".to_string(),
2442 redrive_task_start_time: "-".to_string(),
2443 redrive_task_status: "-".to_string(),
2444 redrive_task_percent: "-".to_string(),
2445 redrive_task_destination: "-".to_string(),
2446 }];
2447
2448 let filtered = filtered_queues(&queues, "my");
2449 assert_eq!(filtered.len(), 1);
2450
2451 let filtered = filtered_queues(&queues, "MY");
2452 assert_eq!(filtered.len(), 1);
2453 }
2454
2455 #[test]
2456 fn test_pagination_page_size() {
2457 let state = State::new();
2458 assert_eq!(state.queues.page_size.value(), 50);
2459 }
2460
2461 #[test]
2462 fn test_state_initialization_with_policy() {
2463 let state = State::new();
2464 assert_eq!(state.policy_scroll, 0);
2465 assert_eq!(state.current_queue, None);
2466 assert!(state.policy_document.contains("Version"));
2467 assert!(state.policy_document.contains("2012-10-17"));
2468 }
2469
2470 #[test]
2471 fn test_extract_region() {
2472 let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
2473 assert_eq!(extract_region(url), "us-east-1");
2474
2475 let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
2476 assert_eq!(extract_region(url2), "eu-west-2");
2477 }
2478
2479 #[test]
2480 fn test_extract_account_id() {
2481 let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
2482 assert_eq!(extract_account_id(url), "123456789012");
2483
2484 let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
2485 assert_eq!(extract_account_id(url2), "987654321098");
2486 }
2487
2488 #[test]
2489 fn test_timestamp_column_width() {
2490 use crate::sqs::queue::Column;
2491 use crate::ui::table::Column as TableColumn;
2492 assert!(Column::Created.width() >= 27);
2494 assert!(Column::LastUpdated.width() >= 27);
2495 }
2496
2497 #[test]
2498 fn test_message_retention_period_formatting() {
2499 let seconds = 345600;
2501 let formatted = crate::common::format_duration_seconds(seconds);
2502 assert_eq!(formatted, "4d");
2504 }
2505
2506 #[test]
2507 fn test_queue_detail_tab_navigation() {
2508 let tab = QueueDetailTab::QueuePolicies;
2509 assert_eq!(tab.next(), QueueDetailTab::Monitoring);
2510 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
2511
2512 let tab = QueueDetailTab::Monitoring;
2513 assert_eq!(tab.next(), QueueDetailTab::SnsSubscriptions);
2514 assert_eq!(tab.prev(), QueueDetailTab::QueuePolicies);
2515
2516 let tab = QueueDetailTab::SnsSubscriptions;
2517 assert_eq!(tab.next(), QueueDetailTab::LambdaTriggers);
2518 assert_eq!(tab.prev(), QueueDetailTab::Monitoring);
2519
2520 let tab = QueueDetailTab::LambdaTriggers;
2521 assert_eq!(tab.next(), QueueDetailTab::EventBridgePipes);
2522 assert_eq!(tab.prev(), QueueDetailTab::SnsSubscriptions);
2523
2524 let tab = QueueDetailTab::EventBridgePipes;
2525 assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueue);
2526 assert_eq!(tab.prev(), QueueDetailTab::LambdaTriggers);
2527
2528 let tab = QueueDetailTab::DeadLetterQueue;
2529 assert_eq!(tab.next(), QueueDetailTab::Tagging);
2530 assert_eq!(tab.prev(), QueueDetailTab::EventBridgePipes);
2531
2532 let tab = QueueDetailTab::Tagging;
2533 assert_eq!(tab.next(), QueueDetailTab::Encryption);
2534 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueue);
2535
2536 let tab = QueueDetailTab::Encryption;
2537 assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueueRedriveTasks);
2538 assert_eq!(tab.prev(), QueueDetailTab::Tagging);
2539
2540 let tab = QueueDetailTab::DeadLetterQueueRedriveTasks;
2541 assert_eq!(tab.next(), QueueDetailTab::QueuePolicies);
2542 assert_eq!(tab.prev(), QueueDetailTab::Encryption);
2543
2544 let tab = QueueDetailTab::QueuePolicies;
2545 assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
2546 }
2547
2548 #[test]
2549 fn test_queue_detail_tab_all() {
2550 let tabs = QueueDetailTab::all();
2551 assert_eq!(tabs.len(), 9);
2552 assert_eq!(tabs[0], QueueDetailTab::QueuePolicies);
2553 assert_eq!(tabs[1], QueueDetailTab::Monitoring);
2554 assert_eq!(tabs[2], QueueDetailTab::SnsSubscriptions);
2555 assert_eq!(tabs[3], QueueDetailTab::LambdaTriggers);
2556 assert_eq!(tabs[4], QueueDetailTab::EventBridgePipes);
2557 assert_eq!(tabs[5], QueueDetailTab::DeadLetterQueue);
2558 assert_eq!(tabs[6], QueueDetailTab::Tagging);
2559 assert_eq!(tabs[7], QueueDetailTab::Encryption);
2560 assert_eq!(tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
2561 }
2562
2563 #[test]
2564 fn test_queue_detail_tab_names() {
2565 assert_eq!(QueueDetailTab::QueuePolicies.name(), "Queue policies");
2566 assert_eq!(QueueDetailTab::SnsSubscriptions.name(), "SNS subscriptions");
2567 assert_eq!(QueueDetailTab::LambdaTriggers.name(), "Lambda triggers");
2568 assert_eq!(QueueDetailTab::EventBridgePipes.name(), "EventBridge Pipes");
2569 assert_eq!(QueueDetailTab::Tagging.name(), "Tagging");
2570 assert_eq!(QueueDetailTab::DeadLetterQueue.name(), "Dead-letter queue");
2571 }
2572
2573 #[test]
2574 fn test_trigger_column_all() {
2575 use crate::sqs::trigger::Column as TriggerColumn;
2576 assert_eq!(TriggerColumn::all().len(), 4);
2577 }
2578
2579 #[test]
2580 fn test_trigger_column_ids() {
2581 use crate::sqs::trigger::Column as TriggerColumn;
2582 let ids = TriggerColumn::ids();
2583 assert_eq!(ids.len(), 4);
2584 assert!(ids.contains(&"column.sqs.trigger.uuid"));
2585 assert!(ids.contains(&"column.sqs.trigger.arn"));
2586 assert!(ids.contains(&"column.sqs.trigger.status"));
2587 assert!(ids.contains(&"column.sqs.trigger.last_modified"));
2588 }
2589
2590 #[test]
2591 fn test_trigger_column_from_id() {
2592 use crate::sqs::trigger::Column as TriggerColumn;
2593 assert_eq!(
2594 TriggerColumn::from_id("column.sqs.trigger.uuid"),
2595 Some(TriggerColumn::Uuid)
2596 );
2597 assert_eq!(
2598 TriggerColumn::from_id("column.sqs.trigger.arn"),
2599 Some(TriggerColumn::Arn)
2600 );
2601 assert_eq!(
2602 TriggerColumn::from_id("column.sqs.trigger.status"),
2603 Some(TriggerColumn::Status)
2604 );
2605 assert_eq!(
2606 TriggerColumn::from_id("column.sqs.trigger.last_modified"),
2607 Some(TriggerColumn::LastModified)
2608 );
2609 assert_eq!(TriggerColumn::from_id("invalid"), None);
2610 }
2611
2612 #[test]
2613 fn test_trigger_status_rendering() {
2614 use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
2615 use crate::ui::table::Column;
2616
2617 let trigger = LambdaTrigger {
2618 uuid: "test-uuid".to_string(),
2619 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
2620 status: "Enabled".to_string(),
2621 last_modified: "1609459200".to_string(),
2622 };
2623
2624 let (text, style) = TriggerColumn::Status.render(&trigger);
2625 assert_eq!(text, "✅ Enabled");
2626 assert_eq!(style.fg, Some(ratatui::style::Color::Green));
2627 }
2628
2629 #[test]
2630 fn test_trigger_timestamp_rendering() {
2631 use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
2632 use crate::ui::table::Column;
2633
2634 let trigger = LambdaTrigger {
2635 uuid: "test-uuid".to_string(),
2636 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
2637 status: "Enabled".to_string(),
2638 last_modified: "1609459200".to_string(),
2639 };
2640
2641 let (text, _) = TriggerColumn::LastModified.render(&trigger);
2642 assert!(text.contains("2021-01-01"));
2643 assert!(text.contains("(UTC)"));
2644 }
2645
2646 #[test]
2647 fn test_state_initializes_trigger_columns() {
2648 let state = State::new();
2649 assert_eq!(state.trigger_column_ids.len(), 4);
2650 assert_eq!(state.trigger_visible_column_ids.len(), 4);
2651 assert_eq!(state.trigger_column_ids, state.trigger_visible_column_ids);
2652 }
2653
2654 #[test]
2655 fn test_trigger_state_has_filter() {
2656 let mut state = State::new();
2657 state.detail_tab = QueueDetailTab::LambdaTriggers;
2658 state.triggers.filter = "test-filter".to_string();
2659
2660 assert_eq!(state.triggers.filter, "test-filter");
2662 assert_eq!(state.detail_tab, QueueDetailTab::LambdaTriggers);
2663 }
2664
2665 #[test]
2666 fn test_trigger_filtering() {
2667 use crate::sqs::trigger::LambdaTrigger;
2668
2669 let triggers = [
2670 LambdaTrigger {
2671 uuid: "uuid-123".to_string(),
2672 arn: "arn:aws:lambda:us-east-1:123:function:test1".to_string(),
2673 status: "Enabled".to_string(),
2674 last_modified: "1609459200".to_string(),
2675 },
2676 LambdaTrigger {
2677 uuid: "uuid-456".to_string(),
2678 arn: "arn:aws:lambda:us-east-1:123:function:test2".to_string(),
2679 status: "Enabled".to_string(),
2680 last_modified: "1609459200".to_string(),
2681 },
2682 ];
2683
2684 let filtered: Vec<_> = triggers.iter().filter(|t| t.uuid.contains("123")).collect();
2686 assert_eq!(filtered.len(), 1);
2687 assert_eq!(filtered[0].uuid, "uuid-123");
2688
2689 let filtered: Vec<_> = triggers
2691 .iter()
2692 .filter(|t| t.arn.contains("test2"))
2693 .collect();
2694 assert_eq!(filtered.len(), 1);
2695 assert_eq!(
2696 filtered[0].arn,
2697 "arn:aws:lambda:us-east-1:123:function:test2"
2698 );
2699 }
2700
2701 #[test]
2702 fn test_trigger_pagination() {
2703 let mut state = State::new();
2704 state.triggers.items = (0..10)
2705 .map(|i| crate::sqs::LambdaTrigger {
2706 uuid: format!("uuid-{}", i),
2707 arn: format!("arn:aws:lambda:us-east-1:123:function:test{}", i),
2708 status: "Enabled".to_string(),
2709 last_modified: "1609459200".to_string(),
2710 })
2711 .collect();
2712
2713 assert_eq!(state.triggers.items.len(), 10);
2714 assert_eq!(state.triggers.page_size.value(), 50); }
2716
2717 #[test]
2718 fn test_trigger_column_visibility() {
2719 let mut state = State::new();
2720
2721 assert_eq!(state.trigger_visible_column_ids.len(), 4);
2723
2724 state.trigger_visible_column_ids.remove(0);
2726 assert_eq!(state.trigger_visible_column_ids.len(), 3);
2727
2728 state
2730 .trigger_visible_column_ids
2731 .push(state.trigger_column_ids[0].clone());
2732 assert_eq!(state.trigger_visible_column_ids.len(), 4);
2733 }
2734
2735 #[test]
2736 fn test_trigger_page_size_options() {
2737 use crate::common::PageSize;
2738 let mut state = State::new();
2739
2740 assert_eq!(state.triggers.page_size, PageSize::Fifty);
2742
2743 state.triggers.page_size = PageSize::Ten;
2745 assert_eq!(state.triggers.page_size.value(), 10);
2746
2747 state.triggers.page_size = PageSize::TwentyFive;
2748 assert_eq!(state.triggers.page_size.value(), 25);
2749
2750 state.triggers.page_size = PageSize::OneHundred;
2751 assert_eq!(state.triggers.page_size.value(), 100);
2752 }
2753
2754 #[test]
2755 fn test_trigger_loading_state() {
2756 let mut state = State::new();
2757
2758 assert!(!state.triggers.loading);
2760
2761 state.triggers.loading = true;
2763 assert!(state.triggers.loading);
2764
2765 state.triggers.loading = false;
2767 assert!(!state.triggers.loading);
2768 }
2769
2770 #[test]
2771 fn test_trigger_sort_by_last_modified() {
2772 let mut triggers = [
2773 crate::sqs::LambdaTrigger {
2774 uuid: "uuid-2".to_string(),
2775 arn: "arn2".to_string(),
2776 status: "Enabled".to_string(),
2777 last_modified: "1609459300".to_string(), },
2779 crate::sqs::LambdaTrigger {
2780 uuid: "uuid-1".to_string(),
2781 arn: "arn1".to_string(),
2782 status: "Enabled".to_string(),
2783 last_modified: "1609459200".to_string(), },
2785 ];
2786
2787 triggers.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
2789
2790 assert_eq!(triggers[0].uuid, "uuid-1");
2791 assert_eq!(triggers[1].uuid, "uuid-2");
2792 }
2793
2794 #[test]
2795 fn test_trigger_pagination_calculation() {
2796 use crate::common::PageSize;
2797 let mut state = State::new();
2798
2799 state.triggers.items = (0..25)
2801 .map(|i| crate::sqs::LambdaTrigger {
2802 uuid: format!("uuid-{}", i),
2803 arn: format!("arn{}", i),
2804 status: "Enabled".to_string(),
2805 last_modified: "1609459200".to_string(),
2806 })
2807 .collect();
2808
2809 state.triggers.page_size = PageSize::Ten;
2811 let page_size = state.triggers.page_size.value();
2812 let total_pages = state.triggers.items.len().div_ceil(page_size);
2813 assert_eq!(total_pages, 3);
2814
2815 let current_page = 0;
2817 let start_idx = current_page * page_size;
2818 let end_idx = (start_idx + page_size).min(state.triggers.items.len());
2819 assert_eq!(start_idx, 0);
2820 assert_eq!(end_idx, 10);
2821
2822 let current_page = 2;
2824 let start_idx = current_page * page_size;
2825 let end_idx = (start_idx + page_size).min(state.triggers.items.len());
2826 assert_eq!(start_idx, 20);
2827 assert_eq!(end_idx, 25);
2828 }
2829
2830 #[test]
2831 fn test_monitoring_metric_data_with_values() {
2832 let mut state = State::new();
2833 state.metric_data = vec![
2835 (1700000000, 0.0),
2836 (1700000060, 5.0),
2837 (1700000120, 10.0),
2838 (1700000180, 0.0),
2839 ];
2840 assert_eq!(state.metric_data.len(), 4);
2841 assert_eq!(state.metric_data[0], (1700000000, 0.0));
2842 assert_eq!(state.metric_data[1], (1700000060, 5.0));
2843 }
2844
2845 #[test]
2846 fn test_monitoring_all_metrics_initialized() {
2847 let state = State::new();
2848 assert!(state.metric_data.is_empty());
2849 assert!(state.metric_data_delayed.is_empty());
2850 assert!(state.metric_data_not_visible.is_empty());
2851 assert!(state.metric_data_visible.is_empty());
2852 assert!(state.metric_data_empty_receives.is_empty());
2853 assert!(state.metric_data_messages_deleted.is_empty());
2854 assert!(state.metric_data_messages_received.is_empty());
2855 assert!(state.metric_data_messages_sent.is_empty());
2856 assert!(state.metric_data_sent_message_size.is_empty());
2857 assert_eq!(state.monitoring_scroll, 0);
2858 }
2859
2860 #[test]
2861 fn test_monitoring_scroll_pages() {
2862 let mut state = State::new();
2863 assert_eq!(state.monitoring_scroll, 0);
2864
2865 state.monitoring_scroll = 1;
2867 assert_eq!(state.monitoring_scroll, 1);
2868
2869 state.monitoring_scroll = 2;
2871 assert_eq!(state.monitoring_scroll, 2);
2872 }
2873
2874 #[test]
2875 fn test_monitoring_delayed_metrics() {
2876 let mut state = State::new();
2877 state.metric_data_delayed = vec![(1700000000, 1.0), (1700000060, 2.0)];
2878 assert_eq!(state.metric_data_delayed.len(), 2);
2879 assert_eq!(state.metric_data_delayed[0].1, 1.0);
2880 }
2881
2882 #[test]
2883 fn test_monitoring_not_visible_metrics() {
2884 let mut state = State::new();
2885 state.metric_data_not_visible = vec![(1700000000, 3.0), (1700000060, 4.0)];
2886 assert_eq!(state.metric_data_not_visible.len(), 2);
2887 assert_eq!(state.metric_data_not_visible[1].1, 4.0);
2888 }
2889
2890 #[test]
2891 fn test_monitoring_visible_metrics() {
2892 let mut state = State::new();
2893 state.metric_data_visible = vec![(1700000000, 5.0), (1700000060, 6.0)];
2894 assert_eq!(state.metric_data_visible.len(), 2);
2895 assert_eq!(state.metric_data_visible[0].1, 5.0);
2896 }
2897
2898 #[test]
2899 fn test_monitoring_empty_receives_metrics() {
2900 let mut state = State::new();
2901 state.metric_data_empty_receives = vec![(1700000000, 10.0), (1700000060, 15.0)];
2902 assert_eq!(state.metric_data_empty_receives.len(), 2);
2903 assert_eq!(state.metric_data_empty_receives[0].1, 10.0);
2904 }
2905
2906 #[test]
2907 fn test_monitoring_messages_deleted_metrics() {
2908 let mut state = State::new();
2909 state.metric_data_messages_deleted = vec![(1700000000, 20.0), (1700000060, 25.0)];
2910 assert_eq!(state.metric_data_messages_deleted.len(), 2);
2911 assert_eq!(state.metric_data_messages_deleted[0].1, 20.0);
2912 }
2913
2914 #[test]
2915 fn test_monitoring_messages_received_metrics() {
2916 let mut state = State::new();
2917 state.metric_data_messages_received = vec![(1700000000, 30.0), (1700000060, 35.0)];
2918 assert_eq!(state.metric_data_messages_received.len(), 2);
2919 assert_eq!(state.metric_data_messages_received[0].1, 30.0);
2920 }
2921
2922 #[test]
2923 fn test_monitoring_messages_sent_metrics() {
2924 let mut state = State::new();
2925 state.metric_data_messages_sent = vec![(1700000000, 40.0), (1700000060, 45.0)];
2926 assert_eq!(state.metric_data_messages_sent.len(), 2);
2927 assert_eq!(state.metric_data_messages_sent[0].1, 40.0);
2928 }
2929
2930 #[test]
2931 fn test_monitoring_sent_message_size_metrics() {
2932 let mut state = State::new();
2933 state.metric_data_sent_message_size = vec![(1700000000, 1024.0), (1700000060, 2048.0)];
2934 assert_eq!(state.metric_data_sent_message_size.len(), 2);
2935 assert_eq!(state.metric_data_sent_message_size[0].1, 1024.0);
2936 }
2937
2938 #[test]
2939 fn test_trigger_expand_collapse() {
2940 let mut state = State::new();
2941
2942 assert_eq!(state.triggers.expanded_item, None);
2944
2945 state.triggers.expanded_item = Some(0);
2947 assert_eq!(state.triggers.expanded_item, Some(0));
2948
2949 state.triggers.expanded_item = None;
2951 assert_eq!(state.triggers.expanded_item, None);
2952 }
2953
2954 #[test]
2955 fn test_trigger_filter_visibility() {
2956 let mut state = State::new();
2957
2958 assert!(state.triggers.filter.is_empty());
2960
2961 state.triggers.filter = "test".to_string();
2963 assert_eq!(state.triggers.filter, "test");
2964
2965 state.triggers.filter.clear();
2967 assert!(state.triggers.filter.is_empty());
2968 }
2969
2970 #[test]
2971 fn test_pipe_column_ids_have_correct_prefix() {
2972 for col in PipeColumn::all() {
2973 assert!(
2974 col.id().starts_with("column.sqs.pipe."),
2975 "PipeColumn ID '{}' should start with 'column.sqs.pipe.'",
2976 col.id()
2977 );
2978 }
2979 }
2980
2981 #[test]
2982 fn test_tags_sorted_by_value() {
2983 let mut state = State::new();
2984 state.tags.items = vec![
2985 QueueTag {
2986 key: "env".to_string(),
2987 value: "prod".to_string(),
2988 },
2989 QueueTag {
2990 key: "team".to_string(),
2991 value: "backend".to_string(),
2992 },
2993 QueueTag {
2994 key: "app".to_string(),
2995 value: "api".to_string(),
2996 },
2997 ];
2998
2999 let mut sorted = state.tags.items.clone();
3000 sorted.sort_by(|a, b| a.value.cmp(&b.value));
3001
3002 assert_eq!(sorted[0].value, "api");
3003 assert_eq!(sorted[1].value, "backend");
3004 assert_eq!(sorted[2].value, "prod");
3005 }
3006
3007 #[test]
3008 fn test_tags_initialization() {
3009 let state = State::new();
3010 assert_eq!(state.tags.items.len(), 0);
3011 assert_eq!(state.tag_column_ids.len(), 2);
3012 assert_eq!(state.tag_visible_column_ids.len(), 2);
3013 }
3014
3015 #[test]
3016 fn test_queue_tag_structure() {
3017 let tag = QueueTag {
3018 key: "Environment".to_string(),
3019 value: "Production".to_string(),
3020 };
3021 assert_eq!(tag.key, "Environment");
3022 assert_eq!(tag.value, "Production");
3023 }
3024
3025 #[test]
3026 fn test_tags_table_state() {
3027 let mut state = State::new();
3028 state.tags.items = vec![
3029 QueueTag {
3030 key: "Env".to_string(),
3031 value: "prod".to_string(),
3032 },
3033 QueueTag {
3034 key: "Team".to_string(),
3035 value: "backend".to_string(),
3036 },
3037 ];
3038 assert_eq!(state.tags.items.len(), 2);
3039 assert_eq!(state.tags.selected, 0);
3040 assert_eq!(state.tags.filter, "");
3041 }
3042
3043 #[test]
3044 fn test_tags_filtering() {
3045 let tags = [
3046 QueueTag {
3047 key: "Environment".to_string(),
3048 value: "production".to_string(),
3049 },
3050 QueueTag {
3051 key: "Team".to_string(),
3052 value: "backend".to_string(),
3053 },
3054 QueueTag {
3055 key: "Project".to_string(),
3056 value: "api".to_string(),
3057 },
3058 ];
3059
3060 let filtered: Vec<_> = tags
3062 .iter()
3063 .filter(|t| t.key.to_lowercase().contains("env"))
3064 .collect();
3065 assert_eq!(filtered.len(), 1);
3066 assert_eq!(filtered[0].key, "Environment");
3067
3068 let filtered: Vec<_> = tags
3070 .iter()
3071 .filter(|t| t.value.to_lowercase().contains("back"))
3072 .collect();
3073 assert_eq!(filtered.len(), 1);
3074 assert_eq!(filtered[0].value, "backend");
3075 }
3076
3077 #[test]
3078 fn test_tags_column_ids() {
3079 use crate::sqs::tag::Column as TagColumn;
3080 let ids = TagColumn::ids();
3081 assert_eq!(ids.len(), 2);
3082 assert_eq!(ids[0], "column.sqs.tag.key");
3083 assert_eq!(ids[1], "column.sqs.tag.value");
3084 }
3085
3086 #[test]
3087 fn test_tags_column_from_id() {
3088 use crate::sqs::tag::Column as TagColumn;
3089 assert!(TagColumn::from_id("column.sqs.tag.key").is_some());
3090 assert!(TagColumn::from_id("column.sqs.tag.value").is_some());
3091 assert!(TagColumn::from_id("invalid").is_none());
3092 }
3093
3094 #[test]
3095 fn test_subscriptions_initialization() {
3096 let state = State::new();
3097 assert_eq!(state.subscriptions.items.len(), 0);
3098 assert_eq!(state.subscription_column_ids.len(), 2);
3099 assert_eq!(state.subscription_visible_column_ids.len(), 2);
3100 assert_eq!(state.subscription_region_filter, "");
3101 }
3102
3103 #[test]
3104 fn test_subscription_column_ids() {
3105 use crate::sqs::sub::Column as SubscriptionColumn;
3106 let ids = SubscriptionColumn::ids();
3107 assert_eq!(ids.len(), 2);
3108 assert_eq!(ids[0], "column.sqs.subscription.subscription_arn");
3109 assert_eq!(ids[1], "column.sqs.subscription.topic_arn");
3110 }
3111
3112 #[test]
3113 fn test_subscription_column_from_id() {
3114 use crate::sqs::sub::Column as SubscriptionColumn;
3115 assert!(SubscriptionColumn::from_id("column.sqs.subscription.subscription_arn").is_some());
3116 assert!(SubscriptionColumn::from_id("column.sqs.subscription.topic_arn").is_some());
3117 assert!(SubscriptionColumn::from_id("invalid").is_none());
3118 }
3119
3120 #[test]
3121 fn test_subscription_region_filter_default() {
3122 let state = State::new();
3123 assert_eq!(state.subscription_region_filter, "");
3125 }
3126
3127 #[test]
3128 fn test_subscription_region_filter_display() {
3129 let mut state = State::new();
3130
3131 assert_eq!(state.subscription_region_filter, "");
3133
3134 state.subscription_region_filter = "us-west-2".to_string();
3136 assert_eq!(state.subscription_region_filter, "us-west-2");
3137 }
3138
3139 #[test]
3140 fn test_subscription_region_selected_index() {
3141 let state = State::new();
3142 assert_eq!(state.subscription_region_selected, 0);
3143 }
3144
3145 #[test]
3146 fn test_encryption_tab_in_all() {
3147 let tabs = QueueDetailTab::all();
3148 assert!(tabs.contains(&QueueDetailTab::Encryption));
3149 }
3150
3151 #[test]
3152 fn test_encryption_tab_name() {
3153 assert_eq!(QueueDetailTab::Encryption.name(), "Encryption");
3154 }
3155
3156 #[test]
3157 fn test_encryption_tab_order() {
3158 let tabs = QueueDetailTab::all();
3159 let dlq_idx = tabs
3160 .iter()
3161 .position(|t| *t == QueueDetailTab::DeadLetterQueue)
3162 .unwrap();
3163 let tagging_idx = tabs
3164 .iter()
3165 .position(|t| *t == QueueDetailTab::Tagging)
3166 .unwrap();
3167 let encryption_idx = tabs
3168 .iter()
3169 .position(|t| *t == QueueDetailTab::Encryption)
3170 .unwrap();
3171
3172 assert!(dlq_idx < tagging_idx);
3174 assert!(encryption_idx > tagging_idx);
3175 }
3176
3177 #[test]
3178 fn test_dlq_redrive_tasks_tab_in_all() {
3179 let tabs = QueueDetailTab::all();
3180 assert!(tabs.contains(&QueueDetailTab::DeadLetterQueueRedriveTasks));
3181 }
3182
3183 #[test]
3184 fn test_dlq_redrive_tasks_tab_name() {
3185 assert_eq!(
3186 QueueDetailTab::DeadLetterQueueRedriveTasks.name(),
3187 "Dead-letter queue redrive tasks"
3188 );
3189 }
3190
3191 #[test]
3192 fn test_dlq_redrive_tasks_tab_order() {
3193 let tabs = QueueDetailTab::all();
3194 let encryption_idx = tabs
3195 .iter()
3196 .position(|t| *t == QueueDetailTab::Encryption)
3197 .unwrap();
3198 let redrive_idx = tabs
3199 .iter()
3200 .position(|t| *t == QueueDetailTab::DeadLetterQueueRedriveTasks)
3201 .unwrap();
3202
3203 assert!(redrive_idx > encryption_idx);
3205 assert_eq!(redrive_idx, tabs.len() - 1);
3206 }
3207
3208 #[test]
3209 fn test_tab_strip_matches_enum_order() {
3210 let all_tabs = QueueDetailTab::all();
3212 assert_eq!(all_tabs.len(), 9);
3213
3214 assert_eq!(all_tabs[0], QueueDetailTab::QueuePolicies);
3216 assert_eq!(all_tabs[1], QueueDetailTab::Monitoring);
3217 assert_eq!(all_tabs[2], QueueDetailTab::SnsSubscriptions);
3218 assert_eq!(all_tabs[3], QueueDetailTab::LambdaTriggers);
3219 assert_eq!(all_tabs[4], QueueDetailTab::EventBridgePipes);
3220 assert_eq!(all_tabs[5], QueueDetailTab::DeadLetterQueue);
3221 assert_eq!(all_tabs[6], QueueDetailTab::Tagging);
3222 assert_eq!(all_tabs[7], QueueDetailTab::Encryption);
3223 assert_eq!(all_tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
3224 }
3225
3226 #[test]
3227 fn test_monitoring_tab_in_all() {
3228 let all_tabs = QueueDetailTab::all();
3229 assert!(all_tabs.contains(&QueueDetailTab::Monitoring));
3230 }
3231
3232 #[test]
3233 fn test_monitoring_tab_name() {
3234 assert_eq!(QueueDetailTab::Monitoring.name(), "Monitoring");
3235 }
3236
3237 #[test]
3238 fn test_monitoring_tab_order() {
3239 let all_tabs = QueueDetailTab::all();
3240 let monitoring_index = all_tabs
3241 .iter()
3242 .position(|t| *t == QueueDetailTab::Monitoring)
3243 .unwrap();
3244 assert_eq!(monitoring_index, 1); }
3246}