rusticity_term/ui/
sqs.rs

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)>, // (timestamp, value) for ApproximateAgeOfOldestMessage
103    pub metric_data_delayed: Vec<(i64, f64)>, // (timestamp, value) for ApproximateNumberOfMessagesDelayed
104    pub metric_data_not_visible: Vec<(i64, f64)>, // (timestamp, value) for ApproximateNumberOfMessagesNotVisible
105    pub metric_data_visible: Vec<(i64, f64)>, // (timestamp, value) for ApproximateNumberOfMessagesVisible
106    pub metric_data_empty_receives: Vec<(i64, f64)>, // (timestamp, value) for NumberOfEmptyReceives
107    pub metric_data_messages_deleted: Vec<(i64, f64)>, // (timestamp, value) for NumberOfMessagesDeleted
108    pub metric_data_messages_received: Vec<(i64, f64)>, // (timestamp, value) for NumberOfMessagesReceived
109    pub metric_data_messages_sent: Vec<(i64, f64)>, // (timestamp, value) for NumberOfMessagesSent
110    pub metric_data_sent_message_size: Vec<(i64, f64)>, // (timestamp, value) for SentMessageSize
111    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    // Sort by last_modified ASC
211    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    // Sort by value ASC
233    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    // Sort by subscription_arn ASC
265    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    // Sort by last_modified ascending (oldest first)
341    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 // fields + 2 borders
469    });
470
471    let chunks = Layout::default()
472        .direction(Direction::Vertical)
473        .constraints([
474            Constraint::Length(1),              // Queue name
475            Constraint::Length(details_height), // Details (dynamic)
476            Constraint::Length(1),              // Tabs
477            Constraint::Min(0),                 // Tab content
478        ])
479        .split(area);
480
481    // Queue name header
482    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    // Details pane
490    if let Some(q) = queue {
491        render_details_pane(frame, q, chunks[1]);
492    }
493
494    // Tabs
495    // Tabs - generated from QueueDetailTab::all()
496    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    // Tab content
504    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    // Access policy JSON using common JSON renderer
647    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    // Determine which page we're on (0-5)
666    let current_page = app.sqs_state.monitoring_scroll;
667
668    if current_page == 0 {
669        // Page 0: Show first chart, and second chart if 10+ lines remain
670        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        // If 14+ lines remain after first chart, show second chart
679        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        // Page 1: Show second chart, and third chart if 14+ lines remain
692        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        // If 14+ lines remain after second chart, show third chart
701        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        // Page 2: Show third chart, and fourth chart if 14+ lines remain
714        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        // If 14+ lines remain after third chart, show fourth chart
723        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        // Page 3: Show fourth chart, and fifth chart if 14+ lines remain
736        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        // If 14+ lines remain after fourth chart, show fifth chart
745        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        // Page 4: Show fifth chart, and sixth chart if 14+ lines remain
758        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        // If 14+ lines remain after fifth chart, show sixth chart
767        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        // Page 5: Show sixth chart, and seventh chart if 14+ lines remain
780        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        // If 14+ lines remain after sixth chart, show seventh chart
789        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        // Page 6: Show seventh chart, and eighth chart if 14+ lines remain
802        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        // If 14+ lines remain after seventh chart, show eighth chart
811        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        // Page 7: Show eighth chart, and ninth chart if 14+ lines remain
824        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        // If 14+ lines remain after eighth chart, show ninth chart
833        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        // Page 8: Show ninth chart
846        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    // Always show scrollbar
856    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    // Convert metric data to chart points
881    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    // Calculate bounds
889    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); // Use at least 1 for better visualization
899
900    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    // X-axis with HH:MM labels every 30 minutes
908    let x_labels: Vec<ratatui::text::Span> = {
909        let mut labels = Vec::new();
910        let step = 1800; // 30 minutes in seconds
911        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    // Y-axis with 0.5 step labels
929    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    // Convert metric data to chart points
978    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    // Calculate bounds
986    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    // X-axis with HH:MM labels every 30 minutes
1005    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    // Y-axis with 0.5 step labels
1026    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    // Convert metric data to chart points
1075    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    // Calculate bounds
1083    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    // X-axis with HH:MM labels every 30 minutes
1102    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    // Y-axis with 0.5 step labels
1123    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    // Pagination
1751    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    // Filter at top
1757    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 filter with region dropdown
1981    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    // Render region dropdown if focused (after table so it appears on top)
2014    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            &region_codes,
2020            app.sqs_state.subscription_region_selected,
2021            chunks[0],
2022            pagination.len() as u16 + 3, // pagination + separator
2023        );
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            // Parse RedrivePolicy JSON
2090            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), // Filter
2203            Constraint::Min(0),    // Table
2204        ])
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        // Timestamps are 27 characters: "YYYY-MM-DD HH:MM:SS (UTC)"
2493        assert!(Column::Created.width() >= 27);
2494        assert!(Column::LastUpdated.width() >= 27);
2495    }
2496
2497    #[test]
2498    fn test_message_retention_period_formatting() {
2499        // Test that 345600 seconds formats to days
2500        let seconds = 345600;
2501        let formatted = crate::common::format_duration_seconds(seconds);
2502        // 345600 seconds = 4 days
2503        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        // Verify filter is set
2661        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        // Filter by UUID
2685        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        // Filter by ARN
2690        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); // Default page size
2715    }
2716
2717    #[test]
2718    fn test_trigger_column_visibility() {
2719        let mut state = State::new();
2720
2721        // All columns visible by default
2722        assert_eq!(state.trigger_visible_column_ids.len(), 4);
2723
2724        // Remove a column
2725        state.trigger_visible_column_ids.remove(0);
2726        assert_eq!(state.trigger_visible_column_ids.len(), 3);
2727
2728        // Add it back
2729        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        // Default is 50
2741        assert_eq!(state.triggers.page_size, PageSize::Fifty);
2742
2743        // Can change to other sizes
2744        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        // Initially not loading
2759        assert!(!state.triggers.loading);
2760
2761        // Can set to loading
2762        state.triggers.loading = true;
2763        assert!(state.triggers.loading);
2764
2765        // Can clear loading
2766        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(), // Later
2778            },
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(), // Earlier
2784            },
2785        ];
2786
2787        // Sort ascending (oldest first)
2788        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        // Add 25 triggers
2800        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        // With page size 10, should have 3 pages
2810        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        // Page 0: items 0-9
2816        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        // Page 2: items 20-24
2823        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        // Mock obfuscated metric data
2834        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        // Scroll to page 1
2866        state.monitoring_scroll = 1;
2867        assert_eq!(state.monitoring_scroll, 1);
2868
2869        // Scroll to page 2
2870        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        // Initially no item expanded
2943        assert_eq!(state.triggers.expanded_item, None);
2944
2945        // Expand item 0
2946        state.triggers.expanded_item = Some(0);
2947        assert_eq!(state.triggers.expanded_item, Some(0));
2948
2949        // Collapse (set to None)
2950        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        // Filter starts empty
2959        assert!(state.triggers.filter.is_empty());
2960
2961        // Can set filter
2962        state.triggers.filter = "test".to_string();
2963        assert_eq!(state.triggers.filter, "test");
2964
2965        // Can clear filter
2966        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        // Test filtering by key
3061        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        // Test filtering by value
3069        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        // Default is empty string, which means use current region
3124        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        // When empty, should show current region
3132        assert_eq!(state.subscription_region_filter, "");
3133
3134        // When set, should show selected region
3135        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        // Encryption should be after Tagging and DeadLetterQueue should be before Tagging
3173        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        // DeadLetterQueueRedriveTasks should be after Encryption (last tab)
3204        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        // This test ensures the hardcoded tab strip in render_queue_detail matches QueueDetailTab::all()
3211        let all_tabs = QueueDetailTab::all();
3212        assert_eq!(all_tabs.len(), 9);
3213
3214        // Verify order matches
3215        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); // Should be second, after QueuePolicies
3245    }
3246}