1pub use crate::aws::{Profile as AwsProfile, Region as AwsRegion};
2use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
3use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
4use crate::cw::insights::{InsightsFocus, InsightsState};
5pub use crate::cw::{Alarm, AlarmColumn};
6use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
7use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
8use crate::iam::{self, UserColumn};
9use crate::keymap::{Action, Mode};
10pub use crate::lambda::DeploymentColumn;
11pub use crate::lambda::ResourceColumn;
12pub use crate::lambda::{
13 Application as LambdaApplication, ApplicationColumn as LambdaApplicationColumn,
14 Function as LambdaFunction, FunctionColumn as LambdaColumn,
15};
16pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
17use crate::session::{Session, SessionTab};
18pub use crate::sqs::queue::Column as SqsColumn;
19pub use crate::sqs::trigger::Column as SqsTriggerColumn;
20use crate::table::TableState;
21pub use crate::ui::cfn::{
22 DetailTab as CfnDetailTab, State as CfnState, StatusFilter as CfnStatusFilter,
23};
24pub use crate::ui::cw::alarms::{AlarmTab, AlarmViewMode};
25pub use crate::ui::ecr::{State as EcrState, Tab as EcrTab};
26use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
27pub use crate::ui::lambda::{
28 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
29 DetailTab as LambdaDetailTab, State as LambdaState,
30};
31pub use crate::ui::s3::{BucketType as S3BucketType, ObjectTab as S3ObjectTab, State as S3State};
32pub use crate::ui::sqs::{QueueDetailTab as SqsQueueDetailTab, State as SqsState};
33pub use crate::ui::{
34 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, EventFilterFocus,
35 LogGroupColumn, Preferences, StreamColumn, StreamSort, TimeUnit,
36};
37use rusticity_core::{
38 AlarmsClient, AwsConfig, CloudFormationClient, CloudWatchClient, EcrClient, IamClient,
39 LambdaClient, LogEvent, LogGroup, LogStream, S3Client, SqsClient,
40};
41
42#[derive(Clone)]
43pub struct Tab {
44 pub service: Service,
45 pub title: String,
46 pub breadcrumb: String,
47}
48
49pub struct App {
50 pub running: bool,
51 pub mode: Mode,
52 pub config: AwsConfig,
53 pub cloudwatch_client: CloudWatchClient,
54 pub s3_client: S3Client,
55 pub sqs_client: SqsClient,
56 pub alarms_client: AlarmsClient,
57 pub ecr_client: EcrClient,
58 pub iam_client: IamClient,
59 pub lambda_client: LambdaClient,
60 pub cloudformation_client: CloudFormationClient,
61 pub current_service: Service,
62 pub tabs: Vec<Tab>,
63 pub current_tab: usize,
64 pub tab_picker_selected: usize,
65 pub tab_filter: String,
66 pub pending_key: Option<char>,
67 pub log_groups_state: CloudWatchLogGroupsState,
68 pub insights_state: CloudWatchInsightsState,
69 pub alarms_state: CloudWatchAlarmsState,
70 pub s3_state: S3State,
71 pub sqs_state: SqsState,
72 pub ecr_state: EcrState,
73 pub lambda_state: LambdaState,
74 pub lambda_application_state: LambdaApplicationState,
75 pub cfn_state: CfnState,
76 pub iam_state: IamState,
77 pub service_picker: ServicePickerState,
78 pub service_selected: bool,
79 pub profile: String,
80 pub region: String,
81 pub region_selector_index: usize,
82 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
83 pub cw_log_group_column_ids: Vec<ColumnId>,
84 pub column_selector_index: usize,
85 pub preference_section: Preferences,
86 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
87 pub cw_log_stream_column_ids: Vec<ColumnId>,
88 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
89 pub cw_log_event_column_ids: Vec<ColumnId>,
90 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
91 pub cw_alarm_column_ids: Vec<ColumnId>,
92 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
93 pub s3_bucket_column_ids: Vec<ColumnId>,
94 pub sqs_visible_column_ids: Vec<ColumnId>,
95 pub sqs_column_ids: Vec<ColumnId>,
96 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
97 pub ecr_repo_column_ids: Vec<ColumnId>,
98 pub ecr_image_visible_column_ids: Vec<ColumnId>,
99 pub ecr_image_column_ids: Vec<ColumnId>,
100 pub lambda_application_visible_column_ids: Vec<ColumnId>,
101 pub lambda_application_column_ids: Vec<ColumnId>,
102 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
103 pub lambda_deployment_column_ids: Vec<ColumnId>,
104 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
105 pub lambda_resource_column_ids: Vec<ColumnId>,
106 pub cfn_visible_column_ids: Vec<ColumnId>,
107 pub cfn_column_ids: Vec<ColumnId>,
108 pub iam_user_visible_column_ids: Vec<ColumnId>,
109 pub iam_user_column_ids: Vec<ColumnId>,
110 pub iam_role_visible_column_ids: Vec<String>,
111 pub iam_role_column_ids: Vec<String>,
112 pub iam_group_visible_column_ids: Vec<String>,
113 pub iam_group_column_ids: Vec<String>,
114 pub iam_policy_visible_column_ids: Vec<String>,
115 pub iam_policy_column_ids: Vec<String>,
116 pub view_mode: ViewMode,
117 pub error_message: Option<String>,
118 pub error_scroll: usize,
119 pub page_input: String,
120 pub calendar_date: Option<time::Date>,
121 pub calendar_selecting: CalendarField,
122 pub cursor_pos: usize,
123 pub current_session: Option<Session>,
124 pub sessions: Vec<Session>,
125 pub session_picker_selected: usize,
126 pub session_filter: String,
127 pub region_filter: String,
128 pub region_picker_selected: usize,
129 pub region_latencies: std::collections::HashMap<String, u64>,
130 pub profile_filter: String,
131 pub profile_picker_selected: usize,
132 pub available_profiles: Vec<AwsProfile>,
133 pub snapshot_requested: bool,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
137pub enum CalendarField {
138 StartDate,
139 EndDate,
140}
141
142pub struct CloudWatchInsightsState {
143 pub insights: InsightsState,
144 pub loading: bool,
145}
146
147pub struct CloudWatchAlarmsState {
148 pub table: TableState<Alarm>,
149 pub alarm_tab: AlarmTab,
150 pub view_as: AlarmViewMode,
151 pub wrap_lines: bool,
152 pub sort_column: String,
153 pub sort_direction: SortDirection,
154 pub input_focus: InputFocus,
155}
156
157impl PageSize {
158 pub fn value(&self) -> usize {
159 match self {
160 PageSize::Ten => 10,
161 PageSize::TwentyFive => 25,
162 PageSize::Fifty => 50,
163 PageSize::OneHundred => 100,
164 }
165 }
166
167 pub fn next(&self) -> Self {
168 match self {
169 PageSize::Ten => PageSize::TwentyFive,
170 PageSize::TwentyFive => PageSize::Fifty,
171 PageSize::Fifty => PageSize::OneHundred,
172 PageSize::OneHundred => PageSize::Ten,
173 }
174 }
175}
176
177pub struct ServicePickerState {
178 pub filter: String,
179 pub selected: usize,
180 pub services: Vec<&'static str>,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq)]
184pub enum ViewMode {
185 List,
186 Detail,
187 Events,
188 InsightsResults,
189 PolicyView,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq)]
193pub enum Service {
194 CloudWatchLogGroups,
195 CloudWatchInsights,
196 CloudWatchAlarms,
197 S3Buckets,
198 SqsQueues,
199 EcrRepositories,
200 LambdaFunctions,
201 LambdaApplications,
202 CloudFormationStacks,
203 IamUsers,
204 IamRoles,
205 IamUserGroups,
206}
207
208impl Service {
209 pub fn name(&self) -> &str {
210 match self {
211 Service::CloudWatchLogGroups => "CloudWatch > Log Groups",
212 Service::CloudWatchInsights => "CloudWatch > Logs Insights",
213 Service::CloudWatchAlarms => "CloudWatch > Alarms",
214 Service::S3Buckets => "S3 > Buckets",
215 Service::SqsQueues => "SQS > Queues",
216 Service::EcrRepositories => "ECR > Repositories",
217 Service::LambdaFunctions => "Lambda > Functions",
218 Service::LambdaApplications => "Lambda > Applications",
219 Service::CloudFormationStacks => "CloudFormation > Stacks",
220 Service::IamUsers => "IAM > Users",
221 Service::IamRoles => "IAM > Roles",
222 Service::IamUserGroups => "IAM > User Groups",
223 }
224 }
225}
226
227fn copy_to_clipboard(text: &str) {
228 use std::io::Write;
229 use std::process::{Command, Stdio};
230 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
231 if let Some(mut stdin) = child.stdin.take() {
232 let _ = stdin.write_all(text.as_bytes());
233 }
234 let _ = child.wait();
235 }
236}
237
238fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
239 if max > 0 {
240 *selected = (*selected + page_size).min(max - 1);
241 }
242}
243
244impl App {
245 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
246 if self.current_service == Service::CloudWatchAlarms {
247 Some(&mut self.alarms_state.table.filter)
248 } else if self.current_service == Service::S3Buckets {
249 if self.s3_state.current_bucket.is_some() {
250 Some(&mut self.s3_state.object_filter)
251 } else {
252 Some(&mut self.s3_state.buckets.filter)
253 }
254 } else if self.current_service == Service::EcrRepositories {
255 if self.ecr_state.current_repository.is_some() {
256 Some(&mut self.ecr_state.images.filter)
257 } else {
258 Some(&mut self.ecr_state.repositories.filter)
259 }
260 } else if self.current_service == Service::SqsQueues {
261 if self.sqs_state.current_queue.is_some()
262 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
263 {
264 Some(&mut self.sqs_state.triggers.filter)
265 } else if self.sqs_state.current_queue.is_some()
266 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
267 {
268 Some(&mut self.sqs_state.pipes.filter)
269 } else if self.sqs_state.current_queue.is_some()
270 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
271 {
272 Some(&mut self.sqs_state.tags.filter)
273 } else if self.sqs_state.current_queue.is_some()
274 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
275 {
276 Some(&mut self.sqs_state.subscriptions.filter)
277 } else {
278 Some(&mut self.sqs_state.queues.filter)
279 }
280 } else if self.current_service == Service::LambdaFunctions {
281 if self.lambda_state.current_version.is_some()
282 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
283 {
284 Some(&mut self.lambda_state.alias_table.filter)
285 } else if self.lambda_state.current_function.is_some()
286 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
287 {
288 Some(&mut self.lambda_state.version_table.filter)
289 } else if self.lambda_state.current_function.is_some()
290 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
291 {
292 Some(&mut self.lambda_state.alias_table.filter)
293 } else {
294 Some(&mut self.lambda_state.table.filter)
295 }
296 } else if self.current_service == Service::LambdaApplications {
297 if self.lambda_application_state.current_application.is_some() {
298 if self.lambda_application_state.detail_tab
299 == LambdaApplicationDetailTab::Deployments
300 {
301 Some(&mut self.lambda_application_state.deployments.filter)
302 } else {
303 Some(&mut self.lambda_application_state.resources.filter)
304 }
305 } else {
306 Some(&mut self.lambda_application_state.table.filter)
307 }
308 } else if self.current_service == Service::CloudFormationStacks {
309 Some(&mut self.cfn_state.table.filter)
310 } else if self.current_service == Service::IamUsers {
311 if self.iam_state.current_user.is_some() {
312 if self.iam_state.user_tab == UserTab::Tags {
313 Some(&mut self.iam_state.user_tags.filter)
314 } else {
315 Some(&mut self.iam_state.policies.filter)
316 }
317 } else {
318 Some(&mut self.iam_state.users.filter)
319 }
320 } else if self.current_service == Service::IamRoles {
321 if self.iam_state.current_role.is_some() {
322 if self.iam_state.role_tab == RoleTab::Tags {
323 Some(&mut self.iam_state.tags.filter)
324 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
325 Some(&mut self.iam_state.last_accessed_filter)
326 } else {
327 Some(&mut self.iam_state.policies.filter)
328 }
329 } else {
330 Some(&mut self.iam_state.roles.filter)
331 }
332 } else if self.current_service == Service::IamUserGroups {
333 if self.iam_state.current_group.is_some() {
334 if self.iam_state.group_tab == GroupTab::Permissions {
335 Some(&mut self.iam_state.policies.filter)
336 } else if self.iam_state.group_tab == GroupTab::Users {
337 Some(&mut self.iam_state.group_users.filter)
338 } else {
339 None
340 }
341 } else {
342 Some(&mut self.iam_state.groups.filter)
343 }
344 } else if self.view_mode == ViewMode::List {
345 Some(&mut self.log_groups_state.log_groups.filter)
346 } else if self.view_mode == ViewMode::Detail
347 && self.log_groups_state.detail_tab == DetailTab::LogStreams
348 {
349 Some(&mut self.log_groups_state.stream_filter)
350 } else {
351 None
352 }
353 }
354
355 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
356 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
357 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
358
359 std::env::set_var("AWS_PROFILE", &profile_name);
360
361 let config = AwsConfig::new(region).await?;
362 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
363 let s3_client = S3Client::new(config.clone());
364 let sqs_client = SqsClient::new(config.clone());
365 let alarms_client = AlarmsClient::new(config.clone());
366 let ecr_client = EcrClient::new(config.clone());
367 let iam_client = IamClient::new(config.clone());
368 let lambda_client = LambdaClient::new(config.clone());
369 let cloudformation_client = CloudFormationClient::new(config.clone());
370 let region_name = config.region.clone();
371
372 Ok(Self {
373 running: true,
374 mode: Mode::ServicePicker,
375 config,
376 cloudwatch_client,
377 s3_client,
378 sqs_client,
379 alarms_client,
380 ecr_client,
381 iam_client,
382 lambda_client,
383 cloudformation_client,
384 current_service: Service::CloudWatchLogGroups,
385 tabs: Vec::new(),
386 current_tab: 0,
387 tab_picker_selected: 0,
388 tab_filter: String::new(),
389 pending_key: None,
390 log_groups_state: CloudWatchLogGroupsState::new(),
391 insights_state: CloudWatchInsightsState::new(),
392 alarms_state: CloudWatchAlarmsState::new(),
393 s3_state: S3State::new(),
394 sqs_state: SqsState::new(),
395 ecr_state: EcrState::new(),
396 lambda_state: LambdaState::new(),
397 lambda_application_state: LambdaApplicationState::new(),
398 cfn_state: CfnState::new(),
399 iam_state: IamState::new(),
400 service_picker: ServicePickerState::new(),
401 service_selected: false,
402 profile: profile_name,
403 region: region_name,
404 region_selector_index: 0,
405 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
406 cw_log_group_column_ids: LogGroupColumn::ids(),
407 column_selector_index: 0,
408 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
409 cw_log_stream_column_ids: StreamColumn::ids(),
410 cw_log_event_visible_column_ids: EventColumn::default_visible(),
411 cw_log_event_column_ids: EventColumn::ids(),
412 cw_alarm_visible_column_ids: [
413 AlarmColumn::Name,
414 AlarmColumn::State,
415 AlarmColumn::LastStateUpdate,
416 AlarmColumn::Conditions,
417 AlarmColumn::Actions,
418 ]
419 .iter()
420 .map(|c| c.id())
421 .collect(),
422 cw_alarm_column_ids: AlarmColumn::ids(),
423 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
424 s3_bucket_column_ids: S3BucketColumn::ids(),
425 sqs_visible_column_ids: [
426 SqsColumn::Name,
427 SqsColumn::Type,
428 SqsColumn::Created,
429 SqsColumn::MessagesAvailable,
430 SqsColumn::MessagesInFlight,
431 SqsColumn::Encryption,
432 SqsColumn::ContentBasedDeduplication,
433 ]
434 .iter()
435 .map(|c| c.id())
436 .collect(),
437 sqs_column_ids: SqsColumn::ids(),
438 ecr_repo_visible_column_ids: EcrColumn::ids(),
439 ecr_repo_column_ids: EcrColumn::ids(),
440 ecr_image_visible_column_ids: EcrImageColumn::ids(),
441 ecr_image_column_ids: EcrImageColumn::ids(),
442 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
443 lambda_application_column_ids: LambdaApplicationColumn::ids(),
444 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
445 lambda_deployment_column_ids: DeploymentColumn::ids(),
446 lambda_resource_visible_column_ids: ResourceColumn::ids(),
447 lambda_resource_column_ids: ResourceColumn::ids(),
448 cfn_visible_column_ids: [
449 CfnColumn::Name,
450 CfnColumn::Status,
451 CfnColumn::CreatedTime,
452 CfnColumn::Description,
453 ]
454 .iter()
455 .map(|c| c.id())
456 .collect(),
457 cfn_column_ids: CfnColumn::ids(),
458 iam_user_visible_column_ids: UserColumn::visible(),
459 iam_user_column_ids: UserColumn::ids(),
460 iam_role_visible_column_ids: vec![
461 "Role name".to_string(),
462 "Trusted entities".to_string(),
463 "Creation time".to_string(),
464 ],
465 iam_role_column_ids: vec![
466 "Role name".to_string(),
467 "Path".to_string(),
468 "Trusted entities".to_string(),
469 "ARN".to_string(),
470 "Creation time".to_string(),
471 "Description".to_string(),
472 "Max session duration".to_string(),
473 ],
474 iam_group_visible_column_ids: vec![
475 "Group name".to_string(),
476 "Users".to_string(),
477 "Permissions".to_string(),
478 "Creation time".to_string(),
479 ],
480 iam_group_column_ids: vec![
481 "Group name".to_string(),
482 "Path".to_string(),
483 "Users".to_string(),
484 "Permissions".to_string(),
485 "Creation time".to_string(),
486 ],
487 iam_policy_visible_column_ids: vec![
488 "Policy name".to_string(),
489 "Type".to_string(),
490 "Attached via".to_string(),
491 ],
492 iam_policy_column_ids: vec![
493 "Policy name".to_string(),
494 "Type".to_string(),
495 "Attached via".to_string(),
496 "Attached entities".to_string(),
497 "Description".to_string(),
498 "Creation time".to_string(),
499 "Edited time".to_string(),
500 ],
501 preference_section: Preferences::Columns,
502 view_mode: ViewMode::List,
503 error_message: None,
504 error_scroll: 0,
505 page_input: String::new(),
506 calendar_date: None,
507 calendar_selecting: CalendarField::StartDate,
508 cursor_pos: 0,
509 current_session: None,
510 sessions: Vec::new(),
511 session_picker_selected: 0,
512 session_filter: String::new(),
513 region_filter: String::new(),
514 region_picker_selected: 0,
515 region_latencies: std::collections::HashMap::new(),
516 profile_filter: String::new(),
517 profile_picker_selected: 0,
518 available_profiles: Vec::new(),
519 snapshot_requested: false,
520 })
521 }
522
523 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
524 let config = AwsConfig::dummy(region.clone());
525 Self {
526 running: true,
527 mode: Mode::ServicePicker,
528 config: config.clone(),
529 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
530 s3_client: S3Client::new(config.clone()),
531 sqs_client: SqsClient::new(config.clone()),
532 alarms_client: AlarmsClient::new(config.clone()),
533 ecr_client: EcrClient::new(config.clone()),
534 iam_client: IamClient::new(config.clone()),
535 lambda_client: LambdaClient::new(config.clone()),
536 cloudformation_client: CloudFormationClient::new(config.clone()),
537 current_service: Service::CloudWatchLogGroups,
538 tabs: Vec::new(),
539 current_tab: 0,
540 tab_picker_selected: 0,
541 tab_filter: String::new(),
542 pending_key: None,
543 log_groups_state: CloudWatchLogGroupsState::new(),
544 insights_state: CloudWatchInsightsState::new(),
545 alarms_state: CloudWatchAlarmsState::new(),
546 s3_state: S3State::new(),
547 sqs_state: SqsState::new(),
548 ecr_state: EcrState::new(),
549 lambda_state: LambdaState::new(),
550 lambda_application_state: LambdaApplicationState::new(),
551 cfn_state: CfnState::new(),
552 iam_state: IamState::new(),
553 service_picker: ServicePickerState::new(),
554 service_selected: false,
555 profile,
556 region: region.unwrap_or_default(),
557 region_selector_index: 0,
558 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
559 cw_log_group_column_ids: LogGroupColumn::ids(),
560 column_selector_index: 0,
561 preference_section: Preferences::Columns,
562 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
563 cw_log_stream_column_ids: StreamColumn::ids(),
564 cw_log_event_visible_column_ids: EventColumn::default_visible(),
565 cw_log_event_column_ids: EventColumn::ids(),
566 cw_alarm_visible_column_ids: [
567 AlarmColumn::Name,
568 AlarmColumn::State,
569 AlarmColumn::LastStateUpdate,
570 AlarmColumn::Conditions,
571 AlarmColumn::Actions,
572 ]
573 .iter()
574 .map(|c| c.id())
575 .collect(),
576 cw_alarm_column_ids: AlarmColumn::ids(),
577 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
578 s3_bucket_column_ids: S3BucketColumn::ids(),
579 sqs_visible_column_ids: [
580 SqsColumn::Name,
581 SqsColumn::Type,
582 SqsColumn::Created,
583 SqsColumn::MessagesAvailable,
584 SqsColumn::MessagesInFlight,
585 SqsColumn::Encryption,
586 SqsColumn::ContentBasedDeduplication,
587 ]
588 .iter()
589 .map(|c| c.id())
590 .collect(),
591 sqs_column_ids: SqsColumn::ids(),
592 ecr_repo_visible_column_ids: EcrColumn::ids(),
593 ecr_repo_column_ids: EcrColumn::ids(),
594 ecr_image_visible_column_ids: EcrImageColumn::ids(),
595 ecr_image_column_ids: EcrImageColumn::ids(),
596 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
597 lambda_application_column_ids: LambdaApplicationColumn::ids(),
598 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
599 lambda_deployment_column_ids: DeploymentColumn::ids(),
600 lambda_resource_visible_column_ids: ResourceColumn::ids(),
601 lambda_resource_column_ids: ResourceColumn::ids(),
602 cfn_visible_column_ids: [
603 CfnColumn::Name,
604 CfnColumn::Status,
605 CfnColumn::CreatedTime,
606 CfnColumn::Description,
607 ]
608 .iter()
609 .map(|c| c.id())
610 .collect(),
611 cfn_column_ids: CfnColumn::ids(),
612 iam_user_visible_column_ids: UserColumn::visible(),
613 iam_user_column_ids: UserColumn::ids(),
614 iam_role_visible_column_ids: vec![
615 "Role name".to_string(),
616 "Trusted entities".to_string(),
617 "Creation time".to_string(),
618 ],
619 iam_role_column_ids: vec![
620 "Role name".to_string(),
621 "Path".to_string(),
622 "Trusted entities".to_string(),
623 "ARN".to_string(),
624 "Creation time".to_string(),
625 "Description".to_string(),
626 "Max session duration".to_string(),
627 ],
628 iam_group_visible_column_ids: vec![
629 "Group name".to_string(),
630 "Users".to_string(),
631 "Permissions".to_string(),
632 "Creation time".to_string(),
633 ],
634 iam_group_column_ids: vec![
635 "Group name".to_string(),
636 "Path".to_string(),
637 "Users".to_string(),
638 "Permissions".to_string(),
639 "Creation time".to_string(),
640 ],
641 iam_policy_visible_column_ids: vec![
642 "Policy name".to_string(),
643 "Type".to_string(),
644 "Attached via".to_string(),
645 ],
646 iam_policy_column_ids: vec![
647 "Policy name".to_string(),
648 "Type".to_string(),
649 "Attached via".to_string(),
650 "Attached entities".to_string(),
651 "Description".to_string(),
652 "Creation time".to_string(),
653 "Edited time".to_string(),
654 ],
655 view_mode: ViewMode::List,
656 error_message: None,
657 error_scroll: 0,
658 page_input: String::new(),
659 calendar_date: None,
660 calendar_selecting: CalendarField::StartDate,
661 cursor_pos: 0,
662 current_session: None,
663 sessions: Vec::new(),
664 session_picker_selected: 0,
665 session_filter: String::new(),
666 region_filter: String::new(),
667 region_picker_selected: 0,
668 region_latencies: std::collections::HashMap::new(),
669 profile_filter: String::new(),
670 profile_picker_selected: 0,
671 available_profiles: Vec::new(),
672 snapshot_requested: false,
673 }
674 }
675
676 pub fn handle_action(&mut self, action: Action) {
677 match action {
678 Action::Quit => {
679 self.save_current_session();
680 self.running = false;
681 }
682 Action::CloseService => {
683 if !self.tabs.is_empty() {
684 self.tabs.remove(self.current_tab);
686
687 if self.tabs.is_empty() {
688 self.service_selected = false;
690 self.current_tab = 0;
691 self.mode = Mode::ServicePicker;
692 } else {
693 if self.current_tab >= self.tabs.len() {
695 self.current_tab = self.tabs.len() - 1;
696 }
697 self.current_service = self.tabs[self.current_tab].service;
698 self.service_selected = true;
699 self.mode = Mode::Normal;
700 }
701 } else {
702 self.service_selected = false;
704 self.mode = Mode::Normal;
705 }
706 self.service_picker.filter.clear();
707 self.service_picker.selected = 0;
708 }
709 Action::NextItem => self.next_item(),
710 Action::PrevItem => self.prev_item(),
711 Action::PageUp => self.page_up(),
712 Action::PageDown => self.page_down(),
713 Action::NextPane => self.next_pane(),
714 Action::PrevPane => self.prev_pane(),
715 Action::Select => self.select_item(),
716 Action::OpenSpaceMenu => {
717 self.mode = Mode::SpaceMenu;
718 self.service_picker.filter.clear();
719 self.service_picker.selected = 0;
720 }
721 Action::CloseMenu => {
722 self.mode = Mode::Normal;
723 self.service_picker.filter.clear();
724 if self.current_service == Service::S3Buckets {
726 self.s3_state.selected_row = 0;
727 self.s3_state.selected_object = 0;
728 }
729 }
730 Action::NextTab => {
731 if !self.tabs.is_empty() {
732 self.current_tab = (self.current_tab + 1) % self.tabs.len();
733 self.current_service = self.tabs[self.current_tab].service;
734 }
735 }
736 Action::PrevTab => {
737 if !self.tabs.is_empty() {
738 self.current_tab = if self.current_tab == 0 {
739 self.tabs.len() - 1
740 } else {
741 self.current_tab - 1
742 };
743 self.current_service = self.tabs[self.current_tab].service;
744 }
745 }
746 Action::CloseTab => {
747 if !self.tabs.is_empty() {
748 self.tabs.remove(self.current_tab);
749 if self.tabs.is_empty() {
750 self.service_selected = false;
752 self.current_tab = 0;
753 self.service_picker.filter.clear();
754 self.service_picker.selected = 0;
755 self.mode = Mode::ServicePicker;
756 } else {
757 if self.current_tab >= self.tabs.len() {
760 self.current_tab = self.tabs.len() - 1;
761 }
762 self.current_service = self.tabs[self.current_tab].service;
763 self.service_selected = true;
764 self.mode = Mode::Normal;
765 }
766 }
767 }
768 Action::OpenTabPicker => {
769 if !self.tabs.is_empty() {
770 self.tab_picker_selected = self.current_tab;
771 self.mode = Mode::TabPicker;
772 } else {
773 self.mode = Mode::Normal;
774 }
775 }
776 Action::OpenSessionPicker => {
777 self.save_current_session();
778 self.sessions = Session::list_all().unwrap_or_default();
779 self.session_picker_selected = 0;
780 self.mode = Mode::SessionPicker;
781 }
782 Action::LoadSession => {
783 let filtered_sessions = self.get_filtered_sessions();
784 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
785 let session = session.clone();
786 self.profile = session.profile.clone();
788 self.region = session.region.clone();
789 self.config.account_id = session.account_id.clone();
790 self.config.role_arn = session.role_arn.clone();
791
792 self.tabs.clear();
794 for session_tab in &session.tabs {
795 let service = match session_tab.service.as_str() {
797 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
798 "CloudWatchInsights" => Service::CloudWatchInsights,
799 "CloudWatchAlarms" => Service::CloudWatchAlarms,
800 "S3Buckets" => Service::S3Buckets,
801 "SqsQueues" => Service::SqsQueues,
802 "EcrRepositories" => Service::EcrRepositories,
803 "LambdaFunctions" => Service::LambdaFunctions,
804 "LambdaApplications" => Service::LambdaApplications,
805 "CloudFormationStacks" => Service::CloudFormationStacks,
806 "IamUsers" => Service::IamUsers,
807 "IamRoles" => Service::IamRoles,
808 "IamUserGroups" => Service::IamUserGroups,
809 _ => continue,
810 };
811
812 self.tabs.push(Tab {
813 service,
814 title: session_tab.title.clone(),
815 breadcrumb: session_tab.breadcrumb.clone(),
816 });
817
818 if let Some(filter) = &session_tab.filter {
820 if service == Service::CloudWatchLogGroups {
821 self.log_groups_state.log_groups.filter = filter.clone();
822 }
823 }
824 }
825
826 if !self.tabs.is_empty() {
827 self.current_tab = 0;
828 self.current_service = self.tabs[0].service;
829 self.service_selected = true;
830 self.current_session = Some(session.clone());
831 }
832 }
833 self.mode = Mode::Normal;
834 }
835 Action::SaveSession => {
836 }
838 Action::OpenServicePicker => {
839 if self.mode == Mode::ServicePicker {
840 self.tabs.push(Tab {
841 service: Service::S3Buckets,
842 title: "S3 > Buckets".to_string(),
843 breadcrumb: "S3 > Buckets".to_string(),
844 });
845 self.current_tab = self.tabs.len() - 1;
846 self.current_service = Service::S3Buckets;
847 self.view_mode = ViewMode::List;
848 self.service_selected = true;
849 self.mode = Mode::Normal;
850 } else {
851 self.mode = Mode::ServicePicker;
852 self.service_picker.filter.clear();
853 self.service_picker.selected = 0;
854 }
855 }
856 Action::OpenCloudWatch => {
857 self.current_service = Service::CloudWatchLogGroups;
858 self.view_mode = ViewMode::List;
859 self.service_selected = true;
860 self.mode = Mode::Normal;
861 }
862 Action::OpenCloudWatchSplit => {
863 self.current_service = Service::CloudWatchInsights;
864 self.view_mode = ViewMode::InsightsResults;
865 self.service_selected = true;
866 self.mode = Mode::Normal;
867 }
868 Action::OpenCloudWatchAlarms => {
869 self.current_service = Service::CloudWatchAlarms;
870 self.view_mode = ViewMode::List;
871 self.service_selected = true;
872 self.mode = Mode::Normal;
873 }
874 Action::FilterInput(c) => {
875 if self.mode == Mode::TabPicker {
876 self.tab_filter.push(c);
877 self.tab_picker_selected = 0;
878 } else if self.mode == Mode::RegionPicker {
879 self.region_filter.push(c);
880 self.region_picker_selected = 0;
881 } else if self.mode == Mode::ProfilePicker {
882 self.profile_filter.push(c);
883 self.profile_picker_selected = 0;
884 } else if self.mode == Mode::SessionPicker {
885 self.session_filter.push(c);
886 self.session_picker_selected = 0;
887 } else if self.mode == Mode::ServicePicker {
888 self.service_picker.filter.push(c);
889 self.service_picker.selected = 0;
890 } else if self.mode == Mode::InsightsInput {
891 use crate::app::InsightsFocus;
892 match self.insights_state.insights.insights_focus {
893 InsightsFocus::Query => {
894 self.insights_state.insights.query_text.push(c);
895 }
896 InsightsFocus::LogGroupSearch => {
897 self.insights_state.insights.log_group_search.push(c);
898 if !self.insights_state.insights.log_group_search.is_empty() {
900 self.insights_state.insights.log_group_matches = self
901 .log_groups_state
902 .log_groups
903 .items
904 .iter()
905 .filter(|g| {
906 g.name.to_lowercase().contains(
907 &self
908 .insights_state
909 .insights
910 .log_group_search
911 .to_lowercase(),
912 )
913 })
914 .take(50)
915 .map(|g| g.name.clone())
916 .collect();
917 self.insights_state.insights.show_dropdown = true;
918 } else {
919 self.insights_state.insights.log_group_matches.clear();
920 self.insights_state.insights.show_dropdown = false;
921 }
922 }
923 _ => {}
924 }
925 } else if self.mode == Mode::FilterInput {
926 let is_pagination_focused = if self.current_service
928 == Service::LambdaApplications
929 {
930 if self.lambda_application_state.current_application.is_some() {
931 if self.lambda_application_state.detail_tab
932 == LambdaApplicationDetailTab::Deployments
933 {
934 self.lambda_application_state.deployment_input_focus
935 == InputFocus::Pagination
936 } else {
937 self.lambda_application_state.resource_input_focus
938 == InputFocus::Pagination
939 }
940 } else {
941 self.lambda_application_state.input_focus == InputFocus::Pagination
942 }
943 } else if self.current_service == Service::CloudFormationStacks {
944 self.cfn_state.input_focus == InputFocus::Pagination
945 } else if self.current_service == Service::IamRoles
946 && self.iam_state.current_role.is_none()
947 {
948 self.iam_state.role_input_focus == InputFocus::Pagination
949 } else if self.view_mode == ViewMode::PolicyView {
950 self.iam_state.policy_input_focus == InputFocus::Pagination
951 } else if self.current_service == Service::CloudWatchAlarms {
952 self.alarms_state.input_focus == InputFocus::Pagination
953 } else if self.current_service == Service::CloudWatchLogGroups {
954 self.log_groups_state.input_focus == InputFocus::Pagination
955 } else if self.current_service == Service::EcrRepositories
956 && self.ecr_state.current_repository.is_none()
957 {
958 self.ecr_state.input_focus == InputFocus::Pagination
959 } else if self.current_service == Service::LambdaFunctions {
960 if self.lambda_state.current_function.is_some()
961 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
962 {
963 self.lambda_state.version_input_focus == InputFocus::Pagination
964 } else if self.lambda_state.current_function.is_none() {
965 self.lambda_state.input_focus == InputFocus::Pagination
966 } else {
967 false
968 }
969 } else if self.current_service == Service::SqsQueues {
970 if self.sqs_state.current_queue.is_some()
971 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
972 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
973 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
974 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
975 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
976 {
977 self.sqs_state.input_focus == InputFocus::Pagination
978 } else {
979 false
980 }
981 } else {
982 false
983 };
984
985 if is_pagination_focused && c.is_ascii_digit() {
986 self.page_input.push(c);
987 } else if self.current_service == Service::LambdaApplications {
988 let is_input_focused =
989 if self.lambda_application_state.current_application.is_some() {
990 if self.lambda_application_state.detail_tab
991 == LambdaApplicationDetailTab::Deployments
992 {
993 self.lambda_application_state.deployment_input_focus
994 == InputFocus::Filter
995 } else {
996 self.lambda_application_state.resource_input_focus
997 == InputFocus::Filter
998 }
999 } else {
1000 self.lambda_application_state.input_focus == InputFocus::Filter
1001 };
1002 if is_input_focused {
1003 if let Some(filter) = self.get_active_filter_mut() {
1004 filter.push(c);
1005 }
1006 }
1007 } else if self.current_service == Service::CloudFormationStacks {
1008 if self.cfn_state.input_focus == InputFocus::Filter {
1009 if let Some(filter) = self.get_active_filter_mut() {
1010 filter.push(c);
1011 }
1012 }
1013 } else if self.current_service == Service::EcrRepositories
1014 && self.ecr_state.current_repository.is_none()
1015 {
1016 if self.ecr_state.input_focus == InputFocus::Filter {
1017 if let Some(filter) = self.get_active_filter_mut() {
1018 filter.push(c);
1019 }
1020 }
1021 } else if self.current_service == Service::IamRoles
1022 && self.iam_state.current_role.is_none()
1023 {
1024 if self.iam_state.role_input_focus == InputFocus::Filter {
1025 if let Some(filter) = self.get_active_filter_mut() {
1026 filter.push(c);
1027 }
1028 }
1029 } else if self.view_mode == ViewMode::PolicyView {
1030 if self.iam_state.policy_input_focus == InputFocus::Filter {
1031 if let Some(filter) = self.get_active_filter_mut() {
1032 filter.push(c);
1033 }
1034 }
1035 } else if self.current_service == Service::LambdaFunctions
1036 && self.lambda_state.current_version.is_some()
1037 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1038 {
1039 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1040 if let Some(filter) = self.get_active_filter_mut() {
1041 filter.push(c);
1042 }
1043 }
1044 } else if self.current_service == Service::LambdaFunctions
1045 && self.lambda_state.current_function.is_some()
1046 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1047 {
1048 if self.lambda_state.version_input_focus == InputFocus::Filter {
1049 if let Some(filter) = self.get_active_filter_mut() {
1050 filter.push(c);
1051 }
1052 }
1053 } else if self.current_service == Service::LambdaFunctions
1054 && self.lambda_state.current_function.is_some()
1055 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1056 {
1057 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1058 if let Some(filter) = self.get_active_filter_mut() {
1059 filter.push(c);
1060 }
1061 }
1062 } else if self.current_service == Service::SqsQueues
1063 && self.sqs_state.current_queue.is_some()
1064 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1065 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1066 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1067 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1068 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1069 {
1070 if self.sqs_state.input_focus == InputFocus::Filter {
1071 if let Some(filter) = self.get_active_filter_mut() {
1072 filter.push(c);
1073 }
1074 }
1075 } else if let Some(filter) = self.get_active_filter_mut() {
1076 filter.push(c);
1077 }
1078 } else if self.mode == Mode::EventFilterInput {
1079 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1080 self.log_groups_state.event_filter.push(c);
1081 } else if c.is_ascii_digit() {
1082 self.log_groups_state.relative_amount.push(c);
1083 }
1084 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1085 self.page_input.push(c);
1086 }
1087 }
1088 Action::FilterBackspace => {
1089 if self.mode == Mode::ServicePicker {
1090 self.service_picker.filter.pop();
1091 self.service_picker.selected = 0;
1092 } else if self.mode == Mode::TabPicker {
1093 self.tab_filter.pop();
1094 self.tab_picker_selected = 0;
1095 } else if self.mode == Mode::RegionPicker {
1096 self.region_filter.pop();
1097 self.region_picker_selected = 0;
1098 } else if self.mode == Mode::ProfilePicker {
1099 self.profile_filter.pop();
1100 self.profile_picker_selected = 0;
1101 } else if self.mode == Mode::SessionPicker {
1102 self.session_filter.pop();
1103 self.session_picker_selected = 0;
1104 } else if self.mode == Mode::InsightsInput {
1105 use crate::app::InsightsFocus;
1106 match self.insights_state.insights.insights_focus {
1107 InsightsFocus::Query => {
1108 self.insights_state.insights.query_text.pop();
1109 }
1110 InsightsFocus::LogGroupSearch => {
1111 self.insights_state.insights.log_group_search.pop();
1112 if !self.insights_state.insights.log_group_search.is_empty() {
1114 self.insights_state.insights.log_group_matches = self
1115 .log_groups_state
1116 .log_groups
1117 .items
1118 .iter()
1119 .filter(|g| {
1120 g.name.to_lowercase().contains(
1121 &self
1122 .insights_state
1123 .insights
1124 .log_group_search
1125 .to_lowercase(),
1126 )
1127 })
1128 .take(50)
1129 .map(|g| g.name.clone())
1130 .collect();
1131 self.insights_state.insights.show_dropdown = true;
1132 } else {
1133 self.insights_state.insights.log_group_matches.clear();
1134 self.insights_state.insights.show_dropdown = false;
1135 }
1136 }
1137 _ => {}
1138 }
1139 } else if self.mode == Mode::FilterInput {
1140 if self.current_service == Service::CloudFormationStacks {
1142 if self.cfn_state.input_focus == InputFocus::Filter {
1143 if let Some(filter) = self.get_active_filter_mut() {
1144 filter.pop();
1145 }
1146 }
1147 } else if let Some(filter) = self.get_active_filter_mut() {
1148 filter.pop();
1149 }
1150 } else if self.mode == Mode::EventFilterInput {
1151 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1152 self.log_groups_state.event_filter.pop();
1153 } else {
1154 self.log_groups_state.relative_amount.pop();
1155 }
1156 }
1157 }
1158 Action::DeleteWord => {
1159 let text = if self.mode == Mode::ServicePicker {
1160 &mut self.service_picker.filter
1161 } else if self.mode == Mode::InsightsInput {
1162 use crate::app::InsightsFocus;
1163 match self.insights_state.insights.insights_focus {
1164 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1165 InsightsFocus::LogGroupSearch => {
1166 &mut self.insights_state.insights.log_group_search
1167 }
1168 _ => return,
1169 }
1170 } else if self.mode == Mode::FilterInput {
1171 if let Some(filter) = self.get_active_filter_mut() {
1172 filter
1173 } else {
1174 return;
1175 }
1176 } else if self.mode == Mode::EventFilterInput {
1177 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1178 &mut self.log_groups_state.event_filter
1179 } else {
1180 &mut self.log_groups_state.relative_amount
1181 }
1182 } else {
1183 return;
1184 };
1185
1186 if text.is_empty() {
1187 return;
1188 }
1189
1190 let mut chars: Vec<char> = text.chars().collect();
1191 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1192 chars.pop();
1193 }
1194 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1195 chars.pop();
1196 }
1197 *text = chars.into_iter().collect();
1198 }
1199 Action::WordLeft => {
1200 }
1202 Action::WordRight => {
1203 }
1205 Action::OpenColumnSelector => {
1206 if !self.page_input.is_empty() {
1208 if let Ok(page) = self.page_input.parse::<usize>() {
1209 self.go_to_page(page);
1210 }
1211 self.page_input.clear();
1212 } else {
1213 self.mode = Mode::ColumnSelector;
1214 self.column_selector_index = 0;
1215 }
1216 }
1217 Action::ToggleColumn => {
1218 if self.current_service == Service::S3Buckets
1219 && self.s3_state.current_bucket.is_none()
1220 {
1221 if let Some(col) = self.s3_bucket_column_ids.get(self.column_selector_index) {
1222 if let Some(pos) = self
1223 .s3_bucket_visible_column_ids
1224 .iter()
1225 .position(|c| c == col)
1226 {
1227 self.s3_bucket_visible_column_ids.remove(pos);
1228 } else {
1229 self.s3_bucket_visible_column_ids.push(*col);
1230 }
1231 }
1232 } else if self.current_service == Service::CloudWatchAlarms {
1233 let idx = self.column_selector_index;
1237 if (1..=16).contains(&idx) {
1238 if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1240 if let Some(pos) = self
1241 .cw_alarm_visible_column_ids
1242 .iter()
1243 .position(|c| c == col)
1244 {
1245 self.cw_alarm_visible_column_ids.remove(pos);
1246 } else {
1247 self.cw_alarm_visible_column_ids.push(*col);
1248 }
1249 }
1250 } else if idx == 19 {
1251 self.alarms_state.view_as = AlarmViewMode::Table;
1252 } else if idx == 20 {
1253 self.alarms_state.view_as = AlarmViewMode::Cards;
1254 } else if idx == 23 {
1255 self.alarms_state.table.page_size = PageSize::Ten;
1256 } else if idx == 24 {
1257 self.alarms_state.table.page_size = PageSize::TwentyFive;
1258 } else if idx == 25 {
1259 self.alarms_state.table.page_size = PageSize::Fifty;
1260 } else if idx == 26 {
1261 self.alarms_state.table.page_size = PageSize::OneHundred;
1262 } else if idx == 29 {
1263 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1264 }
1265 } else if self.current_service == Service::EcrRepositories {
1266 if self.ecr_state.current_repository.is_some() {
1267 let idx = self.column_selector_index;
1269 if let Some(col) = self.ecr_image_column_ids.get(idx) {
1270 if let Some(pos) = self
1271 .ecr_image_visible_column_ids
1272 .iter()
1273 .position(|c| c == col)
1274 {
1275 self.ecr_image_visible_column_ids.remove(pos);
1276 } else {
1277 self.ecr_image_visible_column_ids.push(*col);
1278 }
1279 }
1280 } else {
1281 if let Some(col) = self.ecr_repo_column_ids.get(self.column_selector_index)
1283 {
1284 if let Some(pos) = self
1285 .ecr_repo_visible_column_ids
1286 .iter()
1287 .position(|c| c == col)
1288 {
1289 self.ecr_repo_visible_column_ids.remove(pos);
1290 } else {
1291 self.ecr_repo_visible_column_ids.push(*col);
1292 }
1293 }
1294 }
1295 } else if self.current_service == Service::SqsQueues {
1296 if self.sqs_state.current_queue.is_some()
1297 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1298 {
1299 let idx = self.column_selector_index;
1301 if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
1302 if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
1303 if let Some(pos) = self
1304 .sqs_state
1305 .trigger_visible_column_ids
1306 .iter()
1307 .position(|c| c == col)
1308 {
1309 self.sqs_state.trigger_visible_column_ids.remove(pos);
1310 } else {
1311 self.sqs_state.trigger_visible_column_ids.push(col.clone());
1312 }
1313 }
1314 } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
1315 self.sqs_state.triggers.page_size = PageSize::Ten;
1316 } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
1317 self.sqs_state.triggers.page_size = PageSize::TwentyFive;
1318 } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
1319 self.sqs_state.triggers.page_size = PageSize::Fifty;
1320 } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
1321 self.sqs_state.triggers.page_size = PageSize::OneHundred;
1322 }
1323 } else if self.sqs_state.current_queue.is_some()
1324 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1325 {
1326 let idx = self.column_selector_index;
1328 if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
1329 if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
1330 if let Some(pos) = self
1331 .sqs_state
1332 .pipe_visible_column_ids
1333 .iter()
1334 .position(|c| c == col)
1335 {
1336 self.sqs_state.pipe_visible_column_ids.remove(pos);
1337 } else {
1338 self.sqs_state.pipe_visible_column_ids.push(col.clone());
1339 }
1340 }
1341 } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
1342 self.sqs_state.pipes.page_size = PageSize::Ten;
1343 } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
1344 self.sqs_state.pipes.page_size = PageSize::TwentyFive;
1345 } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
1346 self.sqs_state.pipes.page_size = PageSize::Fifty;
1347 } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
1348 self.sqs_state.pipes.page_size = PageSize::OneHundred;
1349 }
1350 } else if self.sqs_state.current_queue.is_some()
1351 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1352 {
1353 let idx = self.column_selector_index;
1355 if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
1356 if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
1357 if let Some(pos) = self
1358 .sqs_state
1359 .tag_visible_column_ids
1360 .iter()
1361 .position(|c| c == col)
1362 {
1363 self.sqs_state.tag_visible_column_ids.remove(pos);
1364 } else {
1365 self.sqs_state.tag_visible_column_ids.push(col.clone());
1366 }
1367 }
1368 } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
1369 self.sqs_state.tags.page_size = PageSize::Ten;
1370 } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
1371 self.sqs_state.tags.page_size = PageSize::TwentyFive;
1372 } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
1373 self.sqs_state.tags.page_size = PageSize::Fifty;
1374 } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
1375 self.sqs_state.tags.page_size = PageSize::OneHundred;
1376 }
1377 } else if self.sqs_state.current_queue.is_some()
1378 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
1379 {
1380 let idx = self.column_selector_index;
1382 if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
1383 if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
1384 if let Some(pos) = self
1385 .sqs_state
1386 .subscription_visible_column_ids
1387 .iter()
1388 .position(|c| c == col)
1389 {
1390 self.sqs_state.subscription_visible_column_ids.remove(pos);
1391 } else {
1392 self.sqs_state
1393 .subscription_visible_column_ids
1394 .push(col.clone());
1395 }
1396 }
1397 } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
1398 self.sqs_state.subscriptions.page_size = PageSize::Ten;
1399 } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
1400 self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
1401 } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
1402 self.sqs_state.subscriptions.page_size = PageSize::Fifty;
1403 } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
1404 self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
1405 }
1406 } else if let Some(col) = self.sqs_column_ids.get(self.column_selector_index) {
1407 if let Some(pos) = self.sqs_visible_column_ids.iter().position(|c| c == col)
1408 {
1409 self.sqs_visible_column_ids.remove(pos);
1410 } else {
1411 self.sqs_visible_column_ids.push(*col);
1412 }
1413 }
1414 } else if self.current_service == Service::LambdaFunctions {
1415 let idx = self.column_selector_index;
1416 if self.lambda_state.current_function.is_some()
1418 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1419 {
1420 if idx > 0 && idx <= self.lambda_state.version_column_ids.len() {
1422 if let Some(col) = self.lambda_state.version_column_ids.get(idx - 1) {
1423 if let Some(pos) = self
1424 .lambda_state
1425 .version_visible_column_ids
1426 .iter()
1427 .position(|c| *c == *col)
1428 {
1429 self.lambda_state.version_visible_column_ids.remove(pos);
1430 } else {
1431 self.lambda_state
1432 .version_visible_column_ids
1433 .push(col.clone());
1434 }
1435 }
1436 } else if idx == self.lambda_state.version_column_ids.len() + 3 {
1437 self.lambda_state.version_table.page_size = PageSize::Ten;
1438 } else if idx == self.lambda_state.version_column_ids.len() + 4 {
1439 self.lambda_state.version_table.page_size = PageSize::TwentyFive;
1440 } else if idx == self.lambda_state.version_column_ids.len() + 5 {
1441 self.lambda_state.version_table.page_size = PageSize::Fifty;
1442 } else if idx == self.lambda_state.version_column_ids.len() + 6 {
1443 self.lambda_state.version_table.page_size = PageSize::OneHundred;
1444 }
1445 } else if (self.lambda_state.current_function.is_some()
1446 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases)
1447 || (self.lambda_state.current_version.is_some()
1448 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration)
1449 {
1450 if idx > 0 && idx <= self.lambda_state.alias_column_ids.len() {
1452 if let Some(col) = self.lambda_state.alias_column_ids.get(idx - 1) {
1453 if let Some(pos) = self
1454 .lambda_state
1455 .alias_visible_column_ids
1456 .iter()
1457 .position(|c| *c == *col)
1458 {
1459 self.lambda_state.alias_visible_column_ids.remove(pos);
1460 } else {
1461 self.lambda_state.alias_visible_column_ids.push(col.clone());
1462 }
1463 }
1464 } else if idx == self.lambda_state.alias_column_ids.len() + 3 {
1465 self.lambda_state.alias_table.page_size = PageSize::Ten;
1466 } else if idx == self.lambda_state.alias_column_ids.len() + 4 {
1467 self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
1468 } else if idx == self.lambda_state.alias_column_ids.len() + 5 {
1469 self.lambda_state.alias_table.page_size = PageSize::Fifty;
1470 } else if idx == self.lambda_state.alias_column_ids.len() + 6 {
1471 self.lambda_state.alias_table.page_size = PageSize::OneHundred;
1472 }
1473 } else {
1474 if idx > 0 && idx <= self.lambda_state.function_column_ids.len() {
1476 if let Some(col) = self.lambda_state.function_column_ids.get(idx - 1) {
1477 if let Some(pos) = self
1478 .lambda_state
1479 .function_visible_column_ids
1480 .iter()
1481 .position(|c| *c == *col)
1482 {
1483 self.lambda_state.function_visible_column_ids.remove(pos);
1484 } else {
1485 self.lambda_state.function_visible_column_ids.push(*col);
1486 }
1487 }
1488 } else if idx == self.lambda_state.function_column_ids.len() + 3 {
1489 self.lambda_state.table.page_size = PageSize::Ten;
1490 } else if idx == self.lambda_state.function_column_ids.len() + 4 {
1491 self.lambda_state.table.page_size = PageSize::TwentyFive;
1492 } else if idx == self.lambda_state.function_column_ids.len() + 5 {
1493 self.lambda_state.table.page_size = PageSize::Fifty;
1494 } else if idx == self.lambda_state.function_column_ids.len() + 6 {
1495 self.lambda_state.table.page_size = PageSize::OneHundred;
1496 }
1497 }
1498 } else if self.current_service == Service::LambdaApplications {
1499 if self.lambda_application_state.current_application.is_some() {
1500 if self.lambda_application_state.detail_tab
1502 == LambdaApplicationDetailTab::Overview
1503 {
1504 let idx = self.column_selector_index;
1506 if idx > 0 && idx <= self.lambda_resource_column_ids.len() {
1507 if let Some(col) = self.lambda_resource_column_ids.get(idx - 1) {
1508 if let Some(pos) = self
1509 .lambda_resource_visible_column_ids
1510 .iter()
1511 .position(|c| c == col)
1512 {
1513 self.lambda_resource_visible_column_ids.remove(pos);
1514 } else {
1515 self.lambda_resource_visible_column_ids.push(*col);
1516 }
1517 }
1518 } else if idx == self.lambda_resource_column_ids.len() + 3 {
1519 self.lambda_application_state.resources.page_size = PageSize::Ten;
1520 } else if idx == self.lambda_resource_column_ids.len() + 4 {
1521 self.lambda_application_state.resources.page_size =
1522 PageSize::TwentyFive;
1523 } else if idx == self.lambda_resource_column_ids.len() + 5 {
1524 self.lambda_application_state.resources.page_size = PageSize::Fifty;
1525 }
1526 } else {
1527 let idx = self.column_selector_index;
1529 if idx > 0 && idx <= self.lambda_deployment_column_ids.len() {
1530 if let Some(col) = self.lambda_deployment_column_ids.get(idx - 1) {
1531 if let Some(pos) = self
1532 .lambda_deployment_visible_column_ids
1533 .iter()
1534 .position(|c| c == col)
1535 {
1536 self.lambda_deployment_visible_column_ids.remove(pos);
1537 } else {
1538 self.lambda_deployment_visible_column_ids.push(*col);
1539 }
1540 }
1541 } else if idx == self.lambda_deployment_column_ids.len() + 3 {
1542 self.lambda_application_state.deployments.page_size = PageSize::Ten;
1543 } else if idx == self.lambda_deployment_column_ids.len() + 4 {
1544 self.lambda_application_state.deployments.page_size =
1545 PageSize::TwentyFive;
1546 } else if idx == self.lambda_deployment_column_ids.len() + 5 {
1547 self.lambda_application_state.deployments.page_size =
1548 PageSize::Fifty;
1549 }
1550 }
1551 } else {
1552 let idx = self.column_selector_index;
1554 if idx > 0 && idx <= self.lambda_application_column_ids.len() {
1555 if let Some(col) = self.lambda_application_column_ids.get(idx - 1) {
1556 if let Some(pos) = self
1557 .lambda_application_visible_column_ids
1558 .iter()
1559 .position(|c| *c == *col)
1560 {
1561 self.lambda_application_visible_column_ids.remove(pos);
1562 } else {
1563 self.lambda_application_visible_column_ids.push(*col);
1564 }
1565 }
1566 } else if idx == self.lambda_application_column_ids.len() + 3 {
1567 self.lambda_application_state.table.page_size = PageSize::Ten;
1568 } else if idx == self.lambda_application_column_ids.len() + 4 {
1569 self.lambda_application_state.table.page_size = PageSize::TwentyFive;
1570 } else if idx == self.lambda_application_column_ids.len() + 5 {
1571 self.lambda_application_state.table.page_size = PageSize::Fifty;
1572 }
1573 }
1574 } else if self.view_mode == ViewMode::Events {
1575 if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
1576 {
1577 if let Some(pos) = self
1578 .cw_log_event_visible_column_ids
1579 .iter()
1580 .position(|c| c == col)
1581 {
1582 self.cw_log_event_visible_column_ids.remove(pos);
1583 } else {
1584 self.cw_log_event_visible_column_ids.push(*col);
1585 }
1586 }
1587 } else if self.view_mode == ViewMode::Detail {
1588 if let Some(col) = self
1589 .cw_log_stream_column_ids
1590 .get(self.column_selector_index)
1591 {
1592 if let Some(pos) = self
1593 .cw_log_stream_visible_column_ids
1594 .iter()
1595 .position(|c| c == col)
1596 {
1597 self.cw_log_stream_visible_column_ids.remove(pos);
1598 } else {
1599 self.cw_log_stream_visible_column_ids.push(*col);
1600 }
1601 }
1602 } else if self.current_service == Service::CloudFormationStacks {
1603 let idx = self.column_selector_index;
1604 if idx > 0 && idx <= self.cfn_column_ids.len() {
1605 if let Some(col) = self.cfn_column_ids.get(idx - 1) {
1606 if let Some(pos) =
1607 self.cfn_visible_column_ids.iter().position(|c| c == col)
1608 {
1609 self.cfn_visible_column_ids.remove(pos);
1610 } else {
1611 self.cfn_visible_column_ids.push(*col);
1612 }
1613 }
1614 } else if idx == self.cfn_column_ids.len() + 3 {
1615 self.cfn_state.table.page_size = PageSize::Ten;
1616 } else if idx == self.cfn_column_ids.len() + 4 {
1617 self.cfn_state.table.page_size = PageSize::TwentyFive;
1618 } else if idx == self.cfn_column_ids.len() + 5 {
1619 self.cfn_state.table.page_size = PageSize::Fifty;
1620 } else if idx == self.cfn_column_ids.len() + 6 {
1621 self.cfn_state.table.page_size = PageSize::OneHundred;
1622 }
1623 } else if self.current_service == Service::IamUsers {
1624 let idx = self.column_selector_index;
1625 if self.iam_state.current_user.is_some() {
1626 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
1628 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
1629 if let Some(pos) = self
1630 .iam_policy_visible_column_ids
1631 .iter()
1632 .position(|c| c == col)
1633 {
1634 self.iam_policy_visible_column_ids.remove(pos);
1635 } else {
1636 self.iam_policy_visible_column_ids.push(col.clone());
1637 }
1638 }
1639 } else if idx == self.iam_policy_column_ids.len() + 3 {
1640 self.iam_state.policies.page_size = PageSize::Ten;
1641 } else if idx == self.iam_policy_column_ids.len() + 4 {
1642 self.iam_state.policies.page_size = PageSize::TwentyFive;
1643 } else if idx == self.iam_policy_column_ids.len() + 5 {
1644 self.iam_state.policies.page_size = PageSize::Fifty;
1645 }
1646 } else {
1647 if idx > 0 && idx <= self.iam_user_column_ids.len() {
1649 if let Some(col) = self.iam_user_column_ids.get(idx - 1) {
1650 if let Some(pos) = self
1651 .iam_user_visible_column_ids
1652 .iter()
1653 .position(|c| c == col)
1654 {
1655 self.iam_user_visible_column_ids.remove(pos);
1656 } else {
1657 self.iam_user_visible_column_ids.push(*col);
1658 }
1659 }
1660 } else if idx == self.iam_user_column_ids.len() + 3 {
1661 self.iam_state.users.page_size = PageSize::Ten;
1662 } else if idx == self.iam_user_column_ids.len() + 4 {
1663 self.iam_state.users.page_size = PageSize::TwentyFive;
1664 } else if idx == self.iam_user_column_ids.len() + 5 {
1665 self.iam_state.users.page_size = PageSize::Fifty;
1666 }
1667 }
1668 } else if self.current_service == Service::IamRoles {
1669 let idx = self.column_selector_index;
1670 if self.iam_state.current_role.is_some() {
1671 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
1673 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
1674 if let Some(pos) = self
1675 .iam_policy_visible_column_ids
1676 .iter()
1677 .position(|c| c == col)
1678 {
1679 self.iam_policy_visible_column_ids.remove(pos);
1680 } else {
1681 self.iam_policy_visible_column_ids.push(col.clone());
1682 }
1683 }
1684 } else if idx == self.iam_policy_column_ids.len() + 3 {
1685 self.iam_state.policies.page_size = PageSize::Ten;
1686 } else if idx == self.iam_policy_column_ids.len() + 4 {
1687 self.iam_state.policies.page_size = PageSize::TwentyFive;
1688 } else if idx == self.iam_policy_column_ids.len() + 5 {
1689 self.iam_state.policies.page_size = PageSize::Fifty;
1690 }
1691 } else {
1692 if idx > 0 && idx <= self.iam_role_column_ids.len() {
1694 if let Some(col) = self.iam_role_column_ids.get(idx - 1) {
1695 if let Some(pos) = self
1696 .iam_role_visible_column_ids
1697 .iter()
1698 .position(|c| c == col)
1699 {
1700 self.iam_role_visible_column_ids.remove(pos);
1701 } else {
1702 self.iam_role_visible_column_ids.push(col.clone());
1703 }
1704 }
1705 } else if idx == self.iam_role_column_ids.len() + 3 {
1706 self.iam_state.roles.page_size = PageSize::Ten;
1707 } else if idx == self.iam_role_column_ids.len() + 4 {
1708 self.iam_state.roles.page_size = PageSize::TwentyFive;
1709 } else if idx == self.iam_role_column_ids.len() + 5 {
1710 self.iam_state.roles.page_size = PageSize::Fifty;
1711 }
1712 }
1713 } else if self.current_service == Service::IamUserGroups {
1714 let idx = self.column_selector_index;
1715 if idx > 0 && idx <= self.iam_group_column_ids.len() {
1716 if let Some(col) = self.iam_group_column_ids.get(idx - 1) {
1717 if let Some(pos) = self
1718 .iam_group_visible_column_ids
1719 .iter()
1720 .position(|c| c == col)
1721 {
1722 self.iam_group_visible_column_ids.remove(pos);
1723 } else {
1724 self.iam_group_visible_column_ids.push(col.clone());
1725 }
1726 }
1727 } else if idx == self.iam_group_column_ids.len() + 3 {
1728 self.iam_state.groups.page_size = PageSize::Ten;
1729 } else if idx == self.iam_group_column_ids.len() + 4 {
1730 self.iam_state.groups.page_size = PageSize::TwentyFive;
1731 } else if idx == self.iam_group_column_ids.len() + 5 {
1732 self.iam_state.groups.page_size = PageSize::Fifty;
1733 }
1734 } else if let Some(col) =
1735 self.cw_log_group_column_ids.get(self.column_selector_index)
1736 {
1737 if let Some(pos) = self
1738 .cw_log_group_visible_column_ids
1739 .iter()
1740 .position(|c| c == col)
1741 {
1742 self.cw_log_group_visible_column_ids.remove(pos);
1743 } else {
1744 self.cw_log_group_visible_column_ids.push(*col);
1745 }
1746 }
1747 }
1748 Action::NextPreferences => {
1749 if self.current_service == Service::CloudWatchAlarms {
1750 if self.column_selector_index < 18 {
1752 self.column_selector_index = 18; } else if self.column_selector_index < 22 {
1754 self.column_selector_index = 22; } else if self.column_selector_index < 28 {
1756 self.column_selector_index = 28; } else {
1758 self.column_selector_index = 0; }
1760 } else if self.current_service == Service::EcrRepositories
1761 && self.ecr_state.current_repository.is_some()
1762 {
1763 let page_size_idx = self.ecr_image_column_ids.len() + 2;
1765 if self.column_selector_index < page_size_idx {
1766 self.column_selector_index = page_size_idx;
1767 } else {
1768 self.column_selector_index = 0;
1769 }
1770 } else if self.current_service == Service::LambdaFunctions {
1771 let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
1773 if self.column_selector_index < page_size_idx {
1774 self.column_selector_index = page_size_idx;
1775 } else {
1776 self.column_selector_index = 0;
1777 }
1778 } else if self.current_service == Service::LambdaApplications {
1779 let page_size_idx = self.lambda_application_column_ids.len() + 2;
1781 if self.column_selector_index < page_size_idx {
1782 self.column_selector_index = page_size_idx;
1783 } else {
1784 self.column_selector_index = 0;
1785 }
1786 } else if self.current_service == Service::CloudFormationStacks {
1787 let page_size_idx = self.cfn_column_ids.len() + 2;
1789 if self.column_selector_index < page_size_idx {
1790 self.column_selector_index = page_size_idx;
1791 } else {
1792 self.column_selector_index = 0;
1793 }
1794 } else if self.current_service == Service::IamUsers {
1795 if self.iam_state.current_user.is_some() {
1796 if self.iam_state.user_tab == UserTab::Permissions {
1798 let page_size_idx = self.iam_policy_column_ids.len() + 2;
1799 if self.column_selector_index < page_size_idx {
1800 self.column_selector_index = page_size_idx;
1801 } else {
1802 self.column_selector_index = 0;
1803 }
1804 }
1805 } else {
1807 let page_size_idx = self.iam_user_column_ids.len() + 2;
1809 if self.column_selector_index < page_size_idx {
1810 self.column_selector_index = page_size_idx;
1811 } else {
1812 self.column_selector_index = 0;
1813 }
1814 }
1815 } else if self.current_service == Service::IamRoles {
1816 if self.iam_state.current_role.is_some() {
1817 let page_size_idx = self.iam_policy_column_ids.len() + 2;
1819 if self.column_selector_index < page_size_idx {
1820 self.column_selector_index = page_size_idx;
1821 } else {
1822 self.column_selector_index = 0;
1823 }
1824 } else {
1825 let page_size_idx = self.iam_role_column_ids.len() + 2;
1827 if self.column_selector_index < page_size_idx {
1828 self.column_selector_index = page_size_idx;
1829 } else {
1830 self.column_selector_index = 0;
1831 }
1832 }
1833 } else if self.current_service == Service::IamUserGroups {
1834 let page_size_idx = self.iam_group_column_ids.len() + 2;
1836 if self.column_selector_index < page_size_idx {
1837 self.column_selector_index = page_size_idx;
1838 } else {
1839 self.column_selector_index = 0;
1840 }
1841 } else if self.current_service == Service::SqsQueues
1842 && self.sqs_state.current_queue.is_some()
1843 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1844 {
1845 let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
1847 if self.column_selector_index < page_size_idx {
1848 self.column_selector_index = page_size_idx;
1849 } else {
1850 self.column_selector_index = 0;
1851 }
1852 } else if self.current_service == Service::SqsQueues
1853 && self.sqs_state.current_queue.is_some()
1854 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1855 {
1856 let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
1858 if self.column_selector_index < page_size_idx {
1859 self.column_selector_index = page_size_idx;
1860 } else {
1861 self.column_selector_index = 0;
1862 }
1863 } else if self.current_service == Service::SqsQueues
1864 && self.sqs_state.current_queue.is_some()
1865 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1866 {
1867 let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
1869 if self.column_selector_index < page_size_idx {
1870 self.column_selector_index = page_size_idx;
1871 } else {
1872 self.column_selector_index = 0;
1873 }
1874 } else if self.current_service == Service::SqsQueues
1875 && self.sqs_state.current_queue.is_some()
1876 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
1877 {
1878 let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
1880 if self.column_selector_index < page_size_idx {
1881 self.column_selector_index = page_size_idx;
1882 } else {
1883 self.column_selector_index = 0;
1884 }
1885 }
1886 }
1887 Action::CloseColumnSelector => {
1888 self.mode = Mode::Normal;
1889 self.preference_section = Preferences::Columns;
1890 }
1891 Action::NextDetailTab => {
1892 if self.current_service == Service::SqsQueues
1893 && self.sqs_state.current_queue.is_some()
1894 {
1895 self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
1896 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
1897 self.sqs_state.metrics_loading = true;
1898 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
1899 self.sqs_state.triggers.loading = true;
1900 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
1901 self.sqs_state.pipes.loading = true;
1902 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
1903 self.sqs_state.tags.loading = true;
1904 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
1905 self.sqs_state.subscriptions.loading = true;
1906 }
1907 } else if self.current_service == Service::LambdaApplications
1908 && self.lambda_application_state.current_application.is_some()
1909 {
1910 self.lambda_application_state.detail_tab =
1911 self.lambda_application_state.detail_tab.next();
1912 } else if self.current_service == Service::IamRoles
1913 && self.iam_state.current_role.is_some()
1914 {
1915 self.iam_state.role_tab = self.iam_state.role_tab.next();
1916 if self.iam_state.role_tab == RoleTab::Tags {
1917 self.iam_state.tags.loading = true;
1918 }
1919 } else if self.current_service == Service::IamUsers
1920 && self.iam_state.current_user.is_some()
1921 {
1922 self.iam_state.user_tab = self.iam_state.user_tab.next();
1923 if self.iam_state.user_tab == UserTab::Tags {
1924 self.iam_state.user_tags.loading = true;
1925 }
1926 } else if self.current_service == Service::IamUserGroups
1927 && self.iam_state.current_group.is_some()
1928 {
1929 self.iam_state.group_tab = self.iam_state.group_tab.next();
1930 } else if self.view_mode == ViewMode::Detail {
1931 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
1932 } else if self.current_service == Service::S3Buckets {
1933 if self.s3_state.current_bucket.is_some() {
1934 self.s3_state.object_tab = self.s3_state.object_tab.next();
1935 } else {
1936 self.s3_state.bucket_type = match self.s3_state.bucket_type {
1937 S3BucketType::GeneralPurpose => S3BucketType::Directory,
1938 S3BucketType::Directory => S3BucketType::GeneralPurpose,
1939 };
1940 self.s3_state.buckets.reset();
1941 }
1942 } else if self.current_service == Service::CloudWatchAlarms {
1943 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
1944 AlarmTab::AllAlarms => AlarmTab::InAlarm,
1945 AlarmTab::InAlarm => AlarmTab::AllAlarms,
1946 };
1947 self.alarms_state.table.reset();
1948 } else if self.current_service == Service::EcrRepositories
1949 && self.ecr_state.current_repository.is_none()
1950 {
1951 self.ecr_state.tab = self.ecr_state.tab.next();
1952 self.ecr_state.repositories.reset();
1953 self.ecr_state.repositories.loading = true;
1954 } else if self.current_service == Service::LambdaFunctions
1955 && self.lambda_state.current_function.is_some()
1956 {
1957 if self.lambda_state.current_version.is_some() {
1958 self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
1960 LambdaDetailTab::Code => LambdaDetailTab::Configuration,
1961 _ => LambdaDetailTab::Code,
1962 };
1963 } else {
1964 self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
1965 }
1966 } else if self.current_service == Service::CloudFormationStacks
1967 && self.cfn_state.current_stack.is_some()
1968 {
1969 self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
1970 }
1971 }
1972 Action::PrevDetailTab => {
1973 if self.current_service == Service::SqsQueues
1974 && self.sqs_state.current_queue.is_some()
1975 {
1976 self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
1977 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
1978 self.sqs_state.metrics_loading = true;
1979 }
1980 } else if self.current_service == Service::LambdaApplications
1981 && self.lambda_application_state.current_application.is_some()
1982 {
1983 self.lambda_application_state.detail_tab =
1984 self.lambda_application_state.detail_tab.prev();
1985 } else if self.current_service == Service::IamRoles
1986 && self.iam_state.current_role.is_some()
1987 {
1988 self.iam_state.role_tab = self.iam_state.role_tab.prev();
1989 } else if self.current_service == Service::IamUsers
1990 && self.iam_state.current_user.is_some()
1991 {
1992 self.iam_state.user_tab = self.iam_state.user_tab.prev();
1993 } else if self.current_service == Service::IamUserGroups
1994 && self.iam_state.current_group.is_some()
1995 {
1996 self.iam_state.group_tab = self.iam_state.group_tab.prev();
1997 } else if self.view_mode == ViewMode::Detail {
1998 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
1999 } else if self.current_service == Service::S3Buckets {
2000 if self.s3_state.current_bucket.is_some() {
2001 self.s3_state.object_tab = self.s3_state.object_tab.prev();
2002 }
2003 } else if self.current_service == Service::CloudWatchAlarms {
2004 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2005 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2006 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2007 };
2008 } else if self.current_service == Service::EcrRepositories
2009 && self.ecr_state.current_repository.is_none()
2010 {
2011 self.ecr_state.tab = self.ecr_state.tab.prev();
2012 self.ecr_state.repositories.reset();
2013 self.ecr_state.repositories.loading = true;
2014 } else if self.current_service == Service::LambdaFunctions
2015 && self.lambda_state.current_function.is_some()
2016 {
2017 if self.lambda_state.current_version.is_some() {
2018 self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
2020 LambdaDetailTab::Configuration => LambdaDetailTab::Code,
2021 _ => LambdaDetailTab::Configuration,
2022 };
2023 } else {
2024 self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
2025 }
2026 } else if self.current_service == Service::CloudFormationStacks
2027 && self.cfn_state.current_stack.is_some()
2028 {
2029 self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
2030 }
2031 }
2032 Action::StartFilter => {
2033 if !self.service_selected && self.tabs.is_empty() {
2035 return;
2036 }
2037
2038 if self.current_service == Service::CloudWatchInsights {
2039 self.mode = Mode::InsightsInput;
2040 } else if self.current_service == Service::CloudWatchAlarms {
2041 self.mode = Mode::FilterInput;
2042 } else if self.current_service == Service::S3Buckets {
2043 self.mode = Mode::FilterInput;
2044 self.log_groups_state.filter_mode = true;
2045 } else if self.current_service == Service::EcrRepositories
2046 || self.current_service == Service::IamUsers
2047 || self.current_service == Service::IamUserGroups
2048 {
2049 self.mode = Mode::FilterInput;
2050 if self.current_service == Service::EcrRepositories
2051 && self.ecr_state.current_repository.is_none()
2052 {
2053 self.ecr_state.input_focus = InputFocus::Filter;
2054 }
2055 } else if self.current_service == Service::LambdaFunctions {
2056 self.mode = Mode::FilterInput;
2057 if self.lambda_state.current_version.is_some()
2058 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2059 {
2060 self.lambda_state.alias_input_focus = InputFocus::Filter;
2061 } else if self.lambda_state.current_function.is_some()
2062 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2063 {
2064 self.lambda_state.version_input_focus = InputFocus::Filter;
2065 } else if self.lambda_state.current_function.is_none() {
2066 self.lambda_state.input_focus = InputFocus::Filter;
2067 }
2068 } else if self.current_service == Service::LambdaApplications {
2069 self.mode = Mode::FilterInput;
2070 if self.lambda_application_state.current_application.is_some() {
2071 if self.lambda_application_state.detail_tab
2073 == LambdaApplicationDetailTab::Overview
2074 {
2075 self.lambda_application_state.resource_input_focus = InputFocus::Filter;
2076 } else {
2077 self.lambda_application_state.deployment_input_focus =
2078 InputFocus::Filter;
2079 }
2080 } else {
2081 self.lambda_application_state.input_focus = InputFocus::Filter;
2082 }
2083 } else if self.current_service == Service::IamRoles {
2084 self.mode = Mode::FilterInput;
2085 } else if self.current_service == Service::CloudFormationStacks {
2086 self.mode = Mode::FilterInput;
2087 self.cfn_state.input_focus = InputFocus::Filter;
2088 } else if self.current_service == Service::SqsQueues {
2089 self.mode = Mode::FilterInput;
2090 self.sqs_state.input_focus = InputFocus::Filter;
2091 } else if self.view_mode == ViewMode::List
2092 || (self.view_mode == ViewMode::Detail
2093 && self.log_groups_state.detail_tab == DetailTab::LogStreams)
2094 {
2095 self.mode = Mode::FilterInput;
2096 self.log_groups_state.filter_mode = true;
2097 self.log_groups_state.input_focus = InputFocus::Filter;
2098 }
2099 }
2100 Action::StartEventFilter => {
2101 if self.current_service == Service::CloudWatchInsights {
2102 self.mode = Mode::InsightsInput;
2103 } else if self.view_mode == ViewMode::List {
2104 self.mode = Mode::FilterInput;
2105 self.log_groups_state.filter_mode = true;
2106 self.log_groups_state.input_focus = InputFocus::Filter;
2107 } else if self.view_mode == ViewMode::Events {
2108 self.mode = Mode::EventFilterInput;
2109 } else if self.view_mode == ViewMode::Detail
2110 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2111 {
2112 self.mode = Mode::FilterInput;
2113 self.log_groups_state.filter_mode = true;
2114 self.log_groups_state.input_focus = InputFocus::Filter;
2115 }
2116 }
2117 Action::NextFilterFocus => {
2118 if self.mode == Mode::FilterInput
2119 && self.current_service == Service::LambdaApplications
2120 {
2121 use crate::ui::lambda::FILTER_CONTROLS;
2122 if self.lambda_application_state.current_application.is_some() {
2123 if self.lambda_application_state.detail_tab
2124 == LambdaApplicationDetailTab::Deployments
2125 {
2126 self.lambda_application_state.deployment_input_focus = self
2127 .lambda_application_state
2128 .deployment_input_focus
2129 .next(&FILTER_CONTROLS);
2130 } else {
2131 self.lambda_application_state.resource_input_focus = self
2132 .lambda_application_state
2133 .resource_input_focus
2134 .next(&FILTER_CONTROLS);
2135 }
2136 } else {
2137 self.lambda_application_state.input_focus = self
2138 .lambda_application_state
2139 .input_focus
2140 .next(&FILTER_CONTROLS);
2141 }
2142 } else if self.mode == Mode::FilterInput
2143 && self.current_service == Service::IamRoles
2144 && self.iam_state.current_role.is_some()
2145 {
2146 use crate::ui::iam::POLICY_FILTER_CONTROLS;
2147 self.iam_state.policy_input_focus = self
2148 .iam_state
2149 .policy_input_focus
2150 .next(&POLICY_FILTER_CONTROLS);
2151 } else if self.mode == Mode::FilterInput
2152 && self.current_service == Service::IamRoles
2153 && self.iam_state.current_role.is_none()
2154 {
2155 use crate::ui::iam::ROLE_FILTER_CONTROLS;
2156 self.iam_state.role_input_focus =
2157 self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
2158 } else if self.mode == Mode::InsightsInput {
2159 use crate::app::InsightsFocus;
2160 self.insights_state.insights.insights_focus =
2161 match self.insights_state.insights.insights_focus {
2162 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
2163 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
2164 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
2165 InsightsFocus::Query => InsightsFocus::QueryLanguage,
2166 };
2167 } else if self.mode == Mode::FilterInput
2168 && self.current_service == Service::CloudFormationStacks
2169 {
2170 self.cfn_state.input_focus = self
2171 .cfn_state
2172 .input_focus
2173 .next(&crate::ui::cfn::State::FILTER_CONTROLS);
2174 } else if self.mode == Mode::FilterInput
2175 && self.current_service == Service::SqsQueues
2176 {
2177 if self.sqs_state.current_queue.is_some()
2178 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2179 {
2180 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2181 self.sqs_state.input_focus = self
2182 .sqs_state
2183 .input_focus
2184 .next(SUBSCRIPTION_FILTER_CONTROLS);
2185 } else {
2186 use crate::ui::sqs::FILTER_CONTROLS;
2187 self.sqs_state.input_focus =
2188 self.sqs_state.input_focus.next(FILTER_CONTROLS);
2189 }
2190 } else if self.mode == Mode::FilterInput
2191 && self.current_service == Service::CloudWatchLogGroups
2192 {
2193 use crate::ui::cw::logs::FILTER_CONTROLS;
2194 self.log_groups_state.input_focus =
2195 self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
2196 } else if self.mode == Mode::EventFilterInput {
2197 self.log_groups_state.event_input_focus =
2198 self.log_groups_state.event_input_focus.next();
2199 } else if self.mode == Mode::FilterInput
2200 && self.current_service == Service::CloudWatchAlarms
2201 {
2202 use crate::ui::cw::alarms::FILTER_CONTROLS;
2203 self.alarms_state.input_focus =
2204 self.alarms_state.input_focus.next(&FILTER_CONTROLS);
2205 } else if self.mode == Mode::FilterInput
2206 && self.current_service == Service::EcrRepositories
2207 && self.ecr_state.current_repository.is_none()
2208 {
2209 use crate::ui::ecr::FILTER_CONTROLS;
2210 self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
2211 } else if self.mode == Mode::FilterInput
2212 && self.current_service == Service::LambdaFunctions
2213 {
2214 use crate::ui::lambda::FILTER_CONTROLS;
2215 if self.lambda_state.current_version.is_some()
2216 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2217 {
2218 self.lambda_state.alias_input_focus =
2219 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
2220 } else if self.lambda_state.current_function.is_some()
2221 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2222 {
2223 self.lambda_state.version_input_focus =
2224 self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
2225 } else if self.lambda_state.current_function.is_some()
2226 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
2227 {
2228 self.lambda_state.alias_input_focus =
2229 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
2230 } else if self.lambda_state.current_function.is_none() {
2231 self.lambda_state.input_focus =
2232 self.lambda_state.input_focus.next(&FILTER_CONTROLS);
2233 }
2234 }
2235 }
2236 Action::PrevFilterFocus => {
2237 if self.mode == Mode::FilterInput
2238 && self.current_service == Service::LambdaApplications
2239 {
2240 use crate::ui::lambda::FILTER_CONTROLS;
2241 if self.lambda_application_state.current_application.is_some() {
2242 if self.lambda_application_state.detail_tab
2243 == LambdaApplicationDetailTab::Deployments
2244 {
2245 self.lambda_application_state.deployment_input_focus = self
2246 .lambda_application_state
2247 .deployment_input_focus
2248 .prev(&FILTER_CONTROLS);
2249 } else {
2250 self.lambda_application_state.resource_input_focus = self
2251 .lambda_application_state
2252 .resource_input_focus
2253 .prev(&FILTER_CONTROLS);
2254 }
2255 } else {
2256 self.lambda_application_state.input_focus = self
2257 .lambda_application_state
2258 .input_focus
2259 .prev(&FILTER_CONTROLS);
2260 }
2261 } else if self.mode == Mode::FilterInput
2262 && self.current_service == Service::CloudFormationStacks
2263 {
2264 self.cfn_state.input_focus = self
2265 .cfn_state
2266 .input_focus
2267 .prev(&crate::ui::cfn::State::FILTER_CONTROLS);
2268 } else if self.mode == Mode::FilterInput
2269 && self.current_service == Service::SqsQueues
2270 {
2271 if self.sqs_state.current_queue.is_some()
2272 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2273 {
2274 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2275 self.sqs_state.input_focus = self
2276 .sqs_state
2277 .input_focus
2278 .prev(SUBSCRIPTION_FILTER_CONTROLS);
2279 } else {
2280 use crate::ui::sqs::FILTER_CONTROLS;
2281 self.sqs_state.input_focus =
2282 self.sqs_state.input_focus.prev(FILTER_CONTROLS);
2283 }
2284 } else if self.mode == Mode::FilterInput
2285 && self.current_service == Service::IamRoles
2286 && self.iam_state.current_role.is_none()
2287 {
2288 use crate::ui::iam::ROLE_FILTER_CONTROLS;
2289 self.iam_state.role_input_focus =
2290 self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
2291 } else if self.mode == Mode::FilterInput
2292 && self.current_service == Service::CloudWatchLogGroups
2293 {
2294 use crate::ui::cw::logs::FILTER_CONTROLS;
2295 self.log_groups_state.input_focus =
2296 self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
2297 } else if self.mode == Mode::EventFilterInput {
2298 self.log_groups_state.event_input_focus =
2299 self.log_groups_state.event_input_focus.prev();
2300 } else if self.mode == Mode::FilterInput
2301 && self.current_service == Service::IamRoles
2302 && self.iam_state.current_role.is_some()
2303 {
2304 use crate::ui::iam::POLICY_FILTER_CONTROLS;
2305 self.iam_state.policy_input_focus = self
2306 .iam_state
2307 .policy_input_focus
2308 .prev(&POLICY_FILTER_CONTROLS);
2309 } else if self.mode == Mode::FilterInput
2310 && self.current_service == Service::CloudWatchAlarms
2311 {
2312 use crate::ui::cw::alarms::FILTER_CONTROLS;
2313 self.alarms_state.input_focus =
2314 self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
2315 } else if self.mode == Mode::FilterInput
2316 && self.current_service == Service::EcrRepositories
2317 && self.ecr_state.current_repository.is_none()
2318 {
2319 use crate::ui::ecr::FILTER_CONTROLS;
2320 self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
2321 } else if self.mode == Mode::FilterInput
2322 && self.current_service == Service::LambdaFunctions
2323 {
2324 use crate::ui::lambda::FILTER_CONTROLS;
2325 if self.lambda_state.current_version.is_some()
2326 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2327 {
2328 self.lambda_state.alias_input_focus =
2329 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2330 } else if self.lambda_state.current_function.is_some()
2331 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2332 {
2333 self.lambda_state.version_input_focus =
2334 self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
2335 } else if self.lambda_state.current_function.is_some()
2336 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
2337 {
2338 self.lambda_state.alias_input_focus =
2339 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2340 } else if self.lambda_state.current_function.is_none() {
2341 self.lambda_state.input_focus =
2342 self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
2343 }
2344 }
2345 }
2346 Action::ToggleFilterCheckbox => {
2347 if self.mode == Mode::InsightsInput {
2348 use crate::app::InsightsFocus;
2349 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2350 && self.insights_state.insights.show_dropdown
2351 && !self.insights_state.insights.log_group_matches.is_empty()
2352 {
2353 let selected_idx = self.insights_state.insights.dropdown_selected;
2354 if let Some(group_name) = self
2355 .insights_state
2356 .insights
2357 .log_group_matches
2358 .get(selected_idx)
2359 {
2360 let group_name = group_name.clone();
2361 if let Some(pos) = self
2362 .insights_state
2363 .insights
2364 .selected_log_groups
2365 .iter()
2366 .position(|g| g == &group_name)
2367 {
2368 self.insights_state.insights.selected_log_groups.remove(pos);
2369 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2370 self.insights_state
2371 .insights
2372 .selected_log_groups
2373 .push(group_name);
2374 }
2375 }
2376 }
2377 } else if self.mode == Mode::FilterInput
2378 && self.current_service == Service::CloudFormationStacks
2379 {
2380 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
2381 match self.cfn_state.input_focus {
2382 STATUS_FILTER => {
2383 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
2384 }
2385 VIEW_NESTED => {
2386 self.cfn_state.view_nested = !self.cfn_state.view_nested;
2387 }
2388 _ => {}
2389 }
2390 } else if self.mode == Mode::FilterInput
2391 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2392 {
2393 match self.log_groups_state.input_focus {
2394 InputFocus::Checkbox("ExactMatch") => {
2395 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2396 }
2397 InputFocus::Checkbox("ShowExpired") => {
2398 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2399 }
2400 _ => {}
2401 }
2402 } else if self.mode == Mode::EventFilterInput
2403 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2404 {
2405 self.log_groups_state.relative_unit =
2406 self.log_groups_state.relative_unit.next();
2407 }
2408 }
2409 Action::CycleSortColumn => {
2410 if self.view_mode == ViewMode::Detail
2411 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2412 {
2413 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2414 StreamSort::Name => StreamSort::CreationTime,
2415 StreamSort::CreationTime => StreamSort::LastEventTime,
2416 StreamSort::LastEventTime => StreamSort::Name,
2417 };
2418 }
2419 }
2420 Action::ToggleSortDirection => {
2421 if self.view_mode == ViewMode::Detail
2422 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2423 {
2424 self.log_groups_state.stream_sort_desc =
2425 !self.log_groups_state.stream_sort_desc;
2426 }
2427 }
2428 Action::ScrollUp => {
2429 if self.mode == Mode::ErrorModal {
2430 self.error_scroll = self.error_scroll.saturating_sub(1);
2431 } else if self.current_service == Service::SqsQueues
2432 && self.sqs_state.current_queue.is_some()
2433 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2434 {
2435 self.sqs_state.monitoring_scroll =
2436 self.sqs_state.monitoring_scroll.saturating_sub(5);
2437 } else if self.view_mode == ViewMode::PolicyView {
2438 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
2439 } else if self.current_service == Service::IamRoles
2440 && self.iam_state.current_role.is_some()
2441 && self.iam_state.role_tab == RoleTab::TrustRelationships
2442 {
2443 self.iam_state.trust_policy_scroll =
2444 self.iam_state.trust_policy_scroll.saturating_sub(10);
2445 } else if self.view_mode == ViewMode::Events {
2446 if self.log_groups_state.event_scroll_offset == 0
2447 && self.log_groups_state.has_older_events
2448 {
2449 self.log_groups_state.loading = true;
2450 } else {
2451 self.log_groups_state.event_scroll_offset =
2452 self.log_groups_state.event_scroll_offset.saturating_sub(1);
2453 }
2454 } else if self.view_mode == ViewMode::InsightsResults {
2455 self.insights_state.insights.results_selected = self
2456 .insights_state
2457 .insights
2458 .results_selected
2459 .saturating_sub(1);
2460 } else if self.view_mode == ViewMode::Detail {
2461 self.log_groups_state.selected_stream =
2462 self.log_groups_state.selected_stream.saturating_sub(1);
2463 self.log_groups_state.expanded_stream = None;
2464 } else if self.view_mode == ViewMode::List
2465 && self.current_service == Service::CloudWatchLogGroups
2466 {
2467 self.log_groups_state.log_groups.selected =
2468 self.log_groups_state.log_groups.selected.saturating_sub(1);
2469 self.log_groups_state.log_groups.snap_to_page();
2470 } else if self.current_service == Service::EcrRepositories {
2471 if self.ecr_state.current_repository.is_some() {
2472 self.ecr_state.images.page_up();
2473 } else {
2474 self.ecr_state.repositories.page_up();
2475 }
2476 }
2477 }
2478 Action::ScrollDown => {
2479 if self.mode == Mode::ErrorModal {
2480 if let Some(error_msg) = &self.error_message {
2481 let lines = error_msg.lines().count();
2482 let max_scroll = lines.saturating_sub(1);
2483 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
2484 }
2485 } else if self.current_service == Service::SqsQueues
2486 && self.sqs_state.current_queue.is_some()
2487 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2488 {
2489 self.sqs_state.monitoring_scroll =
2490 (self.sqs_state.monitoring_scroll + 1).min(1);
2491 } else if self.view_mode == ViewMode::PolicyView {
2492 let lines = self.iam_state.policy_document.lines().count();
2493 let max_scroll = lines.saturating_sub(1);
2494 self.iam_state.policy_scroll =
2495 (self.iam_state.policy_scroll + 10).min(max_scroll);
2496 } else if self.current_service == Service::IamRoles
2497 && self.iam_state.current_role.is_some()
2498 && self.iam_state.role_tab == RoleTab::TrustRelationships
2499 {
2500 let lines = self.iam_state.trust_policy_document.lines().count();
2501 let max_scroll = lines.saturating_sub(1);
2502 self.iam_state.trust_policy_scroll =
2503 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
2504 } else if self.view_mode == ViewMode::Events {
2505 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
2506 if self.log_groups_state.event_scroll_offset >= max_scroll {
2507 } else {
2509 self.log_groups_state.event_scroll_offset =
2510 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
2511 }
2512 } else if self.view_mode == ViewMode::InsightsResults {
2513 let max = self
2514 .insights_state
2515 .insights
2516 .query_results
2517 .len()
2518 .saturating_sub(1);
2519 self.insights_state.insights.results_selected =
2520 (self.insights_state.insights.results_selected + 1).min(max);
2521 } else if self.view_mode == ViewMode::Detail {
2522 let filtered_streams = self.filtered_log_streams();
2523 let max = filtered_streams.len().saturating_sub(1);
2524 self.log_groups_state.selected_stream =
2525 (self.log_groups_state.selected_stream + 1).min(max);
2526 } else if self.view_mode == ViewMode::List
2527 && self.current_service == Service::CloudWatchLogGroups
2528 {
2529 let filtered_groups = self.filtered_log_groups();
2530 self.log_groups_state
2531 .log_groups
2532 .next_item(filtered_groups.len());
2533 } else if self.current_service == Service::EcrRepositories {
2534 if self.ecr_state.current_repository.is_some() {
2535 let filtered_images = self.filtered_ecr_images();
2536 self.ecr_state.images.page_down(filtered_images.len());
2537 } else {
2538 let filtered_repos = self.filtered_ecr_repositories();
2539 self.ecr_state.repositories.page_down(filtered_repos.len());
2540 }
2541 }
2542 }
2543
2544 Action::Refresh => {
2545 if self.mode == Mode::ProfilePicker {
2546 self.log_groups_state.loading = true;
2547 self.log_groups_state.loading_message = "Refreshing...".to_string();
2548 } else if self.mode == Mode::RegionPicker {
2549 self.measure_region_latencies();
2550 } else if self.mode == Mode::SessionPicker {
2551 self.sessions = Session::list_all().unwrap_or_default();
2552 } else if self.current_service == Service::CloudWatchInsights
2553 && !self.insights_state.insights.selected_log_groups.is_empty()
2554 {
2555 self.log_groups_state.loading = true;
2556 self.insights_state.insights.query_completed = true;
2557 } else if self.current_service == Service::LambdaFunctions {
2558 self.lambda_state.table.loading = true;
2559 } else if self.current_service == Service::LambdaApplications {
2560 self.lambda_application_state.table.loading = true;
2561 } else if matches!(
2562 self.view_mode,
2563 ViewMode::Events | ViewMode::Detail | ViewMode::List
2564 ) {
2565 self.log_groups_state.loading = true;
2566 }
2567 }
2568 Action::Yank => {
2569 if self.mode == Mode::ErrorModal {
2570 if let Some(error) = &self.error_message {
2572 copy_to_clipboard(error);
2573 }
2574 } else if self.view_mode == ViewMode::Events {
2575 if let Some(event) = self
2576 .log_groups_state
2577 .log_events
2578 .get(self.log_groups_state.event_scroll_offset)
2579 {
2580 copy_to_clipboard(&event.message);
2581 }
2582 } else if self.current_service == Service::EcrRepositories {
2583 if self.ecr_state.current_repository.is_some() {
2584 let filtered_images = self.filtered_ecr_images();
2585 if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
2586 copy_to_clipboard(&image.uri);
2587 }
2588 } else {
2589 let filtered_repos = self.filtered_ecr_repositories();
2590 if let Some(repo) =
2591 self.ecr_state.repositories.get_selected(&filtered_repos)
2592 {
2593 copy_to_clipboard(&repo.uri);
2594 }
2595 }
2596 } else if self.current_service == Service::LambdaFunctions {
2597 let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
2598 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
2599 copy_to_clipboard(&func.arn);
2600 }
2601 } else if self.current_service == Service::CloudFormationStacks {
2602 if let Some(stack_name) = &self.cfn_state.current_stack {
2603 if let Some(stack) = self
2605 .cfn_state
2606 .table
2607 .items
2608 .iter()
2609 .find(|s| &s.name == stack_name)
2610 {
2611 copy_to_clipboard(&stack.stack_id);
2612 }
2613 } else {
2614 let filtered_stacks = self.filtered_cloudformation_stacks();
2616 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
2617 copy_to_clipboard(&stack.stack_id);
2618 }
2619 }
2620 } else if self.current_service == Service::IamUsers {
2621 if self.iam_state.current_user.is_some() {
2622 if let Some(user_name) = &self.iam_state.current_user {
2623 if let Some(user) = self
2624 .iam_state
2625 .users
2626 .items
2627 .iter()
2628 .find(|u| u.user_name == *user_name)
2629 {
2630 copy_to_clipboard(&user.arn);
2631 }
2632 }
2633 } else {
2634 let filtered_users = crate::ui::iam::filtered_iam_users(self);
2635 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
2636 copy_to_clipboard(&user.arn);
2637 }
2638 }
2639 } else if self.current_service == Service::IamRoles {
2640 if self.iam_state.current_role.is_some() {
2641 if let Some(role_name) = &self.iam_state.current_role {
2642 if let Some(role) = self
2643 .iam_state
2644 .roles
2645 .items
2646 .iter()
2647 .find(|r| r.role_name == *role_name)
2648 {
2649 copy_to_clipboard(&role.arn);
2650 }
2651 }
2652 } else {
2653 let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
2654 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
2655 copy_to_clipboard(&role.arn);
2656 }
2657 }
2658 } else if self.current_service == Service::IamUserGroups {
2659 if self.iam_state.current_group.is_some() {
2660 if let Some(group_name) = &self.iam_state.current_group {
2661 let arn = iam::format_arn(&self.config.account_id, "group", group_name);
2662 copy_to_clipboard(&arn);
2663 }
2664 } else {
2665 let filtered_groups: Vec<_> = self
2666 .iam_state
2667 .groups
2668 .items
2669 .iter()
2670 .filter(|g| {
2671 if self.iam_state.groups.filter.is_empty() {
2672 true
2673 } else {
2674 g.group_name
2675 .to_lowercase()
2676 .contains(&self.iam_state.groups.filter.to_lowercase())
2677 }
2678 })
2679 .collect();
2680 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
2681 let arn = iam::format_arn(
2682 &self.config.account_id,
2683 "group",
2684 &group.group_name,
2685 );
2686 copy_to_clipboard(&arn);
2687 }
2688 }
2689 } else if self.current_service == Service::SqsQueues {
2690 if self.sqs_state.current_queue.is_some() {
2691 if let Some(queue) = self
2693 .sqs_state
2694 .queues
2695 .items
2696 .iter()
2697 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
2698 {
2699 let arn = format!(
2700 "arn:aws:sqs:{}:{}:{}",
2701 crate::ui::sqs::extract_region(&queue.url),
2702 crate::ui::sqs::extract_account_id(&queue.url),
2703 queue.name
2704 );
2705 copy_to_clipboard(&arn);
2706 }
2707 } else {
2708 let filtered_queues = crate::ui::sqs::filtered_queues(
2710 &self.sqs_state.queues.items,
2711 &self.sqs_state.queues.filter,
2712 );
2713 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
2714 let arn = format!(
2715 "arn:aws:sqs:{}:{}:{}",
2716 crate::ui::sqs::extract_region(&queue.url),
2717 crate::ui::sqs::extract_account_id(&queue.url),
2718 queue.name
2719 );
2720 copy_to_clipboard(&arn);
2721 }
2722 }
2723 }
2724 }
2725 Action::CopyToClipboard => {
2726 self.snapshot_requested = true;
2728 }
2729 Action::RetryLoad => {
2730 self.error_message = None;
2731 self.mode = Mode::Normal;
2732 self.log_groups_state.loading = true;
2733 }
2734 Action::ApplyFilter => {
2735 if self.mode == Mode::FilterInput
2736 && self.current_service == Service::SqsQueues
2737 && self.sqs_state.input_focus
2738 == crate::common::InputFocus::Dropdown("SubscriptionRegion")
2739 {
2740 let regions = crate::aws::Region::all();
2741 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
2742 self.sqs_state.subscription_region_filter = region.code.to_string();
2743 }
2744 self.mode = Mode::Normal;
2745 } else if self.mode == Mode::InsightsInput {
2746 use crate::app::InsightsFocus;
2747 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2748 && self.insights_state.insights.show_dropdown
2749 {
2750 self.insights_state.insights.show_dropdown = false;
2752 self.mode = Mode::Normal;
2753 if !self.insights_state.insights.selected_log_groups.is_empty() {
2754 self.log_groups_state.loading = true;
2755 self.insights_state.insights.query_completed = true;
2756 }
2757 }
2758 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
2759 if let Ok(page) = self.page_input.parse::<usize>() {
2760 self.go_to_page(page);
2761 }
2762 self.page_input.clear();
2763 } else {
2764 self.mode = Mode::Normal;
2765 self.log_groups_state.filter_mode = false;
2766 }
2767 }
2768 Action::ToggleExactMatch => {
2769 if self.view_mode == ViewMode::Detail
2770 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2771 {
2772 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
2773 }
2774 }
2775 Action::ToggleShowExpired => {
2776 if self.view_mode == ViewMode::Detail
2777 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2778 {
2779 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
2780 }
2781 }
2782 Action::GoBack => {
2783 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
2785 self.mode = Mode::Normal;
2786 self.service_picker.filter.clear();
2787 }
2788 else if self.current_service == Service::S3Buckets
2790 && self.s3_state.current_bucket.is_some()
2791 {
2792 if !self.s3_state.prefix_stack.is_empty() {
2793 self.s3_state.prefix_stack.pop();
2794 self.s3_state.buckets.loading = true;
2795 } else {
2796 self.s3_state.current_bucket = None;
2797 self.s3_state.objects.clear();
2798 }
2799 }
2800 else if self.current_service == Service::EcrRepositories
2802 && self.ecr_state.current_repository.is_some()
2803 {
2804 if self.ecr_state.images.has_expanded_item() {
2805 self.ecr_state.images.collapse();
2806 } else {
2807 self.ecr_state.current_repository = None;
2808 self.ecr_state.current_repository_uri = None;
2809 self.ecr_state.images.items.clear();
2810 self.ecr_state.images.reset();
2811 }
2812 }
2813 else if self.current_service == Service::SqsQueues
2815 && self.sqs_state.current_queue.is_some()
2816 {
2817 self.sqs_state.current_queue = None;
2818 }
2819 else if self.current_service == Service::IamUsers
2821 && self.iam_state.current_user.is_some()
2822 {
2823 self.iam_state.current_user = None;
2824 self.iam_state.policies.items.clear();
2825 self.iam_state.policies.reset();
2826 self.update_current_tab_breadcrumb();
2827 }
2828 else if self.current_service == Service::IamUserGroups
2830 && self.iam_state.current_group.is_some()
2831 {
2832 self.iam_state.current_group = None;
2833 self.update_current_tab_breadcrumb();
2834 }
2835 else if self.current_service == Service::IamRoles {
2837 if self.view_mode == ViewMode::PolicyView {
2838 self.view_mode = ViewMode::Detail;
2840 self.iam_state.current_policy = None;
2841 self.iam_state.policy_document.clear();
2842 self.iam_state.policy_scroll = 0;
2843 self.update_current_tab_breadcrumb();
2844 } else if self.iam_state.current_role.is_some() {
2845 self.iam_state.current_role = None;
2846 self.iam_state.policies.items.clear();
2847 self.iam_state.policies.reset();
2848 self.update_current_tab_breadcrumb();
2849 }
2850 }
2851 else if self.current_service == Service::LambdaFunctions
2853 && self.lambda_state.current_version.is_some()
2854 {
2855 self.lambda_state.current_version = None;
2856 self.lambda_state.detail_tab = LambdaDetailTab::Versions;
2857 }
2858 else if self.current_service == Service::LambdaFunctions
2860 && self.lambda_state.current_alias.is_some()
2861 {
2862 self.lambda_state.current_alias = None;
2863 self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
2864 }
2865 else if self.current_service == Service::LambdaFunctions
2867 && self.lambda_state.current_function.is_some()
2868 {
2869 self.lambda_state.current_function = None;
2870 self.update_current_tab_breadcrumb();
2871 }
2872 else if self.current_service == Service::LambdaApplications
2874 && self.lambda_application_state.current_application.is_some()
2875 {
2876 self.lambda_application_state.current_application = None;
2877 self.update_current_tab_breadcrumb();
2878 }
2879 else if self.current_service == Service::CloudFormationStacks
2881 && self.cfn_state.current_stack.is_some()
2882 {
2883 self.cfn_state.current_stack = None;
2884 self.update_current_tab_breadcrumb();
2885 }
2886 else if self.view_mode == ViewMode::InsightsResults {
2888 if self.insights_state.insights.expanded_result.is_some() {
2889 self.insights_state.insights.expanded_result = None;
2890 }
2891 }
2892 else if self.current_service == Service::CloudWatchAlarms {
2894 if self.alarms_state.table.has_expanded_item() {
2895 self.alarms_state.table.collapse();
2896 }
2897 }
2898 else if self.view_mode == ViewMode::Events {
2900 if self.log_groups_state.expanded_event.is_some() {
2901 self.log_groups_state.expanded_event = None;
2902 } else {
2903 self.view_mode = ViewMode::Detail;
2904 self.log_groups_state.event_filter.clear();
2905 }
2906 }
2907 else if self.view_mode == ViewMode::Detail {
2909 self.view_mode = ViewMode::List;
2910 self.log_groups_state.stream_filter.clear();
2911 self.log_groups_state.exact_match = false;
2912 self.log_groups_state.show_expired = false;
2913 }
2914 }
2915 Action::OpenInConsole | Action::OpenInBrowser => {
2916 let url = self.get_console_url();
2917 let _ = webbrowser::open(&url);
2918 }
2919 Action::ShowHelp => {
2920 self.mode = Mode::HelpModal;
2921 }
2922 Action::OpenRegionPicker => {
2923 self.region_filter.clear();
2924 self.region_picker_selected = 0;
2925 self.measure_region_latencies();
2926 self.mode = Mode::RegionPicker;
2927 }
2928 Action::OpenProfilePicker => {
2929 self.profile_filter.clear();
2930 self.profile_picker_selected = 0;
2931 self.available_profiles = Self::load_aws_profiles();
2932 self.mode = Mode::ProfilePicker;
2933 }
2934 Action::OpenCalendar => {
2935 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
2936 self.calendar_selecting = CalendarField::StartDate;
2937 self.mode = Mode::CalendarPicker;
2938 }
2939 Action::CloseCalendar => {
2940 self.mode = Mode::Normal;
2941 self.calendar_date = None;
2942 }
2943 Action::CalendarPrevDay => {
2944 if let Some(date) = self.calendar_date {
2945 self.calendar_date = date.checked_sub(time::Duration::days(1));
2946 }
2947 }
2948 Action::CalendarNextDay => {
2949 if let Some(date) = self.calendar_date {
2950 self.calendar_date = date.checked_add(time::Duration::days(1));
2951 }
2952 }
2953 Action::CalendarPrevWeek => {
2954 if let Some(date) = self.calendar_date {
2955 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
2956 }
2957 }
2958 Action::CalendarNextWeek => {
2959 if let Some(date) = self.calendar_date {
2960 self.calendar_date = date.checked_add(time::Duration::weeks(1));
2961 }
2962 }
2963 Action::CalendarPrevMonth => {
2964 if let Some(date) = self.calendar_date {
2965 self.calendar_date = Some(if date.month() == time::Month::January {
2966 date.replace_month(time::Month::December)
2967 .unwrap()
2968 .replace_year(date.year() - 1)
2969 .unwrap()
2970 } else {
2971 date.replace_month(date.month().previous()).unwrap()
2972 });
2973 }
2974 }
2975 Action::CalendarNextMonth => {
2976 if let Some(date) = self.calendar_date {
2977 self.calendar_date = Some(if date.month() == time::Month::December {
2978 date.replace_month(time::Month::January)
2979 .unwrap()
2980 .replace_year(date.year() + 1)
2981 .unwrap()
2982 } else {
2983 date.replace_month(date.month().next()).unwrap()
2984 });
2985 }
2986 }
2987 Action::CalendarSelect => {
2988 if let Some(date) = self.calendar_date {
2989 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
2990 .unix_timestamp()
2991 * 1000;
2992 match self.calendar_selecting {
2993 CalendarField::StartDate => {
2994 self.log_groups_state.start_time = Some(timestamp);
2995 self.calendar_selecting = CalendarField::EndDate;
2996 }
2997 CalendarField::EndDate => {
2998 self.log_groups_state.end_time = Some(timestamp);
2999 self.mode = Mode::Normal;
3000 self.calendar_date = None;
3001 }
3002 }
3003 }
3004 }
3005 }
3006 }
3007
3008 pub fn filtered_services(&self) -> Vec<&'static str> {
3009 let mut services = if self.service_picker.filter.is_empty() {
3010 self.service_picker.services.clone()
3011 } else {
3012 self.service_picker
3013 .services
3014 .iter()
3015 .filter(|s| {
3016 s.to_lowercase()
3017 .contains(&self.service_picker.filter.to_lowercase())
3018 })
3019 .copied()
3020 .collect()
3021 };
3022 services.sort();
3023 services
3024 }
3025
3026 pub fn selected_log_group(&self) -> Option<&LogGroup> {
3027 crate::ui::cw::logs::selected_log_group(self)
3028 }
3029
3030 pub fn filtered_log_streams(&self) -> Vec<&LogStream> {
3031 crate::ui::cw::logs::filtered_log_streams(self)
3032 }
3033
3034 pub fn filtered_log_events(&self) -> Vec<&LogEvent> {
3035 crate::ui::cw::logs::filtered_log_events(self)
3036 }
3037
3038 pub fn filtered_log_groups(&self) -> Vec<&LogGroup> {
3039 crate::ui::cw::logs::filtered_log_groups(self)
3040 }
3041
3042 pub fn filtered_ecr_repositories(&self) -> Vec<&EcrRepository> {
3043 crate::ui::ecr::filtered_ecr_repositories(self)
3044 }
3045
3046 pub fn filtered_ecr_images(&self) -> Vec<&EcrImage> {
3047 crate::ui::ecr::filtered_ecr_images(self)
3048 }
3049
3050 pub fn filtered_cloudformation_stacks(&self) -> Vec<&CfnStack> {
3051 crate::ui::cfn::filtered_cloudformation_stacks(self)
3052 }
3053
3054 pub fn breadcrumbs(&self) -> String {
3055 if !self.service_selected {
3056 return String::new();
3057 }
3058
3059 let mut parts = vec![];
3060
3061 match self.current_service {
3062 Service::CloudWatchLogGroups => {
3063 parts.push("CloudWatch".to_string());
3064 parts.push("Log groups".to_string());
3065
3066 if self.view_mode != ViewMode::List {
3067 if let Some(group) = self.selected_log_group() {
3068 parts.push(group.name.clone());
3069 }
3070 }
3071
3072 if self.view_mode == ViewMode::Events {
3073 if let Some(stream) = self
3074 .log_groups_state
3075 .log_streams
3076 .get(self.log_groups_state.selected_stream)
3077 {
3078 parts.push(stream.name.clone());
3079 }
3080 }
3081 }
3082 Service::CloudWatchInsights => {
3083 parts.push("CloudWatch".to_string());
3084 parts.push("Insights".to_string());
3085 }
3086 Service::CloudWatchAlarms => {
3087 parts.push("CloudWatch".to_string());
3088 parts.push("Alarms".to_string());
3089 }
3090 Service::S3Buckets => {
3091 parts.push("S3".to_string());
3092 if let Some(bucket) = &self.s3_state.current_bucket {
3093 parts.push(bucket.clone());
3094 if let Some(prefix) = self.s3_state.prefix_stack.last() {
3095 parts.push(prefix.trim_end_matches('/').to_string());
3096 }
3097 } else {
3098 parts.push("Buckets".to_string());
3099 }
3100 }
3101 Service::SqsQueues => {
3102 parts.push("SQS".to_string());
3103 parts.push("Queues".to_string());
3104 }
3105 Service::EcrRepositories => {
3106 parts.push("ECR".to_string());
3107 if let Some(repo) = &self.ecr_state.current_repository {
3108 parts.push(repo.clone());
3109 } else {
3110 parts.push("Repositories".to_string());
3111 }
3112 }
3113 Service::LambdaFunctions => {
3114 parts.push("Lambda".to_string());
3115 if let Some(func) = &self.lambda_state.current_function {
3116 parts.push(func.clone());
3117 } else {
3118 parts.push("Functions".to_string());
3119 }
3120 }
3121 Service::LambdaApplications => {
3122 parts.push("Lambda".to_string());
3123 parts.push("Applications".to_string());
3124 }
3125 Service::CloudFormationStacks => {
3126 parts.push("CloudFormation".to_string());
3127 if let Some(stack_name) = &self.cfn_state.current_stack {
3128 parts.push(stack_name.clone());
3129 } else {
3130 parts.push("Stacks".to_string());
3131 }
3132 }
3133 Service::IamUsers => {
3134 parts.push("IAM".to_string());
3135 parts.push("Users".to_string());
3136 }
3137 Service::IamRoles => {
3138 parts.push("IAM".to_string());
3139 parts.push("Roles".to_string());
3140 if let Some(role_name) = &self.iam_state.current_role {
3141 parts.push(role_name.clone());
3142 if let Some(policy_name) = &self.iam_state.current_policy {
3143 parts.push(policy_name.clone());
3144 }
3145 }
3146 }
3147 Service::IamUserGroups => {
3148 parts.push("IAM".to_string());
3149 parts.push("User Groups".to_string());
3150 if let Some(group_name) = &self.iam_state.current_group {
3151 parts.push(group_name.clone());
3152 }
3153 }
3154 }
3155
3156 parts.join(" > ")
3157 }
3158
3159 pub fn update_current_tab_breadcrumb(&mut self) {
3160 if !self.tabs.is_empty() {
3161 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
3162 }
3163 }
3164
3165 pub fn get_console_url(&self) -> String {
3166 use crate::{cfn, cw, ecr, iam, lambda, s3};
3167
3168 match self.current_service {
3169 Service::CloudWatchLogGroups => {
3170 if self.view_mode == ViewMode::Events {
3171 if let Some(group) = self.selected_log_group() {
3172 if let Some(stream) = self
3173 .log_groups_state
3174 .log_streams
3175 .get(self.log_groups_state.selected_stream)
3176 {
3177 return cw::logs::console_url_stream(
3178 &self.config.region,
3179 &group.name,
3180 &stream.name,
3181 );
3182 }
3183 }
3184 } else if self.view_mode == ViewMode::Detail {
3185 if let Some(group) = self.selected_log_group() {
3186 return cw::logs::console_url_detail(&self.config.region, &group.name);
3187 }
3188 }
3189 cw::logs::console_url_list(&self.config.region)
3190 }
3191 Service::CloudWatchInsights => cw::insights::console_url(
3192 &self.config.region,
3193 &self.config.account_id,
3194 &self.insights_state.insights.query_text,
3195 &self.insights_state.insights.selected_log_groups,
3196 ),
3197 Service::CloudWatchAlarms => {
3198 let view_type = match self.alarms_state.view_as {
3199 AlarmViewMode::Table | AlarmViewMode::Detail => "table",
3200 AlarmViewMode::Cards => "card",
3201 };
3202 cw::alarms::console_url(
3203 &self.config.region,
3204 view_type,
3205 self.alarms_state.table.page_size.value(),
3206 &self.alarms_state.sort_column,
3207 self.alarms_state.sort_direction.as_str(),
3208 )
3209 }
3210 Service::S3Buckets => {
3211 if let Some(bucket_name) = &self.s3_state.current_bucket {
3212 let prefix = self.s3_state.prefix_stack.join("");
3213 s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
3214 } else {
3215 s3::console_url_buckets(&self.config.region)
3216 }
3217 }
3218 Service::SqsQueues => {
3219 if let Some(queue_url) = &self.sqs_state.current_queue {
3220 crate::sqs::console_url_queue_detail(&self.config.region, queue_url)
3221 } else {
3222 crate::sqs::console_url_queues(&self.config.region)
3223 }
3224 }
3225 Service::EcrRepositories => {
3226 if let Some(repo_name) = &self.ecr_state.current_repository {
3227 ecr::console_url_private_repository(
3228 &self.config.region,
3229 &self.config.account_id,
3230 repo_name,
3231 )
3232 } else {
3233 ecr::console_url_repositories(&self.config.region)
3234 }
3235 }
3236 Service::LambdaFunctions => {
3237 if let Some(func_name) = &self.lambda_state.current_function {
3238 if let Some(version) = &self.lambda_state.current_version {
3239 lambda::console_url_function_version(
3240 &self.config.region,
3241 func_name,
3242 version,
3243 &self.lambda_state.detail_tab,
3244 )
3245 } else {
3246 lambda::console_url_function_detail(&self.config.region, func_name)
3247 }
3248 } else {
3249 lambda::console_url_functions(&self.config.region)
3250 }
3251 }
3252 Service::LambdaApplications => {
3253 if let Some(app_name) = &self.lambda_application_state.current_application {
3254 lambda::console_url_application_detail(
3255 &self.config.region,
3256 app_name,
3257 &self.lambda_application_state.detail_tab,
3258 )
3259 } else {
3260 lambda::console_url_applications(&self.config.region)
3261 }
3262 }
3263 Service::CloudFormationStacks => {
3264 if let Some(stack_name) = &self.cfn_state.current_stack {
3265 if let Some(stack) = self
3266 .cfn_state
3267 .table
3268 .items
3269 .iter()
3270 .find(|s| &s.name == stack_name)
3271 {
3272 return cfn::console_url_stack_detail_with_tab(
3273 &self.config.region,
3274 &stack.stack_id,
3275 &self.cfn_state.detail_tab,
3276 );
3277 }
3278 }
3279 cfn::console_url_stacks(&self.config.region)
3280 }
3281 Service::IamUsers => {
3282 if let Some(user_name) = &self.iam_state.current_user {
3283 let section = match self.iam_state.user_tab {
3284 UserTab::Permissions => "permissions",
3285 UserTab::Groups => "groups",
3286 UserTab::Tags => "tags",
3287 UserTab::SecurityCredentials => "security_credentials",
3288 UserTab::LastAccessed => "access_advisor",
3289 };
3290 iam::console_url_user_detail(&self.config.region, user_name, section)
3291 } else {
3292 iam::console_url_users(&self.config.region)
3293 }
3294 }
3295 Service::IamRoles => {
3296 if let Some(policy_name) = &self.iam_state.current_policy {
3297 if let Some(role_name) = &self.iam_state.current_role {
3298 return iam::console_url_role_policy(
3299 &self.config.region,
3300 role_name,
3301 policy_name,
3302 );
3303 }
3304 }
3305 if let Some(role_name) = &self.iam_state.current_role {
3306 let section = match self.iam_state.role_tab {
3307 RoleTab::Permissions => "permissions",
3308 RoleTab::TrustRelationships => "trust_relationships",
3309 RoleTab::Tags => "tags",
3310 RoleTab::LastAccessed => "access_advisor",
3311 RoleTab::RevokeSessions => "revoke_sessions",
3312 };
3313 iam::console_url_role_detail(&self.config.region, role_name, section)
3314 } else {
3315 iam::console_url_roles(&self.config.region)
3316 }
3317 }
3318 Service::IamUserGroups => iam::console_url_groups(&self.config.region),
3319 }
3320 }
3321
3322 fn calculate_total_bucket_rows(&self) -> usize {
3323 crate::ui::s3::calculate_total_bucket_rows(self)
3324 }
3325
3326 fn calculate_total_object_rows(&self) -> usize {
3327 crate::ui::s3::calculate_total_object_rows(self)
3328 }
3329
3330 fn next_item(&mut self) {
3331 match self.mode {
3332 Mode::FilterInput => {
3333 if self.current_service == Service::CloudFormationStacks {
3334 use crate::ui::cfn::STATUS_FILTER;
3335 if self.cfn_state.input_focus == STATUS_FILTER {
3336 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3337 }
3338 } else if self.current_service == Service::SqsQueues {
3339 use crate::ui::sqs::SUBSCRIPTION_REGION;
3340 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3341 let regions = crate::aws::Region::all();
3342 self.sqs_state.subscription_region_selected =
3343 (self.sqs_state.subscription_region_selected + 1)
3344 .min(regions.len() - 1);
3345 }
3346 }
3347 }
3348 Mode::RegionPicker => {
3349 let filtered = self.get_filtered_regions();
3350 if !filtered.is_empty() {
3351 self.region_picker_selected =
3352 (self.region_picker_selected + 1).min(filtered.len() - 1);
3353 }
3354 }
3355 Mode::ProfilePicker => {
3356 let filtered = self.get_filtered_profiles();
3357 if !filtered.is_empty() {
3358 self.profile_picker_selected =
3359 (self.profile_picker_selected + 1).min(filtered.len() - 1);
3360 }
3361 }
3362 Mode::SessionPicker => {
3363 let filtered = self.get_filtered_sessions();
3364 if !filtered.is_empty() {
3365 self.session_picker_selected =
3366 (self.session_picker_selected + 1).min(filtered.len() - 1);
3367 }
3368 }
3369 Mode::InsightsInput => {
3370 use crate::app::InsightsFocus;
3371 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3372 && self.insights_state.insights.show_dropdown
3373 && !self.insights_state.insights.log_group_matches.is_empty()
3374 {
3375 let max = self.insights_state.insights.log_group_matches.len() - 1;
3376 self.insights_state.insights.dropdown_selected =
3377 (self.insights_state.insights.dropdown_selected + 1).min(max);
3378 }
3379 }
3380 Mode::ColumnSelector => {
3381 let max = if self.current_service == Service::S3Buckets
3382 && self.s3_state.current_bucket.is_none()
3383 {
3384 self.s3_bucket_column_ids.len() - 1
3385 } else if self.view_mode == ViewMode::Events {
3386 self.cw_log_event_column_ids.len() - 1
3387 } else if self.view_mode == ViewMode::Detail {
3388 self.cw_log_stream_column_ids.len() - 1
3389 } else if self.current_service == Service::CloudWatchAlarms {
3390 29
3392 } else if self.current_service == Service::EcrRepositories {
3393 if self.ecr_state.current_repository.is_some() {
3394 self.ecr_image_column_ids.len() + 6
3396 } else {
3397 self.ecr_repo_column_ids.len() - 1
3399 }
3400 } else if self.current_service == Service::SqsQueues {
3401 self.sqs_column_ids.len() - 1
3402 } else if self.current_service == Service::LambdaFunctions {
3403 self.lambda_state.function_column_ids.len() + 6
3405 } else if self.current_service == Service::LambdaApplications {
3406 self.lambda_application_column_ids.len() + 5
3408 } else if self.current_service == Service::CloudFormationStacks {
3409 self.cfn_column_ids.len() + 6
3411 } else if self.current_service == Service::IamUsers {
3412 if self.iam_state.current_user.is_some() {
3413 self.iam_policy_column_ids.len() + 5
3415 } else {
3416 self.iam_user_column_ids.len() + 5
3418 }
3419 } else if self.current_service == Service::IamRoles {
3420 if self.iam_state.current_role.is_some() {
3421 self.iam_policy_column_ids.len() + 5
3423 } else {
3424 self.iam_role_column_ids.len() + 5
3426 }
3427 } else {
3428 self.cw_log_group_column_ids.len() - 1
3429 };
3430 self.column_selector_index = (self.column_selector_index + 1).min(max);
3431 }
3432 Mode::ServicePicker => {
3433 let filtered = self.filtered_services();
3434 if !filtered.is_empty() {
3435 self.service_picker.selected =
3436 (self.service_picker.selected + 1).min(filtered.len() - 1);
3437 }
3438 }
3439 Mode::TabPicker => {
3440 let filtered = self.get_filtered_tabs();
3441 if !filtered.is_empty() {
3442 self.tab_picker_selected =
3443 (self.tab_picker_selected + 1).min(filtered.len() - 1);
3444 }
3445 }
3446 Mode::Normal => {
3447 if !self.service_selected {
3448 let filtered = self.filtered_services();
3449 if !filtered.is_empty() {
3450 self.service_picker.selected =
3451 (self.service_picker.selected + 1).min(filtered.len() - 1);
3452 }
3453 } else if self.current_service == Service::S3Buckets {
3454 if self.s3_state.current_bucket.is_some() {
3455 if self.s3_state.object_tab == S3ObjectTab::Properties {
3456 self.s3_state.properties_scroll =
3458 self.s3_state.properties_scroll.saturating_add(1);
3459 } else {
3460 let total_rows = self.calculate_total_object_rows();
3462 let max = total_rows.saturating_sub(1);
3463 self.s3_state.selected_object =
3464 (self.s3_state.selected_object + 1).min(max);
3465
3466 let visible_rows = self.s3_state.object_visible_rows.get();
3468 if self.s3_state.selected_object
3469 >= self.s3_state.object_scroll_offset + visible_rows
3470 {
3471 self.s3_state.object_scroll_offset =
3472 self.s3_state.selected_object - visible_rows + 1;
3473 }
3474 }
3475 } else {
3476 let total_rows = self.calculate_total_bucket_rows();
3478 if total_rows > 0 {
3479 self.s3_state.selected_row =
3480 (self.s3_state.selected_row + 1).min(total_rows - 1);
3481
3482 let visible_rows = self.s3_state.bucket_visible_rows.get();
3484 if self.s3_state.selected_row
3485 >= self.s3_state.bucket_scroll_offset + visible_rows
3486 {
3487 self.s3_state.bucket_scroll_offset =
3488 self.s3_state.selected_row - visible_rows + 1;
3489 }
3490 }
3491 }
3492 } else if self.view_mode == ViewMode::InsightsResults {
3493 let max = self
3494 .insights_state
3495 .insights
3496 .query_results
3497 .len()
3498 .saturating_sub(1);
3499 if self.insights_state.insights.results_selected < max {
3500 self.insights_state.insights.results_selected += 1;
3501 }
3502 } else if self.view_mode == ViewMode::PolicyView {
3503 let lines = self.iam_state.policy_document.lines().count();
3504 let max_scroll = lines.saturating_sub(1);
3505 self.iam_state.policy_scroll =
3506 (self.iam_state.policy_scroll + 1).min(max_scroll);
3507 } else if self.current_service == Service::SqsQueues
3508 && self.sqs_state.current_queue.is_some()
3509 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3510 {
3511 let lines = self.sqs_state.policy_document.lines().count();
3512 let max_scroll = lines.saturating_sub(1);
3513 self.sqs_state.policy_scroll =
3514 (self.sqs_state.policy_scroll + 1).min(max_scroll);
3515 } else if self.current_service == Service::SqsQueues
3516 && self.sqs_state.current_queue.is_some()
3517 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3518 {
3519 self.sqs_state.monitoring_scroll =
3520 (self.sqs_state.monitoring_scroll + 1).min(8);
3521 } else if self.view_mode == ViewMode::Events {
3522 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3523 if self.log_groups_state.event_scroll_offset >= max_scroll {
3524 } else {
3526 self.log_groups_state.event_scroll_offset =
3527 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3528 }
3529 } else if self.current_service == Service::CloudWatchLogGroups {
3530 if self.view_mode == ViewMode::List {
3531 let filtered_groups = self.filtered_log_groups();
3532 self.log_groups_state
3533 .log_groups
3534 .next_item(filtered_groups.len());
3535 } else if self.view_mode == ViewMode::Detail {
3536 let filtered_streams = self.filtered_log_streams();
3537 if !filtered_streams.is_empty() {
3538 let max = filtered_streams.len() - 1;
3539 if self.log_groups_state.selected_stream >= max {
3540 } else {
3542 self.log_groups_state.selected_stream =
3543 (self.log_groups_state.selected_stream + 1).min(max);
3544 }
3545 }
3546 }
3547 } else if self.current_service == Service::CloudWatchAlarms {
3548 let filtered_alarms = match self.alarms_state.alarm_tab {
3549 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
3550 AlarmTab::InAlarm => self
3551 .alarms_state
3552 .table
3553 .items
3554 .iter()
3555 .filter(|a| a.state.to_uppercase() == "ALARM")
3556 .count(),
3557 };
3558 if filtered_alarms > 0 {
3559 self.alarms_state.table.next_item(filtered_alarms);
3560 }
3561 } else if self.current_service == Service::EcrRepositories {
3562 if self.ecr_state.current_repository.is_some() {
3563 let filtered_images = self.filtered_ecr_images();
3564 if !filtered_images.is_empty() {
3565 self.ecr_state.images.next_item(filtered_images.len());
3566 }
3567 } else {
3568 let filtered_repos = self.filtered_ecr_repositories();
3569 if !filtered_repos.is_empty() {
3570 self.ecr_state.repositories.selected =
3571 (self.ecr_state.repositories.selected + 1)
3572 .min(filtered_repos.len() - 1);
3573 self.ecr_state.repositories.snap_to_page();
3574 }
3575 }
3576 } else if self.current_service == Service::SqsQueues {
3577 if self.sqs_state.current_queue.is_some()
3578 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
3579 {
3580 let filtered = crate::ui::sqs::filtered_lambda_triggers(self);
3581 if !filtered.is_empty() {
3582 self.sqs_state.triggers.next_item(filtered.len());
3583 }
3584 } else if self.sqs_state.current_queue.is_some()
3585 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
3586 {
3587 let filtered = crate::ui::sqs::filtered_eventbridge_pipes(self);
3588 if !filtered.is_empty() {
3589 self.sqs_state.pipes.next_item(filtered.len());
3590 }
3591 } else if self.sqs_state.current_queue.is_some()
3592 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
3593 {
3594 let filtered = crate::ui::sqs::filtered_tags(self);
3595 if !filtered.is_empty() {
3596 self.sqs_state.tags.next_item(filtered.len());
3597 }
3598 } else if self.sqs_state.current_queue.is_some()
3599 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3600 {
3601 let filtered = crate::ui::sqs::filtered_subscriptions(self);
3602 if !filtered.is_empty() {
3603 self.sqs_state.subscriptions.next_item(filtered.len());
3604 }
3605 } else {
3606 let filtered_queues = crate::ui::sqs::filtered_queues(
3607 &self.sqs_state.queues.items,
3608 &self.sqs_state.queues.filter,
3609 );
3610 if !filtered_queues.is_empty() {
3611 self.sqs_state.queues.next_item(filtered_queues.len());
3612 }
3613 }
3614 } else if self.current_service == Service::LambdaFunctions {
3615 if self.lambda_state.current_function.is_some()
3616 && self.lambda_state.detail_tab == LambdaDetailTab::Code
3617 {
3618 if let Some(func_name) = &self.lambda_state.current_function {
3620 if let Some(func) = self
3621 .lambda_state
3622 .table
3623 .items
3624 .iter()
3625 .find(|f| f.name == *func_name)
3626 {
3627 let max = func.layers.len().saturating_sub(1);
3628 if !func.layers.is_empty() {
3629 self.lambda_state.layer_selected =
3630 (self.lambda_state.layer_selected + 1).min(max);
3631 }
3632 }
3633 }
3634 } else if self.lambda_state.current_function.is_some()
3635 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3636 {
3637 let filtered: Vec<_> = self
3639 .lambda_state
3640 .version_table
3641 .items
3642 .iter()
3643 .filter(|v| {
3644 self.lambda_state.version_table.filter.is_empty()
3645 || v.version.to_lowercase().contains(
3646 &self.lambda_state.version_table.filter.to_lowercase(),
3647 )
3648 || v.aliases.to_lowercase().contains(
3649 &self.lambda_state.version_table.filter.to_lowercase(),
3650 )
3651 || v.description.to_lowercase().contains(
3652 &self.lambda_state.version_table.filter.to_lowercase(),
3653 )
3654 })
3655 .collect();
3656 if !filtered.is_empty() {
3657 self.lambda_state.version_table.selected =
3658 (self.lambda_state.version_table.selected + 1)
3659 .min(filtered.len() - 1);
3660 self.lambda_state.version_table.snap_to_page();
3661 }
3662 } else if self.lambda_state.current_function.is_some()
3663 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3664 || (self.lambda_state.current_version.is_some()
3665 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3666 {
3667 let version_filter = self.lambda_state.current_version.clone();
3669 let filtered: Vec<_> = self
3670 .lambda_state
3671 .alias_table
3672 .items
3673 .iter()
3674 .filter(|a| {
3675 (version_filter.is_none()
3676 || a.versions.contains(version_filter.as_ref().unwrap()))
3677 && (self.lambda_state.alias_table.filter.is_empty()
3678 || a.name.to_lowercase().contains(
3679 &self.lambda_state.alias_table.filter.to_lowercase(),
3680 )
3681 || a.versions.to_lowercase().contains(
3682 &self.lambda_state.alias_table.filter.to_lowercase(),
3683 )
3684 || a.description.to_lowercase().contains(
3685 &self.lambda_state.alias_table.filter.to_lowercase(),
3686 ))
3687 })
3688 .collect();
3689 if !filtered.is_empty() {
3690 self.lambda_state.alias_table.selected =
3691 (self.lambda_state.alias_table.selected + 1)
3692 .min(filtered.len() - 1);
3693 self.lambda_state.alias_table.snap_to_page();
3694 }
3695 } else if self.lambda_state.current_function.is_none() {
3696 let filtered = crate::ui::lambda::filtered_lambda_functions(self);
3697 if !filtered.is_empty() {
3698 self.lambda_state.table.next_item(filtered.len());
3699 self.lambda_state.table.snap_to_page();
3700 }
3701 }
3702 } else if self.current_service == Service::LambdaApplications {
3703 if self.lambda_application_state.current_application.is_some() {
3704 if self.lambda_application_state.detail_tab
3705 == LambdaApplicationDetailTab::Overview
3706 {
3707 let len = self.lambda_application_state.resources.items.len();
3708 if len > 0 {
3709 self.lambda_application_state.resources.next_item(len);
3710 }
3711 } else {
3712 let len = self.lambda_application_state.deployments.items.len();
3713 if len > 0 {
3714 self.lambda_application_state.deployments.next_item(len);
3715 }
3716 }
3717 } else {
3718 let filtered = crate::ui::lambda::filtered_lambda_applications(self);
3719 if !filtered.is_empty() {
3720 self.lambda_application_state.table.selected =
3721 (self.lambda_application_state.table.selected + 1)
3722 .min(filtered.len() - 1);
3723 self.lambda_application_state.table.snap_to_page();
3724 }
3725 }
3726 } else if self.current_service == Service::CloudFormationStacks {
3727 let filtered = self.filtered_cloudformation_stacks();
3728 self.cfn_state.table.next_item(filtered.len());
3729 } else if self.current_service == Service::IamUsers {
3730 if self.iam_state.current_user.is_some() {
3731 if self.iam_state.user_tab == UserTab::Tags {
3732 let filtered = crate::ui::iam::filtered_user_tags(self);
3733 if !filtered.is_empty() {
3734 self.iam_state.user_tags.next_item(filtered.len());
3735 }
3736 } else {
3737 let filtered = crate::ui::iam::filtered_iam_policies(self);
3738 if !filtered.is_empty() {
3739 self.iam_state.policies.next_item(filtered.len());
3740 }
3741 }
3742 } else {
3743 let filtered = crate::ui::iam::filtered_iam_users(self);
3744 if !filtered.is_empty() {
3745 self.iam_state.users.next_item(filtered.len());
3746 }
3747 }
3748 } else if self.current_service == Service::IamRoles {
3749 if self.iam_state.current_role.is_some() {
3750 if self.iam_state.role_tab == RoleTab::TrustRelationships {
3751 let lines = self.iam_state.trust_policy_document.lines().count();
3752 let max_scroll = lines.saturating_sub(1);
3753 self.iam_state.trust_policy_scroll =
3754 (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
3755 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
3756 self.iam_state.revoke_sessions_scroll =
3757 (self.iam_state.revoke_sessions_scroll + 1).min(19);
3758 } else if self.iam_state.role_tab == RoleTab::Tags {
3759 let filtered = crate::ui::iam::filtered_tags(self);
3760 if !filtered.is_empty() {
3761 self.iam_state.tags.next_item(filtered.len());
3762 }
3763 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
3764 let filtered = crate::ui::iam::filtered_last_accessed(self);
3765 if !filtered.is_empty() {
3766 self.iam_state
3767 .last_accessed_services
3768 .next_item(filtered.len());
3769 }
3770 } else {
3771 let filtered = crate::ui::iam::filtered_iam_policies(self);
3772 if !filtered.is_empty() {
3773 self.iam_state.policies.next_item(filtered.len());
3774 }
3775 }
3776 } else {
3777 let filtered = crate::ui::iam::filtered_iam_roles(self);
3778 if !filtered.is_empty() {
3779 self.iam_state.roles.next_item(filtered.len());
3780 }
3781 }
3782 } else if self.current_service == Service::IamUserGroups {
3783 if self.iam_state.current_group.is_some() {
3784 if self.iam_state.group_tab == GroupTab::Users {
3785 let filtered: Vec<_> = self
3786 .iam_state
3787 .group_users
3788 .items
3789 .iter()
3790 .filter(|u| {
3791 if self.iam_state.group_users.filter.is_empty() {
3792 true
3793 } else {
3794 u.user_name.to_lowercase().contains(
3795 &self.iam_state.group_users.filter.to_lowercase(),
3796 )
3797 }
3798 })
3799 .collect();
3800 if !filtered.is_empty() {
3801 self.iam_state.group_users.next_item(filtered.len());
3802 }
3803 } else if self.iam_state.group_tab == GroupTab::Permissions {
3804 let filtered = crate::ui::iam::filtered_iam_policies(self);
3805 if !filtered.is_empty() {
3806 self.iam_state.policies.next_item(filtered.len());
3807 }
3808 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3809 let filtered = crate::ui::iam::filtered_last_accessed(self);
3810 if !filtered.is_empty() {
3811 self.iam_state
3812 .last_accessed_services
3813 .next_item(filtered.len());
3814 }
3815 }
3816 } else {
3817 let filtered: Vec<_> = self
3818 .iam_state
3819 .groups
3820 .items
3821 .iter()
3822 .filter(|g| {
3823 if self.iam_state.groups.filter.is_empty() {
3824 true
3825 } else {
3826 g.group_name
3827 .to_lowercase()
3828 .contains(&self.iam_state.groups.filter.to_lowercase())
3829 }
3830 })
3831 .collect();
3832 if !filtered.is_empty() {
3833 self.iam_state.groups.next_item(filtered.len());
3834 }
3835 }
3836 }
3837 }
3838 _ => {}
3839 }
3840 }
3841
3842 fn prev_item(&mut self) {
3843 match self.mode {
3844 Mode::FilterInput => {
3845 if self.current_service == Service::CloudFormationStacks {
3846 use crate::ui::cfn::STATUS_FILTER;
3847 if self.cfn_state.input_focus == STATUS_FILTER {
3848 self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
3849 }
3850 } else if self.current_service == Service::SqsQueues {
3851 use crate::ui::sqs::SUBSCRIPTION_REGION;
3852 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3853 self.sqs_state.subscription_region_selected = self
3854 .sqs_state
3855 .subscription_region_selected
3856 .saturating_sub(1);
3857 }
3858 }
3859 }
3860 Mode::RegionPicker => {
3861 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
3862 }
3863 Mode::ProfilePicker => {
3864 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
3865 }
3866 Mode::SessionPicker => {
3867 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
3868 }
3869 Mode::InsightsInput => {
3870 use crate::app::InsightsFocus;
3871 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3872 && self.insights_state.insights.show_dropdown
3873 && !self.insights_state.insights.log_group_matches.is_empty()
3874 {
3875 self.insights_state.insights.dropdown_selected = self
3876 .insights_state
3877 .insights
3878 .dropdown_selected
3879 .saturating_sub(1);
3880 }
3881 }
3882 Mode::ColumnSelector => {
3883 self.column_selector_index = self.column_selector_index.saturating_sub(1);
3884 }
3885 Mode::ServicePicker => {
3886 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3887 }
3888 Mode::TabPicker => {
3889 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
3890 }
3891 Mode::Normal => {
3892 if !self.service_selected {
3893 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3894 } else if self.current_service == Service::S3Buckets {
3895 if self.s3_state.current_bucket.is_some() {
3896 if self.s3_state.object_tab == S3ObjectTab::Properties {
3897 self.s3_state.properties_scroll =
3898 self.s3_state.properties_scroll.saturating_sub(1);
3899 } else {
3900 self.s3_state.selected_object =
3901 self.s3_state.selected_object.saturating_sub(1);
3902
3903 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
3905 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
3906 }
3907 }
3908 } else {
3909 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
3910
3911 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
3913 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
3914 }
3915 }
3916 } else if self.view_mode == ViewMode::InsightsResults {
3917 if self.insights_state.insights.results_selected > 0 {
3918 self.insights_state.insights.results_selected -= 1;
3919 }
3920 } else if self.view_mode == ViewMode::PolicyView {
3921 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
3922 } else if self.current_service == Service::SqsQueues
3923 && self.sqs_state.current_queue.is_some()
3924 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3925 {
3926 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(1);
3927 } else if self.current_service == Service::SqsQueues
3928 && self.sqs_state.current_queue.is_some()
3929 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3930 {
3931 self.sqs_state.monitoring_scroll =
3932 self.sqs_state.monitoring_scroll.saturating_sub(1);
3933 } else if self.view_mode == ViewMode::Events {
3934 if self.log_groups_state.event_scroll_offset == 0 {
3935 if self.log_groups_state.has_older_events {
3936 self.log_groups_state.loading = true;
3937 }
3938 } else {
3940 self.log_groups_state.event_scroll_offset =
3941 self.log_groups_state.event_scroll_offset.saturating_sub(1);
3942 }
3943 } else if self.current_service == Service::CloudWatchLogGroups {
3944 if self.view_mode == ViewMode::List {
3945 self.log_groups_state.log_groups.prev_item();
3946 } else if self.view_mode == ViewMode::Detail
3947 && self.log_groups_state.selected_stream > 0
3948 {
3949 self.log_groups_state.selected_stream =
3950 self.log_groups_state.selected_stream.saturating_sub(1);
3951 self.log_groups_state.expanded_stream = None;
3952 }
3953 } else if self.current_service == Service::CloudWatchAlarms {
3954 self.alarms_state.table.prev_item();
3955 } else if self.current_service == Service::EcrRepositories {
3956 if self.ecr_state.current_repository.is_some() {
3957 self.ecr_state.images.prev_item();
3958 } else {
3959 self.ecr_state.repositories.prev_item();
3960 }
3961 } else if self.current_service == Service::SqsQueues {
3962 if self.sqs_state.current_queue.is_some()
3963 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
3964 {
3965 self.sqs_state.triggers.prev_item();
3966 } else if self.sqs_state.current_queue.is_some()
3967 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
3968 {
3969 self.sqs_state.pipes.prev_item();
3970 } else if self.sqs_state.current_queue.is_some()
3971 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
3972 {
3973 self.sqs_state.tags.prev_item();
3974 } else if self.sqs_state.current_queue.is_some()
3975 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3976 {
3977 self.sqs_state.subscriptions.prev_item();
3978 } else {
3979 self.sqs_state.queues.prev_item();
3980 }
3981 } else if self.current_service == Service::LambdaFunctions {
3982 if self.lambda_state.current_function.is_some()
3983 && self.lambda_state.detail_tab == LambdaDetailTab::Code
3984 {
3985 self.lambda_state.layer_selected =
3987 self.lambda_state.layer_selected.saturating_sub(1);
3988 } else if self.lambda_state.current_function.is_some()
3989 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3990 {
3991 self.lambda_state.version_table.prev_item();
3992 } else if self.lambda_state.current_function.is_some()
3993 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3994 || (self.lambda_state.current_version.is_some()
3995 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3996 {
3997 self.lambda_state.alias_table.prev_item();
3998 } else if self.lambda_state.current_function.is_none() {
3999 self.lambda_state.table.prev_item();
4000 }
4001 } else if self.current_service == Service::LambdaApplications {
4002 if self.lambda_application_state.current_application.is_some()
4003 && self.lambda_application_state.detail_tab
4004 == LambdaApplicationDetailTab::Overview
4005 {
4006 self.lambda_application_state.resources.selected = self
4007 .lambda_application_state
4008 .resources
4009 .selected
4010 .saturating_sub(1);
4011 } else if self.lambda_application_state.current_application.is_some()
4012 && self.lambda_application_state.detail_tab
4013 == LambdaApplicationDetailTab::Deployments
4014 {
4015 self.lambda_application_state.deployments.selected = self
4016 .lambda_application_state
4017 .deployments
4018 .selected
4019 .saturating_sub(1);
4020 } else {
4021 self.lambda_application_state.table.selected = self
4022 .lambda_application_state
4023 .table
4024 .selected
4025 .saturating_sub(1);
4026 self.lambda_application_state.table.snap_to_page();
4027 }
4028 } else if self.current_service == Service::CloudFormationStacks {
4029 self.cfn_state.table.prev_item();
4030 } else if self.current_service == Service::IamUsers {
4031 self.iam_state.users.prev_item();
4032 } else if self.current_service == Service::IamRoles {
4033 if self.iam_state.current_role.is_some() {
4034 if self.iam_state.role_tab == RoleTab::TrustRelationships {
4035 self.iam_state.trust_policy_scroll =
4036 self.iam_state.trust_policy_scroll.saturating_sub(1);
4037 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4038 self.iam_state.revoke_sessions_scroll =
4039 self.iam_state.revoke_sessions_scroll.saturating_sub(1);
4040 } else if self.iam_state.role_tab == RoleTab::Tags {
4041 self.iam_state.tags.prev_item();
4042 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4043 self.iam_state.last_accessed_services.prev_item();
4044 } else {
4045 self.iam_state.policies.prev_item();
4046 }
4047 } else {
4048 self.iam_state.roles.prev_item();
4049 }
4050 } else if self.current_service == Service::IamUserGroups {
4051 if self.iam_state.current_group.is_some() {
4052 if self.iam_state.group_tab == GroupTab::Users {
4053 self.iam_state.group_users.prev_item();
4054 } else if self.iam_state.group_tab == GroupTab::Permissions {
4055 self.iam_state.policies.prev_item();
4056 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4057 self.iam_state.last_accessed_services.prev_item();
4058 }
4059 } else {
4060 self.iam_state.groups.prev_item();
4061 }
4062 }
4063 }
4064 _ => {}
4065 }
4066 }
4067
4068 fn page_down(&mut self) {
4069 if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
4070 use crate::ui::cfn::filtered_cloudformation_stacks;
4071 let page_size = self.cfn_state.table.page_size.value();
4072 let filtered_count = filtered_cloudformation_stacks(self).len();
4073 self.cfn_state.input_focus.handle_page_down(
4074 &mut self.cfn_state.table.selected,
4075 &mut self.cfn_state.table.scroll_offset,
4076 page_size,
4077 filtered_count,
4078 );
4079 } else if self.mode == Mode::FilterInput
4080 && self.current_service == Service::IamRoles
4081 && self.iam_state.current_role.is_none()
4082 {
4083 let page_size = self.iam_state.roles.page_size.value();
4084 let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
4085 self.iam_state.role_input_focus.handle_page_down(
4086 &mut self.iam_state.roles.selected,
4087 &mut self.iam_state.roles.scroll_offset,
4088 page_size,
4089 filtered_count,
4090 );
4091 } else if self.mode == Mode::FilterInput
4092 && self.current_service == Service::CloudWatchAlarms
4093 {
4094 let page_size = self.alarms_state.table.page_size.value();
4095 let filtered_count = self.alarms_state.table.items.len();
4096 self.alarms_state.input_focus.handle_page_down(
4097 &mut self.alarms_state.table.selected,
4098 &mut self.alarms_state.table.scroll_offset,
4099 page_size,
4100 filtered_count,
4101 );
4102 } else if self.mode == Mode::FilterInput
4103 && self.current_service == Service::CloudWatchLogGroups
4104 {
4105 if self.view_mode == ViewMode::List {
4106 let filtered = self.filtered_log_groups();
4108 let page_size = self.log_groups_state.log_groups.page_size.value();
4109 let filtered_count = filtered.len();
4110 self.log_groups_state.input_focus.handle_page_down(
4111 &mut self.log_groups_state.log_groups.selected,
4112 &mut self.log_groups_state.log_groups.scroll_offset,
4113 page_size,
4114 filtered_count,
4115 );
4116 } else {
4117 let filtered = self.filtered_log_streams();
4119 let page_size = 20;
4120 let filtered_count = filtered.len();
4121 self.log_groups_state.input_focus.handle_page_down(
4122 &mut self.log_groups_state.selected_stream,
4123 &mut self.log_groups_state.stream_page,
4124 page_size,
4125 filtered_count,
4126 );
4127 self.log_groups_state.expanded_stream = None;
4128 }
4129 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4130 {
4131 if self.lambda_state.current_function.is_some()
4132 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4133 && self.lambda_state.version_input_focus == InputFocus::Pagination
4134 {
4135 let page_size = self.lambda_state.version_table.page_size.value();
4136 let filtered_count: usize = self
4137 .lambda_state
4138 .version_table
4139 .items
4140 .iter()
4141 .filter(|v| {
4142 self.lambda_state.version_table.filter.is_empty()
4143 || v.version
4144 .to_lowercase()
4145 .contains(&self.lambda_state.version_table.filter.to_lowercase())
4146 || v.aliases
4147 .to_lowercase()
4148 .contains(&self.lambda_state.version_table.filter.to_lowercase())
4149 || v.description
4150 .to_lowercase()
4151 .contains(&self.lambda_state.version_table.filter.to_lowercase())
4152 })
4153 .count();
4154 let target = self.lambda_state.version_table.selected + page_size;
4155 self.lambda_state.version_table.selected =
4156 target.min(filtered_count.saturating_sub(1));
4157 } else if self.lambda_state.current_function.is_some()
4158 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4159 || (self.lambda_state.current_version.is_some()
4160 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4161 && self.lambda_state.alias_input_focus == InputFocus::Pagination
4162 {
4163 let page_size = self.lambda_state.alias_table.page_size.value();
4164 let version_filter = self.lambda_state.current_version.clone();
4165 let filtered_count = self
4166 .lambda_state
4167 .alias_table
4168 .items
4169 .iter()
4170 .filter(|a| {
4171 (version_filter.is_none()
4172 || a.versions.contains(version_filter.as_ref().unwrap()))
4173 && (self.lambda_state.alias_table.filter.is_empty()
4174 || a.name
4175 .to_lowercase()
4176 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
4177 || a.versions
4178 .to_lowercase()
4179 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
4180 || a.description
4181 .to_lowercase()
4182 .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
4183 })
4184 .count();
4185 let target = self.lambda_state.alias_table.selected + page_size;
4186 self.lambda_state.alias_table.selected =
4187 target.min(filtered_count.saturating_sub(1));
4188 } else if self.lambda_state.current_function.is_none() {
4189 let page_size = self.lambda_state.table.page_size.value();
4190 let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
4191 self.lambda_state.input_focus.handle_page_down(
4192 &mut self.lambda_state.table.selected,
4193 &mut self.lambda_state.table.scroll_offset,
4194 page_size,
4195 filtered_count,
4196 );
4197 }
4198 } else if self.mode == Mode::FilterInput
4199 && self.current_service == Service::EcrRepositories
4200 && self.ecr_state.current_repository.is_none()
4201 && self.ecr_state.input_focus == InputFocus::Filter
4202 {
4203 let filtered = self.filtered_ecr_repositories();
4205 self.ecr_state.repositories.page_down(filtered.len());
4206 } else if self.mode == Mode::FilterInput
4207 && self.current_service == Service::EcrRepositories
4208 && self.ecr_state.current_repository.is_none()
4209 {
4210 let page_size = self.ecr_state.repositories.page_size.value();
4211 let filtered_count = self.filtered_ecr_repositories().len();
4212 self.ecr_state.input_focus.handle_page_down(
4213 &mut self.ecr_state.repositories.selected,
4214 &mut self.ecr_state.repositories.scroll_offset,
4215 page_size,
4216 filtered_count,
4217 );
4218 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4219 let page_size = self.iam_state.policies.page_size.value();
4220 let filtered_count = crate::ui::iam::filtered_iam_policies(self).len();
4221 self.iam_state.policy_input_focus.handle_page_down(
4222 &mut self.iam_state.policies.selected,
4223 &mut self.iam_state.policies.scroll_offset,
4224 page_size,
4225 filtered_count,
4226 );
4227 } else if self.view_mode == ViewMode::PolicyView {
4228 let lines = self.iam_state.policy_document.lines().count();
4229 let max_scroll = lines.saturating_sub(1);
4230 self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
4231 } else if self.current_service == Service::SqsQueues
4232 && self.sqs_state.current_queue.is_some()
4233 {
4234 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
4235 self.sqs_state.monitoring_scroll = (self.sqs_state.monitoring_scroll + 1).min(8);
4236 } else {
4237 let lines = self.sqs_state.policy_document.lines().count();
4238 let max_scroll = lines.saturating_sub(1);
4239 self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
4240 }
4241 } else if self.current_service == Service::IamRoles
4242 && self.iam_state.current_role.is_some()
4243 && self.iam_state.role_tab == RoleTab::TrustRelationships
4244 {
4245 let lines = self.iam_state.trust_policy_document.lines().count();
4246 let max_scroll = lines.saturating_sub(1);
4247 self.iam_state.trust_policy_scroll =
4248 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
4249 } else if self.current_service == Service::IamRoles
4250 && self.iam_state.current_role.is_some()
4251 && self.iam_state.role_tab == RoleTab::RevokeSessions
4252 {
4253 self.iam_state.revoke_sessions_scroll =
4254 (self.iam_state.revoke_sessions_scroll + 10).min(19);
4255 } else if self.mode == Mode::Normal {
4256 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4257 {
4258 let total_rows = self.calculate_total_bucket_rows();
4259 self.s3_state.selected_row = self
4260 .s3_state
4261 .selected_row
4262 .saturating_add(10)
4263 .min(total_rows.saturating_sub(1));
4264
4265 let visible_rows = self.s3_state.bucket_visible_rows.get();
4267 if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
4268 self.s3_state.bucket_scroll_offset =
4269 self.s3_state.selected_row - visible_rows + 1;
4270 }
4271 } else if self.current_service == Service::S3Buckets
4272 && self.s3_state.current_bucket.is_some()
4273 {
4274 let total_rows = self.calculate_total_object_rows();
4275 self.s3_state.selected_object = self
4276 .s3_state
4277 .selected_object
4278 .saturating_add(10)
4279 .min(total_rows.saturating_sub(1));
4280
4281 let visible_rows = self.s3_state.object_visible_rows.get();
4283 if self.s3_state.selected_object
4284 >= self.s3_state.object_scroll_offset + visible_rows
4285 {
4286 self.s3_state.object_scroll_offset =
4287 self.s3_state.selected_object - visible_rows + 1;
4288 }
4289 } else if self.current_service == Service::CloudWatchLogGroups
4290 && self.view_mode == ViewMode::List
4291 {
4292 let filtered = self.filtered_log_groups();
4293 self.log_groups_state.log_groups.page_down(filtered.len());
4294 } else if self.current_service == Service::CloudWatchLogGroups
4295 && self.view_mode == ViewMode::Detail
4296 {
4297 let len = self.filtered_log_streams().len();
4298 nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
4299 } else if self.view_mode == ViewMode::Events {
4300 let max = self.log_groups_state.log_events.len();
4301 nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
4302 } else if self.view_mode == ViewMode::InsightsResults {
4303 let max = self.insights_state.insights.query_results.len();
4304 nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
4305 } else if self.current_service == Service::CloudWatchAlarms {
4306 let filtered = match self.alarms_state.alarm_tab {
4307 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4308 AlarmTab::InAlarm => self
4309 .alarms_state
4310 .table
4311 .items
4312 .iter()
4313 .filter(|a| a.state.to_uppercase() == "ALARM")
4314 .count(),
4315 };
4316 if filtered > 0 {
4317 self.alarms_state.table.page_down(filtered);
4318 }
4319 } else if self.current_service == Service::EcrRepositories {
4320 if self.ecr_state.current_repository.is_some() {
4321 let filtered = self.filtered_ecr_images();
4322 self.ecr_state.images.page_down(filtered.len());
4323 } else {
4324 let filtered = self.filtered_ecr_repositories();
4325 self.ecr_state.repositories.page_down(filtered.len());
4326 }
4327 } else if self.current_service == Service::SqsQueues {
4328 let filtered = crate::ui::sqs::filtered_queues(
4329 &self.sqs_state.queues.items,
4330 &self.sqs_state.queues.filter,
4331 );
4332 self.sqs_state.queues.page_down(filtered.len());
4333 } else if self.current_service == Service::LambdaFunctions {
4334 let len = crate::ui::lambda::filtered_lambda_functions(self).len();
4335 self.lambda_state.table.page_down(len);
4336 } else if self.current_service == Service::LambdaApplications {
4337 let len = crate::ui::lambda::filtered_lambda_applications(self).len();
4338 self.lambda_application_state.table.page_down(len);
4339 } else if self.current_service == Service::CloudFormationStacks {
4340 let filtered = self.filtered_cloudformation_stacks();
4341 self.cfn_state.table.page_down(filtered.len());
4342 } else if self.current_service == Service::IamUsers {
4343 let len = crate::ui::iam::filtered_iam_users(self).len();
4344 nav_page_down(&mut self.iam_state.users.selected, len, 10);
4345 } else if self.current_service == Service::IamRoles {
4346 if self.iam_state.current_role.is_some() {
4347 let filtered = crate::ui::iam::filtered_iam_policies(self);
4348 if !filtered.is_empty() {
4349 self.iam_state.policies.page_down(filtered.len());
4350 }
4351 } else {
4352 let filtered = crate::ui::iam::filtered_iam_roles(self);
4353 self.iam_state.roles.page_down(filtered.len());
4354 }
4355 } else if self.current_service == Service::IamUserGroups {
4356 if self.iam_state.current_group.is_some() {
4357 if self.iam_state.group_tab == GroupTab::Users {
4358 let filtered: Vec<_> = self
4359 .iam_state
4360 .group_users
4361 .items
4362 .iter()
4363 .filter(|u| {
4364 if self.iam_state.group_users.filter.is_empty() {
4365 true
4366 } else {
4367 u.user_name
4368 .to_lowercase()
4369 .contains(&self.iam_state.group_users.filter.to_lowercase())
4370 }
4371 })
4372 .collect();
4373 if !filtered.is_empty() {
4374 self.iam_state.group_users.page_down(filtered.len());
4375 }
4376 } else if self.iam_state.group_tab == GroupTab::Permissions {
4377 let filtered = crate::ui::iam::filtered_iam_policies(self);
4378 if !filtered.is_empty() {
4379 self.iam_state.policies.page_down(filtered.len());
4380 }
4381 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4382 let filtered = crate::ui::iam::filtered_last_accessed(self);
4383 if !filtered.is_empty() {
4384 self.iam_state
4385 .last_accessed_services
4386 .page_down(filtered.len());
4387 }
4388 }
4389 } else {
4390 let filtered: Vec<_> = self
4391 .iam_state
4392 .groups
4393 .items
4394 .iter()
4395 .filter(|g| {
4396 if self.iam_state.groups.filter.is_empty() {
4397 true
4398 } else {
4399 g.group_name
4400 .to_lowercase()
4401 .contains(&self.iam_state.groups.filter.to_lowercase())
4402 }
4403 })
4404 .collect();
4405 if !filtered.is_empty() {
4406 self.iam_state.groups.page_down(filtered.len());
4407 }
4408 }
4409 }
4410 }
4411 }
4412
4413 fn page_up(&mut self) {
4414 if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
4415 let page_size = self.cfn_state.table.page_size.value();
4416 self.cfn_state.input_focus.handle_page_up(
4417 &mut self.cfn_state.table.selected,
4418 &mut self.cfn_state.table.scroll_offset,
4419 page_size,
4420 );
4421 } else if self.mode == Mode::FilterInput
4422 && self.current_service == Service::IamRoles
4423 && self.iam_state.current_role.is_none()
4424 {
4425 let page_size = self.iam_state.roles.page_size.value();
4426 self.iam_state.role_input_focus.handle_page_up(
4427 &mut self.iam_state.roles.selected,
4428 &mut self.iam_state.roles.scroll_offset,
4429 page_size,
4430 );
4431 } else if self.mode == Mode::FilterInput
4432 && self.current_service == Service::CloudWatchAlarms
4433 {
4434 let page_size = self.alarms_state.table.page_size.value();
4435 self.alarms_state.input_focus.handle_page_up(
4436 &mut self.alarms_state.table.selected,
4437 &mut self.alarms_state.table.scroll_offset,
4438 page_size,
4439 );
4440 } else if self.mode == Mode::FilterInput
4441 && self.current_service == Service::CloudWatchLogGroups
4442 {
4443 if self.view_mode == ViewMode::List {
4444 let page_size = self.log_groups_state.log_groups.page_size.value();
4446 self.log_groups_state.input_focus.handle_page_up(
4447 &mut self.log_groups_state.log_groups.selected,
4448 &mut self.log_groups_state.log_groups.scroll_offset,
4449 page_size,
4450 );
4451 } else {
4452 let page_size = 20;
4454 self.log_groups_state.input_focus.handle_page_up(
4455 &mut self.log_groups_state.selected_stream,
4456 &mut self.log_groups_state.stream_page,
4457 page_size,
4458 );
4459 self.log_groups_state.expanded_stream = None;
4460 }
4461 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4462 {
4463 if self.lambda_state.current_function.is_some()
4464 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4465 && self.lambda_state.version_input_focus == InputFocus::Pagination
4466 {
4467 let page_size = self.lambda_state.version_table.page_size.value();
4468 self.lambda_state.version_table.selected = self
4469 .lambda_state
4470 .version_table
4471 .selected
4472 .saturating_sub(page_size);
4473 } else if self.lambda_state.current_function.is_some()
4474 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4475 || (self.lambda_state.current_version.is_some()
4476 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4477 && self.lambda_state.alias_input_focus == InputFocus::Pagination
4478 {
4479 let page_size = self.lambda_state.alias_table.page_size.value();
4480 self.lambda_state.alias_table.selected = self
4481 .lambda_state
4482 .alias_table
4483 .selected
4484 .saturating_sub(page_size);
4485 } else if self.lambda_state.current_function.is_none() {
4486 let page_size = self.lambda_state.table.page_size.value();
4487 self.lambda_state.input_focus.handle_page_up(
4488 &mut self.lambda_state.table.selected,
4489 &mut self.lambda_state.table.scroll_offset,
4490 page_size,
4491 );
4492 }
4493 } else if self.mode == Mode::FilterInput
4494 && self.current_service == Service::EcrRepositories
4495 && self.ecr_state.current_repository.is_none()
4496 && self.ecr_state.input_focus == InputFocus::Filter
4497 {
4498 self.ecr_state.repositories.page_up();
4500 } else if self.mode == Mode::FilterInput
4501 && self.current_service == Service::EcrRepositories
4502 && self.ecr_state.current_repository.is_none()
4503 {
4504 let page_size = self.ecr_state.repositories.page_size.value();
4505 self.ecr_state.input_focus.handle_page_up(
4506 &mut self.ecr_state.repositories.selected,
4507 &mut self.ecr_state.repositories.scroll_offset,
4508 page_size,
4509 );
4510 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4511 let page_size = self.iam_state.policies.page_size.value();
4512 self.iam_state.policy_input_focus.handle_page_up(
4513 &mut self.iam_state.policies.selected,
4514 &mut self.iam_state.policies.scroll_offset,
4515 page_size,
4516 );
4517 } else if self.view_mode == ViewMode::PolicyView {
4518 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
4519 } else if self.current_service == Service::SqsQueues
4520 && self.sqs_state.current_queue.is_some()
4521 {
4522 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
4523 self.sqs_state.monitoring_scroll =
4524 self.sqs_state.monitoring_scroll.saturating_sub(1);
4525 } else {
4526 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
4527 }
4528 } else if self.current_service == Service::IamRoles
4529 && self.iam_state.current_role.is_some()
4530 && self.iam_state.role_tab == RoleTab::TrustRelationships
4531 {
4532 self.iam_state.trust_policy_scroll =
4533 self.iam_state.trust_policy_scroll.saturating_sub(10);
4534 } else if self.current_service == Service::IamRoles
4535 && self.iam_state.current_role.is_some()
4536 && self.iam_state.role_tab == RoleTab::RevokeSessions
4537 {
4538 self.iam_state.revoke_sessions_scroll =
4539 self.iam_state.revoke_sessions_scroll.saturating_sub(10);
4540 } else if self.mode == Mode::Normal {
4541 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4542 {
4543 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
4544
4545 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4547 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4548 }
4549 } else if self.current_service == Service::S3Buckets
4550 && self.s3_state.current_bucket.is_some()
4551 {
4552 self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
4553
4554 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4556 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4557 }
4558 } else if self.current_service == Service::CloudWatchLogGroups
4559 && self.view_mode == ViewMode::List
4560 {
4561 self.log_groups_state.log_groups.page_up();
4562 } else if self.current_service == Service::CloudWatchLogGroups
4563 && self.view_mode == ViewMode::Detail
4564 {
4565 self.log_groups_state.selected_stream =
4566 self.log_groups_state.selected_stream.saturating_sub(10);
4567 } else if self.view_mode == ViewMode::Events {
4568 if self.log_groups_state.event_scroll_offset < 10
4569 && self.log_groups_state.has_older_events
4570 {
4571 self.log_groups_state.loading = true;
4572 }
4573 self.log_groups_state.event_scroll_offset =
4574 self.log_groups_state.event_scroll_offset.saturating_sub(10);
4575 } else if self.view_mode == ViewMode::InsightsResults {
4576 self.insights_state.insights.results_selected = self
4577 .insights_state
4578 .insights
4579 .results_selected
4580 .saturating_sub(10);
4581 } else if self.current_service == Service::CloudWatchAlarms {
4582 self.alarms_state.table.page_up();
4583 } else if self.current_service == Service::EcrRepositories {
4584 if self.ecr_state.current_repository.is_some() {
4585 self.ecr_state.images.page_up();
4586 } else {
4587 self.ecr_state.repositories.page_up();
4588 }
4589 } else if self.current_service == Service::SqsQueues {
4590 self.sqs_state.queues.page_up();
4591 } else if self.current_service == Service::LambdaFunctions {
4592 self.lambda_state.table.page_up();
4593 } else if self.current_service == Service::LambdaApplications {
4594 self.lambda_application_state.table.page_up();
4595 } else if self.current_service == Service::CloudFormationStacks {
4596 self.cfn_state.table.page_up();
4597 } else if self.current_service == Service::IamUsers {
4598 self.iam_state.users.page_up();
4599 } else if self.current_service == Service::IamRoles {
4600 if self.iam_state.current_role.is_some() {
4601 self.iam_state.policies.page_up();
4602 } else {
4603 self.iam_state.roles.page_up();
4604 }
4605 }
4606 }
4607 }
4608
4609 fn next_pane(&mut self) {
4610 if self.current_service == Service::S3Buckets {
4611 if self.s3_state.current_bucket.is_some() {
4612 let mut visual_idx = 0;
4615 let mut found_obj: Option<S3Object> = None;
4616
4617 fn check_nested(
4619 obj: &S3Object,
4620 visual_idx: &mut usize,
4621 target_idx: usize,
4622 expanded_prefixes: &std::collections::HashSet<String>,
4623 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
4624 found_obj: &mut Option<S3Object>,
4625 ) {
4626 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4627 if let Some(preview) = prefix_preview.get(&obj.key) {
4628 for nested_obj in preview {
4629 if *visual_idx == target_idx {
4630 *found_obj = Some(nested_obj.clone());
4631 return;
4632 }
4633 *visual_idx += 1;
4634
4635 check_nested(
4637 nested_obj,
4638 visual_idx,
4639 target_idx,
4640 expanded_prefixes,
4641 prefix_preview,
4642 found_obj,
4643 );
4644 if found_obj.is_some() {
4645 return;
4646 }
4647 }
4648 } else {
4649 *visual_idx += 1;
4651 }
4652 }
4653 }
4654
4655 for obj in &self.s3_state.objects {
4656 if visual_idx == self.s3_state.selected_object {
4657 found_obj = Some(obj.clone());
4658 break;
4659 }
4660 visual_idx += 1;
4661
4662 check_nested(
4664 obj,
4665 &mut visual_idx,
4666 self.s3_state.selected_object,
4667 &self.s3_state.expanded_prefixes,
4668 &self.s3_state.prefix_preview,
4669 &mut found_obj,
4670 );
4671 if found_obj.is_some() {
4672 break;
4673 }
4674 }
4675
4676 if let Some(obj) = found_obj {
4677 if obj.is_prefix {
4678 if !self.s3_state.expanded_prefixes.contains(&obj.key) {
4679 self.s3_state.expanded_prefixes.insert(obj.key.clone());
4680 if !self.s3_state.prefix_preview.contains_key(&obj.key) {
4682 self.s3_state.buckets.loading = true;
4683 }
4684 }
4685 if self.s3_state.expanded_prefixes.contains(&obj.key) {
4687 if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
4688 if !preview.is_empty() {
4689 self.s3_state.selected_object += 1;
4690 }
4691 }
4692 }
4693 }
4694 }
4695 } else {
4696 let mut row_idx = 0;
4698 let mut found = false;
4699 for bucket in &self.s3_state.buckets.items {
4700 if row_idx == self.s3_state.selected_row {
4701 if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
4703 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
4704 if !self.s3_state.bucket_preview.contains_key(&bucket.name)
4705 && !self.s3_state.bucket_errors.contains_key(&bucket.name)
4706 {
4707 self.s3_state.buckets.loading = true;
4708 }
4709 }
4710 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4712 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4713 if !preview.is_empty() {
4714 self.s3_state.selected_row = row_idx + 1;
4715 }
4716 }
4717 }
4718 break;
4719 }
4720 row_idx += 1;
4721
4722 if self.s3_state.bucket_errors.contains_key(&bucket.name)
4724 && self.s3_state.expanded_prefixes.contains(&bucket.name)
4725 {
4726 continue;
4727 }
4728
4729 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4730 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4731 #[allow(clippy::too_many_arguments)]
4733 fn check_nested_expansion(
4734 objects: &[crate::s3::Object],
4735 row_idx: &mut usize,
4736 target_row: usize,
4737 expanded_prefixes: &mut std::collections::HashSet<String>,
4738 prefix_preview: &std::collections::HashMap<
4739 String,
4740 Vec<crate::s3::Object>,
4741 >,
4742 found: &mut bool,
4743 loading: &mut bool,
4744 selected_row: &mut usize,
4745 ) {
4746 for obj in objects {
4747 if *row_idx == target_row {
4748 if obj.is_prefix {
4750 if !expanded_prefixes.contains(&obj.key) {
4751 expanded_prefixes.insert(obj.key.clone());
4752 if !prefix_preview.contains_key(&obj.key) {
4753 *loading = true;
4754 }
4755 }
4756 if expanded_prefixes.contains(&obj.key) {
4758 if let Some(preview) = prefix_preview.get(&obj.key)
4759 {
4760 if !preview.is_empty() {
4761 *selected_row = *row_idx + 1;
4762 }
4763 }
4764 }
4765 }
4766 *found = true;
4767 return;
4768 }
4769 *row_idx += 1;
4770
4771 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4773 if let Some(nested) = prefix_preview.get(&obj.key) {
4774 check_nested_expansion(
4775 nested,
4776 row_idx,
4777 target_row,
4778 expanded_prefixes,
4779 prefix_preview,
4780 found,
4781 loading,
4782 selected_row,
4783 );
4784 if *found {
4785 return;
4786 }
4787 } else {
4788 *row_idx += 1; }
4790 }
4791 }
4792 }
4793
4794 check_nested_expansion(
4795 preview,
4796 &mut row_idx,
4797 self.s3_state.selected_row,
4798 &mut self.s3_state.expanded_prefixes,
4799 &self.s3_state.prefix_preview,
4800 &mut found,
4801 &mut self.s3_state.buckets.loading,
4802 &mut self.s3_state.selected_row,
4803 );
4804 if found || row_idx > self.s3_state.selected_row {
4805 break;
4806 }
4807 } else {
4808 row_idx += 1;
4809 if row_idx > self.s3_state.selected_row {
4810 break;
4811 }
4812 }
4813 }
4814 if found {
4815 break;
4816 }
4817 }
4818 }
4819 } else if self.view_mode == ViewMode::InsightsResults {
4820 let max_cols = self
4822 .insights_state
4823 .insights
4824 .query_results
4825 .first()
4826 .map(|r| r.len())
4827 .unwrap_or(0);
4828 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
4829 self.insights_state.insights.results_horizontal_scroll += 1;
4830 }
4831 } else if self.current_service == Service::CloudWatchLogGroups
4832 && self.view_mode == ViewMode::List
4833 {
4834 if self.log_groups_state.log_groups.expanded_item
4836 != Some(self.log_groups_state.log_groups.selected)
4837 {
4838 self.log_groups_state.log_groups.expanded_item =
4839 Some(self.log_groups_state.log_groups.selected);
4840 }
4841 } else if self.current_service == Service::CloudWatchLogGroups
4842 && self.view_mode == ViewMode::Detail
4843 {
4844 if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
4846 {
4847 self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
4848 }
4849 } else if self.view_mode == ViewMode::Events {
4850 if self.log_groups_state.expanded_event
4853 != Some(self.log_groups_state.event_scroll_offset)
4854 {
4855 self.log_groups_state.expanded_event =
4856 Some(self.log_groups_state.event_scroll_offset);
4857 }
4858 } else if self.current_service == Service::CloudWatchAlarms {
4859 if !self.alarms_state.table.is_expanded() {
4861 self.alarms_state.table.toggle_expand();
4862 }
4863 } else if self.current_service == Service::EcrRepositories {
4864 if self.ecr_state.current_repository.is_some() {
4865 self.ecr_state.images.toggle_expand();
4867 } else {
4868 self.ecr_state.repositories.toggle_expand();
4870 }
4871 } else if self.current_service == Service::SqsQueues {
4872 if self.sqs_state.current_queue.is_some()
4873 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4874 {
4875 self.sqs_state.triggers.toggle_expand();
4876 } else if self.sqs_state.current_queue.is_some()
4877 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4878 {
4879 self.sqs_state.pipes.toggle_expand();
4880 } else if self.sqs_state.current_queue.is_some()
4881 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4882 {
4883 self.sqs_state.tags.toggle_expand();
4884 } else if self.sqs_state.current_queue.is_some()
4885 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4886 {
4887 self.sqs_state.subscriptions.toggle_expand();
4888 } else {
4889 self.sqs_state.queues.expand();
4890 }
4891 } else if self.current_service == Service::LambdaFunctions {
4892 if self.lambda_state.current_function.is_some()
4893 && self.lambda_state.detail_tab == LambdaDetailTab::Code
4894 {
4895 if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
4897 self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
4898 } else {
4899 self.lambda_state.layer_expanded = None;
4900 }
4901 } else if self.lambda_state.current_function.is_some()
4902 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4903 {
4904 self.lambda_state.version_table.toggle_expand();
4906 } else if self.lambda_state.current_function.is_some()
4907 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4908 || (self.lambda_state.current_version.is_some()
4909 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4910 {
4911 self.lambda_state.alias_table.toggle_expand();
4913 } else if self.lambda_state.current_function.is_none() {
4914 self.lambda_state.table.toggle_expand();
4916 }
4917 } else if self.current_service == Service::LambdaApplications {
4918 if self.lambda_application_state.current_application.is_some() {
4919 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
4921 {
4922 self.lambda_application_state.resources.toggle_expand();
4923 } else {
4924 self.lambda_application_state.deployments.toggle_expand();
4925 }
4926 } else {
4927 if self.lambda_application_state.table.expanded_item
4929 != Some(self.lambda_application_state.table.selected)
4930 {
4931 self.lambda_application_state.table.expanded_item =
4932 Some(self.lambda_application_state.table.selected);
4933 }
4934 }
4935 } else if self.current_service == Service::CloudFormationStacks
4936 && self.cfn_state.current_stack.is_none()
4937 {
4938 self.cfn_state.table.toggle_expand();
4939 } else if self.current_service == Service::IamUsers {
4940 if self.iam_state.current_user.is_some() {
4941 if self.iam_state.user_tab == UserTab::Tags {
4942 if self.iam_state.user_tags.expanded_item
4943 != Some(self.iam_state.user_tags.selected)
4944 {
4945 self.iam_state.user_tags.expanded_item =
4946 Some(self.iam_state.user_tags.selected);
4947 }
4948 } else if self.iam_state.policies.expanded_item
4949 != Some(self.iam_state.policies.selected)
4950 {
4951 self.iam_state.policies.toggle_expand();
4952 }
4953 } else if !self.iam_state.users.is_expanded() {
4954 self.iam_state.users.toggle_expand();
4955 }
4956 } else if self.current_service == Service::IamRoles {
4957 if self.iam_state.current_role.is_some() {
4958 if self.iam_state.role_tab == RoleTab::Tags {
4960 if !self.iam_state.tags.is_expanded() {
4961 self.iam_state.tags.expand();
4962 }
4963 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4964 if !self.iam_state.last_accessed_services.is_expanded() {
4965 self.iam_state.last_accessed_services.expand();
4966 }
4967 } else if !self.iam_state.policies.is_expanded() {
4968 self.iam_state.policies.expand();
4969 }
4970 } else if !self.iam_state.roles.is_expanded() {
4971 self.iam_state.roles.expand();
4972 }
4973 } else if self.current_service == Service::IamUserGroups {
4974 if self.iam_state.current_group.is_some() {
4975 if self.iam_state.group_tab == GroupTab::Users {
4976 if !self.iam_state.group_users.is_expanded() {
4977 self.iam_state.group_users.expand();
4978 }
4979 } else if self.iam_state.group_tab == GroupTab::Permissions {
4980 if !self.iam_state.policies.is_expanded() {
4981 self.iam_state.policies.expand();
4982 }
4983 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
4984 && !self.iam_state.last_accessed_services.is_expanded()
4985 {
4986 self.iam_state.last_accessed_services.expand();
4987 }
4988 } else if !self.iam_state.groups.is_expanded() {
4989 self.iam_state.groups.expand();
4990 }
4991 }
4992 }
4993
4994 fn go_to_page(&mut self, page: usize) {
4995 if page == 0 {
4996 return;
4997 }
4998
4999 match self.current_service {
5000 Service::CloudWatchAlarms => {
5001 let alarm_page_size = self.alarms_state.table.page_size.value();
5002 let target = (page - 1) * alarm_page_size;
5003 let filtered_count = match self.alarms_state.alarm_tab {
5004 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5005 AlarmTab::InAlarm => self
5006 .alarms_state
5007 .table
5008 .items
5009 .iter()
5010 .filter(|a| a.state.to_uppercase() == "ALARM")
5011 .count(),
5012 };
5013 let max_offset = filtered_count.saturating_sub(alarm_page_size);
5014 self.alarms_state.table.scroll_offset = target.min(max_offset);
5015 self.alarms_state.table.selected = self
5016 .alarms_state
5017 .table
5018 .scroll_offset
5019 .min(filtered_count.saturating_sub(1));
5020 }
5021 Service::CloudWatchLogGroups => match self.view_mode {
5022 ViewMode::Events => {
5023 let page_size = 20;
5024 let target = (page - 1) * page_size;
5025 let max = self.log_groups_state.log_events.len().saturating_sub(1);
5026 self.log_groups_state.event_scroll_offset = target.min(max);
5027 }
5028 ViewMode::Detail => {
5029 let page_size = 20;
5030 let target = (page - 1) * page_size;
5031 let max = self.log_groups_state.log_streams.len().saturating_sub(1);
5032 self.log_groups_state.selected_stream = target.min(max);
5033 }
5034 ViewMode::List => {
5035 let total = self.log_groups_state.log_groups.items.len();
5036 self.log_groups_state.log_groups.goto_page(page, total);
5037 }
5038 _ => {}
5039 },
5040 Service::EcrRepositories => {
5041 if self.ecr_state.current_repository.is_some() {
5042 let filtered_count = self
5043 .ecr_state
5044 .images
5045 .filtered(|img| {
5046 self.ecr_state.images.filter.is_empty()
5047 || img
5048 .tag
5049 .to_lowercase()
5050 .contains(&self.ecr_state.images.filter.to_lowercase())
5051 || img
5052 .digest
5053 .to_lowercase()
5054 .contains(&self.ecr_state.images.filter.to_lowercase())
5055 })
5056 .len();
5057 self.ecr_state.images.goto_page(page, filtered_count);
5058 } else {
5059 let filtered_count = self
5060 .ecr_state
5061 .repositories
5062 .filtered(|r| {
5063 self.ecr_state.repositories.filter.is_empty()
5064 || r.name
5065 .to_lowercase()
5066 .contains(&self.ecr_state.repositories.filter.to_lowercase())
5067 })
5068 .len();
5069 self.ecr_state.repositories.goto_page(page, filtered_count);
5070 }
5071 }
5072 Service::SqsQueues => {
5073 let filtered_count = crate::ui::sqs::filtered_queues(
5074 &self.sqs_state.queues.items,
5075 &self.sqs_state.queues.filter,
5076 )
5077 .len();
5078 self.sqs_state.queues.goto_page(page, filtered_count);
5079 }
5080 Service::S3Buckets => {
5081 if self.s3_state.current_bucket.is_some() {
5082 let page_size = 50; let target = (page - 1) * page_size;
5084 let total_rows = self.calculate_total_object_rows();
5085 let max = total_rows.saturating_sub(1);
5086 self.s3_state.selected_object = target.min(max);
5087 } else {
5088 let page_size = 50; let target = (page - 1) * page_size;
5090 let total_rows = self.calculate_total_bucket_rows();
5091 let max = total_rows.saturating_sub(1);
5092 self.s3_state.selected_row = target.min(max);
5093 }
5094 }
5095 Service::LambdaFunctions => {
5096 if self.lambda_state.current_function.is_some()
5097 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5098 {
5099 let filtered_count = self
5100 .lambda_state
5101 .version_table
5102 .filtered(|v| {
5103 self.lambda_state.version_table.filter.is_empty()
5104 || v.version.to_lowercase().contains(
5105 &self.lambda_state.version_table.filter.to_lowercase(),
5106 )
5107 || v.aliases.to_lowercase().contains(
5108 &self.lambda_state.version_table.filter.to_lowercase(),
5109 )
5110 || v.description.to_lowercase().contains(
5111 &self.lambda_state.version_table.filter.to_lowercase(),
5112 )
5113 })
5114 .len();
5115 self.lambda_state
5116 .version_table
5117 .goto_page(page, filtered_count);
5118 } else {
5119 let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
5120 self.lambda_state.table.goto_page(page, filtered_count);
5121 }
5122 }
5123 Service::LambdaApplications => {
5124 let filtered_count = crate::ui::lambda::filtered_lambda_applications(self).len();
5125 self.lambda_application_state
5126 .table
5127 .goto_page(page, filtered_count);
5128 }
5129 Service::CloudFormationStacks => {
5130 let filtered_count = self.filtered_cloudformation_stacks().len();
5131 self.cfn_state.table.goto_page(page, filtered_count);
5132 }
5133 Service::IamUsers => {
5134 let filtered_count = crate::ui::iam::filtered_iam_users(self).len();
5135 self.iam_state.users.goto_page(page, filtered_count);
5136 }
5137 Service::IamRoles => {
5138 let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
5139 self.iam_state.roles.goto_page(page, filtered_count);
5140 }
5141 _ => {}
5142 }
5143 }
5144
5145 fn prev_pane(&mut self) {
5146 if self.current_service == Service::S3Buckets {
5147 if self.s3_state.current_bucket.is_some() {
5148 let mut visual_idx = 0;
5151 let mut found_obj: Option<S3Object> = None;
5152 let mut parent_idx: Option<usize> = None;
5153
5154 #[allow(clippy::too_many_arguments)]
5156 fn find_with_parent(
5157 objects: &[S3Object],
5158 visual_idx: &mut usize,
5159 target_idx: usize,
5160 expanded_prefixes: &std::collections::HashSet<String>,
5161 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5162 found_obj: &mut Option<S3Object>,
5163 parent_idx: &mut Option<usize>,
5164 current_parent: Option<usize>,
5165 ) {
5166 for obj in objects {
5167 if *visual_idx == target_idx {
5168 *found_obj = Some(obj.clone());
5169 *parent_idx = current_parent;
5170 return;
5171 }
5172 let obj_idx = *visual_idx;
5173 *visual_idx += 1;
5174
5175 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5177 if let Some(preview) = prefix_preview.get(&obj.key) {
5178 find_with_parent(
5179 preview,
5180 visual_idx,
5181 target_idx,
5182 expanded_prefixes,
5183 prefix_preview,
5184 found_obj,
5185 parent_idx,
5186 Some(obj_idx),
5187 );
5188 if found_obj.is_some() {
5189 return;
5190 }
5191 }
5192 }
5193 }
5194 }
5195
5196 find_with_parent(
5197 &self.s3_state.objects,
5198 &mut visual_idx,
5199 self.s3_state.selected_object,
5200 &self.s3_state.expanded_prefixes,
5201 &self.s3_state.prefix_preview,
5202 &mut found_obj,
5203 &mut parent_idx,
5204 None,
5205 );
5206
5207 if let Some(obj) = found_obj {
5208 if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
5209 self.s3_state.expanded_prefixes.remove(&obj.key);
5211 } else if let Some(parent) = parent_idx {
5212 self.s3_state.selected_object = parent;
5214 }
5215 }
5216
5217 let visible_rows = self.s3_state.object_visible_rows.get();
5219 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5220 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5221 } else if self.s3_state.selected_object
5222 >= self.s3_state.object_scroll_offset + visible_rows
5223 {
5224 self.s3_state.object_scroll_offset = self
5225 .s3_state
5226 .selected_object
5227 .saturating_sub(visible_rows - 1);
5228 }
5229 } else {
5230 let mut row_idx = 0;
5232 for bucket in &self.s3_state.buckets.items {
5233 if row_idx == self.s3_state.selected_row {
5234 self.s3_state.expanded_prefixes.remove(&bucket.name);
5236 break;
5237 }
5238 row_idx += 1;
5239 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5240 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5241 #[allow(clippy::too_many_arguments)]
5243 fn check_nested_collapse(
5244 objects: &[crate::s3::Object],
5245 row_idx: &mut usize,
5246 target_row: usize,
5247 expanded_prefixes: &mut std::collections::HashSet<String>,
5248 prefix_preview: &std::collections::HashMap<
5249 String,
5250 Vec<crate::s3::Object>,
5251 >,
5252 found: &mut bool,
5253 selected_row: &mut usize,
5254 parent_row: usize,
5255 ) {
5256 for obj in objects {
5257 let current_row = *row_idx;
5258 if *row_idx == target_row {
5259 if obj.is_prefix {
5261 if expanded_prefixes.contains(&obj.key) {
5262 expanded_prefixes.remove(&obj.key);
5264 } else {
5265 *selected_row = parent_row;
5267 }
5268 } else {
5269 *selected_row = parent_row;
5271 }
5272 *found = true;
5273 return;
5274 }
5275 *row_idx += 1;
5276
5277 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5279 if let Some(nested) = prefix_preview.get(&obj.key) {
5280 check_nested_collapse(
5281 nested,
5282 row_idx,
5283 target_row,
5284 expanded_prefixes,
5285 prefix_preview,
5286 found,
5287 selected_row,
5288 current_row,
5289 );
5290 if *found {
5291 return;
5292 }
5293 } else {
5294 *row_idx += 1; }
5296 }
5297 }
5298 }
5299
5300 let mut found = false;
5301 let parent_row = row_idx - 1; check_nested_collapse(
5303 preview,
5304 &mut row_idx,
5305 self.s3_state.selected_row,
5306 &mut self.s3_state.expanded_prefixes,
5307 &self.s3_state.prefix_preview,
5308 &mut found,
5309 &mut self.s3_state.selected_row,
5310 parent_row,
5311 );
5312 if found {
5313 let visible_rows = self.s3_state.bucket_visible_rows.get();
5315 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5316 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5317 } else if self.s3_state.selected_row
5318 >= self.s3_state.bucket_scroll_offset + visible_rows
5319 {
5320 self.s3_state.bucket_scroll_offset =
5321 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
5322 }
5323 return;
5324 }
5325 } else {
5326 row_idx += 1;
5327 }
5328 }
5329 }
5330
5331 let visible_rows = self.s3_state.bucket_visible_rows.get();
5333 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5334 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5335 } else if self.s3_state.selected_row
5336 >= self.s3_state.bucket_scroll_offset + visible_rows
5337 {
5338 self.s3_state.bucket_scroll_offset =
5339 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
5340 }
5341 }
5342 } else if self.view_mode == ViewMode::InsightsResults {
5343 self.insights_state.insights.results_horizontal_scroll = self
5345 .insights_state
5346 .insights
5347 .results_horizontal_scroll
5348 .saturating_sub(1);
5349 } else if self.current_service == Service::CloudWatchLogGroups
5350 && self.view_mode == ViewMode::List
5351 {
5352 if self.log_groups_state.log_groups.has_expanded_item() {
5354 self.log_groups_state.log_groups.collapse();
5355 }
5356 } else if self.current_service == Service::CloudWatchLogGroups
5357 && self.view_mode == ViewMode::Detail
5358 {
5359 if self.log_groups_state.expanded_stream.is_some() {
5361 self.log_groups_state.expanded_stream = None;
5362 }
5363 } else if self.view_mode == ViewMode::Events {
5364 if self.log_groups_state.expanded_event.is_some() {
5366 self.log_groups_state.expanded_event = None;
5367 }
5368 } else if self.current_service == Service::CloudWatchAlarms {
5369 self.alarms_state.table.collapse();
5371 } else if self.current_service == Service::EcrRepositories {
5372 if self.ecr_state.current_repository.is_some() {
5373 self.ecr_state.images.collapse();
5375 } else {
5376 self.ecr_state.repositories.collapse();
5378 }
5379 } else if self.current_service == Service::SqsQueues {
5380 if self.sqs_state.current_queue.is_some()
5381 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5382 {
5383 self.sqs_state.triggers.collapse();
5384 } else if self.sqs_state.current_queue.is_some()
5385 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5386 {
5387 self.sqs_state.pipes.collapse();
5388 } else if self.sqs_state.current_queue.is_some()
5389 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5390 {
5391 self.sqs_state.tags.collapse();
5392 } else if self.sqs_state.current_queue.is_some()
5393 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5394 {
5395 self.sqs_state.subscriptions.collapse();
5396 } else {
5397 self.sqs_state.queues.collapse();
5398 }
5399 } else if self.current_service == Service::LambdaFunctions {
5400 if self.lambda_state.current_function.is_some()
5401 && self.lambda_state.detail_tab == LambdaDetailTab::Code
5402 {
5403 self.lambda_state.layer_expanded = None;
5405 } else if self.lambda_state.current_function.is_some()
5406 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5407 {
5408 self.lambda_state.version_table.collapse();
5410 } else if self.lambda_state.current_function.is_some()
5411 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5412 || (self.lambda_state.current_version.is_some()
5413 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5414 {
5415 self.lambda_state.alias_table.collapse();
5417 } else if self.lambda_state.current_function.is_none() {
5418 self.lambda_state.table.collapse();
5420 }
5421 } else if self.current_service == Service::LambdaApplications {
5422 if self.lambda_application_state.current_application.is_some() {
5423 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
5425 {
5426 self.lambda_application_state.resources.collapse();
5427 } else {
5428 self.lambda_application_state.deployments.collapse();
5429 }
5430 } else {
5431 if self.lambda_application_state.table.has_expanded_item() {
5433 self.lambda_application_state.table.collapse();
5434 }
5435 }
5436 } else if self.current_service == Service::CloudFormationStacks
5437 && self.cfn_state.current_stack.is_none()
5438 {
5439 self.cfn_state.table.collapse();
5440 } else if self.current_service == Service::IamUsers {
5441 if self.iam_state.users.has_expanded_item() {
5442 self.iam_state.users.collapse();
5443 }
5444 } else if self.current_service == Service::IamRoles {
5445 if self.view_mode == ViewMode::PolicyView {
5446 self.view_mode = ViewMode::Detail;
5448 self.iam_state.current_policy = None;
5449 self.iam_state.policy_document.clear();
5450 self.iam_state.policy_scroll = 0;
5451 } else if self.iam_state.current_role.is_some() {
5452 if self.iam_state.role_tab == RoleTab::Tags
5453 && self.iam_state.tags.has_expanded_item()
5454 {
5455 self.iam_state.tags.collapse();
5456 } else if self.iam_state.role_tab == RoleTab::LastAccessed
5457 && self
5458 .iam_state
5459 .last_accessed_services
5460 .expanded_item
5461 .is_some()
5462 {
5463 self.iam_state.last_accessed_services.collapse();
5464 } else if self.iam_state.policies.has_expanded_item() {
5465 self.iam_state.policies.collapse();
5466 }
5467 } else if self.iam_state.roles.has_expanded_item() {
5468 self.iam_state.roles.collapse();
5469 }
5470 } else if self.current_service == Service::IamUserGroups {
5471 if self.iam_state.current_group.is_some() {
5472 if self.iam_state.group_tab == GroupTab::Users
5473 && self.iam_state.group_users.has_expanded_item()
5474 {
5475 self.iam_state.group_users.collapse();
5476 } else if self.iam_state.group_tab == GroupTab::Permissions
5477 && self.iam_state.policies.has_expanded_item()
5478 {
5479 self.iam_state.policies.collapse();
5480 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
5481 && self
5482 .iam_state
5483 .last_accessed_services
5484 .expanded_item
5485 .is_some()
5486 {
5487 self.iam_state.last_accessed_services.collapse();
5488 }
5489 } else if self.iam_state.groups.has_expanded_item() {
5490 self.iam_state.groups.collapse();
5491 }
5492 }
5493 }
5494
5495 fn select_item(&mut self) {
5496 if self.mode == Mode::RegionPicker {
5497 let filtered = self.get_filtered_regions();
5498 if let Some(region) = filtered.get(self.region_picker_selected) {
5499 if !self.tabs.is_empty() {
5501 let mut session = Session::new(
5502 self.profile.clone(),
5503 self.region.clone(),
5504 self.config.account_id.clone(),
5505 self.config.role_arn.clone(),
5506 );
5507
5508 for tab in &self.tabs {
5509 session.tabs.push(SessionTab {
5510 service: format!("{:?}", tab.service),
5511 title: tab.title.clone(),
5512 breadcrumb: tab.breadcrumb.clone(),
5513 filter: None,
5514 selected_item: None,
5515 });
5516 }
5517
5518 let _ = session.save();
5519 }
5520
5521 self.region = region.code.to_string();
5522 self.config.region = region.code.to_string();
5523
5524 self.tabs.clear();
5526 self.current_tab = 0;
5527 self.service_selected = false;
5528
5529 self.mode = Mode::Normal;
5530 }
5531 } else if self.mode == Mode::ProfilePicker {
5532 let filtered = self.get_filtered_profiles();
5533 if let Some(profile) = filtered.get(self.profile_picker_selected) {
5534 let profile_name = profile.name.clone();
5535 let profile_region = profile.region.clone();
5536
5537 self.profile = profile_name.clone();
5538 std::env::set_var("AWS_PROFILE", &profile_name);
5539
5540 if let Some(region) = profile_region {
5542 self.region = region;
5543 }
5544
5545 self.mode = Mode::Normal;
5546 }
5548 } else if self.mode == Mode::ServicePicker {
5549 let filtered = self.filtered_services();
5550 if let Some(&service) = filtered.get(self.service_picker.selected) {
5551 let new_service = match service {
5552 "CloudWatch > Log Groups" => Service::CloudWatchLogGroups,
5553 "CloudWatch > Logs Insights" => Service::CloudWatchInsights,
5554 "CloudWatch > Alarms" => Service::CloudWatchAlarms,
5555 "CloudFormation > Stacks" => Service::CloudFormationStacks,
5556 "ECR > Repositories" => Service::EcrRepositories,
5557 "IAM > Users" => Service::IamUsers,
5558 "IAM > Roles" => Service::IamRoles,
5559 "IAM > User Groups" => Service::IamUserGroups,
5560 "Lambda > Functions" => Service::LambdaFunctions,
5561 "Lambda > Applications" => Service::LambdaApplications,
5562 "S3 > Buckets" => Service::S3Buckets,
5563 "SQS > Queues" => Service::SqsQueues,
5564 _ => return,
5565 };
5566
5567 self.tabs.push(Tab {
5569 service: new_service,
5570 title: service.to_string(),
5571 breadcrumb: service.to_string(),
5572 });
5573 self.current_tab = self.tabs.len() - 1;
5574 self.current_service = new_service;
5575 self.view_mode = ViewMode::List;
5576 self.service_selected = true;
5577 self.mode = Mode::Normal;
5578 }
5579 } else if self.mode == Mode::TabPicker {
5580 let filtered = self.get_filtered_tabs();
5581 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
5582 self.current_tab = idx;
5583 self.current_service = self.tabs[idx].service;
5584 self.mode = Mode::Normal;
5585 self.tab_filter.clear();
5586 }
5587 } else if self.mode == Mode::SessionPicker {
5588 let filtered = self.get_filtered_sessions();
5589 if let Some(&session) = filtered.get(self.session_picker_selected) {
5590 let session = session.clone();
5591
5592 self.current_session = Some(session.clone());
5594 self.profile = session.profile.clone();
5595 self.region = session.region.clone();
5596 self.config.region = session.region.clone();
5597 self.config.account_id = session.account_id.clone();
5598 self.config.role_arn = session.role_arn.clone();
5599
5600 self.tabs = session
5602 .tabs
5603 .iter()
5604 .map(|st| Tab {
5605 service: match st.service.as_str() {
5606 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
5607 "CloudWatchInsights" => Service::CloudWatchInsights,
5608 "CloudWatchAlarms" => Service::CloudWatchAlarms,
5609 "S3Buckets" => Service::S3Buckets,
5610 "SqsQueues" => Service::SqsQueues,
5611 _ => Service::CloudWatchLogGroups,
5612 },
5613 title: st.title.clone(),
5614 breadcrumb: st.breadcrumb.clone(),
5615 })
5616 .collect();
5617
5618 if !self.tabs.is_empty() {
5619 self.current_tab = 0;
5620 self.current_service = self.tabs[0].service;
5621 self.service_selected = true;
5622 }
5623
5624 self.mode = Mode::Normal;
5625 }
5626 } else if self.mode == Mode::InsightsInput {
5627 use crate::app::InsightsFocus;
5629 match self.insights_state.insights.insights_focus {
5630 InsightsFocus::Query => {
5631 self.insights_state.insights.query_text.push('\n');
5633 self.insights_state.insights.query_cursor_line += 1;
5634 self.insights_state.insights.query_cursor_col = 0;
5635 }
5636 InsightsFocus::LogGroupSearch => {
5637 self.insights_state.insights.show_dropdown =
5639 !self.insights_state.insights.show_dropdown;
5640 }
5641 _ => {}
5642 }
5643 } else if self.mode == Mode::Normal {
5644 if !self.service_selected {
5646 let filtered = self.filtered_services();
5647 if let Some(&service) = filtered.get(self.service_picker.selected) {
5648 match service {
5649 "CloudWatch > Log Groups" => {
5650 self.current_service = Service::CloudWatchLogGroups;
5651 self.view_mode = ViewMode::List;
5652 self.service_selected = true;
5653 }
5654 "CloudWatch > Logs Insights" => {
5655 self.current_service = Service::CloudWatchInsights;
5656 self.view_mode = ViewMode::InsightsResults;
5657 self.service_selected = true;
5658 }
5659 "CloudWatch > Alarms" => {
5660 self.current_service = Service::CloudWatchAlarms;
5661 self.view_mode = ViewMode::List;
5662 self.service_selected = true;
5663 }
5664 "S3 > Buckets" => {
5665 self.current_service = Service::S3Buckets;
5666 self.view_mode = ViewMode::List;
5667 self.service_selected = true;
5668 }
5669 "ECR > Repositories" => {
5670 self.current_service = Service::EcrRepositories;
5671 self.view_mode = ViewMode::List;
5672 self.service_selected = true;
5673 }
5674 "Lambda > Functions" => {
5675 self.current_service = Service::LambdaFunctions;
5676 self.view_mode = ViewMode::List;
5677 self.service_selected = true;
5678 }
5679 "Lambda > Applications" => {
5680 self.current_service = Service::LambdaApplications;
5681 self.view_mode = ViewMode::List;
5682 self.service_selected = true;
5683 }
5684 _ => {}
5685 }
5686 }
5687 return;
5688 }
5689
5690 if self.view_mode == ViewMode::InsightsResults {
5692 if self.insights_state.insights.expanded_result
5694 == Some(self.insights_state.insights.results_selected)
5695 {
5696 self.insights_state.insights.expanded_result = None;
5697 } else {
5698 self.insights_state.insights.expanded_result =
5699 Some(self.insights_state.insights.results_selected);
5700 }
5701 } else if self.current_service == Service::S3Buckets {
5702 if self.s3_state.current_bucket.is_none() {
5703 let mut row_idx = 0;
5705 for bucket in &self.s3_state.buckets.items {
5706 if row_idx == self.s3_state.selected_row {
5707 self.s3_state.current_bucket = Some(bucket.name.clone());
5709 self.s3_state.prefix_stack.clear();
5710 self.s3_state.buckets.loading = true;
5711 return;
5712 }
5713 row_idx += 1;
5714
5715 if self.s3_state.bucket_errors.contains_key(&bucket.name)
5717 && self.s3_state.expanded_prefixes.contains(&bucket.name)
5718 {
5719 continue;
5720 }
5721
5722 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5723 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5724 for obj in preview {
5725 if row_idx == self.s3_state.selected_row {
5726 if obj.is_prefix {
5728 self.s3_state.current_bucket =
5729 Some(bucket.name.clone());
5730 self.s3_state.prefix_stack = vec![obj.key.clone()];
5731 self.s3_state.buckets.loading = true;
5732 }
5733 return;
5734 }
5735 row_idx += 1;
5736
5737 if obj.is_prefix
5739 && self.s3_state.expanded_prefixes.contains(&obj.key)
5740 {
5741 if let Some(nested) =
5742 self.s3_state.prefix_preview.get(&obj.key)
5743 {
5744 for nested_obj in nested {
5745 if row_idx == self.s3_state.selected_row {
5746 if nested_obj.is_prefix {
5748 self.s3_state.current_bucket =
5749 Some(bucket.name.clone());
5750 self.s3_state.prefix_stack = vec![
5752 obj.key.clone(),
5753 nested_obj.key.clone(),
5754 ];
5755 self.s3_state.buckets.loading = true;
5756 }
5757 return;
5758 }
5759 row_idx += 1;
5760 }
5761 } else {
5762 row_idx += 1;
5763 }
5764 }
5765 }
5766 } else {
5767 row_idx += 1;
5768 }
5769 }
5770 }
5771 } else {
5772 let mut visual_idx = 0;
5774 let mut found_obj: Option<S3Object> = None;
5775
5776 fn check_nested_select(
5778 obj: &S3Object,
5779 visual_idx: &mut usize,
5780 target_idx: usize,
5781 expanded_prefixes: &std::collections::HashSet<String>,
5782 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5783 found_obj: &mut Option<S3Object>,
5784 ) {
5785 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5786 if let Some(preview) = prefix_preview.get(&obj.key) {
5787 for nested_obj in preview {
5788 if *visual_idx == target_idx {
5789 *found_obj = Some(nested_obj.clone());
5790 return;
5791 }
5792 *visual_idx += 1;
5793
5794 check_nested_select(
5796 nested_obj,
5797 visual_idx,
5798 target_idx,
5799 expanded_prefixes,
5800 prefix_preview,
5801 found_obj,
5802 );
5803 if found_obj.is_some() {
5804 return;
5805 }
5806 }
5807 } else {
5808 *visual_idx += 1;
5810 }
5811 }
5812 }
5813
5814 for obj in &self.s3_state.objects {
5815 if visual_idx == self.s3_state.selected_object {
5816 found_obj = Some(obj.clone());
5817 break;
5818 }
5819 visual_idx += 1;
5820
5821 check_nested_select(
5823 obj,
5824 &mut visual_idx,
5825 self.s3_state.selected_object,
5826 &self.s3_state.expanded_prefixes,
5827 &self.s3_state.prefix_preview,
5828 &mut found_obj,
5829 );
5830 if found_obj.is_some() {
5831 break;
5832 }
5833 }
5834
5835 if let Some(obj) = found_obj {
5836 if obj.is_prefix {
5837 self.s3_state.prefix_stack.push(obj.key.clone());
5839 self.s3_state.buckets.loading = true;
5840 }
5841 }
5842 }
5843 } else if self.current_service == Service::CloudFormationStacks {
5844 if self.cfn_state.current_stack.is_none() {
5845 let filtered_stacks = self.filtered_cloudformation_stacks();
5847 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
5848 self.cfn_state.current_stack = Some(stack.name.clone());
5849 self.update_current_tab_breadcrumb();
5850 }
5851 }
5852 } else if self.current_service == Service::EcrRepositories {
5853 if self.ecr_state.current_repository.is_none() {
5854 let filtered_repos = self.filtered_ecr_repositories();
5856 if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
5857 let repo_name = repo.name.clone();
5858 let repo_uri = repo.uri.clone();
5859 self.ecr_state.current_repository = Some(repo_name);
5860 self.ecr_state.current_repository_uri = Some(repo_uri);
5861 self.ecr_state.images.reset();
5862 self.ecr_state.repositories.loading = true;
5863 }
5864 }
5865 } else if self.current_service == Service::SqsQueues {
5866 if self.sqs_state.current_queue.is_none() {
5867 let filtered_queues = crate::ui::sqs::filtered_queues(
5868 &self.sqs_state.queues.items,
5869 &self.sqs_state.queues.filter,
5870 );
5871 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
5872 self.sqs_state.current_queue = Some(queue.url.clone());
5873
5874 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5875 self.sqs_state.metrics_loading = true;
5876 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
5877 self.sqs_state.triggers.loading = true;
5878 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
5879 self.sqs_state.pipes.loading = true;
5880 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
5881 self.sqs_state.tags.loading = true;
5882 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
5883 self.sqs_state.subscriptions.loading = true;
5884 }
5885 }
5886 }
5887 } else if self.current_service == Service::IamUsers {
5888 if self.iam_state.current_user.is_some() {
5889 if self.iam_state.user_tab != UserTab::Tags {
5891 let filtered = crate::ui::iam::filtered_iam_policies(self);
5892 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5893 self.iam_state.current_policy = Some(policy.policy_name.clone());
5894 self.iam_state.policy_scroll = 0;
5895 self.view_mode = ViewMode::PolicyView;
5896 self.iam_state.policies.loading = true;
5897 self.update_current_tab_breadcrumb();
5898 }
5899 }
5900 } else if self.iam_state.current_user.is_none() {
5901 let filtered_users = crate::ui::iam::filtered_iam_users(self);
5902 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
5903 self.iam_state.current_user = Some(user.user_name.clone());
5904 self.iam_state.user_tab = UserTab::Permissions;
5905 self.iam_state.policies.reset();
5906 self.update_current_tab_breadcrumb();
5907 }
5908 }
5909 } else if self.current_service == Service::IamRoles {
5910 if self.iam_state.current_role.is_some() {
5911 if self.iam_state.role_tab != RoleTab::Tags {
5913 let filtered = crate::ui::iam::filtered_iam_policies(self);
5914 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5915 self.iam_state.current_policy = Some(policy.policy_name.clone());
5916 self.iam_state.policy_scroll = 0;
5917 self.view_mode = ViewMode::PolicyView;
5918 self.iam_state.policies.loading = true;
5919 self.update_current_tab_breadcrumb();
5920 }
5921 }
5922 } else if self.iam_state.current_role.is_none() {
5923 let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
5924 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
5925 self.iam_state.current_role = Some(role.role_name.clone());
5926 self.iam_state.role_tab = RoleTab::Permissions;
5927 self.iam_state.policies.reset();
5928 self.update_current_tab_breadcrumb();
5929 }
5930 }
5931 } else if self.current_service == Service::IamUserGroups {
5932 if self.iam_state.current_group.is_none() {
5933 let filtered_groups: Vec<_> = self
5934 .iam_state
5935 .groups
5936 .items
5937 .iter()
5938 .filter(|g| {
5939 if self.iam_state.groups.filter.is_empty() {
5940 true
5941 } else {
5942 g.group_name
5943 .to_lowercase()
5944 .contains(&self.iam_state.groups.filter.to_lowercase())
5945 }
5946 })
5947 .collect();
5948 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
5949 self.iam_state.current_group = Some(group.group_name.clone());
5950 self.update_current_tab_breadcrumb();
5951 }
5952 }
5953 } else if self.current_service == Service::LambdaFunctions {
5954 if self.lambda_state.current_function.is_some()
5955 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5956 {
5957 if self.mode == Mode::Normal {
5960 let page_size = self.lambda_state.version_table.page_size.value();
5961 let filtered: Vec<_> = self
5962 .lambda_state
5963 .version_table
5964 .items
5965 .iter()
5966 .filter(|v| {
5967 self.lambda_state.version_table.filter.is_empty()
5968 || v.version.to_lowercase().contains(
5969 &self.lambda_state.version_table.filter.to_lowercase(),
5970 )
5971 || v.aliases.to_lowercase().contains(
5972 &self.lambda_state.version_table.filter.to_lowercase(),
5973 )
5974 })
5975 .collect();
5976 let current_page = self.lambda_state.version_table.selected / page_size;
5977 let start_idx = current_page * page_size;
5978 let end_idx = (start_idx + page_size).min(filtered.len());
5979 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
5980 let page_index = self.lambda_state.version_table.selected % page_size;
5981 if let Some(version) = paginated.get(page_index) {
5982 self.lambda_state.current_version = Some(version.version.clone());
5983 self.lambda_state.detail_tab = LambdaDetailTab::Code;
5984 }
5985 } else {
5986 if self.lambda_state.version_table.expanded_item
5988 == Some(self.lambda_state.version_table.selected)
5989 {
5990 self.lambda_state.version_table.collapse();
5991 } else {
5992 self.lambda_state.version_table.expanded_item =
5993 Some(self.lambda_state.version_table.selected);
5994 }
5995 }
5996 } else if self.lambda_state.current_function.is_some()
5997 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5998 {
5999 let filtered: Vec<_> = self
6001 .lambda_state
6002 .alias_table
6003 .items
6004 .iter()
6005 .filter(|a| {
6006 self.lambda_state.alias_table.filter.is_empty()
6007 || a.name
6008 .to_lowercase()
6009 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6010 || a.versions
6011 .to_lowercase()
6012 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6013 })
6014 .collect();
6015 if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
6016 self.lambda_state.current_alias = Some(alias.name.clone());
6017 }
6018 } else if self.lambda_state.current_function.is_none() {
6019 let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
6020 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
6021 self.lambda_state.current_function = Some(func.name.clone());
6022 self.lambda_state.detail_tab = LambdaDetailTab::Code;
6023 self.update_current_tab_breadcrumb();
6024 }
6025 }
6026 } else if self.current_service == Service::LambdaApplications {
6027 let filtered = crate::ui::lambda::filtered_lambda_applications(self);
6028 if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
6029 let app_name = app.name.clone();
6030 self.lambda_application_state.current_application = Some(app_name.clone());
6031 self.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Overview;
6032
6033 use crate::lambda::Resource;
6035 self.lambda_application_state.resources.items = vec![
6036 Resource {
6037 logical_id: "ApiGatewayRestApi".to_string(),
6038 physical_id: "abc123xyz".to_string(),
6039 resource_type: "AWS::ApiGateway::RestApi".to_string(),
6040 last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
6041 },
6042 Resource {
6043 logical_id: "LambdaFunction".to_string(),
6044 physical_id: format!("{}-function", app_name),
6045 resource_type: "AWS::Lambda::Function".to_string(),
6046 last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
6047 },
6048 Resource {
6049 logical_id: "DynamoDBTable".to_string(),
6050 physical_id: format!("{}-table", app_name),
6051 resource_type: "AWS::DynamoDB::Table".to_string(),
6052 last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
6053 },
6054 ];
6055
6056 use crate::lambda::Deployment;
6058 self.lambda_application_state.deployments.items = vec![
6059 Deployment {
6060 deployment_id: "d-ABC123XYZ".to_string(),
6061 resource_type: "AWS::Serverless::Application".to_string(),
6062 last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
6063 status: "Succeeded".to_string(),
6064 },
6065 Deployment {
6066 deployment_id: "d-DEF456UVW".to_string(),
6067 resource_type: "AWS::Serverless::Application".to_string(),
6068 last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
6069 status: "Succeeded".to_string(),
6070 },
6071 ];
6072
6073 self.update_current_tab_breadcrumb();
6074 }
6075 } else if self.current_service == Service::CloudWatchLogGroups {
6076 if self.view_mode == ViewMode::List {
6077 let filtered_groups = self.filtered_log_groups();
6079 if let Some(selected_group) =
6080 filtered_groups.get(self.log_groups_state.log_groups.selected)
6081 {
6082 if let Some(actual_idx) = self
6083 .log_groups_state
6084 .log_groups
6085 .items
6086 .iter()
6087 .position(|g| g.name == selected_group.name)
6088 {
6089 self.log_groups_state.log_groups.selected = actual_idx;
6090 }
6091 }
6092 self.view_mode = ViewMode::Detail;
6093 self.log_groups_state.log_streams.clear();
6094 self.log_groups_state.selected_stream = 0;
6095 self.log_groups_state.loading = true;
6096 self.update_current_tab_breadcrumb();
6097 } else if self.view_mode == ViewMode::Detail {
6098 let filtered_streams = self.filtered_log_streams();
6100 if let Some(selected_stream) =
6101 filtered_streams.get(self.log_groups_state.selected_stream)
6102 {
6103 if let Some(actual_idx) = self
6104 .log_groups_state
6105 .log_streams
6106 .iter()
6107 .position(|s| s.name == selected_stream.name)
6108 {
6109 self.log_groups_state.selected_stream = actual_idx;
6110 }
6111 }
6112 self.view_mode = ViewMode::Events;
6113 self.update_current_tab_breadcrumb();
6114 self.log_groups_state.log_events.clear();
6115 self.log_groups_state.event_scroll_offset = 0;
6116 self.log_groups_state.next_backward_token = None;
6117 self.log_groups_state.loading = true;
6118 } else if self.view_mode == ViewMode::Events {
6119 if self.log_groups_state.expanded_event
6121 == Some(self.log_groups_state.event_scroll_offset)
6122 {
6123 self.log_groups_state.expanded_event = None;
6124 } else {
6125 self.log_groups_state.expanded_event =
6126 Some(self.log_groups_state.event_scroll_offset);
6127 }
6128 }
6129 } else if self.current_service == Service::CloudWatchAlarms {
6130 self.alarms_state.table.toggle_expand();
6132 } else if self.current_service == Service::CloudWatchInsights {
6133 if !self.insights_state.insights.selected_log_groups.is_empty() {
6135 self.log_groups_state.loading = true;
6136 self.insights_state.insights.query_completed = true;
6137 }
6138 }
6139 }
6140 }
6141
6142 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
6143 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
6144 Ok(())
6145 }
6146
6147 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
6148 let alarms = self.alarms_client.list_alarms().await?;
6149 self.alarms_state.table.items = alarms
6150 .into_iter()
6151 .map(
6152 |(
6153 name,
6154 state,
6155 state_updated,
6156 description,
6157 metric_name,
6158 namespace,
6159 statistic,
6160 period,
6161 comparison,
6162 threshold,
6163 actions_enabled,
6164 state_reason,
6165 resource,
6166 dimensions,
6167 expression,
6168 alarm_type,
6169 cross_account,
6170 )| Alarm {
6171 name,
6172 state,
6173 state_updated_timestamp: state_updated,
6174 description,
6175 metric_name,
6176 namespace,
6177 statistic,
6178 period,
6179 comparison_operator: comparison,
6180 threshold,
6181 actions_enabled,
6182 state_reason,
6183 resource,
6184 dimensions,
6185 expression,
6186 alarm_type,
6187 cross_account,
6188 },
6189 )
6190 .collect();
6191 Ok(())
6192 }
6193
6194 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
6195 if let Some(bucket_name) = &self.s3_state.current_bucket {
6196 let bucket_region = if let Some(bucket) = self
6198 .s3_state
6199 .buckets
6200 .items
6201 .iter_mut()
6202 .find(|b| &b.name == bucket_name)
6203 {
6204 if bucket.region.is_empty() {
6205 let region = self.s3_client.get_bucket_location(bucket_name).await?;
6207 bucket.region = region.clone();
6208 region
6209 } else {
6210 bucket.region.clone()
6211 }
6212 } else {
6213 self.config.region.clone()
6214 };
6215
6216 let prefix = self
6217 .s3_state
6218 .prefix_stack
6219 .last()
6220 .cloned()
6221 .unwrap_or_default();
6222 let objects = self
6223 .s3_client
6224 .list_objects(bucket_name, &bucket_region, &prefix)
6225 .await?;
6226 self.s3_state.objects = objects
6227 .into_iter()
6228 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
6229 key,
6230 size,
6231 last_modified: modified,
6232 is_prefix,
6233 storage_class,
6234 })
6235 .collect();
6236 self.s3_state.selected_object = 0;
6237 }
6238 Ok(())
6239 }
6240
6241 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
6242 let bucket_region = self
6243 .s3_state
6244 .buckets
6245 .items
6246 .iter()
6247 .find(|b| b.name == bucket_name)
6248 .and_then(|b| {
6249 if b.region.is_empty() {
6250 None
6251 } else {
6252 Some(b.region.as_str())
6253 }
6254 })
6255 .unwrap_or(self.config.region.as_str());
6256 let objects = self
6257 .s3_client
6258 .list_objects(&bucket_name, bucket_region, "")
6259 .await?;
6260 let preview: Vec<S3Object> = objects
6261 .into_iter()
6262 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
6263 key,
6264 size,
6265 last_modified: modified,
6266 is_prefix,
6267 storage_class,
6268 })
6269 .collect();
6270 self.s3_state.bucket_preview.insert(bucket_name, preview);
6271 Ok(())
6272 }
6273
6274 pub async fn load_prefix_preview(
6275 &mut self,
6276 bucket_name: String,
6277 prefix: String,
6278 ) -> anyhow::Result<()> {
6279 let bucket_region = self
6280 .s3_state
6281 .buckets
6282 .items
6283 .iter()
6284 .find(|b| b.name == bucket_name)
6285 .and_then(|b| {
6286 if b.region.is_empty() {
6287 None
6288 } else {
6289 Some(b.region.as_str())
6290 }
6291 })
6292 .unwrap_or(self.config.region.as_str());
6293 let objects = self
6294 .s3_client
6295 .list_objects(&bucket_name, bucket_region, &prefix)
6296 .await?;
6297 let preview: Vec<S3Object> = objects
6298 .into_iter()
6299 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
6300 key,
6301 size,
6302 last_modified: modified,
6303 is_prefix,
6304 storage_class,
6305 })
6306 .collect();
6307 self.s3_state.prefix_preview.insert(prefix, preview);
6308 Ok(())
6309 }
6310
6311 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
6312 let repos = match self.ecr_state.tab {
6313 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
6314 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
6315 };
6316
6317 self.ecr_state.repositories.items = repos
6318 .into_iter()
6319 .map(|r| EcrRepository {
6320 name: r.name,
6321 uri: r.uri,
6322 created_at: r.created_at,
6323 tag_immutability: r.tag_immutability,
6324 encryption_type: r.encryption_type,
6325 })
6326 .collect();
6327
6328 self.ecr_state
6329 .repositories
6330 .items
6331 .sort_by(|a, b| a.name.cmp(&b.name));
6332 Ok(())
6333 }
6334
6335 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
6336 if let Some(repo_name) = &self.ecr_state.current_repository {
6337 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
6338 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
6339
6340 self.ecr_state.images.items = images
6341 .into_iter()
6342 .map(|i| EcrImage {
6343 tag: i.tag,
6344 artifact_type: i.artifact_type,
6345 pushed_at: i.pushed_at,
6346 size_bytes: i.size_bytes,
6347 uri: i.uri,
6348 digest: i.digest,
6349 last_pull_time: i.last_pull_time,
6350 })
6351 .collect();
6352
6353 self.ecr_state
6354 .images
6355 .items
6356 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
6357 }
6358 }
6359 Ok(())
6360 }
6361
6362 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
6363 let stacks = self
6364 .cloudformation_client
6365 .list_stacks(self.cfn_state.view_nested)
6366 .await?;
6367
6368 let mut stacks: Vec<CfnStack> = stacks
6369 .into_iter()
6370 .map(|s| CfnStack {
6371 name: s.name,
6372 stack_id: s.stack_id,
6373 status: s.status,
6374 created_time: s.created_time,
6375 updated_time: s.updated_time,
6376 deleted_time: s.deleted_time,
6377 drift_status: s.drift_status,
6378 last_drift_check_time: s.last_drift_check_time,
6379 status_reason: s.status_reason,
6380 description: s.description,
6381 detailed_status: String::new(),
6382 root_stack: String::new(),
6383 parent_stack: String::new(),
6384 termination_protection: false,
6385 iam_role: String::new(),
6386 tags: Vec::new(),
6387 stack_policy: String::new(),
6388 rollback_monitoring_time: String::new(),
6389 rollback_alarms: Vec::new(),
6390 notification_arns: Vec::new(),
6391 })
6392 .collect();
6393
6394 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
6396
6397 self.cfn_state.table.items = stacks;
6398
6399 Ok(())
6400 }
6401
6402 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
6403 let attached_policies = self
6405 .iam_client
6406 .list_attached_role_policies(role_name)
6407 .await
6408 .map_err(|e| anyhow::anyhow!(e))?;
6409
6410 let mut policies: Vec<crate::iam::Policy> = attached_policies
6411 .into_iter()
6412 .map(|p| crate::iam::Policy {
6413 policy_name: p.policy_name().unwrap_or("").to_string(),
6414 policy_type: "Managed".to_string(),
6415 attached_via: "Direct".to_string(),
6416 attached_entities: "-".to_string(),
6417 description: "-".to_string(),
6418 creation_time: "-".to_string(),
6419 edited_time: "-".to_string(),
6420 policy_arn: p.policy_arn().map(|s| s.to_string()),
6421 })
6422 .collect();
6423
6424 let inline_policy_names = self
6426 .iam_client
6427 .list_role_policies(role_name)
6428 .await
6429 .map_err(|e| anyhow::anyhow!(e))?;
6430
6431 for policy_name in inline_policy_names {
6432 policies.push(crate::iam::Policy {
6433 policy_name,
6434 policy_type: "Inline".to_string(),
6435 attached_via: "Direct".to_string(),
6436 attached_entities: "-".to_string(),
6437 description: "-".to_string(),
6438 creation_time: "-".to_string(),
6439 edited_time: "-".to_string(),
6440 policy_arn: None,
6441 });
6442 }
6443
6444 self.iam_state.policies.items = policies;
6445
6446 Ok(())
6447 }
6448
6449 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
6450 let attached_policies = self
6451 .iam_client
6452 .list_attached_group_policies(group_name)
6453 .await
6454 .map_err(|e| anyhow::anyhow!(e))?;
6455
6456 let mut policies: Vec<crate::iam::Policy> = attached_policies
6457 .into_iter()
6458 .map(|p| crate::iam::Policy {
6459 policy_name: p.policy_name().unwrap_or("").to_string(),
6460 policy_type: "AWS managed".to_string(),
6461 attached_via: "Direct".to_string(),
6462 attached_entities: "-".to_string(),
6463 description: "-".to_string(),
6464 creation_time: "-".to_string(),
6465 edited_time: "-".to_string(),
6466 policy_arn: p.policy_arn().map(|s| s.to_string()),
6467 })
6468 .collect();
6469
6470 let inline_policy_names = self
6471 .iam_client
6472 .list_group_policies(group_name)
6473 .await
6474 .map_err(|e| anyhow::anyhow!(e))?;
6475
6476 for policy_name in inline_policy_names {
6477 policies.push(crate::iam::Policy {
6478 policy_name,
6479 policy_type: "Inline".to_string(),
6480 attached_via: "Direct".to_string(),
6481 attached_entities: "-".to_string(),
6482 description: "-".to_string(),
6483 creation_time: "-".to_string(),
6484 edited_time: "-".to_string(),
6485 policy_arn: None,
6486 });
6487 }
6488
6489 self.iam_state.policies.items = policies;
6490
6491 Ok(())
6492 }
6493
6494 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
6495 let users = self
6496 .iam_client
6497 .get_group_users(group_name)
6498 .await
6499 .map_err(|e| anyhow::anyhow!(e))?;
6500
6501 let group_users: Vec<crate::iam::GroupUser> = users
6502 .into_iter()
6503 .map(|u| {
6504 let creation_time = {
6505 let dt = u.create_date();
6506 let timestamp = dt.secs();
6507 let datetime =
6508 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
6509 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
6510 };
6511
6512 crate::iam::GroupUser {
6513 user_name: u.user_name().to_string(),
6514 groups: String::new(),
6515 last_activity: String::new(),
6516 creation_time,
6517 }
6518 })
6519 .collect();
6520
6521 self.iam_state.group_users.items = group_users;
6522
6523 Ok(())
6524 }
6525
6526 pub async fn load_policy_document(
6527 &mut self,
6528 role_name: &str,
6529 policy_name: &str,
6530 ) -> anyhow::Result<()> {
6531 let policy = self
6533 .iam_state
6534 .policies
6535 .items
6536 .iter()
6537 .find(|p| p.policy_name == policy_name)
6538 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
6539
6540 let document = if let Some(policy_arn) = &policy.policy_arn {
6541 self.iam_client
6543 .get_policy_version(policy_arn)
6544 .await
6545 .map_err(|e| anyhow::anyhow!(e))?
6546 } else {
6547 self.iam_client
6549 .get_role_policy(role_name, policy_name)
6550 .await
6551 .map_err(|e| anyhow::anyhow!(e))?
6552 };
6553
6554 self.iam_state.policy_document = document;
6555
6556 Ok(())
6557 }
6558
6559 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
6560 let document = self
6561 .iam_client
6562 .get_role(role_name)
6563 .await
6564 .map_err(|e| anyhow::anyhow!(e))?;
6565
6566 self.iam_state.trust_policy_document = document;
6567
6568 Ok(())
6569 }
6570
6571 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
6572 self.iam_state.last_accessed_services.items = vec![];
6574 self.iam_state.last_accessed_services.selected = 0;
6575
6576 Ok(())
6577 }
6578
6579 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
6580 let tags = self
6581 .iam_client
6582 .list_role_tags(role_name)
6583 .await
6584 .map_err(|e| anyhow::anyhow!(e))?;
6585 self.iam_state.tags.items = tags
6586 .into_iter()
6587 .map(|(k, v)| crate::iam::RoleTag { key: k, value: v })
6588 .collect();
6589 self.iam_state.tags.reset();
6590 Ok(())
6591 }
6592
6593 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
6594 let tags = self
6595 .iam_client
6596 .list_user_tags(user_name)
6597 .await
6598 .map_err(|e| anyhow::anyhow!(e))?;
6599 self.iam_state.user_tags.items = tags
6600 .into_iter()
6601 .map(|(k, v)| crate::iam::UserTag { key: k, value: v })
6602 .collect();
6603 self.iam_state.user_tags.reset();
6604 Ok(())
6605 }
6606
6607 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
6608 if let Some(group) = self
6609 .log_groups_state
6610 .log_groups
6611 .items
6612 .get(self.log_groups_state.log_groups.selected)
6613 {
6614 self.log_groups_state.log_streams =
6615 self.cloudwatch_client.list_log_streams(&group.name).await?;
6616 self.log_groups_state.selected_stream = 0;
6617 }
6618 Ok(())
6619 }
6620
6621 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
6622 if let Some(group) = self
6623 .log_groups_state
6624 .log_groups
6625 .items
6626 .get(self.log_groups_state.log_groups.selected)
6627 {
6628 if let Some(stream) = self
6629 .log_groups_state
6630 .log_streams
6631 .get(self.log_groups_state.selected_stream)
6632 {
6633 let (start_time, end_time) =
6635 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
6636 let now = chrono::Utc::now().timestamp_millis();
6637 let duration_ms = match self.log_groups_state.relative_unit {
6638 crate::app::TimeUnit::Minutes => amount * 60 * 1000,
6639 crate::app::TimeUnit::Hours => amount * 60 * 60 * 1000,
6640 crate::app::TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6641 crate::app::TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6642 };
6643 (Some(now - duration_ms), Some(now))
6644 } else {
6645 (None, None)
6646 };
6647
6648 let (mut events, has_more, token) = self
6649 .cloudwatch_client
6650 .get_log_events(
6651 &group.name,
6652 &stream.name,
6653 self.log_groups_state.next_backward_token.clone(),
6654 start_time,
6655 end_time,
6656 )
6657 .await?;
6658
6659 if self.log_groups_state.next_backward_token.is_some() {
6660 events.append(&mut self.log_groups_state.log_events);
6662 self.log_groups_state.event_scroll_offset = 0;
6663 } else {
6664 self.log_groups_state.event_scroll_offset = 0;
6666 }
6667
6668 self.log_groups_state.log_events = events;
6669 self.log_groups_state.has_older_events =
6670 has_more && self.log_groups_state.log_events.len() >= 25;
6671 self.log_groups_state.next_backward_token = token;
6672 self.log_groups_state.selected_event = 0;
6673 }
6674 }
6675 Ok(())
6676 }
6677
6678 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
6679 if self.insights_state.insights.selected_log_groups.is_empty() {
6680 return Err(anyhow::anyhow!(
6681 "No log groups selected. Please select at least one log group."
6682 ));
6683 }
6684
6685 let now = chrono::Utc::now().timestamp_millis();
6686 let amount = self
6687 .insights_state
6688 .insights
6689 .insights_relative_amount
6690 .parse::<i64>()
6691 .unwrap_or(1);
6692 let duration_ms = match self.insights_state.insights.insights_relative_unit {
6693 TimeUnit::Minutes => amount * 60 * 1000,
6694 TimeUnit::Hours => amount * 60 * 60 * 1000,
6695 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6696 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6697 };
6698 let start_time = now - duration_ms;
6699
6700 let query_id = self
6701 .cloudwatch_client
6702 .start_query(
6703 self.insights_state.insights.selected_log_groups.clone(),
6704 self.insights_state.insights.query_text.trim().to_string(),
6705 start_time,
6706 now,
6707 )
6708 .await?;
6709
6710 for _ in 0..60 {
6712 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
6713 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
6714
6715 if status == "Complete" {
6716 self.insights_state.insights.query_results = results;
6717 self.insights_state.insights.query_completed = true;
6718 self.insights_state.insights.results_selected = 0;
6719 self.insights_state.insights.expanded_result = None;
6720 self.view_mode = ViewMode::InsightsResults;
6721 return Ok(());
6722 } else if status == "Failed" || status == "Cancelled" {
6723 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
6724 }
6725 }
6726
6727 Err(anyhow::anyhow!("Query timeout"))
6728 }
6729}
6730
6731impl CloudWatchInsightsState {
6732 fn new() -> Self {
6733 Self {
6734 insights: InsightsState::default(),
6735 loading: false,
6736 }
6737 }
6738}
6739
6740impl CloudWatchAlarmsState {
6741 fn new() -> Self {
6742 Self {
6743 table: TableState::new(),
6744 alarm_tab: AlarmTab::AllAlarms,
6745 view_as: AlarmViewMode::Table,
6746 wrap_lines: false,
6747 sort_column: "Last state update".to_string(),
6748 sort_direction: SortDirection::Asc,
6749 input_focus: InputFocus::Filter,
6750 }
6751 }
6752}
6753
6754impl ServicePickerState {
6755 fn new() -> Self {
6756 Self {
6757 filter: String::new(),
6758 selected: 0,
6759 services: vec![
6760 "CloudWatch > Log Groups",
6761 "CloudWatch > Logs Insights",
6762 "CloudWatch > Alarms",
6763 "CloudFormation > Stacks",
6764 "ECR > Repositories",
6765 "IAM > Users",
6766 "IAM > Roles",
6767 "IAM > User Groups",
6768 "Lambda > Functions",
6769 "Lambda > Applications",
6770 "S3 > Buckets",
6771 "SQS > Queues",
6772 ],
6773 }
6774 }
6775}
6776
6777#[cfg(test)]
6778mod test_helpers {
6779 use super::*;
6780
6781 #[allow(dead_code)]
6783 pub fn test_app() -> App {
6784 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
6785 }
6786
6787 #[allow(dead_code)]
6788 pub fn test_app_no_region() -> App {
6789 App::new_without_client("test".to_string(), None)
6790 }
6791
6792 #[allow(dead_code)]
6793 pub fn test_tab(service: Service) -> Tab {
6794 Tab {
6795 service,
6796 title: service.name().to_string(),
6797 breadcrumb: service.name().to_string(),
6798 }
6799 }
6800
6801 #[allow(dead_code)]
6802 pub fn test_iam_role(name: &str) -> crate::iam::IamRole {
6803 crate::iam::IamRole {
6804 role_name: name.to_string(),
6805 path: "/".to_string(),
6806 description: format!("Test role {}", name),
6807 trusted_entities: "AWS Service: ec2.amazonaws.com".to_string(),
6808 creation_time: "2024-01-01 00:00:00".to_string(),
6809 arn: format!("arn:aws:iam::123456789012:role/{}", name),
6810 max_session_duration: Some(3600),
6811 last_activity: "-".to_string(),
6812 }
6813 }
6814}
6815
6816#[cfg(test)]
6817mod tests {
6818 use super::*;
6819 use crate::keymap::Action;
6820 use test_helpers::*;
6821
6822 #[test]
6823 fn test_next_tab_cycles_forward() {
6824 let mut app = test_app();
6825 app.tabs = vec![
6826 Tab {
6827 service: Service::CloudWatchLogGroups,
6828 title: "CloudWatch > Log Groups".to_string(),
6829 breadcrumb: "CloudWatch > Log Groups".to_string(),
6830 },
6831 Tab {
6832 service: Service::CloudWatchInsights,
6833 title: "CloudWatch > Logs Insights".to_string(),
6834 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6835 },
6836 Tab {
6837 service: Service::CloudWatchAlarms,
6838 title: "CloudWatch > Alarms".to_string(),
6839 breadcrumb: "CloudWatch > Alarms".to_string(),
6840 },
6841 ];
6842 app.current_tab = 0;
6843
6844 app.handle_action(Action::NextTab);
6845 assert_eq!(app.current_tab, 1);
6846 assert_eq!(app.current_service, Service::CloudWatchInsights);
6847
6848 app.handle_action(Action::NextTab);
6849 assert_eq!(app.current_tab, 2);
6850 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6851
6852 app.handle_action(Action::NextTab);
6854 assert_eq!(app.current_tab, 0);
6855 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6856 }
6857
6858 #[test]
6859 fn test_prev_tab_cycles_backward() {
6860 let mut app = test_app();
6861 app.tabs = vec![
6862 Tab {
6863 service: Service::CloudWatchLogGroups,
6864 title: "CloudWatch > Log Groups".to_string(),
6865 breadcrumb: "CloudWatch > Log Groups".to_string(),
6866 },
6867 Tab {
6868 service: Service::CloudWatchInsights,
6869 title: "CloudWatch > Logs Insights".to_string(),
6870 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6871 },
6872 Tab {
6873 service: Service::CloudWatchAlarms,
6874 title: "CloudWatch > Alarms".to_string(),
6875 breadcrumb: "CloudWatch > Alarms".to_string(),
6876 },
6877 ];
6878 app.current_tab = 2;
6879
6880 app.handle_action(Action::PrevTab);
6881 assert_eq!(app.current_tab, 1);
6882 assert_eq!(app.current_service, Service::CloudWatchInsights);
6883
6884 app.handle_action(Action::PrevTab);
6885 assert_eq!(app.current_tab, 0);
6886 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6887
6888 app.handle_action(Action::PrevTab);
6890 assert_eq!(app.current_tab, 2);
6891 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6892 }
6893
6894 #[test]
6895 fn test_close_tab_removes_current() {
6896 let mut app = test_app();
6897 app.tabs = vec![
6898 Tab {
6899 service: Service::CloudWatchLogGroups,
6900 title: "CloudWatch > Log Groups".to_string(),
6901 breadcrumb: "CloudWatch > Log Groups".to_string(),
6902 },
6903 Tab {
6904 service: Service::CloudWatchInsights,
6905 title: "CloudWatch > Logs Insights".to_string(),
6906 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6907 },
6908 Tab {
6909 service: Service::CloudWatchAlarms,
6910 title: "CloudWatch > Alarms".to_string(),
6911 breadcrumb: "CloudWatch > Alarms".to_string(),
6912 },
6913 ];
6914 app.current_tab = 1;
6915 app.service_selected = true;
6916
6917 app.handle_action(Action::CloseTab);
6918 assert_eq!(app.tabs.len(), 2);
6919 assert_eq!(app.current_tab, 1);
6920 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6921 }
6922
6923 #[test]
6924 fn test_close_last_tab_exits_service() {
6925 let mut app = test_app();
6926 app.tabs = vec![Tab {
6927 service: Service::CloudWatchLogGroups,
6928 title: "CloudWatch > Log Groups".to_string(),
6929 breadcrumb: "CloudWatch > Log Groups".to_string(),
6930 }];
6931 app.current_tab = 0;
6932 app.service_selected = true;
6933
6934 app.handle_action(Action::CloseTab);
6935 assert_eq!(app.tabs.len(), 0);
6936 assert!(!app.service_selected);
6937 assert_eq!(app.current_tab, 0);
6938 }
6939
6940 #[test]
6941 fn test_close_service_removes_current_tab() {
6942 let mut app = test_app();
6943 app.tabs = vec![
6944 Tab {
6945 service: Service::CloudWatchLogGroups,
6946 title: "CloudWatch > Log Groups".to_string(),
6947 breadcrumb: "CloudWatch > Log Groups".to_string(),
6948 },
6949 Tab {
6950 service: Service::CloudWatchInsights,
6951 title: "CloudWatch > Logs Insights".to_string(),
6952 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6953 },
6954 Tab {
6955 service: Service::CloudWatchAlarms,
6956 title: "CloudWatch > Alarms".to_string(),
6957 breadcrumb: "CloudWatch > Alarms".to_string(),
6958 },
6959 ];
6960 app.current_tab = 1;
6961 app.service_selected = true;
6962
6963 app.handle_action(Action::CloseService);
6964
6965 assert_eq!(app.tabs.len(), 2);
6967 assert_eq!(app.current_tab, 1);
6969 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6970 assert!(app.service_selected);
6972 assert_eq!(app.mode, Mode::Normal);
6973 }
6974
6975 #[test]
6976 fn test_close_service_last_tab_shows_picker() {
6977 let mut app = test_app();
6978 app.tabs = vec![Tab {
6979 service: Service::CloudWatchLogGroups,
6980 title: "CloudWatch > Log Groups".to_string(),
6981 breadcrumb: "CloudWatch > Log Groups".to_string(),
6982 }];
6983 app.current_tab = 0;
6984 app.service_selected = true;
6985
6986 app.handle_action(Action::CloseService);
6987
6988 assert_eq!(app.tabs.len(), 0);
6990 assert!(!app.service_selected);
6992 assert_eq!(app.mode, Mode::ServicePicker);
6993 }
6994
6995 #[test]
6996 fn test_open_tab_picker_with_tabs() {
6997 let mut app = test_app();
6998 app.tabs = vec![
6999 Tab {
7000 service: Service::CloudWatchLogGroups,
7001 title: "CloudWatch > Log Groups".to_string(),
7002 breadcrumb: "CloudWatch > Log Groups".to_string(),
7003 },
7004 Tab {
7005 service: Service::CloudWatchInsights,
7006 title: "CloudWatch > Logs Insights".to_string(),
7007 breadcrumb: "CloudWatch > Logs Insights".to_string(),
7008 },
7009 ];
7010 app.current_tab = 1;
7011
7012 app.handle_action(Action::OpenTabPicker);
7013 assert_eq!(app.mode, Mode::TabPicker);
7014 assert_eq!(app.tab_picker_selected, 1);
7015 }
7016
7017 #[test]
7018 fn test_open_tab_picker_without_tabs() {
7019 let mut app = test_app();
7020 app.tabs = vec![];
7021
7022 app.handle_action(Action::OpenTabPicker);
7023 assert_eq!(app.mode, Mode::Normal);
7024 }
7025
7026 #[test]
7027 fn test_pending_key_state() {
7028 let mut app = test_app();
7029 assert_eq!(app.pending_key, None);
7030
7031 app.pending_key = Some('g');
7032 assert_eq!(app.pending_key, Some('g'));
7033 }
7034
7035 #[test]
7036 fn test_tab_breadcrumb_updates() {
7037 let mut app = test_app();
7038 app.tabs = vec![Tab {
7039 service: Service::CloudWatchLogGroups,
7040 title: "CloudWatch > Log Groups".to_string(),
7041 breadcrumb: "CloudWatch > Log groups".to_string(),
7042 }];
7043 app.current_tab = 0;
7044 app.service_selected = true;
7045 app.current_service = Service::CloudWatchLogGroups;
7046
7047 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
7049
7050 app.log_groups_state
7052 .log_groups
7053 .items
7054 .push(rusticity_core::LogGroup {
7055 name: "/aws/lambda/test".to_string(),
7056 creation_time: None,
7057 stored_bytes: Some(1024),
7058 retention_days: None,
7059 log_class: None,
7060 arn: None,
7061 });
7062 app.log_groups_state.log_groups.reset();
7063 app.view_mode = ViewMode::Detail;
7064 app.update_current_tab_breadcrumb();
7065
7066 assert_eq!(
7068 app.tabs[0].breadcrumb,
7069 "CloudWatch > Log groups > /aws/lambda/test"
7070 );
7071 }
7072
7073 #[test]
7074 fn test_s3_bucket_column_selector_navigation() {
7075 let mut app = test_app();
7076 app.current_service = Service::S3Buckets;
7077 app.mode = Mode::ColumnSelector;
7078 app.column_selector_index = 0;
7079
7080 app.handle_action(Action::NextItem);
7082 assert_eq!(app.column_selector_index, 1);
7083
7084 app.handle_action(Action::NextItem);
7085 assert_eq!(app.column_selector_index, 2);
7086
7087 app.handle_action(Action::NextItem);
7089 assert_eq!(app.column_selector_index, 2);
7090
7091 app.handle_action(Action::PrevItem);
7093 assert_eq!(app.column_selector_index, 1);
7094
7095 app.handle_action(Action::PrevItem);
7096 assert_eq!(app.column_selector_index, 0);
7097
7098 app.handle_action(Action::PrevItem);
7100 assert_eq!(app.column_selector_index, 0);
7101 }
7102
7103 #[test]
7104 fn test_cloudwatch_alarms_state_initialized() {
7105 let app = test_app();
7106
7107 assert_eq!(app.alarms_state.table.items.len(), 0);
7109 assert_eq!(app.alarms_state.table.selected, 0);
7110 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
7111 assert!(!app.alarms_state.table.loading);
7112 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
7113 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
7114 }
7115
7116 #[test]
7117 fn test_cloudwatch_alarms_service_selection() {
7118 let mut app = test_app();
7119
7120 app.current_service = Service::CloudWatchAlarms;
7122 app.service_selected = true;
7123
7124 assert_eq!(app.current_service, Service::CloudWatchAlarms);
7125 assert!(app.service_selected);
7126 }
7127
7128 #[test]
7129 fn test_cloudwatch_alarms_column_preferences() {
7130 let app = test_app();
7131
7132 assert!(!app.cw_alarm_column_ids.is_empty());
7134 assert!(!app.cw_alarm_visible_column_ids.is_empty());
7135
7136 assert!(app
7138 .cw_alarm_visible_column_ids
7139 .contains(&AlarmColumn::Name.id()));
7140 assert!(app
7141 .cw_alarm_visible_column_ids
7142 .contains(&AlarmColumn::State.id()));
7143 }
7144
7145 #[test]
7146 fn test_s3_bucket_navigation_without_expansion() {
7147 let mut app = test_app();
7148 app.current_service = Service::S3Buckets;
7149 app.service_selected = true;
7150 app.mode = Mode::Normal;
7151
7152 app.s3_state.buckets.items = vec![
7154 S3Bucket {
7155 name: "bucket1".to_string(),
7156 region: "us-east-1".to_string(),
7157 creation_date: "2024-01-01T00:00:00Z".to_string(),
7158 },
7159 S3Bucket {
7160 name: "bucket2".to_string(),
7161 region: "us-east-1".to_string(),
7162 creation_date: "2024-01-02T00:00:00Z".to_string(),
7163 },
7164 S3Bucket {
7165 name: "bucket3".to_string(),
7166 region: "us-east-1".to_string(),
7167 creation_date: "2024-01-03T00:00:00Z".to_string(),
7168 },
7169 ];
7170 app.s3_state.selected_row = 0;
7171
7172 app.handle_action(Action::NextItem);
7174 assert_eq!(app.s3_state.selected_row, 1);
7175
7176 app.handle_action(Action::NextItem);
7177 assert_eq!(app.s3_state.selected_row, 2);
7178
7179 app.handle_action(Action::NextItem);
7181 assert_eq!(app.s3_state.selected_row, 2);
7182
7183 app.handle_action(Action::PrevItem);
7185 assert_eq!(app.s3_state.selected_row, 1);
7186
7187 app.handle_action(Action::PrevItem);
7188 assert_eq!(app.s3_state.selected_row, 0);
7189
7190 app.handle_action(Action::PrevItem);
7192 assert_eq!(app.s3_state.selected_row, 0);
7193 }
7194
7195 #[test]
7196 fn test_s3_bucket_navigation_with_expansion() {
7197 let mut app = test_app();
7198 app.current_service = Service::S3Buckets;
7199 app.service_selected = true;
7200 app.mode = Mode::Normal;
7201
7202 app.s3_state.buckets.items = vec![
7204 S3Bucket {
7205 name: "bucket1".to_string(),
7206 region: "us-east-1".to_string(),
7207 creation_date: "2024-01-01T00:00:00Z".to_string(),
7208 },
7209 S3Bucket {
7210 name: "bucket2".to_string(),
7211 region: "us-east-1".to_string(),
7212 creation_date: "2024-01-02T00:00:00Z".to_string(),
7213 },
7214 ];
7215
7216 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
7218 app.s3_state.bucket_preview.insert(
7219 "bucket1".to_string(),
7220 vec![
7221 S3Object {
7222 key: "file1.txt".to_string(),
7223 size: 100,
7224 last_modified: "2024-01-01T00:00:00Z".to_string(),
7225 is_prefix: false,
7226 storage_class: "STANDARD".to_string(),
7227 },
7228 S3Object {
7229 key: "folder/".to_string(),
7230 size: 0,
7231 last_modified: "2024-01-01T00:00:00Z".to_string(),
7232 is_prefix: true,
7233 storage_class: String::new(),
7234 },
7235 ],
7236 );
7237
7238 app.s3_state.selected_row = 0;
7239
7240 app.handle_action(Action::NextItem);
7243 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
7246 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
7249 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
7253 assert_eq!(app.s3_state.selected_row, 3);
7254 }
7255
7256 #[test]
7257 fn test_s3_bucket_navigation_with_nested_expansion() {
7258 let mut app = test_app();
7259 app.current_service = Service::S3Buckets;
7260 app.service_selected = true;
7261 app.mode = Mode::Normal;
7262
7263 app.s3_state.buckets.items = vec![S3Bucket {
7265 name: "bucket1".to_string(),
7266 region: "us-east-1".to_string(),
7267 creation_date: "2024-01-01T00:00:00Z".to_string(),
7268 }];
7269
7270 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
7272 app.s3_state.bucket_preview.insert(
7273 "bucket1".to_string(),
7274 vec![S3Object {
7275 key: "folder/".to_string(),
7276 size: 0,
7277 last_modified: "2024-01-01T00:00:00Z".to_string(),
7278 is_prefix: true,
7279 storage_class: String::new(),
7280 }],
7281 );
7282
7283 app.s3_state.expanded_prefixes.insert("folder/".to_string());
7285 app.s3_state.prefix_preview.insert(
7286 "folder/".to_string(),
7287 vec![
7288 S3Object {
7289 key: "folder/file1.txt".to_string(),
7290 size: 100,
7291 last_modified: "2024-01-01T00:00:00Z".to_string(),
7292 is_prefix: false,
7293 storage_class: "STANDARD".to_string(),
7294 },
7295 S3Object {
7296 key: "folder/file2.txt".to_string(),
7297 size: 200,
7298 last_modified: "2024-01-01T00:00:00Z".to_string(),
7299 is_prefix: false,
7300 storage_class: "STANDARD".to_string(),
7301 },
7302 ],
7303 );
7304
7305 app.s3_state.selected_row = 0;
7306
7307 app.handle_action(Action::NextItem);
7309 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
7312 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
7315 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
7319 assert_eq!(app.s3_state.selected_row, 3);
7320 }
7321
7322 #[test]
7323 fn test_calculate_total_bucket_rows() {
7324 let mut app = test_app();
7325
7326 assert_eq!(app.calculate_total_bucket_rows(), 0);
7328
7329 app.s3_state.buckets.items = vec![
7331 S3Bucket {
7332 name: "bucket1".to_string(),
7333 region: "us-east-1".to_string(),
7334 creation_date: "2024-01-01T00:00:00Z".to_string(),
7335 },
7336 S3Bucket {
7337 name: "bucket2".to_string(),
7338 region: "us-east-1".to_string(),
7339 creation_date: "2024-01-02T00:00:00Z".to_string(),
7340 },
7341 ];
7342 assert_eq!(app.calculate_total_bucket_rows(), 2);
7343
7344 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
7346 app.s3_state.bucket_preview.insert(
7347 "bucket1".to_string(),
7348 vec![
7349 S3Object {
7350 key: "file1.txt".to_string(),
7351 size: 100,
7352 last_modified: "2024-01-01T00:00:00Z".to_string(),
7353 is_prefix: false,
7354 storage_class: "STANDARD".to_string(),
7355 },
7356 S3Object {
7357 key: "file2.txt".to_string(),
7358 size: 200,
7359 last_modified: "2024-01-01T00:00:00Z".to_string(),
7360 is_prefix: false,
7361 storage_class: "STANDARD".to_string(),
7362 },
7363 S3Object {
7364 key: "folder/".to_string(),
7365 size: 0,
7366 last_modified: "2024-01-01T00:00:00Z".to_string(),
7367 is_prefix: true,
7368 storage_class: String::new(),
7369 },
7370 ],
7371 );
7372 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
7376 app.s3_state.prefix_preview.insert(
7377 "folder/".to_string(),
7378 vec![
7379 S3Object {
7380 key: "folder/nested1.txt".to_string(),
7381 size: 50,
7382 last_modified: "2024-01-01T00:00:00Z".to_string(),
7383 is_prefix: false,
7384 storage_class: "STANDARD".to_string(),
7385 },
7386 S3Object {
7387 key: "folder/nested2.txt".to_string(),
7388 size: 75,
7389 last_modified: "2024-01-01T00:00:00Z".to_string(),
7390 is_prefix: false,
7391 storage_class: "STANDARD".to_string(),
7392 },
7393 ],
7394 );
7395 assert_eq!(app.calculate_total_bucket_rows(), 7); }
7397
7398 #[test]
7399 fn test_calculate_total_object_rows() {
7400 let mut app = test_app();
7401 app.s3_state.current_bucket = Some("test-bucket".to_string());
7402
7403 assert_eq!(app.calculate_total_object_rows(), 0);
7405
7406 app.s3_state.objects = vec![
7408 S3Object {
7409 key: "file1.txt".to_string(),
7410 size: 100,
7411 last_modified: "2024-01-01T00:00:00Z".to_string(),
7412 is_prefix: false,
7413 storage_class: "STANDARD".to_string(),
7414 },
7415 S3Object {
7416 key: "folder/".to_string(),
7417 size: 0,
7418 last_modified: "2024-01-01T00:00:00Z".to_string(),
7419 is_prefix: true,
7420 storage_class: String::new(),
7421 },
7422 ];
7423 assert_eq!(app.calculate_total_object_rows(), 2);
7424
7425 app.s3_state.expanded_prefixes.insert("folder/".to_string());
7427 app.s3_state.prefix_preview.insert(
7428 "folder/".to_string(),
7429 vec![
7430 S3Object {
7431 key: "folder/file2.txt".to_string(),
7432 size: 200,
7433 last_modified: "2024-01-01T00:00:00Z".to_string(),
7434 is_prefix: false,
7435 storage_class: "STANDARD".to_string(),
7436 },
7437 S3Object {
7438 key: "folder/subfolder/".to_string(),
7439 size: 0,
7440 last_modified: "2024-01-01T00:00:00Z".to_string(),
7441 is_prefix: true,
7442 storage_class: String::new(),
7443 },
7444 ],
7445 );
7446 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
7450 .expanded_prefixes
7451 .insert("folder/subfolder/".to_string());
7452 app.s3_state.prefix_preview.insert(
7453 "folder/subfolder/".to_string(),
7454 vec![S3Object {
7455 key: "folder/subfolder/deep.txt".to_string(),
7456 size: 50,
7457 last_modified: "2024-01-01T00:00:00Z".to_string(),
7458 is_prefix: false,
7459 storage_class: "STANDARD".to_string(),
7460 }],
7461 );
7462 assert_eq!(app.calculate_total_object_rows(), 5); }
7464
7465 #[test]
7466 fn test_s3_object_navigation_with_deep_nesting() {
7467 let mut app = test_app();
7468 app.current_service = Service::S3Buckets;
7469 app.service_selected = true;
7470 app.mode = Mode::Normal;
7471 app.s3_state.current_bucket = Some("test-bucket".to_string());
7472
7473 app.s3_state.objects = vec![S3Object {
7475 key: "folder1/".to_string(),
7476 size: 0,
7477 last_modified: "2024-01-01T00:00:00Z".to_string(),
7478 is_prefix: true,
7479 storage_class: String::new(),
7480 }];
7481
7482 app.s3_state
7484 .expanded_prefixes
7485 .insert("folder1/".to_string());
7486 app.s3_state.prefix_preview.insert(
7487 "folder1/".to_string(),
7488 vec![S3Object {
7489 key: "folder1/folder2/".to_string(),
7490 size: 0,
7491 last_modified: "2024-01-01T00:00:00Z".to_string(),
7492 is_prefix: true,
7493 storage_class: String::new(),
7494 }],
7495 );
7496
7497 app.s3_state
7499 .expanded_prefixes
7500 .insert("folder1/folder2/".to_string());
7501 app.s3_state.prefix_preview.insert(
7502 "folder1/folder2/".to_string(),
7503 vec![S3Object {
7504 key: "folder1/folder2/file.txt".to_string(),
7505 size: 100,
7506 last_modified: "2024-01-01T00:00:00Z".to_string(),
7507 is_prefix: false,
7508 storage_class: "STANDARD".to_string(),
7509 }],
7510 );
7511
7512 app.s3_state.selected_object = 0;
7513
7514 app.handle_action(Action::NextItem);
7516 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
7519 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
7523 assert_eq!(app.s3_state.selected_object, 2);
7524 }
7525
7526 #[test]
7527 fn test_s3_expand_nested_folder_in_objects_view() {
7528 let mut app = test_app();
7529 app.current_service = Service::S3Buckets;
7530 app.service_selected = true;
7531 app.mode = Mode::Normal;
7532 app.s3_state.current_bucket = Some("test-bucket".to_string());
7533
7534 app.s3_state.objects = vec![S3Object {
7536 key: "parent/".to_string(),
7537 size: 0,
7538 last_modified: "2024-01-01T00:00:00Z".to_string(),
7539 is_prefix: true,
7540 storage_class: String::new(),
7541 }];
7542
7543 app.s3_state.expanded_prefixes.insert("parent/".to_string());
7545 app.s3_state.prefix_preview.insert(
7546 "parent/".to_string(),
7547 vec![S3Object {
7548 key: "parent/child/".to_string(),
7549 size: 0,
7550 last_modified: "2024-01-01T00:00:00Z".to_string(),
7551 is_prefix: true,
7552 storage_class: String::new(),
7553 }],
7554 );
7555
7556 app.s3_state.selected_object = 1;
7558
7559 app.handle_action(Action::NextPane);
7561
7562 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
7564 assert!(app.s3_state.buckets.loading); }
7566
7567 #[test]
7568 fn test_s3_drill_into_nested_folder() {
7569 let mut app = test_app();
7570 app.current_service = Service::S3Buckets;
7571 app.service_selected = true;
7572 app.mode = Mode::Normal;
7573 app.s3_state.current_bucket = Some("test-bucket".to_string());
7574
7575 app.s3_state.objects = vec![S3Object {
7577 key: "parent/".to_string(),
7578 size: 0,
7579 last_modified: "2024-01-01T00:00:00Z".to_string(),
7580 is_prefix: true,
7581 storage_class: String::new(),
7582 }];
7583
7584 app.s3_state.expanded_prefixes.insert("parent/".to_string());
7586 app.s3_state.prefix_preview.insert(
7587 "parent/".to_string(),
7588 vec![S3Object {
7589 key: "parent/child/".to_string(),
7590 size: 0,
7591 last_modified: "2024-01-01T00:00:00Z".to_string(),
7592 is_prefix: true,
7593 storage_class: String::new(),
7594 }],
7595 );
7596
7597 app.s3_state.selected_object = 1;
7599
7600 app.handle_action(Action::Select);
7602
7603 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
7605 assert!(app.s3_state.buckets.loading); }
7607
7608 #[test]
7609 fn test_s3_esc_pops_navigation_stack() {
7610 let mut app = test_app();
7611 app.current_service = Service::S3Buckets;
7612 app.s3_state.current_bucket = Some("test-bucket".to_string());
7613 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
7614
7615 app.handle_action(Action::GoBack);
7617 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
7618 assert!(app.s3_state.buckets.loading);
7619
7620 app.s3_state.buckets.loading = false;
7622 app.handle_action(Action::GoBack);
7623 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7624 assert!(app.s3_state.buckets.loading);
7625
7626 app.s3_state.buckets.loading = false;
7628 app.handle_action(Action::GoBack);
7629 assert_eq!(app.s3_state.current_bucket, None);
7630 }
7631
7632 #[test]
7633 fn test_s3_esc_from_bucket_root_exits() {
7634 let mut app = test_app();
7635 app.current_service = Service::S3Buckets;
7636 app.s3_state.current_bucket = Some("test-bucket".to_string());
7637 app.s3_state.prefix_stack = vec![];
7638
7639 app.handle_action(Action::GoBack);
7641 assert_eq!(app.s3_state.current_bucket, None);
7642 assert_eq!(app.s3_state.objects.len(), 0);
7643 }
7644
7645 #[test]
7646 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
7647 let mut app = test_app();
7648 app.current_service = Service::S3Buckets;
7649 app.service_selected = true;
7650 app.mode = Mode::Normal;
7651
7652 app.s3_state.buckets.items = vec![S3Bucket {
7654 name: "test-bucket".to_string(),
7655 region: "us-east-1".to_string(),
7656 creation_date: "2024-01-01".to_string(),
7657 }];
7658
7659 app.s3_state
7661 .expanded_prefixes
7662 .insert("test-bucket".to_string());
7663 app.s3_state.bucket_preview.insert(
7664 "test-bucket".to_string(),
7665 vec![S3Object {
7666 key: "parent/".to_string(),
7667 size: 0,
7668 last_modified: "2024-01-01".to_string(),
7669 is_prefix: true,
7670 storage_class: String::new(),
7671 }],
7672 );
7673
7674 app.s3_state.expanded_prefixes.insert("parent/".to_string());
7676 app.s3_state.prefix_preview.insert(
7677 "parent/".to_string(),
7678 vec![S3Object {
7679 key: "parent/child/".to_string(),
7680 size: 0,
7681 last_modified: "2024-01-01".to_string(),
7682 is_prefix: true,
7683 storage_class: String::new(),
7684 }],
7685 );
7686
7687 app.s3_state.selected_row = 2;
7689
7690 app.handle_action(Action::Select);
7692
7693 assert_eq!(
7695 app.s3_state.prefix_stack,
7696 vec!["parent/".to_string(), "parent/child/".to_string()]
7697 );
7698 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
7699 assert!(app.s3_state.buckets.loading);
7700
7701 app.s3_state.buckets.loading = false;
7703 app.handle_action(Action::GoBack);
7704 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
7705 assert!(app.s3_state.buckets.loading);
7706
7707 app.s3_state.buckets.loading = false;
7709 app.handle_action(Action::GoBack);
7710 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7711 assert!(app.s3_state.buckets.loading);
7712
7713 app.s3_state.buckets.loading = false;
7715 app.handle_action(Action::GoBack);
7716 assert_eq!(app.s3_state.current_bucket, None);
7717 }
7718
7719 #[test]
7720 fn test_region_picker_fuzzy_filter() {
7721 let mut app = test_app();
7722 app.region_latencies.insert("us-east-1".to_string(), 10);
7723 app.region_filter = "vir".to_string();
7724 let filtered = app.get_filtered_regions();
7725 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
7726 }
7727
7728 #[test]
7729 fn test_profile_picker_loads_profiles() {
7730 let profiles = App::load_aws_profiles();
7731 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
7733 }
7734
7735 #[test]
7736 fn test_profile_with_region_uses_it() {
7737 let mut app = test_app_no_region();
7738 app.available_profiles = vec![AwsProfile {
7739 name: "test-profile".to_string(),
7740 region: Some("eu-west-1".to_string()),
7741 account: Some("123456789".to_string()),
7742 role_arn: None,
7743 source_profile: None,
7744 }];
7745 app.profile_picker_selected = 0;
7746 app.mode = Mode::ProfilePicker;
7747
7748 let filtered = app.get_filtered_profiles();
7750 if let Some(profile) = filtered.first() {
7751 let profile_name = profile.name.clone();
7752 let profile_region = profile.region.clone();
7753
7754 app.profile = profile_name;
7755 if let Some(region) = profile_region {
7756 app.region = region;
7757 }
7758 }
7759
7760 assert_eq!(app.profile, "test-profile");
7761 assert_eq!(app.region, "eu-west-1");
7762 }
7763
7764 #[test]
7765 fn test_profile_without_region_keeps_unknown() {
7766 let mut app = test_app_no_region();
7767 let initial_region = app.region.clone();
7768
7769 app.available_profiles = vec![AwsProfile {
7770 name: "test-profile".to_string(),
7771 region: None,
7772 account: None,
7773 role_arn: None,
7774 source_profile: None,
7775 }];
7776 app.profile_picker_selected = 0;
7777 app.mode = Mode::ProfilePicker;
7778
7779 let filtered = app.get_filtered_profiles();
7780 if let Some(profile) = filtered.first() {
7781 let profile_name = profile.name.clone();
7782 let profile_region = profile.region.clone();
7783
7784 app.profile = profile_name;
7785 if let Some(region) = profile_region {
7786 app.region = region;
7787 }
7788 }
7789
7790 assert_eq!(app.profile, "test-profile");
7791 assert_eq!(app.region, initial_region); }
7793
7794 #[test]
7795 fn test_region_selection_closes_all_tabs() {
7796 let mut app = test_app();
7797
7798 app.tabs.push(Tab {
7800 service: Service::CloudWatchLogGroups,
7801 title: "CloudWatch".to_string(),
7802 breadcrumb: "CloudWatch".to_string(),
7803 });
7804 app.tabs.push(Tab {
7805 service: Service::S3Buckets,
7806 title: "S3".to_string(),
7807 breadcrumb: "S3".to_string(),
7808 });
7809 app.service_selected = true;
7810 app.current_tab = 1;
7811
7812 app.region_latencies.insert("eu-west-1".to_string(), 50);
7814
7815 app.mode = Mode::RegionPicker;
7817 app.region_picker_selected = 0;
7818
7819 let filtered = app.get_filtered_regions();
7820 if let Some(region) = filtered.first() {
7821 app.region = region.code.to_string();
7822 app.tabs.clear();
7823 app.current_tab = 0;
7824 app.service_selected = false;
7825 app.mode = Mode::Normal;
7826 }
7827
7828 assert_eq!(app.tabs.len(), 0);
7829 assert_eq!(app.current_tab, 0);
7830 assert!(!app.service_selected);
7831 assert_eq!(app.region, "eu-west-1");
7832 }
7833
7834 #[test]
7835 fn test_region_picker_can_be_closed_without_selection() {
7836 let mut app = test_app();
7837 let initial_region = app.region.clone();
7838
7839 app.mode = Mode::RegionPicker;
7840
7841 app.mode = Mode::Normal;
7843
7844 assert_eq!(app.region, initial_region);
7846 }
7847
7848 #[test]
7849 fn test_session_filter_works() {
7850 let mut app = test_app();
7851
7852 app.sessions = vec![
7853 Session {
7854 id: "1".to_string(),
7855 timestamp: "2024-01-01".to_string(),
7856 profile: "prod-profile".to_string(),
7857 region: "us-east-1".to_string(),
7858 account_id: "123456789".to_string(),
7859 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7860 tabs: vec![],
7861 },
7862 Session {
7863 id: "2".to_string(),
7864 timestamp: "2024-01-02".to_string(),
7865 profile: "dev-profile".to_string(),
7866 region: "eu-west-1".to_string(),
7867 account_id: "987654321".to_string(),
7868 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
7869 tabs: vec![],
7870 },
7871 ];
7872
7873 app.session_filter = "prod".to_string();
7875 let filtered = app.get_filtered_sessions();
7876 assert_eq!(filtered.len(), 1);
7877 assert_eq!(filtered[0].profile, "prod-profile");
7878
7879 app.session_filter = "eu".to_string();
7881 let filtered = app.get_filtered_sessions();
7882 assert_eq!(filtered.len(), 1);
7883 assert_eq!(filtered[0].region, "eu-west-1");
7884
7885 app.session_filter.clear();
7887 let filtered = app.get_filtered_sessions();
7888 assert_eq!(filtered.len(), 2);
7889 }
7890
7891 #[test]
7892 fn test_profile_picker_shows_account() {
7893 let mut app = test_app_no_region();
7894 app.available_profiles = vec![AwsProfile {
7895 name: "test-profile".to_string(),
7896 region: Some("us-east-1".to_string()),
7897 account: Some("123456789".to_string()),
7898 role_arn: None,
7899 source_profile: None,
7900 }];
7901
7902 let filtered = app.get_filtered_profiles();
7903 assert_eq!(filtered.len(), 1);
7904 assert_eq!(filtered[0].account, Some("123456789".to_string()));
7905 }
7906
7907 #[test]
7908 fn test_profile_without_account() {
7909 let mut app = test_app_no_region();
7910 app.available_profiles = vec![AwsProfile {
7911 name: "test-profile".to_string(),
7912 region: Some("us-east-1".to_string()),
7913 account: None,
7914 role_arn: None,
7915 source_profile: None,
7916 }];
7917
7918 let filtered = app.get_filtered_profiles();
7919 assert_eq!(filtered.len(), 1);
7920 assert_eq!(filtered[0].account, None);
7921 }
7922
7923 #[test]
7924 fn test_profile_with_all_fields() {
7925 let mut app = test_app_no_region();
7926 app.available_profiles = vec![AwsProfile {
7927 name: "prod-profile".to_string(),
7928 region: Some("us-west-2".to_string()),
7929 account: Some("123456789".to_string()),
7930 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
7931 source_profile: Some("base-profile".to_string()),
7932 }];
7933
7934 let filtered = app.get_filtered_profiles();
7935 assert_eq!(filtered.len(), 1);
7936 assert_eq!(filtered[0].name, "prod-profile");
7937 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
7938 assert_eq!(filtered[0].account, Some("123456789".to_string()));
7939 assert_eq!(
7940 filtered[0].role_arn,
7941 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
7942 );
7943 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
7944 }
7945
7946 #[test]
7947 fn test_profile_filter_by_source_profile() {
7948 let mut app = test_app_no_region();
7949 app.available_profiles = vec![
7950 AwsProfile {
7951 name: "profile1".to_string(),
7952 region: None,
7953 account: None,
7954 role_arn: None,
7955 source_profile: Some("base".to_string()),
7956 },
7957 AwsProfile {
7958 name: "profile2".to_string(),
7959 region: None,
7960 account: None,
7961 role_arn: None,
7962 source_profile: Some("other".to_string()),
7963 },
7964 ];
7965
7966 app.profile_filter = "base".to_string();
7967 let filtered = app.get_filtered_profiles();
7968 assert_eq!(filtered.len(), 1);
7969 assert_eq!(filtered[0].name, "profile1");
7970 }
7971
7972 #[test]
7973 fn test_profile_filter_by_role() {
7974 let mut app = test_app_no_region();
7975 app.available_profiles = vec![
7976 AwsProfile {
7977 name: "admin-profile".to_string(),
7978 region: None,
7979 account: None,
7980 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
7981 source_profile: None,
7982 },
7983 AwsProfile {
7984 name: "dev-profile".to_string(),
7985 region: None,
7986 account: None,
7987 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
7988 source_profile: None,
7989 },
7990 ];
7991
7992 app.profile_filter = "Admin".to_string();
7993 let filtered = app.get_filtered_profiles();
7994 assert_eq!(filtered.len(), 1);
7995 assert_eq!(filtered[0].name, "admin-profile");
7996 }
7997
7998 #[test]
7999 fn test_profiles_sorted_by_name() {
8000 let mut app = test_app_no_region();
8001 app.available_profiles = vec![
8002 AwsProfile {
8003 name: "zebra-profile".to_string(),
8004 region: None,
8005 account: None,
8006 role_arn: None,
8007 source_profile: None,
8008 },
8009 AwsProfile {
8010 name: "alpha-profile".to_string(),
8011 region: None,
8012 account: None,
8013 role_arn: None,
8014 source_profile: None,
8015 },
8016 AwsProfile {
8017 name: "beta-profile".to_string(),
8018 region: None,
8019 account: None,
8020 role_arn: None,
8021 source_profile: None,
8022 },
8023 ];
8024
8025 let filtered = app.get_filtered_profiles();
8026 assert_eq!(filtered.len(), 3);
8027 assert_eq!(filtered[0].name, "alpha-profile");
8028 assert_eq!(filtered[1].name, "beta-profile");
8029 assert_eq!(filtered[2].name, "zebra-profile");
8030 }
8031
8032 #[test]
8033 fn test_profile_with_role_arn() {
8034 let mut app = test_app_no_region();
8035 app.available_profiles = vec![AwsProfile {
8036 name: "role-profile".to_string(),
8037 region: Some("us-east-1".to_string()),
8038 account: Some("123456789".to_string()),
8039 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
8040 source_profile: None,
8041 }];
8042
8043 let filtered = app.get_filtered_profiles();
8044 assert_eq!(filtered.len(), 1);
8045 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
8046 }
8047
8048 #[test]
8049 fn test_profile_with_user_arn() {
8050 let mut app = test_app_no_region();
8051 app.available_profiles = vec![AwsProfile {
8052 name: "user-profile".to_string(),
8053 region: Some("us-east-1".to_string()),
8054 account: Some("123456789".to_string()),
8055 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
8056 source_profile: None,
8057 }];
8058
8059 let filtered = app.get_filtered_profiles();
8060 assert_eq!(filtered.len(), 1);
8061 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
8062 }
8063
8064 #[test]
8065 fn test_filtered_profiles_also_sorted() {
8066 let mut app = test_app_no_region();
8067 app.available_profiles = vec![
8068 AwsProfile {
8069 name: "prod-zebra".to_string(),
8070 region: Some("us-east-1".to_string()),
8071 account: None,
8072 role_arn: None,
8073 source_profile: None,
8074 },
8075 AwsProfile {
8076 name: "prod-alpha".to_string(),
8077 region: Some("us-east-1".to_string()),
8078 account: None,
8079 role_arn: None,
8080 source_profile: None,
8081 },
8082 AwsProfile {
8083 name: "dev-profile".to_string(),
8084 region: Some("us-west-2".to_string()),
8085 account: None,
8086 role_arn: None,
8087 source_profile: None,
8088 },
8089 ];
8090
8091 app.profile_filter = "prod".to_string();
8092 let filtered = app.get_filtered_profiles();
8093 assert_eq!(filtered.len(), 2);
8094 assert_eq!(filtered[0].name, "prod-alpha");
8095 assert_eq!(filtered[1].name, "prod-zebra");
8096 }
8097
8098 #[test]
8099 fn test_profile_picker_has_all_columns() {
8100 let mut app = test_app_no_region();
8101 app.available_profiles = vec![AwsProfile {
8102 name: "test".to_string(),
8103 region: Some("us-east-1".to_string()),
8104 account: Some("123456789".to_string()),
8105 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
8106 source_profile: Some("base".to_string()),
8107 }];
8108
8109 let filtered = app.get_filtered_profiles();
8110 assert_eq!(filtered.len(), 1);
8111 assert!(filtered[0].name == "test");
8112 assert!(filtered[0].region.is_some());
8113 assert!(filtered[0].account.is_some());
8114 assert!(filtered[0].role_arn.is_some());
8115 assert!(filtered[0].source_profile.is_some());
8116 }
8117
8118 #[test]
8119 fn test_session_picker_shows_tab_count() {
8120 let mut app = test_app_no_region();
8121 app.sessions = vec![Session {
8122 id: "1".to_string(),
8123 timestamp: "2024-01-01".to_string(),
8124 profile: "test".to_string(),
8125 region: "us-east-1".to_string(),
8126 account_id: "123".to_string(),
8127 role_arn: String::new(),
8128 tabs: vec![
8129 SessionTab {
8130 service: "CloudWatch".to_string(),
8131 title: "Logs".to_string(),
8132 breadcrumb: String::new(),
8133 filter: None,
8134 selected_item: None,
8135 },
8136 SessionTab {
8137 service: "S3".to_string(),
8138 title: "Buckets".to_string(),
8139 breadcrumb: String::new(),
8140 filter: None,
8141 selected_item: None,
8142 },
8143 ],
8144 }];
8145
8146 let filtered = app.get_filtered_sessions();
8147 assert_eq!(filtered.len(), 1);
8148 assert_eq!(filtered[0].tabs.len(), 2);
8149 }
8150
8151 #[test]
8152 fn test_start_background_data_fetch_loads_profiles() {
8153 let mut app = test_app_no_region();
8154 assert!(app.available_profiles.is_empty());
8155
8156 app.available_profiles = App::load_aws_profiles();
8158
8159 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
8161 }
8162
8163 #[test]
8164 fn test_refresh_in_profile_picker() {
8165 let mut app = test_app_no_region();
8166 app.mode = Mode::ProfilePicker;
8167 app.available_profiles = vec![AwsProfile {
8168 name: "test".to_string(),
8169 region: None,
8170 account: None,
8171 role_arn: None,
8172 source_profile: None,
8173 }];
8174
8175 app.handle_action(Action::Refresh);
8176
8177 assert!(app.log_groups_state.loading);
8179 assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
8180 }
8181
8182 #[test]
8183 fn test_refresh_sets_loading_for_profile_picker() {
8184 let mut app = test_app_no_region();
8185 app.mode = Mode::ProfilePicker;
8186
8187 assert!(!app.log_groups_state.loading);
8188
8189 app.handle_action(Action::Refresh);
8190
8191 assert!(app.log_groups_state.loading);
8192 }
8193
8194 #[test]
8195 fn test_profiles_loaded_on_demand() {
8196 let mut app = test_app_no_region();
8197
8198 assert!(app.available_profiles.is_empty());
8200
8201 app.available_profiles = App::load_aws_profiles();
8203
8204 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
8206 }
8207
8208 #[test]
8209 fn test_profile_accounts_not_fetched_automatically() {
8210 let mut app = test_app_no_region();
8211 app.available_profiles = App::load_aws_profiles();
8212
8213 for profile in &app.available_profiles {
8215 assert!(profile.account.is_none() || profile.account.is_some());
8218 }
8219 }
8220
8221 #[test]
8222 fn test_ctrl_r_triggers_account_fetch() {
8223 let mut app = test_app_no_region();
8224 app.mode = Mode::ProfilePicker;
8225 app.available_profiles = vec![AwsProfile {
8226 name: "test".to_string(),
8227 region: Some("us-east-1".to_string()),
8228 account: None,
8229 role_arn: None,
8230 source_profile: None,
8231 }];
8232
8233 assert!(app.available_profiles[0].account.is_none());
8235
8236 app.handle_action(Action::Refresh);
8238
8239 assert!(app.log_groups_state.loading);
8241 }
8242
8243 #[test]
8244 fn test_refresh_in_region_picker() {
8245 let mut app = test_app_no_region();
8246 app.mode = Mode::RegionPicker;
8247
8248 let initial_latencies = app.region_latencies.len();
8249 app.handle_action(Action::Refresh);
8250
8251 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
8253 }
8254
8255 #[test]
8256 fn test_refresh_in_session_picker() {
8257 let mut app = test_app_no_region();
8258 app.mode = Mode::SessionPicker;
8259 app.sessions = vec![];
8260
8261 app.handle_action(Action::Refresh);
8262
8263 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
8265 }
8266
8267 #[test]
8268 fn test_session_picker_selection() {
8269 let mut app = test_app();
8270
8271 app.sessions = vec![Session {
8272 id: "1".to_string(),
8273 timestamp: "2024-01-01".to_string(),
8274 profile: "prod-profile".to_string(),
8275 region: "us-west-2".to_string(),
8276 account_id: "123456789".to_string(),
8277 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
8278 tabs: vec![SessionTab {
8279 service: "CloudWatchLogGroups".to_string(),
8280 title: "Log Groups".to_string(),
8281 breadcrumb: "CloudWatch > Log Groups".to_string(),
8282 filter: Some("test".to_string()),
8283 selected_item: None,
8284 }],
8285 }];
8286
8287 app.mode = Mode::SessionPicker;
8288 app.session_picker_selected = 0;
8289
8290 app.handle_action(Action::Select);
8292
8293 assert_eq!(app.mode, Mode::Normal);
8294 assert_eq!(app.profile, "prod-profile");
8295 assert_eq!(app.region, "us-west-2");
8296 assert_eq!(app.config.account_id, "123456789");
8297 assert_eq!(app.tabs.len(), 1);
8298 assert_eq!(app.tabs[0].title, "Log Groups");
8299 }
8300
8301 #[test]
8302 fn test_save_session_creates_session() {
8303 let mut app =
8304 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8305 app.config.account_id = "123456789".to_string();
8306 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8307
8308 app.tabs.push(Tab {
8309 service: Service::CloudWatchLogGroups,
8310 title: "Log Groups".to_string(),
8311 breadcrumb: "CloudWatch > Log Groups".to_string(),
8312 });
8313
8314 app.save_current_session();
8315
8316 assert!(app.current_session.is_some());
8317 let session = app.current_session.clone().unwrap();
8318 assert_eq!(session.profile, "test-profile");
8319 assert_eq!(session.region, "us-east-1");
8320 assert_eq!(session.account_id, "123456789");
8321 assert_eq!(session.tabs.len(), 1);
8322
8323 let _ = session.delete();
8325 }
8326
8327 #[test]
8328 fn test_save_session_updates_existing() {
8329 let mut app =
8330 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8331 app.config.account_id = "123456789".to_string();
8332 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8333
8334 app.current_session = Some(Session {
8335 id: "existing".to_string(),
8336 timestamp: "2024-01-01".to_string(),
8337 profile: "test-profile".to_string(),
8338 region: "us-east-1".to_string(),
8339 account_id: "123456789".to_string(),
8340 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
8341 tabs: vec![],
8342 });
8343
8344 app.tabs.push(Tab {
8345 service: Service::CloudWatchLogGroups,
8346 title: "Log Groups".to_string(),
8347 breadcrumb: "CloudWatch > Log Groups".to_string(),
8348 });
8349
8350 app.save_current_session();
8351
8352 let session = app.current_session.clone().unwrap();
8353 assert_eq!(session.id, "existing");
8354 assert_eq!(session.tabs.len(), 1);
8355
8356 let _ = session.delete();
8358 }
8359
8360 #[test]
8361 fn test_save_session_skips_empty_tabs() {
8362 let mut app =
8363 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8364 app.config.account_id = "123456789".to_string();
8365
8366 app.save_current_session();
8367
8368 assert!(app.current_session.is_none());
8369 }
8370
8371 #[test]
8372 fn test_save_session_deletes_when_tabs_closed() {
8373 let mut app =
8374 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8375 app.config.account_id = "123456789".to_string();
8376 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8377
8378 app.current_session = Some(Session {
8380 id: "test_delete".to_string(),
8381 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
8382 profile: "test-profile".to_string(),
8383 region: "us-east-1".to_string(),
8384 account_id: "123456789".to_string(),
8385 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
8386 tabs: vec![],
8387 });
8388
8389 app.save_current_session();
8391
8392 assert!(app.current_session.is_none());
8393 }
8394
8395 #[test]
8396 fn test_closing_all_tabs_deletes_session() {
8397 let mut app =
8398 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8399 app.config.account_id = "123456789".to_string();
8400 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8401
8402 app.tabs.push(Tab {
8404 service: Service::CloudWatchLogGroups,
8405 title: "Log Groups".to_string(),
8406 breadcrumb: "CloudWatch > Log Groups".to_string(),
8407 });
8408
8409 app.save_current_session();
8411 assert!(app.current_session.is_some());
8412 let session_id = app.current_session.as_ref().unwrap().id.clone();
8413
8414 app.tabs.clear();
8416
8417 app.save_current_session();
8419 assert!(app.current_session.is_none());
8420
8421 let _ = Session::load(&session_id).map(|s| s.delete());
8423 }
8424
8425 #[test]
8426 fn test_credential_error_opens_profile_picker() {
8427 let mut app = App::new_without_client("default".to_string(), None);
8429 let error_str = "Unable to load credentials from any source";
8430
8431 if error_str.contains("credentials") {
8432 app.available_profiles = App::load_aws_profiles();
8433 app.mode = Mode::ProfilePicker;
8434 }
8435
8436 assert_eq!(app.mode, Mode::ProfilePicker);
8437 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
8439 }
8440
8441 #[test]
8442 fn test_non_credential_error_shows_error_modal() {
8443 let mut app = App::new_without_client("default".to_string(), None);
8444 let error_str = "Network timeout";
8445
8446 if !error_str.contains("credentials") {
8447 app.error_message = Some(error_str.to_string());
8448 app.mode = Mode::ErrorModal;
8449 }
8450
8451 assert_eq!(app.mode, Mode::ErrorModal);
8452 assert!(app.error_message.is_some());
8453 }
8454
8455 #[tokio::test]
8456 async fn test_profile_selection_loads_credentials() {
8457 std::env::set_var("AWS_PROFILE", "default");
8459
8460 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
8462
8463 if let Ok(app) = result {
8464 assert!(!app.config.account_id.is_empty());
8466 assert!(!app.config.role_arn.is_empty());
8467 assert_eq!(app.profile, "default");
8468 assert_eq!(app.config.region, "us-east-1");
8469 }
8470 }
8472
8473 #[test]
8474 fn test_new_app_shows_service_picker_with_no_tabs() {
8475 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
8476
8477 assert!(!app.service_selected);
8479 assert_eq!(app.mode, Mode::ServicePicker);
8481 assert!(app.tabs.is_empty());
8483 }
8484
8485 #[tokio::test]
8486 async fn test_aws_profile_env_var_read_before_config_load() {
8487 std::env::set_var("AWS_PROFILE", "test-profile");
8489
8490 let profile_name = None
8492 .or_else(|| std::env::var("AWS_PROFILE").ok())
8493 .unwrap_or_else(|| "default".to_string());
8494
8495 assert_eq!(profile_name, "test-profile");
8497
8498 std::env::set_var("AWS_PROFILE", &profile_name);
8500
8501 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
8503
8504 std::env::remove_var("AWS_PROFILE");
8505 }
8506
8507 #[test]
8508 fn test_next_preferences_cloudformation() {
8509 let mut app = test_app();
8510 app.current_service = Service::CloudFormationStacks;
8511 app.mode = Mode::ColumnSelector;
8512 app.column_selector_index = 0;
8513
8514 let page_size_idx = app.cfn_column_ids.len() + 2;
8516 app.handle_action(Action::NextPreferences);
8517 assert_eq!(app.column_selector_index, page_size_idx);
8518
8519 app.handle_action(Action::NextPreferences);
8521 assert_eq!(app.column_selector_index, 0);
8522 }
8523
8524 #[test]
8525 fn test_next_preferences_lambda_functions() {
8526 let mut app = test_app();
8527 app.current_service = Service::LambdaFunctions;
8528 app.mode = Mode::ColumnSelector;
8529 app.column_selector_index = 0;
8530
8531 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
8532 app.handle_action(Action::NextPreferences);
8533 assert_eq!(app.column_selector_index, page_size_idx);
8534
8535 app.handle_action(Action::NextPreferences);
8536 assert_eq!(app.column_selector_index, 0);
8537 }
8538
8539 #[test]
8540 fn test_next_preferences_lambda_applications() {
8541 let mut app = test_app();
8542 app.current_service = Service::LambdaApplications;
8543 app.mode = Mode::ColumnSelector;
8544 app.column_selector_index = 0;
8545
8546 let page_size_idx = app.lambda_application_column_ids.len() + 2;
8547 app.handle_action(Action::NextPreferences);
8548 assert_eq!(app.column_selector_index, page_size_idx);
8549
8550 app.handle_action(Action::NextPreferences);
8551 assert_eq!(app.column_selector_index, 0);
8552 }
8553
8554 #[test]
8555 fn test_next_preferences_ecr_images() {
8556 let mut app = test_app();
8557 app.current_service = Service::EcrRepositories;
8558 app.ecr_state.current_repository = Some("test-repo".to_string());
8559 app.mode = Mode::ColumnSelector;
8560 app.column_selector_index = 0;
8561
8562 let page_size_idx = app.ecr_image_column_ids.len() + 2;
8563 app.handle_action(Action::NextPreferences);
8564 assert_eq!(app.column_selector_index, page_size_idx);
8565
8566 app.handle_action(Action::NextPreferences);
8567 assert_eq!(app.column_selector_index, 0);
8568 }
8569
8570 #[test]
8571 fn test_cloudformation_next_item() {
8572 let mut app = test_app();
8573 app.current_service = Service::CloudFormationStacks;
8574 app.service_selected = true;
8575 app.mode = Mode::Normal;
8576 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8577 app.cfn_state.table.items = vec![
8578 CfnStack {
8579 name: "stack1".to_string(),
8580 stack_id: "id1".to_string(),
8581 status: "CREATE_COMPLETE".to_string(),
8582 created_time: "2024-01-01".to_string(),
8583 updated_time: String::new(),
8584 deleted_time: String::new(),
8585 drift_status: String::new(),
8586 last_drift_check_time: String::new(),
8587 status_reason: String::new(),
8588 description: String::new(),
8589 detailed_status: String::new(),
8590 root_stack: String::new(),
8591 parent_stack: String::new(),
8592 termination_protection: false,
8593 iam_role: String::new(),
8594 tags: Vec::new(),
8595 stack_policy: String::new(),
8596 rollback_monitoring_time: String::new(),
8597 rollback_alarms: Vec::new(),
8598 notification_arns: Vec::new(),
8599 },
8600 CfnStack {
8601 name: "stack2".to_string(),
8602 stack_id: "id2".to_string(),
8603 status: "UPDATE_COMPLETE".to_string(),
8604 created_time: "2024-01-02".to_string(),
8605 updated_time: String::new(),
8606 deleted_time: String::new(),
8607 drift_status: String::new(),
8608 last_drift_check_time: String::new(),
8609 status_reason: String::new(),
8610 description: String::new(),
8611 detailed_status: String::new(),
8612 root_stack: String::new(),
8613 parent_stack: String::new(),
8614 termination_protection: false,
8615 iam_role: String::new(),
8616 tags: Vec::new(),
8617 stack_policy: String::new(),
8618 rollback_monitoring_time: String::new(),
8619 rollback_alarms: Vec::new(),
8620 notification_arns: Vec::new(),
8621 },
8622 ];
8623 app.cfn_state.table.reset();
8624
8625 app.handle_action(Action::NextItem);
8626 assert_eq!(app.cfn_state.table.selected, 1);
8627
8628 app.handle_action(Action::NextItem);
8629 assert_eq!(app.cfn_state.table.selected, 1); }
8631
8632 #[test]
8633 fn test_cloudformation_prev_item() {
8634 let mut app = test_app();
8635 app.current_service = Service::CloudFormationStacks;
8636 app.service_selected = true;
8637 app.mode = Mode::Normal;
8638 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8639 app.cfn_state.table.items = vec![
8640 CfnStack {
8641 name: "stack1".to_string(),
8642 stack_id: "id1".to_string(),
8643 status: "CREATE_COMPLETE".to_string(),
8644 created_time: "2024-01-01".to_string(),
8645 updated_time: String::new(),
8646 deleted_time: String::new(),
8647 drift_status: String::new(),
8648 last_drift_check_time: String::new(),
8649 status_reason: String::new(),
8650 description: String::new(),
8651 detailed_status: String::new(),
8652 root_stack: String::new(),
8653 parent_stack: String::new(),
8654 termination_protection: false,
8655 iam_role: String::new(),
8656 tags: Vec::new(),
8657 stack_policy: String::new(),
8658 rollback_monitoring_time: String::new(),
8659 rollback_alarms: Vec::new(),
8660 notification_arns: Vec::new(),
8661 },
8662 CfnStack {
8663 name: "stack2".to_string(),
8664 stack_id: "id2".to_string(),
8665 status: "UPDATE_COMPLETE".to_string(),
8666 created_time: "2024-01-02".to_string(),
8667 updated_time: String::new(),
8668 deleted_time: String::new(),
8669 drift_status: String::new(),
8670 last_drift_check_time: String::new(),
8671 status_reason: String::new(),
8672 description: String::new(),
8673 detailed_status: String::new(),
8674 root_stack: String::new(),
8675 parent_stack: String::new(),
8676 termination_protection: false,
8677 iam_role: String::new(),
8678 tags: Vec::new(),
8679 stack_policy: String::new(),
8680 rollback_monitoring_time: String::new(),
8681 rollback_alarms: Vec::new(),
8682 notification_arns: Vec::new(),
8683 },
8684 ];
8685 app.cfn_state.table.selected = 1;
8686
8687 app.handle_action(Action::PrevItem);
8688 assert_eq!(app.cfn_state.table.selected, 0);
8689
8690 app.handle_action(Action::PrevItem);
8691 assert_eq!(app.cfn_state.table.selected, 0); }
8693
8694 #[test]
8695 fn test_cloudformation_page_down() {
8696 let mut app = test_app();
8697 app.current_service = Service::CloudFormationStacks;
8698 app.service_selected = true;
8699 app.mode = Mode::Normal;
8700 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8701
8702 for i in 0..20 {
8704 app.cfn_state.table.items.push(CfnStack {
8705 name: format!("stack{}", i),
8706 stack_id: format!("id{}", i),
8707 status: "CREATE_COMPLETE".to_string(),
8708 created_time: format!("2024-01-{:02}", i + 1),
8709 updated_time: String::new(),
8710 deleted_time: String::new(),
8711 drift_status: String::new(),
8712 last_drift_check_time: String::new(),
8713 status_reason: String::new(),
8714 description: String::new(),
8715 detailed_status: String::new(),
8716 root_stack: String::new(),
8717 parent_stack: String::new(),
8718 termination_protection: false,
8719 iam_role: String::new(),
8720 tags: Vec::new(),
8721 stack_policy: String::new(),
8722 rollback_monitoring_time: String::new(),
8723 rollback_alarms: Vec::new(),
8724 notification_arns: Vec::new(),
8725 });
8726 }
8727 app.cfn_state.table.reset();
8728
8729 app.handle_action(Action::PageDown);
8730 assert_eq!(app.cfn_state.table.selected, 10);
8731
8732 app.handle_action(Action::PageDown);
8733 assert_eq!(app.cfn_state.table.selected, 19); }
8735
8736 #[test]
8737 fn test_cloudformation_page_up() {
8738 let mut app = test_app();
8739 app.current_service = Service::CloudFormationStacks;
8740 app.service_selected = true;
8741 app.mode = Mode::Normal;
8742 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8743
8744 for i in 0..20 {
8746 app.cfn_state.table.items.push(CfnStack {
8747 name: format!("stack{}", i),
8748 stack_id: format!("id{}", i),
8749 status: "CREATE_COMPLETE".to_string(),
8750 created_time: format!("2024-01-{:02}", i + 1),
8751 updated_time: String::new(),
8752 deleted_time: String::new(),
8753 drift_status: String::new(),
8754 last_drift_check_time: String::new(),
8755 status_reason: String::new(),
8756 description: String::new(),
8757 detailed_status: String::new(),
8758 root_stack: String::new(),
8759 parent_stack: String::new(),
8760 termination_protection: false,
8761 iam_role: String::new(),
8762 tags: Vec::new(),
8763 stack_policy: String::new(),
8764 rollback_monitoring_time: String::new(),
8765 rollback_alarms: Vec::new(),
8766 notification_arns: Vec::new(),
8767 });
8768 }
8769 app.cfn_state.table.selected = 15;
8770
8771 app.handle_action(Action::PageUp);
8772 assert_eq!(app.cfn_state.table.selected, 5);
8773
8774 app.handle_action(Action::PageUp);
8775 assert_eq!(app.cfn_state.table.selected, 0); }
8777
8778 #[test]
8779 fn test_cloudformation_filter_input() {
8780 let mut app = test_app();
8781 app.current_service = Service::CloudFormationStacks;
8782 app.service_selected = true;
8783 app.mode = Mode::Normal;
8784
8785 app.handle_action(Action::StartFilter);
8786 assert_eq!(app.mode, Mode::FilterInput);
8787
8788 app.cfn_state.table.filter = "test".to_string();
8790 assert_eq!(app.cfn_state.table.filter, "test");
8791 }
8792
8793 #[test]
8794 fn test_cloudformation_filter_applies() {
8795 let mut app = test_app();
8796 app.current_service = Service::CloudFormationStacks;
8797 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8798 app.cfn_state.table.items = vec![
8799 CfnStack {
8800 name: "prod-stack".to_string(),
8801 stack_id: "id1".to_string(),
8802 status: "CREATE_COMPLETE".to_string(),
8803 created_time: "2024-01-01".to_string(),
8804 updated_time: String::new(),
8805 deleted_time: String::new(),
8806 drift_status: String::new(),
8807 last_drift_check_time: String::new(),
8808 status_reason: String::new(),
8809 description: "Production stack".to_string(),
8810 detailed_status: String::new(),
8811 root_stack: String::new(),
8812 parent_stack: String::new(),
8813 termination_protection: false,
8814 iam_role: String::new(),
8815 tags: Vec::new(),
8816 stack_policy: String::new(),
8817 rollback_monitoring_time: String::new(),
8818 rollback_alarms: Vec::new(),
8819 notification_arns: Vec::new(),
8820 },
8821 CfnStack {
8822 name: "dev-stack".to_string(),
8823 stack_id: "id2".to_string(),
8824 status: "UPDATE_COMPLETE".to_string(),
8825 created_time: "2024-01-02".to_string(),
8826 updated_time: String::new(),
8827 deleted_time: String::new(),
8828 drift_status: String::new(),
8829 last_drift_check_time: String::new(),
8830 status_reason: String::new(),
8831 description: "Development stack".to_string(),
8832 detailed_status: String::new(),
8833 root_stack: String::new(),
8834 parent_stack: String::new(),
8835 termination_protection: false,
8836 iam_role: String::new(),
8837 tags: Vec::new(),
8838 stack_policy: String::new(),
8839 rollback_monitoring_time: String::new(),
8840 rollback_alarms: Vec::new(),
8841 notification_arns: Vec::new(),
8842 },
8843 ];
8844 app.cfn_state.table.filter = "prod".to_string();
8845
8846 let filtered = app.filtered_cloudformation_stacks();
8847 assert_eq!(filtered.len(), 1);
8848 assert_eq!(filtered[0].name, "prod-stack");
8849 }
8850
8851 #[test]
8852 fn test_cloudformation_right_arrow_expands() {
8853 let mut app = test_app();
8854 app.current_service = Service::CloudFormationStacks;
8855 app.service_selected = true;
8856 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8857 app.cfn_state.table.items = vec![CfnStack {
8858 name: "test-stack".to_string(),
8859 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8860 .to_string(),
8861 status: "CREATE_COMPLETE".to_string(),
8862 created_time: "2024-01-01".to_string(),
8863 updated_time: String::new(),
8864 deleted_time: String::new(),
8865 drift_status: String::new(),
8866 last_drift_check_time: String::new(),
8867 status_reason: String::new(),
8868 description: "Test stack".to_string(),
8869 detailed_status: String::new(),
8870 root_stack: String::new(),
8871 parent_stack: String::new(),
8872 termination_protection: false,
8873 iam_role: String::new(),
8874 tags: Vec::new(),
8875 stack_policy: String::new(),
8876 rollback_monitoring_time: String::new(),
8877 rollback_alarms: Vec::new(),
8878 notification_arns: Vec::new(),
8879 }];
8880 app.cfn_state.table.reset();
8881
8882 assert_eq!(app.cfn_state.table.expanded_item, None);
8883
8884 app.handle_action(Action::NextPane);
8885 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
8886 }
8887
8888 #[test]
8889 fn test_cloudformation_left_arrow_collapses() {
8890 let mut app = test_app();
8891 app.current_service = Service::CloudFormationStacks;
8892 app.service_selected = true;
8893 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8894 app.cfn_state.table.items = vec![CfnStack {
8895 name: "test-stack".to_string(),
8896 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8897 .to_string(),
8898 status: "CREATE_COMPLETE".to_string(),
8899 created_time: "2024-01-01".to_string(),
8900 updated_time: String::new(),
8901 deleted_time: String::new(),
8902 drift_status: String::new(),
8903 last_drift_check_time: String::new(),
8904 status_reason: String::new(),
8905 description: "Test stack".to_string(),
8906 detailed_status: String::new(),
8907 root_stack: String::new(),
8908 parent_stack: String::new(),
8909 termination_protection: false,
8910 iam_role: String::new(),
8911 tags: Vec::new(),
8912 stack_policy: String::new(),
8913 rollback_monitoring_time: String::new(),
8914 rollback_alarms: Vec::new(),
8915 notification_arns: Vec::new(),
8916 }];
8917 app.cfn_state.table.reset();
8918 app.cfn_state.table.expanded_item = Some(0);
8919
8920 app.handle_action(Action::PrevPane);
8921 assert_eq!(app.cfn_state.table.expanded_item, None);
8922 }
8923
8924 #[test]
8925 fn test_cloudformation_enter_drills_into_stack() {
8926 let mut app = test_app();
8927 app.current_service = Service::CloudFormationStacks;
8928 app.service_selected = true;
8929 app.mode = Mode::Normal;
8930 app.tabs = vec![Tab {
8931 service: Service::CloudFormationStacks,
8932 title: "CloudFormation > Stacks".to_string(),
8933 breadcrumb: "CloudFormation > Stacks".to_string(),
8934 }];
8935 app.current_tab = 0;
8936 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8937 app.cfn_state.table.items = vec![CfnStack {
8938 name: "test-stack".to_string(),
8939 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8940 .to_string(),
8941 status: "CREATE_COMPLETE".to_string(),
8942 created_time: "2024-01-01".to_string(),
8943 updated_time: String::new(),
8944 deleted_time: String::new(),
8945 drift_status: String::new(),
8946 last_drift_check_time: String::new(),
8947 status_reason: String::new(),
8948 description: "Test stack".to_string(),
8949 detailed_status: String::new(),
8950 root_stack: String::new(),
8951 parent_stack: String::new(),
8952 termination_protection: false,
8953 iam_role: String::new(),
8954 tags: Vec::new(),
8955 stack_policy: String::new(),
8956 rollback_monitoring_time: String::new(),
8957 rollback_alarms: Vec::new(),
8958 notification_arns: Vec::new(),
8959 }];
8960 app.cfn_state.table.reset();
8961
8962 let filtered = app.filtered_cloudformation_stacks();
8964 assert_eq!(filtered.len(), 1);
8965 assert_eq!(filtered[0].name, "test-stack");
8966
8967 assert_eq!(app.cfn_state.current_stack, None);
8968
8969 app.handle_action(Action::Select);
8971 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
8972 }
8973
8974 #[test]
8975 fn test_cloudformation_copy_to_clipboard() {
8976 let mut app = test_app();
8977 app.current_service = Service::CloudFormationStacks;
8978 app.service_selected = true;
8979 app.mode = Mode::Normal;
8980 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8981 app.cfn_state.table.items = vec![
8982 CfnStack {
8983 name: "stack1".to_string(),
8984 stack_id: "id1".to_string(),
8985 status: "CREATE_COMPLETE".to_string(),
8986 created_time: "2024-01-01".to_string(),
8987 updated_time: String::new(),
8988 deleted_time: String::new(),
8989 drift_status: String::new(),
8990 last_drift_check_time: String::new(),
8991 status_reason: String::new(),
8992 description: String::new(),
8993 detailed_status: String::new(),
8994 root_stack: String::new(),
8995 parent_stack: String::new(),
8996 termination_protection: false,
8997 iam_role: String::new(),
8998 tags: Vec::new(),
8999 stack_policy: String::new(),
9000 rollback_monitoring_time: String::new(),
9001 rollback_alarms: Vec::new(),
9002 notification_arns: Vec::new(),
9003 },
9004 CfnStack {
9005 name: "stack2".to_string(),
9006 stack_id: "id2".to_string(),
9007 status: "UPDATE_COMPLETE".to_string(),
9008 created_time: "2024-01-02".to_string(),
9009 updated_time: String::new(),
9010 deleted_time: String::new(),
9011 drift_status: String::new(),
9012 last_drift_check_time: String::new(),
9013 status_reason: String::new(),
9014 description: String::new(),
9015 detailed_status: String::new(),
9016 root_stack: String::new(),
9017 parent_stack: String::new(),
9018 termination_protection: false,
9019 iam_role: String::new(),
9020 tags: Vec::new(),
9021 stack_policy: String::new(),
9022 rollback_monitoring_time: String::new(),
9023 rollback_alarms: Vec::new(),
9024 notification_arns: Vec::new(),
9025 },
9026 ];
9027
9028 assert!(!app.snapshot_requested);
9029 app.handle_action(Action::CopyToClipboard);
9030
9031 assert!(app.snapshot_requested);
9033 }
9034
9035 #[test]
9036 fn test_cloudformation_expansion_shows_all_visible_columns() {
9037 let mut app = test_app();
9038 app.current_service = Service::CloudFormationStacks;
9039 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9040 app.cfn_state.table.items = vec![CfnStack {
9041 name: "test-stack".to_string(),
9042 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
9043 .to_string(),
9044 status: "CREATE_COMPLETE".to_string(),
9045 created_time: "2024-01-01".to_string(),
9046 updated_time: "2024-01-02".to_string(),
9047 deleted_time: String::new(),
9048 drift_status: "IN_SYNC".to_string(),
9049 last_drift_check_time: "2024-01-03".to_string(),
9050 status_reason: String::new(),
9051 description: "Test description".to_string(),
9052 detailed_status: String::new(),
9053 root_stack: String::new(),
9054 parent_stack: String::new(),
9055 termination_protection: false,
9056 iam_role: String::new(),
9057 tags: Vec::new(),
9058 stack_policy: String::new(),
9059 rollback_monitoring_time: String::new(),
9060 rollback_alarms: Vec::new(),
9061 notification_arns: Vec::new(),
9062 }];
9063
9064 app.cfn_visible_column_ids = [
9066 CfnColumn::Name,
9067 CfnColumn::Status,
9068 CfnColumn::CreatedTime,
9069 CfnColumn::Description,
9070 ]
9071 .iter()
9072 .map(|c| c.id())
9073 .collect();
9074
9075 app.cfn_state.table.expanded_item = Some(0);
9076
9077 assert_eq!(app.cfn_visible_column_ids.len(), 4);
9080 assert!(app.cfn_state.table.has_expanded_item());
9081 }
9082
9083 #[test]
9084 fn test_cloudformation_empty_list_shows_page_1() {
9085 let mut app = test_app();
9086 app.current_service = Service::CloudFormationStacks;
9087 app.cfn_state.table.items = vec![];
9088
9089 let filtered = app.filtered_cloudformation_stacks();
9090 assert_eq!(filtered.len(), 0);
9091
9092 let page_size = app.cfn_state.table.page_size.value();
9094 let total_pages = filtered.len().div_ceil(page_size);
9095 assert_eq!(total_pages, 0);
9096
9097 }
9100}
9101
9102impl App {
9103 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
9104 let mut all = AwsRegion::all();
9105
9106 for region in &mut all {
9108 region.latency_ms = self.region_latencies.get(region.code).copied();
9109 }
9110
9111 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
9113 all
9114 } else {
9115 let filter_lower = self.region_filter.to_lowercase();
9116 all.into_iter()
9117 .filter(|r| {
9118 r.name.to_lowercase().contains(&filter_lower)
9119 || r.code.to_lowercase().contains(&filter_lower)
9120 || r.group.to_lowercase().contains(&filter_lower)
9121 })
9122 .collect()
9123 };
9124
9125 let mut sorted = filtered;
9127 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
9128 sorted
9129 }
9130
9131 pub fn measure_region_latencies(&mut self) {
9132 use std::time::Instant;
9133 self.region_latencies.clear();
9134
9135 let regions = AwsRegion::all();
9136 let start_all = Instant::now();
9137 tracing::info!("Starting latency measurement for {} regions", regions.len());
9138
9139 let handles: Vec<_> = regions
9140 .iter()
9141 .map(|region| {
9142 let code = region.code.to_string();
9143 std::thread::spawn(move || {
9144 let endpoint = format!("https://sts.{}.amazonaws.com", code);
9146 let start = Instant::now();
9147
9148 match ureq::get(&endpoint)
9149 .timeout(std::time::Duration::from_secs(2))
9150 .call()
9151 {
9152 Ok(_) => {
9153 let latency = start.elapsed().as_millis() as u64;
9154 Some((code, latency))
9155 }
9156 Err(e) => {
9157 tracing::debug!("Failed to measure {}: {}", code, e);
9158 Some((code, 9999))
9159 }
9160 }
9161 })
9162 })
9163 .collect();
9164
9165 for handle in handles {
9166 if let Ok(Some((code, latency))) = handle.join() {
9167 self.region_latencies.insert(code, latency);
9168 }
9169 }
9170
9171 tracing::info!(
9172 "Measured {} regions in {:?}",
9173 self.region_latencies.len(),
9174 start_all.elapsed()
9175 );
9176 }
9177
9178 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
9179 crate::aws::filter_profiles(&self.available_profiles, &self.profile_filter)
9180 }
9181
9182 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
9183 if self.session_filter.is_empty() {
9184 return self.sessions.iter().collect();
9185 }
9186 let filter_lower = self.session_filter.to_lowercase();
9187 self.sessions
9188 .iter()
9189 .filter(|s| {
9190 s.profile.to_lowercase().contains(&filter_lower)
9191 || s.region.to_lowercase().contains(&filter_lower)
9192 || s.account_id.to_lowercase().contains(&filter_lower)
9193 || s.role_arn.to_lowercase().contains(&filter_lower)
9194 })
9195 .collect()
9196 }
9197
9198 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
9199 if self.tab_filter.is_empty() {
9200 return self.tabs.iter().enumerate().collect();
9201 }
9202 let filter_lower = self.tab_filter.to_lowercase();
9203 self.tabs
9204 .iter()
9205 .enumerate()
9206 .filter(|(_, tab)| {
9207 tab.title.to_lowercase().contains(&filter_lower)
9208 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
9209 })
9210 .collect()
9211 }
9212
9213 pub fn load_aws_profiles() -> Vec<AwsProfile> {
9214 AwsProfile::load_all()
9215 }
9216
9217 pub async fn fetch_profile_accounts(&mut self) {
9218 for profile in &mut self.available_profiles {
9219 if profile.account.is_none() {
9220 let region = profile
9221 .region
9222 .clone()
9223 .unwrap_or_else(|| "us-east-1".to_string());
9224 if let Ok(account) =
9225 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
9226 {
9227 profile.account = Some(account);
9228 }
9229 }
9230 }
9231 }
9232
9233 fn save_current_session(&mut self) {
9234 if self.tabs.is_empty() {
9236 if let Some(ref session) = self.current_session {
9237 let _ = session.delete();
9238 self.current_session = None;
9239 }
9240 return;
9241 }
9242
9243 let session = if let Some(ref mut current) = self.current_session {
9244 current.tabs = self
9246 .tabs
9247 .iter()
9248 .map(|t| SessionTab {
9249 service: format!("{:?}", t.service),
9250 title: t.title.clone(),
9251 breadcrumb: t.breadcrumb.clone(),
9252 filter: match t.service {
9253 Service::CloudWatchLogGroups => {
9254 Some(self.log_groups_state.log_groups.filter.clone())
9255 }
9256 _ => None,
9257 },
9258 selected_item: None,
9259 })
9260 .collect();
9261 current.clone()
9262 } else {
9263 let mut session = Session::new(
9265 self.profile.clone(),
9266 self.region.clone(),
9267 self.config.account_id.clone(),
9268 self.config.role_arn.clone(),
9269 );
9270 session.tabs = self
9271 .tabs
9272 .iter()
9273 .map(|t| SessionTab {
9274 service: format!("{:?}", t.service),
9275 title: t.title.clone(),
9276 breadcrumb: t.breadcrumb.clone(),
9277 filter: match t.service {
9278 Service::CloudWatchLogGroups => {
9279 Some(self.log_groups_state.log_groups.filter.clone())
9280 }
9281 _ => None,
9282 },
9283 selected_item: None,
9284 })
9285 .collect();
9286 self.current_session = Some(session.clone());
9287 session
9288 };
9289
9290 let _ = session.save();
9291 }
9292}
9293
9294#[cfg(test)]
9295mod iam_policy_view_tests {
9296 use super::*;
9297 use test_helpers::*;
9298
9299 #[test]
9300 fn test_enter_opens_policy_view() {
9301 let mut app = test_app();
9302 app.current_service = Service::IamRoles;
9303 app.service_selected = true;
9304 app.mode = Mode::Normal;
9305 app.view_mode = ViewMode::Detail;
9306 app.iam_state.current_role = Some("TestRole".to_string());
9307 app.iam_state.policies.items = vec![crate::iam::Policy {
9308 policy_name: "TestPolicy".to_string(),
9309 policy_type: "Inline".to_string(),
9310 attached_via: "Direct".to_string(),
9311 attached_entities: "1".to_string(),
9312 description: "Test".to_string(),
9313 creation_time: "2023-01-01".to_string(),
9314 edited_time: "2023-01-01".to_string(),
9315 policy_arn: None,
9316 }];
9317 app.iam_state.policies.reset();
9318
9319 app.handle_action(Action::Select);
9320
9321 assert_eq!(app.view_mode, ViewMode::PolicyView);
9322 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
9323 assert_eq!(app.iam_state.policy_scroll, 0);
9324 assert!(app.iam_state.policies.loading);
9325 }
9326
9327 #[test]
9328 fn test_escape_closes_policy_view() {
9329 let mut app = test_app();
9330 app.current_service = Service::IamRoles;
9331 app.service_selected = true;
9332 app.mode = Mode::Normal;
9333 app.view_mode = ViewMode::PolicyView;
9334 app.iam_state.current_role = Some("TestRole".to_string());
9335 app.iam_state.current_policy = Some("TestPolicy".to_string());
9336 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
9337 app.iam_state.policy_scroll = 5;
9338
9339 app.handle_action(Action::PrevPane);
9340
9341 assert_eq!(app.view_mode, ViewMode::Detail);
9342 assert_eq!(app.iam_state.current_policy, None);
9343 assert_eq!(app.iam_state.policy_document, "");
9344 assert_eq!(app.iam_state.policy_scroll, 0);
9345 }
9346
9347 #[test]
9348 fn test_ctrl_d_scrolls_down_in_policy_view() {
9349 let mut app = test_app();
9350 app.current_service = Service::IamRoles;
9351 app.service_selected = true;
9352 app.mode = Mode::Normal;
9353 app.view_mode = ViewMode::PolicyView;
9354 app.iam_state.current_role = Some("TestRole".to_string());
9355 app.iam_state.current_policy = Some("TestPolicy".to_string());
9356 app.iam_state.policy_document = (0..100)
9357 .map(|i| format!("line {}", i))
9358 .collect::<Vec<_>>()
9359 .join("\n");
9360 app.iam_state.policy_scroll = 0;
9361
9362 app.handle_action(Action::ScrollDown);
9363
9364 assert_eq!(app.iam_state.policy_scroll, 10);
9365
9366 app.handle_action(Action::ScrollDown);
9367
9368 assert_eq!(app.iam_state.policy_scroll, 20);
9369 }
9370
9371 #[test]
9372 fn test_ctrl_u_scrolls_up_in_policy_view() {
9373 let mut app = test_app();
9374 app.current_service = Service::IamRoles;
9375 app.service_selected = true;
9376 app.mode = Mode::Normal;
9377 app.view_mode = ViewMode::PolicyView;
9378 app.iam_state.current_role = Some("TestRole".to_string());
9379 app.iam_state.current_policy = Some("TestPolicy".to_string());
9380 app.iam_state.policy_document = (0..100)
9381 .map(|i| format!("line {}", i))
9382 .collect::<Vec<_>>()
9383 .join("\n");
9384 app.iam_state.policy_scroll = 30;
9385
9386 app.handle_action(Action::ScrollUp);
9387
9388 assert_eq!(app.iam_state.policy_scroll, 20);
9389
9390 app.handle_action(Action::ScrollUp);
9391
9392 assert_eq!(app.iam_state.policy_scroll, 10);
9393 }
9394
9395 #[test]
9396 fn test_scroll_does_not_go_negative() {
9397 let mut app = test_app();
9398 app.current_service = Service::IamRoles;
9399 app.service_selected = true;
9400 app.mode = Mode::Normal;
9401 app.view_mode = ViewMode::PolicyView;
9402 app.iam_state.current_role = Some("TestRole".to_string());
9403 app.iam_state.current_policy = Some("TestPolicy".to_string());
9404 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
9405 app.iam_state.policy_scroll = 0;
9406
9407 app.handle_action(Action::ScrollUp);
9408
9409 assert_eq!(app.iam_state.policy_scroll, 0);
9410 }
9411
9412 #[test]
9413 fn test_scroll_does_not_exceed_max() {
9414 let mut app = test_app();
9415 app.current_service = Service::IamRoles;
9416 app.service_selected = true;
9417 app.mode = Mode::Normal;
9418 app.view_mode = ViewMode::PolicyView;
9419 app.iam_state.current_role = Some("TestRole".to_string());
9420 app.iam_state.current_policy = Some("TestPolicy".to_string());
9421 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
9422 app.iam_state.policy_scroll = 0;
9423
9424 app.handle_action(Action::ScrollDown);
9425
9426 assert_eq!(app.iam_state.policy_scroll, 2); }
9428
9429 #[test]
9430 fn test_policy_view_console_url() {
9431 let mut app = test_app();
9432 app.current_service = Service::IamRoles;
9433 app.service_selected = true;
9434 app.view_mode = ViewMode::PolicyView;
9435 app.iam_state.current_role = Some("TestRole".to_string());
9436 app.iam_state.current_policy = Some("TestPolicy".to_string());
9437
9438 let url = app.get_console_url();
9439
9440 assert!(url.contains("us-east-1.console.aws.amazon.com"));
9441 assert!(url.contains("/roles/details/TestRole"));
9442 assert!(url.contains("/editPolicy/TestPolicy"));
9443 assert!(url.contains("step=addPermissions"));
9444 }
9445
9446 #[test]
9447 fn test_esc_from_policy_view_goes_to_role_detail() {
9448 let mut app = test_app();
9449 app.current_service = Service::IamRoles;
9450 app.service_selected = true;
9451 app.mode = Mode::Normal;
9452 app.view_mode = ViewMode::PolicyView;
9453 app.iam_state.current_role = Some("TestRole".to_string());
9454 app.iam_state.current_policy = Some("TestPolicy".to_string());
9455 app.iam_state.policy_document = "test".to_string();
9456 app.iam_state.policy_scroll = 5;
9457
9458 app.handle_action(Action::GoBack);
9459
9460 assert_eq!(app.view_mode, ViewMode::Detail);
9461 assert_eq!(app.iam_state.current_policy, None);
9462 assert_eq!(app.iam_state.policy_document, "");
9463 assert_eq!(app.iam_state.policy_scroll, 0);
9464 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
9465 }
9466
9467 #[test]
9468 fn test_esc_from_role_detail_goes_to_role_list() {
9469 let mut app = test_app();
9470 app.current_service = Service::IamRoles;
9471 app.service_selected = true;
9472 app.mode = Mode::Normal;
9473 app.view_mode = ViewMode::Detail;
9474 app.iam_state.current_role = Some("TestRole".to_string());
9475
9476 app.handle_action(Action::GoBack);
9477
9478 assert_eq!(app.iam_state.current_role, None);
9479 }
9480
9481 #[test]
9482 fn test_right_arrow_expands_policy_row() {
9483 let mut app = test_app();
9484 app.current_service = Service::IamRoles;
9485 app.service_selected = true;
9486 app.mode = Mode::Normal;
9487 app.view_mode = ViewMode::Detail;
9488 app.iam_state.current_role = Some("TestRole".to_string());
9489 app.iam_state.policies.items = vec![crate::iam::Policy {
9490 policy_name: "TestPolicy".to_string(),
9491 policy_type: "Inline".to_string(),
9492 attached_via: "Direct".to_string(),
9493 attached_entities: "1".to_string(),
9494 description: "Test".to_string(),
9495 creation_time: "2023-01-01".to_string(),
9496 edited_time: "2023-01-01".to_string(),
9497 policy_arn: None,
9498 }];
9499 app.iam_state.policies.reset();
9500
9501 app.handle_action(Action::NextPane);
9502
9503 assert_eq!(app.view_mode, ViewMode::Detail);
9505 assert_eq!(app.iam_state.current_policy, None);
9506 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
9507 }
9508}
9509
9510#[cfg(test)]
9511mod tab_filter_tests {
9512 use super::*;
9513 use test_helpers::*;
9514
9515 #[test]
9516 fn test_space_t_opens_tab_picker() {
9517 let mut app = test_app();
9518 app.tabs = vec![
9519 Tab {
9520 service: Service::CloudWatchLogGroups,
9521 title: "Tab 1".to_string(),
9522 breadcrumb: "CloudWatch > Log groups".to_string(),
9523 },
9524 Tab {
9525 service: Service::S3Buckets,
9526 title: "Tab 2".to_string(),
9527 breadcrumb: "S3 > Buckets".to_string(),
9528 },
9529 ];
9530 app.current_tab = 0;
9531
9532 app.handle_action(Action::OpenTabPicker);
9533
9534 assert_eq!(app.mode, Mode::TabPicker);
9535 assert_eq!(app.tab_picker_selected, 0);
9536 }
9537
9538 #[test]
9539 fn test_tab_filter_works() {
9540 let mut app = test_app();
9541 app.tabs = vec![
9542 Tab {
9543 service: Service::CloudWatchLogGroups,
9544 title: "CloudWatch Logs".to_string(),
9545 breadcrumb: "CloudWatch > Log groups".to_string(),
9546 },
9547 Tab {
9548 service: Service::S3Buckets,
9549 title: "S3 Buckets".to_string(),
9550 breadcrumb: "S3 > Buckets".to_string(),
9551 },
9552 Tab {
9553 service: Service::CloudWatchAlarms,
9554 title: "CloudWatch Alarms".to_string(),
9555 breadcrumb: "CloudWatch > Alarms".to_string(),
9556 },
9557 ];
9558 app.mode = Mode::TabPicker;
9559
9560 app.handle_action(Action::FilterInput('s'));
9562 app.handle_action(Action::FilterInput('3'));
9563
9564 let filtered = app.get_filtered_tabs();
9565 assert_eq!(filtered.len(), 1);
9566 assert_eq!(filtered[0].1.title, "S3 Buckets");
9567 }
9568
9569 #[test]
9570 fn test_tab_filter_by_breadcrumb() {
9571 let mut app = test_app();
9572 app.tabs = vec![
9573 Tab {
9574 service: Service::CloudWatchLogGroups,
9575 title: "Tab 1".to_string(),
9576 breadcrumb: "CloudWatch > Log groups".to_string(),
9577 },
9578 Tab {
9579 service: Service::S3Buckets,
9580 title: "Tab 2".to_string(),
9581 breadcrumb: "S3 > Buckets".to_string(),
9582 },
9583 ];
9584 app.mode = Mode::TabPicker;
9585
9586 app.handle_action(Action::FilterInput('c'));
9588 app.handle_action(Action::FilterInput('l'));
9589 app.handle_action(Action::FilterInput('o'));
9590 app.handle_action(Action::FilterInput('u'));
9591 app.handle_action(Action::FilterInput('d'));
9592
9593 let filtered = app.get_filtered_tabs();
9594 assert_eq!(filtered.len(), 1);
9595 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
9596 }
9597
9598 #[test]
9599 fn test_tab_filter_backspace() {
9600 let mut app = test_app();
9601 app.tabs = vec![
9602 Tab {
9603 service: Service::CloudWatchLogGroups,
9604 title: "CloudWatch Logs".to_string(),
9605 breadcrumb: "CloudWatch > Log groups".to_string(),
9606 },
9607 Tab {
9608 service: Service::S3Buckets,
9609 title: "S3 Buckets".to_string(),
9610 breadcrumb: "S3 > Buckets".to_string(),
9611 },
9612 ];
9613 app.mode = Mode::TabPicker;
9614
9615 app.handle_action(Action::FilterInput('s'));
9616 app.handle_action(Action::FilterInput('3'));
9617 assert_eq!(app.tab_filter, "s3");
9618
9619 app.handle_action(Action::FilterBackspace);
9620 assert_eq!(app.tab_filter, "s");
9621
9622 let filtered = app.get_filtered_tabs();
9623 assert_eq!(filtered.len(), 2); }
9625
9626 #[test]
9627 fn test_tab_selection_with_filter() {
9628 let mut app = test_app();
9629 app.tabs = vec![
9630 Tab {
9631 service: Service::CloudWatchLogGroups,
9632 title: "CloudWatch Logs".to_string(),
9633 breadcrumb: "CloudWatch > Log groups".to_string(),
9634 },
9635 Tab {
9636 service: Service::S3Buckets,
9637 title: "S3 Buckets".to_string(),
9638 breadcrumb: "S3 > Buckets".to_string(),
9639 },
9640 ];
9641 app.mode = Mode::TabPicker;
9642 app.current_tab = 0;
9643
9644 app.handle_action(Action::FilterInput('s'));
9646 app.handle_action(Action::FilterInput('3'));
9647
9648 app.handle_action(Action::Select);
9650
9651 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
9653 assert_eq!(app.tab_filter, ""); }
9655}
9656
9657#[cfg(test)]
9658mod region_latency_tests {
9659 use super::*;
9660 use test_helpers::*;
9661
9662 #[test]
9663 fn test_regions_sorted_by_latency() {
9664 let mut app = test_app();
9665
9666 app.region_latencies.insert("us-west-2".to_string(), 50);
9668 app.region_latencies.insert("us-east-1".to_string(), 10);
9669 app.region_latencies.insert("eu-west-1".to_string(), 100);
9670
9671 let filtered = app.get_filtered_regions();
9672
9673 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
9675
9676 assert!(with_latency.len() >= 3);
9677 assert_eq!(with_latency[0].code, "us-east-1");
9678 assert_eq!(with_latency[0].latency_ms, Some(10));
9679 assert_eq!(with_latency[1].code, "us-west-2");
9680 assert_eq!(with_latency[1].latency_ms, Some(50));
9681 assert_eq!(with_latency[2].code, "eu-west-1");
9682 assert_eq!(with_latency[2].latency_ms, Some(100));
9683 }
9684
9685 #[test]
9686 fn test_regions_with_latency_before_without() {
9687 let mut app = test_app();
9688
9689 app.region_latencies.insert("eu-west-1".to_string(), 100);
9691
9692 let filtered = app.get_filtered_regions();
9693
9694 assert_eq!(filtered[0].code, "eu-west-1");
9696 assert_eq!(filtered[0].latency_ms, Some(100));
9697
9698 for region in &filtered[1..] {
9700 assert!(region.latency_ms.is_none());
9701 }
9702 }
9703
9704 #[test]
9705 fn test_region_filter_with_latency() {
9706 let mut app = test_app();
9707
9708 app.region_latencies.insert("us-east-1".to_string(), 10);
9709 app.region_latencies.insert("us-west-2".to_string(), 50);
9710 app.region_filter = "us".to_string();
9711
9712 let filtered = app.get_filtered_regions();
9713
9714 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
9716 assert_eq!(filtered[0].code, "us-east-1");
9717 assert_eq!(filtered[1].code, "us-west-2");
9718 }
9719
9720 #[test]
9721 fn test_latency_persists_across_filters() {
9722 let mut app = test_app();
9723
9724 app.region_latencies.insert("us-east-1".to_string(), 10);
9725
9726 app.region_filter = "eu".to_string();
9728 let filtered = app.get_filtered_regions();
9729 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
9730
9731 app.region_filter.clear();
9733 let all = app.get_filtered_regions();
9734
9735 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
9737 assert_eq!(us_east.latency_ms, Some(10));
9738 }
9739
9740 #[test]
9741 fn test_measure_region_latencies_clears_previous() {
9742 let mut app = test_app();
9743
9744 app.region_latencies.insert("us-east-1".to_string(), 100);
9746 app.region_latencies.insert("eu-west-1".to_string(), 200);
9747
9748 app.measure_region_latencies();
9750
9751 assert!(
9753 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
9754 );
9755 }
9756
9757 #[test]
9758 fn test_regions_with_latency_sorted_first() {
9759 let mut app = test_app();
9760
9761 app.region_latencies.insert("us-east-1".to_string(), 50);
9763 app.region_latencies.insert("eu-west-1".to_string(), 500);
9764
9765 let filtered = app.get_filtered_regions();
9766
9767 assert!(filtered.len() > 2);
9769
9770 assert_eq!(filtered[0].code, "us-east-1");
9772 assert_eq!(filtered[0].latency_ms, Some(50));
9773 assert_eq!(filtered[1].code, "eu-west-1");
9774 assert_eq!(filtered[1].latency_ms, Some(500));
9775
9776 for region in &filtered[2..] {
9778 assert!(region.latency_ms.is_none());
9779 }
9780 }
9781
9782 #[test]
9783 fn test_regions_without_latency_sorted_as_1000ms() {
9784 let mut app = test_app();
9785
9786 app.region_latencies
9788 .insert("ap-southeast-2".to_string(), 1500);
9789 app.region_latencies.insert("us-east-1".to_string(), 50);
9791
9792 let filtered = app.get_filtered_regions();
9793
9794 assert_eq!(filtered[0].code, "us-east-1");
9796 assert_eq!(filtered[0].latency_ms, Some(50));
9797
9798 let slow_region_idx = filtered
9800 .iter()
9801 .position(|r| r.code == "ap-southeast-2")
9802 .unwrap();
9803 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
9807 assert!(region.latency_ms.is_none());
9808 }
9809 }
9810
9811 #[test]
9812 fn test_region_picker_opens_with_latencies() {
9813 let mut app = test_app();
9814
9815 app.region_filter.clear();
9817 app.region_picker_selected = 0;
9818 app.measure_region_latencies();
9819
9820 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
9823 }
9824
9825 #[test]
9826 fn test_ecr_tab_next() {
9827 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
9828 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
9829 }
9830
9831 #[test]
9832 fn test_ecr_tab_switching() {
9833 let mut app = test_app();
9834 app.current_service = Service::EcrRepositories;
9835 app.service_selected = true;
9836 app.ecr_state.tab = EcrTab::Private;
9837
9838 app.handle_action(Action::NextDetailTab);
9839 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9840 assert_eq!(app.ecr_state.repositories.selected, 0);
9841
9842 app.handle_action(Action::NextDetailTab);
9843 assert_eq!(app.ecr_state.tab, EcrTab::Private);
9844 }
9845
9846 #[test]
9847 fn test_ecr_navigation() {
9848 let mut app = test_app();
9849 app.current_service = Service::EcrRepositories;
9850 app.service_selected = true;
9851 app.mode = Mode::Normal;
9852 app.ecr_state.repositories.items = vec![
9853 EcrRepository {
9854 name: "repo1".to_string(),
9855 uri: "uri1".to_string(),
9856 created_at: "2023-01-01".to_string(),
9857 tag_immutability: "MUTABLE".to_string(),
9858 encryption_type: "AES256".to_string(),
9859 },
9860 EcrRepository {
9861 name: "repo2".to_string(),
9862 uri: "uri2".to_string(),
9863 created_at: "2023-01-02".to_string(),
9864 tag_immutability: "IMMUTABLE".to_string(),
9865 encryption_type: "KMS".to_string(),
9866 },
9867 ];
9868
9869 app.handle_action(Action::NextItem);
9870 assert_eq!(app.ecr_state.repositories.selected, 1);
9871
9872 app.handle_action(Action::PrevItem);
9873 assert_eq!(app.ecr_state.repositories.selected, 0);
9874 }
9875
9876 #[test]
9877 fn test_ecr_filter() {
9878 let mut app = test_app();
9879 app.current_service = Service::EcrRepositories;
9880 app.service_selected = true;
9881 app.ecr_state.repositories.items = vec![
9882 EcrRepository {
9883 name: "my-app".to_string(),
9884 uri: "uri1".to_string(),
9885 created_at: "2023-01-01".to_string(),
9886 tag_immutability: "MUTABLE".to_string(),
9887 encryption_type: "AES256".to_string(),
9888 },
9889 EcrRepository {
9890 name: "other-service".to_string(),
9891 uri: "uri2".to_string(),
9892 created_at: "2023-01-02".to_string(),
9893 tag_immutability: "IMMUTABLE".to_string(),
9894 encryption_type: "KMS".to_string(),
9895 },
9896 ];
9897
9898 app.ecr_state.repositories.filter = "app".to_string();
9899 let filtered = app.filtered_ecr_repositories();
9900 assert_eq!(filtered.len(), 1);
9901 assert_eq!(filtered[0].name, "my-app");
9902 }
9903
9904 #[test]
9905 fn test_ecr_filter_input() {
9906 let mut app = test_app();
9907 app.current_service = Service::EcrRepositories;
9908 app.service_selected = true;
9909 app.mode = Mode::FilterInput;
9910
9911 app.handle_action(Action::FilterInput('t'));
9912 app.handle_action(Action::FilterInput('e'));
9913 app.handle_action(Action::FilterInput('s'));
9914 app.handle_action(Action::FilterInput('t'));
9915 assert_eq!(app.ecr_state.repositories.filter, "test");
9916
9917 app.handle_action(Action::FilterBackspace);
9918 assert_eq!(app.ecr_state.repositories.filter, "tes");
9919 }
9920
9921 #[test]
9922 fn test_iam_users_filter_input() {
9923 let mut app = test_app();
9924 app.current_service = Service::IamUsers;
9925 app.service_selected = true;
9926 app.mode = Mode::FilterInput;
9927
9928 app.handle_action(Action::FilterInput('a'));
9929 app.handle_action(Action::FilterInput('d'));
9930 app.handle_action(Action::FilterInput('m'));
9931 app.handle_action(Action::FilterInput('i'));
9932 app.handle_action(Action::FilterInput('n'));
9933 assert_eq!(app.iam_state.users.filter, "admin");
9934
9935 app.handle_action(Action::FilterBackspace);
9936 assert_eq!(app.iam_state.users.filter, "admi");
9937 }
9938
9939 #[test]
9940 fn test_iam_policies_filter_input() {
9941 let mut app = test_app();
9942 app.current_service = Service::IamUsers;
9943 app.service_selected = true;
9944 app.iam_state.current_user = Some("testuser".to_string());
9945 app.mode = Mode::FilterInput;
9946
9947 app.handle_action(Action::FilterInput('r'));
9948 app.handle_action(Action::FilterInput('e'));
9949 app.handle_action(Action::FilterInput('a'));
9950 app.handle_action(Action::FilterInput('d'));
9951 assert_eq!(app.iam_state.policies.filter, "read");
9952
9953 app.handle_action(Action::FilterBackspace);
9954 assert_eq!(app.iam_state.policies.filter, "rea");
9955 }
9956
9957 #[test]
9958 fn test_iam_start_filter() {
9959 let mut app = test_app();
9960 app.current_service = Service::IamUsers;
9961 app.service_selected = true;
9962 app.mode = Mode::Normal;
9963
9964 app.handle_action(Action::StartFilter);
9965 assert_eq!(app.mode, Mode::FilterInput);
9966 }
9967
9968 #[test]
9969 fn test_iam_roles_filter_input() {
9970 let mut app = test_app();
9971 app.current_service = Service::IamRoles;
9972 app.service_selected = true;
9973 app.mode = Mode::FilterInput;
9974
9975 app.handle_action(Action::FilterInput('a'));
9976 app.handle_action(Action::FilterInput('d'));
9977 app.handle_action(Action::FilterInput('m'));
9978 app.handle_action(Action::FilterInput('i'));
9979 app.handle_action(Action::FilterInput('n'));
9980 assert_eq!(app.iam_state.roles.filter, "admin");
9981
9982 app.handle_action(Action::FilterBackspace);
9983 assert_eq!(app.iam_state.roles.filter, "admi");
9984 }
9985
9986 #[test]
9987 fn test_iam_roles_start_filter() {
9988 let mut app = test_app();
9989 app.current_service = Service::IamRoles;
9990 app.service_selected = true;
9991 app.mode = Mode::Normal;
9992
9993 app.handle_action(Action::StartFilter);
9994 assert_eq!(app.mode, Mode::FilterInput);
9995 }
9996
9997 #[test]
9998 fn test_iam_roles_navigation() {
9999 let mut app = test_app();
10000 app.current_service = Service::IamRoles;
10001 app.service_selected = true;
10002 app.mode = Mode::Normal;
10003 app.iam_state.roles.items = (0..10)
10004 .map(|i| crate::iam::IamRole {
10005 role_name: format!("role{}", i),
10006 path: "/".to_string(),
10007 trusted_entities: String::new(),
10008 last_activity: String::new(),
10009 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
10010 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10011 description: String::new(),
10012 max_session_duration: Some(3600),
10013 })
10014 .collect();
10015
10016 assert_eq!(app.iam_state.roles.selected, 0);
10017
10018 app.handle_action(Action::NextItem);
10019 assert_eq!(app.iam_state.roles.selected, 1);
10020
10021 app.handle_action(Action::NextItem);
10022 assert_eq!(app.iam_state.roles.selected, 2);
10023
10024 app.handle_action(Action::PrevItem);
10025 assert_eq!(app.iam_state.roles.selected, 1);
10026 }
10027
10028 #[test]
10029 fn test_iam_roles_page_hotkey() {
10030 let mut app = test_app();
10031 app.current_service = Service::IamRoles;
10032 app.service_selected = true;
10033 app.mode = Mode::Normal;
10034 app.iam_state.roles.page_size = PageSize::Ten;
10035 app.iam_state.roles.items = (0..100)
10036 .map(|i| crate::iam::IamRole {
10037 role_name: format!("role{}", i),
10038 path: "/".to_string(),
10039 trusted_entities: String::new(),
10040 last_activity: String::new(),
10041 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
10042 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10043 description: String::new(),
10044 max_session_duration: Some(3600),
10045 })
10046 .collect();
10047
10048 app.handle_action(Action::FilterInput('2'));
10049 app.handle_action(Action::OpenColumnSelector);
10050 assert_eq!(app.iam_state.roles.selected, 10); }
10052
10053 #[test]
10054 fn test_iam_users_page_hotkey() {
10055 let mut app = test_app();
10056 app.current_service = Service::IamUsers;
10057 app.service_selected = true;
10058 app.mode = Mode::Normal;
10059 app.iam_state.users.page_size = PageSize::Ten;
10060 app.iam_state.users.items = (0..100)
10061 .map(|i| crate::iam::IamUser {
10062 user_name: format!("user{}", i),
10063 path: "/".to_string(),
10064 groups: String::new(),
10065 last_activity: String::new(),
10066 mfa: String::new(),
10067 password_age: String::new(),
10068 console_last_sign_in: String::new(),
10069 access_key_id: String::new(),
10070 active_key_age: String::new(),
10071 access_key_last_used: String::new(),
10072 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
10073 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10074 console_access: String::new(),
10075 signing_certs: String::new(),
10076 })
10077 .collect();
10078
10079 app.handle_action(Action::FilterInput('3'));
10080 app.handle_action(Action::OpenColumnSelector);
10081 assert_eq!(app.iam_state.users.selected, 20); }
10083
10084 #[test]
10085 fn test_ecr_scroll_navigation() {
10086 let mut app = test_app();
10087 app.current_service = Service::EcrRepositories;
10088 app.service_selected = true;
10089 app.ecr_state.repositories.items = (0..20)
10090 .map(|i| EcrRepository {
10091 name: format!("repo{}", i),
10092 uri: format!("uri{}", i),
10093 created_at: "2023-01-01".to_string(),
10094 tag_immutability: "MUTABLE".to_string(),
10095 encryption_type: "AES256".to_string(),
10096 })
10097 .collect();
10098
10099 app.handle_action(Action::ScrollDown);
10100 assert_eq!(app.ecr_state.repositories.selected, 10);
10101
10102 app.handle_action(Action::ScrollUp);
10103 assert_eq!(app.ecr_state.repositories.selected, 0);
10104 }
10105
10106 #[test]
10107 fn test_ecr_tab_switching_triggers_reload() {
10108 let mut app = test_app();
10109 app.current_service = Service::EcrRepositories;
10110 app.service_selected = true;
10111 app.ecr_state.tab = EcrTab::Private;
10112 app.ecr_state.repositories.loading = false;
10113 app.ecr_state.repositories.items = vec![EcrRepository {
10114 name: "private-repo".to_string(),
10115 uri: "uri".to_string(),
10116 created_at: "2023-01-01".to_string(),
10117 tag_immutability: "MUTABLE".to_string(),
10118 encryption_type: "AES256".to_string(),
10119 }];
10120
10121 app.handle_action(Action::NextDetailTab);
10122 assert_eq!(app.ecr_state.tab, EcrTab::Public);
10123 assert!(app.ecr_state.repositories.loading);
10124 assert_eq!(app.ecr_state.repositories.selected, 0);
10125 }
10126
10127 #[test]
10128 fn test_ecr_tab_cycles_between_private_and_public() {
10129 let mut app = test_app();
10130 app.current_service = Service::EcrRepositories;
10131 app.service_selected = true;
10132 app.ecr_state.tab = EcrTab::Private;
10133
10134 app.handle_action(Action::NextDetailTab);
10135 assert_eq!(app.ecr_state.tab, EcrTab::Public);
10136
10137 app.handle_action(Action::NextDetailTab);
10138 assert_eq!(app.ecr_state.tab, EcrTab::Private);
10139 }
10140
10141 #[test]
10142 fn test_page_size_values() {
10143 assert_eq!(PageSize::Ten.value(), 10);
10144 assert_eq!(PageSize::TwentyFive.value(), 25);
10145 assert_eq!(PageSize::Fifty.value(), 50);
10146 assert_eq!(PageSize::OneHundred.value(), 100);
10147 }
10148
10149 #[test]
10150 fn test_page_size_next() {
10151 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
10152 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
10153 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
10154 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
10155 }
10156
10157 #[test]
10158 fn test_ecr_enter_drills_into_repository() {
10159 let mut app = test_app();
10160 app.current_service = Service::EcrRepositories;
10161 app.service_selected = true;
10162 app.mode = Mode::Normal;
10163 app.ecr_state.repositories.items = vec![EcrRepository {
10164 name: "my-repo".to_string(),
10165 uri: "uri".to_string(),
10166 created_at: "2023-01-01".to_string(),
10167 tag_immutability: "MUTABLE".to_string(),
10168 encryption_type: "AES256".to_string(),
10169 }];
10170
10171 app.handle_action(Action::Select);
10172 assert_eq!(
10173 app.ecr_state.current_repository,
10174 Some("my-repo".to_string())
10175 );
10176 assert!(app.ecr_state.repositories.loading);
10177 }
10178
10179 #[test]
10180 fn test_ecr_repository_expansion() {
10181 let mut app = test_app();
10182 app.current_service = Service::EcrRepositories;
10183 app.service_selected = true;
10184 app.ecr_state.repositories.items = vec![EcrRepository {
10185 name: "my-repo".to_string(),
10186 uri: "uri".to_string(),
10187 created_at: "2023-01-01".to_string(),
10188 tag_immutability: "MUTABLE".to_string(),
10189 encryption_type: "AES256".to_string(),
10190 }];
10191 app.ecr_state.repositories.selected = 0;
10192
10193 assert_eq!(app.ecr_state.repositories.expanded_item, None);
10194
10195 app.handle_action(Action::NextPane);
10196 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
10197
10198 app.handle_action(Action::PrevPane);
10199 assert_eq!(app.ecr_state.repositories.expanded_item, None);
10200 }
10201
10202 #[test]
10203 fn test_ecr_ctrl_d_scrolls_down() {
10204 let mut app = test_app();
10205 app.current_service = Service::EcrRepositories;
10206 app.service_selected = true;
10207 app.mode = Mode::Normal;
10208 app.ecr_state.repositories.items = (0..30)
10209 .map(|i| EcrRepository {
10210 name: format!("repo{}", i),
10211 uri: format!("uri{}", i),
10212 created_at: "2023-01-01".to_string(),
10213 tag_immutability: "MUTABLE".to_string(),
10214 encryption_type: "AES256".to_string(),
10215 })
10216 .collect();
10217 app.ecr_state.repositories.selected = 0;
10218
10219 app.handle_action(Action::PageDown);
10220 assert_eq!(app.ecr_state.repositories.selected, 10);
10221 }
10222
10223 #[test]
10224 fn test_ecr_ctrl_u_scrolls_up() {
10225 let mut app = test_app();
10226 app.current_service = Service::EcrRepositories;
10227 app.service_selected = true;
10228 app.mode = Mode::Normal;
10229 app.ecr_state.repositories.items = (0..30)
10230 .map(|i| EcrRepository {
10231 name: format!("repo{}", i),
10232 uri: format!("uri{}", i),
10233 created_at: "2023-01-01".to_string(),
10234 tag_immutability: "MUTABLE".to_string(),
10235 encryption_type: "AES256".to_string(),
10236 })
10237 .collect();
10238 app.ecr_state.repositories.selected = 15;
10239
10240 app.handle_action(Action::PageUp);
10241 assert_eq!(app.ecr_state.repositories.selected, 5);
10242 }
10243
10244 #[test]
10245 fn test_ecr_images_ctrl_d_scrolls_down() {
10246 let mut app = test_app();
10247 app.current_service = Service::EcrRepositories;
10248 app.service_selected = true;
10249 app.mode = Mode::Normal;
10250 app.ecr_state.current_repository = Some("repo".to_string());
10251 app.ecr_state.images.items = (0..30)
10252 .map(|i| EcrImage {
10253 tag: format!("tag{}", i),
10254 artifact_type: "container".to_string(),
10255 pushed_at: "2023-01-01T12:00:00Z".to_string(),
10256 size_bytes: 104857600,
10257 uri: format!("uri{}", i),
10258 digest: format!("sha256:{}", i),
10259 last_pull_time: String::new(),
10260 })
10261 .collect();
10262 app.ecr_state.images.selected = 0;
10263
10264 app.handle_action(Action::PageDown);
10265 assert_eq!(app.ecr_state.images.selected, 10);
10266 }
10267
10268 #[test]
10269 fn test_ecr_esc_goes_back_from_images_to_repos() {
10270 let mut app = test_app();
10271 app.current_service = Service::EcrRepositories;
10272 app.service_selected = true;
10273 app.mode = Mode::Normal;
10274 app.ecr_state.current_repository = Some("my-repo".to_string());
10275 app.ecr_state.images.items = vec![EcrImage {
10276 tag: "latest".to_string(),
10277 artifact_type: "container".to_string(),
10278 pushed_at: "2023-01-01T12:00:00Z".to_string(),
10279 size_bytes: 104857600,
10280 uri: "uri".to_string(),
10281 digest: "sha256:abc".to_string(),
10282 last_pull_time: String::new(),
10283 }];
10284
10285 app.handle_action(Action::GoBack);
10286 assert_eq!(app.ecr_state.current_repository, None);
10287 assert!(app.ecr_state.images.items.is_empty());
10288 }
10289
10290 #[test]
10291 fn test_ecr_esc_collapses_expanded_image_first() {
10292 let mut app = test_app();
10293 app.current_service = Service::EcrRepositories;
10294 app.service_selected = true;
10295 app.mode = Mode::Normal;
10296 app.ecr_state.current_repository = Some("my-repo".to_string());
10297 app.ecr_state.images.expanded_item = Some(0);
10298
10299 app.handle_action(Action::GoBack);
10300 assert_eq!(app.ecr_state.images.expanded_item, None);
10301 assert_eq!(
10302 app.ecr_state.current_repository,
10303 Some("my-repo".to_string())
10304 );
10305 }
10306
10307 #[test]
10308 fn test_pagination_with_lowercase_p() {
10309 let mut app = test_app();
10310 app.current_service = Service::EcrRepositories;
10311 app.service_selected = true;
10312 app.mode = Mode::Normal;
10313 app.ecr_state.repositories.items = (0..100)
10314 .map(|i| EcrRepository {
10315 name: format!("repo{}", i),
10316 uri: format!("uri{}", i),
10317 created_at: "2023-01-01".to_string(),
10318 tag_immutability: "MUTABLE".to_string(),
10319 encryption_type: "AES256".to_string(),
10320 })
10321 .collect();
10322
10323 app.handle_action(Action::FilterInput('2'));
10325 assert_eq!(app.page_input, "2");
10326
10327 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
10331
10332 #[test]
10333 fn test_lowercase_p_without_number_opens_preferences() {
10334 let mut app = test_app();
10335 app.current_service = Service::EcrRepositories;
10336 app.service_selected = true;
10337 app.mode = Mode::Normal;
10338
10339 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
10341 }
10342
10343 #[test]
10344 fn test_ctrl_o_generates_correct_console_url() {
10345 let mut app = test_app();
10346 app.current_service = Service::EcrRepositories;
10347 app.service_selected = true;
10348 app.mode = Mode::Normal;
10349 app.config.account_id = "123456789012".to_string();
10350
10351 let url = app.get_console_url();
10353 assert!(url.contains("ecr/private-registry/repositories"));
10354 assert!(url.contains("region=us-east-1"));
10355
10356 app.ecr_state.current_repository = Some("my-repo".to_string());
10358 let url = app.get_console_url();
10359 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
10360 assert!(url.contains("region=us-east-1"));
10361 }
10362
10363 #[test]
10364 fn test_page_input_display_and_reset() {
10365 let mut app = test_app();
10366 app.current_service = Service::EcrRepositories;
10367 app.service_selected = true;
10368 app.mode = Mode::Normal;
10369 app.ecr_state.repositories.items = (0..100)
10370 .map(|i| EcrRepository {
10371 name: format!("repo{}", i),
10372 uri: format!("uri{}", i),
10373 created_at: "2023-01-01".to_string(),
10374 tag_immutability: "MUTABLE".to_string(),
10375 encryption_type: "AES256".to_string(),
10376 })
10377 .collect();
10378
10379 app.handle_action(Action::FilterInput('2'));
10381 assert_eq!(app.page_input, "2");
10382
10383 app.handle_action(Action::OpenColumnSelector);
10385 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
10388
10389 #[test]
10390 fn test_page_navigation_updates_scroll_offset_for_cfn() {
10391 let mut app = test_app();
10392 app.current_service = Service::CloudFormationStacks;
10393 app.service_selected = true;
10394 app.mode = Mode::Normal;
10395 app.cfn_state.table.items = (0..100)
10396 .map(|i| crate::cfn::Stack {
10397 name: format!("stack-{}", i),
10398 stack_id: format!(
10399 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
10400 i
10401 ),
10402 status: "CREATE_COMPLETE".to_string(),
10403 created_time: "2023-01-01T00:00:00Z".to_string(),
10404 updated_time: "2023-01-01T00:00:00Z".to_string(),
10405 deleted_time: String::new(),
10406 drift_status: "IN_SYNC".to_string(),
10407 last_drift_check_time: String::new(),
10408 status_reason: String::new(),
10409 description: String::new(),
10410 detailed_status: String::new(),
10411 root_stack: String::new(),
10412 parent_stack: String::new(),
10413 termination_protection: false,
10414 iam_role: String::new(),
10415 tags: vec![],
10416 stack_policy: String::new(),
10417 rollback_monitoring_time: String::new(),
10418 rollback_alarms: vec![],
10419 notification_arns: vec![],
10420 })
10421 .collect();
10422
10423 app.handle_action(Action::FilterInput('2'));
10425 assert_eq!(app.page_input, "2");
10426
10427 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
10432 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
10434 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
10435
10436 let current_page = app.cfn_state.table.scroll_offset / page_size;
10438 assert_eq!(
10439 current_page, 1,
10440 "2p should go to page 2 (0-indexed as 1), not page 3"
10441 ); }
10443
10444 #[test]
10445 fn test_3p_goes_to_page_3_not_page_5() {
10446 let mut app = test_app();
10447 app.current_service = Service::CloudFormationStacks;
10448 app.service_selected = true;
10449 app.mode = Mode::Normal;
10450 app.cfn_state.table.items = (0..200)
10451 .map(|i| crate::cfn::Stack {
10452 name: format!("stack-{}", i),
10453 stack_id: format!(
10454 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
10455 i
10456 ),
10457 status: "CREATE_COMPLETE".to_string(),
10458 created_time: "2023-01-01T00:00:00Z".to_string(),
10459 updated_time: "2023-01-01T00:00:00Z".to_string(),
10460 deleted_time: String::new(),
10461 drift_status: "IN_SYNC".to_string(),
10462 last_drift_check_time: String::new(),
10463 status_reason: String::new(),
10464 description: String::new(),
10465 detailed_status: String::new(),
10466 root_stack: String::new(),
10467 parent_stack: String::new(),
10468 termination_protection: false,
10469 iam_role: String::new(),
10470 tags: vec![],
10471 stack_policy: String::new(),
10472 rollback_monitoring_time: String::new(),
10473 rollback_alarms: vec![],
10474 notification_arns: vec![],
10475 })
10476 .collect();
10477
10478 app.handle_action(Action::FilterInput('3'));
10480 app.handle_action(Action::OpenColumnSelector);
10481
10482 let page_size = app.cfn_state.table.page_size.value();
10483 let current_page = app.cfn_state.table.scroll_offset / page_size;
10484 assert_eq!(
10485 current_page, 2,
10486 "3p should go to page 3 (0-indexed as 2), not page 5"
10487 );
10488 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
10489 }
10490
10491 #[test]
10492 fn test_log_streams_page_navigation_uses_correct_page_size() {
10493 let mut app = test_app();
10494 app.current_service = Service::CloudWatchLogGroups;
10495 app.view_mode = ViewMode::Detail;
10496 app.service_selected = true;
10497 app.mode = Mode::Normal;
10498 app.log_groups_state.log_streams = (0..100)
10499 .map(|i| LogStream {
10500 name: format!("stream-{}", i),
10501 creation_time: None,
10502 last_event_time: None,
10503 })
10504 .collect();
10505
10506 app.handle_action(Action::FilterInput('2'));
10508 app.handle_action(Action::OpenColumnSelector);
10509
10510 assert_eq!(app.log_groups_state.selected_stream, 20);
10512
10513 let page_size = 20;
10515 let current_page = app.log_groups_state.selected_stream / page_size;
10516 assert_eq!(
10517 current_page, 1,
10518 "2p should go to page 2 (0-indexed as 1), not page 3"
10519 );
10520 }
10521
10522 #[test]
10523 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
10524 let mut app = test_app();
10525 app.current_service = Service::EcrRepositories;
10526 app.service_selected = true;
10527 app.mode = Mode::Normal;
10528 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
10530 .map(|i| EcrRepository {
10531 name: format!("repo{}", i),
10532 uri: format!("uri{}", i),
10533 created_at: "2023-01-01".to_string(),
10534 tag_immutability: "MUTABLE".to_string(),
10535 encryption_type: "AES256".to_string(),
10536 })
10537 .collect();
10538
10539 app.handle_action(Action::FilterInput('3'));
10541 app.handle_action(Action::OpenColumnSelector);
10542
10543 assert_eq!(app.ecr_state.repositories.selected, 50);
10545
10546 let page_size = app.ecr_state.repositories.page_size.value();
10547 let current_page = app.ecr_state.repositories.selected / page_size;
10548 assert_eq!(
10549 current_page, 2,
10550 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
10551 );
10552 }
10553
10554 #[test]
10555 fn test_page_navigation_updates_scroll_offset_for_alarms() {
10556 let mut app = test_app();
10557 app.current_service = Service::CloudWatchAlarms;
10558 app.service_selected = true;
10559 app.mode = Mode::Normal;
10560 app.alarms_state.table.items = (0..100)
10561 .map(|i| crate::cw::alarms::Alarm {
10562 name: format!("alarm-{}", i),
10563 state: "OK".to_string(),
10564 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
10565 description: String::new(),
10566 metric_name: "CPUUtilization".to_string(),
10567 namespace: "AWS/EC2".to_string(),
10568 statistic: "Average".to_string(),
10569 period: 300,
10570 comparison_operator: "GreaterThanThreshold".to_string(),
10571 threshold: 80.0,
10572 actions_enabled: true,
10573 state_reason: String::new(),
10574 resource: String::new(),
10575 dimensions: String::new(),
10576 expression: String::new(),
10577 alarm_type: "MetricAlarm".to_string(),
10578 cross_account: String::new(),
10579 })
10580 .collect();
10581
10582 app.handle_action(Action::FilterInput('2'));
10584 app.handle_action(Action::OpenColumnSelector);
10585
10586 let page_size = app.alarms_state.table.page_size.value();
10588 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
10590 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
10591 }
10592
10593 #[test]
10594 fn test_ecr_pagination_with_65_repos() {
10595 let mut app = test_app();
10596 app.current_service = Service::EcrRepositories;
10597 app.service_selected = true;
10598 app.mode = Mode::Normal;
10599 app.ecr_state.repositories.items = (0..65)
10600 .map(|i| EcrRepository {
10601 name: format!("repo{:02}", i),
10602 uri: format!("uri{}", i),
10603 created_at: "2023-01-01".to_string(),
10604 tag_immutability: "MUTABLE".to_string(),
10605 encryption_type: "AES256".to_string(),
10606 })
10607 .collect();
10608
10609 assert_eq!(app.ecr_state.repositories.selected, 0);
10611 let page_size = 50;
10612 let current_page = app.ecr_state.repositories.selected / page_size;
10613 assert_eq!(current_page, 0);
10614
10615 app.handle_action(Action::FilterInput('2'));
10617 app.handle_action(Action::OpenColumnSelector);
10618 assert_eq!(app.ecr_state.repositories.selected, 50);
10619
10620 let current_page = app.ecr_state.repositories.selected / page_size;
10622 assert_eq!(current_page, 1);
10623 }
10624
10625 #[test]
10626 fn test_ecr_repos_input_focus_tab_cycling() {
10627 let mut app = test_app();
10628 app.current_service = Service::EcrRepositories;
10629 app.service_selected = true;
10630 app.mode = Mode::FilterInput;
10631 app.ecr_state.input_focus = InputFocus::Filter;
10632
10633 app.handle_action(Action::NextFilterFocus);
10635 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10636
10637 app.handle_action(Action::NextFilterFocus);
10639 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10640
10641 app.handle_action(Action::PrevFilterFocus);
10643 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10644
10645 app.handle_action(Action::PrevFilterFocus);
10647 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10648 }
10649
10650 #[test]
10651 fn test_ecr_images_column_toggle_not_off_by_one() {
10652 use crate::ecr::image::Column as ImageColumn;
10653 let mut app = test_app();
10654 app.current_service = Service::EcrRepositories;
10655 app.service_selected = true;
10656 app.mode = Mode::ColumnSelector;
10657 app.ecr_state.current_repository = Some("test-repo".to_string());
10658
10659 app.ecr_image_visible_column_ids = ImageColumn::ids();
10661 let initial_count = app.ecr_image_visible_column_ids.len();
10662
10663 app.column_selector_index = 0;
10665 app.handle_action(Action::ToggleColumn);
10666
10667 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
10669 assert!(!app
10670 .ecr_image_visible_column_ids
10671 .contains(&ImageColumn::Tag.id()));
10672
10673 app.handle_action(Action::ToggleColumn);
10675 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
10676 assert!(app
10677 .ecr_image_visible_column_ids
10678 .contains(&ImageColumn::Tag.id()));
10679 }
10680
10681 #[test]
10682 fn test_ecr_repos_column_toggle_works() {
10683 let mut app = test_app();
10684 app.current_service = Service::EcrRepositories;
10685 app.service_selected = true;
10686 app.mode = Mode::ColumnSelector;
10687 app.ecr_state.current_repository = None;
10688
10689 app.ecr_repo_visible_column_ids = EcrColumn::ids();
10691 let initial_count = app.ecr_repo_visible_column_ids.len();
10692
10693 app.column_selector_index = 0;
10695 app.handle_action(Action::ToggleColumn);
10696
10697 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
10699 assert!(!app
10700 .ecr_repo_visible_column_ids
10701 .contains(&EcrColumn::Name.id()));
10702
10703 app.handle_action(Action::ToggleColumn);
10705 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
10706 assert!(app
10707 .ecr_repo_visible_column_ids
10708 .contains(&EcrColumn::Name.id()));
10709 }
10710
10711 #[test]
10712 fn test_ecr_repos_pagination_left_right_navigation() {
10713 use crate::ecr::repo::Repository as EcrRepository;
10714 let mut app = test_app();
10715 app.current_service = Service::EcrRepositories;
10716 app.service_selected = true;
10717 app.mode = Mode::FilterInput;
10718 app.ecr_state.input_focus = InputFocus::Pagination;
10719
10720 app.ecr_state.repositories.items = (0..150)
10722 .map(|i| EcrRepository {
10723 name: format!("repo{:03}", i),
10724 uri: format!("uri{}", i),
10725 created_at: "2023-01-01".to_string(),
10726 tag_immutability: "MUTABLE".to_string(),
10727 encryption_type: "AES256".to_string(),
10728 })
10729 .collect();
10730
10731 app.ecr_state.repositories.selected = 0;
10733 eprintln!(
10734 "Initial: selected={}, focus={:?}, mode={:?}",
10735 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
10736 );
10737
10738 app.handle_action(Action::PageDown);
10740 eprintln!(
10741 "After PageDown: selected={}",
10742 app.ecr_state.repositories.selected
10743 );
10744 assert_eq!(app.ecr_state.repositories.selected, 50);
10745
10746 app.handle_action(Action::PageDown);
10748 eprintln!(
10749 "After 2nd PageDown: selected={}",
10750 app.ecr_state.repositories.selected
10751 );
10752 assert_eq!(app.ecr_state.repositories.selected, 100);
10753
10754 app.handle_action(Action::PageDown);
10756 eprintln!(
10757 "After 3rd PageDown: selected={}",
10758 app.ecr_state.repositories.selected
10759 );
10760 assert_eq!(app.ecr_state.repositories.selected, 100);
10761
10762 app.handle_action(Action::PageUp);
10764 eprintln!(
10765 "After PageUp: selected={}",
10766 app.ecr_state.repositories.selected
10767 );
10768 assert_eq!(app.ecr_state.repositories.selected, 50);
10769
10770 app.handle_action(Action::PageUp);
10772 eprintln!(
10773 "After 2nd PageUp: selected={}",
10774 app.ecr_state.repositories.selected
10775 );
10776 assert_eq!(app.ecr_state.repositories.selected, 0);
10777
10778 app.handle_action(Action::PageUp);
10780 eprintln!(
10781 "After 3rd PageUp: selected={}",
10782 app.ecr_state.repositories.selected
10783 );
10784 assert_eq!(app.ecr_state.repositories.selected, 0);
10785 }
10786
10787 #[test]
10788 fn test_ecr_repos_filter_input_when_input_focused() {
10789 use crate::ecr::repo::Repository as EcrRepository;
10790 let mut app = test_app();
10791 app.current_service = Service::EcrRepositories;
10792 app.service_selected = true;
10793 app.mode = Mode::FilterInput;
10794 app.ecr_state.input_focus = InputFocus::Filter;
10795
10796 app.ecr_state.repositories.items = vec![
10798 EcrRepository {
10799 name: "test-repo".to_string(),
10800 uri: "uri1".to_string(),
10801 created_at: "2023-01-01".to_string(),
10802 tag_immutability: "MUTABLE".to_string(),
10803 encryption_type: "AES256".to_string(),
10804 },
10805 EcrRepository {
10806 name: "prod-repo".to_string(),
10807 uri: "uri2".to_string(),
10808 created_at: "2023-01-01".to_string(),
10809 tag_immutability: "MUTABLE".to_string(),
10810 encryption_type: "AES256".to_string(),
10811 },
10812 ];
10813
10814 assert_eq!(app.ecr_state.repositories.filter, "");
10816 app.handle_action(Action::FilterInput('t'));
10817 assert_eq!(app.ecr_state.repositories.filter, "t");
10818 app.handle_action(Action::FilterInput('e'));
10819 assert_eq!(app.ecr_state.repositories.filter, "te");
10820 app.handle_action(Action::FilterInput('s'));
10821 assert_eq!(app.ecr_state.repositories.filter, "tes");
10822 app.handle_action(Action::FilterInput('t'));
10823 assert_eq!(app.ecr_state.repositories.filter, "test");
10824 }
10825
10826 #[test]
10827 fn test_ecr_repos_digit_input_when_pagination_focused() {
10828 use crate::ecr::repo::Repository as EcrRepository;
10829 let mut app = test_app();
10830 app.current_service = Service::EcrRepositories;
10831 app.service_selected = true;
10832 app.mode = Mode::FilterInput;
10833 app.ecr_state.input_focus = InputFocus::Pagination;
10834
10835 app.ecr_state.repositories.items = vec![EcrRepository {
10837 name: "test-repo".to_string(),
10838 uri: "uri1".to_string(),
10839 created_at: "2023-01-01".to_string(),
10840 tag_immutability: "MUTABLE".to_string(),
10841 encryption_type: "AES256".to_string(),
10842 }];
10843
10844 assert_eq!(app.ecr_state.repositories.filter, "");
10846 assert_eq!(app.page_input, "");
10847 app.handle_action(Action::FilterInput('2'));
10848 assert_eq!(app.ecr_state.repositories.filter, "");
10849 assert_eq!(app.page_input, "2");
10850
10851 app.handle_action(Action::FilterInput('a'));
10853 assert_eq!(app.ecr_state.repositories.filter, "");
10854 assert_eq!(app.page_input, "2");
10855 }
10856
10857 #[test]
10858 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
10859 use crate::ecr::repo::Repository as EcrRepository;
10860 let mut app = test_app();
10861 app.current_service = Service::EcrRepositories;
10862 app.service_selected = true;
10863 app.mode = Mode::FilterInput;
10864 app.ecr_state.input_focus = InputFocus::Filter;
10865
10866 app.ecr_state.repositories.items = (0..150)
10868 .map(|i| EcrRepository {
10869 name: format!("repo{:03}", i),
10870 uri: format!("uri{}", i),
10871 created_at: "2023-01-01".to_string(),
10872 tag_immutability: "MUTABLE".to_string(),
10873 encryption_type: "AES256".to_string(),
10874 })
10875 .collect();
10876
10877 app.ecr_state.repositories.selected = 0;
10879
10880 app.handle_action(Action::PageDown);
10882 assert_eq!(
10883 app.ecr_state.repositories.selected, 10,
10884 "Should scroll down by 10"
10885 );
10886
10887 app.handle_action(Action::PageUp);
10888 assert_eq!(
10889 app.ecr_state.repositories.selected, 0,
10890 "Should scroll back up"
10891 );
10892 }
10893
10894 #[test]
10895 fn test_ecr_repos_pagination_control_actually_works() {
10896 use crate::ecr::repo::Repository as EcrRepository;
10897
10898 let mut app = test_app();
10900 app.current_service = Service::EcrRepositories;
10901 app.service_selected = true;
10902 app.mode = Mode::FilterInput;
10903 app.ecr_state.current_repository = None;
10904 app.ecr_state.input_focus = InputFocus::Pagination;
10905
10906 app.ecr_state.repositories.items = (0..100)
10908 .map(|i| EcrRepository {
10909 name: format!("repo{:03}", i),
10910 uri: format!("uri{}", i),
10911 created_at: "2023-01-01".to_string(),
10912 tag_immutability: "MUTABLE".to_string(),
10913 encryption_type: "AES256".to_string(),
10914 })
10915 .collect();
10916
10917 app.ecr_state.repositories.selected = 0;
10918
10919 assert_eq!(app.mode, Mode::FilterInput);
10921 assert_eq!(app.current_service, Service::EcrRepositories);
10922 assert_eq!(app.ecr_state.current_repository, None);
10923 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10924
10925 app.handle_action(Action::PageDown);
10927 assert_eq!(
10928 app.ecr_state.repositories.selected, 50,
10929 "PageDown should move to page 2"
10930 );
10931
10932 app.handle_action(Action::PageUp);
10933 assert_eq!(
10934 app.ecr_state.repositories.selected, 0,
10935 "PageUp should move back to page 1"
10936 );
10937 }
10938
10939 #[test]
10940 fn test_ecr_repos_start_filter_resets_focus_to_input() {
10941 let mut app = test_app();
10942 app.current_service = Service::EcrRepositories;
10943 app.service_selected = true;
10944 app.mode = Mode::Normal;
10945 app.ecr_state.current_repository = None;
10946
10947 app.ecr_state.input_focus = InputFocus::Pagination;
10949
10950 app.handle_action(Action::StartFilter);
10952
10953 assert_eq!(app.mode, Mode::FilterInput);
10955 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10956 }
10957
10958 #[test]
10959 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
10960 use crate::ecr::repo::Repository as EcrRepository;
10961
10962 let mut app = test_app();
10963 app.current_service = Service::EcrRepositories;
10964 app.service_selected = true;
10965 app.mode = Mode::Normal;
10966 app.ecr_state.current_repository = None;
10967
10968 app.ecr_state.repositories.items = (0..100)
10970 .map(|i| EcrRepository {
10971 name: format!("repo{:03}", i),
10972 uri: format!("uri{}", i),
10973 created_at: "2023-01-01".to_string(),
10974 tag_immutability: "MUTABLE".to_string(),
10975 encryption_type: "AES256".to_string(),
10976 })
10977 .collect();
10978
10979 app.ecr_state.repositories.selected = 0;
10980
10981 app.handle_action(Action::StartFilter);
10983 assert_eq!(app.mode, Mode::FilterInput);
10984 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10985
10986 app.handle_action(Action::NextFilterFocus);
10988 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10989
10990 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
10992 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
10993 app.handle_action(Action::PageDown);
10994 eprintln!(
10995 "After PageDown: selected={}",
10996 app.ecr_state.repositories.selected
10997 );
10998
10999 assert_eq!(
11001 app.ecr_state.repositories.selected, 50,
11002 "Right arrow should move to page 2"
11003 );
11004
11005 app.handle_action(Action::PageUp);
11007 assert_eq!(
11008 app.ecr_state.repositories.selected, 0,
11009 "Left arrow should move back to page 1"
11010 );
11011 }
11012
11013 #[test]
11014 fn test_service_picker_i_key_activates_filter() {
11015 let mut app = test_app();
11016
11017 assert_eq!(app.mode, Mode::ServicePicker);
11019 assert!(app.service_picker.filter.is_empty());
11020
11021 app.handle_action(Action::FilterInput('i'));
11023
11024 assert_eq!(app.mode, Mode::ServicePicker);
11026 assert_eq!(app.service_picker.filter, "i");
11027 }
11028
11029 #[test]
11030 fn test_service_picker_typing_filters_services() {
11031 let mut app = test_app();
11032
11033 assert_eq!(app.mode, Mode::ServicePicker);
11035
11036 app.handle_action(Action::FilterInput('s'));
11038 app.handle_action(Action::FilterInput('3'));
11039
11040 assert_eq!(app.service_picker.filter, "s3");
11041 assert_eq!(app.mode, Mode::ServicePicker);
11042 }
11043
11044 #[test]
11045 fn test_service_picker_resets_on_open() {
11046 let mut app = test_app();
11047
11048 app.service_selected = true;
11050 app.mode = Mode::Normal;
11051
11052 app.service_picker.filter = "previous".to_string();
11054 app.service_picker.selected = 5;
11055
11056 app.handle_action(Action::OpenSpaceMenu);
11058
11059 assert_eq!(app.mode, Mode::SpaceMenu);
11061 assert!(app.service_picker.filter.is_empty());
11062 assert_eq!(app.service_picker.selected, 0);
11063 }
11064
11065 #[test]
11066 fn test_no_pii_in_test_data() {
11067 let test_repo = EcrRepository {
11069 name: "test-repo".to_string(),
11070 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
11071 created_at: "2024-01-01".to_string(),
11072 tag_immutability: "MUTABLE".to_string(),
11073 encryption_type: "AES256".to_string(),
11074 };
11075
11076 assert!(test_repo.uri.starts_with("123456789012"));
11078 assert!(!test_repo.uri.contains("123456789013")); }
11080
11081 #[test]
11082 fn test_lambda_versions_tab_triggers_loading() {
11083 let mut app = test_app();
11084 app.current_service = Service::LambdaFunctions;
11085 app.service_selected = true;
11086
11087 app.lambda_state.current_function = Some("test-function".to_string());
11089 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11090
11091 assert!(app.lambda_state.version_table.items.is_empty());
11093
11094 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11096
11097 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11100 assert!(app.lambda_state.current_function.is_some());
11101 }
11102
11103 #[test]
11104 fn test_lambda_versions_navigation() {
11105 use crate::lambda::Version;
11106
11107 let mut app = test_app();
11108 app.current_service = Service::LambdaFunctions;
11109 app.service_selected = true;
11110 app.lambda_state.current_function = Some("test-function".to_string());
11111 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11112
11113 app.lambda_state.version_table.items = vec![
11115 Version {
11116 version: "3".to_string(),
11117 aliases: "prod".to_string(),
11118 description: "".to_string(),
11119 last_modified: "".to_string(),
11120 architecture: "X86_64".to_string(),
11121 },
11122 Version {
11123 version: "2".to_string(),
11124 aliases: "".to_string(),
11125 description: "".to_string(),
11126 last_modified: "".to_string(),
11127 architecture: "X86_64".to_string(),
11128 },
11129 Version {
11130 version: "1".to_string(),
11131 aliases: "".to_string(),
11132 description: "".to_string(),
11133 last_modified: "".to_string(),
11134 architecture: "X86_64".to_string(),
11135 },
11136 ];
11137
11138 assert_eq!(app.lambda_state.version_table.items.len(), 3);
11140 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
11141 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
11142
11143 app.lambda_state.version_table.selected = 1;
11145 assert_eq!(app.lambda_state.version_table.selected, 1);
11146 }
11147
11148 #[test]
11149 fn test_lambda_versions_with_aliases() {
11150 use crate::lambda::Version;
11151
11152 let version = Version {
11153 version: "35".to_string(),
11154 aliases: "prod, staging".to_string(),
11155 description: "Production version".to_string(),
11156 last_modified: "2024-01-01".to_string(),
11157 architecture: "X86_64".to_string(),
11158 };
11159
11160 assert_eq!(version.aliases, "prod, staging");
11161 assert!(!version.aliases.is_empty());
11162 }
11163
11164 #[test]
11165 fn test_lambda_versions_expansion() {
11166 use crate::lambda::Version;
11167
11168 let mut app = test_app();
11169 app.current_service = Service::LambdaFunctions;
11170 app.service_selected = true;
11171 app.lambda_state.current_function = Some("test-function".to_string());
11172 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11173
11174 app.lambda_state.version_table.items = vec![
11176 Version {
11177 version: "2".to_string(),
11178 aliases: "prod".to_string(),
11179 description: "Production".to_string(),
11180 last_modified: "2024-01-01".to_string(),
11181 architecture: "X86_64".to_string(),
11182 },
11183 Version {
11184 version: "1".to_string(),
11185 aliases: "".to_string(),
11186 description: "".to_string(),
11187 last_modified: "2024-01-01".to_string(),
11188 architecture: "Arm64".to_string(),
11189 },
11190 ];
11191
11192 app.lambda_state.version_table.selected = 0;
11193
11194 app.lambda_state.version_table.expanded_item = Some(0);
11196 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11197
11198 app.lambda_state.version_table.selected = 1;
11200 app.lambda_state.version_table.expanded_item = Some(1);
11201 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
11202 }
11203
11204 #[test]
11205 fn test_lambda_versions_page_navigation() {
11206 use crate::lambda::Version;
11207
11208 let mut app = test_app();
11209 app.current_service = Service::LambdaFunctions;
11210 app.service_selected = true;
11211 app.lambda_state.current_function = Some("test-function".to_string());
11212 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11213
11214 app.lambda_state.version_table.items = (1..=30)
11216 .map(|i| Version {
11217 version: i.to_string(),
11218 aliases: "".to_string(),
11219 description: "".to_string(),
11220 last_modified: "".to_string(),
11221 architecture: "X86_64".to_string(),
11222 })
11223 .collect();
11224
11225 app.lambda_state.version_table.page_size = PageSize::Ten;
11226 app.lambda_state.version_table.selected = 0;
11227
11228 app.page_input = "2".to_string();
11230 app.handle_action(Action::OpenColumnSelector);
11231
11232 assert_eq!(app.lambda_state.version_table.selected, 10);
11234 }
11235
11236 #[test]
11237 fn test_lambda_versions_pagination_arrow_keys() {
11238 use crate::lambda::Version;
11239
11240 let mut app = test_app();
11241 app.current_service = Service::LambdaFunctions;
11242 app.service_selected = true;
11243 app.lambda_state.current_function = Some("test-function".to_string());
11244 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11245 app.mode = Mode::FilterInput;
11246 app.lambda_state.version_input_focus = InputFocus::Pagination;
11247
11248 app.lambda_state.version_table.items = (1..=30)
11250 .map(|i| Version {
11251 version: i.to_string(),
11252 aliases: "".to_string(),
11253 description: "".to_string(),
11254 last_modified: "".to_string(),
11255 architecture: "X86_64".to_string(),
11256 })
11257 .collect();
11258
11259 app.lambda_state.version_table.page_size = PageSize::Ten;
11260 app.lambda_state.version_table.selected = 0;
11261
11262 app.handle_action(Action::PageDown);
11264 assert_eq!(app.lambda_state.version_table.selected, 10);
11265
11266 app.handle_action(Action::PageUp);
11268 assert_eq!(app.lambda_state.version_table.selected, 0);
11269 }
11270
11271 #[test]
11272 fn test_lambda_versions_page_input_in_filter_mode() {
11273 use crate::lambda::Version;
11274
11275 let mut app = test_app();
11276 app.current_service = Service::LambdaFunctions;
11277 app.service_selected = true;
11278 app.lambda_state.current_function = Some("test-function".to_string());
11279 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11280 app.mode = Mode::FilterInput;
11281 app.lambda_state.version_input_focus = InputFocus::Pagination;
11282
11283 app.lambda_state.version_table.items = (1..=30)
11285 .map(|i| Version {
11286 version: i.to_string(),
11287 aliases: "".to_string(),
11288 description: "".to_string(),
11289 last_modified: "".to_string(),
11290 architecture: "X86_64".to_string(),
11291 })
11292 .collect();
11293
11294 app.lambda_state.version_table.page_size = PageSize::Ten;
11295 app.lambda_state.version_table.selected = 0;
11296
11297 app.handle_action(Action::FilterInput('2'));
11299 assert_eq!(app.page_input, "2");
11300 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
11304 assert_eq!(app.lambda_state.version_table.selected, 10);
11305 assert_eq!(app.page_input, ""); }
11307
11308 #[test]
11309 fn test_lambda_versions_filter_input() {
11310 use crate::lambda::Version;
11311
11312 let mut app = test_app();
11313 app.current_service = Service::LambdaFunctions;
11314 app.service_selected = true;
11315 app.lambda_state.current_function = Some("test-function".to_string());
11316 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11317 app.mode = Mode::FilterInput;
11318 app.lambda_state.version_input_focus = InputFocus::Filter;
11319
11320 app.lambda_state.version_table.items = vec![
11322 Version {
11323 version: "1".to_string(),
11324 aliases: "prod".to_string(),
11325 description: "Production".to_string(),
11326 last_modified: "".to_string(),
11327 architecture: "X86_64".to_string(),
11328 },
11329 Version {
11330 version: "2".to_string(),
11331 aliases: "staging".to_string(),
11332 description: "Staging".to_string(),
11333 last_modified: "".to_string(),
11334 architecture: "X86_64".to_string(),
11335 },
11336 ];
11337
11338 app.handle_action(Action::FilterInput('p'));
11340 app.handle_action(Action::FilterInput('r'));
11341 app.handle_action(Action::FilterInput('o'));
11342 app.handle_action(Action::FilterInput('d'));
11343 assert_eq!(app.lambda_state.version_table.filter, "prod");
11344
11345 app.handle_action(Action::FilterBackspace);
11347 assert_eq!(app.lambda_state.version_table.filter, "pro");
11348 }
11349
11350 #[test]
11351 fn test_lambda_aliases_table_expansion() {
11352 use crate::lambda::Alias;
11353
11354 let mut app = test_app();
11355 app.current_service = Service::LambdaFunctions;
11356 app.service_selected = true;
11357 app.lambda_state.current_function = Some("test-function".to_string());
11358 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11359 app.mode = Mode::Normal;
11360
11361 app.lambda_state.alias_table.items = vec![
11362 Alias {
11363 name: "prod".to_string(),
11364 versions: "1".to_string(),
11365 description: "Production alias".to_string(),
11366 },
11367 Alias {
11368 name: "staging".to_string(),
11369 versions: "2".to_string(),
11370 description: "Staging alias".to_string(),
11371 },
11372 ];
11373
11374 app.lambda_state.alias_table.selected = 0;
11375
11376 app.handle_action(Action::Select);
11378 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
11379 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11380
11381 app.handle_action(Action::GoBack);
11383 assert_eq!(app.lambda_state.current_alias, None);
11384 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11385
11386 app.lambda_state.alias_table.selected = 1;
11388 app.handle_action(Action::Select);
11389 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
11390 }
11391
11392 #[test]
11393 fn test_lambda_versions_arrow_key_expansion() {
11394 use crate::lambda::Version;
11395
11396 let mut app = test_app();
11397 app.current_service = Service::LambdaFunctions;
11398 app.service_selected = true;
11399 app.lambda_state.current_function = Some("test-function".to_string());
11400 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11401 app.mode = Mode::Normal;
11402
11403 app.lambda_state.version_table.items = vec![Version {
11404 version: "1".to_string(),
11405 aliases: "prod".to_string(),
11406 description: "Production".to_string(),
11407 last_modified: "2024-01-01".to_string(),
11408 architecture: "X86_64".to_string(),
11409 }];
11410
11411 app.lambda_state.version_table.selected = 0;
11412
11413 app.handle_action(Action::NextPane);
11415 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11416
11417 app.handle_action(Action::PrevPane);
11419 assert_eq!(app.lambda_state.version_table.expanded_item, None);
11420 }
11421
11422 #[test]
11423 fn test_lambda_version_detail_view() {
11424 use crate::lambda::Function;
11425
11426 let mut app = test_app();
11427 app.current_service = Service::LambdaFunctions;
11428 app.service_selected = true;
11429 app.lambda_state.current_function = Some("test-function".to_string());
11430 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11431 app.mode = Mode::Normal;
11432
11433 app.lambda_state.table.items = vec![Function {
11434 name: "test-function".to_string(),
11435 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11436 application: None,
11437 description: "Test".to_string(),
11438 package_type: "Zip".to_string(),
11439 runtime: "python3.12".to_string(),
11440 architecture: "X86_64".to_string(),
11441 code_size: 1024,
11442 code_sha256: "hash".to_string(),
11443 memory_mb: 128,
11444 timeout_seconds: 30,
11445 last_modified: "2024-01-01".to_string(),
11446 layers: vec![],
11447 }];
11448
11449 app.lambda_state.version_table.items = vec![crate::lambda::Version {
11450 version: "1".to_string(),
11451 aliases: "prod".to_string(),
11452 description: "Production".to_string(),
11453 last_modified: "2024-01-01".to_string(),
11454 architecture: "X86_64".to_string(),
11455 }];
11456
11457 app.lambda_state.version_table.selected = 0;
11458
11459 app.handle_action(Action::Select);
11461 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11462 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11463
11464 app.handle_action(Action::GoBack);
11466 assert_eq!(app.lambda_state.current_version, None);
11467 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11468 }
11469
11470 #[test]
11471 fn test_lambda_version_detail_tabs() {
11472 use crate::lambda::Function;
11473
11474 let mut app = test_app();
11475 app.current_service = Service::LambdaFunctions;
11476 app.service_selected = true;
11477 app.lambda_state.current_function = Some("test-function".to_string());
11478 app.lambda_state.current_version = Some("1".to_string());
11479 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11480 app.mode = Mode::Normal;
11481
11482 app.lambda_state.table.items = vec![Function {
11483 name: "test-function".to_string(),
11484 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11485 application: None,
11486 description: "Test".to_string(),
11487 package_type: "Zip".to_string(),
11488 runtime: "python3.12".to_string(),
11489 architecture: "X86_64".to_string(),
11490 code_size: 1024,
11491 code_sha256: "hash".to_string(),
11492 memory_mb: 128,
11493 timeout_seconds: 30,
11494 last_modified: "2024-01-01".to_string(),
11495 layers: vec![],
11496 }];
11497
11498 app.handle_action(Action::NextDetailTab);
11500 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11501
11502 app.handle_action(Action::NextDetailTab);
11503 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11504
11505 app.handle_action(Action::PrevDetailTab);
11507 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11508 }
11509
11510 #[test]
11511 fn test_lambda_aliases_arrow_key_expansion() {
11512 use crate::lambda::Alias;
11513
11514 let mut app = test_app();
11515 app.current_service = Service::LambdaFunctions;
11516 app.service_selected = true;
11517 app.lambda_state.current_function = Some("test-function".to_string());
11518 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11519 app.mode = Mode::Normal;
11520
11521 app.lambda_state.alias_table.items = vec![Alias {
11522 name: "prod".to_string(),
11523 versions: "1".to_string(),
11524 description: "Production alias".to_string(),
11525 }];
11526
11527 app.lambda_state.alias_table.selected = 0;
11528
11529 app.handle_action(Action::NextPane);
11531 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
11532
11533 app.handle_action(Action::PrevPane);
11535 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
11536 }
11537
11538 #[test]
11539 fn test_lambda_functions_arrow_key_expansion() {
11540 use crate::lambda::Function;
11541
11542 let mut app = test_app();
11543 app.current_service = Service::LambdaFunctions;
11544 app.service_selected = true;
11545 app.mode = Mode::Normal;
11546
11547 app.lambda_state.table.items = vec![Function {
11548 name: "test-function".to_string(),
11549 arn: "arn".to_string(),
11550 application: None,
11551 description: "Test".to_string(),
11552 package_type: "Zip".to_string(),
11553 runtime: "python3.12".to_string(),
11554 architecture: "X86_64".to_string(),
11555 code_size: 1024,
11556 code_sha256: "hash".to_string(),
11557 memory_mb: 128,
11558 timeout_seconds: 30,
11559 last_modified: "2024-01-01".to_string(),
11560 layers: vec![],
11561 }];
11562
11563 app.lambda_state.table.selected = 0;
11564
11565 app.handle_action(Action::NextPane);
11567 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
11568
11569 app.handle_action(Action::PrevPane);
11571 assert_eq!(app.lambda_state.table.expanded_item, None);
11572 }
11573
11574 #[test]
11575 fn test_lambda_version_detail_with_application() {
11576 use crate::lambda::Function;
11577
11578 let mut app = test_app();
11579 app.current_service = Service::LambdaFunctions;
11580 app.service_selected = true;
11581 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
11582 app.lambda_state.current_version = Some("1".to_string());
11583 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11584 app.mode = Mode::Normal;
11585
11586 app.lambda_state.table.items = vec![Function {
11587 name: "storefront-studio-beta-api".to_string(),
11588 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
11589 .to_string(),
11590 application: Some("storefront-studio-beta".to_string()),
11591 description: "API function".to_string(),
11592 package_type: "Zip".to_string(),
11593 runtime: "python3.12".to_string(),
11594 architecture: "X86_64".to_string(),
11595 code_size: 1024,
11596 code_sha256: "hash".to_string(),
11597 memory_mb: 128,
11598 timeout_seconds: 30,
11599 last_modified: "2024-01-01".to_string(),
11600 layers: vec![],
11601 }];
11602
11603 assert_eq!(
11605 app.lambda_state.table.items[0].application,
11606 Some("storefront-studio-beta".to_string())
11607 );
11608 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11609 }
11610
11611 #[test]
11612 fn test_lambda_layer_navigation() {
11613 use crate::lambda::{Function, Layer};
11614
11615 let mut app = test_app();
11616 app.current_service = Service::LambdaFunctions;
11617 app.service_selected = true;
11618 app.lambda_state.current_function = Some("test-function".to_string());
11619 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11620 app.mode = Mode::Normal;
11621
11622 app.lambda_state.table.items = vec![Function {
11623 name: "test-function".to_string(),
11624 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11625 application: None,
11626 description: "Test".to_string(),
11627 package_type: "Zip".to_string(),
11628 runtime: "python3.12".to_string(),
11629 architecture: "X86_64".to_string(),
11630 code_size: 1024,
11631 code_sha256: "hash".to_string(),
11632 memory_mb: 128,
11633 timeout_seconds: 30,
11634 last_modified: "2024-01-01".to_string(),
11635 layers: vec![
11636 Layer {
11637 merge_order: "1".to_string(),
11638 name: "layer1".to_string(),
11639 layer_version: "1".to_string(),
11640 compatible_runtimes: "python3.9".to_string(),
11641 compatible_architectures: "x86_64".to_string(),
11642 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11643 },
11644 Layer {
11645 merge_order: "2".to_string(),
11646 name: "layer2".to_string(),
11647 layer_version: "2".to_string(),
11648 compatible_runtimes: "python3.9".to_string(),
11649 compatible_architectures: "x86_64".to_string(),
11650 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11651 },
11652 Layer {
11653 merge_order: "3".to_string(),
11654 name: "layer3".to_string(),
11655 layer_version: "3".to_string(),
11656 compatible_runtimes: "python3.9".to_string(),
11657 compatible_architectures: "x86_64".to_string(),
11658 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
11659 },
11660 ],
11661 }];
11662
11663 assert_eq!(app.lambda_state.layer_selected, 0);
11664
11665 app.handle_action(Action::NextItem);
11666 assert_eq!(app.lambda_state.layer_selected, 1);
11667
11668 app.handle_action(Action::NextItem);
11669 assert_eq!(app.lambda_state.layer_selected, 2);
11670
11671 app.handle_action(Action::NextItem);
11672 assert_eq!(app.lambda_state.layer_selected, 2);
11673
11674 app.handle_action(Action::PrevItem);
11675 assert_eq!(app.lambda_state.layer_selected, 1);
11676
11677 app.handle_action(Action::PrevItem);
11678 assert_eq!(app.lambda_state.layer_selected, 0);
11679
11680 app.handle_action(Action::PrevItem);
11681 assert_eq!(app.lambda_state.layer_selected, 0);
11682 }
11683
11684 #[test]
11685 fn test_lambda_layer_expansion() {
11686 use crate::lambda::{Function, Layer};
11687
11688 let mut app = test_app();
11689 app.current_service = Service::LambdaFunctions;
11690 app.service_selected = true;
11691 app.lambda_state.current_function = Some("test-function".to_string());
11692 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11693 app.mode = Mode::Normal;
11694
11695 app.lambda_state.table.items = vec![Function {
11696 name: "test-function".to_string(),
11697 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11698 application: None,
11699 description: "Test".to_string(),
11700 package_type: "Zip".to_string(),
11701 runtime: "python3.12".to_string(),
11702 architecture: "X86_64".to_string(),
11703 code_size: 1024,
11704 code_sha256: "hash".to_string(),
11705 memory_mb: 128,
11706 timeout_seconds: 30,
11707 last_modified: "2024-01-01".to_string(),
11708 layers: vec![Layer {
11709 merge_order: "1".to_string(),
11710 name: "test-layer".to_string(),
11711 layer_version: "1".to_string(),
11712 compatible_runtimes: "python3.9".to_string(),
11713 compatible_architectures: "x86_64".to_string(),
11714 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
11715 }],
11716 }];
11717
11718 assert_eq!(app.lambda_state.layer_expanded, None);
11719
11720 app.handle_action(Action::NextPane);
11721 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11722
11723 app.handle_action(Action::PrevPane);
11724 assert_eq!(app.lambda_state.layer_expanded, None);
11725
11726 app.handle_action(Action::NextPane);
11727 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11728
11729 app.handle_action(Action::NextPane);
11730 assert_eq!(app.lambda_state.layer_expanded, None);
11731 }
11732
11733 #[test]
11734 fn test_lambda_layer_selection_and_expansion_workflow() {
11735 use crate::lambda::{Function, Layer};
11736
11737 let mut app = test_app();
11738 app.current_service = Service::LambdaFunctions;
11739 app.service_selected = true;
11740 app.lambda_state.current_function = Some("test-function".to_string());
11741 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11742 app.mode = Mode::Normal;
11743
11744 app.lambda_state.table.items = vec![Function {
11745 name: "test-function".to_string(),
11746 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11747 application: None,
11748 description: "Test".to_string(),
11749 package_type: "Zip".to_string(),
11750 runtime: "python3.12".to_string(),
11751 architecture: "X86_64".to_string(),
11752 code_size: 1024,
11753 code_sha256: "hash".to_string(),
11754 memory_mb: 128,
11755 timeout_seconds: 30,
11756 last_modified: "2024-01-01".to_string(),
11757 layers: vec![
11758 Layer {
11759 merge_order: "1".to_string(),
11760 name: "layer1".to_string(),
11761 layer_version: "1".to_string(),
11762 compatible_runtimes: "python3.9".to_string(),
11763 compatible_architectures: "x86_64".to_string(),
11764 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11765 },
11766 Layer {
11767 merge_order: "2".to_string(),
11768 name: "layer2".to_string(),
11769 layer_version: "2".to_string(),
11770 compatible_runtimes: "python3.9".to_string(),
11771 compatible_architectures: "x86_64".to_string(),
11772 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11773 },
11774 ],
11775 }];
11776
11777 assert_eq!(app.lambda_state.layer_selected, 0);
11779 assert_eq!(app.lambda_state.layer_expanded, None);
11780
11781 app.handle_action(Action::NextPane);
11783 assert_eq!(app.lambda_state.layer_selected, 0);
11784 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11785
11786 app.handle_action(Action::NextItem);
11788 assert_eq!(app.lambda_state.layer_selected, 1);
11789 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
11793 assert_eq!(app.lambda_state.layer_selected, 1);
11794 assert_eq!(app.lambda_state.layer_expanded, Some(1));
11795
11796 app.handle_action(Action::PrevPane);
11798 assert_eq!(app.lambda_state.layer_selected, 1);
11799 assert_eq!(app.lambda_state.layer_expanded, None);
11800
11801 app.handle_action(Action::PrevItem);
11803 assert_eq!(app.lambda_state.layer_selected, 0);
11804 assert_eq!(app.lambda_state.layer_expanded, None);
11805 }
11806
11807 #[test]
11808 fn test_backtab_cycles_detail_tabs_backward() {
11809 let mut app = test_app();
11810 app.mode = Mode::Normal;
11811
11812 app.current_service = Service::LambdaFunctions;
11814 app.service_selected = true;
11815 app.lambda_state.current_function = Some("test-function".to_string());
11816 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11817
11818 app.handle_action(Action::PrevDetailTab);
11819 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11820
11821 app.handle_action(Action::PrevDetailTab);
11822 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11823
11824 app.current_service = Service::IamRoles;
11826 app.iam_state.current_role = Some("test-role".to_string());
11827 app.iam_state.role_tab = RoleTab::Permissions;
11828
11829 app.handle_action(Action::PrevDetailTab);
11830 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11831
11832 app.current_service = Service::IamUsers;
11834 app.iam_state.current_user = Some("test-user".to_string());
11835 app.iam_state.user_tab = UserTab::Permissions;
11836
11837 app.handle_action(Action::PrevDetailTab);
11838 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
11839
11840 app.current_service = Service::IamUserGroups;
11842 app.iam_state.current_group = Some("test-group".to_string());
11843 app.iam_state.group_tab = GroupTab::Permissions;
11844
11845 app.handle_action(Action::PrevDetailTab);
11846 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
11847
11848 app.current_service = Service::S3Buckets;
11850 app.s3_state.current_bucket = Some("test-bucket".to_string());
11851 app.s3_state.object_tab = S3ObjectTab::Properties;
11852
11853 app.handle_action(Action::PrevDetailTab);
11854 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
11855
11856 app.current_service = Service::EcrRepositories;
11858 app.ecr_state.current_repository = None;
11859 app.ecr_state.tab = EcrTab::Private;
11860
11861 app.handle_action(Action::PrevDetailTab);
11862 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11863
11864 app.current_service = Service::CloudFormationStacks;
11866 app.cfn_state.current_stack = Some("test-stack".to_string());
11867 app.cfn_state.detail_tab = CfnDetailTab::Resources;
11868 }
11869
11870 #[test]
11871 fn test_cloudformation_status_filter_active() {
11872 use crate::ui::cfn::StatusFilter;
11873 let filter = StatusFilter::Active;
11874 assert!(filter.matches("CREATE_IN_PROGRESS"));
11875 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11876 assert!(!filter.matches("CREATE_COMPLETE"));
11877 assert!(!filter.matches("DELETE_COMPLETE"));
11878 assert!(!filter.matches("CREATE_FAILED"));
11879 }
11880
11881 #[test]
11882 fn test_cloudformation_status_filter_complete() {
11883 use crate::ui::cfn::StatusFilter;
11884 let filter = StatusFilter::Complete;
11885 assert!(filter.matches("CREATE_COMPLETE"));
11886 assert!(filter.matches("UPDATE_COMPLETE"));
11887 assert!(!filter.matches("DELETE_COMPLETE"));
11888 assert!(!filter.matches("CREATE_IN_PROGRESS"));
11889 }
11890
11891 #[test]
11892 fn test_cloudformation_status_filter_failed() {
11893 use crate::ui::cfn::StatusFilter;
11894 let filter = StatusFilter::Failed;
11895 assert!(filter.matches("CREATE_FAILED"));
11896 assert!(filter.matches("UPDATE_FAILED"));
11897 assert!(!filter.matches("CREATE_COMPLETE"));
11898 }
11899
11900 #[test]
11901 fn test_cloudformation_status_filter_deleted() {
11902 use crate::ui::cfn::StatusFilter;
11903 let filter = StatusFilter::Deleted;
11904 assert!(filter.matches("DELETE_COMPLETE"));
11905 assert!(filter.matches("DELETE_IN_PROGRESS"));
11906 assert!(!filter.matches("CREATE_COMPLETE"));
11907 }
11908
11909 #[test]
11910 fn test_cloudformation_status_filter_in_progress() {
11911 use crate::ui::cfn::StatusFilter;
11912 let filter = StatusFilter::InProgress;
11913 assert!(filter.matches("CREATE_IN_PROGRESS"));
11914 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11915 assert!(filter.matches("DELETE_IN_PROGRESS"));
11916 assert!(!filter.matches("CREATE_COMPLETE"));
11917 }
11918
11919 #[test]
11920 fn test_cloudformation_status_filter_cycle() {
11921 use crate::ui::cfn::StatusFilter;
11922 let filter = StatusFilter::All;
11923 assert_eq!(filter.next(), StatusFilter::Active);
11924 assert_eq!(filter.next().next(), StatusFilter::Complete);
11925 assert_eq!(filter.next().next().next(), StatusFilter::Failed);
11926 assert_eq!(filter.next().next().next().next(), StatusFilter::Deleted);
11927 assert_eq!(
11928 filter.next().next().next().next().next(),
11929 StatusFilter::InProgress
11930 );
11931 assert_eq!(
11932 filter.next().next().next().next().next().next(),
11933 StatusFilter::All
11934 );
11935 }
11936
11937 #[test]
11938 fn test_cloudformation_default_columns() {
11939 let app = test_app();
11940 assert_eq!(app.cfn_visible_column_ids.len(), 4);
11941 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
11942 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
11943 assert!(app
11944 .cfn_visible_column_ids
11945 .contains(&CfnColumn::CreatedTime.id()));
11946 assert!(app
11947 .cfn_visible_column_ids
11948 .contains(&CfnColumn::Description.id()));
11949 }
11950
11951 #[test]
11952 fn test_cloudformation_all_columns() {
11953 let app = test_app();
11954 assert_eq!(app.cfn_column_ids.len(), 10);
11955 }
11956
11957 #[test]
11958 fn test_cloudformation_filter_by_name() {
11959 use crate::ui::cfn::StatusFilter;
11960 let mut app = test_app();
11961 app.cfn_state.status_filter = StatusFilter::Complete;
11962 app.cfn_state.table.items = vec![
11963 CfnStack {
11964 name: "my-stack".to_string(),
11965 stack_id: "id1".to_string(),
11966 status: "CREATE_COMPLETE".to_string(),
11967 created_time: "2024-01-01".to_string(),
11968 updated_time: String::new(),
11969 deleted_time: String::new(),
11970 drift_status: String::new(),
11971 last_drift_check_time: String::new(),
11972 status_reason: String::new(),
11973 description: String::new(),
11974 detailed_status: String::new(),
11975 root_stack: String::new(),
11976 parent_stack: String::new(),
11977 termination_protection: false,
11978 iam_role: String::new(),
11979 tags: Vec::new(),
11980 stack_policy: String::new(),
11981 rollback_monitoring_time: String::new(),
11982 rollback_alarms: Vec::new(),
11983 notification_arns: Vec::new(),
11984 },
11985 CfnStack {
11986 name: "other-stack".to_string(),
11987 stack_id: "id2".to_string(),
11988 status: "CREATE_COMPLETE".to_string(),
11989 created_time: "2024-01-02".to_string(),
11990 updated_time: String::new(),
11991 deleted_time: String::new(),
11992 drift_status: String::new(),
11993 last_drift_check_time: String::new(),
11994 status_reason: String::new(),
11995 description: String::new(),
11996 detailed_status: String::new(),
11997 root_stack: String::new(),
11998 parent_stack: String::new(),
11999 termination_protection: false,
12000 iam_role: String::new(),
12001 tags: Vec::new(),
12002 stack_policy: String::new(),
12003 rollback_monitoring_time: String::new(),
12004 rollback_alarms: Vec::new(),
12005 notification_arns: Vec::new(),
12006 },
12007 ];
12008
12009 app.cfn_state.table.filter = "my".to_string();
12010 let filtered = app.filtered_cloudformation_stacks();
12011 assert_eq!(filtered.len(), 1);
12012 assert_eq!(filtered[0].name, "my-stack");
12013 }
12014
12015 #[test]
12016 fn test_cloudformation_filter_by_description() {
12017 use crate::ui::cfn::StatusFilter;
12018 let mut app = test_app();
12019 app.cfn_state.status_filter = StatusFilter::Complete;
12020 app.cfn_state.table.items = vec![CfnStack {
12021 name: "stack1".to_string(),
12022 stack_id: "id1".to_string(),
12023 status: "CREATE_COMPLETE".to_string(),
12024 created_time: "2024-01-01".to_string(),
12025 updated_time: String::new(),
12026 deleted_time: String::new(),
12027 drift_status: String::new(),
12028 last_drift_check_time: String::new(),
12029 status_reason: String::new(),
12030 description: "production stack".to_string(),
12031 detailed_status: String::new(),
12032 root_stack: String::new(),
12033 parent_stack: String::new(),
12034 termination_protection: false,
12035 iam_role: String::new(),
12036 tags: Vec::new(),
12037 stack_policy: String::new(),
12038 rollback_monitoring_time: String::new(),
12039 rollback_alarms: Vec::new(),
12040 notification_arns: Vec::new(),
12041 }];
12042
12043 app.cfn_state.table.filter = "production".to_string();
12044 let filtered = app.filtered_cloudformation_stacks();
12045 assert_eq!(filtered.len(), 1);
12046 }
12047
12048 #[test]
12049 fn test_cloudformation_status_filter_applied() {
12050 use crate::ui::cfn::StatusFilter;
12051 let mut app = test_app();
12052 app.cfn_state.table.items = vec![
12053 CfnStack {
12054 name: "complete-stack".to_string(),
12055 stack_id: "id1".to_string(),
12056 status: "CREATE_COMPLETE".to_string(),
12057 created_time: "2024-01-01".to_string(),
12058 updated_time: String::new(),
12059 deleted_time: String::new(),
12060 drift_status: String::new(),
12061 last_drift_check_time: String::new(),
12062 status_reason: String::new(),
12063 description: String::new(),
12064 detailed_status: String::new(),
12065 root_stack: String::new(),
12066 parent_stack: String::new(),
12067 termination_protection: false,
12068 iam_role: String::new(),
12069 tags: Vec::new(),
12070 stack_policy: String::new(),
12071 rollback_monitoring_time: String::new(),
12072 rollback_alarms: Vec::new(),
12073 notification_arns: Vec::new(),
12074 },
12075 CfnStack {
12076 name: "failed-stack".to_string(),
12077 stack_id: "id2".to_string(),
12078 status: "CREATE_FAILED".to_string(),
12079 created_time: "2024-01-02".to_string(),
12080 updated_time: String::new(),
12081 deleted_time: String::new(),
12082 drift_status: String::new(),
12083 last_drift_check_time: String::new(),
12084 status_reason: String::new(),
12085 description: String::new(),
12086 detailed_status: String::new(),
12087 root_stack: String::new(),
12088 parent_stack: String::new(),
12089 termination_protection: false,
12090 iam_role: String::new(),
12091 tags: Vec::new(),
12092 stack_policy: String::new(),
12093 rollback_monitoring_time: String::new(),
12094 rollback_alarms: Vec::new(),
12095 notification_arns: Vec::new(),
12096 },
12097 ];
12098
12099 app.cfn_state.status_filter = StatusFilter::Complete;
12100 let filtered = app.filtered_cloudformation_stacks();
12101 assert_eq!(filtered.len(), 1);
12102 assert_eq!(filtered[0].name, "complete-stack");
12103
12104 app.cfn_state.status_filter = StatusFilter::Failed;
12105 let filtered = app.filtered_cloudformation_stacks();
12106 assert_eq!(filtered.len(), 1);
12107 assert_eq!(filtered[0].name, "failed-stack");
12108 }
12109
12110 #[test]
12111 fn test_cloudformation_default_page_size() {
12112 let app = test_app();
12113 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
12114 }
12115
12116 #[test]
12117 fn test_cloudformation_default_status_filter() {
12118 use crate::ui::cfn::StatusFilter;
12119 let app = test_app();
12120 assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
12121 }
12122
12123 #[test]
12124 fn test_cloudformation_view_nested_default_false() {
12125 let app = test_app();
12126 assert!(!app.cfn_state.view_nested);
12127 }
12128
12129 #[test]
12130 fn test_cloudformation_pagination_hotkeys() {
12131 use crate::ui::cfn::StatusFilter;
12132 let mut app = test_app();
12133 app.current_service = Service::CloudFormationStacks;
12134 app.service_selected = true;
12135 app.cfn_state.status_filter = StatusFilter::All;
12136
12137 for i in 0..150 {
12139 app.cfn_state.table.items.push(CfnStack {
12140 name: format!("stack-{}", i),
12141 stack_id: format!("id-{}", i),
12142 status: "CREATE_COMPLETE".to_string(),
12143 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12144 updated_time: String::new(),
12145 deleted_time: String::new(),
12146 drift_status: String::new(),
12147 last_drift_check_time: String::new(),
12148 status_reason: String::new(),
12149 description: String::new(),
12150 detailed_status: String::new(),
12151 root_stack: String::new(),
12152 parent_stack: String::new(),
12153 termination_protection: false,
12154 iam_role: String::new(),
12155 tags: vec![],
12156 stack_policy: String::new(),
12157 rollback_monitoring_time: String::new(),
12158 rollback_alarms: vec![],
12159 notification_arns: vec![],
12160 });
12161 }
12162
12163 app.go_to_page(2);
12165 assert_eq!(app.cfn_state.table.selected, 50);
12166
12167 app.go_to_page(3);
12169 assert_eq!(app.cfn_state.table.selected, 100);
12170
12171 app.go_to_page(1);
12173 assert_eq!(app.cfn_state.table.selected, 0);
12174 }
12175
12176 #[test]
12177 fn test_cloudformation_tab_cycling_in_filter_mode() {
12178 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12179 let mut app = test_app();
12180 app.current_service = Service::CloudFormationStacks;
12181 app.service_selected = true;
12182 app.mode = Mode::FilterInput;
12183 app.cfn_state.input_focus = InputFocus::Filter;
12184
12185 app.handle_action(Action::NextFilterFocus);
12187 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12188
12189 app.handle_action(Action::NextFilterFocus);
12191 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12192
12193 app.handle_action(Action::NextFilterFocus);
12195 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12196
12197 app.handle_action(Action::NextFilterFocus);
12199 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12200 }
12201
12202 #[test]
12203 fn test_cloudformation_timestamp_format_includes_utc() {
12204 let stack = CfnStack {
12205 name: "test-stack".to_string(),
12206 stack_id: "id-123".to_string(),
12207 status: "CREATE_COMPLETE".to_string(),
12208 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
12209 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
12210 deleted_time: String::new(),
12211 drift_status: String::new(),
12212 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
12213 status_reason: String::new(),
12214 description: String::new(),
12215 detailed_status: String::new(),
12216 root_stack: String::new(),
12217 parent_stack: String::new(),
12218 termination_protection: false,
12219 iam_role: String::new(),
12220 tags: vec![],
12221 stack_policy: String::new(),
12222 rollback_monitoring_time: String::new(),
12223 rollback_alarms: vec![],
12224 notification_arns: vec![],
12225 };
12226
12227 assert!(stack.created_time.contains("(UTC)"));
12228 assert!(stack.updated_time.contains("(UTC)"));
12229 assert!(stack.last_drift_check_time.contains("(UTC)"));
12230 assert_eq!(stack.created_time.len(), 25);
12231 }
12232
12233 #[test]
12234 fn test_cloudformation_enter_drills_into_stack_view() {
12235 use crate::ui::cfn::StatusFilter;
12236 let mut app = test_app();
12237 app.current_service = Service::CloudFormationStacks;
12238 app.service_selected = true;
12239 app.mode = Mode::Normal;
12240 app.cfn_state.status_filter = StatusFilter::All;
12241 app.tabs = vec![Tab {
12242 service: Service::CloudFormationStacks,
12243 title: "CloudFormation > Stacks".to_string(),
12244 breadcrumb: "CloudFormation > Stacks".to_string(),
12245 }];
12246 app.current_tab = 0;
12247
12248 app.cfn_state.table.items.push(CfnStack {
12249 name: "test-stack".to_string(),
12250 stack_id: "id-123".to_string(),
12251 status: "CREATE_COMPLETE".to_string(),
12252 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12253 updated_time: String::new(),
12254 deleted_time: String::new(),
12255 drift_status: String::new(),
12256 last_drift_check_time: String::new(),
12257 status_reason: String::new(),
12258 description: String::new(),
12259 detailed_status: String::new(),
12260 root_stack: String::new(),
12261 parent_stack: String::new(),
12262 termination_protection: false,
12263 iam_role: String::new(),
12264 tags: vec![],
12265 stack_policy: String::new(),
12266 rollback_monitoring_time: String::new(),
12267 rollback_alarms: vec![],
12268 notification_arns: vec![],
12269 });
12270
12271 app.cfn_state.table.reset();
12272 assert_eq!(app.cfn_state.current_stack, None);
12273
12274 app.handle_action(Action::Select);
12276 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
12277 }
12278
12279 #[test]
12280 fn test_cloudformation_arrow_keys_expand_collapse() {
12281 use crate::ui::cfn::StatusFilter;
12282 let mut app = test_app();
12283 app.current_service = Service::CloudFormationStacks;
12284 app.service_selected = true;
12285 app.mode = Mode::Normal;
12286 app.cfn_state.status_filter = StatusFilter::All;
12287
12288 app.cfn_state.table.items.push(CfnStack {
12289 name: "test-stack".to_string(),
12290 stack_id: "id-123".to_string(),
12291 status: "CREATE_COMPLETE".to_string(),
12292 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12293 updated_time: String::new(),
12294 deleted_time: String::new(),
12295 drift_status: String::new(),
12296 last_drift_check_time: String::new(),
12297 status_reason: String::new(),
12298 description: String::new(),
12299 detailed_status: String::new(),
12300 root_stack: String::new(),
12301 parent_stack: String::new(),
12302 termination_protection: false,
12303 iam_role: String::new(),
12304 tags: vec![],
12305 stack_policy: String::new(),
12306 rollback_monitoring_time: String::new(),
12307 rollback_alarms: vec![],
12308 notification_arns: vec![],
12309 });
12310
12311 app.cfn_state.table.reset();
12312 assert_eq!(app.cfn_state.table.expanded_item, None);
12313
12314 app.handle_action(Action::NextPane);
12316 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
12317
12318 app.handle_action(Action::PrevPane);
12320 assert_eq!(app.cfn_state.table.expanded_item, None);
12321
12322 assert_eq!(app.cfn_state.current_stack, None);
12324 }
12325
12326 #[test]
12327 fn test_cloudformation_tab_cycling() {
12328 use crate::ui::cfn::{DetailTab, StatusFilter};
12329 let mut app = test_app();
12330 app.current_service = Service::CloudFormationStacks;
12331 app.service_selected = true;
12332 app.mode = Mode::Normal;
12333 app.cfn_state.status_filter = StatusFilter::All;
12334 app.cfn_state.current_stack = Some("test-stack".to_string());
12335
12336 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
12337 }
12338
12339 #[test]
12340 fn test_cloudformation_console_url() {
12341 use crate::ui::cfn::{DetailTab, StatusFilter};
12342 let mut app = test_app();
12343 app.current_service = Service::CloudFormationStacks;
12344 app.service_selected = true;
12345 app.cfn_state.status_filter = StatusFilter::All;
12346
12347 app.cfn_state.table.items.push(CfnStack {
12348 name: "test-stack".to_string(),
12349 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12350 .to_string(),
12351 status: "CREATE_COMPLETE".to_string(),
12352 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12353 updated_time: String::new(),
12354 deleted_time: String::new(),
12355 drift_status: String::new(),
12356 last_drift_check_time: String::new(),
12357 status_reason: String::new(),
12358 description: String::new(),
12359 detailed_status: String::new(),
12360 root_stack: String::new(),
12361 parent_stack: String::new(),
12362 termination_protection: false,
12363 iam_role: String::new(),
12364 tags: vec![],
12365 stack_policy: String::new(),
12366 rollback_monitoring_time: String::new(),
12367 rollback_alarms: vec![],
12368 notification_arns: vec![],
12369 });
12370
12371 app.cfn_state.current_stack = Some("test-stack".to_string());
12372
12373 app.cfn_state.detail_tab = DetailTab::StackInfo;
12375 let url = app.get_console_url();
12376 assert!(url.contains("stackinfo"));
12377 assert!(url.contains("arn%3Aaws%3Acloudformation"));
12378
12379 app.cfn_state.detail_tab = DetailTab::Events;
12381 let url = app.get_console_url();
12382 assert!(url.contains("events"));
12383 assert!(url.contains("arn%3Aaws%3Acloudformation"));
12384 }
12385
12386 #[test]
12387 fn test_iam_role_select() {
12388 let mut app = test_app();
12389 app.current_service = Service::IamRoles;
12390 app.service_selected = true;
12391 app.mode = Mode::Normal;
12392
12393 app.iam_state.roles.items = vec![
12394 crate::iam::IamRole {
12395 role_name: "role1".to_string(),
12396 path: "/".to_string(),
12397 trusted_entities: "AWS Service: ec2".to_string(),
12398 last_activity: "-".to_string(),
12399 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
12400 creation_time: "2025-01-01".to_string(),
12401 description: "Test role 1".to_string(),
12402 max_session_duration: Some(3600),
12403 },
12404 crate::iam::IamRole {
12405 role_name: "role2".to_string(),
12406 path: "/".to_string(),
12407 trusted_entities: "AWS Service: lambda".to_string(),
12408 last_activity: "-".to_string(),
12409 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
12410 creation_time: "2025-01-02".to_string(),
12411 description: "Test role 2".to_string(),
12412 max_session_duration: Some(7200),
12413 },
12414 ];
12415
12416 app.iam_state.roles.selected = 0;
12418 app.handle_action(Action::Select);
12419
12420 assert_eq!(
12421 app.iam_state.current_role,
12422 Some("role1".to_string()),
12423 "Should open role detail view"
12424 );
12425 assert_eq!(
12426 app.iam_state.role_tab,
12427 RoleTab::Permissions,
12428 "Should default to Permissions tab"
12429 );
12430 }
12431
12432 #[test]
12433 fn test_iam_role_back_navigation() {
12434 let mut app = test_app();
12435 app.current_service = Service::IamRoles;
12436 app.service_selected = true;
12437 app.iam_state.current_role = Some("test-role".to_string());
12438
12439 app.handle_action(Action::GoBack);
12440
12441 assert_eq!(
12442 app.iam_state.current_role, None,
12443 "Should return to roles list"
12444 );
12445 }
12446
12447 #[test]
12448 fn test_iam_role_tab_navigation() {
12449 let mut app = test_app();
12450 app.current_service = Service::IamRoles;
12451 app.service_selected = true;
12452 app.iam_state.current_role = Some("test-role".to_string());
12453 app.iam_state.role_tab = RoleTab::Permissions;
12454
12455 app.handle_action(Action::NextDetailTab);
12456
12457 assert_eq!(
12458 app.iam_state.role_tab,
12459 RoleTab::TrustRelationships,
12460 "Should move to next tab"
12461 );
12462 }
12463
12464 #[test]
12465 fn test_iam_role_tab_cycle_order() {
12466 let mut app = test_app();
12467 app.current_service = Service::IamRoles;
12468 app.service_selected = true;
12469 app.iam_state.current_role = Some("test-role".to_string());
12470 app.iam_state.role_tab = RoleTab::Permissions;
12471
12472 app.handle_action(Action::NextDetailTab);
12473 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
12474
12475 app.handle_action(Action::NextDetailTab);
12476 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
12477
12478 app.handle_action(Action::NextDetailTab);
12479 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
12480
12481 app.handle_action(Action::NextDetailTab);
12482 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
12483
12484 app.handle_action(Action::NextDetailTab);
12485 assert_eq!(
12486 app.iam_state.role_tab,
12487 RoleTab::Permissions,
12488 "Should cycle back to first tab"
12489 );
12490 }
12491
12492 #[test]
12493 fn test_iam_role_pagination() {
12494 let mut app = test_app();
12495 app.current_service = Service::IamRoles;
12496 app.service_selected = true;
12497 app.iam_state.roles.page_size = crate::common::PageSize::Ten;
12498
12499 app.iam_state.roles.items = (0..25)
12500 .map(|i| crate::iam::IamRole {
12501 role_name: format!("role{}", i),
12502 path: "/".to_string(),
12503 trusted_entities: "AWS Service: ec2".to_string(),
12504 last_activity: "-".to_string(),
12505 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12506 creation_time: "2025-01-01".to_string(),
12507 description: format!("Test role {}", i),
12508 max_session_duration: Some(3600),
12509 })
12510 .collect();
12511
12512 app.go_to_page(2);
12514
12515 assert_eq!(
12516 app.iam_state.roles.selected, 10,
12517 "Should select first item of page 2"
12518 );
12519 assert_eq!(
12520 app.iam_state.roles.scroll_offset, 10,
12521 "Should update scroll offset"
12522 );
12523 }
12524
12525 #[test]
12526 fn test_tags_table_populated_on_role_detail() {
12527 let mut app = test_app();
12528 app.current_service = Service::IamRoles;
12529 app.service_selected = true;
12530 app.mode = Mode::Normal;
12531 app.iam_state.roles.items = vec![crate::iam::IamRole {
12532 role_name: "TestRole".to_string(),
12533 path: "/".to_string(),
12534 trusted_entities: String::new(),
12535 last_activity: String::new(),
12536 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
12537 creation_time: "2025-01-01".to_string(),
12538 description: String::new(),
12539 max_session_duration: Some(3600),
12540 }];
12541
12542 app.iam_state.tags.items = vec![
12544 crate::iam::RoleTag {
12545 key: "Environment".to_string(),
12546 value: "Production".to_string(),
12547 },
12548 crate::iam::RoleTag {
12549 key: "Team".to_string(),
12550 value: "Platform".to_string(),
12551 },
12552 ];
12553
12554 assert_eq!(app.iam_state.tags.items.len(), 2);
12555 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
12556 assert_eq!(app.iam_state.tags.items[0].value, "Production");
12557 assert_eq!(app.iam_state.tags.selected, 0);
12558 }
12559
12560 #[test]
12561 fn test_tags_table_navigation() {
12562 let mut app = test_app();
12563 app.current_service = Service::IamRoles;
12564 app.service_selected = true;
12565 app.mode = Mode::Normal;
12566 app.iam_state.current_role = Some("TestRole".to_string());
12567 app.iam_state.role_tab = RoleTab::Tags;
12568 app.iam_state.tags.items = vec![
12569 crate::iam::RoleTag {
12570 key: "Tag1".to_string(),
12571 value: "Value1".to_string(),
12572 },
12573 crate::iam::RoleTag {
12574 key: "Tag2".to_string(),
12575 value: "Value2".to_string(),
12576 },
12577 ];
12578
12579 app.handle_action(Action::NextItem);
12580 assert_eq!(app.iam_state.tags.selected, 1);
12581
12582 app.handle_action(Action::PrevItem);
12583 assert_eq!(app.iam_state.tags.selected, 0);
12584 }
12585
12586 #[test]
12587 fn test_last_accessed_table_navigation() {
12588 let mut app = test_app();
12589 app.current_service = Service::IamRoles;
12590 app.service_selected = true;
12591 app.mode = Mode::Normal;
12592 app.iam_state.current_role = Some("TestRole".to_string());
12593 app.iam_state.role_tab = RoleTab::LastAccessed;
12594 app.iam_state.last_accessed_services.items = vec![
12595 crate::iam::LastAccessedService {
12596 service: "S3".to_string(),
12597 policies_granting: "Policy1".to_string(),
12598 last_accessed: "2025-01-01".to_string(),
12599 },
12600 crate::iam::LastAccessedService {
12601 service: "EC2".to_string(),
12602 policies_granting: "Policy2".to_string(),
12603 last_accessed: "2025-01-02".to_string(),
12604 },
12605 ];
12606
12607 app.handle_action(Action::NextItem);
12608 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
12609
12610 app.handle_action(Action::PrevItem);
12611 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
12612 }
12613
12614 #[test]
12615 fn test_cfn_input_focus_next() {
12616 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12617 let mut app = test_app();
12618 app.current_service = Service::CloudFormationStacks;
12619 app.mode = Mode::FilterInput;
12620 app.cfn_state.input_focus = InputFocus::Filter;
12621
12622 app.handle_action(Action::NextFilterFocus);
12623 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12624
12625 app.handle_action(Action::NextFilterFocus);
12626 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12627
12628 app.handle_action(Action::NextFilterFocus);
12629 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12630
12631 app.handle_action(Action::NextFilterFocus);
12632 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12633 }
12634
12635 #[test]
12636 fn test_cfn_input_focus_prev() {
12637 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12638 let mut app = test_app();
12639 app.current_service = Service::CloudFormationStacks;
12640 app.mode = Mode::FilterInput;
12641 app.cfn_state.input_focus = InputFocus::Filter;
12642
12643 app.handle_action(Action::PrevFilterFocus);
12644 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12645
12646 app.handle_action(Action::PrevFilterFocus);
12647 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12648
12649 app.handle_action(Action::PrevFilterFocus);
12650 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12651
12652 app.handle_action(Action::PrevFilterFocus);
12653 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12654 }
12655
12656 #[test]
12657 fn test_cw_logs_input_focus_prev() {
12658 let mut app = test_app();
12659 app.current_service = Service::CloudWatchLogGroups;
12660 app.mode = Mode::FilterInput;
12661 app.view_mode = ViewMode::Detail;
12662 app.log_groups_state.detail_tab = crate::ui::cw::logs::DetailTab::LogStreams;
12663 app.log_groups_state.input_focus = InputFocus::Filter;
12664
12665 app.handle_action(Action::PrevFilterFocus);
12666 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
12667
12668 app.handle_action(Action::PrevFilterFocus);
12669 assert_eq!(
12670 app.log_groups_state.input_focus,
12671 InputFocus::Checkbox("ShowExpired")
12672 );
12673
12674 app.handle_action(Action::PrevFilterFocus);
12675 assert_eq!(
12676 app.log_groups_state.input_focus,
12677 InputFocus::Checkbox("ExactMatch")
12678 );
12679
12680 app.handle_action(Action::PrevFilterFocus);
12681 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
12682 }
12683
12684 #[test]
12685 fn test_cw_events_input_focus_prev() {
12686 use crate::ui::cw::logs::EventFilterFocus;
12687 let mut app = test_app();
12688 app.mode = Mode::EventFilterInput;
12689 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
12690
12691 app.handle_action(Action::PrevFilterFocus);
12692 assert_eq!(
12693 app.log_groups_state.event_input_focus,
12694 EventFilterFocus::DateRange
12695 );
12696
12697 app.handle_action(Action::PrevFilterFocus);
12698 assert_eq!(
12699 app.log_groups_state.event_input_focus,
12700 EventFilterFocus::Filter
12701 );
12702 }
12703
12704 #[test]
12705 fn test_cfn_input_focus_cycle_complete() {
12706 let mut app = test_app();
12707 app.current_service = Service::CloudFormationStacks;
12708 app.mode = Mode::FilterInput;
12709 app.cfn_state.input_focus = InputFocus::Filter;
12710
12711 for _ in 0..4 {
12713 app.handle_action(Action::NextFilterFocus);
12714 }
12715 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12716
12717 for _ in 0..4 {
12719 app.handle_action(Action::PrevFilterFocus);
12720 }
12721 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12722 }
12723
12724 #[test]
12725 fn test_cfn_filter_status_arrow_keys() {
12726 use crate::ui::cfn::{StatusFilter, STATUS_FILTER};
12727 let mut app = test_app();
12728 app.current_service = Service::CloudFormationStacks;
12729 app.mode = Mode::FilterInput;
12730 app.cfn_state.input_focus = STATUS_FILTER;
12731 app.cfn_state.status_filter = StatusFilter::All;
12732
12733 app.handle_action(Action::NextItem);
12734 assert_eq!(app.cfn_state.status_filter, StatusFilter::Active);
12735
12736 app.handle_action(Action::PrevItem);
12737 assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
12738 }
12739
12740 #[test]
12741 fn test_cfn_filter_shift_tab_cycles_backward() {
12742 use crate::ui::cfn::STATUS_FILTER;
12743 let mut app = test_app();
12744 app.current_service = Service::CloudFormationStacks;
12745 app.mode = Mode::FilterInput;
12746 app.cfn_state.input_focus = STATUS_FILTER;
12747
12748 app.handle_action(Action::PrevFilterFocus);
12750 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12751
12752 app.handle_action(Action::PrevFilterFocus);
12754 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12755 }
12756
12757 #[test]
12758 fn test_cfn_pagination_arrow_keys() {
12759 let mut app = test_app();
12760 app.current_service = Service::CloudFormationStacks;
12761 app.mode = Mode::FilterInput;
12762 app.cfn_state.input_focus = InputFocus::Pagination;
12763 app.cfn_state.table.scroll_offset = 0;
12764 app.cfn_state.table.page_size = crate::common::PageSize::Ten;
12765
12766 app.cfn_state.table.items = (0..30)
12768 .map(|i| crate::cfn::Stack {
12769 name: format!("stack-{}", i),
12770 stack_id: format!("id-{}", i),
12771 status: "CREATE_COMPLETE".to_string(),
12772 created_time: "2024-01-01".to_string(),
12773 updated_time: String::new(),
12774 deleted_time: String::new(),
12775 drift_status: String::new(),
12776 last_drift_check_time: String::new(),
12777 status_reason: String::new(),
12778 description: String::new(),
12779 detailed_status: String::new(),
12780 root_stack: String::new(),
12781 parent_stack: String::new(),
12782 termination_protection: false,
12783 iam_role: String::new(),
12784 tags: Vec::new(),
12785 stack_policy: String::new(),
12786 rollback_monitoring_time: String::new(),
12787 rollback_alarms: Vec::new(),
12788 notification_arns: Vec::new(),
12789 })
12790 .collect();
12791
12792 app.handle_action(Action::PageDown);
12794 assert_eq!(app.cfn_state.table.scroll_offset, 10);
12795 let page_size = app.cfn_state.table.page_size.value();
12797 let current_page = app.cfn_state.table.scroll_offset / page_size;
12798 assert_eq!(current_page, 1);
12799
12800 app.handle_action(Action::PageUp);
12802 assert_eq!(app.cfn_state.table.scroll_offset, 0);
12803 let current_page = app.cfn_state.table.scroll_offset / page_size;
12804 assert_eq!(current_page, 0);
12805 }
12806
12807 #[test]
12808 fn test_cfn_page_navigation_updates_selection() {
12809 let mut app = test_app();
12810 app.current_service = Service::CloudFormationStacks;
12811 app.mode = Mode::Normal;
12812
12813 app.cfn_state.table.items = (0..30)
12815 .map(|i| crate::cfn::Stack {
12816 name: format!("stack-{}", i),
12817 stack_id: format!("id-{}", i),
12818 status: "CREATE_COMPLETE".to_string(),
12819 created_time: "2024-01-01".to_string(),
12820 updated_time: String::new(),
12821 deleted_time: String::new(),
12822 drift_status: String::new(),
12823 last_drift_check_time: String::new(),
12824 status_reason: String::new(),
12825 description: String::new(),
12826 detailed_status: String::new(),
12827 root_stack: String::new(),
12828 parent_stack: String::new(),
12829 termination_protection: false,
12830 iam_role: String::new(),
12831 tags: Vec::new(),
12832 stack_policy: String::new(),
12833 rollback_monitoring_time: String::new(),
12834 rollback_alarms: Vec::new(),
12835 notification_arns: Vec::new(),
12836 })
12837 .collect();
12838
12839 app.cfn_state.table.reset();
12840 app.cfn_state.table.scroll_offset = 0;
12841
12842 app.handle_action(Action::PageDown);
12844 assert_eq!(app.cfn_state.table.selected, 10);
12845
12846 app.handle_action(Action::PageDown);
12848 assert_eq!(app.cfn_state.table.selected, 20);
12849
12850 app.handle_action(Action::PageUp);
12852 assert_eq!(app.cfn_state.table.selected, 10);
12853 }
12854
12855 #[test]
12856 fn test_cfn_filter_input_only_when_focused() {
12857 use crate::ui::cfn::STATUS_FILTER;
12858 let mut app = test_app();
12859 app.current_service = Service::CloudFormationStacks;
12860 app.mode = Mode::FilterInput;
12861 app.cfn_state.input_focus = STATUS_FILTER;
12862 app.cfn_state.table.filter = String::new();
12863
12864 app.handle_action(Action::FilterInput('t'));
12866 app.handle_action(Action::FilterInput('e'));
12867 app.handle_action(Action::FilterInput('s'));
12868 app.handle_action(Action::FilterInput('t'));
12869 assert_eq!(app.cfn_state.table.filter, "");
12870
12871 app.cfn_state.input_focus = InputFocus::Filter;
12873 app.handle_action(Action::FilterInput('t'));
12874 app.handle_action(Action::FilterInput('e'));
12875 app.handle_action(Action::FilterInput('s'));
12876 app.handle_action(Action::FilterInput('t'));
12877 assert_eq!(app.cfn_state.table.filter, "test");
12878 }
12879
12880 #[test]
12881 fn test_cfn_input_focus_resets_on_start() {
12882 let mut app = test_app();
12883 app.current_service = Service::CloudFormationStacks;
12884 app.service_selected = true;
12885 app.mode = Mode::Normal;
12886 app.cfn_state.input_focus = InputFocus::Pagination;
12887
12888 app.handle_action(Action::StartFilter);
12890 assert_eq!(app.mode, Mode::FilterInput);
12891 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12892 }
12893
12894 #[test]
12895 fn test_iam_roles_input_focus_cycles_forward() {
12896 let mut app = test_app();
12897 app.current_service = Service::IamRoles;
12898 app.mode = Mode::FilterInput;
12899 app.iam_state.role_input_focus = InputFocus::Filter;
12900
12901 app.handle_action(Action::NextFilterFocus);
12902 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12903
12904 app.handle_action(Action::NextFilterFocus);
12905 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12906 }
12907
12908 #[test]
12909 fn test_iam_roles_input_focus_cycles_backward() {
12910 let mut app = test_app();
12911 app.current_service = Service::IamRoles;
12912 app.mode = Mode::FilterInput;
12913 app.iam_state.role_input_focus = InputFocus::Filter;
12914
12915 app.handle_action(Action::PrevFilterFocus);
12916 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12917
12918 app.handle_action(Action::PrevFilterFocus);
12919 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12920 }
12921
12922 #[test]
12923 fn test_iam_roles_filter_input_only_when_focused() {
12924 let mut app = test_app();
12925 app.current_service = Service::IamRoles;
12926 app.mode = Mode::FilterInput;
12927 app.iam_state.role_input_focus = InputFocus::Pagination;
12928 app.iam_state.roles.filter = String::new();
12929
12930 app.handle_action(Action::FilterInput('t'));
12932 app.handle_action(Action::FilterInput('e'));
12933 app.handle_action(Action::FilterInput('s'));
12934 app.handle_action(Action::FilterInput('t'));
12935 assert_eq!(app.iam_state.roles.filter, "");
12936
12937 app.iam_state.role_input_focus = InputFocus::Filter;
12939 app.handle_action(Action::FilterInput('t'));
12940 app.handle_action(Action::FilterInput('e'));
12941 app.handle_action(Action::FilterInput('s'));
12942 app.handle_action(Action::FilterInput('t'));
12943 assert_eq!(app.iam_state.roles.filter, "test");
12944 }
12945
12946 #[test]
12947 fn test_iam_roles_page_down_updates_scroll_offset() {
12948 let mut app = test_app();
12949 app.current_service = Service::IamRoles;
12950 app.mode = Mode::Normal;
12951 app.iam_state.roles.items = (0..50)
12952 .map(|i| crate::iam::IamRole {
12953 role_name: format!("role-{}", i),
12954 path: "/".to_string(),
12955 trusted_entities: "AWS Service".to_string(),
12956 last_activity: "N/A".to_string(),
12957 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
12958 creation_time: "2024-01-01".to_string(),
12959 description: String::new(),
12960 max_session_duration: Some(3600),
12961 })
12962 .collect();
12963
12964 app.iam_state.roles.selected = 0;
12965 app.iam_state.roles.scroll_offset = 0;
12966
12967 app.handle_action(Action::PageDown);
12969 assert_eq!(app.iam_state.roles.selected, 10);
12970 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12972
12973 app.handle_action(Action::PageDown);
12975 assert_eq!(app.iam_state.roles.selected, 20);
12976 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12977 }
12978
12979 #[test]
12980 fn test_application_selection_and_deployments_tab() {
12981 use crate::lambda::Application as LambdaApplication;
12982 use LambdaApplicationDetailTab;
12983
12984 let mut app = test_app();
12985 app.current_service = Service::LambdaApplications;
12986 app.service_selected = true;
12987 app.mode = Mode::Normal;
12988
12989 app.lambda_application_state.table.items = vec![LambdaApplication {
12990 name: "test-app".to_string(),
12991 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12992 description: "Test application".to_string(),
12993 status: "CREATE_COMPLETE".to_string(),
12994 last_modified: "2024-01-01".to_string(),
12995 }];
12996
12997 app.handle_action(Action::Select);
12999 assert_eq!(
13000 app.lambda_application_state.current_application,
13001 Some("test-app".to_string())
13002 );
13003 assert_eq!(
13004 app.lambda_application_state.detail_tab,
13005 LambdaApplicationDetailTab::Overview
13006 );
13007
13008 app.handle_action(Action::NextDetailTab);
13010 assert_eq!(
13011 app.lambda_application_state.detail_tab,
13012 LambdaApplicationDetailTab::Deployments
13013 );
13014
13015 app.handle_action(Action::GoBack);
13017 assert_eq!(app.lambda_application_state.current_application, None);
13018 }
13019
13020 #[test]
13021 fn test_application_resources_filter_and_pagination() {
13022 use crate::lambda::Application as LambdaApplication;
13023 use LambdaApplicationDetailTab;
13024
13025 let mut app = test_app();
13026 app.current_service = Service::LambdaApplications;
13027 app.service_selected = true;
13028 app.mode = Mode::Normal;
13029
13030 app.lambda_application_state.table.items = vec![LambdaApplication {
13031 name: "test-app".to_string(),
13032 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13033 description: "Test application".to_string(),
13034 status: "CREATE_COMPLETE".to_string(),
13035 last_modified: "2024-01-01".to_string(),
13036 }];
13037
13038 app.handle_action(Action::Select);
13040 assert_eq!(
13041 app.lambda_application_state.detail_tab,
13042 LambdaApplicationDetailTab::Overview
13043 );
13044
13045 assert!(!app.lambda_application_state.resources.items.is_empty());
13047
13048 app.mode = Mode::FilterInput;
13050 assert_eq!(
13051 app.lambda_application_state.resource_input_focus,
13052 InputFocus::Filter
13053 );
13054
13055 app.handle_action(Action::NextFilterFocus);
13056 assert_eq!(
13057 app.lambda_application_state.resource_input_focus,
13058 InputFocus::Pagination
13059 );
13060
13061 app.handle_action(Action::PrevFilterFocus);
13062 assert_eq!(
13063 app.lambda_application_state.resource_input_focus,
13064 InputFocus::Filter
13065 );
13066 }
13067
13068 #[test]
13069 fn test_application_deployments_filter_and_pagination() {
13070 use crate::lambda::Application as LambdaApplication;
13071 use LambdaApplicationDetailTab;
13072
13073 let mut app = test_app();
13074 app.current_service = Service::LambdaApplications;
13075 app.service_selected = true;
13076 app.mode = Mode::Normal;
13077
13078 app.lambda_application_state.table.items = vec![LambdaApplication {
13079 name: "test-app".to_string(),
13080 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13081 description: "Test application".to_string(),
13082 status: "CREATE_COMPLETE".to_string(),
13083 last_modified: "2024-01-01".to_string(),
13084 }];
13085
13086 app.handle_action(Action::Select);
13088 app.handle_action(Action::NextDetailTab);
13089 assert_eq!(
13090 app.lambda_application_state.detail_tab,
13091 LambdaApplicationDetailTab::Deployments
13092 );
13093
13094 assert!(!app.lambda_application_state.deployments.items.is_empty());
13096
13097 app.mode = Mode::FilterInput;
13099 assert_eq!(
13100 app.lambda_application_state.deployment_input_focus,
13101 InputFocus::Filter
13102 );
13103
13104 app.handle_action(Action::NextFilterFocus);
13105 assert_eq!(
13106 app.lambda_application_state.deployment_input_focus,
13107 InputFocus::Pagination
13108 );
13109
13110 app.handle_action(Action::PrevFilterFocus);
13111 assert_eq!(
13112 app.lambda_application_state.deployment_input_focus,
13113 InputFocus::Filter
13114 );
13115 }
13116
13117 #[test]
13118 fn test_application_resource_expansion() {
13119 use crate::lambda::Application as LambdaApplication;
13120 use LambdaApplicationDetailTab;
13121
13122 let mut app = test_app();
13123 app.current_service = Service::LambdaApplications;
13124 app.service_selected = true;
13125 app.mode = Mode::Normal;
13126
13127 app.lambda_application_state.table.items = vec![LambdaApplication {
13128 name: "test-app".to_string(),
13129 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13130 description: "Test application".to_string(),
13131 status: "CREATE_COMPLETE".to_string(),
13132 last_modified: "2024-01-01".to_string(),
13133 }];
13134
13135 app.handle_action(Action::Select);
13137 assert_eq!(
13138 app.lambda_application_state.detail_tab,
13139 LambdaApplicationDetailTab::Overview
13140 );
13141
13142 app.handle_action(Action::NextPane);
13144 assert_eq!(
13145 app.lambda_application_state.resources.expanded_item,
13146 Some(0)
13147 );
13148
13149 app.handle_action(Action::PrevPane);
13151 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
13152 }
13153
13154 #[test]
13155 fn test_application_deployment_expansion() {
13156 use crate::lambda::Application as LambdaApplication;
13157 use LambdaApplicationDetailTab;
13158
13159 let mut app = test_app();
13160 app.current_service = Service::LambdaApplications;
13161 app.service_selected = true;
13162 app.mode = Mode::Normal;
13163
13164 app.lambda_application_state.table.items = vec![LambdaApplication {
13165 name: "test-app".to_string(),
13166 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13167 description: "Test application".to_string(),
13168 status: "CREATE_COMPLETE".to_string(),
13169 last_modified: "2024-01-01".to_string(),
13170 }];
13171
13172 app.handle_action(Action::Select);
13174 app.handle_action(Action::NextDetailTab);
13175 assert_eq!(
13176 app.lambda_application_state.detail_tab,
13177 LambdaApplicationDetailTab::Deployments
13178 );
13179
13180 app.handle_action(Action::NextPane);
13182 assert_eq!(
13183 app.lambda_application_state.deployments.expanded_item,
13184 Some(0)
13185 );
13186
13187 app.handle_action(Action::PrevPane);
13189 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
13190 }
13191
13192 #[test]
13193 fn test_s3_nested_prefix_expansion() {
13194 use crate::s3::Bucket;
13195 use crate::s3::Object as S3Object;
13196
13197 let mut app = test_app();
13198 app.current_service = Service::S3Buckets;
13199 app.service_selected = true;
13200 app.mode = Mode::Normal;
13201
13202 app.s3_state.buckets.items = vec![Bucket {
13204 name: "test-bucket".to_string(),
13205 region: "us-east-1".to_string(),
13206 creation_date: "2024-01-01".to_string(),
13207 }];
13208
13209 app.s3_state.bucket_preview.insert(
13211 "test-bucket".to_string(),
13212 vec![S3Object {
13213 key: "level1/".to_string(),
13214 size: 0,
13215 last_modified: "".to_string(),
13216 is_prefix: true,
13217 storage_class: "".to_string(),
13218 }],
13219 );
13220
13221 app.s3_state.prefix_preview.insert(
13223 "level1/".to_string(),
13224 vec![S3Object {
13225 key: "level1/level2/".to_string(),
13226 size: 0,
13227 last_modified: "".to_string(),
13228 is_prefix: true,
13229 storage_class: "".to_string(),
13230 }],
13231 );
13232
13233 app.s3_state.selected_row = 0;
13235 app.handle_action(Action::NextPane);
13236 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13237
13238 app.s3_state.selected_row = 1;
13240 app.handle_action(Action::NextPane);
13241 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13242
13243 app.s3_state.selected_row = 2;
13245 app.handle_action(Action::NextPane);
13246 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
13247
13248 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13250 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13251 }
13252
13253 #[test]
13254 fn test_s3_nested_prefix_collapse() {
13255 use crate::s3::Bucket;
13256 use crate::s3::Object as S3Object;
13257
13258 let mut app = test_app();
13259 app.current_service = Service::S3Buckets;
13260 app.service_selected = true;
13261 app.mode = Mode::Normal;
13262
13263 app.s3_state.buckets.items = vec![Bucket {
13264 name: "test-bucket".to_string(),
13265 region: "us-east-1".to_string(),
13266 creation_date: "2024-01-01".to_string(),
13267 }];
13268
13269 app.s3_state.bucket_preview.insert(
13270 "test-bucket".to_string(),
13271 vec![S3Object {
13272 key: "level1/".to_string(),
13273 size: 0,
13274 last_modified: "".to_string(),
13275 is_prefix: true,
13276 storage_class: "".to_string(),
13277 }],
13278 );
13279
13280 app.s3_state.prefix_preview.insert(
13281 "level1/".to_string(),
13282 vec![S3Object {
13283 key: "level1/level2/".to_string(),
13284 size: 0,
13285 last_modified: "".to_string(),
13286 is_prefix: true,
13287 storage_class: "".to_string(),
13288 }],
13289 );
13290
13291 app.s3_state
13293 .expanded_prefixes
13294 .insert("test-bucket".to_string());
13295 app.s3_state.expanded_prefixes.insert("level1/".to_string());
13296 app.s3_state
13297 .expanded_prefixes
13298 .insert("level1/level2/".to_string());
13299
13300 app.s3_state.selected_row = 2;
13302 app.handle_action(Action::PrevPane);
13303 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
13304 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
13308 app.handle_action(Action::PrevPane);
13309 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
13310 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
13314 app.handle_action(Action::PrevPane);
13315 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
13316 }
13317}
13318
13319#[cfg(test)]
13320mod sqs_tests {
13321 use super::*;
13322 use test_helpers::*;
13323
13324 #[test]
13325 fn test_sqs_filter_input() {
13326 let mut app = test_app();
13327 app.current_service = Service::SqsQueues;
13328 app.service_selected = true;
13329 app.mode = Mode::FilterInput;
13330
13331 app.handle_action(Action::FilterInput('t'));
13332 app.handle_action(Action::FilterInput('e'));
13333 app.handle_action(Action::FilterInput('s'));
13334 app.handle_action(Action::FilterInput('t'));
13335 assert_eq!(app.sqs_state.queues.filter, "test");
13336
13337 app.handle_action(Action::FilterBackspace);
13338 assert_eq!(app.sqs_state.queues.filter, "tes");
13339 }
13340
13341 #[test]
13342 fn test_sqs_start_filter() {
13343 let mut app = test_app();
13344 app.current_service = Service::SqsQueues;
13345 app.service_selected = true;
13346 app.mode = Mode::Normal;
13347
13348 app.handle_action(Action::StartFilter);
13349 assert_eq!(app.mode, Mode::FilterInput);
13350 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13351 }
13352
13353 #[test]
13354 fn test_sqs_filter_focus_cycling() {
13355 let mut app = test_app();
13356 app.current_service = Service::SqsQueues;
13357 app.service_selected = true;
13358 app.mode = Mode::FilterInput;
13359 app.sqs_state.input_focus = InputFocus::Filter;
13360
13361 app.handle_action(Action::NextFilterFocus);
13362 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13363
13364 app.handle_action(Action::NextFilterFocus);
13365 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13366
13367 app.handle_action(Action::PrevFilterFocus);
13368 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13369 }
13370
13371 #[test]
13372 fn test_sqs_navigation() {
13373 let mut app = test_app();
13374 app.current_service = Service::SqsQueues;
13375 app.service_selected = true;
13376 app.mode = Mode::Normal;
13377 app.sqs_state.queues.items = (0..10)
13378 .map(|i| crate::sqs::Queue {
13379 name: format!("queue{}", i),
13380 url: String::new(),
13381 queue_type: "Standard".to_string(),
13382 created_timestamp: String::new(),
13383 messages_available: "0".to_string(),
13384 messages_in_flight: "0".to_string(),
13385 encryption: "Disabled".to_string(),
13386 content_based_deduplication: "Disabled".to_string(),
13387 last_modified_timestamp: String::new(),
13388 visibility_timeout: String::new(),
13389 message_retention_period: String::new(),
13390 maximum_message_size: String::new(),
13391 delivery_delay: String::new(),
13392 receive_message_wait_time: String::new(),
13393 high_throughput_fifo: "N/A".to_string(),
13394 deduplication_scope: "N/A".to_string(),
13395 fifo_throughput_limit: "N/A".to_string(),
13396 dead_letter_queue: "-".to_string(),
13397 messages_delayed: "0".to_string(),
13398 redrive_allow_policy: "-".to_string(),
13399 redrive_policy: "".to_string(),
13400 redrive_task_id: "-".to_string(),
13401 redrive_task_start_time: "-".to_string(),
13402 redrive_task_status: "-".to_string(),
13403 redrive_task_percent: "-".to_string(),
13404 redrive_task_destination: "-".to_string(),
13405 })
13406 .collect();
13407
13408 app.handle_action(Action::NextItem);
13409 assert_eq!(app.sqs_state.queues.selected, 1);
13410
13411 app.handle_action(Action::PrevItem);
13412 assert_eq!(app.sqs_state.queues.selected, 0);
13413 }
13414
13415 #[test]
13416 fn test_sqs_page_navigation() {
13417 let mut app = test_app();
13418 app.current_service = Service::SqsQueues;
13419 app.service_selected = true;
13420 app.mode = Mode::Normal;
13421 app.sqs_state.queues.items = (0..100)
13422 .map(|i| crate::sqs::Queue {
13423 name: format!("queue{}", i),
13424 url: String::new(),
13425 queue_type: "Standard".to_string(),
13426 created_timestamp: String::new(),
13427 messages_available: "0".to_string(),
13428 messages_in_flight: "0".to_string(),
13429 encryption: "Disabled".to_string(),
13430 content_based_deduplication: "Disabled".to_string(),
13431 last_modified_timestamp: String::new(),
13432 visibility_timeout: String::new(),
13433 message_retention_period: String::new(),
13434 maximum_message_size: String::new(),
13435 delivery_delay: String::new(),
13436 receive_message_wait_time: String::new(),
13437 high_throughput_fifo: "N/A".to_string(),
13438 deduplication_scope: "N/A".to_string(),
13439 fifo_throughput_limit: "N/A".to_string(),
13440 dead_letter_queue: "-".to_string(),
13441 messages_delayed: "0".to_string(),
13442 redrive_allow_policy: "-".to_string(),
13443 redrive_policy: "".to_string(),
13444 redrive_task_id: "-".to_string(),
13445 redrive_task_start_time: "-".to_string(),
13446 redrive_task_status: "-".to_string(),
13447 redrive_task_percent: "-".to_string(),
13448 redrive_task_destination: "-".to_string(),
13449 })
13450 .collect();
13451
13452 app.handle_action(Action::PageDown);
13453 assert_eq!(app.sqs_state.queues.selected, 10);
13454
13455 app.handle_action(Action::PageUp);
13456 assert_eq!(app.sqs_state.queues.selected, 0);
13457 }
13458
13459 #[test]
13460 fn test_sqs_queue_expansion() {
13461 let mut app = test_app();
13462 app.current_service = Service::SqsQueues;
13463 app.service_selected = true;
13464 app.sqs_state.queues.items = vec![crate::sqs::Queue {
13465 name: "my-queue".to_string(),
13466 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13467 queue_type: "Standard".to_string(),
13468 created_timestamp: "2023-01-01".to_string(),
13469 messages_available: "5".to_string(),
13470 messages_in_flight: "2".to_string(),
13471 encryption: "Enabled".to_string(),
13472 content_based_deduplication: "Disabled".to_string(),
13473 last_modified_timestamp: "2023-01-02".to_string(),
13474 visibility_timeout: "30".to_string(),
13475 message_retention_period: "345600".to_string(),
13476 maximum_message_size: "262144".to_string(),
13477 delivery_delay: "0".to_string(),
13478 receive_message_wait_time: "0".to_string(),
13479 high_throughput_fifo: "N/A".to_string(),
13480 deduplication_scope: "N/A".to_string(),
13481 fifo_throughput_limit: "N/A".to_string(),
13482 dead_letter_queue: "-".to_string(),
13483 messages_delayed: "0".to_string(),
13484 redrive_allow_policy: "-".to_string(),
13485 redrive_policy: "".to_string(),
13486 redrive_task_id: "-".to_string(),
13487 redrive_task_start_time: "-".to_string(),
13488 redrive_task_status: "-".to_string(),
13489 redrive_task_percent: "-".to_string(),
13490 redrive_task_destination: "-".to_string(),
13491 }];
13492 app.sqs_state.queues.selected = 0;
13493
13494 assert_eq!(app.sqs_state.queues.expanded_item, None);
13495
13496 app.handle_action(Action::NextPane);
13498 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13499
13500 app.handle_action(Action::NextPane);
13502 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13503
13504 app.handle_action(Action::PrevPane);
13506 assert_eq!(app.sqs_state.queues.expanded_item, None);
13507
13508 app.handle_action(Action::PrevPane);
13510 assert_eq!(app.sqs_state.queues.expanded_item, None);
13511 }
13512
13513 #[test]
13514 fn test_sqs_column_toggle() {
13515 use crate::sqs::queue::Column as SqsColumn;
13516 let mut app = test_app();
13517 app.current_service = Service::SqsQueues;
13518 app.service_selected = true;
13519 app.mode = Mode::ColumnSelector;
13520
13521 app.sqs_visible_column_ids = SqsColumn::ids();
13523 let initial_count = app.sqs_visible_column_ids.len();
13524
13525 app.column_selector_index = 0;
13527 app.handle_action(Action::ToggleColumn);
13528
13529 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
13531 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13532
13533 app.handle_action(Action::ToggleColumn);
13535 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
13536 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13537 }
13538
13539 #[test]
13540 fn test_sqs_column_selector_navigation() {
13541 let mut app = test_app();
13542 app.current_service = Service::SqsQueues;
13543 app.service_selected = true;
13544 app.mode = Mode::ColumnSelector;
13545 app.column_selector_index = 0;
13546
13547 let max_index = app.sqs_column_ids.len() - 1;
13549
13550 for _ in 0..max_index {
13552 app.handle_action(Action::NextItem);
13553 }
13554 assert_eq!(app.column_selector_index, max_index);
13555
13556 for _ in 0..max_index {
13558 app.handle_action(Action::PrevItem);
13559 }
13560 assert_eq!(app.column_selector_index, 0);
13561 }
13562
13563 #[test]
13564 fn test_sqs_queue_selection() {
13565 let mut app = test_app();
13566 app.current_service = Service::SqsQueues;
13567 app.service_selected = true;
13568 app.mode = Mode::Normal;
13569 app.sqs_state.queues.items = vec![crate::sqs::Queue {
13570 name: "my-queue".to_string(),
13571 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13572 queue_type: "Standard".to_string(),
13573 created_timestamp: "2023-01-01".to_string(),
13574 messages_available: "5".to_string(),
13575 messages_in_flight: "2".to_string(),
13576 encryption: "Enabled".to_string(),
13577 content_based_deduplication: "Disabled".to_string(),
13578 last_modified_timestamp: "2023-01-02".to_string(),
13579 visibility_timeout: "30".to_string(),
13580 message_retention_period: "345600".to_string(),
13581 maximum_message_size: "262144".to_string(),
13582 delivery_delay: "0".to_string(),
13583 receive_message_wait_time: "0".to_string(),
13584 high_throughput_fifo: "N/A".to_string(),
13585 deduplication_scope: "N/A".to_string(),
13586 fifo_throughput_limit: "N/A".to_string(),
13587 dead_letter_queue: "-".to_string(),
13588 messages_delayed: "0".to_string(),
13589 redrive_allow_policy: "-".to_string(),
13590 redrive_policy: "".to_string(),
13591 redrive_task_id: "-".to_string(),
13592 redrive_task_start_time: "-".to_string(),
13593 redrive_task_status: "-".to_string(),
13594 redrive_task_percent: "-".to_string(),
13595 redrive_task_destination: "-".to_string(),
13596 }];
13597 app.sqs_state.queues.selected = 0;
13598
13599 assert_eq!(app.sqs_state.current_queue, None);
13600
13601 app.handle_action(Action::Select);
13603 assert_eq!(
13604 app.sqs_state.current_queue,
13605 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
13606 );
13607
13608 app.handle_action(Action::GoBack);
13610 assert_eq!(app.sqs_state.current_queue, None);
13611 }
13612
13613 #[test]
13614 fn test_sqs_lambda_triggers_expand_collapse() {
13615 let mut app = test_app();
13616 app.current_service = Service::SqsQueues;
13617 app.service_selected = true;
13618 app.sqs_state.current_queue =
13619 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13620 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13621 app.sqs_state.triggers.items = vec![crate::sqs::LambdaTrigger {
13622 uuid: "test-uuid".to_string(),
13623 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13624 status: "Enabled".to_string(),
13625 last_modified: "2024-01-01T00:00:00Z".to_string(),
13626 }];
13627 app.sqs_state.triggers.selected = 0;
13628
13629 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13630
13631 app.handle_action(Action::NextPane);
13633 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13634
13635 app.handle_action(Action::PrevPane);
13637 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13638 }
13639
13640 #[test]
13641 fn test_sqs_lambda_triggers_expand_toggle() {
13642 let mut app = test_app();
13643 app.current_service = Service::SqsQueues;
13644 app.service_selected = true;
13645 app.sqs_state.current_queue =
13646 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13647 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13648 app.sqs_state.triggers.items = vec![crate::sqs::LambdaTrigger {
13649 uuid: "test-uuid".to_string(),
13650 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13651 status: "Enabled".to_string(),
13652 last_modified: "2024-01-01T00:00:00Z".to_string(),
13653 }];
13654 app.sqs_state.triggers.selected = 0;
13655
13656 app.handle_action(Action::NextPane);
13658 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13659
13660 app.handle_action(Action::NextPane);
13662 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13663
13664 app.handle_action(Action::NextPane);
13666 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13667 }
13668
13669 #[test]
13670 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
13671 use crate::ui::sqs::filtered_lambda_triggers;
13672
13673 let mut app = test_app();
13674 app.current_service = Service::SqsQueues;
13675 app.service_selected = true;
13676 app.sqs_state.current_queue =
13677 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13678 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13679 app.sqs_state.triggers.items = vec![
13680 crate::sqs::LambdaTrigger {
13681 uuid: "uuid-3".to_string(),
13682 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
13683 status: "Enabled".to_string(),
13684 last_modified: "2024-03-01T00:00:00Z".to_string(),
13685 },
13686 crate::sqs::LambdaTrigger {
13687 uuid: "uuid-1".to_string(),
13688 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
13689 status: "Enabled".to_string(),
13690 last_modified: "2024-01-01T00:00:00Z".to_string(),
13691 },
13692 crate::sqs::LambdaTrigger {
13693 uuid: "uuid-2".to_string(),
13694 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
13695 status: "Enabled".to_string(),
13696 last_modified: "2024-02-01T00:00:00Z".to_string(),
13697 },
13698 ];
13699
13700 let sorted = filtered_lambda_triggers(&app);
13701
13702 assert_eq!(sorted.len(), 3);
13704 assert_eq!(sorted[0].uuid, "uuid-1");
13705 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
13706 assert_eq!(sorted[1].uuid, "uuid-2");
13707 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
13708 assert_eq!(sorted[2].uuid, "uuid-3");
13709 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
13710 }
13711
13712 #[test]
13713 fn test_sqs_lambda_triggers_filter_input() {
13714 let mut app = test_app();
13715 app.current_service = Service::SqsQueues;
13716 app.service_selected = true;
13717 app.mode = Mode::FilterInput;
13718 app.sqs_state.current_queue =
13719 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13720 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13721 app.sqs_state.input_focus = InputFocus::Filter;
13722
13723 assert_eq!(app.sqs_state.triggers.filter, "");
13724
13725 app.handle_action(Action::FilterInput('t'));
13727 assert_eq!(app.sqs_state.triggers.filter, "t");
13728
13729 app.handle_action(Action::FilterInput('e'));
13730 assert_eq!(app.sqs_state.triggers.filter, "te");
13731
13732 app.handle_action(Action::FilterInput('s'));
13733 assert_eq!(app.sqs_state.triggers.filter, "tes");
13734
13735 app.handle_action(Action::FilterInput('t'));
13736 assert_eq!(app.sqs_state.triggers.filter, "test");
13737
13738 app.handle_action(Action::FilterBackspace);
13740 assert_eq!(app.sqs_state.triggers.filter, "tes");
13741 }
13742
13743 #[test]
13744 fn test_sqs_lambda_triggers_filter_applied() {
13745 use crate::ui::sqs::filtered_lambda_triggers;
13746
13747 let mut app = test_app();
13748 app.current_service = Service::SqsQueues;
13749 app.service_selected = true;
13750 app.sqs_state.current_queue =
13751 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13752 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13753 app.sqs_state.triggers.items = vec![
13754 crate::sqs::LambdaTrigger {
13755 uuid: "uuid-1".to_string(),
13756 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
13757 status: "Enabled".to_string(),
13758 last_modified: "2024-01-01T00:00:00Z".to_string(),
13759 },
13760 crate::sqs::LambdaTrigger {
13761 uuid: "uuid-2".to_string(),
13762 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
13763 status: "Enabled".to_string(),
13764 last_modified: "2024-02-01T00:00:00Z".to_string(),
13765 },
13766 crate::sqs::LambdaTrigger {
13767 uuid: "uuid-3".to_string(),
13768 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
13769 status: "Enabled".to_string(),
13770 last_modified: "2024-03-01T00:00:00Z".to_string(),
13771 },
13772 ];
13773
13774 let filtered = filtered_lambda_triggers(&app);
13776 assert_eq!(filtered.len(), 3);
13777
13778 app.sqs_state.triggers.filter = "alpha".to_string();
13780 let filtered = filtered_lambda_triggers(&app);
13781 assert_eq!(filtered.len(), 1);
13782 assert_eq!(
13783 filtered[0].arn,
13784 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13785 );
13786
13787 app.sqs_state.triggers.filter = "test".to_string();
13789 let filtered = filtered_lambda_triggers(&app);
13790 assert_eq!(filtered.len(), 2);
13791 assert_eq!(
13792 filtered[0].arn,
13793 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13794 );
13795 assert_eq!(
13796 filtered[1].arn,
13797 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
13798 );
13799
13800 app.sqs_state.triggers.filter = "uuid-3".to_string();
13802 let filtered = filtered_lambda_triggers(&app);
13803 assert_eq!(filtered.len(), 1);
13804 assert_eq!(filtered[0].uuid, "uuid-3");
13805 }
13806
13807 #[test]
13808 fn test_sqs_triggers_navigation() {
13809 let mut app = test_app();
13810 app.service_selected = true;
13811 app.mode = Mode::Normal;
13812 app.current_service = Service::SqsQueues;
13813 app.sqs_state.current_queue = Some("test-queue".to_string());
13814 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13815 app.sqs_state.triggers.items = vec![
13816 crate::sqs::LambdaTrigger {
13817 uuid: "1".to_string(),
13818 arn: "arn1".to_string(),
13819 status: "Enabled".to_string(),
13820 last_modified: "2024-01-01".to_string(),
13821 },
13822 crate::sqs::LambdaTrigger {
13823 uuid: "2".to_string(),
13824 arn: "arn2".to_string(),
13825 status: "Enabled".to_string(),
13826 last_modified: "2024-01-02".to_string(),
13827 },
13828 ];
13829
13830 assert_eq!(app.sqs_state.triggers.selected, 0);
13831 app.next_item();
13832 assert_eq!(app.sqs_state.triggers.selected, 1);
13833 app.prev_item();
13834 assert_eq!(app.sqs_state.triggers.selected, 0);
13835 }
13836
13837 #[test]
13838 fn test_sqs_pipes_navigation() {
13839 let mut app = test_app();
13840 app.service_selected = true;
13841 app.mode = Mode::Normal;
13842 app.current_service = Service::SqsQueues;
13843 app.sqs_state.current_queue = Some("test-queue".to_string());
13844 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
13845 app.sqs_state.pipes.items = vec![
13846 crate::sqs::EventBridgePipe {
13847 name: "pipe1".to_string(),
13848 status: "RUNNING".to_string(),
13849 target: "target1".to_string(),
13850 last_modified: "2024-01-01".to_string(),
13851 },
13852 crate::sqs::EventBridgePipe {
13853 name: "pipe2".to_string(),
13854 status: "RUNNING".to_string(),
13855 target: "target2".to_string(),
13856 last_modified: "2024-01-02".to_string(),
13857 },
13858 ];
13859
13860 assert_eq!(app.sqs_state.pipes.selected, 0);
13861 app.next_item();
13862 assert_eq!(app.sqs_state.pipes.selected, 1);
13863 app.prev_item();
13864 assert_eq!(app.sqs_state.pipes.selected, 0);
13865 }
13866
13867 #[test]
13868 fn test_sqs_tags_navigation() {
13869 let mut app = test_app();
13870 app.service_selected = true;
13871 app.mode = Mode::Normal;
13872 app.current_service = Service::SqsQueues;
13873 app.sqs_state.current_queue = Some("test-queue".to_string());
13874 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
13875 app.sqs_state.tags.items = vec![
13876 crate::sqs::QueueTag {
13877 key: "Env".to_string(),
13878 value: "prod".to_string(),
13879 },
13880 crate::sqs::QueueTag {
13881 key: "Team".to_string(),
13882 value: "backend".to_string(),
13883 },
13884 ];
13885
13886 assert_eq!(app.sqs_state.tags.selected, 0);
13887 app.next_item();
13888 assert_eq!(app.sqs_state.tags.selected, 1);
13889 app.prev_item();
13890 assert_eq!(app.sqs_state.tags.selected, 0);
13891 }
13892
13893 #[test]
13894 fn test_sqs_queues_navigation() {
13895 let mut app = test_app();
13896 app.service_selected = true;
13897 app.mode = Mode::Normal;
13898 app.current_service = Service::SqsQueues;
13899 app.sqs_state.queues.items = vec![
13900 crate::sqs::Queue {
13901 name: "queue1".to_string(),
13902 url: "url1".to_string(),
13903 queue_type: "Standard".to_string(),
13904 created_timestamp: "".to_string(),
13905 messages_available: "0".to_string(),
13906 messages_in_flight: "0".to_string(),
13907 encryption: "Disabled".to_string(),
13908 content_based_deduplication: "Disabled".to_string(),
13909 last_modified_timestamp: "".to_string(),
13910 visibility_timeout: "".to_string(),
13911 message_retention_period: "".to_string(),
13912 maximum_message_size: "".to_string(),
13913 delivery_delay: "".to_string(),
13914 receive_message_wait_time: "".to_string(),
13915 high_throughput_fifo: "-".to_string(),
13916 deduplication_scope: "-".to_string(),
13917 fifo_throughput_limit: "-".to_string(),
13918 dead_letter_queue: "-".to_string(),
13919 messages_delayed: "0".to_string(),
13920 redrive_allow_policy: "-".to_string(),
13921 redrive_policy: "".to_string(),
13922 redrive_task_id: "-".to_string(),
13923 redrive_task_start_time: "-".to_string(),
13924 redrive_task_status: "-".to_string(),
13925 redrive_task_percent: "-".to_string(),
13926 redrive_task_destination: "-".to_string(),
13927 },
13928 crate::sqs::Queue {
13929 name: "queue2".to_string(),
13930 url: "url2".to_string(),
13931 queue_type: "Standard".to_string(),
13932 created_timestamp: "".to_string(),
13933 messages_available: "0".to_string(),
13934 messages_in_flight: "0".to_string(),
13935 encryption: "Disabled".to_string(),
13936 content_based_deduplication: "Disabled".to_string(),
13937 last_modified_timestamp: "".to_string(),
13938 visibility_timeout: "".to_string(),
13939 message_retention_period: "".to_string(),
13940 maximum_message_size: "".to_string(),
13941 delivery_delay: "".to_string(),
13942 receive_message_wait_time: "".to_string(),
13943 high_throughput_fifo: "-".to_string(),
13944 deduplication_scope: "-".to_string(),
13945 fifo_throughput_limit: "-".to_string(),
13946 dead_letter_queue: "-".to_string(),
13947 messages_delayed: "0".to_string(),
13948 redrive_allow_policy: "-".to_string(),
13949 redrive_policy: "".to_string(),
13950 redrive_task_id: "-".to_string(),
13951 redrive_task_start_time: "-".to_string(),
13952 redrive_task_status: "-".to_string(),
13953 redrive_task_percent: "-".to_string(),
13954 redrive_task_destination: "-".to_string(),
13955 },
13956 ];
13957
13958 assert_eq!(app.sqs_state.queues.selected, 0);
13959 app.next_item();
13960 assert_eq!(app.sqs_state.queues.selected, 1);
13961 app.prev_item();
13962 assert_eq!(app.sqs_state.queues.selected, 0);
13963 }
13964
13965 #[test]
13966 fn test_sqs_subscriptions_navigation() {
13967 let mut app = test_app();
13968 app.service_selected = true;
13969 app.mode = Mode::Normal;
13970 app.current_service = Service::SqsQueues;
13971 app.sqs_state.current_queue = Some("test-queue".to_string());
13972 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13973 app.sqs_state.subscriptions.items = vec![
13974 crate::sqs::SnsSubscription {
13975 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
13976 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
13977 },
13978 crate::sqs::SnsSubscription {
13979 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
13980 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
13981 },
13982 ];
13983
13984 assert_eq!(app.sqs_state.subscriptions.selected, 0);
13985 app.next_item();
13986 assert_eq!(app.sqs_state.subscriptions.selected, 1);
13987 app.prev_item();
13988 assert_eq!(app.sqs_state.subscriptions.selected, 0);
13989 }
13990
13991 #[test]
13992 fn test_sqs_subscription_region_dropdown_navigation() {
13993 let mut app = test_app();
13994 app.service_selected = true;
13995 app.mode = Mode::FilterInput;
13996 app.current_service = Service::SqsQueues;
13997 app.sqs_state.current_queue = Some("test-queue".to_string());
13998 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13999 app.sqs_state.input_focus = crate::common::InputFocus::Dropdown("SubscriptionRegion");
14000
14001 assert_eq!(app.sqs_state.subscription_region_selected, 0);
14002 app.next_item();
14003 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14004 app.next_item();
14005 assert_eq!(app.sqs_state.subscription_region_selected, 2);
14006 app.prev_item();
14007 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14008 app.prev_item();
14009 assert_eq!(app.sqs_state.subscription_region_selected, 0);
14010 }
14011
14012 #[test]
14013 fn test_sqs_subscription_region_selection() {
14014 let mut app = test_app();
14015 app.service_selected = true;
14016 app.mode = Mode::FilterInput;
14017 app.current_service = Service::SqsQueues;
14018 app.sqs_state.current_queue = Some("test-queue".to_string());
14019 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14020 app.sqs_state.input_focus = crate::common::InputFocus::Dropdown("SubscriptionRegion");
14021 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
14024 app.handle_action(Action::ApplyFilter);
14025 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
14026 assert_eq!(app.mode, Mode::Normal);
14027 }
14028
14029 #[test]
14030 fn test_s3_object_filter_resets_selection() {
14031 let mut app = test_app();
14032 app.service_selected = true;
14033 app.current_service = Service::S3Buckets;
14034 app.s3_state.current_bucket = Some("test-bucket".to_string());
14035 app.s3_state.selected_row = 5;
14036 app.mode = Mode::FilterInput;
14037
14038 app.handle_action(Action::CloseMenu);
14039
14040 assert_eq!(app.s3_state.selected_row, 0);
14041 assert_eq!(app.mode, Mode::Normal);
14042 }
14043
14044 #[test]
14045 fn test_s3_bucket_filter_resets_selection() {
14046 let mut app = test_app();
14047 app.service_selected = true;
14048 app.current_service = Service::S3Buckets;
14049 app.s3_state.selected_row = 10;
14050 app.mode = Mode::FilterInput;
14051
14052 app.handle_action(Action::CloseMenu);
14053
14054 assert_eq!(app.s3_state.selected_row, 0);
14055 assert_eq!(app.mode, Mode::Normal);
14056 }
14057
14058 #[test]
14059 fn test_s3_selection_stays_in_bounds() {
14060 let mut app = test_app();
14061 app.service_selected = true;
14062 app.current_service = Service::S3Buckets;
14063 app.s3_state.selected_row = 0;
14064 app.s3_state.selected_object = 0;
14065
14066 app.prev_item();
14068
14069 assert_eq!(app.s3_state.selected_row, 0);
14071 assert_eq!(app.s3_state.selected_object, 0);
14072 }
14073}