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};
6pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
7use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
8use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
9use crate::iam::{self, UserColumn};
10use crate::keymap::{Action, Mode};
11pub use crate::lambda::DeploymentColumn;
12pub use crate::lambda::ResourceColumn;
13pub use crate::lambda::{
14 Application as LambdaApplication, ApplicationColumn as LambdaApplicationColumn,
15 Function as LambdaFunction, FunctionColumn as LambdaColumn,
16};
17pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
18use crate::session::{Session, SessionTab};
19pub use crate::sqs::queue::Column as SqsColumn;
20pub use crate::sqs::trigger::Column as SqsTriggerColumn;
21use crate::table::TableState;
22use crate::ui::cfn::State as CfnStateConstants;
23pub use crate::ui::cfn::{
24 filtered_outputs, filtered_parameters, filtered_resources, DetailTab as CfnDetailTab,
25 State as CfnState, StatusFilter as CfnStatusFilter,
26};
27pub use crate::ui::cw::alarms::{AlarmTab, AlarmViewMode};
28use crate::ui::ec2;
29pub use crate::ui::ec2::{
30 State as Ec2State, StateFilter as Ec2StateFilter, STATE_FILTER as EC2_STATE_FILTER,
31};
32pub use crate::ui::ecr::{State as EcrState, Tab as EcrTab};
33use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
34pub use crate::ui::lambda::{
35 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
36 DetailTab as LambdaDetailTab, State as LambdaState,
37};
38use crate::ui::monitoring::MonitoringState;
39pub use crate::ui::s3::{BucketType as S3BucketType, ObjectTab as S3ObjectTab, State as S3State};
40pub use crate::ui::sqs::{QueueDetailTab as SqsQueueDetailTab, State as SqsState};
41pub use crate::ui::{
42 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, EventFilterFocus,
43 LogGroupColumn, Preferences, StreamColumn, StreamSort, TimeUnit,
44};
45use rusticity_core::{
46 AlarmsClient, AwsConfig, CloudFormationClient, CloudWatchClient, Ec2Client, EcrClient,
47 IamClient, LambdaClient, LogEvent, LogGroup, LogStream, S3Client, SqsClient,
48};
49
50#[derive(Clone)]
51pub struct Tab {
52 pub service: Service,
53 pub title: String,
54 pub breadcrumb: String,
55}
56
57pub struct App {
58 pub running: bool,
59 pub mode: Mode,
60 pub config: AwsConfig,
61 pub cloudwatch_client: CloudWatchClient,
62 pub s3_client: S3Client,
63 pub sqs_client: SqsClient,
64 pub alarms_client: AlarmsClient,
65 pub ec2_client: Ec2Client,
66 pub ecr_client: EcrClient,
67 pub iam_client: IamClient,
68 pub lambda_client: LambdaClient,
69 pub cloudformation_client: CloudFormationClient,
70 pub current_service: Service,
71 pub tabs: Vec<Tab>,
72 pub current_tab: usize,
73 pub tab_picker_selected: usize,
74 pub tab_filter: String,
75 pub pending_key: Option<char>,
76 pub log_groups_state: CloudWatchLogGroupsState,
77 pub insights_state: CloudWatchInsightsState,
78 pub alarms_state: CloudWatchAlarmsState,
79 pub s3_state: S3State,
80 pub sqs_state: SqsState,
81 pub ec2_state: Ec2State,
82 pub ecr_state: EcrState,
83 pub lambda_state: LambdaState,
84 pub lambda_application_state: LambdaApplicationState,
85 pub cfn_state: CfnState,
86 pub iam_state: IamState,
87 pub service_picker: ServicePickerState,
88 pub service_selected: bool,
89 pub profile: String,
90 pub region: String,
91 pub region_selector_index: usize,
92 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
93 pub cw_log_group_column_ids: Vec<ColumnId>,
94 pub column_selector_index: usize,
95 pub preference_section: Preferences,
96 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
97 pub cw_log_stream_column_ids: Vec<ColumnId>,
98 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
99 pub cw_log_event_column_ids: Vec<ColumnId>,
100 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
101 pub cw_alarm_column_ids: Vec<ColumnId>,
102 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
103 pub s3_bucket_column_ids: Vec<ColumnId>,
104 pub sqs_visible_column_ids: Vec<ColumnId>,
105 pub sqs_column_ids: Vec<ColumnId>,
106 pub ec2_visible_column_ids: Vec<ColumnId>,
107 pub ec2_column_ids: Vec<ColumnId>,
108 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
109 pub ecr_repo_column_ids: Vec<ColumnId>,
110 pub ecr_image_visible_column_ids: Vec<ColumnId>,
111 pub ecr_image_column_ids: Vec<ColumnId>,
112 pub lambda_application_visible_column_ids: Vec<ColumnId>,
113 pub lambda_application_column_ids: Vec<ColumnId>,
114 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
115 pub lambda_deployment_column_ids: Vec<ColumnId>,
116 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
117 pub lambda_resource_column_ids: Vec<ColumnId>,
118 pub cfn_visible_column_ids: Vec<ColumnId>,
119 pub cfn_column_ids: Vec<ColumnId>,
120 pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
121 pub cfn_parameter_column_ids: Vec<ColumnId>,
122 pub cfn_output_visible_column_ids: Vec<ColumnId>,
123 pub cfn_output_column_ids: Vec<ColumnId>,
124 pub cfn_resource_visible_column_ids: Vec<ColumnId>,
125 pub cfn_resource_column_ids: Vec<ColumnId>,
126 pub iam_user_visible_column_ids: Vec<ColumnId>,
127 pub iam_user_column_ids: Vec<ColumnId>,
128 pub iam_role_visible_column_ids: Vec<String>,
129 pub iam_role_column_ids: Vec<String>,
130 pub iam_group_visible_column_ids: Vec<String>,
131 pub iam_group_column_ids: Vec<String>,
132 pub iam_policy_visible_column_ids: Vec<String>,
133 pub iam_policy_column_ids: Vec<String>,
134 pub view_mode: ViewMode,
135 pub error_message: Option<String>,
136 pub error_scroll: usize,
137 pub page_input: String,
138 pub calendar_date: Option<time::Date>,
139 pub calendar_selecting: CalendarField,
140 pub cursor_pos: usize,
141 pub current_session: Option<Session>,
142 pub sessions: Vec<Session>,
143 pub session_picker_selected: usize,
144 pub session_filter: String,
145 pub region_filter: String,
146 pub region_picker_selected: usize,
147 pub region_latencies: std::collections::HashMap<String, u64>,
148 pub profile_filter: String,
149 pub profile_picker_selected: usize,
150 pub available_profiles: Vec<AwsProfile>,
151 pub snapshot_requested: bool,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq)]
155pub enum CalendarField {
156 StartDate,
157 EndDate,
158}
159
160pub struct CloudWatchInsightsState {
161 pub insights: InsightsState,
162 pub loading: bool,
163}
164
165pub struct CloudWatchAlarmsState {
166 pub table: TableState<Alarm>,
167 pub alarm_tab: AlarmTab,
168 pub view_as: AlarmViewMode,
169 pub wrap_lines: bool,
170 pub sort_column: String,
171 pub sort_direction: SortDirection,
172 pub input_focus: InputFocus,
173}
174
175impl PageSize {
176 pub fn value(&self) -> usize {
177 match self {
178 PageSize::Ten => 10,
179 PageSize::TwentyFive => 25,
180 PageSize::Fifty => 50,
181 PageSize::OneHundred => 100,
182 }
183 }
184
185 pub fn next(&self) -> Self {
186 match self {
187 PageSize::Ten => PageSize::TwentyFive,
188 PageSize::TwentyFive => PageSize::Fifty,
189 PageSize::Fifty => PageSize::OneHundred,
190 PageSize::OneHundred => PageSize::Ten,
191 }
192 }
193}
194
195pub struct ServicePickerState {
196 pub filter: String,
197 pub selected: usize,
198 pub services: Vec<&'static str>,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq)]
202pub enum ViewMode {
203 List,
204 Detail,
205 Events,
206 InsightsResults,
207 PolicyView,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq)]
211pub enum Service {
212 CloudWatchLogGroups,
213 CloudWatchInsights,
214 CloudWatchAlarms,
215 S3Buckets,
216 SqsQueues,
217 Ec2Instances,
218 EcrRepositories,
219 LambdaFunctions,
220 LambdaApplications,
221 CloudFormationStacks,
222 IamUsers,
223 IamRoles,
224 IamUserGroups,
225}
226
227impl Service {
228 pub fn name(&self) -> &str {
229 match self {
230 Service::CloudWatchLogGroups => "CloudWatch > Log Groups",
231 Service::CloudWatchInsights => "CloudWatch > Logs Insights",
232 Service::CloudWatchAlarms => "CloudWatch > Alarms",
233 Service::S3Buckets => "S3 > Buckets",
234 Service::SqsQueues => "SQS > Queues",
235 Service::Ec2Instances => "EC2 > Instances",
236 Service::EcrRepositories => "ECR > Repositories",
237 Service::LambdaFunctions => "Lambda > Functions",
238 Service::LambdaApplications => "Lambda > Applications",
239 Service::CloudFormationStacks => "CloudFormation > Stacks",
240 Service::IamUsers => "IAM > Users",
241 Service::IamRoles => "IAM > Roles",
242 Service::IamUserGroups => "IAM > User Groups",
243 }
244 }
245}
246
247fn copy_to_clipboard(text: &str) {
248 use std::io::Write;
249 use std::process::{Command, Stdio};
250 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
251 if let Some(mut stdin) = child.stdin.take() {
252 let _ = stdin.write_all(text.as_bytes());
253 }
254 let _ = child.wait();
255 }
256}
257
258fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
259 if max > 0 {
260 *selected = (*selected + page_size).min(max - 1);
261 }
262}
263
264impl App {
265 pub fn get_input_focus(&self) -> InputFocus {
266 InputFocus::Filter
267 }
268
269 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
270 if self.current_service == Service::CloudWatchAlarms {
271 Some(&mut self.alarms_state.table.filter)
272 } else if self.current_service == Service::Ec2Instances {
273 Some(&mut self.ec2_state.table.filter)
274 } else if self.current_service == Service::S3Buckets {
275 if self.s3_state.current_bucket.is_some() {
276 Some(&mut self.s3_state.object_filter)
277 } else {
278 Some(&mut self.s3_state.buckets.filter)
279 }
280 } else if self.current_service == Service::EcrRepositories {
281 if self.ecr_state.current_repository.is_some() {
282 Some(&mut self.ecr_state.images.filter)
283 } else {
284 Some(&mut self.ecr_state.repositories.filter)
285 }
286 } else if self.current_service == Service::SqsQueues {
287 if self.sqs_state.current_queue.is_some()
288 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
289 {
290 Some(&mut self.sqs_state.triggers.filter)
291 } else if self.sqs_state.current_queue.is_some()
292 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
293 {
294 Some(&mut self.sqs_state.pipes.filter)
295 } else if self.sqs_state.current_queue.is_some()
296 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
297 {
298 Some(&mut self.sqs_state.tags.filter)
299 } else if self.sqs_state.current_queue.is_some()
300 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
301 {
302 Some(&mut self.sqs_state.subscriptions.filter)
303 } else {
304 Some(&mut self.sqs_state.queues.filter)
305 }
306 } else if self.current_service == Service::LambdaFunctions {
307 if self.lambda_state.current_version.is_some()
308 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
309 {
310 Some(&mut self.lambda_state.alias_table.filter)
311 } else if self.lambda_state.current_function.is_some()
312 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
313 {
314 Some(&mut self.lambda_state.version_table.filter)
315 } else if self.lambda_state.current_function.is_some()
316 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
317 {
318 Some(&mut self.lambda_state.alias_table.filter)
319 } else {
320 Some(&mut self.lambda_state.table.filter)
321 }
322 } else if self.current_service == Service::LambdaApplications {
323 if self.lambda_application_state.current_application.is_some() {
324 if self.lambda_application_state.detail_tab
325 == LambdaApplicationDetailTab::Deployments
326 {
327 Some(&mut self.lambda_application_state.deployments.filter)
328 } else {
329 Some(&mut self.lambda_application_state.resources.filter)
330 }
331 } else {
332 Some(&mut self.lambda_application_state.table.filter)
333 }
334 } else if self.current_service == Service::CloudFormationStacks {
335 if self.cfn_state.current_stack.is_some()
336 && self.cfn_state.detail_tab == CfnDetailTab::Resources
337 {
338 Some(&mut self.cfn_state.resources.filter)
339 } else {
340 Some(&mut self.cfn_state.table.filter)
341 }
342 } else if self.current_service == Service::IamUsers {
343 if self.iam_state.current_user.is_some() {
344 if self.iam_state.user_tab == UserTab::Tags {
345 Some(&mut self.iam_state.user_tags.filter)
346 } else {
347 Some(&mut self.iam_state.policies.filter)
348 }
349 } else {
350 Some(&mut self.iam_state.users.filter)
351 }
352 } else if self.current_service == Service::IamRoles {
353 if self.iam_state.current_role.is_some() {
354 if self.iam_state.role_tab == RoleTab::Tags {
355 Some(&mut self.iam_state.tags.filter)
356 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
357 Some(&mut self.iam_state.last_accessed_filter)
358 } else {
359 Some(&mut self.iam_state.policies.filter)
360 }
361 } else {
362 Some(&mut self.iam_state.roles.filter)
363 }
364 } else if self.current_service == Service::IamUserGroups {
365 if self.iam_state.current_group.is_some() {
366 if self.iam_state.group_tab == GroupTab::Permissions {
367 Some(&mut self.iam_state.policies.filter)
368 } else if self.iam_state.group_tab == GroupTab::Users {
369 Some(&mut self.iam_state.group_users.filter)
370 } else {
371 None
372 }
373 } else {
374 Some(&mut self.iam_state.groups.filter)
375 }
376 } else if self.view_mode == ViewMode::List {
377 Some(&mut self.log_groups_state.log_groups.filter)
378 } else if self.view_mode == ViewMode::Detail
379 && self.log_groups_state.detail_tab == DetailTab::LogStreams
380 {
381 Some(&mut self.log_groups_state.stream_filter)
382 } else {
383 None
384 }
385 }
386
387 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
388 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
389 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
390
391 std::env::set_var("AWS_PROFILE", &profile_name);
392
393 let config = AwsConfig::new(region).await?;
394 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
395 let s3_client = S3Client::new(config.clone());
396 let sqs_client = SqsClient::new(config.clone());
397 let alarms_client = AlarmsClient::new(config.clone());
398 let ec2_client = Ec2Client::new(config.clone());
399 let ecr_client = EcrClient::new(config.clone());
400 let iam_client = IamClient::new(config.clone());
401 let lambda_client = LambdaClient::new(config.clone());
402 let cloudformation_client = CloudFormationClient::new(config.clone());
403 let region_name = config.region.clone();
404
405 Ok(Self {
406 running: true,
407 mode: Mode::ServicePicker,
408 config,
409 cloudwatch_client,
410 s3_client,
411 sqs_client,
412 alarms_client,
413 ec2_client,
414 ecr_client,
415 iam_client,
416 lambda_client,
417 cloudformation_client,
418 current_service: Service::CloudWatchLogGroups,
419 tabs: Vec::new(),
420 current_tab: 0,
421 tab_picker_selected: 0,
422 tab_filter: String::new(),
423 pending_key: None,
424 log_groups_state: CloudWatchLogGroupsState::new(),
425 insights_state: CloudWatchInsightsState::new(),
426 alarms_state: CloudWatchAlarmsState::new(),
427 s3_state: S3State::new(),
428 sqs_state: SqsState::new(),
429 ec2_state: Ec2State::default(),
430 ecr_state: EcrState::new(),
431 lambda_state: LambdaState::new(),
432 lambda_application_state: LambdaApplicationState::new(),
433 cfn_state: CfnState::new(),
434 iam_state: IamState::new(),
435 service_picker: ServicePickerState::new(),
436 service_selected: false,
437 profile: profile_name,
438 region: region_name,
439 region_selector_index: 0,
440 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
441 cw_log_group_column_ids: LogGroupColumn::ids(),
442 column_selector_index: 0,
443 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
444 cw_log_stream_column_ids: StreamColumn::ids(),
445 cw_log_event_visible_column_ids: EventColumn::default_visible(),
446 cw_log_event_column_ids: EventColumn::ids(),
447 cw_alarm_visible_column_ids: [
448 AlarmColumn::Name,
449 AlarmColumn::State,
450 AlarmColumn::LastStateUpdate,
451 AlarmColumn::Conditions,
452 AlarmColumn::Actions,
453 ]
454 .iter()
455 .map(|c| c.id())
456 .collect(),
457 cw_alarm_column_ids: AlarmColumn::ids(),
458 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
459 s3_bucket_column_ids: S3BucketColumn::ids(),
460 sqs_visible_column_ids: [
461 SqsColumn::Name,
462 SqsColumn::Type,
463 SqsColumn::Created,
464 SqsColumn::MessagesAvailable,
465 SqsColumn::MessagesInFlight,
466 SqsColumn::Encryption,
467 SqsColumn::ContentBasedDeduplication,
468 ]
469 .iter()
470 .map(|c| c.id())
471 .collect(),
472 sqs_column_ids: SqsColumn::ids(),
473 ec2_visible_column_ids: [
474 Ec2Column::Name,
475 Ec2Column::InstanceId,
476 Ec2Column::InstanceState,
477 Ec2Column::InstanceType,
478 Ec2Column::StatusCheck,
479 Ec2Column::AlarmStatus,
480 Ec2Column::AvailabilityZone,
481 Ec2Column::PublicIpv4Dns,
482 Ec2Column::PublicIpv4Address,
483 Ec2Column::ElasticIp,
484 Ec2Column::Ipv6Ips,
485 Ec2Column::Monitoring,
486 Ec2Column::SecurityGroupName,
487 Ec2Column::KeyName,
488 Ec2Column::LaunchTime,
489 Ec2Column::PlatformDetails,
490 ]
491 .iter()
492 .map(|c| c.id())
493 .collect(),
494 ec2_column_ids: Ec2Column::ids(),
495 ecr_repo_visible_column_ids: EcrColumn::ids(),
496 ecr_repo_column_ids: EcrColumn::ids(),
497 ecr_image_visible_column_ids: EcrImageColumn::ids(),
498 ecr_image_column_ids: EcrImageColumn::ids(),
499 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
500 lambda_application_column_ids: LambdaApplicationColumn::ids(),
501 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
502 lambda_deployment_column_ids: DeploymentColumn::ids(),
503 lambda_resource_visible_column_ids: ResourceColumn::ids(),
504 lambda_resource_column_ids: ResourceColumn::ids(),
505 cfn_visible_column_ids: [
506 CfnColumn::Name,
507 CfnColumn::Status,
508 CfnColumn::CreatedTime,
509 CfnColumn::Description,
510 ]
511 .iter()
512 .map(|c| c.id())
513 .collect(),
514 cfn_column_ids: CfnColumn::ids(),
515 cfn_parameter_visible_column_ids: crate::ui::cfn::parameter_column_ids(),
516 cfn_parameter_column_ids: crate::ui::cfn::parameter_column_ids(),
517 cfn_output_visible_column_ids: crate::ui::cfn::output_column_ids(),
518 cfn_output_column_ids: crate::ui::cfn::output_column_ids(),
519 cfn_resource_visible_column_ids: crate::ui::cfn::resource_column_ids(),
520 cfn_resource_column_ids: crate::ui::cfn::resource_column_ids(),
521 iam_user_visible_column_ids: UserColumn::visible(),
522 iam_user_column_ids: UserColumn::ids(),
523 iam_role_visible_column_ids: vec![
524 "Role name".to_string(),
525 "Trusted entities".to_string(),
526 "Creation time".to_string(),
527 ],
528 iam_role_column_ids: vec![
529 "Role name".to_string(),
530 "Path".to_string(),
531 "Trusted entities".to_string(),
532 "ARN".to_string(),
533 "Creation time".to_string(),
534 "Description".to_string(),
535 "Max session duration".to_string(),
536 ],
537 iam_group_visible_column_ids: vec![
538 "Group name".to_string(),
539 "Users".to_string(),
540 "Permissions".to_string(),
541 "Creation time".to_string(),
542 ],
543 iam_group_column_ids: vec![
544 "Group name".to_string(),
545 "Path".to_string(),
546 "Users".to_string(),
547 "Permissions".to_string(),
548 "Creation time".to_string(),
549 ],
550 iam_policy_visible_column_ids: vec![
551 "Policy name".to_string(),
552 "Type".to_string(),
553 "Attached via".to_string(),
554 ],
555 iam_policy_column_ids: vec![
556 "Policy name".to_string(),
557 "Type".to_string(),
558 "Attached via".to_string(),
559 "Attached entities".to_string(),
560 "Description".to_string(),
561 "Creation time".to_string(),
562 "Edited time".to_string(),
563 ],
564 preference_section: Preferences::Columns,
565 view_mode: ViewMode::List,
566 error_message: None,
567 error_scroll: 0,
568 page_input: String::new(),
569 calendar_date: None,
570 calendar_selecting: CalendarField::StartDate,
571 cursor_pos: 0,
572 current_session: None,
573 sessions: Vec::new(),
574 session_picker_selected: 0,
575 session_filter: String::new(),
576 region_filter: String::new(),
577 region_picker_selected: 0,
578 region_latencies: std::collections::HashMap::new(),
579 profile_filter: String::new(),
580 profile_picker_selected: 0,
581 available_profiles: Vec::new(),
582 snapshot_requested: false,
583 })
584 }
585
586 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
587 let config = AwsConfig::dummy(region.clone());
588 Self {
589 running: true,
590 mode: Mode::ServicePicker,
591 config: config.clone(),
592 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
593 s3_client: S3Client::new(config.clone()),
594 sqs_client: SqsClient::new(config.clone()),
595 alarms_client: AlarmsClient::new(config.clone()),
596 ec2_client: Ec2Client::new(config.clone()),
597 ecr_client: EcrClient::new(config.clone()),
598 iam_client: IamClient::new(config.clone()),
599 lambda_client: LambdaClient::new(config.clone()),
600 cloudformation_client: CloudFormationClient::new(config.clone()),
601 current_service: Service::CloudWatchLogGroups,
602 tabs: Vec::new(),
603 current_tab: 0,
604 tab_picker_selected: 0,
605 tab_filter: String::new(),
606 pending_key: None,
607 log_groups_state: CloudWatchLogGroupsState::new(),
608 insights_state: CloudWatchInsightsState::new(),
609 alarms_state: CloudWatchAlarmsState::new(),
610 s3_state: S3State::new(),
611 sqs_state: SqsState::new(),
612 ec2_state: Ec2State::default(),
613 ecr_state: EcrState::new(),
614 lambda_state: LambdaState::new(),
615 lambda_application_state: LambdaApplicationState::new(),
616 cfn_state: CfnState::new(),
617 iam_state: IamState::new(),
618 service_picker: ServicePickerState::new(),
619 service_selected: false,
620 profile,
621 region: region.unwrap_or_default(),
622 region_selector_index: 0,
623 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
624 cw_log_group_column_ids: LogGroupColumn::ids(),
625 column_selector_index: 0,
626 preference_section: Preferences::Columns,
627 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
628 cw_log_stream_column_ids: StreamColumn::ids(),
629 cw_log_event_visible_column_ids: EventColumn::default_visible(),
630 cw_log_event_column_ids: EventColumn::ids(),
631 cw_alarm_visible_column_ids: [
632 AlarmColumn::Name,
633 AlarmColumn::State,
634 AlarmColumn::LastStateUpdate,
635 AlarmColumn::Conditions,
636 AlarmColumn::Actions,
637 ]
638 .iter()
639 .map(|c| c.id())
640 .collect(),
641 cw_alarm_column_ids: AlarmColumn::ids(),
642 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
643 s3_bucket_column_ids: S3BucketColumn::ids(),
644 sqs_visible_column_ids: [
645 SqsColumn::Name,
646 SqsColumn::Type,
647 SqsColumn::Created,
648 SqsColumn::MessagesAvailable,
649 SqsColumn::MessagesInFlight,
650 SqsColumn::Encryption,
651 SqsColumn::ContentBasedDeduplication,
652 ]
653 .iter()
654 .map(|c| c.id())
655 .collect(),
656 sqs_column_ids: SqsColumn::ids(),
657 ec2_visible_column_ids: [
658 Ec2Column::Name,
659 Ec2Column::InstanceId,
660 Ec2Column::InstanceState,
661 Ec2Column::InstanceType,
662 Ec2Column::StatusCheck,
663 Ec2Column::AlarmStatus,
664 Ec2Column::AvailabilityZone,
665 Ec2Column::PublicIpv4Dns,
666 Ec2Column::PublicIpv4Address,
667 Ec2Column::ElasticIp,
668 Ec2Column::Ipv6Ips,
669 Ec2Column::Monitoring,
670 Ec2Column::SecurityGroupName,
671 Ec2Column::KeyName,
672 Ec2Column::LaunchTime,
673 Ec2Column::PlatformDetails,
674 ]
675 .iter()
676 .map(|c| c.id())
677 .collect(),
678 ec2_column_ids: Ec2Column::ids(),
679 ecr_repo_visible_column_ids: EcrColumn::ids(),
680 ecr_repo_column_ids: EcrColumn::ids(),
681 ecr_image_visible_column_ids: EcrImageColumn::ids(),
682 ecr_image_column_ids: EcrImageColumn::ids(),
683 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
684 lambda_application_column_ids: LambdaApplicationColumn::ids(),
685 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
686 lambda_deployment_column_ids: DeploymentColumn::ids(),
687 lambda_resource_visible_column_ids: ResourceColumn::ids(),
688 lambda_resource_column_ids: ResourceColumn::ids(),
689 cfn_visible_column_ids: [
690 CfnColumn::Name,
691 CfnColumn::Status,
692 CfnColumn::CreatedTime,
693 CfnColumn::Description,
694 ]
695 .iter()
696 .map(|c| c.id())
697 .collect(),
698 cfn_column_ids: CfnColumn::ids(),
699 iam_user_visible_column_ids: UserColumn::visible(),
700 cfn_parameter_visible_column_ids: crate::ui::cfn::parameter_column_ids(),
701 cfn_parameter_column_ids: crate::ui::cfn::parameter_column_ids(),
702 cfn_output_visible_column_ids: crate::ui::cfn::output_column_ids(),
703 cfn_output_column_ids: crate::ui::cfn::output_column_ids(),
704 cfn_resource_visible_column_ids: crate::ui::cfn::resource_column_ids(),
705 cfn_resource_column_ids: crate::ui::cfn::resource_column_ids(),
706 iam_user_column_ids: UserColumn::ids(),
707 iam_role_visible_column_ids: vec![
708 "Role name".to_string(),
709 "Trusted entities".to_string(),
710 "Creation time".to_string(),
711 ],
712 iam_role_column_ids: vec![
713 "Role name".to_string(),
714 "Path".to_string(),
715 "Trusted entities".to_string(),
716 "ARN".to_string(),
717 "Creation time".to_string(),
718 "Description".to_string(),
719 "Max session duration".to_string(),
720 ],
721 iam_group_visible_column_ids: vec![
722 "Group name".to_string(),
723 "Users".to_string(),
724 "Permissions".to_string(),
725 "Creation time".to_string(),
726 ],
727 iam_group_column_ids: vec![
728 "Group name".to_string(),
729 "Path".to_string(),
730 "Users".to_string(),
731 "Permissions".to_string(),
732 "Creation time".to_string(),
733 ],
734 iam_policy_visible_column_ids: vec![
735 "Policy name".to_string(),
736 "Type".to_string(),
737 "Attached via".to_string(),
738 ],
739 iam_policy_column_ids: vec![
740 "Policy name".to_string(),
741 "Type".to_string(),
742 "Attached via".to_string(),
743 "Attached entities".to_string(),
744 "Description".to_string(),
745 "Creation time".to_string(),
746 "Edited time".to_string(),
747 ],
748 view_mode: ViewMode::List,
749 error_message: None,
750 error_scroll: 0,
751 page_input: String::new(),
752 calendar_date: None,
753 calendar_selecting: CalendarField::StartDate,
754 cursor_pos: 0,
755 current_session: None,
756 sessions: Vec::new(),
757 session_picker_selected: 0,
758 session_filter: String::new(),
759 region_filter: String::new(),
760 region_picker_selected: 0,
761 region_latencies: std::collections::HashMap::new(),
762 profile_filter: String::new(),
763 profile_picker_selected: 0,
764 available_profiles: Vec::new(),
765 snapshot_requested: false,
766 }
767 }
768
769 pub fn handle_action(&mut self, action: Action) {
770 match action {
771 Action::Quit => {
772 self.save_current_session();
773 self.running = false;
774 }
775 Action::CloseService => {
776 if !self.tabs.is_empty() {
777 self.tabs.remove(self.current_tab);
779
780 if self.tabs.is_empty() {
781 self.service_selected = false;
783 self.current_tab = 0;
784 self.mode = Mode::ServicePicker;
785 } else {
786 if self.current_tab >= self.tabs.len() {
788 self.current_tab = self.tabs.len() - 1;
789 }
790 self.current_service = self.tabs[self.current_tab].service;
791 self.service_selected = true;
792 self.mode = Mode::Normal;
793 }
794 } else {
795 self.service_selected = false;
797 self.mode = Mode::Normal;
798 }
799 self.service_picker.filter.clear();
800 self.service_picker.selected = 0;
801 }
802 Action::NextItem => self.next_item(),
803 Action::PrevItem => self.prev_item(),
804 Action::PageUp => self.page_up(),
805 Action::PageDown => self.page_down(),
806 Action::NextPane => self.next_pane(),
807 Action::PrevPane => self.prev_pane(),
808 Action::Select => self.select_item(),
809 Action::OpenSpaceMenu => {
810 self.mode = Mode::SpaceMenu;
811 self.service_picker.filter.clear();
812 self.service_picker.selected = 0;
813 }
814 Action::CloseMenu => {
815 self.mode = Mode::Normal;
816 self.service_picker.filter.clear();
817 match self.current_service {
819 Service::S3Buckets => {
820 self.s3_state.selected_row = 0;
821 self.s3_state.selected_object = 0;
822 }
823 Service::CloudFormationStacks => {
824 if self.cfn_state.current_stack.is_some()
825 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
826 {
827 self.cfn_state.parameters.reset();
828 } else if self.cfn_state.current_stack.is_some()
829 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
830 {
831 self.cfn_state.outputs.reset();
832 } else {
833 self.cfn_state.table.reset();
834 }
835 }
836 Service::LambdaFunctions => {
837 self.lambda_state.table.reset();
838 }
839 Service::SqsQueues => {
840 self.sqs_state.queues.reset();
841 }
842 Service::IamRoles => {
843 self.iam_state.roles.reset();
844 }
845 Service::IamUsers => {
846 self.iam_state.users.reset();
847 }
848 Service::IamUserGroups => {
849 self.iam_state.groups.reset();
850 }
851 Service::CloudWatchAlarms => {
852 self.alarms_state.table.reset();
853 }
854 Service::Ec2Instances => {
855 self.ec2_state.table.reset();
856 }
857 Service::EcrRepositories => {
858 self.ecr_state.repositories.reset();
859 }
860 Service::LambdaApplications => {
861 self.lambda_application_state.table.reset();
862 }
863 _ => {}
864 }
865 }
866 Action::NextTab => {
867 if !self.tabs.is_empty() {
868 self.current_tab = (self.current_tab + 1) % self.tabs.len();
869 self.current_service = self.tabs[self.current_tab].service;
870 }
871 }
872 Action::PrevTab => {
873 if !self.tabs.is_empty() {
874 self.current_tab = if self.current_tab == 0 {
875 self.tabs.len() - 1
876 } else {
877 self.current_tab - 1
878 };
879 self.current_service = self.tabs[self.current_tab].service;
880 }
881 }
882 Action::CloseTab => {
883 if !self.tabs.is_empty() {
884 self.tabs.remove(self.current_tab);
885 if self.tabs.is_empty() {
886 self.service_selected = false;
888 self.current_tab = 0;
889 self.service_picker.filter.clear();
890 self.service_picker.selected = 0;
891 self.mode = Mode::ServicePicker;
892 } else {
893 if self.current_tab >= self.tabs.len() {
896 self.current_tab = self.tabs.len() - 1;
897 }
898 self.current_service = self.tabs[self.current_tab].service;
899 self.service_selected = true;
900 self.mode = Mode::Normal;
901 }
902 }
903 }
904 Action::OpenTabPicker => {
905 if !self.tabs.is_empty() {
906 self.tab_picker_selected = self.current_tab;
907 self.mode = Mode::TabPicker;
908 } else {
909 self.mode = Mode::Normal;
910 }
911 }
912 Action::OpenSessionPicker => {
913 self.save_current_session();
914 self.sessions = Session::list_all().unwrap_or_default();
915 self.session_picker_selected = 0;
916 self.mode = Mode::SessionPicker;
917 }
918 Action::LoadSession => {
919 let filtered_sessions = self.get_filtered_sessions();
920 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
921 let session = session.clone();
922 self.profile = session.profile.clone();
924 self.region = session.region.clone();
925 self.config.account_id = session.account_id.clone();
926 self.config.role_arn = session.role_arn.clone();
927
928 self.tabs.clear();
930 for session_tab in &session.tabs {
931 let service = match session_tab.service.as_str() {
933 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
934 "CloudWatchInsights" => Service::CloudWatchInsights,
935 "CloudWatchAlarms" => Service::CloudWatchAlarms,
936 "S3Buckets" => Service::S3Buckets,
937 "SqsQueues" => Service::SqsQueues,
938 "Ec2Instances" => Service::Ec2Instances,
939 "EcrRepositories" => Service::EcrRepositories,
940 "LambdaFunctions" => Service::LambdaFunctions,
941 "LambdaApplications" => Service::LambdaApplications,
942 "CloudFormationStacks" => Service::CloudFormationStacks,
943 "IamUsers" => Service::IamUsers,
944 "IamRoles" => Service::IamRoles,
945 "IamUserGroups" => Service::IamUserGroups,
946 _ => continue,
947 };
948
949 self.tabs.push(Tab {
950 service,
951 title: session_tab.title.clone(),
952 breadcrumb: session_tab.breadcrumb.clone(),
953 });
954
955 if let Some(filter) = &session_tab.filter {
957 if service == Service::CloudWatchLogGroups {
958 self.log_groups_state.log_groups.filter = filter.clone();
959 }
960 }
961 }
962
963 if !self.tabs.is_empty() {
964 self.current_tab = 0;
965 self.current_service = self.tabs[0].service;
966 self.service_selected = true;
967 self.current_session = Some(session.clone());
968 }
969 }
970 self.mode = Mode::Normal;
971 }
972 Action::SaveSession => {
973 }
975 Action::OpenServicePicker => {
976 if self.mode == Mode::ServicePicker {
977 self.tabs.push(Tab {
978 service: Service::S3Buckets,
979 title: "S3 > Buckets".to_string(),
980 breadcrumb: "S3 > Buckets".to_string(),
981 });
982 self.current_tab = self.tabs.len() - 1;
983 self.current_service = Service::S3Buckets;
984 self.view_mode = ViewMode::List;
985 self.service_selected = true;
986 self.mode = Mode::Normal;
987 } else {
988 self.mode = Mode::ServicePicker;
989 self.service_picker.filter.clear();
990 self.service_picker.selected = 0;
991 }
992 }
993 Action::OpenCloudWatch => {
994 self.current_service = Service::CloudWatchLogGroups;
995 self.view_mode = ViewMode::List;
996 self.service_selected = true;
997 self.mode = Mode::Normal;
998 }
999 Action::OpenCloudWatchSplit => {
1000 self.current_service = Service::CloudWatchInsights;
1001 self.view_mode = ViewMode::InsightsResults;
1002 self.service_selected = true;
1003 self.mode = Mode::Normal;
1004 }
1005 Action::OpenCloudWatchAlarms => {
1006 self.current_service = Service::CloudWatchAlarms;
1007 self.view_mode = ViewMode::List;
1008 self.service_selected = true;
1009 self.mode = Mode::Normal;
1010 }
1011 Action::FilterInput(c) => {
1012 if self.mode == Mode::TabPicker {
1013 self.tab_filter.push(c);
1014 self.tab_picker_selected = 0;
1015 } else if self.mode == Mode::RegionPicker {
1016 self.region_filter.push(c);
1017 self.region_picker_selected = 0;
1018 } else if self.mode == Mode::ProfilePicker {
1019 self.profile_filter.push(c);
1020 self.profile_picker_selected = 0;
1021 } else if self.mode == Mode::SessionPicker {
1022 self.session_filter.push(c);
1023 self.session_picker_selected = 0;
1024 } else if self.mode == Mode::ServicePicker {
1025 self.service_picker.filter.push(c);
1026 self.service_picker.selected = 0;
1027 } else if self.mode == Mode::InsightsInput {
1028 use crate::app::InsightsFocus;
1029 match self.insights_state.insights.insights_focus {
1030 InsightsFocus::Query => {
1031 self.insights_state.insights.query_text.push(c);
1032 }
1033 InsightsFocus::LogGroupSearch => {
1034 self.insights_state.insights.log_group_search.push(c);
1035 if !self.insights_state.insights.log_group_search.is_empty() {
1037 self.insights_state.insights.log_group_matches = self
1038 .log_groups_state
1039 .log_groups
1040 .items
1041 .iter()
1042 .filter(|g| {
1043 g.name.to_lowercase().contains(
1044 &self
1045 .insights_state
1046 .insights
1047 .log_group_search
1048 .to_lowercase(),
1049 )
1050 })
1051 .take(50)
1052 .map(|g| g.name.clone())
1053 .collect();
1054 self.insights_state.insights.show_dropdown = true;
1055 } else {
1056 self.insights_state.insights.log_group_matches.clear();
1057 self.insights_state.insights.show_dropdown = false;
1058 }
1059 }
1060 _ => {}
1061 }
1062 } else if self.mode == Mode::FilterInput {
1063 let is_pagination_focused = if self.current_service
1065 == Service::LambdaApplications
1066 {
1067 if self.lambda_application_state.current_application.is_some() {
1068 if self.lambda_application_state.detail_tab
1069 == LambdaApplicationDetailTab::Deployments
1070 {
1071 self.lambda_application_state.deployment_input_focus
1072 == InputFocus::Pagination
1073 } else {
1074 self.lambda_application_state.resource_input_focus
1075 == InputFocus::Pagination
1076 }
1077 } else {
1078 self.lambda_application_state.input_focus == InputFocus::Pagination
1079 }
1080 } else if self.current_service == Service::CloudFormationStacks {
1081 self.cfn_state.input_focus == InputFocus::Pagination
1082 } else if self.current_service == Service::IamRoles
1083 && self.iam_state.current_role.is_none()
1084 {
1085 self.iam_state.role_input_focus == InputFocus::Pagination
1086 } else if self.view_mode == ViewMode::PolicyView {
1087 self.iam_state.policy_input_focus == InputFocus::Pagination
1088 } else if self.current_service == Service::CloudWatchAlarms {
1089 self.alarms_state.input_focus == InputFocus::Pagination
1090 } else if self.current_service == Service::Ec2Instances {
1091 self.ec2_state.input_focus == InputFocus::Pagination
1092 } else if self.current_service == Service::CloudWatchLogGroups {
1093 self.log_groups_state.input_focus == InputFocus::Pagination
1094 } else if self.current_service == Service::EcrRepositories
1095 && self.ecr_state.current_repository.is_none()
1096 {
1097 self.ecr_state.input_focus == InputFocus::Pagination
1098 } else if self.current_service == Service::LambdaFunctions {
1099 if self.lambda_state.current_function.is_some()
1100 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1101 {
1102 self.lambda_state.version_input_focus == InputFocus::Pagination
1103 } else if self.lambda_state.current_function.is_none() {
1104 self.lambda_state.input_focus == InputFocus::Pagination
1105 } else {
1106 false
1107 }
1108 } else if self.current_service == Service::SqsQueues {
1109 if self.sqs_state.current_queue.is_some()
1110 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1111 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1112 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1113 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1114 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1115 {
1116 self.sqs_state.input_focus == InputFocus::Pagination
1117 } else {
1118 false
1119 }
1120 } else {
1121 false
1122 };
1123
1124 if is_pagination_focused && c.is_ascii_digit() {
1125 self.page_input.push(c);
1126 } else if self.current_service == Service::LambdaApplications {
1127 let is_input_focused =
1128 if self.lambda_application_state.current_application.is_some() {
1129 if self.lambda_application_state.detail_tab
1130 == LambdaApplicationDetailTab::Deployments
1131 {
1132 self.lambda_application_state.deployment_input_focus
1133 == InputFocus::Filter
1134 } else {
1135 self.lambda_application_state.resource_input_focus
1136 == InputFocus::Filter
1137 }
1138 } else {
1139 self.lambda_application_state.input_focus == InputFocus::Filter
1140 };
1141 if is_input_focused {
1142 if let Some(filter) = self.get_active_filter_mut() {
1143 filter.push(c);
1144 }
1145 }
1146 } else if self.current_service == Service::CloudFormationStacks {
1147 if self.cfn_state.current_stack.is_some()
1148 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1149 {
1150 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1151 self.cfn_state.parameters.filter.push(c);
1152 self.cfn_state.parameters.selected = 0;
1153 }
1154 } else if self.cfn_state.current_stack.is_some()
1155 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1156 {
1157 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1158 self.cfn_state.outputs.filter.push(c);
1159 self.cfn_state.outputs.selected = 0;
1160 }
1161 } else if self.cfn_state.current_stack.is_some()
1162 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1163 {
1164 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1165 self.cfn_state.resources.filter.push(c);
1166 self.cfn_state.resources.selected = 0;
1167 }
1168 } else if self.cfn_state.input_focus == InputFocus::Filter {
1169 if let Some(filter) = self.get_active_filter_mut() {
1170 filter.push(c);
1171 }
1172 }
1173 } else if self.current_service == Service::EcrRepositories
1174 && self.ecr_state.current_repository.is_none()
1175 {
1176 if self.ecr_state.input_focus == InputFocus::Filter {
1177 if let Some(filter) = self.get_active_filter_mut() {
1178 filter.push(c);
1179 }
1180 }
1181 } else if self.current_service == Service::IamRoles
1182 && self.iam_state.current_role.is_none()
1183 {
1184 if self.iam_state.role_input_focus == InputFocus::Filter {
1185 if let Some(filter) = self.get_active_filter_mut() {
1186 filter.push(c);
1187 }
1188 }
1189 } else if self.view_mode == ViewMode::PolicyView {
1190 if self.iam_state.policy_input_focus == InputFocus::Filter {
1191 if let Some(filter) = self.get_active_filter_mut() {
1192 filter.push(c);
1193 }
1194 }
1195 } else if self.current_service == Service::LambdaFunctions
1196 && self.lambda_state.current_version.is_some()
1197 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1198 {
1199 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1200 if let Some(filter) = self.get_active_filter_mut() {
1201 filter.push(c);
1202 }
1203 }
1204 } else if self.current_service == Service::LambdaFunctions
1205 && self.lambda_state.current_function.is_some()
1206 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1207 {
1208 if self.lambda_state.version_input_focus == InputFocus::Filter {
1209 if let Some(filter) = self.get_active_filter_mut() {
1210 filter.push(c);
1211 }
1212 }
1213 } else if self.current_service == Service::LambdaFunctions
1214 && self.lambda_state.current_function.is_some()
1215 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1216 {
1217 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1218 if let Some(filter) = self.get_active_filter_mut() {
1219 filter.push(c);
1220 }
1221 }
1222 } else if self.current_service == Service::SqsQueues
1223 && self.sqs_state.current_queue.is_some()
1224 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1225 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1226 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1227 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1228 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1229 {
1230 if self.sqs_state.input_focus == InputFocus::Filter {
1231 if let Some(filter) = self.get_active_filter_mut() {
1232 filter.push(c);
1233 }
1234 }
1235 } else if let Some(filter) = self.get_active_filter_mut() {
1236 filter.push(c);
1237 }
1238 } else if self.mode == Mode::EventFilterInput {
1239 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1240 self.log_groups_state.event_filter.push(c);
1241 } else if c.is_ascii_digit() {
1242 self.log_groups_state.relative_amount.push(c);
1243 }
1244 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1245 self.page_input.push(c);
1246 }
1247 }
1248 Action::FilterBackspace => {
1249 if self.mode == Mode::ServicePicker {
1250 self.service_picker.filter.pop();
1251 self.service_picker.selected = 0;
1252 } else if self.mode == Mode::TabPicker {
1253 self.tab_filter.pop();
1254 self.tab_picker_selected = 0;
1255 } else if self.mode == Mode::RegionPicker {
1256 self.region_filter.pop();
1257 self.region_picker_selected = 0;
1258 } else if self.mode == Mode::ProfilePicker {
1259 self.profile_filter.pop();
1260 self.profile_picker_selected = 0;
1261 } else if self.mode == Mode::SessionPicker {
1262 self.session_filter.pop();
1263 self.session_picker_selected = 0;
1264 } else if self.mode == Mode::InsightsInput {
1265 use crate::app::InsightsFocus;
1266 match self.insights_state.insights.insights_focus {
1267 InsightsFocus::Query => {
1268 self.insights_state.insights.query_text.pop();
1269 }
1270 InsightsFocus::LogGroupSearch => {
1271 self.insights_state.insights.log_group_search.pop();
1272 if !self.insights_state.insights.log_group_search.is_empty() {
1274 self.insights_state.insights.log_group_matches = self
1275 .log_groups_state
1276 .log_groups
1277 .items
1278 .iter()
1279 .filter(|g| {
1280 g.name.to_lowercase().contains(
1281 &self
1282 .insights_state
1283 .insights
1284 .log_group_search
1285 .to_lowercase(),
1286 )
1287 })
1288 .take(50)
1289 .map(|g| g.name.clone())
1290 .collect();
1291 self.insights_state.insights.show_dropdown = true;
1292 } else {
1293 self.insights_state.insights.log_group_matches.clear();
1294 self.insights_state.insights.show_dropdown = false;
1295 }
1296 }
1297 _ => {}
1298 }
1299 } else if self.mode == Mode::FilterInput {
1300 if self.current_service == Service::CloudFormationStacks {
1302 if self.cfn_state.current_stack.is_some()
1303 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1304 {
1305 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1306 self.cfn_state.parameters.filter.pop();
1307 self.cfn_state.parameters.selected = 0;
1308 }
1309 } else if self.cfn_state.current_stack.is_some()
1310 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1311 {
1312 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1313 self.cfn_state.outputs.filter.pop();
1314 self.cfn_state.outputs.selected = 0;
1315 }
1316 } else if self.cfn_state.current_stack.is_some()
1317 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1318 {
1319 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1320 self.cfn_state.resources.filter.pop();
1321 self.cfn_state.resources.selected = 0;
1322 }
1323 } else if self.cfn_state.input_focus == InputFocus::Filter {
1324 if let Some(filter) = self.get_active_filter_mut() {
1325 filter.pop();
1326 }
1327 }
1328 } else if let Some(filter) = self.get_active_filter_mut() {
1329 filter.pop();
1330 }
1331 } else if self.mode == Mode::EventFilterInput {
1332 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1333 self.log_groups_state.event_filter.pop();
1334 } else {
1335 self.log_groups_state.relative_amount.pop();
1336 }
1337 }
1338 }
1339 Action::DeleteWord => {
1340 let text = if self.mode == Mode::ServicePicker {
1341 &mut self.service_picker.filter
1342 } else if self.mode == Mode::InsightsInput {
1343 use crate::app::InsightsFocus;
1344 match self.insights_state.insights.insights_focus {
1345 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1346 InsightsFocus::LogGroupSearch => {
1347 &mut self.insights_state.insights.log_group_search
1348 }
1349 _ => return,
1350 }
1351 } else if self.mode == Mode::FilterInput {
1352 if let Some(filter) = self.get_active_filter_mut() {
1353 filter
1354 } else {
1355 return;
1356 }
1357 } else if self.mode == Mode::EventFilterInput {
1358 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1359 &mut self.log_groups_state.event_filter
1360 } else {
1361 &mut self.log_groups_state.relative_amount
1362 }
1363 } else {
1364 return;
1365 };
1366
1367 if text.is_empty() {
1368 return;
1369 }
1370
1371 let mut chars: Vec<char> = text.chars().collect();
1372 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1373 chars.pop();
1374 }
1375 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1376 chars.pop();
1377 }
1378 *text = chars.into_iter().collect();
1379 }
1380 Action::WordLeft => {
1381 }
1383 Action::WordRight => {
1384 }
1386 Action::OpenColumnSelector => {
1387 if self.current_service == Service::CloudFormationStacks
1389 && self.cfn_state.current_stack.is_some()
1390 && (self.cfn_state.detail_tab == CfnDetailTab::Template
1391 || self.cfn_state.detail_tab == CfnDetailTab::GitSync)
1392 {
1393 return;
1394 }
1395
1396 if !self.page_input.is_empty() {
1398 if let Ok(page) = self.page_input.parse::<usize>() {
1399 self.go_to_page(page);
1400 }
1401 self.page_input.clear();
1402 } else {
1403 self.mode = Mode::ColumnSelector;
1404 self.column_selector_index = 0;
1405 }
1406 }
1407 Action::ToggleColumn => {
1408 if self.current_service == Service::S3Buckets
1409 && self.s3_state.current_bucket.is_none()
1410 {
1411 if let Some(col) = self.s3_bucket_column_ids.get(self.column_selector_index) {
1412 if let Some(pos) = self
1413 .s3_bucket_visible_column_ids
1414 .iter()
1415 .position(|c| c == col)
1416 {
1417 self.s3_bucket_visible_column_ids.remove(pos);
1418 } else {
1419 self.s3_bucket_visible_column_ids.push(*col);
1420 }
1421 }
1422 } else if self.current_service == Service::CloudWatchAlarms {
1423 let idx = self.column_selector_index;
1427 if (1..=16).contains(&idx) {
1428 if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1430 if let Some(pos) = self
1431 .cw_alarm_visible_column_ids
1432 .iter()
1433 .position(|c| c == col)
1434 {
1435 self.cw_alarm_visible_column_ids.remove(pos);
1436 } else {
1437 self.cw_alarm_visible_column_ids.push(*col);
1438 }
1439 }
1440 } else if idx == 19 {
1441 self.alarms_state.view_as = AlarmViewMode::Table;
1442 } else if idx == 20 {
1443 self.alarms_state.view_as = AlarmViewMode::Cards;
1444 } else if idx == 23 {
1445 self.alarms_state.table.page_size = PageSize::Ten;
1446 } else if idx == 24 {
1447 self.alarms_state.table.page_size = PageSize::TwentyFive;
1448 } else if idx == 25 {
1449 self.alarms_state.table.page_size = PageSize::Fifty;
1450 } else if idx == 26 {
1451 self.alarms_state.table.page_size = PageSize::OneHundred;
1452 } else if idx == 29 {
1453 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1454 }
1455 } else if self.current_service == Service::EcrRepositories {
1456 if self.ecr_state.current_repository.is_some() {
1457 let idx = self.column_selector_index;
1459 if let Some(col) = self.ecr_image_column_ids.get(idx) {
1460 if let Some(pos) = self
1461 .ecr_image_visible_column_ids
1462 .iter()
1463 .position(|c| c == col)
1464 {
1465 self.ecr_image_visible_column_ids.remove(pos);
1466 } else {
1467 self.ecr_image_visible_column_ids.push(*col);
1468 }
1469 }
1470 } else {
1471 if let Some(col) = self.ecr_repo_column_ids.get(self.column_selector_index)
1473 {
1474 if let Some(pos) = self
1475 .ecr_repo_visible_column_ids
1476 .iter()
1477 .position(|c| c == col)
1478 {
1479 self.ecr_repo_visible_column_ids.remove(pos);
1480 } else {
1481 self.ecr_repo_visible_column_ids.push(*col);
1482 }
1483 }
1484 }
1485 } else if self.current_service == Service::Ec2Instances {
1486 let idx = self.column_selector_index;
1487 if idx > 0 && idx <= self.ec2_column_ids.len() {
1488 if let Some(col) = self.ec2_column_ids.get(idx - 1) {
1489 if let Some(pos) =
1490 self.ec2_visible_column_ids.iter().position(|c| c == col)
1491 {
1492 self.ec2_visible_column_ids.remove(pos);
1493 } else {
1494 self.ec2_visible_column_ids.push(*col);
1495 }
1496 }
1497 } else if idx == self.ec2_column_ids.len() + 3 {
1498 self.ec2_state.table.page_size = PageSize::Ten;
1499 } else if idx == self.ec2_column_ids.len() + 4 {
1500 self.ec2_state.table.page_size = PageSize::TwentyFive;
1501 } else if idx == self.ec2_column_ids.len() + 5 {
1502 self.ec2_state.table.page_size = PageSize::Fifty;
1503 } else if idx == self.ec2_column_ids.len() + 6 {
1504 self.ec2_state.table.page_size = PageSize::OneHundred;
1505 }
1506 } else if self.current_service == Service::SqsQueues {
1507 if self.sqs_state.current_queue.is_some()
1508 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1509 {
1510 let idx = self.column_selector_index;
1512 if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
1513 if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
1514 if let Some(pos) = self
1515 .sqs_state
1516 .trigger_visible_column_ids
1517 .iter()
1518 .position(|c| c == col)
1519 {
1520 self.sqs_state.trigger_visible_column_ids.remove(pos);
1521 } else {
1522 self.sqs_state.trigger_visible_column_ids.push(col.clone());
1523 }
1524 }
1525 } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
1526 self.sqs_state.triggers.page_size = PageSize::Ten;
1527 } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
1528 self.sqs_state.triggers.page_size = PageSize::TwentyFive;
1529 } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
1530 self.sqs_state.triggers.page_size = PageSize::Fifty;
1531 } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
1532 self.sqs_state.triggers.page_size = PageSize::OneHundred;
1533 }
1534 } else if self.sqs_state.current_queue.is_some()
1535 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1536 {
1537 let idx = self.column_selector_index;
1539 if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
1540 if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
1541 if let Some(pos) = self
1542 .sqs_state
1543 .pipe_visible_column_ids
1544 .iter()
1545 .position(|c| c == col)
1546 {
1547 self.sqs_state.pipe_visible_column_ids.remove(pos);
1548 } else {
1549 self.sqs_state.pipe_visible_column_ids.push(col.clone());
1550 }
1551 }
1552 } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
1553 self.sqs_state.pipes.page_size = PageSize::Ten;
1554 } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
1555 self.sqs_state.pipes.page_size = PageSize::TwentyFive;
1556 } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
1557 self.sqs_state.pipes.page_size = PageSize::Fifty;
1558 } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
1559 self.sqs_state.pipes.page_size = PageSize::OneHundred;
1560 }
1561 } else if self.sqs_state.current_queue.is_some()
1562 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1563 {
1564 let idx = self.column_selector_index;
1566 if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
1567 if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
1568 if let Some(pos) = self
1569 .sqs_state
1570 .tag_visible_column_ids
1571 .iter()
1572 .position(|c| c == col)
1573 {
1574 self.sqs_state.tag_visible_column_ids.remove(pos);
1575 } else {
1576 self.sqs_state.tag_visible_column_ids.push(col.clone());
1577 }
1578 }
1579 } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
1580 self.sqs_state.tags.page_size = PageSize::Ten;
1581 } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
1582 self.sqs_state.tags.page_size = PageSize::TwentyFive;
1583 } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
1584 self.sqs_state.tags.page_size = PageSize::Fifty;
1585 } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
1586 self.sqs_state.tags.page_size = PageSize::OneHundred;
1587 }
1588 } else if self.sqs_state.current_queue.is_some()
1589 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
1590 {
1591 let idx = self.column_selector_index;
1593 if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
1594 if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
1595 if let Some(pos) = self
1596 .sqs_state
1597 .subscription_visible_column_ids
1598 .iter()
1599 .position(|c| c == col)
1600 {
1601 self.sqs_state.subscription_visible_column_ids.remove(pos);
1602 } else {
1603 self.sqs_state
1604 .subscription_visible_column_ids
1605 .push(col.clone());
1606 }
1607 }
1608 } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
1609 self.sqs_state.subscriptions.page_size = PageSize::Ten;
1610 } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
1611 self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
1612 } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
1613 self.sqs_state.subscriptions.page_size = PageSize::Fifty;
1614 } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
1615 self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
1616 }
1617 } else if let Some(col) = self.sqs_column_ids.get(self.column_selector_index) {
1618 if let Some(pos) = self.sqs_visible_column_ids.iter().position(|c| c == col)
1619 {
1620 self.sqs_visible_column_ids.remove(pos);
1621 } else {
1622 self.sqs_visible_column_ids.push(*col);
1623 }
1624 }
1625 } else if self.current_service == Service::LambdaFunctions {
1626 let idx = self.column_selector_index;
1627 if self.lambda_state.current_function.is_some()
1629 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1630 {
1631 if idx > 0 && idx <= self.lambda_state.version_column_ids.len() {
1633 if let Some(col) = self.lambda_state.version_column_ids.get(idx - 1) {
1634 if let Some(pos) = self
1635 .lambda_state
1636 .version_visible_column_ids
1637 .iter()
1638 .position(|c| *c == *col)
1639 {
1640 self.lambda_state.version_visible_column_ids.remove(pos);
1641 } else {
1642 self.lambda_state
1643 .version_visible_column_ids
1644 .push(col.clone());
1645 }
1646 }
1647 } else if idx == self.lambda_state.version_column_ids.len() + 3 {
1648 self.lambda_state.version_table.page_size = PageSize::Ten;
1649 } else if idx == self.lambda_state.version_column_ids.len() + 4 {
1650 self.lambda_state.version_table.page_size = PageSize::TwentyFive;
1651 } else if idx == self.lambda_state.version_column_ids.len() + 5 {
1652 self.lambda_state.version_table.page_size = PageSize::Fifty;
1653 } else if idx == self.lambda_state.version_column_ids.len() + 6 {
1654 self.lambda_state.version_table.page_size = PageSize::OneHundred;
1655 }
1656 } else if (self.lambda_state.current_function.is_some()
1657 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases)
1658 || (self.lambda_state.current_version.is_some()
1659 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration)
1660 {
1661 if idx > 0 && idx <= self.lambda_state.alias_column_ids.len() {
1663 if let Some(col) = self.lambda_state.alias_column_ids.get(idx - 1) {
1664 if let Some(pos) = self
1665 .lambda_state
1666 .alias_visible_column_ids
1667 .iter()
1668 .position(|c| *c == *col)
1669 {
1670 self.lambda_state.alias_visible_column_ids.remove(pos);
1671 } else {
1672 self.lambda_state.alias_visible_column_ids.push(col.clone());
1673 }
1674 }
1675 } else if idx == self.lambda_state.alias_column_ids.len() + 3 {
1676 self.lambda_state.alias_table.page_size = PageSize::Ten;
1677 } else if idx == self.lambda_state.alias_column_ids.len() + 4 {
1678 self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
1679 } else if idx == self.lambda_state.alias_column_ids.len() + 5 {
1680 self.lambda_state.alias_table.page_size = PageSize::Fifty;
1681 } else if idx == self.lambda_state.alias_column_ids.len() + 6 {
1682 self.lambda_state.alias_table.page_size = PageSize::OneHundred;
1683 }
1684 } else {
1685 if idx > 0 && idx <= self.lambda_state.function_column_ids.len() {
1687 if let Some(col) = self.lambda_state.function_column_ids.get(idx - 1) {
1688 if let Some(pos) = self
1689 .lambda_state
1690 .function_visible_column_ids
1691 .iter()
1692 .position(|c| *c == *col)
1693 {
1694 self.lambda_state.function_visible_column_ids.remove(pos);
1695 } else {
1696 self.lambda_state.function_visible_column_ids.push(*col);
1697 }
1698 }
1699 } else if idx == self.lambda_state.function_column_ids.len() + 3 {
1700 self.lambda_state.table.page_size = PageSize::Ten;
1701 } else if idx == self.lambda_state.function_column_ids.len() + 4 {
1702 self.lambda_state.table.page_size = PageSize::TwentyFive;
1703 } else if idx == self.lambda_state.function_column_ids.len() + 5 {
1704 self.lambda_state.table.page_size = PageSize::Fifty;
1705 } else if idx == self.lambda_state.function_column_ids.len() + 6 {
1706 self.lambda_state.table.page_size = PageSize::OneHundred;
1707 }
1708 }
1709 } else if self.current_service == Service::LambdaApplications {
1710 if self.lambda_application_state.current_application.is_some() {
1711 if self.lambda_application_state.detail_tab
1713 == LambdaApplicationDetailTab::Overview
1714 {
1715 let idx = self.column_selector_index;
1717 if idx > 0 && idx <= self.lambda_resource_column_ids.len() {
1718 if let Some(col) = self.lambda_resource_column_ids.get(idx - 1) {
1719 if let Some(pos) = self
1720 .lambda_resource_visible_column_ids
1721 .iter()
1722 .position(|c| c == col)
1723 {
1724 self.lambda_resource_visible_column_ids.remove(pos);
1725 } else {
1726 self.lambda_resource_visible_column_ids.push(*col);
1727 }
1728 }
1729 } else if idx == self.lambda_resource_column_ids.len() + 3 {
1730 self.lambda_application_state.resources.page_size = PageSize::Ten;
1731 } else if idx == self.lambda_resource_column_ids.len() + 4 {
1732 self.lambda_application_state.resources.page_size =
1733 PageSize::TwentyFive;
1734 } else if idx == self.lambda_resource_column_ids.len() + 5 {
1735 self.lambda_application_state.resources.page_size = PageSize::Fifty;
1736 }
1737 } else {
1738 let idx = self.column_selector_index;
1740 if idx > 0 && idx <= self.lambda_deployment_column_ids.len() {
1741 if let Some(col) = self.lambda_deployment_column_ids.get(idx - 1) {
1742 if let Some(pos) = self
1743 .lambda_deployment_visible_column_ids
1744 .iter()
1745 .position(|c| c == col)
1746 {
1747 self.lambda_deployment_visible_column_ids.remove(pos);
1748 } else {
1749 self.lambda_deployment_visible_column_ids.push(*col);
1750 }
1751 }
1752 } else if idx == self.lambda_deployment_column_ids.len() + 3 {
1753 self.lambda_application_state.deployments.page_size = PageSize::Ten;
1754 } else if idx == self.lambda_deployment_column_ids.len() + 4 {
1755 self.lambda_application_state.deployments.page_size =
1756 PageSize::TwentyFive;
1757 } else if idx == self.lambda_deployment_column_ids.len() + 5 {
1758 self.lambda_application_state.deployments.page_size =
1759 PageSize::Fifty;
1760 }
1761 }
1762 } else {
1763 let idx = self.column_selector_index;
1765 if idx > 0 && idx <= self.lambda_application_column_ids.len() {
1766 if let Some(col) = self.lambda_application_column_ids.get(idx - 1) {
1767 if let Some(pos) = self
1768 .lambda_application_visible_column_ids
1769 .iter()
1770 .position(|c| *c == *col)
1771 {
1772 self.lambda_application_visible_column_ids.remove(pos);
1773 } else {
1774 self.lambda_application_visible_column_ids.push(*col);
1775 }
1776 }
1777 } else if idx == self.lambda_application_column_ids.len() + 3 {
1778 self.lambda_application_state.table.page_size = PageSize::Ten;
1779 } else if idx == self.lambda_application_column_ids.len() + 4 {
1780 self.lambda_application_state.table.page_size = PageSize::TwentyFive;
1781 } else if idx == self.lambda_application_column_ids.len() + 5 {
1782 self.lambda_application_state.table.page_size = PageSize::Fifty;
1783 }
1784 }
1785 } else if self.view_mode == ViewMode::Events {
1786 if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
1787 {
1788 if let Some(pos) = self
1789 .cw_log_event_visible_column_ids
1790 .iter()
1791 .position(|c| c == col)
1792 {
1793 self.cw_log_event_visible_column_ids.remove(pos);
1794 } else {
1795 self.cw_log_event_visible_column_ids.push(*col);
1796 }
1797 }
1798 } else if self.view_mode == ViewMode::Detail {
1799 if let Some(col) = self
1800 .cw_log_stream_column_ids
1801 .get(self.column_selector_index)
1802 {
1803 if let Some(pos) = self
1804 .cw_log_stream_visible_column_ids
1805 .iter()
1806 .position(|c| c == col)
1807 {
1808 self.cw_log_stream_visible_column_ids.remove(pos);
1809 } else {
1810 self.cw_log_stream_visible_column_ids.push(*col);
1811 }
1812 }
1813 } else if self.current_service == Service::CloudFormationStacks {
1814 let idx = self.column_selector_index;
1815 if self.cfn_state.current_stack.is_some()
1817 && self.cfn_state.detail_tab == CfnDetailTab::StackInfo
1818 {
1819 if idx == 4 {
1821 self.cfn_state.tags.page_size = PageSize::Ten;
1822 } else if idx == 5 {
1823 self.cfn_state.tags.page_size = PageSize::TwentyFive;
1824 } else if idx == 6 {
1825 self.cfn_state.tags.page_size = PageSize::Fifty;
1826 } else if idx == 7 {
1827 self.cfn_state.tags.page_size = PageSize::OneHundred;
1828 }
1829 } else if self.cfn_state.current_stack.is_some()
1830 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1831 {
1832 if idx > 0 && idx <= self.cfn_parameter_column_ids.len() {
1833 if let Some(col) = self.cfn_parameter_column_ids.get(idx - 1) {
1834 if let Some(pos) = self
1835 .cfn_parameter_visible_column_ids
1836 .iter()
1837 .position(|c| c == col)
1838 {
1839 self.cfn_parameter_visible_column_ids.remove(pos);
1840 } else {
1841 self.cfn_parameter_visible_column_ids.push(col);
1842 }
1843 }
1844 } else if idx == self.cfn_parameter_column_ids.len() + 3 {
1845 self.cfn_state.parameters.page_size = PageSize::Ten;
1846 } else if idx == self.cfn_parameter_column_ids.len() + 4 {
1847 self.cfn_state.parameters.page_size = PageSize::TwentyFive;
1848 } else if idx == self.cfn_parameter_column_ids.len() + 5 {
1849 self.cfn_state.parameters.page_size = PageSize::Fifty;
1850 } else if idx == self.cfn_parameter_column_ids.len() + 6 {
1851 self.cfn_state.parameters.page_size = PageSize::OneHundred;
1852 }
1853 } else if self.cfn_state.current_stack.is_some()
1854 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1855 {
1856 if idx > 0 && idx <= self.cfn_output_column_ids.len() {
1857 if let Some(col) = self.cfn_output_column_ids.get(idx - 1) {
1858 if let Some(pos) = self
1859 .cfn_output_visible_column_ids
1860 .iter()
1861 .position(|c| c == col)
1862 {
1863 self.cfn_output_visible_column_ids.remove(pos);
1864 } else {
1865 self.cfn_output_visible_column_ids.push(col);
1866 }
1867 }
1868 } else if idx == self.cfn_output_column_ids.len() + 3 {
1869 self.cfn_state.outputs.page_size = PageSize::Ten;
1870 } else if idx == self.cfn_output_column_ids.len() + 4 {
1871 self.cfn_state.outputs.page_size = PageSize::TwentyFive;
1872 } else if idx == self.cfn_output_column_ids.len() + 5 {
1873 self.cfn_state.outputs.page_size = PageSize::Fifty;
1874 } else if idx == self.cfn_output_column_ids.len() + 6 {
1875 self.cfn_state.outputs.page_size = PageSize::OneHundred;
1876 }
1877 } else if self.cfn_state.current_stack.is_some()
1878 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1879 {
1880 if idx > 0 && idx <= self.cfn_resource_column_ids.len() {
1881 if let Some(col) = self.cfn_resource_column_ids.get(idx - 1) {
1882 if let Some(pos) = self
1883 .cfn_resource_visible_column_ids
1884 .iter()
1885 .position(|c| c == col)
1886 {
1887 self.cfn_resource_visible_column_ids.remove(pos);
1888 } else {
1889 self.cfn_resource_visible_column_ids.push(col);
1890 }
1891 }
1892 } else if idx == self.cfn_resource_column_ids.len() + 3 {
1893 self.cfn_state.resources.page_size = PageSize::Ten;
1894 } else if idx == self.cfn_resource_column_ids.len() + 4 {
1895 self.cfn_state.resources.page_size = PageSize::TwentyFive;
1896 } else if idx == self.cfn_resource_column_ids.len() + 5 {
1897 self.cfn_state.resources.page_size = PageSize::Fifty;
1898 } else if idx == self.cfn_resource_column_ids.len() + 6 {
1899 self.cfn_state.resources.page_size = PageSize::OneHundred;
1900 }
1901 } else if self.cfn_state.current_stack.is_none() {
1902 if idx > 0 && idx <= self.cfn_column_ids.len() {
1904 if let Some(col) = self.cfn_column_ids.get(idx - 1) {
1905 if let Some(pos) =
1906 self.cfn_visible_column_ids.iter().position(|c| c == col)
1907 {
1908 self.cfn_visible_column_ids.remove(pos);
1909 } else {
1910 self.cfn_visible_column_ids.push(*col);
1911 }
1912 }
1913 } else if idx == self.cfn_column_ids.len() + 3 {
1914 self.cfn_state.table.page_size = PageSize::Ten;
1915 } else if idx == self.cfn_column_ids.len() + 4 {
1916 self.cfn_state.table.page_size = PageSize::TwentyFive;
1917 } else if idx == self.cfn_column_ids.len() + 5 {
1918 self.cfn_state.table.page_size = PageSize::Fifty;
1919 } else if idx == self.cfn_column_ids.len() + 6 {
1920 self.cfn_state.table.page_size = PageSize::OneHundred;
1921 }
1922 }
1923 } else if self.current_service == Service::IamUsers {
1925 let idx = self.column_selector_index;
1926 if self.iam_state.current_user.is_some() {
1927 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
1929 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
1930 if let Some(pos) = self
1931 .iam_policy_visible_column_ids
1932 .iter()
1933 .position(|c| c == col)
1934 {
1935 self.iam_policy_visible_column_ids.remove(pos);
1936 } else {
1937 self.iam_policy_visible_column_ids.push(col.clone());
1938 }
1939 }
1940 } else if idx == self.iam_policy_column_ids.len() + 3 {
1941 self.iam_state.policies.page_size = PageSize::Ten;
1942 } else if idx == self.iam_policy_column_ids.len() + 4 {
1943 self.iam_state.policies.page_size = PageSize::TwentyFive;
1944 } else if idx == self.iam_policy_column_ids.len() + 5 {
1945 self.iam_state.policies.page_size = PageSize::Fifty;
1946 }
1947 } else {
1948 if idx > 0 && idx <= self.iam_user_column_ids.len() {
1950 if let Some(col) = self.iam_user_column_ids.get(idx - 1) {
1951 if let Some(pos) = self
1952 .iam_user_visible_column_ids
1953 .iter()
1954 .position(|c| c == col)
1955 {
1956 self.iam_user_visible_column_ids.remove(pos);
1957 } else {
1958 self.iam_user_visible_column_ids.push(*col);
1959 }
1960 }
1961 } else if idx == self.iam_user_column_ids.len() + 3 {
1962 self.iam_state.users.page_size = PageSize::Ten;
1963 } else if idx == self.iam_user_column_ids.len() + 4 {
1964 self.iam_state.users.page_size = PageSize::TwentyFive;
1965 } else if idx == self.iam_user_column_ids.len() + 5 {
1966 self.iam_state.users.page_size = PageSize::Fifty;
1967 }
1968 }
1969 } else if self.current_service == Service::IamRoles {
1970 let idx = self.column_selector_index;
1971 if self.iam_state.current_role.is_some() {
1972 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
1974 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
1975 if let Some(pos) = self
1976 .iam_policy_visible_column_ids
1977 .iter()
1978 .position(|c| c == col)
1979 {
1980 self.iam_policy_visible_column_ids.remove(pos);
1981 } else {
1982 self.iam_policy_visible_column_ids.push(col.clone());
1983 }
1984 }
1985 } else if idx == self.iam_policy_column_ids.len() + 3 {
1986 self.iam_state.policies.page_size = PageSize::Ten;
1987 } else if idx == self.iam_policy_column_ids.len() + 4 {
1988 self.iam_state.policies.page_size = PageSize::TwentyFive;
1989 } else if idx == self.iam_policy_column_ids.len() + 5 {
1990 self.iam_state.policies.page_size = PageSize::Fifty;
1991 }
1992 } else {
1993 if idx > 0 && idx <= self.iam_role_column_ids.len() {
1995 if let Some(col) = self.iam_role_column_ids.get(idx - 1) {
1996 if let Some(pos) = self
1997 .iam_role_visible_column_ids
1998 .iter()
1999 .position(|c| c == col)
2000 {
2001 self.iam_role_visible_column_ids.remove(pos);
2002 } else {
2003 self.iam_role_visible_column_ids.push(col.clone());
2004 }
2005 }
2006 } else if idx == self.iam_role_column_ids.len() + 3 {
2007 self.iam_state.roles.page_size = PageSize::Ten;
2008 } else if idx == self.iam_role_column_ids.len() + 4 {
2009 self.iam_state.roles.page_size = PageSize::TwentyFive;
2010 } else if idx == self.iam_role_column_ids.len() + 5 {
2011 self.iam_state.roles.page_size = PageSize::Fifty;
2012 }
2013 }
2014 } else if self.current_service == Service::IamUserGroups {
2015 let idx = self.column_selector_index;
2016 if idx > 0 && idx <= self.iam_group_column_ids.len() {
2017 if let Some(col) = self.iam_group_column_ids.get(idx - 1) {
2018 if let Some(pos) = self
2019 .iam_group_visible_column_ids
2020 .iter()
2021 .position(|c| c == col)
2022 {
2023 self.iam_group_visible_column_ids.remove(pos);
2024 } else {
2025 self.iam_group_visible_column_ids.push(col.clone());
2026 }
2027 }
2028 } else if idx == self.iam_group_column_ids.len() + 3 {
2029 self.iam_state.groups.page_size = PageSize::Ten;
2030 } else if idx == self.iam_group_column_ids.len() + 4 {
2031 self.iam_state.groups.page_size = PageSize::TwentyFive;
2032 } else if idx == self.iam_group_column_ids.len() + 5 {
2033 self.iam_state.groups.page_size = PageSize::Fifty;
2034 }
2035 } else if let Some(col) =
2036 self.cw_log_group_column_ids.get(self.column_selector_index)
2037 {
2038 if let Some(pos) = self
2039 .cw_log_group_visible_column_ids
2040 .iter()
2041 .position(|c| c == col)
2042 {
2043 self.cw_log_group_visible_column_ids.remove(pos);
2044 } else {
2045 self.cw_log_group_visible_column_ids.push(*col);
2046 }
2047 }
2048 }
2049 Action::NextPreferences => {
2050 if self.current_service == Service::CloudWatchAlarms {
2051 if self.column_selector_index < 18 {
2053 self.column_selector_index = 18; } else if self.column_selector_index < 22 {
2055 self.column_selector_index = 22; } else if self.column_selector_index < 28 {
2057 self.column_selector_index = 28; } else {
2059 self.column_selector_index = 0; }
2061 } else if self.current_service == Service::EcrRepositories
2062 && self.ecr_state.current_repository.is_some()
2063 {
2064 let page_size_idx = self.ecr_image_column_ids.len() + 2;
2066 if self.column_selector_index < page_size_idx {
2067 self.column_selector_index = page_size_idx;
2068 } else {
2069 self.column_selector_index = 0;
2070 }
2071 } else if self.current_service == Service::LambdaFunctions {
2072 let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2074 if self.column_selector_index < page_size_idx {
2075 self.column_selector_index = page_size_idx;
2076 } else {
2077 self.column_selector_index = 0;
2078 }
2079 } else if self.current_service == Service::LambdaApplications {
2080 let page_size_idx = self.lambda_application_column_ids.len() + 2;
2082 if self.column_selector_index < page_size_idx {
2083 self.column_selector_index = page_size_idx;
2084 } else {
2085 self.column_selector_index = 0;
2086 }
2087 } else if self.current_service == Service::CloudFormationStacks {
2088 let page_size_idx = self.cfn_column_ids.len() + 2;
2090 if self.column_selector_index < page_size_idx {
2091 self.column_selector_index = page_size_idx;
2092 } else {
2093 self.column_selector_index = 0;
2094 }
2095 } else if self.current_service == Service::Ec2Instances {
2096 let page_size_idx = self.ec2_column_ids.len() + 2;
2097 if self.column_selector_index < page_size_idx {
2098 self.column_selector_index = page_size_idx;
2099 } else {
2100 self.column_selector_index = 0;
2101 }
2102 } else if self.current_service == Service::IamUsers {
2103 if self.iam_state.current_user.is_some() {
2104 if self.iam_state.user_tab == UserTab::Permissions {
2106 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2107 if self.column_selector_index < page_size_idx {
2108 self.column_selector_index = page_size_idx;
2109 } else {
2110 self.column_selector_index = 0;
2111 }
2112 }
2113 } else {
2115 let page_size_idx = self.iam_user_column_ids.len() + 2;
2117 if self.column_selector_index < page_size_idx {
2118 self.column_selector_index = page_size_idx;
2119 } else {
2120 self.column_selector_index = 0;
2121 }
2122 }
2123 } else if self.current_service == Service::IamRoles {
2124 if self.iam_state.current_role.is_some() {
2125 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2127 if self.column_selector_index < page_size_idx {
2128 self.column_selector_index = page_size_idx;
2129 } else {
2130 self.column_selector_index = 0;
2131 }
2132 } else {
2133 let page_size_idx = self.iam_role_column_ids.len() + 2;
2135 if self.column_selector_index < page_size_idx {
2136 self.column_selector_index = page_size_idx;
2137 } else {
2138 self.column_selector_index = 0;
2139 }
2140 }
2141 } else if self.current_service == Service::IamUserGroups {
2142 let page_size_idx = self.iam_group_column_ids.len() + 2;
2144 if self.column_selector_index < page_size_idx {
2145 self.column_selector_index = page_size_idx;
2146 } else {
2147 self.column_selector_index = 0;
2148 }
2149 } else if self.current_service == Service::SqsQueues
2150 && self.sqs_state.current_queue.is_some()
2151 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2152 {
2153 let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2155 if self.column_selector_index < page_size_idx {
2156 self.column_selector_index = page_size_idx;
2157 } else {
2158 self.column_selector_index = 0;
2159 }
2160 } else if self.current_service == Service::SqsQueues
2161 && self.sqs_state.current_queue.is_some()
2162 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2163 {
2164 let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2166 if self.column_selector_index < page_size_idx {
2167 self.column_selector_index = page_size_idx;
2168 } else {
2169 self.column_selector_index = 0;
2170 }
2171 } else if self.current_service == Service::SqsQueues
2172 && self.sqs_state.current_queue.is_some()
2173 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2174 {
2175 let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2177 if self.column_selector_index < page_size_idx {
2178 self.column_selector_index = page_size_idx;
2179 } else {
2180 self.column_selector_index = 0;
2181 }
2182 } else if self.current_service == Service::SqsQueues
2183 && self.sqs_state.current_queue.is_some()
2184 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2185 {
2186 let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2188 if self.column_selector_index < page_size_idx {
2189 self.column_selector_index = page_size_idx;
2190 } else {
2191 self.column_selector_index = 0;
2192 }
2193 }
2194 }
2195 Action::PrevPreferences => {
2196 if self.current_service == Service::CloudWatchAlarms {
2197 if self.column_selector_index >= 28 {
2199 self.column_selector_index = 22;
2200 } else if self.column_selector_index >= 22 {
2201 self.column_selector_index = 18;
2202 } else if self.column_selector_index >= 18 {
2203 self.column_selector_index = 0;
2204 } else {
2205 self.column_selector_index = 28;
2206 }
2207 } else if self.current_service == Service::EcrRepositories
2208 && self.ecr_state.current_repository.is_some()
2209 {
2210 let page_size_idx = self.ecr_image_column_ids.len() + 2;
2211 if self.column_selector_index >= page_size_idx {
2212 self.column_selector_index = 0;
2213 } else {
2214 self.column_selector_index = page_size_idx;
2215 }
2216 } else if self.current_service == Service::LambdaFunctions {
2217 let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2218 if self.column_selector_index >= page_size_idx {
2219 self.column_selector_index = 0;
2220 } else {
2221 self.column_selector_index = page_size_idx;
2222 }
2223 } else if self.current_service == Service::LambdaApplications {
2224 let page_size_idx = self.lambda_application_column_ids.len() + 2;
2225 if self.column_selector_index >= page_size_idx {
2226 self.column_selector_index = 0;
2227 } else {
2228 self.column_selector_index = page_size_idx;
2229 }
2230 } else if self.current_service == Service::CloudFormationStacks {
2231 let page_size_idx = self.cfn_column_ids.len() + 2;
2232 if self.column_selector_index >= page_size_idx {
2233 self.column_selector_index = 0;
2234 } else {
2235 self.column_selector_index = page_size_idx;
2236 }
2237 } else if self.current_service == Service::Ec2Instances {
2238 let page_size_idx = self.ec2_column_ids.len() + 2;
2239 if self.column_selector_index >= page_size_idx {
2240 self.column_selector_index = 0;
2241 } else {
2242 self.column_selector_index = page_size_idx;
2243 }
2244 } else if self.current_service == Service::IamUsers {
2245 if self.iam_state.current_user.is_some()
2246 && self.iam_state.user_tab == UserTab::Permissions
2247 {
2248 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2249 if self.column_selector_index >= page_size_idx {
2250 self.column_selector_index = 0;
2251 } else {
2252 self.column_selector_index = page_size_idx;
2253 }
2254 } else if self.iam_state.current_user.is_none() {
2255 let page_size_idx = self.iam_user_column_ids.len() + 2;
2256 if self.column_selector_index >= page_size_idx {
2257 self.column_selector_index = 0;
2258 } else {
2259 self.column_selector_index = page_size_idx;
2260 }
2261 }
2262 } else if self.current_service == Service::IamRoles {
2263 let page_size_idx = if self.iam_state.current_role.is_some() {
2264 self.iam_policy_column_ids.len() + 2
2265 } else {
2266 self.iam_role_column_ids.len() + 2
2267 };
2268 if self.column_selector_index >= page_size_idx {
2269 self.column_selector_index = 0;
2270 } else {
2271 self.column_selector_index = page_size_idx;
2272 }
2273 } else if self.current_service == Service::IamUserGroups {
2274 let page_size_idx = self.iam_group_column_ids.len() + 2;
2275 if self.column_selector_index >= page_size_idx {
2276 self.column_selector_index = 0;
2277 } else {
2278 self.column_selector_index = page_size_idx;
2279 }
2280 } else if self.current_service == Service::SqsQueues
2281 && self.sqs_state.current_queue.is_some()
2282 {
2283 let page_size_idx = match self.sqs_state.detail_tab {
2284 SqsQueueDetailTab::LambdaTriggers => {
2285 self.sqs_state.trigger_column_ids.len() + 2
2286 }
2287 SqsQueueDetailTab::EventBridgePipes => {
2288 self.sqs_state.pipe_column_ids.len() + 2
2289 }
2290 SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
2291 SqsQueueDetailTab::SnsSubscriptions => {
2292 self.sqs_state.subscription_column_ids.len() + 2
2293 }
2294 _ => 0,
2295 };
2296 if page_size_idx > 0 {
2297 if self.column_selector_index >= page_size_idx {
2298 self.column_selector_index = 0;
2299 } else {
2300 self.column_selector_index = page_size_idx;
2301 }
2302 }
2303 }
2304 }
2305 Action::CloseColumnSelector => {
2306 self.mode = Mode::Normal;
2307 self.preference_section = Preferences::Columns;
2308 }
2309 Action::NextDetailTab => {
2310 if self.current_service == Service::SqsQueues
2311 && self.sqs_state.current_queue.is_some()
2312 {
2313 self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
2314 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2315 self.sqs_state.set_metrics_loading(true);
2316 self.sqs_state.set_monitoring_scroll(0);
2317 self.sqs_state.clear_metrics();
2318 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
2319 self.sqs_state.triggers.loading = true;
2320 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
2321 self.sqs_state.pipes.loading = true;
2322 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
2323 self.sqs_state.tags.loading = true;
2324 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
2325 self.sqs_state.subscriptions.loading = true;
2326 }
2327 } else if self.current_service == Service::LambdaApplications
2328 && self.lambda_application_state.current_application.is_some()
2329 {
2330 self.lambda_application_state.detail_tab =
2331 self.lambda_application_state.detail_tab.next();
2332 } else if self.current_service == Service::IamRoles
2333 && self.iam_state.current_role.is_some()
2334 {
2335 self.iam_state.role_tab = self.iam_state.role_tab.next();
2336 if self.iam_state.role_tab == RoleTab::Tags {
2337 self.iam_state.tags.loading = true;
2338 }
2339 } else if self.current_service == Service::IamUsers
2340 && self.iam_state.current_user.is_some()
2341 {
2342 self.iam_state.user_tab = self.iam_state.user_tab.next();
2343 if self.iam_state.user_tab == UserTab::Tags {
2344 self.iam_state.user_tags.loading = true;
2345 }
2346 } else if self.current_service == Service::IamUserGroups
2347 && self.iam_state.current_group.is_some()
2348 {
2349 self.iam_state.group_tab = self.iam_state.group_tab.next();
2350 } else if self.view_mode == ViewMode::Detail {
2351 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
2352 } else if self.current_service == Service::S3Buckets {
2353 if self.s3_state.current_bucket.is_some() {
2354 self.s3_state.object_tab = self.s3_state.object_tab.next();
2355 } else {
2356 self.s3_state.bucket_type = match self.s3_state.bucket_type {
2357 S3BucketType::GeneralPurpose => S3BucketType::Directory,
2358 S3BucketType::Directory => S3BucketType::GeneralPurpose,
2359 };
2360 self.s3_state.buckets.reset();
2361 }
2362 } else if self.current_service == Service::CloudWatchAlarms {
2363 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2364 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2365 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2366 };
2367 self.alarms_state.table.reset();
2368 } else if self.current_service == Service::EcrRepositories
2369 && self.ecr_state.current_repository.is_none()
2370 {
2371 self.ecr_state.tab = self.ecr_state.tab.next();
2372 self.ecr_state.repositories.reset();
2373 self.ecr_state.repositories.loading = true;
2374 } else if self.current_service == Service::LambdaFunctions
2375 && self.lambda_state.current_function.is_some()
2376 {
2377 if self.lambda_state.current_version.is_some() {
2378 self.lambda_state.version_detail_tab =
2380 self.lambda_state.version_detail_tab.next();
2381 self.lambda_state.detail_tab =
2382 self.lambda_state.version_detail_tab.to_detail_tab();
2383 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2384 self.lambda_state.set_metrics_loading(true);
2385 self.lambda_state.set_monitoring_scroll(0);
2386 self.lambda_state.clear_metrics();
2387 }
2388 } else {
2389 self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
2390 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2391 self.lambda_state.set_metrics_loading(true);
2392 self.lambda_state.set_monitoring_scroll(0);
2393 self.lambda_state.clear_metrics();
2394 }
2395 }
2396 } else if self.current_service == Service::CloudFormationStacks
2397 && self.cfn_state.current_stack.is_some()
2398 {
2399 self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
2400 }
2401 }
2402 Action::PrevDetailTab => {
2403 if self.current_service == Service::SqsQueues
2404 && self.sqs_state.current_queue.is_some()
2405 {
2406 self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
2407 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2408 self.sqs_state.set_metrics_loading(true);
2409 self.sqs_state.set_monitoring_scroll(0);
2410 self.sqs_state.clear_metrics();
2411 }
2412 } else if self.current_service == Service::LambdaApplications
2413 && self.lambda_application_state.current_application.is_some()
2414 {
2415 self.lambda_application_state.detail_tab =
2416 self.lambda_application_state.detail_tab.prev();
2417 } else if self.current_service == Service::IamRoles
2418 && self.iam_state.current_role.is_some()
2419 {
2420 self.iam_state.role_tab = self.iam_state.role_tab.prev();
2421 } else if self.current_service == Service::IamUsers
2422 && self.iam_state.current_user.is_some()
2423 {
2424 self.iam_state.user_tab = self.iam_state.user_tab.prev();
2425 } else if self.current_service == Service::IamUserGroups
2426 && self.iam_state.current_group.is_some()
2427 {
2428 self.iam_state.group_tab = self.iam_state.group_tab.prev();
2429 } else if self.view_mode == ViewMode::Detail {
2430 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
2431 } else if self.current_service == Service::S3Buckets {
2432 if self.s3_state.current_bucket.is_some() {
2433 self.s3_state.object_tab = self.s3_state.object_tab.prev();
2434 }
2435 } else if self.current_service == Service::CloudWatchAlarms {
2436 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2437 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2438 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2439 };
2440 } else if self.current_service == Service::EcrRepositories
2441 && self.ecr_state.current_repository.is_none()
2442 {
2443 self.ecr_state.tab = self.ecr_state.tab.prev();
2444 self.ecr_state.repositories.reset();
2445 self.ecr_state.repositories.loading = true;
2446 } else if self.current_service == Service::LambdaFunctions
2447 && self.lambda_state.current_function.is_some()
2448 {
2449 if self.lambda_state.current_version.is_some() {
2450 self.lambda_state.version_detail_tab =
2452 self.lambda_state.version_detail_tab.prev();
2453 self.lambda_state.detail_tab =
2454 self.lambda_state.version_detail_tab.to_detail_tab();
2455 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2456 self.lambda_state.set_metrics_loading(true);
2457 self.lambda_state.set_monitoring_scroll(0);
2458 self.lambda_state.clear_metrics();
2459 }
2460 } else {
2461 self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
2462 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2463 self.lambda_state.set_metrics_loading(true);
2464 self.lambda_state.set_monitoring_scroll(0);
2465 self.lambda_state.clear_metrics();
2466 }
2467 }
2468 } else if self.current_service == Service::CloudFormationStacks
2469 && self.cfn_state.current_stack.is_some()
2470 {
2471 self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
2472 }
2473 }
2474 Action::StartFilter => {
2475 if !self.service_selected && self.tabs.is_empty() {
2477 return;
2478 }
2479
2480 if self.current_service == Service::CloudWatchInsights {
2481 self.mode = Mode::InsightsInput;
2482 } else if self.current_service == Service::CloudWatchAlarms {
2483 self.mode = Mode::FilterInput;
2484 } else if self.current_service == Service::S3Buckets {
2485 self.mode = Mode::FilterInput;
2486 self.log_groups_state.filter_mode = true;
2487 } else if self.current_service == Service::EcrRepositories
2488 || self.current_service == Service::IamUsers
2489 || self.current_service == Service::IamUserGroups
2490 {
2491 self.mode = Mode::FilterInput;
2492 if self.current_service == Service::EcrRepositories
2493 && self.ecr_state.current_repository.is_none()
2494 {
2495 self.ecr_state.input_focus = InputFocus::Filter;
2496 }
2497 } else if self.current_service == Service::LambdaFunctions {
2498 self.mode = Mode::FilterInput;
2499 if self.lambda_state.current_version.is_some()
2500 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2501 {
2502 self.lambda_state.alias_input_focus = InputFocus::Filter;
2503 } else if self.lambda_state.current_function.is_some()
2504 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2505 {
2506 self.lambda_state.version_input_focus = InputFocus::Filter;
2507 } else if self.lambda_state.current_function.is_none() {
2508 self.lambda_state.input_focus = InputFocus::Filter;
2509 }
2510 } else if self.current_service == Service::LambdaApplications {
2511 self.mode = Mode::FilterInput;
2512 if self.lambda_application_state.current_application.is_some() {
2513 if self.lambda_application_state.detail_tab
2515 == LambdaApplicationDetailTab::Overview
2516 {
2517 self.lambda_application_state.resource_input_focus = InputFocus::Filter;
2518 } else {
2519 self.lambda_application_state.deployment_input_focus =
2520 InputFocus::Filter;
2521 }
2522 } else {
2523 self.lambda_application_state.input_focus = InputFocus::Filter;
2524 }
2525 } else if self.current_service == Service::IamRoles {
2526 self.mode = Mode::FilterInput;
2527 } else if self.current_service == Service::CloudFormationStacks {
2528 self.mode = Mode::FilterInput;
2529 if self.cfn_state.current_stack.is_some()
2530 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2531 {
2532 self.cfn_state.parameters_input_focus = InputFocus::Filter;
2533 } else if self.cfn_state.current_stack.is_some()
2534 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2535 {
2536 self.cfn_state.outputs_input_focus = InputFocus::Filter;
2537 } else {
2538 self.cfn_state.input_focus = InputFocus::Filter;
2539 }
2540 } else if self.current_service == Service::SqsQueues {
2541 self.mode = Mode::FilterInput;
2542 self.sqs_state.input_focus = InputFocus::Filter;
2543 } else if self.view_mode == ViewMode::List
2544 || (self.view_mode == ViewMode::Detail
2545 && self.log_groups_state.detail_tab == DetailTab::LogStreams)
2546 {
2547 self.mode = Mode::FilterInput;
2548 self.log_groups_state.filter_mode = true;
2549 self.log_groups_state.input_focus = InputFocus::Filter;
2550 }
2551 }
2552 Action::StartEventFilter => {
2553 if self.current_service == Service::CloudWatchInsights {
2554 self.mode = Mode::InsightsInput;
2555 } else if self.view_mode == ViewMode::List {
2556 self.mode = Mode::FilterInput;
2557 self.log_groups_state.filter_mode = true;
2558 self.log_groups_state.input_focus = InputFocus::Filter;
2559 } else if self.view_mode == ViewMode::Events {
2560 self.mode = Mode::EventFilterInput;
2561 } else if self.view_mode == ViewMode::Detail
2562 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2563 {
2564 self.mode = Mode::FilterInput;
2565 self.log_groups_state.filter_mode = true;
2566 self.log_groups_state.input_focus = InputFocus::Filter;
2567 }
2568 }
2569 Action::NextFilterFocus => {
2570 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
2571 self.ec2_state.input_focus =
2572 self.ec2_state.input_focus.next(&ec2::FILTER_CONTROLS);
2573 } else if self.mode == Mode::FilterInput
2574 && self.current_service == Service::LambdaApplications
2575 {
2576 use crate::ui::lambda::FILTER_CONTROLS;
2577 if self.lambda_application_state.current_application.is_some() {
2578 if self.lambda_application_state.detail_tab
2579 == LambdaApplicationDetailTab::Deployments
2580 {
2581 self.lambda_application_state.deployment_input_focus = self
2582 .lambda_application_state
2583 .deployment_input_focus
2584 .next(&FILTER_CONTROLS);
2585 } else {
2586 self.lambda_application_state.resource_input_focus = self
2587 .lambda_application_state
2588 .resource_input_focus
2589 .next(&FILTER_CONTROLS);
2590 }
2591 } else {
2592 self.lambda_application_state.input_focus = self
2593 .lambda_application_state
2594 .input_focus
2595 .next(&FILTER_CONTROLS);
2596 }
2597 } else if self.mode == Mode::FilterInput
2598 && self.current_service == Service::IamRoles
2599 && self.iam_state.current_role.is_some()
2600 {
2601 use crate::ui::iam::POLICY_FILTER_CONTROLS;
2602 self.iam_state.policy_input_focus = self
2603 .iam_state
2604 .policy_input_focus
2605 .next(&POLICY_FILTER_CONTROLS);
2606 } else if self.mode == Mode::FilterInput
2607 && self.current_service == Service::IamRoles
2608 && self.iam_state.current_role.is_none()
2609 {
2610 use crate::ui::iam::ROLE_FILTER_CONTROLS;
2611 self.iam_state.role_input_focus =
2612 self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
2613 } else if self.mode == Mode::InsightsInput {
2614 use crate::app::InsightsFocus;
2615 self.insights_state.insights.insights_focus =
2616 match self.insights_state.insights.insights_focus {
2617 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
2618 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
2619 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
2620 InsightsFocus::Query => InsightsFocus::QueryLanguage,
2621 };
2622 } else if self.mode == Mode::FilterInput
2623 && self.current_service == Service::CloudFormationStacks
2624 {
2625 if self.cfn_state.current_stack.is_some()
2626 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2627 {
2628 self.cfn_state.parameters_input_focus = self
2629 .cfn_state
2630 .parameters_input_focus
2631 .next(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
2632 } else if self.cfn_state.current_stack.is_some()
2633 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2634 {
2635 self.cfn_state.outputs_input_focus = self
2636 .cfn_state
2637 .outputs_input_focus
2638 .next(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
2639 } else if self.cfn_state.current_stack.is_some()
2640 && self.cfn_state.detail_tab == CfnDetailTab::Resources
2641 {
2642 self.cfn_state.resources_input_focus = self
2643 .cfn_state
2644 .resources_input_focus
2645 .next(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
2646 } else {
2647 self.cfn_state.input_focus = self
2648 .cfn_state
2649 .input_focus
2650 .next(&CfnStateConstants::FILTER_CONTROLS);
2651 }
2652 } else if self.mode == Mode::FilterInput
2653 && self.current_service == Service::SqsQueues
2654 {
2655 if self.sqs_state.current_queue.is_some()
2656 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2657 {
2658 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2659 self.sqs_state.input_focus = self
2660 .sqs_state
2661 .input_focus
2662 .next(SUBSCRIPTION_FILTER_CONTROLS);
2663 } else {
2664 use crate::ui::sqs::FILTER_CONTROLS;
2665 self.sqs_state.input_focus =
2666 self.sqs_state.input_focus.next(FILTER_CONTROLS);
2667 }
2668 } else if self.mode == Mode::FilterInput
2669 && self.current_service == Service::CloudWatchLogGroups
2670 {
2671 use crate::ui::cw::logs::FILTER_CONTROLS;
2672 self.log_groups_state.input_focus =
2673 self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
2674 } else if self.mode == Mode::EventFilterInput {
2675 self.log_groups_state.event_input_focus =
2676 self.log_groups_state.event_input_focus.next();
2677 } else if self.mode == Mode::FilterInput
2678 && self.current_service == Service::CloudWatchAlarms
2679 {
2680 use crate::ui::cw::alarms::FILTER_CONTROLS;
2681 self.alarms_state.input_focus =
2682 self.alarms_state.input_focus.next(&FILTER_CONTROLS);
2683 } else if self.mode == Mode::FilterInput
2684 && self.current_service == Service::EcrRepositories
2685 && self.ecr_state.current_repository.is_none()
2686 {
2687 use crate::ui::ecr::FILTER_CONTROLS;
2688 self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
2689 } else if self.mode == Mode::FilterInput
2690 && self.current_service == Service::LambdaFunctions
2691 {
2692 use crate::ui::lambda::FILTER_CONTROLS;
2693 if self.lambda_state.current_version.is_some()
2694 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2695 {
2696 self.lambda_state.alias_input_focus =
2697 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
2698 } else if self.lambda_state.current_function.is_some()
2699 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2700 {
2701 self.lambda_state.version_input_focus =
2702 self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
2703 } else if self.lambda_state.current_function.is_some()
2704 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
2705 {
2706 self.lambda_state.alias_input_focus =
2707 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
2708 } else if self.lambda_state.current_function.is_none() {
2709 self.lambda_state.input_focus =
2710 self.lambda_state.input_focus.next(&FILTER_CONTROLS);
2711 }
2712 }
2713 }
2714 Action::PrevFilterFocus => {
2715 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
2716 self.ec2_state.input_focus =
2717 self.ec2_state.input_focus.prev(&ec2::FILTER_CONTROLS);
2718 } else if self.mode == Mode::FilterInput
2719 && self.current_service == Service::LambdaApplications
2720 {
2721 use crate::ui::lambda::FILTER_CONTROLS;
2722 if self.lambda_application_state.current_application.is_some() {
2723 if self.lambda_application_state.detail_tab
2724 == LambdaApplicationDetailTab::Deployments
2725 {
2726 self.lambda_application_state.deployment_input_focus = self
2727 .lambda_application_state
2728 .deployment_input_focus
2729 .prev(&FILTER_CONTROLS);
2730 } else {
2731 self.lambda_application_state.resource_input_focus = self
2732 .lambda_application_state
2733 .resource_input_focus
2734 .prev(&FILTER_CONTROLS);
2735 }
2736 } else {
2737 self.lambda_application_state.input_focus = self
2738 .lambda_application_state
2739 .input_focus
2740 .prev(&FILTER_CONTROLS);
2741 }
2742 } else if self.mode == Mode::FilterInput
2743 && self.current_service == Service::CloudFormationStacks
2744 {
2745 if self.cfn_state.current_stack.is_some()
2746 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2747 {
2748 self.cfn_state.parameters_input_focus = self
2749 .cfn_state
2750 .parameters_input_focus
2751 .prev(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
2752 } else if self.cfn_state.current_stack.is_some()
2753 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2754 {
2755 self.cfn_state.outputs_input_focus = self
2756 .cfn_state
2757 .outputs_input_focus
2758 .prev(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
2759 } else if self.cfn_state.current_stack.is_some()
2760 && self.cfn_state.detail_tab == CfnDetailTab::Resources
2761 {
2762 self.cfn_state.resources_input_focus = self
2763 .cfn_state
2764 .resources_input_focus
2765 .prev(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
2766 } else {
2767 self.cfn_state.input_focus = self
2768 .cfn_state
2769 .input_focus
2770 .prev(&CfnStateConstants::FILTER_CONTROLS);
2771 }
2772 } else if self.mode == Mode::FilterInput
2773 && self.current_service == Service::SqsQueues
2774 {
2775 if self.sqs_state.current_queue.is_some()
2776 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2777 {
2778 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2779 self.sqs_state.input_focus = self
2780 .sqs_state
2781 .input_focus
2782 .prev(SUBSCRIPTION_FILTER_CONTROLS);
2783 } else {
2784 use crate::ui::sqs::FILTER_CONTROLS;
2785 self.sqs_state.input_focus =
2786 self.sqs_state.input_focus.prev(FILTER_CONTROLS);
2787 }
2788 } else if self.mode == Mode::FilterInput
2789 && self.current_service == Service::IamRoles
2790 && self.iam_state.current_role.is_none()
2791 {
2792 use crate::ui::iam::ROLE_FILTER_CONTROLS;
2793 self.iam_state.role_input_focus =
2794 self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
2795 } else if self.mode == Mode::FilterInput
2796 && self.current_service == Service::CloudWatchLogGroups
2797 {
2798 use crate::ui::cw::logs::FILTER_CONTROLS;
2799 self.log_groups_state.input_focus =
2800 self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
2801 } else if self.mode == Mode::EventFilterInput {
2802 self.log_groups_state.event_input_focus =
2803 self.log_groups_state.event_input_focus.prev();
2804 } else if self.mode == Mode::FilterInput
2805 && self.current_service == Service::IamRoles
2806 && self.iam_state.current_role.is_some()
2807 {
2808 use crate::ui::iam::POLICY_FILTER_CONTROLS;
2809 self.iam_state.policy_input_focus = self
2810 .iam_state
2811 .policy_input_focus
2812 .prev(&POLICY_FILTER_CONTROLS);
2813 } else if self.mode == Mode::FilterInput
2814 && self.current_service == Service::CloudWatchAlarms
2815 {
2816 use crate::ui::cw::alarms::FILTER_CONTROLS;
2817 self.alarms_state.input_focus =
2818 self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
2819 } else if self.mode == Mode::FilterInput
2820 && self.current_service == Service::EcrRepositories
2821 && self.ecr_state.current_repository.is_none()
2822 {
2823 use crate::ui::ecr::FILTER_CONTROLS;
2824 self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
2825 } else if self.mode == Mode::FilterInput
2826 && self.current_service == Service::LambdaFunctions
2827 {
2828 use crate::ui::lambda::FILTER_CONTROLS;
2829 if self.lambda_state.current_version.is_some()
2830 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2831 {
2832 self.lambda_state.alias_input_focus =
2833 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2834 } else if self.lambda_state.current_function.is_some()
2835 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2836 {
2837 self.lambda_state.version_input_focus =
2838 self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
2839 } else if self.lambda_state.current_function.is_some()
2840 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
2841 {
2842 self.lambda_state.alias_input_focus =
2843 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2844 } else if self.lambda_state.current_function.is_none() {
2845 self.lambda_state.input_focus =
2846 self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
2847 }
2848 }
2849 }
2850 Action::ToggleFilterCheckbox => {
2851 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
2852 if self.ec2_state.input_focus == EC2_STATE_FILTER {
2853 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
2854 self.ec2_state.table.reset();
2855 }
2856 } else if self.mode == Mode::InsightsInput {
2857 use crate::app::InsightsFocus;
2858 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2859 && self.insights_state.insights.show_dropdown
2860 && !self.insights_state.insights.log_group_matches.is_empty()
2861 {
2862 let selected_idx = self.insights_state.insights.dropdown_selected;
2863 if let Some(group_name) = self
2864 .insights_state
2865 .insights
2866 .log_group_matches
2867 .get(selected_idx)
2868 {
2869 let group_name = group_name.clone();
2870 if let Some(pos) = self
2871 .insights_state
2872 .insights
2873 .selected_log_groups
2874 .iter()
2875 .position(|g| g == &group_name)
2876 {
2877 self.insights_state.insights.selected_log_groups.remove(pos);
2878 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2879 self.insights_state
2880 .insights
2881 .selected_log_groups
2882 .push(group_name);
2883 }
2884 }
2885 }
2886 } else if self.mode == Mode::FilterInput
2887 && self.current_service == Service::CloudFormationStacks
2888 {
2889 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
2890 match self.cfn_state.input_focus {
2891 STATUS_FILTER => {
2892 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
2893 self.cfn_state.table.reset();
2894 }
2895 VIEW_NESTED => {
2896 self.cfn_state.view_nested = !self.cfn_state.view_nested;
2897 self.cfn_state.table.reset();
2898 }
2899 _ => {}
2900 }
2901 } else if self.mode == Mode::FilterInput
2902 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2903 {
2904 match self.log_groups_state.input_focus {
2905 InputFocus::Checkbox("ExactMatch") => {
2906 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2907 }
2908 InputFocus::Checkbox("ShowExpired") => {
2909 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2910 }
2911 _ => {}
2912 }
2913 } else if self.mode == Mode::EventFilterInput
2914 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2915 {
2916 self.log_groups_state.relative_unit =
2917 self.log_groups_state.relative_unit.next();
2918 }
2919 }
2920 Action::CycleSortColumn => {
2921 if self.view_mode == ViewMode::Detail
2922 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2923 {
2924 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2925 StreamSort::Name => StreamSort::CreationTime,
2926 StreamSort::CreationTime => StreamSort::LastEventTime,
2927 StreamSort::LastEventTime => StreamSort::Name,
2928 };
2929 }
2930 }
2931 Action::ToggleSortDirection => {
2932 if self.view_mode == ViewMode::Detail
2933 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2934 {
2935 self.log_groups_state.stream_sort_desc =
2936 !self.log_groups_state.stream_sort_desc;
2937 }
2938 }
2939 Action::ScrollUp => {
2940 if self.mode == Mode::ErrorModal {
2941 self.error_scroll = self.error_scroll.saturating_sub(1);
2942 } else if self.current_service == Service::LambdaFunctions
2943 && self.lambda_state.current_function.is_some()
2944 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
2945 && !self.lambda_state.is_metrics_loading()
2946 {
2947 self.lambda_state.set_monitoring_scroll(
2948 self.lambda_state.monitoring_scroll().saturating_sub(1),
2949 );
2950 } else if self.current_service == Service::SqsQueues
2951 && self.sqs_state.current_queue.is_some()
2952 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2953 && !self.sqs_state.is_metrics_loading()
2954 {
2955 self.sqs_state.set_monitoring_scroll(
2956 self.sqs_state.monitoring_scroll().saturating_sub(1),
2957 );
2958 } else if self.view_mode == ViewMode::PolicyView {
2959 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
2960 } else if self.current_service == Service::IamRoles
2961 && self.iam_state.current_role.is_some()
2962 && self.iam_state.role_tab == RoleTab::TrustRelationships
2963 {
2964 self.iam_state.trust_policy_scroll =
2965 self.iam_state.trust_policy_scroll.saturating_sub(10);
2966 } else if self.view_mode == ViewMode::Events {
2967 if self.log_groups_state.event_scroll_offset == 0
2968 && self.log_groups_state.has_older_events
2969 {
2970 self.log_groups_state.loading = true;
2971 } else {
2972 self.log_groups_state.event_scroll_offset =
2973 self.log_groups_state.event_scroll_offset.saturating_sub(1);
2974 }
2975 } else if self.view_mode == ViewMode::InsightsResults {
2976 self.insights_state.insights.results_selected = self
2977 .insights_state
2978 .insights
2979 .results_selected
2980 .saturating_sub(1);
2981 } else if self.view_mode == ViewMode::Detail {
2982 self.log_groups_state.selected_stream =
2983 self.log_groups_state.selected_stream.saturating_sub(1);
2984 self.log_groups_state.expanded_stream = None;
2985 } else if self.view_mode == ViewMode::List
2986 && self.current_service == Service::CloudWatchLogGroups
2987 {
2988 self.log_groups_state.log_groups.selected =
2989 self.log_groups_state.log_groups.selected.saturating_sub(1);
2990 self.log_groups_state.log_groups.snap_to_page();
2991 } else if self.current_service == Service::EcrRepositories {
2992 if self.ecr_state.current_repository.is_some() {
2993 self.ecr_state.images.page_up();
2994 } else {
2995 self.ecr_state.repositories.page_up();
2996 }
2997 }
2998 }
2999 Action::ScrollDown => {
3000 if self.mode == Mode::ErrorModal {
3001 if let Some(error_msg) = &self.error_message {
3002 let lines = error_msg.lines().count();
3003 let max_scroll = lines.saturating_sub(1);
3004 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
3005 }
3006 } else if self.current_service == Service::SqsQueues
3007 && self.sqs_state.current_queue.is_some()
3008 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3009 {
3010 self.sqs_state
3011 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
3012 } else if self.view_mode == ViewMode::PolicyView {
3013 let lines = self.iam_state.policy_document.lines().count();
3014 let max_scroll = lines.saturating_sub(1);
3015 self.iam_state.policy_scroll =
3016 (self.iam_state.policy_scroll + 10).min(max_scroll);
3017 } else if self.current_service == Service::IamRoles
3018 && self.iam_state.current_role.is_some()
3019 && self.iam_state.role_tab == RoleTab::TrustRelationships
3020 {
3021 let lines = self.iam_state.trust_policy_document.lines().count();
3022 let max_scroll = lines.saturating_sub(1);
3023 self.iam_state.trust_policy_scroll =
3024 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
3025 } else if self.view_mode == ViewMode::Events {
3026 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3027 if self.log_groups_state.event_scroll_offset >= max_scroll {
3028 } else {
3030 self.log_groups_state.event_scroll_offset =
3031 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3032 }
3033 } else if self.view_mode == ViewMode::InsightsResults {
3034 let max = self
3035 .insights_state
3036 .insights
3037 .query_results
3038 .len()
3039 .saturating_sub(1);
3040 self.insights_state.insights.results_selected =
3041 (self.insights_state.insights.results_selected + 1).min(max);
3042 } else if self.view_mode == ViewMode::Detail {
3043 let filtered_streams = self.filtered_log_streams();
3044 let max = filtered_streams.len().saturating_sub(1);
3045 self.log_groups_state.selected_stream =
3046 (self.log_groups_state.selected_stream + 1).min(max);
3047 } else if self.view_mode == ViewMode::List
3048 && self.current_service == Service::CloudWatchLogGroups
3049 {
3050 let filtered_groups = self.filtered_log_groups();
3051 self.log_groups_state
3052 .log_groups
3053 .next_item(filtered_groups.len());
3054 } else if self.current_service == Service::EcrRepositories {
3055 if self.ecr_state.current_repository.is_some() {
3056 let filtered_images = self.filtered_ecr_images();
3057 self.ecr_state.images.page_down(filtered_images.len());
3058 } else {
3059 let filtered_repos = self.filtered_ecr_repositories();
3060 self.ecr_state.repositories.page_down(filtered_repos.len());
3061 }
3062 }
3063 }
3064
3065 Action::Refresh => {
3066 if self.mode == Mode::ProfilePicker {
3067 self.log_groups_state.loading = true;
3068 self.log_groups_state.loading_message = "Refreshing...".to_string();
3069 } else if self.mode == Mode::RegionPicker {
3070 self.measure_region_latencies();
3071 } else if self.mode == Mode::SessionPicker {
3072 self.sessions = Session::list_all().unwrap_or_default();
3073 } else if self.current_service == Service::CloudWatchInsights
3074 && !self.insights_state.insights.selected_log_groups.is_empty()
3075 {
3076 self.log_groups_state.loading = true;
3077 self.insights_state.insights.query_completed = true;
3078 } else if self.current_service == Service::LambdaFunctions {
3079 self.lambda_state.table.loading = true;
3080 } else if self.current_service == Service::LambdaApplications {
3081 self.lambda_application_state.table.loading = true;
3082 } else if matches!(
3083 self.view_mode,
3084 ViewMode::Events | ViewMode::Detail | ViewMode::List
3085 ) {
3086 self.log_groups_state.loading = true;
3087 }
3088 }
3089 Action::Yank => {
3090 if self.mode == Mode::ErrorModal {
3091 if let Some(error) = &self.error_message {
3093 copy_to_clipboard(error);
3094 }
3095 } else if self.view_mode == ViewMode::Events {
3096 if let Some(event) = self
3097 .log_groups_state
3098 .log_events
3099 .get(self.log_groups_state.event_scroll_offset)
3100 {
3101 copy_to_clipboard(&event.message);
3102 }
3103 } else if self.current_service == Service::EcrRepositories {
3104 if self.ecr_state.current_repository.is_some() {
3105 let filtered_images = self.filtered_ecr_images();
3106 if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
3107 copy_to_clipboard(&image.uri);
3108 }
3109 } else {
3110 let filtered_repos = self.filtered_ecr_repositories();
3111 if let Some(repo) =
3112 self.ecr_state.repositories.get_selected(&filtered_repos)
3113 {
3114 copy_to_clipboard(&repo.uri);
3115 }
3116 }
3117 } else if self.current_service == Service::LambdaFunctions {
3118 let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
3119 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
3120 copy_to_clipboard(&func.arn);
3121 }
3122 } else if self.current_service == Service::CloudFormationStacks {
3123 if let Some(stack_name) = &self.cfn_state.current_stack {
3124 if let Some(stack) = self
3126 .cfn_state
3127 .table
3128 .items
3129 .iter()
3130 .find(|s| &s.name == stack_name)
3131 {
3132 copy_to_clipboard(&stack.stack_id);
3133 }
3134 } else {
3135 let filtered_stacks = self.filtered_cloudformation_stacks();
3137 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
3138 copy_to_clipboard(&stack.stack_id);
3139 }
3140 }
3141 } else if self.current_service == Service::IamUsers {
3142 if self.iam_state.current_user.is_some() {
3143 if let Some(user_name) = &self.iam_state.current_user {
3144 if let Some(user) = self
3145 .iam_state
3146 .users
3147 .items
3148 .iter()
3149 .find(|u| u.user_name == *user_name)
3150 {
3151 copy_to_clipboard(&user.arn);
3152 }
3153 }
3154 } else {
3155 let filtered_users = crate::ui::iam::filtered_iam_users(self);
3156 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
3157 copy_to_clipboard(&user.arn);
3158 }
3159 }
3160 } else if self.current_service == Service::IamRoles {
3161 if self.iam_state.current_role.is_some() {
3162 if let Some(role_name) = &self.iam_state.current_role {
3163 if let Some(role) = self
3164 .iam_state
3165 .roles
3166 .items
3167 .iter()
3168 .find(|r| r.role_name == *role_name)
3169 {
3170 copy_to_clipboard(&role.arn);
3171 }
3172 }
3173 } else {
3174 let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
3175 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
3176 copy_to_clipboard(&role.arn);
3177 }
3178 }
3179 } else if self.current_service == Service::IamUserGroups {
3180 if self.iam_state.current_group.is_some() {
3181 if let Some(group_name) = &self.iam_state.current_group {
3182 let arn = iam::format_arn(&self.config.account_id, "group", group_name);
3183 copy_to_clipboard(&arn);
3184 }
3185 } else {
3186 let filtered_groups: Vec<_> = self
3187 .iam_state
3188 .groups
3189 .items
3190 .iter()
3191 .filter(|g| {
3192 if self.iam_state.groups.filter.is_empty() {
3193 true
3194 } else {
3195 g.group_name
3196 .to_lowercase()
3197 .contains(&self.iam_state.groups.filter.to_lowercase())
3198 }
3199 })
3200 .collect();
3201 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
3202 let arn = iam::format_arn(
3203 &self.config.account_id,
3204 "group",
3205 &group.group_name,
3206 );
3207 copy_to_clipboard(&arn);
3208 }
3209 }
3210 } else if self.current_service == Service::SqsQueues {
3211 if self.sqs_state.current_queue.is_some() {
3212 if let Some(queue) = self
3214 .sqs_state
3215 .queues
3216 .items
3217 .iter()
3218 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
3219 {
3220 let arn = format!(
3221 "arn:aws:sqs:{}:{}:{}",
3222 crate::ui::sqs::extract_region(&queue.url),
3223 crate::ui::sqs::extract_account_id(&queue.url),
3224 queue.name
3225 );
3226 copy_to_clipboard(&arn);
3227 }
3228 } else {
3229 let filtered_queues = crate::ui::sqs::filtered_queues(
3231 &self.sqs_state.queues.items,
3232 &self.sqs_state.queues.filter,
3233 );
3234 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
3235 let arn = format!(
3236 "arn:aws:sqs:{}:{}:{}",
3237 crate::ui::sqs::extract_region(&queue.url),
3238 crate::ui::sqs::extract_account_id(&queue.url),
3239 queue.name
3240 );
3241 copy_to_clipboard(&arn);
3242 }
3243 }
3244 }
3245 }
3246 Action::CopyToClipboard => {
3247 self.snapshot_requested = true;
3249 }
3250 Action::RetryLoad => {
3251 self.error_message = None;
3252 self.mode = Mode::Normal;
3253 self.log_groups_state.loading = true;
3254 }
3255 Action::ApplyFilter => {
3256 if self.mode == Mode::FilterInput
3257 && self.current_service == Service::SqsQueues
3258 && self.sqs_state.input_focus
3259 == crate::common::InputFocus::Dropdown("SubscriptionRegion")
3260 {
3261 let regions = crate::aws::Region::all();
3262 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
3263 self.sqs_state.subscription_region_filter = region.code.to_string();
3264 }
3265 self.mode = Mode::Normal;
3266 } else if self.mode == Mode::InsightsInput {
3267 use crate::app::InsightsFocus;
3268 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3269 && self.insights_state.insights.show_dropdown
3270 {
3271 self.insights_state.insights.show_dropdown = false;
3273 self.mode = Mode::Normal;
3274 if !self.insights_state.insights.selected_log_groups.is_empty() {
3275 self.log_groups_state.loading = true;
3276 self.insights_state.insights.query_completed = true;
3277 }
3278 }
3279 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
3280 if let Ok(page) = self.page_input.parse::<usize>() {
3281 self.go_to_page(page);
3282 }
3283 self.page_input.clear();
3284 } else {
3285 self.mode = Mode::Normal;
3286 self.log_groups_state.filter_mode = false;
3287 }
3288 }
3289 Action::ToggleExactMatch => {
3290 if self.view_mode == ViewMode::Detail
3291 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3292 {
3293 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
3294 }
3295 }
3296 Action::ToggleShowExpired => {
3297 if self.view_mode == ViewMode::Detail
3298 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3299 {
3300 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
3301 }
3302 }
3303 Action::GoBack => {
3304 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
3306 self.mode = Mode::Normal;
3307 self.service_picker.filter.clear();
3308 }
3309 else if self.current_service == Service::S3Buckets
3311 && self.s3_state.current_bucket.is_some()
3312 {
3313 if !self.s3_state.prefix_stack.is_empty() {
3314 self.s3_state.prefix_stack.pop();
3315 self.s3_state.buckets.loading = true;
3316 } else {
3317 self.s3_state.current_bucket = None;
3318 self.s3_state.objects.clear();
3319 }
3320 }
3321 else if self.current_service == Service::EcrRepositories
3323 && self.ecr_state.current_repository.is_some()
3324 {
3325 if self.ecr_state.images.has_expanded_item() {
3326 self.ecr_state.images.collapse();
3327 } else {
3328 self.ecr_state.current_repository = None;
3329 self.ecr_state.current_repository_uri = None;
3330 self.ecr_state.images.items.clear();
3331 self.ecr_state.images.reset();
3332 }
3333 }
3334 else if self.current_service == Service::SqsQueues
3336 && self.sqs_state.current_queue.is_some()
3337 {
3338 self.sqs_state.current_queue = None;
3339 }
3340 else if self.current_service == Service::IamUsers
3342 && self.iam_state.current_user.is_some()
3343 {
3344 self.iam_state.current_user = None;
3345 self.iam_state.policies.items.clear();
3346 self.iam_state.policies.reset();
3347 self.update_current_tab_breadcrumb();
3348 }
3349 else if self.current_service == Service::IamUserGroups
3351 && self.iam_state.current_group.is_some()
3352 {
3353 self.iam_state.current_group = None;
3354 self.update_current_tab_breadcrumb();
3355 }
3356 else if self.current_service == Service::IamRoles {
3358 if self.view_mode == ViewMode::PolicyView {
3359 self.view_mode = ViewMode::Detail;
3361 self.iam_state.current_policy = None;
3362 self.iam_state.policy_document.clear();
3363 self.iam_state.policy_scroll = 0;
3364 self.update_current_tab_breadcrumb();
3365 } else if self.iam_state.current_role.is_some() {
3366 self.iam_state.current_role = None;
3367 self.iam_state.policies.items.clear();
3368 self.iam_state.policies.reset();
3369 self.update_current_tab_breadcrumb();
3370 }
3371 }
3372 else if self.current_service == Service::LambdaFunctions
3374 && self.lambda_state.current_version.is_some()
3375 {
3376 self.lambda_state.current_version = None;
3377 self.lambda_state.detail_tab = LambdaDetailTab::Versions;
3378 }
3379 else if self.current_service == Service::LambdaFunctions
3381 && self.lambda_state.current_alias.is_some()
3382 {
3383 self.lambda_state.current_alias = None;
3384 self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
3385 }
3386 else if self.current_service == Service::LambdaFunctions
3388 && self.lambda_state.current_function.is_some()
3389 {
3390 self.lambda_state.current_function = None;
3391 self.update_current_tab_breadcrumb();
3392 }
3393 else if self.current_service == Service::LambdaApplications
3395 && self.lambda_application_state.current_application.is_some()
3396 {
3397 self.lambda_application_state.current_application = None;
3398 self.update_current_tab_breadcrumb();
3399 }
3400 else if self.current_service == Service::CloudFormationStacks
3402 && self.cfn_state.current_stack.is_some()
3403 {
3404 self.cfn_state.current_stack = None;
3405 self.update_current_tab_breadcrumb();
3406 }
3407 else if self.view_mode == ViewMode::InsightsResults {
3409 if self.insights_state.insights.expanded_result.is_some() {
3410 self.insights_state.insights.expanded_result = None;
3411 }
3412 }
3413 else if self.current_service == Service::CloudWatchAlarms {
3415 if self.alarms_state.table.has_expanded_item() {
3416 self.alarms_state.table.collapse();
3417 }
3418 }
3419 else if self.current_service == Service::Ec2Instances {
3421 if self.ec2_state.table.has_expanded_item() {
3422 self.ec2_state.table.collapse();
3423 }
3424 }
3425 else if self.view_mode == ViewMode::Events {
3427 if self.log_groups_state.expanded_event.is_some() {
3428 self.log_groups_state.expanded_event = None;
3429 } else {
3430 self.view_mode = ViewMode::Detail;
3431 self.log_groups_state.event_filter.clear();
3432 }
3433 }
3434 else if self.view_mode == ViewMode::Detail {
3436 self.view_mode = ViewMode::List;
3437 self.log_groups_state.stream_filter.clear();
3438 self.log_groups_state.exact_match = false;
3439 self.log_groups_state.show_expired = false;
3440 }
3441 }
3442 Action::OpenInConsole | Action::OpenInBrowser => {
3443 let url = self.get_console_url();
3444 let _ = webbrowser::open(&url);
3445 }
3446 Action::ShowHelp => {
3447 self.mode = Mode::HelpModal;
3448 }
3449 Action::OpenRegionPicker => {
3450 self.region_filter.clear();
3451 self.region_picker_selected = 0;
3452 self.measure_region_latencies();
3453 self.mode = Mode::RegionPicker;
3454 }
3455 Action::OpenProfilePicker => {
3456 self.profile_filter.clear();
3457 self.profile_picker_selected = 0;
3458 self.available_profiles = Self::load_aws_profiles();
3459 self.mode = Mode::ProfilePicker;
3460 }
3461 Action::OpenCalendar => {
3462 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
3463 self.calendar_selecting = CalendarField::StartDate;
3464 self.mode = Mode::CalendarPicker;
3465 }
3466 Action::CloseCalendar => {
3467 self.mode = Mode::Normal;
3468 self.calendar_date = None;
3469 }
3470 Action::CalendarPrevDay => {
3471 if let Some(date) = self.calendar_date {
3472 self.calendar_date = date.checked_sub(time::Duration::days(1));
3473 }
3474 }
3475 Action::CalendarNextDay => {
3476 if let Some(date) = self.calendar_date {
3477 self.calendar_date = date.checked_add(time::Duration::days(1));
3478 }
3479 }
3480 Action::CalendarPrevWeek => {
3481 if let Some(date) = self.calendar_date {
3482 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
3483 }
3484 }
3485 Action::CalendarNextWeek => {
3486 if let Some(date) = self.calendar_date {
3487 self.calendar_date = date.checked_add(time::Duration::weeks(1));
3488 }
3489 }
3490 Action::CalendarPrevMonth => {
3491 if let Some(date) = self.calendar_date {
3492 self.calendar_date = Some(if date.month() == time::Month::January {
3493 date.replace_month(time::Month::December)
3494 .unwrap()
3495 .replace_year(date.year() - 1)
3496 .unwrap()
3497 } else {
3498 date.replace_month(date.month().previous()).unwrap()
3499 });
3500 }
3501 }
3502 Action::CalendarNextMonth => {
3503 if let Some(date) = self.calendar_date {
3504 self.calendar_date = Some(if date.month() == time::Month::December {
3505 date.replace_month(time::Month::January)
3506 .unwrap()
3507 .replace_year(date.year() + 1)
3508 .unwrap()
3509 } else {
3510 date.replace_month(date.month().next()).unwrap()
3511 });
3512 }
3513 }
3514 Action::CalendarSelect => {
3515 if let Some(date) = self.calendar_date {
3516 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
3517 .unix_timestamp()
3518 * 1000;
3519 match self.calendar_selecting {
3520 CalendarField::StartDate => {
3521 self.log_groups_state.start_time = Some(timestamp);
3522 self.calendar_selecting = CalendarField::EndDate;
3523 }
3524 CalendarField::EndDate => {
3525 self.log_groups_state.end_time = Some(timestamp);
3526 self.mode = Mode::Normal;
3527 self.calendar_date = None;
3528 }
3529 }
3530 }
3531 }
3532 }
3533 }
3534
3535 pub fn filtered_services(&self) -> Vec<&'static str> {
3536 let mut services = if self.service_picker.filter.is_empty() {
3537 self.service_picker.services.clone()
3538 } else {
3539 self.service_picker
3540 .services
3541 .iter()
3542 .filter(|s| {
3543 s.to_lowercase()
3544 .contains(&self.service_picker.filter.to_lowercase())
3545 })
3546 .copied()
3547 .collect()
3548 };
3549 services.sort();
3550 services
3551 }
3552
3553 pub fn selected_log_group(&self) -> Option<&LogGroup> {
3554 crate::ui::cw::logs::selected_log_group(self)
3555 }
3556
3557 pub fn filtered_log_streams(&self) -> Vec<&LogStream> {
3558 crate::ui::cw::logs::filtered_log_streams(self)
3559 }
3560
3561 pub fn filtered_log_events(&self) -> Vec<&LogEvent> {
3562 crate::ui::cw::logs::filtered_log_events(self)
3563 }
3564
3565 pub fn filtered_log_groups(&self) -> Vec<&LogGroup> {
3566 crate::ui::cw::logs::filtered_log_groups(self)
3567 }
3568
3569 pub fn filtered_ecr_repositories(&self) -> Vec<&EcrRepository> {
3570 crate::ui::ecr::filtered_ecr_repositories(self)
3571 }
3572
3573 pub fn filtered_ecr_images(&self) -> Vec<&EcrImage> {
3574 crate::ui::ecr::filtered_ecr_images(self)
3575 }
3576
3577 pub fn filtered_cloudformation_stacks(&self) -> Vec<&CfnStack> {
3578 crate::ui::cfn::filtered_cloudformation_stacks(self)
3579 }
3580
3581 pub fn breadcrumbs(&self) -> String {
3582 if !self.service_selected {
3583 return String::new();
3584 }
3585
3586 let mut parts = vec![];
3587
3588 match self.current_service {
3589 Service::CloudWatchLogGroups => {
3590 parts.push("CloudWatch".to_string());
3591 parts.push("Log groups".to_string());
3592
3593 if self.view_mode != ViewMode::List {
3594 if let Some(group) = self.selected_log_group() {
3595 parts.push(group.name.clone());
3596 }
3597 }
3598
3599 if self.view_mode == ViewMode::Events {
3600 if let Some(stream) = self
3601 .log_groups_state
3602 .log_streams
3603 .get(self.log_groups_state.selected_stream)
3604 {
3605 parts.push(stream.name.clone());
3606 }
3607 }
3608 }
3609 Service::CloudWatchInsights => {
3610 parts.push("CloudWatch".to_string());
3611 parts.push("Insights".to_string());
3612 }
3613 Service::CloudWatchAlarms => {
3614 parts.push("CloudWatch".to_string());
3615 parts.push("Alarms".to_string());
3616 }
3617 Service::S3Buckets => {
3618 parts.push("S3".to_string());
3619 if let Some(bucket) = &self.s3_state.current_bucket {
3620 parts.push(bucket.clone());
3621 if let Some(prefix) = self.s3_state.prefix_stack.last() {
3622 parts.push(prefix.trim_end_matches('/').to_string());
3623 }
3624 } else {
3625 parts.push("Buckets".to_string());
3626 }
3627 }
3628 Service::SqsQueues => {
3629 parts.push("SQS".to_string());
3630 parts.push("Queues".to_string());
3631 }
3632 Service::EcrRepositories => {
3633 parts.push("ECR".to_string());
3634 if let Some(repo) = &self.ecr_state.current_repository {
3635 parts.push(repo.clone());
3636 } else {
3637 parts.push("Repositories".to_string());
3638 }
3639 }
3640 Service::LambdaFunctions => {
3641 parts.push("Lambda".to_string());
3642 if let Some(func) = &self.lambda_state.current_function {
3643 parts.push(func.clone());
3644 } else {
3645 parts.push("Functions".to_string());
3646 }
3647 }
3648 Service::LambdaApplications => {
3649 parts.push("Lambda".to_string());
3650 parts.push("Applications".to_string());
3651 }
3652 Service::CloudFormationStacks => {
3653 parts.push("CloudFormation".to_string());
3654 if let Some(stack_name) = &self.cfn_state.current_stack {
3655 parts.push(stack_name.clone());
3656 } else {
3657 parts.push("Stacks".to_string());
3658 }
3659 }
3660 Service::IamUsers => {
3661 parts.push("IAM".to_string());
3662 parts.push("Users".to_string());
3663 }
3664 Service::IamRoles => {
3665 parts.push("IAM".to_string());
3666 parts.push("Roles".to_string());
3667 if let Some(role_name) = &self.iam_state.current_role {
3668 parts.push(role_name.clone());
3669 if let Some(policy_name) = &self.iam_state.current_policy {
3670 parts.push(policy_name.clone());
3671 }
3672 }
3673 }
3674 Service::IamUserGroups => {
3675 parts.push("IAM".to_string());
3676 parts.push("User Groups".to_string());
3677 if let Some(group_name) = &self.iam_state.current_group {
3678 parts.push(group_name.clone());
3679 }
3680 }
3681 Service::Ec2Instances => {
3682 parts.push("EC2".to_string());
3683 parts.push("Instances".to_string());
3684 }
3685 }
3686
3687 parts.join(" > ")
3688 }
3689
3690 pub fn update_current_tab_breadcrumb(&mut self) {
3691 if !self.tabs.is_empty() {
3692 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
3693 }
3694 }
3695
3696 pub fn get_console_url(&self) -> String {
3697 use crate::{cfn, cw, ecr, iam, lambda, s3};
3698
3699 match self.current_service {
3700 Service::CloudWatchLogGroups => {
3701 if self.view_mode == ViewMode::Events {
3702 if let Some(group) = self.selected_log_group() {
3703 if let Some(stream) = self
3704 .log_groups_state
3705 .log_streams
3706 .get(self.log_groups_state.selected_stream)
3707 {
3708 return cw::logs::console_url_stream(
3709 &self.config.region,
3710 &group.name,
3711 &stream.name,
3712 );
3713 }
3714 }
3715 } else if self.view_mode == ViewMode::Detail {
3716 if let Some(group) = self.selected_log_group() {
3717 return cw::logs::console_url_detail(&self.config.region, &group.name);
3718 }
3719 }
3720 cw::logs::console_url_list(&self.config.region)
3721 }
3722 Service::CloudWatchInsights => cw::insights::console_url(
3723 &self.config.region,
3724 &self.config.account_id,
3725 &self.insights_state.insights.query_text,
3726 &self.insights_state.insights.selected_log_groups,
3727 ),
3728 Service::CloudWatchAlarms => {
3729 let view_type = match self.alarms_state.view_as {
3730 AlarmViewMode::Table | AlarmViewMode::Detail => "table",
3731 AlarmViewMode::Cards => "card",
3732 };
3733 cw::alarms::console_url(
3734 &self.config.region,
3735 view_type,
3736 self.alarms_state.table.page_size.value(),
3737 &self.alarms_state.sort_column,
3738 self.alarms_state.sort_direction.as_str(),
3739 )
3740 }
3741 Service::S3Buckets => {
3742 if let Some(bucket_name) = &self.s3_state.current_bucket {
3743 let prefix = self.s3_state.prefix_stack.join("");
3744 s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
3745 } else {
3746 s3::console_url_buckets(&self.config.region)
3747 }
3748 }
3749 Service::SqsQueues => {
3750 if let Some(queue_url) = &self.sqs_state.current_queue {
3751 crate::sqs::console_url_queue_detail(&self.config.region, queue_url)
3752 } else {
3753 crate::sqs::console_url_queues(&self.config.region)
3754 }
3755 }
3756 Service::EcrRepositories => {
3757 if let Some(repo_name) = &self.ecr_state.current_repository {
3758 ecr::console_url_private_repository(
3759 &self.config.region,
3760 &self.config.account_id,
3761 repo_name,
3762 )
3763 } else {
3764 ecr::console_url_repositories(&self.config.region)
3765 }
3766 }
3767 Service::LambdaFunctions => {
3768 if let Some(func_name) = &self.lambda_state.current_function {
3769 if let Some(version) = &self.lambda_state.current_version {
3770 lambda::console_url_function_version(
3771 &self.config.region,
3772 func_name,
3773 version,
3774 &self.lambda_state.detail_tab,
3775 )
3776 } else {
3777 lambda::console_url_function_detail(&self.config.region, func_name)
3778 }
3779 } else {
3780 lambda::console_url_functions(&self.config.region)
3781 }
3782 }
3783 Service::LambdaApplications => {
3784 if let Some(app_name) = &self.lambda_application_state.current_application {
3785 lambda::console_url_application_detail(
3786 &self.config.region,
3787 app_name,
3788 &self.lambda_application_state.detail_tab,
3789 )
3790 } else {
3791 lambda::console_url_applications(&self.config.region)
3792 }
3793 }
3794 Service::CloudFormationStacks => {
3795 if let Some(stack_name) = &self.cfn_state.current_stack {
3796 if let Some(stack) = self
3797 .cfn_state
3798 .table
3799 .items
3800 .iter()
3801 .find(|s| &s.name == stack_name)
3802 {
3803 return cfn::console_url_stack_detail_with_tab(
3804 &self.config.region,
3805 &stack.stack_id,
3806 &self.cfn_state.detail_tab,
3807 );
3808 }
3809 }
3810 cfn::console_url_stacks(&self.config.region)
3811 }
3812 Service::IamUsers => {
3813 if let Some(user_name) = &self.iam_state.current_user {
3814 let section = match self.iam_state.user_tab {
3815 UserTab::Permissions => "permissions",
3816 UserTab::Groups => "groups",
3817 UserTab::Tags => "tags",
3818 UserTab::SecurityCredentials => "security_credentials",
3819 UserTab::LastAccessed => "access_advisor",
3820 };
3821 iam::console_url_user_detail(&self.config.region, user_name, section)
3822 } else {
3823 iam::console_url_users(&self.config.region)
3824 }
3825 }
3826 Service::IamRoles => {
3827 if let Some(policy_name) = &self.iam_state.current_policy {
3828 if let Some(role_name) = &self.iam_state.current_role {
3829 return iam::console_url_role_policy(
3830 &self.config.region,
3831 role_name,
3832 policy_name,
3833 );
3834 }
3835 }
3836 if let Some(role_name) = &self.iam_state.current_role {
3837 let section = match self.iam_state.role_tab {
3838 RoleTab::Permissions => "permissions",
3839 RoleTab::TrustRelationships => "trust_relationships",
3840 RoleTab::Tags => "tags",
3841 RoleTab::LastAccessed => "access_advisor",
3842 RoleTab::RevokeSessions => "revoke_sessions",
3843 };
3844 iam::console_url_role_detail(&self.config.region, role_name, section)
3845 } else {
3846 iam::console_url_roles(&self.config.region)
3847 }
3848 }
3849 Service::IamUserGroups => iam::console_url_groups(&self.config.region),
3850 Service::Ec2Instances => {
3851 format!(
3852 "https://{}.console.aws.amazon.com/ec2/home?region={}#Instances:",
3853 self.config.region, self.config.region
3854 )
3855 }
3856 }
3857 }
3858
3859 fn calculate_total_bucket_rows(&self) -> usize {
3860 crate::ui::s3::calculate_total_bucket_rows(self)
3861 }
3862
3863 fn calculate_total_object_rows(&self) -> usize {
3864 crate::ui::s3::calculate_total_object_rows(self)
3865 }
3866
3867 fn get_column_selector_max(&self) -> usize {
3868 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none() {
3869 self.s3_bucket_column_ids.len() - 1
3870 } else if self.view_mode == ViewMode::Events {
3871 self.cw_log_event_column_ids.len() - 1
3872 } else if self.view_mode == ViewMode::Detail {
3873 self.cw_log_stream_column_ids.len() - 1
3874 } else if self.current_service == Service::CloudWatchAlarms {
3875 29
3876 } else if self.current_service == Service::Ec2Instances {
3877 self.ec2_column_ids.len() + 6
3878 } else if self.current_service == Service::EcrRepositories {
3879 if self.ecr_state.current_repository.is_some() {
3880 self.ecr_image_column_ids.len() + 6
3881 } else {
3882 self.ecr_repo_column_ids.len() - 1
3883 }
3884 } else if self.current_service == Service::SqsQueues {
3885 self.sqs_column_ids.len() - 1
3886 } else if self.current_service == Service::LambdaFunctions {
3887 self.lambda_state.function_column_ids.len() + 6
3888 } else if self.current_service == Service::LambdaApplications {
3889 self.lambda_application_column_ids.len() + 5
3890 } else if self.current_service == Service::CloudFormationStacks {
3891 self.cfn_column_ids.len() + 6
3892 } else if self.current_service == Service::IamUsers {
3893 if self.iam_state.current_user.is_some() {
3894 self.iam_policy_column_ids.len() + 5
3895 } else {
3896 self.iam_user_column_ids.len() + 5
3897 }
3898 } else if self.current_service == Service::IamRoles {
3899 if self.iam_state.current_role.is_some() {
3900 self.iam_policy_column_ids.len() + 5
3901 } else {
3902 self.iam_role_column_ids.len() + 5
3903 }
3904 } else {
3905 self.cw_log_group_column_ids.len() - 1
3906 }
3907 }
3908
3909 fn next_item(&mut self) {
3910 match self.mode {
3911 Mode::FilterInput => {
3912 if self.current_service == Service::CloudFormationStacks {
3913 use crate::ui::cfn::STATUS_FILTER;
3914 if self.cfn_state.input_focus == STATUS_FILTER {
3915 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3916 self.cfn_state.table.reset();
3917 }
3918 } else if self.current_service == Service::Ec2Instances {
3919 if self.ec2_state.input_focus == EC2_STATE_FILTER {
3920 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
3921 self.ec2_state.table.reset();
3922 }
3923 } else if self.current_service == Service::SqsQueues {
3924 use crate::ui::sqs::SUBSCRIPTION_REGION;
3925 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3926 let regions = crate::aws::Region::all();
3927 self.sqs_state.subscription_region_selected =
3928 (self.sqs_state.subscription_region_selected + 1)
3929 .min(regions.len() - 1);
3930 self.sqs_state.subscriptions.reset();
3931 }
3932 }
3933 }
3934 Mode::RegionPicker => {
3935 let filtered = self.get_filtered_regions();
3936 if !filtered.is_empty() {
3937 self.region_picker_selected =
3938 (self.region_picker_selected + 1).min(filtered.len() - 1);
3939 }
3940 }
3941 Mode::ProfilePicker => {
3942 let filtered = self.get_filtered_profiles();
3943 if !filtered.is_empty() {
3944 self.profile_picker_selected =
3945 (self.profile_picker_selected + 1).min(filtered.len() - 1);
3946 }
3947 }
3948 Mode::SessionPicker => {
3949 let filtered = self.get_filtered_sessions();
3950 if !filtered.is_empty() {
3951 self.session_picker_selected =
3952 (self.session_picker_selected + 1).min(filtered.len() - 1);
3953 }
3954 }
3955 Mode::InsightsInput => {
3956 use crate::app::InsightsFocus;
3957 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3958 && self.insights_state.insights.show_dropdown
3959 && !self.insights_state.insights.log_group_matches.is_empty()
3960 {
3961 let max = self.insights_state.insights.log_group_matches.len() - 1;
3962 self.insights_state.insights.dropdown_selected =
3963 (self.insights_state.insights.dropdown_selected + 1).min(max);
3964 }
3965 }
3966 Mode::ColumnSelector => {
3967 let max = self.get_column_selector_max();
3968 self.column_selector_index = (self.column_selector_index + 1).min(max);
3969 }
3970 Mode::ServicePicker => {
3971 let filtered = self.filtered_services();
3972 if !filtered.is_empty() {
3973 self.service_picker.selected =
3974 (self.service_picker.selected + 1).min(filtered.len() - 1);
3975 }
3976 }
3977 Mode::TabPicker => {
3978 let filtered = self.get_filtered_tabs();
3979 if !filtered.is_empty() {
3980 self.tab_picker_selected =
3981 (self.tab_picker_selected + 1).min(filtered.len() - 1);
3982 }
3983 }
3984 Mode::Normal => {
3985 if !self.service_selected {
3986 let filtered = self.filtered_services();
3987 if !filtered.is_empty() {
3988 self.service_picker.selected =
3989 (self.service_picker.selected + 1).min(filtered.len() - 1);
3990 }
3991 } else if self.current_service == Service::S3Buckets {
3992 if self.s3_state.current_bucket.is_some() {
3993 if self.s3_state.object_tab == S3ObjectTab::Properties {
3994 self.s3_state.properties_scroll =
3996 self.s3_state.properties_scroll.saturating_add(1);
3997 } else {
3998 let total_rows = self.calculate_total_object_rows();
4000 let max = total_rows.saturating_sub(1);
4001 self.s3_state.selected_object =
4002 (self.s3_state.selected_object + 1).min(max);
4003
4004 let visible_rows = self.s3_state.object_visible_rows.get();
4006 if self.s3_state.selected_object
4007 >= self.s3_state.object_scroll_offset + visible_rows
4008 {
4009 self.s3_state.object_scroll_offset =
4010 self.s3_state.selected_object - visible_rows + 1;
4011 }
4012 }
4013 } else {
4014 let total_rows = self.calculate_total_bucket_rows();
4016 if total_rows > 0 {
4017 self.s3_state.selected_row =
4018 (self.s3_state.selected_row + 1).min(total_rows - 1);
4019
4020 let visible_rows = self.s3_state.bucket_visible_rows.get();
4022 if self.s3_state.selected_row
4023 >= self.s3_state.bucket_scroll_offset + visible_rows
4024 {
4025 self.s3_state.bucket_scroll_offset =
4026 self.s3_state.selected_row - visible_rows + 1;
4027 }
4028 }
4029 }
4030 } else if self.view_mode == ViewMode::InsightsResults {
4031 let max = self
4032 .insights_state
4033 .insights
4034 .query_results
4035 .len()
4036 .saturating_sub(1);
4037 if self.insights_state.insights.results_selected < max {
4038 self.insights_state.insights.results_selected += 1;
4039 }
4040 } else if self.view_mode == ViewMode::PolicyView {
4041 let lines = self.iam_state.policy_document.lines().count();
4042 let max_scroll = lines.saturating_sub(1);
4043 self.iam_state.policy_scroll =
4044 (self.iam_state.policy_scroll + 1).min(max_scroll);
4045 } else if self.current_service == Service::CloudFormationStacks
4046 && self.cfn_state.current_stack.is_some()
4047 && self.cfn_state.detail_tab == CfnDetailTab::Template
4048 {
4049 let lines = self.cfn_state.template_body.lines().count();
4050 let max_scroll = lines.saturating_sub(1);
4051 self.cfn_state.template_scroll =
4052 (self.cfn_state.template_scroll + 1).min(max_scroll);
4053 } else if self.current_service == Service::SqsQueues
4054 && self.sqs_state.current_queue.is_some()
4055 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
4056 {
4057 let lines = self.sqs_state.policy_document.lines().count();
4058 let max_scroll = lines.saturating_sub(1);
4059 self.sqs_state.policy_scroll =
4060 (self.sqs_state.policy_scroll + 1).min(max_scroll);
4061 } else if self.current_service == Service::LambdaFunctions
4062 && self.lambda_state.current_function.is_some()
4063 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4064 && !self.lambda_state.is_metrics_loading()
4065 {
4066 self.lambda_state
4067 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
4068 } else if self.current_service == Service::SqsQueues
4069 && self.sqs_state.current_queue.is_some()
4070 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4071 && !self.sqs_state.is_metrics_loading()
4072 {
4073 self.sqs_state
4074 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
4075 } else if self.view_mode == ViewMode::Events {
4076 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
4077 if self.log_groups_state.event_scroll_offset >= max_scroll {
4078 } else {
4080 self.log_groups_state.event_scroll_offset =
4081 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
4082 }
4083 } else if self.current_service == Service::CloudWatchLogGroups {
4084 if self.view_mode == ViewMode::List {
4085 let filtered_groups = self.filtered_log_groups();
4086 self.log_groups_state
4087 .log_groups
4088 .next_item(filtered_groups.len());
4089 } else if self.view_mode == ViewMode::Detail {
4090 let filtered_streams = self.filtered_log_streams();
4091 if !filtered_streams.is_empty() {
4092 let max = filtered_streams.len() - 1;
4093 if self.log_groups_state.selected_stream >= max {
4094 } else {
4096 self.log_groups_state.selected_stream =
4097 (self.log_groups_state.selected_stream + 1).min(max);
4098 }
4099 }
4100 }
4101 } else if self.current_service == Service::CloudWatchAlarms {
4102 let filtered_alarms = match self.alarms_state.alarm_tab {
4103 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4104 AlarmTab::InAlarm => self
4105 .alarms_state
4106 .table
4107 .items
4108 .iter()
4109 .filter(|a| a.state.to_uppercase() == "ALARM")
4110 .count(),
4111 };
4112 if filtered_alarms > 0 {
4113 self.alarms_state.table.next_item(filtered_alarms);
4114 }
4115 } else if self.current_service == Service::Ec2Instances {
4116 let filtered: Vec<_> = self
4117 .ec2_state
4118 .table
4119 .items
4120 .iter()
4121 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
4122 .filter(|i| {
4123 if self.ec2_state.table.filter.is_empty() {
4124 return true;
4125 }
4126 i.name.contains(&self.ec2_state.table.filter)
4127 || i.instance_id.contains(&self.ec2_state.table.filter)
4128 || i.state.contains(&self.ec2_state.table.filter)
4129 || i.instance_type.contains(&self.ec2_state.table.filter)
4130 || i.availability_zone.contains(&self.ec2_state.table.filter)
4131 || i.security_groups.contains(&self.ec2_state.table.filter)
4132 || i.key_name.contains(&self.ec2_state.table.filter)
4133 })
4134 .collect();
4135 if !filtered.is_empty() {
4136 self.ec2_state.table.next_item(filtered.len());
4137 }
4138 } else if self.current_service == Service::EcrRepositories {
4139 if self.ecr_state.current_repository.is_some() {
4140 let filtered_images = self.filtered_ecr_images();
4141 if !filtered_images.is_empty() {
4142 self.ecr_state.images.next_item(filtered_images.len());
4143 }
4144 } else {
4145 let filtered_repos = self.filtered_ecr_repositories();
4146 if !filtered_repos.is_empty() {
4147 self.ecr_state.repositories.selected =
4148 (self.ecr_state.repositories.selected + 1)
4149 .min(filtered_repos.len() - 1);
4150 self.ecr_state.repositories.snap_to_page();
4151 }
4152 }
4153 } else if self.current_service == Service::SqsQueues {
4154 if self.sqs_state.current_queue.is_some()
4155 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4156 {
4157 let filtered = crate::ui::sqs::filtered_lambda_triggers(self);
4158 if !filtered.is_empty() {
4159 self.sqs_state.triggers.next_item(filtered.len());
4160 }
4161 } else if self.sqs_state.current_queue.is_some()
4162 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4163 {
4164 let filtered = crate::ui::sqs::filtered_eventbridge_pipes(self);
4165 if !filtered.is_empty() {
4166 self.sqs_state.pipes.next_item(filtered.len());
4167 }
4168 } else if self.sqs_state.current_queue.is_some()
4169 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4170 {
4171 let filtered = crate::ui::sqs::filtered_tags(self);
4172 if !filtered.is_empty() {
4173 self.sqs_state.tags.next_item(filtered.len());
4174 }
4175 } else if self.sqs_state.current_queue.is_some()
4176 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4177 {
4178 let filtered = crate::ui::sqs::filtered_subscriptions(self);
4179 if !filtered.is_empty() {
4180 self.sqs_state.subscriptions.next_item(filtered.len());
4181 }
4182 } else {
4183 let filtered_queues = crate::ui::sqs::filtered_queues(
4184 &self.sqs_state.queues.items,
4185 &self.sqs_state.queues.filter,
4186 );
4187 if !filtered_queues.is_empty() {
4188 self.sqs_state.queues.next_item(filtered_queues.len());
4189 }
4190 }
4191 } else if self.current_service == Service::LambdaFunctions {
4192 if self.lambda_state.current_function.is_some()
4193 && self.lambda_state.detail_tab == LambdaDetailTab::Code
4194 {
4195 if let Some(func_name) = &self.lambda_state.current_function {
4197 if let Some(func) = self
4198 .lambda_state
4199 .table
4200 .items
4201 .iter()
4202 .find(|f| f.name == *func_name)
4203 {
4204 let max = func.layers.len().saturating_sub(1);
4205 if !func.layers.is_empty() {
4206 self.lambda_state.layer_selected =
4207 (self.lambda_state.layer_selected + 1).min(max);
4208 }
4209 }
4210 }
4211 } else if self.lambda_state.current_function.is_some()
4212 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4213 {
4214 let filtered: Vec<_> = self
4216 .lambda_state
4217 .version_table
4218 .items
4219 .iter()
4220 .filter(|v| {
4221 self.lambda_state.version_table.filter.is_empty()
4222 || v.version.to_lowercase().contains(
4223 &self.lambda_state.version_table.filter.to_lowercase(),
4224 )
4225 || v.aliases.to_lowercase().contains(
4226 &self.lambda_state.version_table.filter.to_lowercase(),
4227 )
4228 || v.description.to_lowercase().contains(
4229 &self.lambda_state.version_table.filter.to_lowercase(),
4230 )
4231 })
4232 .collect();
4233 if !filtered.is_empty() {
4234 self.lambda_state.version_table.selected =
4235 (self.lambda_state.version_table.selected + 1)
4236 .min(filtered.len() - 1);
4237 self.lambda_state.version_table.snap_to_page();
4238 }
4239 } else if self.lambda_state.current_function.is_some()
4240 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4241 || (self.lambda_state.current_version.is_some()
4242 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4243 {
4244 let version_filter = self.lambda_state.current_version.clone();
4246 let filtered: Vec<_> = self
4247 .lambda_state
4248 .alias_table
4249 .items
4250 .iter()
4251 .filter(|a| {
4252 (version_filter.is_none()
4253 || a.versions.contains(version_filter.as_ref().unwrap()))
4254 && (self.lambda_state.alias_table.filter.is_empty()
4255 || a.name.to_lowercase().contains(
4256 &self.lambda_state.alias_table.filter.to_lowercase(),
4257 )
4258 || a.versions.to_lowercase().contains(
4259 &self.lambda_state.alias_table.filter.to_lowercase(),
4260 )
4261 || a.description.to_lowercase().contains(
4262 &self.lambda_state.alias_table.filter.to_lowercase(),
4263 ))
4264 })
4265 .collect();
4266 if !filtered.is_empty() {
4267 self.lambda_state.alias_table.selected =
4268 (self.lambda_state.alias_table.selected + 1)
4269 .min(filtered.len() - 1);
4270 self.lambda_state.alias_table.snap_to_page();
4271 }
4272 } else if self.lambda_state.current_function.is_none() {
4273 let filtered = crate::ui::lambda::filtered_lambda_functions(self);
4274 if !filtered.is_empty() {
4275 self.lambda_state.table.next_item(filtered.len());
4276 self.lambda_state.table.snap_to_page();
4277 }
4278 }
4279 } else if self.current_service == Service::LambdaApplications {
4280 if self.lambda_application_state.current_application.is_some() {
4281 if self.lambda_application_state.detail_tab
4282 == LambdaApplicationDetailTab::Overview
4283 {
4284 let len = self.lambda_application_state.resources.items.len();
4285 if len > 0 {
4286 self.lambda_application_state.resources.next_item(len);
4287 }
4288 } else {
4289 let len = self.lambda_application_state.deployments.items.len();
4290 if len > 0 {
4291 self.lambda_application_state.deployments.next_item(len);
4292 }
4293 }
4294 } else {
4295 let filtered = crate::ui::lambda::filtered_lambda_applications(self);
4296 if !filtered.is_empty() {
4297 self.lambda_application_state.table.selected =
4298 (self.lambda_application_state.table.selected + 1)
4299 .min(filtered.len() - 1);
4300 self.lambda_application_state.table.snap_to_page();
4301 }
4302 }
4303 } else if self.current_service == Service::CloudFormationStacks {
4304 if self.cfn_state.current_stack.is_some()
4305 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4306 {
4307 let filtered = filtered_parameters(self);
4308 self.cfn_state.parameters.next_item(filtered.len());
4309 } else if self.cfn_state.current_stack.is_some()
4310 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4311 {
4312 let filtered = filtered_outputs(self);
4313 self.cfn_state.outputs.next_item(filtered.len());
4314 } else if self.cfn_state.current_stack.is_some()
4315 && self.cfn_state.detail_tab == CfnDetailTab::Resources
4316 {
4317 let filtered = filtered_resources(self);
4318 self.cfn_state.resources.next_item(filtered.len());
4319 } else {
4320 let filtered = self.filtered_cloudformation_stacks();
4321 self.cfn_state.table.next_item(filtered.len());
4322 }
4323 } else if self.current_service == Service::IamUsers {
4324 if self.iam_state.current_user.is_some() {
4325 if self.iam_state.user_tab == UserTab::Tags {
4326 let filtered = crate::ui::iam::filtered_user_tags(self);
4327 if !filtered.is_empty() {
4328 self.iam_state.user_tags.next_item(filtered.len());
4329 }
4330 } else {
4331 let filtered = crate::ui::iam::filtered_iam_policies(self);
4332 if !filtered.is_empty() {
4333 self.iam_state.policies.next_item(filtered.len());
4334 }
4335 }
4336 } else {
4337 let filtered = crate::ui::iam::filtered_iam_users(self);
4338 if !filtered.is_empty() {
4339 self.iam_state.users.next_item(filtered.len());
4340 }
4341 }
4342 } else if self.current_service == Service::IamRoles {
4343 if self.iam_state.current_role.is_some() {
4344 if self.iam_state.role_tab == RoleTab::TrustRelationships {
4345 let lines = self.iam_state.trust_policy_document.lines().count();
4346 let max_scroll = lines.saturating_sub(1);
4347 self.iam_state.trust_policy_scroll =
4348 (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
4349 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4350 self.iam_state.revoke_sessions_scroll =
4351 (self.iam_state.revoke_sessions_scroll + 1).min(19);
4352 } else if self.iam_state.role_tab == RoleTab::Tags {
4353 let filtered = crate::ui::iam::filtered_tags(self);
4354 if !filtered.is_empty() {
4355 self.iam_state.tags.next_item(filtered.len());
4356 }
4357 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4358 let filtered = crate::ui::iam::filtered_last_accessed(self);
4359 if !filtered.is_empty() {
4360 self.iam_state
4361 .last_accessed_services
4362 .next_item(filtered.len());
4363 }
4364 } else {
4365 let filtered = crate::ui::iam::filtered_iam_policies(self);
4366 if !filtered.is_empty() {
4367 self.iam_state.policies.next_item(filtered.len());
4368 }
4369 }
4370 } else {
4371 let filtered = crate::ui::iam::filtered_iam_roles(self);
4372 if !filtered.is_empty() {
4373 self.iam_state.roles.next_item(filtered.len());
4374 }
4375 }
4376 } else if self.current_service == Service::IamUserGroups {
4377 if self.iam_state.current_group.is_some() {
4378 if self.iam_state.group_tab == GroupTab::Users {
4379 let filtered: Vec<_> = self
4380 .iam_state
4381 .group_users
4382 .items
4383 .iter()
4384 .filter(|u| {
4385 if self.iam_state.group_users.filter.is_empty() {
4386 true
4387 } else {
4388 u.user_name.to_lowercase().contains(
4389 &self.iam_state.group_users.filter.to_lowercase(),
4390 )
4391 }
4392 })
4393 .collect();
4394 if !filtered.is_empty() {
4395 self.iam_state.group_users.next_item(filtered.len());
4396 }
4397 } else if self.iam_state.group_tab == GroupTab::Permissions {
4398 let filtered = crate::ui::iam::filtered_iam_policies(self);
4399 if !filtered.is_empty() {
4400 self.iam_state.policies.next_item(filtered.len());
4401 }
4402 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4403 let filtered = crate::ui::iam::filtered_last_accessed(self);
4404 if !filtered.is_empty() {
4405 self.iam_state
4406 .last_accessed_services
4407 .next_item(filtered.len());
4408 }
4409 }
4410 } else {
4411 let filtered: Vec<_> = self
4412 .iam_state
4413 .groups
4414 .items
4415 .iter()
4416 .filter(|g| {
4417 if self.iam_state.groups.filter.is_empty() {
4418 true
4419 } else {
4420 g.group_name
4421 .to_lowercase()
4422 .contains(&self.iam_state.groups.filter.to_lowercase())
4423 }
4424 })
4425 .collect();
4426 if !filtered.is_empty() {
4427 self.iam_state.groups.next_item(filtered.len());
4428 }
4429 }
4430 }
4431 }
4432 _ => {}
4433 }
4434 }
4435
4436 fn prev_item(&mut self) {
4437 match self.mode {
4438 Mode::FilterInput => {
4439 if self.current_service == Service::CloudFormationStacks {
4440 use crate::ui::cfn::STATUS_FILTER;
4441 if self.cfn_state.input_focus == STATUS_FILTER {
4442 self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
4443 self.cfn_state.table.reset();
4444 }
4445 } else if self.current_service == Service::Ec2Instances {
4446 if self.ec2_state.input_focus == EC2_STATE_FILTER {
4447 self.ec2_state.state_filter = self.ec2_state.state_filter.prev();
4448 self.ec2_state.table.reset();
4449 }
4450 } else if self.current_service == Service::SqsQueues {
4451 use crate::ui::sqs::SUBSCRIPTION_REGION;
4452 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
4453 self.sqs_state.subscription_region_selected = self
4454 .sqs_state
4455 .subscription_region_selected
4456 .saturating_sub(1);
4457 self.sqs_state.subscriptions.reset();
4458 }
4459 }
4460 }
4461 Mode::RegionPicker => {
4462 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
4463 }
4464 Mode::ProfilePicker => {
4465 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
4466 }
4467 Mode::SessionPicker => {
4468 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
4469 }
4470 Mode::InsightsInput => {
4471 use crate::app::InsightsFocus;
4472 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4473 && self.insights_state.insights.show_dropdown
4474 && !self.insights_state.insights.log_group_matches.is_empty()
4475 {
4476 self.insights_state.insights.dropdown_selected = self
4477 .insights_state
4478 .insights
4479 .dropdown_selected
4480 .saturating_sub(1);
4481 }
4482 }
4483 Mode::ColumnSelector => {
4484 self.column_selector_index = self.column_selector_index.saturating_sub(1);
4485 }
4486 Mode::ServicePicker => {
4487 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
4488 }
4489 Mode::TabPicker => {
4490 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
4491 }
4492 Mode::Normal => {
4493 if !self.service_selected {
4494 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
4495 } else if self.current_service == Service::S3Buckets {
4496 if self.s3_state.current_bucket.is_some() {
4497 if self.s3_state.object_tab == S3ObjectTab::Properties {
4498 self.s3_state.properties_scroll =
4499 self.s3_state.properties_scroll.saturating_sub(1);
4500 } else {
4501 self.s3_state.selected_object =
4502 self.s3_state.selected_object.saturating_sub(1);
4503
4504 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4506 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4507 }
4508 }
4509 } else {
4510 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
4511
4512 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4514 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4515 }
4516 }
4517 } else if self.view_mode == ViewMode::InsightsResults {
4518 if self.insights_state.insights.results_selected > 0 {
4519 self.insights_state.insights.results_selected -= 1;
4520 }
4521 } else if self.view_mode == ViewMode::PolicyView {
4522 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
4523 } else if self.current_service == Service::CloudFormationStacks
4524 && self.cfn_state.current_stack.is_some()
4525 && self.cfn_state.detail_tab == CfnDetailTab::Template
4526 {
4527 self.cfn_state.template_scroll =
4528 self.cfn_state.template_scroll.saturating_sub(1);
4529 } else if self.current_service == Service::SqsQueues
4530 && self.sqs_state.current_queue.is_some()
4531 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
4532 {
4533 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(1);
4534 } else if self.current_service == Service::LambdaFunctions
4535 && self.lambda_state.current_function.is_some()
4536 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4537 && !self.lambda_state.is_metrics_loading()
4538 {
4539 self.lambda_state.set_monitoring_scroll(
4540 self.lambda_state.monitoring_scroll().saturating_sub(1),
4541 );
4542 } else if self.current_service == Service::SqsQueues
4543 && self.sqs_state.current_queue.is_some()
4544 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4545 && !self.sqs_state.is_metrics_loading()
4546 {
4547 self.sqs_state.set_monitoring_scroll(
4548 self.sqs_state.monitoring_scroll().saturating_sub(1),
4549 );
4550 } else if self.view_mode == ViewMode::Events {
4551 if self.log_groups_state.event_scroll_offset == 0 {
4552 if self.log_groups_state.has_older_events {
4553 self.log_groups_state.loading = true;
4554 }
4555 } else {
4557 self.log_groups_state.event_scroll_offset =
4558 self.log_groups_state.event_scroll_offset.saturating_sub(1);
4559 }
4560 } else if self.current_service == Service::CloudWatchLogGroups {
4561 if self.view_mode == ViewMode::List {
4562 self.log_groups_state.log_groups.prev_item();
4563 } else if self.view_mode == ViewMode::Detail
4564 && self.log_groups_state.selected_stream > 0
4565 {
4566 self.log_groups_state.selected_stream =
4567 self.log_groups_state.selected_stream.saturating_sub(1);
4568 self.log_groups_state.expanded_stream = None;
4569 }
4570 } else if self.current_service == Service::CloudWatchAlarms {
4571 self.alarms_state.table.prev_item();
4572 } else if self.current_service == Service::Ec2Instances {
4573 self.ec2_state.table.prev_item();
4574 } else if self.current_service == Service::EcrRepositories {
4575 if self.ecr_state.current_repository.is_some() {
4576 self.ecr_state.images.prev_item();
4577 } else {
4578 self.ecr_state.repositories.prev_item();
4579 }
4580 } else if self.current_service == Service::SqsQueues {
4581 if self.sqs_state.current_queue.is_some()
4582 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4583 {
4584 self.sqs_state.triggers.prev_item();
4585 } else if self.sqs_state.current_queue.is_some()
4586 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4587 {
4588 self.sqs_state.pipes.prev_item();
4589 } else if self.sqs_state.current_queue.is_some()
4590 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4591 {
4592 self.sqs_state.tags.prev_item();
4593 } else if self.sqs_state.current_queue.is_some()
4594 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4595 {
4596 self.sqs_state.subscriptions.prev_item();
4597 } else {
4598 self.sqs_state.queues.prev_item();
4599 }
4600 } else if self.current_service == Service::LambdaFunctions {
4601 if self.lambda_state.current_function.is_some()
4602 && self.lambda_state.detail_tab == LambdaDetailTab::Code
4603 {
4604 self.lambda_state.layer_selected =
4606 self.lambda_state.layer_selected.saturating_sub(1);
4607 } else if self.lambda_state.current_function.is_some()
4608 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4609 {
4610 self.lambda_state.version_table.prev_item();
4611 } else if self.lambda_state.current_function.is_some()
4612 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4613 || (self.lambda_state.current_version.is_some()
4614 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4615 {
4616 self.lambda_state.alias_table.prev_item();
4617 } else if self.lambda_state.current_function.is_none() {
4618 self.lambda_state.table.prev_item();
4619 }
4620 } else if self.current_service == Service::LambdaApplications {
4621 if self.lambda_application_state.current_application.is_some()
4622 && self.lambda_application_state.detail_tab
4623 == LambdaApplicationDetailTab::Overview
4624 {
4625 self.lambda_application_state.resources.selected = self
4626 .lambda_application_state
4627 .resources
4628 .selected
4629 .saturating_sub(1);
4630 } else if self.lambda_application_state.current_application.is_some()
4631 && self.lambda_application_state.detail_tab
4632 == LambdaApplicationDetailTab::Deployments
4633 {
4634 self.lambda_application_state.deployments.selected = self
4635 .lambda_application_state
4636 .deployments
4637 .selected
4638 .saturating_sub(1);
4639 } else {
4640 self.lambda_application_state.table.selected = self
4641 .lambda_application_state
4642 .table
4643 .selected
4644 .saturating_sub(1);
4645 self.lambda_application_state.table.snap_to_page();
4646 }
4647 } else if self.current_service == Service::CloudFormationStacks {
4648 if self.cfn_state.current_stack.is_some()
4649 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4650 {
4651 self.cfn_state.parameters.prev_item();
4652 } else if self.cfn_state.current_stack.is_some()
4653 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4654 {
4655 self.cfn_state.outputs.prev_item();
4656 } else if self.cfn_state.current_stack.is_some()
4657 && self.cfn_state.detail_tab == CfnDetailTab::Resources
4658 {
4659 self.cfn_state.resources.prev_item();
4660 } else {
4661 self.cfn_state.table.prev_item();
4662 }
4663 } else if self.current_service == Service::IamUsers {
4664 self.iam_state.users.prev_item();
4665 } else if self.current_service == Service::IamRoles {
4666 if self.iam_state.current_role.is_some() {
4667 if self.iam_state.role_tab == RoleTab::TrustRelationships {
4668 self.iam_state.trust_policy_scroll =
4669 self.iam_state.trust_policy_scroll.saturating_sub(1);
4670 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4671 self.iam_state.revoke_sessions_scroll =
4672 self.iam_state.revoke_sessions_scroll.saturating_sub(1);
4673 } else if self.iam_state.role_tab == RoleTab::Tags {
4674 self.iam_state.tags.prev_item();
4675 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4676 self.iam_state.last_accessed_services.prev_item();
4677 } else {
4678 self.iam_state.policies.prev_item();
4679 }
4680 } else {
4681 self.iam_state.roles.prev_item();
4682 }
4683 } else if self.current_service == Service::IamUserGroups {
4684 if self.iam_state.current_group.is_some() {
4685 if self.iam_state.group_tab == GroupTab::Users {
4686 self.iam_state.group_users.prev_item();
4687 } else if self.iam_state.group_tab == GroupTab::Permissions {
4688 self.iam_state.policies.prev_item();
4689 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4690 self.iam_state.last_accessed_services.prev_item();
4691 }
4692 } else {
4693 self.iam_state.groups.prev_item();
4694 }
4695 }
4696 }
4697 _ => {}
4698 }
4699 }
4700
4701 fn page_down(&mut self) {
4702 if self.mode == Mode::ColumnSelector {
4703 let max = self.get_column_selector_max();
4704 self.column_selector_index = (self.column_selector_index + 10).min(max);
4705 } else if self.mode == Mode::FilterInput
4706 && self.current_service == Service::CloudFormationStacks
4707 {
4708 if self.cfn_state.current_stack.is_some()
4709 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4710 {
4711 let page_size = self.cfn_state.parameters.page_size.value();
4712 let filtered_count = filtered_parameters(self).len();
4713 self.cfn_state.parameters_input_focus.handle_page_down(
4714 &mut self.cfn_state.parameters.selected,
4715 &mut self.cfn_state.parameters.scroll_offset,
4716 page_size,
4717 filtered_count,
4718 );
4719 } else if self.cfn_state.current_stack.is_some()
4720 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4721 {
4722 let page_size = self.cfn_state.outputs.page_size.value();
4723 let filtered_count = filtered_outputs(self).len();
4724 self.cfn_state.outputs_input_focus.handle_page_down(
4725 &mut self.cfn_state.outputs.selected,
4726 &mut self.cfn_state.outputs.scroll_offset,
4727 page_size,
4728 filtered_count,
4729 );
4730 } else {
4731 use crate::ui::cfn::filtered_cloudformation_stacks;
4732 let page_size = self.cfn_state.table.page_size.value();
4733 let filtered_count = filtered_cloudformation_stacks(self).len();
4734 self.cfn_state.input_focus.handle_page_down(
4735 &mut self.cfn_state.table.selected,
4736 &mut self.cfn_state.table.scroll_offset,
4737 page_size,
4738 filtered_count,
4739 );
4740 }
4741 } else if self.mode == Mode::FilterInput
4742 && self.current_service == Service::IamRoles
4743 && self.iam_state.current_role.is_none()
4744 {
4745 let page_size = self.iam_state.roles.page_size.value();
4746 let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
4747 self.iam_state.role_input_focus.handle_page_down(
4748 &mut self.iam_state.roles.selected,
4749 &mut self.iam_state.roles.scroll_offset,
4750 page_size,
4751 filtered_count,
4752 );
4753 } else if self.mode == Mode::FilterInput
4754 && self.current_service == Service::CloudWatchAlarms
4755 {
4756 let page_size = self.alarms_state.table.page_size.value();
4757 let filtered_count = self.alarms_state.table.items.len();
4758 self.alarms_state.input_focus.handle_page_down(
4759 &mut self.alarms_state.table.selected,
4760 &mut self.alarms_state.table.scroll_offset,
4761 page_size,
4762 filtered_count,
4763 );
4764 } else if self.mode == Mode::FilterInput
4765 && self.current_service == Service::CloudWatchLogGroups
4766 {
4767 if self.view_mode == ViewMode::List {
4768 let filtered = self.filtered_log_groups();
4770 let page_size = self.log_groups_state.log_groups.page_size.value();
4771 let filtered_count = filtered.len();
4772 self.log_groups_state.input_focus.handle_page_down(
4773 &mut self.log_groups_state.log_groups.selected,
4774 &mut self.log_groups_state.log_groups.scroll_offset,
4775 page_size,
4776 filtered_count,
4777 );
4778 } else {
4779 let filtered = self.filtered_log_streams();
4781 let page_size = 20;
4782 let filtered_count = filtered.len();
4783 self.log_groups_state.input_focus.handle_page_down(
4784 &mut self.log_groups_state.selected_stream,
4785 &mut self.log_groups_state.stream_page,
4786 page_size,
4787 filtered_count,
4788 );
4789 self.log_groups_state.expanded_stream = None;
4790 }
4791 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4792 {
4793 if self.lambda_state.current_function.is_some()
4794 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4795 && self.lambda_state.version_input_focus == InputFocus::Pagination
4796 {
4797 let page_size = self.lambda_state.version_table.page_size.value();
4798 let filtered_count: usize = self
4799 .lambda_state
4800 .version_table
4801 .items
4802 .iter()
4803 .filter(|v| {
4804 self.lambda_state.version_table.filter.is_empty()
4805 || v.version
4806 .to_lowercase()
4807 .contains(&self.lambda_state.version_table.filter.to_lowercase())
4808 || v.aliases
4809 .to_lowercase()
4810 .contains(&self.lambda_state.version_table.filter.to_lowercase())
4811 || v.description
4812 .to_lowercase()
4813 .contains(&self.lambda_state.version_table.filter.to_lowercase())
4814 })
4815 .count();
4816 let target = self.lambda_state.version_table.selected + page_size;
4817 self.lambda_state.version_table.selected =
4818 target.min(filtered_count.saturating_sub(1));
4819 } else if self.lambda_state.current_function.is_some()
4820 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4821 || (self.lambda_state.current_version.is_some()
4822 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4823 && self.lambda_state.alias_input_focus == InputFocus::Pagination
4824 {
4825 let page_size = self.lambda_state.alias_table.page_size.value();
4826 let version_filter = self.lambda_state.current_version.clone();
4827 let filtered_count = self
4828 .lambda_state
4829 .alias_table
4830 .items
4831 .iter()
4832 .filter(|a| {
4833 (version_filter.is_none()
4834 || a.versions.contains(version_filter.as_ref().unwrap()))
4835 && (self.lambda_state.alias_table.filter.is_empty()
4836 || a.name
4837 .to_lowercase()
4838 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
4839 || a.versions
4840 .to_lowercase()
4841 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
4842 || a.description
4843 .to_lowercase()
4844 .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
4845 })
4846 .count();
4847 let target = self.lambda_state.alias_table.selected + page_size;
4848 self.lambda_state.alias_table.selected =
4849 target.min(filtered_count.saturating_sub(1));
4850 } else if self.lambda_state.current_function.is_none() {
4851 let page_size = self.lambda_state.table.page_size.value();
4852 let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
4853 self.lambda_state.input_focus.handle_page_down(
4854 &mut self.lambda_state.table.selected,
4855 &mut self.lambda_state.table.scroll_offset,
4856 page_size,
4857 filtered_count,
4858 );
4859 }
4860 } else if self.mode == Mode::FilterInput
4861 && self.current_service == Service::EcrRepositories
4862 && self.ecr_state.current_repository.is_none()
4863 && self.ecr_state.input_focus == InputFocus::Filter
4864 {
4865 let filtered = self.filtered_ecr_repositories();
4867 self.ecr_state.repositories.page_down(filtered.len());
4868 } else if self.mode == Mode::FilterInput
4869 && self.current_service == Service::EcrRepositories
4870 && self.ecr_state.current_repository.is_none()
4871 {
4872 let page_size = self.ecr_state.repositories.page_size.value();
4873 let filtered_count = self.filtered_ecr_repositories().len();
4874 self.ecr_state.input_focus.handle_page_down(
4875 &mut self.ecr_state.repositories.selected,
4876 &mut self.ecr_state.repositories.scroll_offset,
4877 page_size,
4878 filtered_count,
4879 );
4880 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4881 let page_size = self.iam_state.policies.page_size.value();
4882 let filtered_count = crate::ui::iam::filtered_iam_policies(self).len();
4883 self.iam_state.policy_input_focus.handle_page_down(
4884 &mut self.iam_state.policies.selected,
4885 &mut self.iam_state.policies.scroll_offset,
4886 page_size,
4887 filtered_count,
4888 );
4889 } else if self.view_mode == ViewMode::PolicyView {
4890 let lines = self.iam_state.policy_document.lines().count();
4891 let max_scroll = lines.saturating_sub(1);
4892 self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
4893 } else if self.current_service == Service::CloudFormationStacks
4894 && self.cfn_state.current_stack.is_some()
4895 && self.cfn_state.detail_tab == CfnDetailTab::Template
4896 {
4897 let lines = self.cfn_state.template_body.lines().count();
4898 let max_scroll = lines.saturating_sub(1);
4899 self.cfn_state.template_scroll = (self.cfn_state.template_scroll + 10).min(max_scroll);
4900 } else if self.current_service == Service::LambdaFunctions
4901 && self.lambda_state.current_function.is_some()
4902 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4903 && !self.lambda_state.is_metrics_loading()
4904 {
4905 self.lambda_state
4906 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
4907 } else if self.current_service == Service::SqsQueues
4908 && self.sqs_state.current_queue.is_some()
4909 {
4910 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
4911 self.sqs_state
4912 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
4913 } else {
4914 let lines = self.sqs_state.policy_document.lines().count();
4915 let max_scroll = lines.saturating_sub(1);
4916 self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
4917 }
4918 } else if self.current_service == Service::IamRoles
4919 && self.iam_state.current_role.is_some()
4920 && self.iam_state.role_tab == RoleTab::TrustRelationships
4921 {
4922 let lines = self.iam_state.trust_policy_document.lines().count();
4923 let max_scroll = lines.saturating_sub(1);
4924 self.iam_state.trust_policy_scroll =
4925 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
4926 } else if self.current_service == Service::IamRoles
4927 && self.iam_state.current_role.is_some()
4928 && self.iam_state.role_tab == RoleTab::RevokeSessions
4929 {
4930 self.iam_state.revoke_sessions_scroll =
4931 (self.iam_state.revoke_sessions_scroll + 10).min(19);
4932 } else if self.mode == Mode::Normal {
4933 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4934 {
4935 let total_rows = self.calculate_total_bucket_rows();
4936 self.s3_state.selected_row = self
4937 .s3_state
4938 .selected_row
4939 .saturating_add(10)
4940 .min(total_rows.saturating_sub(1));
4941
4942 let visible_rows = self.s3_state.bucket_visible_rows.get();
4944 if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
4945 self.s3_state.bucket_scroll_offset =
4946 self.s3_state.selected_row - visible_rows + 1;
4947 }
4948 } else if self.current_service == Service::S3Buckets
4949 && self.s3_state.current_bucket.is_some()
4950 {
4951 let total_rows = self.calculate_total_object_rows();
4952 self.s3_state.selected_object = self
4953 .s3_state
4954 .selected_object
4955 .saturating_add(10)
4956 .min(total_rows.saturating_sub(1));
4957
4958 let visible_rows = self.s3_state.object_visible_rows.get();
4960 if self.s3_state.selected_object
4961 >= self.s3_state.object_scroll_offset + visible_rows
4962 {
4963 self.s3_state.object_scroll_offset =
4964 self.s3_state.selected_object - visible_rows + 1;
4965 }
4966 } else if self.current_service == Service::CloudWatchLogGroups
4967 && self.view_mode == ViewMode::List
4968 {
4969 let filtered = self.filtered_log_groups();
4970 self.log_groups_state.log_groups.page_down(filtered.len());
4971 } else if self.current_service == Service::CloudWatchLogGroups
4972 && self.view_mode == ViewMode::Detail
4973 {
4974 let len = self.filtered_log_streams().len();
4975 nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
4976 } else if self.view_mode == ViewMode::Events {
4977 let max = self.log_groups_state.log_events.len();
4978 nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
4979 } else if self.view_mode == ViewMode::InsightsResults {
4980 let max = self.insights_state.insights.query_results.len();
4981 nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
4982 } else if self.current_service == Service::CloudWatchAlarms {
4983 let filtered = match self.alarms_state.alarm_tab {
4984 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4985 AlarmTab::InAlarm => self
4986 .alarms_state
4987 .table
4988 .items
4989 .iter()
4990 .filter(|a| a.state.to_uppercase() == "ALARM")
4991 .count(),
4992 };
4993 if filtered > 0 {
4994 self.alarms_state.table.page_down(filtered);
4995 }
4996 } else if self.current_service == Service::Ec2Instances {
4997 let filtered: Vec<_> = self
4998 .ec2_state
4999 .table
5000 .items
5001 .iter()
5002 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
5003 .filter(|i| {
5004 if self.ec2_state.table.filter.is_empty() {
5005 return true;
5006 }
5007 i.name.contains(&self.ec2_state.table.filter)
5008 || i.instance_id.contains(&self.ec2_state.table.filter)
5009 || i.state.contains(&self.ec2_state.table.filter)
5010 || i.instance_type.contains(&self.ec2_state.table.filter)
5011 || i.availability_zone.contains(&self.ec2_state.table.filter)
5012 || i.security_groups.contains(&self.ec2_state.table.filter)
5013 || i.key_name.contains(&self.ec2_state.table.filter)
5014 })
5015 .collect();
5016 if !filtered.is_empty() {
5017 self.ec2_state.table.page_down(filtered.len());
5018 }
5019 } else if self.current_service == Service::EcrRepositories {
5020 if self.ecr_state.current_repository.is_some() {
5021 let filtered = self.filtered_ecr_images();
5022 self.ecr_state.images.page_down(filtered.len());
5023 } else {
5024 let filtered = self.filtered_ecr_repositories();
5025 self.ecr_state.repositories.page_down(filtered.len());
5026 }
5027 } else if self.current_service == Service::SqsQueues {
5028 let filtered = crate::ui::sqs::filtered_queues(
5029 &self.sqs_state.queues.items,
5030 &self.sqs_state.queues.filter,
5031 );
5032 self.sqs_state.queues.page_down(filtered.len());
5033 } else if self.current_service == Service::LambdaFunctions {
5034 let len = crate::ui::lambda::filtered_lambda_functions(self).len();
5035 self.lambda_state.table.page_down(len);
5036 } else if self.current_service == Service::LambdaApplications {
5037 let len = crate::ui::lambda::filtered_lambda_applications(self).len();
5038 self.lambda_application_state.table.page_down(len);
5039 } else if self.current_service == Service::CloudFormationStacks {
5040 if self.cfn_state.current_stack.is_some()
5041 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5042 {
5043 let filtered = filtered_parameters(self);
5044 self.cfn_state.parameters.page_down(filtered.len());
5045 } else if self.cfn_state.current_stack.is_some()
5046 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5047 {
5048 let filtered = filtered_outputs(self);
5049 self.cfn_state.outputs.page_down(filtered.len());
5050 } else {
5051 let filtered = self.filtered_cloudformation_stacks();
5052 self.cfn_state.table.page_down(filtered.len());
5053 }
5054 } else if self.current_service == Service::IamUsers {
5055 let len = crate::ui::iam::filtered_iam_users(self).len();
5056 nav_page_down(&mut self.iam_state.users.selected, len, 10);
5057 } else if self.current_service == Service::IamRoles {
5058 if self.iam_state.current_role.is_some() {
5059 let filtered = crate::ui::iam::filtered_iam_policies(self);
5060 if !filtered.is_empty() {
5061 self.iam_state.policies.page_down(filtered.len());
5062 }
5063 } else {
5064 let filtered = crate::ui::iam::filtered_iam_roles(self);
5065 self.iam_state.roles.page_down(filtered.len());
5066 }
5067 } else if self.current_service == Service::IamUserGroups {
5068 if self.iam_state.current_group.is_some() {
5069 if self.iam_state.group_tab == GroupTab::Users {
5070 let filtered: Vec<_> = self
5071 .iam_state
5072 .group_users
5073 .items
5074 .iter()
5075 .filter(|u| {
5076 if self.iam_state.group_users.filter.is_empty() {
5077 true
5078 } else {
5079 u.user_name
5080 .to_lowercase()
5081 .contains(&self.iam_state.group_users.filter.to_lowercase())
5082 }
5083 })
5084 .collect();
5085 if !filtered.is_empty() {
5086 self.iam_state.group_users.page_down(filtered.len());
5087 }
5088 } else if self.iam_state.group_tab == GroupTab::Permissions {
5089 let filtered = crate::ui::iam::filtered_iam_policies(self);
5090 if !filtered.is_empty() {
5091 self.iam_state.policies.page_down(filtered.len());
5092 }
5093 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5094 let filtered = crate::ui::iam::filtered_last_accessed(self);
5095 if !filtered.is_empty() {
5096 self.iam_state
5097 .last_accessed_services
5098 .page_down(filtered.len());
5099 }
5100 }
5101 } else {
5102 let filtered: Vec<_> = self
5103 .iam_state
5104 .groups
5105 .items
5106 .iter()
5107 .filter(|g| {
5108 if self.iam_state.groups.filter.is_empty() {
5109 true
5110 } else {
5111 g.group_name
5112 .to_lowercase()
5113 .contains(&self.iam_state.groups.filter.to_lowercase())
5114 }
5115 })
5116 .collect();
5117 if !filtered.is_empty() {
5118 self.iam_state.groups.page_down(filtered.len());
5119 }
5120 }
5121 }
5122 }
5123 }
5124
5125 fn page_up(&mut self) {
5126 if self.mode == Mode::ColumnSelector {
5127 self.column_selector_index = self.column_selector_index.saturating_sub(10);
5128 } else if self.mode == Mode::FilterInput
5129 && self.current_service == Service::CloudFormationStacks
5130 {
5131 if self.cfn_state.current_stack.is_some()
5132 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5133 {
5134 let page_size = self.cfn_state.parameters.page_size.value();
5135 self.cfn_state.parameters_input_focus.handle_page_up(
5136 &mut self.cfn_state.parameters.selected,
5137 &mut self.cfn_state.parameters.scroll_offset,
5138 page_size,
5139 );
5140 } else if self.cfn_state.current_stack.is_some()
5141 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5142 {
5143 let page_size = self.cfn_state.outputs.page_size.value();
5144 self.cfn_state.outputs_input_focus.handle_page_up(
5145 &mut self.cfn_state.outputs.selected,
5146 &mut self.cfn_state.outputs.scroll_offset,
5147 page_size,
5148 );
5149 } else {
5150 let page_size = self.cfn_state.table.page_size.value();
5151 self.cfn_state.input_focus.handle_page_up(
5152 &mut self.cfn_state.table.selected,
5153 &mut self.cfn_state.table.scroll_offset,
5154 page_size,
5155 );
5156 }
5157 } else if self.mode == Mode::FilterInput
5158 && self.current_service == Service::IamRoles
5159 && self.iam_state.current_role.is_none()
5160 {
5161 let page_size = self.iam_state.roles.page_size.value();
5162 self.iam_state.role_input_focus.handle_page_up(
5163 &mut self.iam_state.roles.selected,
5164 &mut self.iam_state.roles.scroll_offset,
5165 page_size,
5166 );
5167 } else if self.mode == Mode::FilterInput
5168 && self.current_service == Service::CloudWatchAlarms
5169 {
5170 let page_size = self.alarms_state.table.page_size.value();
5171 self.alarms_state.input_focus.handle_page_up(
5172 &mut self.alarms_state.table.selected,
5173 &mut self.alarms_state.table.scroll_offset,
5174 page_size,
5175 );
5176 } else if self.mode == Mode::FilterInput
5177 && self.current_service == Service::CloudWatchLogGroups
5178 {
5179 if self.view_mode == ViewMode::List {
5180 let page_size = self.log_groups_state.log_groups.page_size.value();
5182 self.log_groups_state.input_focus.handle_page_up(
5183 &mut self.log_groups_state.log_groups.selected,
5184 &mut self.log_groups_state.log_groups.scroll_offset,
5185 page_size,
5186 );
5187 } else {
5188 let page_size = 20;
5190 self.log_groups_state.input_focus.handle_page_up(
5191 &mut self.log_groups_state.selected_stream,
5192 &mut self.log_groups_state.stream_page,
5193 page_size,
5194 );
5195 self.log_groups_state.expanded_stream = None;
5196 }
5197 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5198 {
5199 if self.lambda_state.current_function.is_some()
5200 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5201 && self.lambda_state.version_input_focus == InputFocus::Pagination
5202 {
5203 let page_size = self.lambda_state.version_table.page_size.value();
5204 self.lambda_state.version_table.selected = self
5205 .lambda_state
5206 .version_table
5207 .selected
5208 .saturating_sub(page_size);
5209 } else if self.lambda_state.current_function.is_some()
5210 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5211 || (self.lambda_state.current_version.is_some()
5212 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5213 && self.lambda_state.alias_input_focus == InputFocus::Pagination
5214 {
5215 let page_size = self.lambda_state.alias_table.page_size.value();
5216 self.lambda_state.alias_table.selected = self
5217 .lambda_state
5218 .alias_table
5219 .selected
5220 .saturating_sub(page_size);
5221 } else if self.lambda_state.current_function.is_none() {
5222 let page_size = self.lambda_state.table.page_size.value();
5223 self.lambda_state.input_focus.handle_page_up(
5224 &mut self.lambda_state.table.selected,
5225 &mut self.lambda_state.table.scroll_offset,
5226 page_size,
5227 );
5228 }
5229 } else if self.mode == Mode::FilterInput
5230 && self.current_service == Service::EcrRepositories
5231 && self.ecr_state.current_repository.is_none()
5232 && self.ecr_state.input_focus == InputFocus::Filter
5233 {
5234 self.ecr_state.repositories.page_up();
5236 } else if self.mode == Mode::FilterInput
5237 && self.current_service == Service::EcrRepositories
5238 && self.ecr_state.current_repository.is_none()
5239 {
5240 let page_size = self.ecr_state.repositories.page_size.value();
5241 self.ecr_state.input_focus.handle_page_up(
5242 &mut self.ecr_state.repositories.selected,
5243 &mut self.ecr_state.repositories.scroll_offset,
5244 page_size,
5245 );
5246 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5247 let page_size = self.iam_state.policies.page_size.value();
5248 self.iam_state.policy_input_focus.handle_page_up(
5249 &mut self.iam_state.policies.selected,
5250 &mut self.iam_state.policies.scroll_offset,
5251 page_size,
5252 );
5253 } else if self.view_mode == ViewMode::PolicyView {
5254 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
5255 } else if self.current_service == Service::CloudFormationStacks
5256 && self.cfn_state.current_stack.is_some()
5257 && self.cfn_state.detail_tab == CfnDetailTab::Template
5258 {
5259 self.cfn_state.template_scroll = self.cfn_state.template_scroll.saturating_sub(10);
5260 } else if self.current_service == Service::SqsQueues
5261 && self.sqs_state.current_queue.is_some()
5262 {
5263 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5264 self.sqs_state
5265 .set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
5266 } else {
5267 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
5268 }
5269 } else if self.current_service == Service::IamRoles
5270 && self.iam_state.current_role.is_some()
5271 && self.iam_state.role_tab == RoleTab::TrustRelationships
5272 {
5273 self.iam_state.trust_policy_scroll =
5274 self.iam_state.trust_policy_scroll.saturating_sub(10);
5275 } else if self.current_service == Service::IamRoles
5276 && self.iam_state.current_role.is_some()
5277 && self.iam_state.role_tab == RoleTab::RevokeSessions
5278 {
5279 self.iam_state.revoke_sessions_scroll =
5280 self.iam_state.revoke_sessions_scroll.saturating_sub(10);
5281 } else if self.mode == Mode::Normal {
5282 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5283 {
5284 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
5285
5286 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5288 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5289 }
5290 } else if self.current_service == Service::S3Buckets
5291 && self.s3_state.current_bucket.is_some()
5292 {
5293 self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
5294
5295 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5297 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5298 }
5299 } else if self.current_service == Service::CloudWatchLogGroups
5300 && self.view_mode == ViewMode::List
5301 {
5302 self.log_groups_state.log_groups.page_up();
5303 } else if self.current_service == Service::CloudWatchLogGroups
5304 && self.view_mode == ViewMode::Detail
5305 {
5306 self.log_groups_state.selected_stream =
5307 self.log_groups_state.selected_stream.saturating_sub(10);
5308 } else if self.view_mode == ViewMode::Events {
5309 if self.log_groups_state.event_scroll_offset < 10
5310 && self.log_groups_state.has_older_events
5311 {
5312 self.log_groups_state.loading = true;
5313 }
5314 self.log_groups_state.event_scroll_offset =
5315 self.log_groups_state.event_scroll_offset.saturating_sub(10);
5316 } else if self.view_mode == ViewMode::InsightsResults {
5317 self.insights_state.insights.results_selected = self
5318 .insights_state
5319 .insights
5320 .results_selected
5321 .saturating_sub(10);
5322 } else if self.current_service == Service::CloudWatchAlarms {
5323 self.alarms_state.table.page_up();
5324 } else if self.current_service == Service::Ec2Instances {
5325 self.ec2_state.table.page_up();
5326 } else if self.current_service == Service::EcrRepositories {
5327 if self.ecr_state.current_repository.is_some() {
5328 self.ecr_state.images.page_up();
5329 } else {
5330 self.ecr_state.repositories.page_up();
5331 }
5332 } else if self.current_service == Service::SqsQueues {
5333 self.sqs_state.queues.page_up();
5334 } else if self.current_service == Service::LambdaFunctions {
5335 self.lambda_state.table.page_up();
5336 } else if self.current_service == Service::LambdaApplications {
5337 self.lambda_application_state.table.page_up();
5338 } else if self.current_service == Service::CloudFormationStacks {
5339 if self.cfn_state.current_stack.is_some()
5340 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5341 {
5342 self.cfn_state.parameters.page_up();
5343 } else if self.cfn_state.current_stack.is_some()
5344 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5345 {
5346 self.cfn_state.outputs.page_up();
5347 } else {
5348 self.cfn_state.table.page_up();
5349 }
5350 } else if self.current_service == Service::IamUsers {
5351 self.iam_state.users.page_up();
5352 } else if self.current_service == Service::IamRoles {
5353 if self.iam_state.current_role.is_some() {
5354 self.iam_state.policies.page_up();
5355 } else {
5356 self.iam_state.roles.page_up();
5357 }
5358 }
5359 }
5360 }
5361
5362 fn next_pane(&mut self) {
5363 if self.current_service == Service::S3Buckets {
5364 if self.s3_state.current_bucket.is_some() {
5365 let mut visual_idx = 0;
5368 let mut found_obj: Option<S3Object> = None;
5369
5370 fn check_nested(
5372 obj: &S3Object,
5373 visual_idx: &mut usize,
5374 target_idx: usize,
5375 expanded_prefixes: &std::collections::HashSet<String>,
5376 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5377 found_obj: &mut Option<S3Object>,
5378 ) {
5379 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5380 if let Some(preview) = prefix_preview.get(&obj.key) {
5381 for nested_obj in preview {
5382 if *visual_idx == target_idx {
5383 *found_obj = Some(nested_obj.clone());
5384 return;
5385 }
5386 *visual_idx += 1;
5387
5388 check_nested(
5390 nested_obj,
5391 visual_idx,
5392 target_idx,
5393 expanded_prefixes,
5394 prefix_preview,
5395 found_obj,
5396 );
5397 if found_obj.is_some() {
5398 return;
5399 }
5400 }
5401 } else {
5402 *visual_idx += 1;
5404 }
5405 }
5406 }
5407
5408 for obj in &self.s3_state.objects {
5409 if visual_idx == self.s3_state.selected_object {
5410 found_obj = Some(obj.clone());
5411 break;
5412 }
5413 visual_idx += 1;
5414
5415 check_nested(
5417 obj,
5418 &mut visual_idx,
5419 self.s3_state.selected_object,
5420 &self.s3_state.expanded_prefixes,
5421 &self.s3_state.prefix_preview,
5422 &mut found_obj,
5423 );
5424 if found_obj.is_some() {
5425 break;
5426 }
5427 }
5428
5429 if let Some(obj) = found_obj {
5430 if obj.is_prefix {
5431 if !self.s3_state.expanded_prefixes.contains(&obj.key) {
5432 self.s3_state.expanded_prefixes.insert(obj.key.clone());
5433 if !self.s3_state.prefix_preview.contains_key(&obj.key) {
5435 self.s3_state.buckets.loading = true;
5436 }
5437 }
5438 if self.s3_state.expanded_prefixes.contains(&obj.key) {
5440 if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
5441 if !preview.is_empty() {
5442 self.s3_state.selected_object += 1;
5443 }
5444 }
5445 }
5446 }
5447 }
5448 } else {
5449 let mut row_idx = 0;
5451 let mut found = false;
5452 for bucket in &self.s3_state.buckets.items {
5453 if row_idx == self.s3_state.selected_row {
5454 if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
5456 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
5457 if !self.s3_state.bucket_preview.contains_key(&bucket.name)
5458 && !self.s3_state.bucket_errors.contains_key(&bucket.name)
5459 {
5460 self.s3_state.buckets.loading = true;
5461 }
5462 }
5463 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5465 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5466 if !preview.is_empty() {
5467 self.s3_state.selected_row = row_idx + 1;
5468 }
5469 }
5470 }
5471 break;
5472 }
5473 row_idx += 1;
5474
5475 if self.s3_state.bucket_errors.contains_key(&bucket.name)
5477 && self.s3_state.expanded_prefixes.contains(&bucket.name)
5478 {
5479 continue;
5480 }
5481
5482 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5483 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5484 #[allow(clippy::too_many_arguments)]
5486 fn check_nested_expansion(
5487 objects: &[crate::s3::Object],
5488 row_idx: &mut usize,
5489 target_row: usize,
5490 expanded_prefixes: &mut std::collections::HashSet<String>,
5491 prefix_preview: &std::collections::HashMap<
5492 String,
5493 Vec<crate::s3::Object>,
5494 >,
5495 found: &mut bool,
5496 loading: &mut bool,
5497 selected_row: &mut usize,
5498 ) {
5499 for obj in objects {
5500 if *row_idx == target_row {
5501 if obj.is_prefix {
5503 if !expanded_prefixes.contains(&obj.key) {
5504 expanded_prefixes.insert(obj.key.clone());
5505 if !prefix_preview.contains_key(&obj.key) {
5506 *loading = true;
5507 }
5508 }
5509 if expanded_prefixes.contains(&obj.key) {
5511 if let Some(preview) = prefix_preview.get(&obj.key)
5512 {
5513 if !preview.is_empty() {
5514 *selected_row = *row_idx + 1;
5515 }
5516 }
5517 }
5518 }
5519 *found = true;
5520 return;
5521 }
5522 *row_idx += 1;
5523
5524 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5526 if let Some(nested) = prefix_preview.get(&obj.key) {
5527 check_nested_expansion(
5528 nested,
5529 row_idx,
5530 target_row,
5531 expanded_prefixes,
5532 prefix_preview,
5533 found,
5534 loading,
5535 selected_row,
5536 );
5537 if *found {
5538 return;
5539 }
5540 } else {
5541 *row_idx += 1; }
5543 }
5544 }
5545 }
5546
5547 check_nested_expansion(
5548 preview,
5549 &mut row_idx,
5550 self.s3_state.selected_row,
5551 &mut self.s3_state.expanded_prefixes,
5552 &self.s3_state.prefix_preview,
5553 &mut found,
5554 &mut self.s3_state.buckets.loading,
5555 &mut self.s3_state.selected_row,
5556 );
5557 if found || row_idx > self.s3_state.selected_row {
5558 break;
5559 }
5560 } else {
5561 row_idx += 1;
5562 if row_idx > self.s3_state.selected_row {
5563 break;
5564 }
5565 }
5566 }
5567 if found {
5568 break;
5569 }
5570 }
5571 }
5572 } else if self.view_mode == ViewMode::InsightsResults {
5573 let max_cols = self
5575 .insights_state
5576 .insights
5577 .query_results
5578 .first()
5579 .map(|r| r.len())
5580 .unwrap_or(0);
5581 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
5582 self.insights_state.insights.results_horizontal_scroll += 1;
5583 }
5584 } else if self.current_service == Service::CloudWatchLogGroups
5585 && self.view_mode == ViewMode::List
5586 {
5587 if self.log_groups_state.log_groups.expanded_item
5589 != Some(self.log_groups_state.log_groups.selected)
5590 {
5591 self.log_groups_state.log_groups.expanded_item =
5592 Some(self.log_groups_state.log_groups.selected);
5593 }
5594 } else if self.current_service == Service::CloudWatchLogGroups
5595 && self.view_mode == ViewMode::Detail
5596 {
5597 if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
5599 {
5600 self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
5601 }
5602 } else if self.view_mode == ViewMode::Events {
5603 if self.log_groups_state.expanded_event
5606 != Some(self.log_groups_state.event_scroll_offset)
5607 {
5608 self.log_groups_state.expanded_event =
5609 Some(self.log_groups_state.event_scroll_offset);
5610 }
5611 } else if self.current_service == Service::CloudWatchAlarms {
5612 if !self.alarms_state.table.is_expanded() {
5614 self.alarms_state.table.toggle_expand();
5615 }
5616 } else if self.current_service == Service::Ec2Instances {
5617 if !self.ec2_state.table.is_expanded() {
5618 self.ec2_state.table.toggle_expand();
5619 }
5620 } else if self.current_service == Service::EcrRepositories {
5621 if self.ecr_state.current_repository.is_some() {
5622 self.ecr_state.images.toggle_expand();
5624 } else {
5625 self.ecr_state.repositories.toggle_expand();
5627 }
5628 } else if self.current_service == Service::SqsQueues {
5629 if self.sqs_state.current_queue.is_some()
5630 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5631 {
5632 self.sqs_state.triggers.toggle_expand();
5633 } else if self.sqs_state.current_queue.is_some()
5634 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5635 {
5636 self.sqs_state.pipes.toggle_expand();
5637 } else if self.sqs_state.current_queue.is_some()
5638 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5639 {
5640 self.sqs_state.tags.toggle_expand();
5641 } else if self.sqs_state.current_queue.is_some()
5642 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5643 {
5644 self.sqs_state.subscriptions.toggle_expand();
5645 } else {
5646 self.sqs_state.queues.expand();
5647 }
5648 } else if self.current_service == Service::LambdaFunctions {
5649 if self.lambda_state.current_function.is_some()
5650 && self.lambda_state.detail_tab == LambdaDetailTab::Code
5651 {
5652 if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
5654 self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
5655 } else {
5656 self.lambda_state.layer_expanded = None;
5657 }
5658 } else if self.lambda_state.current_function.is_some()
5659 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5660 {
5661 self.lambda_state.version_table.toggle_expand();
5663 } else if self.lambda_state.current_function.is_some()
5664 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5665 || (self.lambda_state.current_version.is_some()
5666 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5667 {
5668 self.lambda_state.alias_table.toggle_expand();
5670 } else if self.lambda_state.current_function.is_none() {
5671 self.lambda_state.table.toggle_expand();
5673 }
5674 } else if self.current_service == Service::LambdaApplications {
5675 if self.lambda_application_state.current_application.is_some() {
5676 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
5678 {
5679 self.lambda_application_state.resources.toggle_expand();
5680 } else {
5681 self.lambda_application_state.deployments.toggle_expand();
5682 }
5683 } else {
5684 if self.lambda_application_state.table.expanded_item
5686 != Some(self.lambda_application_state.table.selected)
5687 {
5688 self.lambda_application_state.table.expanded_item =
5689 Some(self.lambda_application_state.table.selected);
5690 }
5691 }
5692 } else if self.current_service == Service::CloudFormationStacks
5693 && self.cfn_state.current_stack.is_none()
5694 {
5695 self.cfn_state.table.toggle_expand();
5696 } else if self.current_service == Service::CloudFormationStacks
5697 && self.cfn_state.current_stack.is_some()
5698 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5699 {
5700 self.cfn_state.parameters.toggle_expand();
5701 } else if self.current_service == Service::CloudFormationStacks
5702 && self.cfn_state.current_stack.is_some()
5703 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5704 {
5705 self.cfn_state.outputs.toggle_expand();
5706 } else if self.current_service == Service::CloudFormationStacks
5707 && self.cfn_state.current_stack.is_some()
5708 && self.cfn_state.detail_tab == CfnDetailTab::Resources
5709 {
5710 self.cfn_state.resources.toggle_expand();
5711 } else if self.current_service == Service::IamUsers {
5712 if self.iam_state.current_user.is_some() {
5713 if self.iam_state.user_tab == UserTab::Tags {
5714 if self.iam_state.user_tags.expanded_item
5715 != Some(self.iam_state.user_tags.selected)
5716 {
5717 self.iam_state.user_tags.expanded_item =
5718 Some(self.iam_state.user_tags.selected);
5719 }
5720 } else if self.iam_state.policies.expanded_item
5721 != Some(self.iam_state.policies.selected)
5722 {
5723 self.iam_state.policies.toggle_expand();
5724 }
5725 } else if !self.iam_state.users.is_expanded() {
5726 self.iam_state.users.toggle_expand();
5727 }
5728 } else if self.current_service == Service::IamRoles {
5729 if self.iam_state.current_role.is_some() {
5730 if self.iam_state.role_tab == RoleTab::Tags {
5732 if !self.iam_state.tags.is_expanded() {
5733 self.iam_state.tags.expand();
5734 }
5735 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5736 if !self.iam_state.last_accessed_services.is_expanded() {
5737 self.iam_state.last_accessed_services.expand();
5738 }
5739 } else if !self.iam_state.policies.is_expanded() {
5740 self.iam_state.policies.expand();
5741 }
5742 } else if !self.iam_state.roles.is_expanded() {
5743 self.iam_state.roles.expand();
5744 }
5745 } else if self.current_service == Service::IamUserGroups {
5746 if self.iam_state.current_group.is_some() {
5747 if self.iam_state.group_tab == GroupTab::Users {
5748 if !self.iam_state.group_users.is_expanded() {
5749 self.iam_state.group_users.expand();
5750 }
5751 } else if self.iam_state.group_tab == GroupTab::Permissions {
5752 if !self.iam_state.policies.is_expanded() {
5753 self.iam_state.policies.expand();
5754 }
5755 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
5756 && !self.iam_state.last_accessed_services.is_expanded()
5757 {
5758 self.iam_state.last_accessed_services.expand();
5759 }
5760 } else if !self.iam_state.groups.is_expanded() {
5761 self.iam_state.groups.expand();
5762 }
5763 }
5764 }
5765
5766 fn go_to_page(&mut self, page: usize) {
5767 if page == 0 {
5768 return;
5769 }
5770
5771 match self.current_service {
5772 Service::CloudWatchAlarms => {
5773 let alarm_page_size = self.alarms_state.table.page_size.value();
5774 let target = (page - 1) * alarm_page_size;
5775 let filtered_count = match self.alarms_state.alarm_tab {
5776 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5777 AlarmTab::InAlarm => self
5778 .alarms_state
5779 .table
5780 .items
5781 .iter()
5782 .filter(|a| a.state.to_uppercase() == "ALARM")
5783 .count(),
5784 };
5785 let max_offset = filtered_count.saturating_sub(alarm_page_size);
5786 self.alarms_state.table.scroll_offset = target.min(max_offset);
5787 self.alarms_state.table.selected = self
5788 .alarms_state
5789 .table
5790 .scroll_offset
5791 .min(filtered_count.saturating_sub(1));
5792 }
5793 Service::CloudWatchLogGroups => match self.view_mode {
5794 ViewMode::Events => {
5795 let page_size = 20;
5796 let target = (page - 1) * page_size;
5797 let max = self.log_groups_state.log_events.len().saturating_sub(1);
5798 self.log_groups_state.event_scroll_offset = target.min(max);
5799 }
5800 ViewMode::Detail => {
5801 let page_size = 20;
5802 let target = (page - 1) * page_size;
5803 let max = self.log_groups_state.log_streams.len().saturating_sub(1);
5804 self.log_groups_state.selected_stream = target.min(max);
5805 }
5806 ViewMode::List => {
5807 let total = self.log_groups_state.log_groups.items.len();
5808 self.log_groups_state.log_groups.goto_page(page, total);
5809 }
5810 _ => {}
5811 },
5812 Service::EcrRepositories => {
5813 if self.ecr_state.current_repository.is_some() {
5814 let filtered_count = self
5815 .ecr_state
5816 .images
5817 .filtered(|img| {
5818 self.ecr_state.images.filter.is_empty()
5819 || img
5820 .tag
5821 .to_lowercase()
5822 .contains(&self.ecr_state.images.filter.to_lowercase())
5823 || img
5824 .digest
5825 .to_lowercase()
5826 .contains(&self.ecr_state.images.filter.to_lowercase())
5827 })
5828 .len();
5829 self.ecr_state.images.goto_page(page, filtered_count);
5830 } else {
5831 let filtered_count = self
5832 .ecr_state
5833 .repositories
5834 .filtered(|r| {
5835 self.ecr_state.repositories.filter.is_empty()
5836 || r.name
5837 .to_lowercase()
5838 .contains(&self.ecr_state.repositories.filter.to_lowercase())
5839 })
5840 .len();
5841 self.ecr_state.repositories.goto_page(page, filtered_count);
5842 }
5843 }
5844 Service::SqsQueues => {
5845 let filtered_count = crate::ui::sqs::filtered_queues(
5846 &self.sqs_state.queues.items,
5847 &self.sqs_state.queues.filter,
5848 )
5849 .len();
5850 self.sqs_state.queues.goto_page(page, filtered_count);
5851 }
5852 Service::S3Buckets => {
5853 if self.s3_state.current_bucket.is_some() {
5854 let page_size = 50; let target = (page - 1) * page_size;
5856 let total_rows = self.calculate_total_object_rows();
5857 let max = total_rows.saturating_sub(1);
5858 self.s3_state.selected_object = target.min(max);
5859 } else {
5860 let page_size = 50; let target = (page - 1) * page_size;
5862 let total_rows = self.calculate_total_bucket_rows();
5863 let max = total_rows.saturating_sub(1);
5864 self.s3_state.selected_row = target.min(max);
5865 }
5866 }
5867 Service::LambdaFunctions => {
5868 if self.lambda_state.current_function.is_some()
5869 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5870 {
5871 let filtered_count = self
5872 .lambda_state
5873 .version_table
5874 .filtered(|v| {
5875 self.lambda_state.version_table.filter.is_empty()
5876 || v.version.to_lowercase().contains(
5877 &self.lambda_state.version_table.filter.to_lowercase(),
5878 )
5879 || v.aliases.to_lowercase().contains(
5880 &self.lambda_state.version_table.filter.to_lowercase(),
5881 )
5882 || v.description.to_lowercase().contains(
5883 &self.lambda_state.version_table.filter.to_lowercase(),
5884 )
5885 })
5886 .len();
5887 self.lambda_state
5888 .version_table
5889 .goto_page(page, filtered_count);
5890 } else {
5891 let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
5892 self.lambda_state.table.goto_page(page, filtered_count);
5893 }
5894 }
5895 Service::LambdaApplications => {
5896 let filtered_count = crate::ui::lambda::filtered_lambda_applications(self).len();
5897 self.lambda_application_state
5898 .table
5899 .goto_page(page, filtered_count);
5900 }
5901 Service::CloudFormationStacks => {
5902 let filtered_count = self.filtered_cloudformation_stacks().len();
5903 self.cfn_state.table.goto_page(page, filtered_count);
5904 }
5905 Service::IamUsers => {
5906 let filtered_count = crate::ui::iam::filtered_iam_users(self).len();
5907 self.iam_state.users.goto_page(page, filtered_count);
5908 }
5909 Service::IamRoles => {
5910 let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
5911 self.iam_state.roles.goto_page(page, filtered_count);
5912 }
5913 _ => {}
5914 }
5915 }
5916
5917 fn prev_pane(&mut self) {
5918 if self.current_service == Service::S3Buckets {
5919 if self.s3_state.current_bucket.is_some() {
5920 let mut visual_idx = 0;
5923 let mut found_obj: Option<S3Object> = None;
5924 let mut parent_idx: Option<usize> = None;
5925
5926 #[allow(clippy::too_many_arguments)]
5928 fn find_with_parent(
5929 objects: &[S3Object],
5930 visual_idx: &mut usize,
5931 target_idx: usize,
5932 expanded_prefixes: &std::collections::HashSet<String>,
5933 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5934 found_obj: &mut Option<S3Object>,
5935 parent_idx: &mut Option<usize>,
5936 current_parent: Option<usize>,
5937 ) {
5938 for obj in objects {
5939 if *visual_idx == target_idx {
5940 *found_obj = Some(obj.clone());
5941 *parent_idx = current_parent;
5942 return;
5943 }
5944 let obj_idx = *visual_idx;
5945 *visual_idx += 1;
5946
5947 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5949 if let Some(preview) = prefix_preview.get(&obj.key) {
5950 find_with_parent(
5951 preview,
5952 visual_idx,
5953 target_idx,
5954 expanded_prefixes,
5955 prefix_preview,
5956 found_obj,
5957 parent_idx,
5958 Some(obj_idx),
5959 );
5960 if found_obj.is_some() {
5961 return;
5962 }
5963 }
5964 }
5965 }
5966 }
5967
5968 find_with_parent(
5969 &self.s3_state.objects,
5970 &mut visual_idx,
5971 self.s3_state.selected_object,
5972 &self.s3_state.expanded_prefixes,
5973 &self.s3_state.prefix_preview,
5974 &mut found_obj,
5975 &mut parent_idx,
5976 None,
5977 );
5978
5979 if let Some(obj) = found_obj {
5980 if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
5981 self.s3_state.expanded_prefixes.remove(&obj.key);
5983 } else if let Some(parent) = parent_idx {
5984 self.s3_state.selected_object = parent;
5986 }
5987 }
5988
5989 let visible_rows = self.s3_state.object_visible_rows.get();
5991 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5992 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5993 } else if self.s3_state.selected_object
5994 >= self.s3_state.object_scroll_offset + visible_rows
5995 {
5996 self.s3_state.object_scroll_offset = self
5997 .s3_state
5998 .selected_object
5999 .saturating_sub(visible_rows - 1);
6000 }
6001 } else {
6002 let mut row_idx = 0;
6004 for bucket in &self.s3_state.buckets.items {
6005 if row_idx == self.s3_state.selected_row {
6006 self.s3_state.expanded_prefixes.remove(&bucket.name);
6008 break;
6009 }
6010 row_idx += 1;
6011 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6012 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6013 #[allow(clippy::too_many_arguments)]
6015 fn check_nested_collapse(
6016 objects: &[crate::s3::Object],
6017 row_idx: &mut usize,
6018 target_row: usize,
6019 expanded_prefixes: &mut std::collections::HashSet<String>,
6020 prefix_preview: &std::collections::HashMap<
6021 String,
6022 Vec<crate::s3::Object>,
6023 >,
6024 found: &mut bool,
6025 selected_row: &mut usize,
6026 parent_row: usize,
6027 ) {
6028 for obj in objects {
6029 let current_row = *row_idx;
6030 if *row_idx == target_row {
6031 if obj.is_prefix {
6033 if expanded_prefixes.contains(&obj.key) {
6034 expanded_prefixes.remove(&obj.key);
6036 } else {
6037 *selected_row = parent_row;
6039 }
6040 } else {
6041 *selected_row = parent_row;
6043 }
6044 *found = true;
6045 return;
6046 }
6047 *row_idx += 1;
6048
6049 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6051 if let Some(nested) = prefix_preview.get(&obj.key) {
6052 check_nested_collapse(
6053 nested,
6054 row_idx,
6055 target_row,
6056 expanded_prefixes,
6057 prefix_preview,
6058 found,
6059 selected_row,
6060 current_row,
6061 );
6062 if *found {
6063 return;
6064 }
6065 } else {
6066 *row_idx += 1; }
6068 }
6069 }
6070 }
6071
6072 let mut found = false;
6073 let parent_row = row_idx - 1; check_nested_collapse(
6075 preview,
6076 &mut row_idx,
6077 self.s3_state.selected_row,
6078 &mut self.s3_state.expanded_prefixes,
6079 &self.s3_state.prefix_preview,
6080 &mut found,
6081 &mut self.s3_state.selected_row,
6082 parent_row,
6083 );
6084 if found {
6085 let visible_rows = self.s3_state.bucket_visible_rows.get();
6087 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6088 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6089 } else if self.s3_state.selected_row
6090 >= self.s3_state.bucket_scroll_offset + visible_rows
6091 {
6092 self.s3_state.bucket_scroll_offset =
6093 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6094 }
6095 return;
6096 }
6097 } else {
6098 row_idx += 1;
6099 }
6100 }
6101 }
6102
6103 let visible_rows = self.s3_state.bucket_visible_rows.get();
6105 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6106 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6107 } else if self.s3_state.selected_row
6108 >= self.s3_state.bucket_scroll_offset + visible_rows
6109 {
6110 self.s3_state.bucket_scroll_offset =
6111 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6112 }
6113 }
6114 } else if self.view_mode == ViewMode::InsightsResults {
6115 self.insights_state.insights.results_horizontal_scroll = self
6117 .insights_state
6118 .insights
6119 .results_horizontal_scroll
6120 .saturating_sub(1);
6121 } else if self.current_service == Service::CloudWatchLogGroups
6122 && self.view_mode == ViewMode::List
6123 {
6124 if self.log_groups_state.log_groups.has_expanded_item() {
6126 self.log_groups_state.log_groups.collapse();
6127 }
6128 } else if self.current_service == Service::CloudWatchLogGroups
6129 && self.view_mode == ViewMode::Detail
6130 {
6131 if self.log_groups_state.expanded_stream.is_some() {
6133 self.log_groups_state.expanded_stream = None;
6134 }
6135 } else if self.view_mode == ViewMode::Events {
6136 if self.log_groups_state.expanded_event.is_some() {
6138 self.log_groups_state.expanded_event = None;
6139 }
6140 } else if self.current_service == Service::CloudWatchAlarms {
6141 self.alarms_state.table.collapse();
6143 } else if self.current_service == Service::Ec2Instances {
6144 self.ec2_state.table.collapse();
6145 } else if self.current_service == Service::EcrRepositories {
6146 if self.ecr_state.current_repository.is_some() {
6147 self.ecr_state.images.collapse();
6149 } else {
6150 self.ecr_state.repositories.collapse();
6152 }
6153 } else if self.current_service == Service::SqsQueues {
6154 if self.sqs_state.current_queue.is_some()
6155 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6156 {
6157 self.sqs_state.triggers.collapse();
6158 } else if self.sqs_state.current_queue.is_some()
6159 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6160 {
6161 self.sqs_state.pipes.collapse();
6162 } else if self.sqs_state.current_queue.is_some()
6163 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6164 {
6165 self.sqs_state.tags.collapse();
6166 } else if self.sqs_state.current_queue.is_some()
6167 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6168 {
6169 self.sqs_state.subscriptions.collapse();
6170 } else {
6171 self.sqs_state.queues.collapse();
6172 }
6173 } else if self.current_service == Service::LambdaFunctions {
6174 if self.lambda_state.current_function.is_some()
6175 && self.lambda_state.detail_tab == LambdaDetailTab::Code
6176 {
6177 self.lambda_state.layer_expanded = None;
6179 } else if self.lambda_state.current_function.is_some()
6180 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6181 {
6182 self.lambda_state.version_table.collapse();
6184 } else if self.lambda_state.current_function.is_some()
6185 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6186 || (self.lambda_state.current_version.is_some()
6187 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6188 {
6189 self.lambda_state.alias_table.collapse();
6191 } else if self.lambda_state.current_function.is_none() {
6192 self.lambda_state.table.collapse();
6194 }
6195 } else if self.current_service == Service::LambdaApplications {
6196 if self.lambda_application_state.current_application.is_some() {
6197 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
6199 {
6200 self.lambda_application_state.resources.collapse();
6201 } else {
6202 self.lambda_application_state.deployments.collapse();
6203 }
6204 } else {
6205 if self.lambda_application_state.table.has_expanded_item() {
6207 self.lambda_application_state.table.collapse();
6208 }
6209 }
6210 } else if self.current_service == Service::CloudFormationStacks
6211 && self.cfn_state.current_stack.is_none()
6212 {
6213 self.cfn_state.table.collapse();
6214 } else if self.current_service == Service::CloudFormationStacks
6215 && self.cfn_state.current_stack.is_some()
6216 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6217 {
6218 self.cfn_state.parameters.collapse();
6219 } else if self.current_service == Service::CloudFormationStacks
6220 && self.cfn_state.current_stack.is_some()
6221 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6222 {
6223 self.cfn_state.outputs.collapse();
6224 } else if self.current_service == Service::CloudFormationStacks
6225 && self.cfn_state.current_stack.is_some()
6226 && self.cfn_state.detail_tab == CfnDetailTab::Resources
6227 {
6228 self.cfn_state.resources.collapse();
6229 } else if self.current_service == Service::IamUsers {
6230 if self.iam_state.users.has_expanded_item() {
6231 self.iam_state.users.collapse();
6232 }
6233 } else if self.current_service == Service::IamRoles {
6234 if self.view_mode == ViewMode::PolicyView {
6235 self.view_mode = ViewMode::Detail;
6237 self.iam_state.current_policy = None;
6238 self.iam_state.policy_document.clear();
6239 self.iam_state.policy_scroll = 0;
6240 } else if self.iam_state.current_role.is_some() {
6241 if self.iam_state.role_tab == RoleTab::Tags
6242 && self.iam_state.tags.has_expanded_item()
6243 {
6244 self.iam_state.tags.collapse();
6245 } else if self.iam_state.role_tab == RoleTab::LastAccessed
6246 && self
6247 .iam_state
6248 .last_accessed_services
6249 .expanded_item
6250 .is_some()
6251 {
6252 self.iam_state.last_accessed_services.collapse();
6253 } else if self.iam_state.policies.has_expanded_item() {
6254 self.iam_state.policies.collapse();
6255 }
6256 } else if self.iam_state.roles.has_expanded_item() {
6257 self.iam_state.roles.collapse();
6258 }
6259 } else if self.current_service == Service::IamUserGroups {
6260 if self.iam_state.current_group.is_some() {
6261 if self.iam_state.group_tab == GroupTab::Users
6262 && self.iam_state.group_users.has_expanded_item()
6263 {
6264 self.iam_state.group_users.collapse();
6265 } else if self.iam_state.group_tab == GroupTab::Permissions
6266 && self.iam_state.policies.has_expanded_item()
6267 {
6268 self.iam_state.policies.collapse();
6269 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6270 && self
6271 .iam_state
6272 .last_accessed_services
6273 .expanded_item
6274 .is_some()
6275 {
6276 self.iam_state.last_accessed_services.collapse();
6277 }
6278 } else if self.iam_state.groups.has_expanded_item() {
6279 self.iam_state.groups.collapse();
6280 }
6281 }
6282 }
6283
6284 fn select_item(&mut self) {
6285 if self.mode == Mode::RegionPicker {
6286 let filtered = self.get_filtered_regions();
6287 if let Some(region) = filtered.get(self.region_picker_selected) {
6288 if !self.tabs.is_empty() {
6290 let mut session = Session::new(
6291 self.profile.clone(),
6292 self.region.clone(),
6293 self.config.account_id.clone(),
6294 self.config.role_arn.clone(),
6295 );
6296
6297 for tab in &self.tabs {
6298 session.tabs.push(SessionTab {
6299 service: format!("{:?}", tab.service),
6300 title: tab.title.clone(),
6301 breadcrumb: tab.breadcrumb.clone(),
6302 filter: None,
6303 selected_item: None,
6304 });
6305 }
6306
6307 let _ = session.save();
6308 }
6309
6310 self.region = region.code.to_string();
6311 self.config.region = region.code.to_string();
6312
6313 self.tabs.clear();
6315 self.current_tab = 0;
6316 self.service_selected = false;
6317
6318 self.mode = Mode::Normal;
6319 }
6320 } else if self.mode == Mode::ProfilePicker {
6321 let filtered = self.get_filtered_profiles();
6322 if let Some(profile) = filtered.get(self.profile_picker_selected) {
6323 let profile_name = profile.name.clone();
6324 let profile_region = profile.region.clone();
6325
6326 self.profile = profile_name.clone();
6327 std::env::set_var("AWS_PROFILE", &profile_name);
6328
6329 if let Some(region) = profile_region {
6331 self.region = region;
6332 }
6333
6334 self.mode = Mode::Normal;
6335 }
6337 } else if self.mode == Mode::ServicePicker {
6338 let filtered = self.filtered_services();
6339 if let Some(&service) = filtered.get(self.service_picker.selected) {
6340 let new_service = match service {
6341 "CloudWatch > Log Groups" => Service::CloudWatchLogGroups,
6342 "CloudWatch > Logs Insights" => Service::CloudWatchInsights,
6343 "CloudWatch > Alarms" => Service::CloudWatchAlarms,
6344 "CloudFormation > Stacks" => Service::CloudFormationStacks,
6345 "EC2 > Instances" => Service::Ec2Instances,
6346 "ECR > Repositories" => Service::EcrRepositories,
6347 "IAM > Users" => Service::IamUsers,
6348 "IAM > Roles" => Service::IamRoles,
6349 "IAM > User Groups" => Service::IamUserGroups,
6350 "Lambda > Functions" => Service::LambdaFunctions,
6351 "Lambda > Applications" => Service::LambdaApplications,
6352 "S3 > Buckets" => Service::S3Buckets,
6353 "SQS > Queues" => Service::SqsQueues,
6354 _ => return,
6355 };
6356
6357 self.tabs.push(Tab {
6359 service: new_service,
6360 title: service.to_string(),
6361 breadcrumb: service.to_string(),
6362 });
6363 self.current_tab = self.tabs.len() - 1;
6364 self.current_service = new_service;
6365 self.view_mode = ViewMode::List;
6366 self.service_selected = true;
6367 self.mode = Mode::Normal;
6368 }
6369 } else if self.mode == Mode::TabPicker {
6370 let filtered = self.get_filtered_tabs();
6371 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
6372 self.current_tab = idx;
6373 self.current_service = self.tabs[idx].service;
6374 self.mode = Mode::Normal;
6375 self.tab_filter.clear();
6376 }
6377 } else if self.mode == Mode::SessionPicker {
6378 let filtered = self.get_filtered_sessions();
6379 if let Some(&session) = filtered.get(self.session_picker_selected) {
6380 let session = session.clone();
6381
6382 self.current_session = Some(session.clone());
6384 self.profile = session.profile.clone();
6385 self.region = session.region.clone();
6386 self.config.region = session.region.clone();
6387 self.config.account_id = session.account_id.clone();
6388 self.config.role_arn = session.role_arn.clone();
6389
6390 self.tabs = session
6392 .tabs
6393 .iter()
6394 .map(|st| Tab {
6395 service: match st.service.as_str() {
6396 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
6397 "CloudWatchInsights" => Service::CloudWatchInsights,
6398 "CloudWatchAlarms" => Service::CloudWatchAlarms,
6399 "S3Buckets" => Service::S3Buckets,
6400 "SqsQueues" => Service::SqsQueues,
6401 _ => Service::CloudWatchLogGroups,
6402 },
6403 title: st.title.clone(),
6404 breadcrumb: st.breadcrumb.clone(),
6405 })
6406 .collect();
6407
6408 if !self.tabs.is_empty() {
6409 self.current_tab = 0;
6410 self.current_service = self.tabs[0].service;
6411 self.service_selected = true;
6412 }
6413
6414 self.mode = Mode::Normal;
6415 }
6416 } else if self.mode == Mode::InsightsInput {
6417 use crate::app::InsightsFocus;
6419 match self.insights_state.insights.insights_focus {
6420 InsightsFocus::Query => {
6421 self.insights_state.insights.query_text.push('\n');
6423 self.insights_state.insights.query_cursor_line += 1;
6424 self.insights_state.insights.query_cursor_col = 0;
6425 }
6426 InsightsFocus::LogGroupSearch => {
6427 self.insights_state.insights.show_dropdown =
6429 !self.insights_state.insights.show_dropdown;
6430 }
6431 _ => {}
6432 }
6433 } else if self.mode == Mode::Normal {
6434 if !self.service_selected {
6436 let filtered = self.filtered_services();
6437 if let Some(&service) = filtered.get(self.service_picker.selected) {
6438 match service {
6439 "CloudWatch > Log Groups" => {
6440 self.current_service = Service::CloudWatchLogGroups;
6441 self.view_mode = ViewMode::List;
6442 self.service_selected = true;
6443 }
6444 "CloudWatch > Logs Insights" => {
6445 self.current_service = Service::CloudWatchInsights;
6446 self.view_mode = ViewMode::InsightsResults;
6447 self.service_selected = true;
6448 }
6449 "CloudWatch > Alarms" => {
6450 self.current_service = Service::CloudWatchAlarms;
6451 self.view_mode = ViewMode::List;
6452 self.service_selected = true;
6453 }
6454 "S3 > Buckets" => {
6455 self.current_service = Service::S3Buckets;
6456 self.view_mode = ViewMode::List;
6457 self.service_selected = true;
6458 }
6459 "EC2 > Instances" => {
6460 self.current_service = Service::Ec2Instances;
6461 self.view_mode = ViewMode::List;
6462 self.service_selected = true;
6463 }
6464 "ECR > Repositories" => {
6465 self.current_service = Service::EcrRepositories;
6466 self.view_mode = ViewMode::List;
6467 self.service_selected = true;
6468 }
6469 "Lambda > Functions" => {
6470 self.current_service = Service::LambdaFunctions;
6471 self.view_mode = ViewMode::List;
6472 self.service_selected = true;
6473 }
6474 "Lambda > Applications" => {
6475 self.current_service = Service::LambdaApplications;
6476 self.view_mode = ViewMode::List;
6477 self.service_selected = true;
6478 }
6479 _ => {}
6480 }
6481 }
6482 return;
6483 }
6484
6485 if self.view_mode == ViewMode::InsightsResults {
6487 if self.insights_state.insights.expanded_result
6489 == Some(self.insights_state.insights.results_selected)
6490 {
6491 self.insights_state.insights.expanded_result = None;
6492 } else {
6493 self.insights_state.insights.expanded_result =
6494 Some(self.insights_state.insights.results_selected);
6495 }
6496 } else if self.current_service == Service::S3Buckets {
6497 if self.s3_state.current_bucket.is_none() {
6498 let mut row_idx = 0;
6500 for bucket in &self.s3_state.buckets.items {
6501 if row_idx == self.s3_state.selected_row {
6502 self.s3_state.current_bucket = Some(bucket.name.clone());
6504 self.s3_state.prefix_stack.clear();
6505 self.s3_state.buckets.loading = true;
6506 return;
6507 }
6508 row_idx += 1;
6509
6510 if self.s3_state.bucket_errors.contains_key(&bucket.name)
6512 && self.s3_state.expanded_prefixes.contains(&bucket.name)
6513 {
6514 continue;
6515 }
6516
6517 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6518 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6519 for obj in preview {
6520 if row_idx == self.s3_state.selected_row {
6521 if obj.is_prefix {
6523 self.s3_state.current_bucket =
6524 Some(bucket.name.clone());
6525 self.s3_state.prefix_stack = vec![obj.key.clone()];
6526 self.s3_state.buckets.loading = true;
6527 }
6528 return;
6529 }
6530 row_idx += 1;
6531
6532 if obj.is_prefix
6534 && self.s3_state.expanded_prefixes.contains(&obj.key)
6535 {
6536 if let Some(nested) =
6537 self.s3_state.prefix_preview.get(&obj.key)
6538 {
6539 for nested_obj in nested {
6540 if row_idx == self.s3_state.selected_row {
6541 if nested_obj.is_prefix {
6543 self.s3_state.current_bucket =
6544 Some(bucket.name.clone());
6545 self.s3_state.prefix_stack = vec![
6547 obj.key.clone(),
6548 nested_obj.key.clone(),
6549 ];
6550 self.s3_state.buckets.loading = true;
6551 }
6552 return;
6553 }
6554 row_idx += 1;
6555 }
6556 } else {
6557 row_idx += 1;
6558 }
6559 }
6560 }
6561 } else {
6562 row_idx += 1;
6563 }
6564 }
6565 }
6566 } else {
6567 let mut visual_idx = 0;
6569 let mut found_obj: Option<S3Object> = None;
6570
6571 fn check_nested_select(
6573 obj: &S3Object,
6574 visual_idx: &mut usize,
6575 target_idx: usize,
6576 expanded_prefixes: &std::collections::HashSet<String>,
6577 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6578 found_obj: &mut Option<S3Object>,
6579 ) {
6580 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6581 if let Some(preview) = prefix_preview.get(&obj.key) {
6582 for nested_obj in preview {
6583 if *visual_idx == target_idx {
6584 *found_obj = Some(nested_obj.clone());
6585 return;
6586 }
6587 *visual_idx += 1;
6588
6589 check_nested_select(
6591 nested_obj,
6592 visual_idx,
6593 target_idx,
6594 expanded_prefixes,
6595 prefix_preview,
6596 found_obj,
6597 );
6598 if found_obj.is_some() {
6599 return;
6600 }
6601 }
6602 } else {
6603 *visual_idx += 1;
6605 }
6606 }
6607 }
6608
6609 for obj in &self.s3_state.objects {
6610 if visual_idx == self.s3_state.selected_object {
6611 found_obj = Some(obj.clone());
6612 break;
6613 }
6614 visual_idx += 1;
6615
6616 check_nested_select(
6618 obj,
6619 &mut visual_idx,
6620 self.s3_state.selected_object,
6621 &self.s3_state.expanded_prefixes,
6622 &self.s3_state.prefix_preview,
6623 &mut found_obj,
6624 );
6625 if found_obj.is_some() {
6626 break;
6627 }
6628 }
6629
6630 if let Some(obj) = found_obj {
6631 if obj.is_prefix {
6632 self.s3_state.prefix_stack.push(obj.key.clone());
6634 self.s3_state.buckets.loading = true;
6635 }
6636 }
6637 }
6638 } else if self.current_service == Service::CloudFormationStacks {
6639 if self.cfn_state.current_stack.is_none() {
6640 let filtered_stacks = self.filtered_cloudformation_stacks();
6642 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
6643 let stack_name = stack.name.clone();
6644 let mut tags = stack.tags.clone();
6645 tags.sort_by(|a, b| a.0.cmp(&b.0));
6646
6647 self.cfn_state.current_stack = Some(stack_name);
6648 self.cfn_state.tags.items = tags;
6649 self.cfn_state.tags.reset();
6650 self.cfn_state.table.loading = true;
6651 self.update_current_tab_breadcrumb();
6652 }
6653 }
6654 } else if self.current_service == Service::EcrRepositories {
6655 if self.ecr_state.current_repository.is_none() {
6656 let filtered_repos = self.filtered_ecr_repositories();
6658 if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
6659 let repo_name = repo.name.clone();
6660 let repo_uri = repo.uri.clone();
6661 self.ecr_state.current_repository = Some(repo_name);
6662 self.ecr_state.current_repository_uri = Some(repo_uri);
6663 self.ecr_state.images.reset();
6664 self.ecr_state.repositories.loading = true;
6665 }
6666 }
6667 } else if self.current_service == Service::SqsQueues {
6668 if self.sqs_state.current_queue.is_none() {
6669 let filtered_queues = crate::ui::sqs::filtered_queues(
6670 &self.sqs_state.queues.items,
6671 &self.sqs_state.queues.filter,
6672 );
6673 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
6674 self.sqs_state.current_queue = Some(queue.url.clone());
6675
6676 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
6677 self.sqs_state.metrics_loading = true;
6678 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
6679 self.sqs_state.triggers.loading = true;
6680 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
6681 self.sqs_state.pipes.loading = true;
6682 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
6683 self.sqs_state.tags.loading = true;
6684 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
6685 self.sqs_state.subscriptions.loading = true;
6686 }
6687 }
6688 }
6689 } else if self.current_service == Service::IamUsers {
6690 if self.iam_state.current_user.is_some() {
6691 if self.iam_state.user_tab != UserTab::Tags {
6693 let filtered = crate::ui::iam::filtered_iam_policies(self);
6694 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
6695 self.iam_state.current_policy = Some(policy.policy_name.clone());
6696 self.iam_state.policy_scroll = 0;
6697 self.view_mode = ViewMode::PolicyView;
6698 self.iam_state.policies.loading = true;
6699 self.update_current_tab_breadcrumb();
6700 }
6701 }
6702 } else if self.iam_state.current_user.is_none() {
6703 let filtered_users = crate::ui::iam::filtered_iam_users(self);
6704 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
6705 self.iam_state.current_user = Some(user.user_name.clone());
6706 self.iam_state.user_tab = UserTab::Permissions;
6707 self.iam_state.policies.reset();
6708 self.update_current_tab_breadcrumb();
6709 }
6710 }
6711 } else if self.current_service == Service::IamRoles {
6712 if self.iam_state.current_role.is_some() {
6713 if self.iam_state.role_tab != RoleTab::Tags {
6715 let filtered = crate::ui::iam::filtered_iam_policies(self);
6716 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
6717 self.iam_state.current_policy = Some(policy.policy_name.clone());
6718 self.iam_state.policy_scroll = 0;
6719 self.view_mode = ViewMode::PolicyView;
6720 self.iam_state.policies.loading = true;
6721 self.update_current_tab_breadcrumb();
6722 }
6723 }
6724 } else if self.iam_state.current_role.is_none() {
6725 let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
6726 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
6727 self.iam_state.current_role = Some(role.role_name.clone());
6728 self.iam_state.role_tab = RoleTab::Permissions;
6729 self.iam_state.policies.reset();
6730 self.update_current_tab_breadcrumb();
6731 }
6732 }
6733 } else if self.current_service == Service::IamUserGroups {
6734 if self.iam_state.current_group.is_none() {
6735 let filtered_groups: Vec<_> = self
6736 .iam_state
6737 .groups
6738 .items
6739 .iter()
6740 .filter(|g| {
6741 if self.iam_state.groups.filter.is_empty() {
6742 true
6743 } else {
6744 g.group_name
6745 .to_lowercase()
6746 .contains(&self.iam_state.groups.filter.to_lowercase())
6747 }
6748 })
6749 .collect();
6750 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
6751 self.iam_state.current_group = Some(group.group_name.clone());
6752 self.update_current_tab_breadcrumb();
6753 }
6754 }
6755 } else if self.current_service == Service::LambdaFunctions {
6756 if self.lambda_state.current_function.is_some()
6757 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6758 {
6759 if self.mode == Mode::Normal {
6762 let page_size = self.lambda_state.version_table.page_size.value();
6763 let filtered: Vec<_> = self
6764 .lambda_state
6765 .version_table
6766 .items
6767 .iter()
6768 .filter(|v| {
6769 self.lambda_state.version_table.filter.is_empty()
6770 || v.version.to_lowercase().contains(
6771 &self.lambda_state.version_table.filter.to_lowercase(),
6772 )
6773 || v.aliases.to_lowercase().contains(
6774 &self.lambda_state.version_table.filter.to_lowercase(),
6775 )
6776 })
6777 .collect();
6778 let current_page = self.lambda_state.version_table.selected / page_size;
6779 let start_idx = current_page * page_size;
6780 let end_idx = (start_idx + page_size).min(filtered.len());
6781 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
6782 let page_index = self.lambda_state.version_table.selected % page_size;
6783 if let Some(version) = paginated.get(page_index) {
6784 self.lambda_state.current_version = Some(version.version.clone());
6785 self.lambda_state.detail_tab = LambdaDetailTab::Code;
6786 }
6787 } else {
6788 if self.lambda_state.version_table.expanded_item
6790 == Some(self.lambda_state.version_table.selected)
6791 {
6792 self.lambda_state.version_table.collapse();
6793 } else {
6794 self.lambda_state.version_table.expanded_item =
6795 Some(self.lambda_state.version_table.selected);
6796 }
6797 }
6798 } else if self.lambda_state.current_function.is_some()
6799 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6800 {
6801 let filtered: Vec<_> = self
6803 .lambda_state
6804 .alias_table
6805 .items
6806 .iter()
6807 .filter(|a| {
6808 self.lambda_state.alias_table.filter.is_empty()
6809 || a.name
6810 .to_lowercase()
6811 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6812 || a.versions
6813 .to_lowercase()
6814 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6815 })
6816 .collect();
6817 if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
6818 self.lambda_state.current_alias = Some(alias.name.clone());
6819 }
6820 } else if self.lambda_state.current_function.is_none() {
6821 let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
6822 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
6823 self.lambda_state.current_function = Some(func.name.clone());
6824 self.lambda_state.detail_tab = LambdaDetailTab::Code;
6825 self.update_current_tab_breadcrumb();
6826 }
6827 }
6828 } else if self.current_service == Service::LambdaApplications {
6829 let filtered = crate::ui::lambda::filtered_lambda_applications(self);
6830 if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
6831 let app_name = app.name.clone();
6832 self.lambda_application_state.current_application = Some(app_name.clone());
6833 self.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Overview;
6834
6835 use crate::lambda::Resource;
6837 self.lambda_application_state.resources.items = vec![
6838 Resource {
6839 logical_id: "ApiGatewayRestApi".to_string(),
6840 physical_id: "abc123xyz".to_string(),
6841 resource_type: "AWS::ApiGateway::RestApi".to_string(),
6842 last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
6843 },
6844 Resource {
6845 logical_id: "LambdaFunction".to_string(),
6846 physical_id: format!("{}-function", app_name),
6847 resource_type: "AWS::Lambda::Function".to_string(),
6848 last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
6849 },
6850 Resource {
6851 logical_id: "DynamoDBTable".to_string(),
6852 physical_id: format!("{}-table", app_name),
6853 resource_type: "AWS::DynamoDB::Table".to_string(),
6854 last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
6855 },
6856 ];
6857
6858 use crate::lambda::Deployment;
6860 self.lambda_application_state.deployments.items = vec![
6861 Deployment {
6862 deployment_id: "d-ABC123XYZ".to_string(),
6863 resource_type: "AWS::Serverless::Application".to_string(),
6864 last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
6865 status: "Succeeded".to_string(),
6866 },
6867 Deployment {
6868 deployment_id: "d-DEF456UVW".to_string(),
6869 resource_type: "AWS::Serverless::Application".to_string(),
6870 last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
6871 status: "Succeeded".to_string(),
6872 },
6873 ];
6874
6875 self.update_current_tab_breadcrumb();
6876 }
6877 } else if self.current_service == Service::CloudWatchLogGroups {
6878 if self.view_mode == ViewMode::List {
6879 let filtered_groups = self.filtered_log_groups();
6881 if let Some(selected_group) =
6882 filtered_groups.get(self.log_groups_state.log_groups.selected)
6883 {
6884 if let Some(actual_idx) = self
6885 .log_groups_state
6886 .log_groups
6887 .items
6888 .iter()
6889 .position(|g| g.name == selected_group.name)
6890 {
6891 self.log_groups_state.log_groups.selected = actual_idx;
6892 }
6893 }
6894 self.view_mode = ViewMode::Detail;
6895 self.log_groups_state.log_streams.clear();
6896 self.log_groups_state.selected_stream = 0;
6897 self.log_groups_state.loading = true;
6898 self.update_current_tab_breadcrumb();
6899 } else if self.view_mode == ViewMode::Detail {
6900 let filtered_streams = self.filtered_log_streams();
6902 if let Some(selected_stream) =
6903 filtered_streams.get(self.log_groups_state.selected_stream)
6904 {
6905 if let Some(actual_idx) = self
6906 .log_groups_state
6907 .log_streams
6908 .iter()
6909 .position(|s| s.name == selected_stream.name)
6910 {
6911 self.log_groups_state.selected_stream = actual_idx;
6912 }
6913 }
6914 self.view_mode = ViewMode::Events;
6915 self.update_current_tab_breadcrumb();
6916 self.log_groups_state.log_events.clear();
6917 self.log_groups_state.event_scroll_offset = 0;
6918 self.log_groups_state.next_backward_token = None;
6919 self.log_groups_state.loading = true;
6920 } else if self.view_mode == ViewMode::Events {
6921 if self.log_groups_state.expanded_event
6923 == Some(self.log_groups_state.event_scroll_offset)
6924 {
6925 self.log_groups_state.expanded_event = None;
6926 } else {
6927 self.log_groups_state.expanded_event =
6928 Some(self.log_groups_state.event_scroll_offset);
6929 }
6930 }
6931 } else if self.current_service == Service::CloudWatchAlarms {
6932 self.alarms_state.table.toggle_expand();
6934 } else if self.current_service == Service::CloudWatchInsights {
6935 if !self.insights_state.insights.selected_log_groups.is_empty() {
6937 self.log_groups_state.loading = true;
6938 self.insights_state.insights.query_completed = true;
6939 }
6940 }
6941 }
6942 }
6943
6944 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
6945 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
6946 Ok(())
6947 }
6948
6949 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
6950 let alarms = self.alarms_client.list_alarms().await?;
6951 self.alarms_state.table.items = alarms
6952 .into_iter()
6953 .map(
6954 |(
6955 name,
6956 state,
6957 state_updated,
6958 description,
6959 metric_name,
6960 namespace,
6961 statistic,
6962 period,
6963 comparison,
6964 threshold,
6965 actions_enabled,
6966 state_reason,
6967 resource,
6968 dimensions,
6969 expression,
6970 alarm_type,
6971 cross_account,
6972 )| Alarm {
6973 name,
6974 state,
6975 state_updated_timestamp: state_updated,
6976 description,
6977 metric_name,
6978 namespace,
6979 statistic,
6980 period,
6981 comparison_operator: comparison,
6982 threshold,
6983 actions_enabled,
6984 state_reason,
6985 resource,
6986 dimensions,
6987 expression,
6988 alarm_type,
6989 cross_account,
6990 },
6991 )
6992 .collect();
6993 Ok(())
6994 }
6995
6996 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
6997 if let Some(bucket_name) = &self.s3_state.current_bucket {
6998 let bucket_region = if let Some(bucket) = self
7000 .s3_state
7001 .buckets
7002 .items
7003 .iter_mut()
7004 .find(|b| &b.name == bucket_name)
7005 {
7006 if bucket.region.is_empty() {
7007 let region = self.s3_client.get_bucket_location(bucket_name).await?;
7009 bucket.region = region.clone();
7010 region
7011 } else {
7012 bucket.region.clone()
7013 }
7014 } else {
7015 self.config.region.clone()
7016 };
7017
7018 let prefix = self
7019 .s3_state
7020 .prefix_stack
7021 .last()
7022 .cloned()
7023 .unwrap_or_default();
7024 let objects = self
7025 .s3_client
7026 .list_objects(bucket_name, &bucket_region, &prefix)
7027 .await?;
7028 self.s3_state.objects = objects
7029 .into_iter()
7030 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
7031 key,
7032 size,
7033 last_modified: modified,
7034 is_prefix,
7035 storage_class,
7036 })
7037 .collect();
7038 self.s3_state.selected_object = 0;
7039 }
7040 Ok(())
7041 }
7042
7043 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
7044 let bucket_region = self
7045 .s3_state
7046 .buckets
7047 .items
7048 .iter()
7049 .find(|b| b.name == bucket_name)
7050 .and_then(|b| {
7051 if b.region.is_empty() {
7052 None
7053 } else {
7054 Some(b.region.as_str())
7055 }
7056 })
7057 .unwrap_or(self.config.region.as_str());
7058 let objects = self
7059 .s3_client
7060 .list_objects(&bucket_name, bucket_region, "")
7061 .await?;
7062 let preview: Vec<S3Object> = objects
7063 .into_iter()
7064 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
7065 key,
7066 size,
7067 last_modified: modified,
7068 is_prefix,
7069 storage_class,
7070 })
7071 .collect();
7072 self.s3_state.bucket_preview.insert(bucket_name, preview);
7073 Ok(())
7074 }
7075
7076 pub async fn load_prefix_preview(
7077 &mut self,
7078 bucket_name: String,
7079 prefix: String,
7080 ) -> anyhow::Result<()> {
7081 let bucket_region = self
7082 .s3_state
7083 .buckets
7084 .items
7085 .iter()
7086 .find(|b| b.name == bucket_name)
7087 .and_then(|b| {
7088 if b.region.is_empty() {
7089 None
7090 } else {
7091 Some(b.region.as_str())
7092 }
7093 })
7094 .unwrap_or(self.config.region.as_str());
7095 let objects = self
7096 .s3_client
7097 .list_objects(&bucket_name, bucket_region, &prefix)
7098 .await?;
7099 let preview: Vec<S3Object> = objects
7100 .into_iter()
7101 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
7102 key,
7103 size,
7104 last_modified: modified,
7105 is_prefix,
7106 storage_class,
7107 })
7108 .collect();
7109 self.s3_state.prefix_preview.insert(prefix, preview);
7110 Ok(())
7111 }
7112
7113 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
7114 let repos = match self.ecr_state.tab {
7115 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
7116 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
7117 };
7118
7119 self.ecr_state.repositories.items = repos
7120 .into_iter()
7121 .map(|r| EcrRepository {
7122 name: r.name,
7123 uri: r.uri,
7124 created_at: r.created_at,
7125 tag_immutability: r.tag_immutability,
7126 encryption_type: r.encryption_type,
7127 })
7128 .collect();
7129
7130 self.ecr_state
7131 .repositories
7132 .items
7133 .sort_by(|a, b| a.name.cmp(&b.name));
7134 Ok(())
7135 }
7136
7137 pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
7138 let instances = self.ec2_client.list_instances().await?;
7139
7140 self.ec2_state.table.items = instances
7141 .into_iter()
7142 .map(|i| Ec2Instance {
7143 instance_id: i.instance_id,
7144 name: i.name,
7145 state: i.state,
7146 instance_type: i.instance_type,
7147 availability_zone: i.availability_zone,
7148 public_ipv4_dns: i.public_ipv4_dns,
7149 public_ipv4_address: i.public_ipv4_address,
7150 elastic_ip: i.elastic_ip,
7151 ipv6_ips: i.ipv6_ips,
7152 monitoring: i.monitoring,
7153 security_groups: i.security_groups,
7154 key_name: i.key_name,
7155 launch_time: i.launch_time,
7156 platform_details: i.platform_details,
7157 status_checks: i.status_checks,
7158 alarm_status: i.alarm_status,
7159 private_dns_name: String::new(),
7160 private_ip_address: String::new(),
7161 security_group_ids: String::new(),
7162 owner_id: String::new(),
7163 volume_id: String::new(),
7164 root_device_name: String::new(),
7165 root_device_type: String::new(),
7166 ebs_optimized: String::new(),
7167 image_id: String::new(),
7168 kernel_id: String::new(),
7169 ramdisk_id: String::new(),
7170 ami_launch_index: String::new(),
7171 reservation_id: String::new(),
7172 vpc_id: String::new(),
7173 subnet_ids: String::new(),
7174 instance_lifecycle: String::new(),
7175 architecture: String::new(),
7176 virtualization_type: String::new(),
7177 platform: String::new(),
7178 iam_instance_profile_arn: String::new(),
7179 tenancy: String::new(),
7180 affinity: String::new(),
7181 host_id: String::new(),
7182 placement_group: String::new(),
7183 partition_number: String::new(),
7184 capacity_reservation_id: String::new(),
7185 state_transition_reason_code: String::new(),
7186 state_transition_reason_message: String::new(),
7187 stop_hibernation_behavior: String::new(),
7188 outpost_arn: String::new(),
7189 product_codes: String::new(),
7190 availability_zone_id: String::new(),
7191 imdsv2: String::new(),
7192 usage_operation: String::new(),
7193 managed: String::new(),
7194 operator: String::new(),
7195 })
7196 .collect();
7197
7198 self.ec2_state
7200 .table
7201 .items
7202 .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
7203 Ok(())
7204 }
7205
7206 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
7207 if let Some(repo_name) = &self.ecr_state.current_repository {
7208 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
7209 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
7210
7211 self.ecr_state.images.items = images
7212 .into_iter()
7213 .map(|i| EcrImage {
7214 tag: i.tag,
7215 artifact_type: i.artifact_type,
7216 pushed_at: i.pushed_at,
7217 size_bytes: i.size_bytes,
7218 uri: i.uri,
7219 digest: i.digest,
7220 last_pull_time: i.last_pull_time,
7221 })
7222 .collect();
7223
7224 self.ecr_state
7225 .images
7226 .items
7227 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
7228 }
7229 }
7230 Ok(())
7231 }
7232
7233 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
7234 let stacks = self
7235 .cloudformation_client
7236 .list_stacks(self.cfn_state.view_nested)
7237 .await?;
7238
7239 let mut stacks: Vec<CfnStack> = stacks
7240 .into_iter()
7241 .map(|s| CfnStack {
7242 name: s.name,
7243 stack_id: s.stack_id,
7244 status: s.status,
7245 created_time: s.created_time,
7246 updated_time: s.updated_time,
7247 deleted_time: s.deleted_time,
7248 drift_status: s.drift_status,
7249 last_drift_check_time: s.last_drift_check_time,
7250 status_reason: s.status_reason,
7251 description: s.description,
7252 detailed_status: String::new(),
7253 root_stack: String::new(),
7254 parent_stack: String::new(),
7255 termination_protection: false,
7256 iam_role: String::new(),
7257 tags: Vec::new(),
7258 stack_policy: String::new(),
7259 rollback_monitoring_time: String::new(),
7260 rollback_alarms: Vec::new(),
7261 notification_arns: Vec::new(),
7262 })
7263 .collect();
7264
7265 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
7267
7268 self.cfn_state.table.items = stacks;
7269
7270 Ok(())
7271 }
7272
7273 pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
7274 let template = self.cloudformation_client.get_template(stack_name).await?;
7275 self.cfn_state.template_body = template;
7276 self.cfn_state.template_scroll = 0;
7277 Ok(())
7278 }
7279
7280 pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
7281 let mut parameters = self
7282 .cloudformation_client
7283 .get_stack_parameters(stack_name)
7284 .await?;
7285 parameters.sort_by(|a, b| a.key.cmp(&b.key));
7286 self.cfn_state.parameters.items = parameters;
7287 self.cfn_state.parameters.reset();
7288 Ok(())
7289 }
7290
7291 pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
7292 let outputs = self
7293 .cloudformation_client
7294 .get_stack_outputs(stack_name)
7295 .await?;
7296 self.cfn_state.outputs.items = outputs;
7297 self.cfn_state.outputs.reset();
7298 Ok(())
7299 }
7300
7301 pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
7302 let resources = self
7303 .cloudformation_client
7304 .get_stack_resources(stack_name)
7305 .await?;
7306 self.cfn_state.resources.items = resources;
7307 self.cfn_state.resources.reset();
7308 Ok(())
7309 }
7310
7311 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
7312 let attached_policies = self
7314 .iam_client
7315 .list_attached_role_policies(role_name)
7316 .await
7317 .map_err(|e| anyhow::anyhow!(e))?;
7318
7319 let mut policies: Vec<crate::iam::Policy> = attached_policies
7320 .into_iter()
7321 .map(|p| crate::iam::Policy {
7322 policy_name: p.policy_name().unwrap_or("").to_string(),
7323 policy_type: "Managed".to_string(),
7324 attached_via: "Direct".to_string(),
7325 attached_entities: "-".to_string(),
7326 description: "-".to_string(),
7327 creation_time: "-".to_string(),
7328 edited_time: "-".to_string(),
7329 policy_arn: p.policy_arn().map(|s| s.to_string()),
7330 })
7331 .collect();
7332
7333 let inline_policy_names = self
7335 .iam_client
7336 .list_role_policies(role_name)
7337 .await
7338 .map_err(|e| anyhow::anyhow!(e))?;
7339
7340 for policy_name in inline_policy_names {
7341 policies.push(crate::iam::Policy {
7342 policy_name,
7343 policy_type: "Inline".to_string(),
7344 attached_via: "Direct".to_string(),
7345 attached_entities: "-".to_string(),
7346 description: "-".to_string(),
7347 creation_time: "-".to_string(),
7348 edited_time: "-".to_string(),
7349 policy_arn: None,
7350 });
7351 }
7352
7353 self.iam_state.policies.items = policies;
7354
7355 Ok(())
7356 }
7357
7358 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
7359 let attached_policies = self
7360 .iam_client
7361 .list_attached_group_policies(group_name)
7362 .await
7363 .map_err(|e| anyhow::anyhow!(e))?;
7364
7365 let mut policies: Vec<crate::iam::Policy> = attached_policies
7366 .into_iter()
7367 .map(|p| crate::iam::Policy {
7368 policy_name: p.policy_name().unwrap_or("").to_string(),
7369 policy_type: "AWS managed".to_string(),
7370 attached_via: "Direct".to_string(),
7371 attached_entities: "-".to_string(),
7372 description: "-".to_string(),
7373 creation_time: "-".to_string(),
7374 edited_time: "-".to_string(),
7375 policy_arn: p.policy_arn().map(|s| s.to_string()),
7376 })
7377 .collect();
7378
7379 let inline_policy_names = self
7380 .iam_client
7381 .list_group_policies(group_name)
7382 .await
7383 .map_err(|e| anyhow::anyhow!(e))?;
7384
7385 for policy_name in inline_policy_names {
7386 policies.push(crate::iam::Policy {
7387 policy_name,
7388 policy_type: "Inline".to_string(),
7389 attached_via: "Direct".to_string(),
7390 attached_entities: "-".to_string(),
7391 description: "-".to_string(),
7392 creation_time: "-".to_string(),
7393 edited_time: "-".to_string(),
7394 policy_arn: None,
7395 });
7396 }
7397
7398 self.iam_state.policies.items = policies;
7399
7400 Ok(())
7401 }
7402
7403 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
7404 let users = self
7405 .iam_client
7406 .get_group_users(group_name)
7407 .await
7408 .map_err(|e| anyhow::anyhow!(e))?;
7409
7410 let group_users: Vec<crate::iam::GroupUser> = users
7411 .into_iter()
7412 .map(|u| {
7413 let creation_time = {
7414 let dt = u.create_date();
7415 let timestamp = dt.secs();
7416 let datetime =
7417 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
7418 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
7419 };
7420
7421 crate::iam::GroupUser {
7422 user_name: u.user_name().to_string(),
7423 groups: String::new(),
7424 last_activity: String::new(),
7425 creation_time,
7426 }
7427 })
7428 .collect();
7429
7430 self.iam_state.group_users.items = group_users;
7431
7432 Ok(())
7433 }
7434
7435 pub async fn load_policy_document(
7436 &mut self,
7437 role_name: &str,
7438 policy_name: &str,
7439 ) -> anyhow::Result<()> {
7440 let policy = self
7442 .iam_state
7443 .policies
7444 .items
7445 .iter()
7446 .find(|p| p.policy_name == policy_name)
7447 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
7448
7449 let document = if let Some(policy_arn) = &policy.policy_arn {
7450 self.iam_client
7452 .get_policy_version(policy_arn)
7453 .await
7454 .map_err(|e| anyhow::anyhow!(e))?
7455 } else {
7456 self.iam_client
7458 .get_role_policy(role_name, policy_name)
7459 .await
7460 .map_err(|e| anyhow::anyhow!(e))?
7461 };
7462
7463 self.iam_state.policy_document = document;
7464
7465 Ok(())
7466 }
7467
7468 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
7469 let document = self
7470 .iam_client
7471 .get_role(role_name)
7472 .await
7473 .map_err(|e| anyhow::anyhow!(e))?;
7474
7475 self.iam_state.trust_policy_document = document;
7476
7477 Ok(())
7478 }
7479
7480 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
7481 self.iam_state.last_accessed_services.items = vec![];
7483 self.iam_state.last_accessed_services.selected = 0;
7484
7485 Ok(())
7486 }
7487
7488 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
7489 let tags = self
7490 .iam_client
7491 .list_role_tags(role_name)
7492 .await
7493 .map_err(|e| anyhow::anyhow!(e))?;
7494 self.iam_state.tags.items = tags
7495 .into_iter()
7496 .map(|(k, v)| crate::iam::RoleTag { key: k, value: v })
7497 .collect();
7498 self.iam_state.tags.reset();
7499 Ok(())
7500 }
7501
7502 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
7503 let tags = self
7504 .iam_client
7505 .list_user_tags(user_name)
7506 .await
7507 .map_err(|e| anyhow::anyhow!(e))?;
7508 self.iam_state.user_tags.items = tags
7509 .into_iter()
7510 .map(|(k, v)| crate::iam::UserTag { key: k, value: v })
7511 .collect();
7512 self.iam_state.user_tags.reset();
7513 Ok(())
7514 }
7515
7516 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
7517 if let Some(group) = self
7518 .log_groups_state
7519 .log_groups
7520 .items
7521 .get(self.log_groups_state.log_groups.selected)
7522 {
7523 self.log_groups_state.log_streams =
7524 self.cloudwatch_client.list_log_streams(&group.name).await?;
7525 self.log_groups_state.selected_stream = 0;
7526 }
7527 Ok(())
7528 }
7529
7530 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
7531 if let Some(group) = self
7532 .log_groups_state
7533 .log_groups
7534 .items
7535 .get(self.log_groups_state.log_groups.selected)
7536 {
7537 if let Some(stream) = self
7538 .log_groups_state
7539 .log_streams
7540 .get(self.log_groups_state.selected_stream)
7541 {
7542 let (start_time, end_time) =
7544 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
7545 let now = chrono::Utc::now().timestamp_millis();
7546 let duration_ms = match self.log_groups_state.relative_unit {
7547 crate::app::TimeUnit::Minutes => amount * 60 * 1000,
7548 crate::app::TimeUnit::Hours => amount * 60 * 60 * 1000,
7549 crate::app::TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
7550 crate::app::TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
7551 };
7552 (Some(now - duration_ms), Some(now))
7553 } else {
7554 (None, None)
7555 };
7556
7557 let (mut events, has_more, token) = self
7558 .cloudwatch_client
7559 .get_log_events(
7560 &group.name,
7561 &stream.name,
7562 self.log_groups_state.next_backward_token.clone(),
7563 start_time,
7564 end_time,
7565 )
7566 .await?;
7567
7568 if self.log_groups_state.next_backward_token.is_some() {
7569 events.append(&mut self.log_groups_state.log_events);
7571 self.log_groups_state.event_scroll_offset = 0;
7572 } else {
7573 self.log_groups_state.event_scroll_offset = 0;
7575 }
7576
7577 self.log_groups_state.log_events = events;
7578 self.log_groups_state.has_older_events =
7579 has_more && self.log_groups_state.log_events.len() >= 25;
7580 self.log_groups_state.next_backward_token = token;
7581 self.log_groups_state.selected_event = 0;
7582 }
7583 }
7584 Ok(())
7585 }
7586
7587 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
7588 if self.insights_state.insights.selected_log_groups.is_empty() {
7589 return Err(anyhow::anyhow!(
7590 "No log groups selected. Please select at least one log group."
7591 ));
7592 }
7593
7594 let now = chrono::Utc::now().timestamp_millis();
7595 let amount = self
7596 .insights_state
7597 .insights
7598 .insights_relative_amount
7599 .parse::<i64>()
7600 .unwrap_or(1);
7601 let duration_ms = match self.insights_state.insights.insights_relative_unit {
7602 TimeUnit::Minutes => amount * 60 * 1000,
7603 TimeUnit::Hours => amount * 60 * 60 * 1000,
7604 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
7605 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
7606 };
7607 let start_time = now - duration_ms;
7608
7609 let query_id = self
7610 .cloudwatch_client
7611 .start_query(
7612 self.insights_state.insights.selected_log_groups.clone(),
7613 self.insights_state.insights.query_text.trim().to_string(),
7614 start_time,
7615 now,
7616 )
7617 .await?;
7618
7619 for _ in 0..60 {
7621 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
7622 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
7623
7624 if status == "Complete" {
7625 self.insights_state.insights.query_results = results;
7626 self.insights_state.insights.query_completed = true;
7627 self.insights_state.insights.results_selected = 0;
7628 self.insights_state.insights.expanded_result = None;
7629 self.view_mode = ViewMode::InsightsResults;
7630 return Ok(());
7631 } else if status == "Failed" || status == "Cancelled" {
7632 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
7633 }
7634 }
7635
7636 Err(anyhow::anyhow!("Query timeout"))
7637 }
7638}
7639
7640impl CloudWatchInsightsState {
7641 fn new() -> Self {
7642 Self {
7643 insights: InsightsState::default(),
7644 loading: false,
7645 }
7646 }
7647}
7648
7649impl CloudWatchAlarmsState {
7650 fn new() -> Self {
7651 Self {
7652 table: TableState::new(),
7653 alarm_tab: AlarmTab::AllAlarms,
7654 view_as: AlarmViewMode::Table,
7655 wrap_lines: false,
7656 sort_column: "Last state update".to_string(),
7657 sort_direction: SortDirection::Asc,
7658 input_focus: InputFocus::Filter,
7659 }
7660 }
7661}
7662
7663impl ServicePickerState {
7664 fn new() -> Self {
7665 Self {
7666 filter: String::new(),
7667 selected: 0,
7668 services: vec![
7669 "CloudWatch > Log Groups",
7670 "CloudWatch > Logs Insights",
7671 "CloudWatch > Alarms",
7672 "CloudFormation > Stacks",
7673 "EC2 > Instances",
7674 "ECR > Repositories",
7675 "IAM > Users",
7676 "IAM > Roles",
7677 "IAM > User Groups",
7678 "Lambda > Functions",
7679 "Lambda > Applications",
7680 "S3 > Buckets",
7681 "SQS > Queues",
7682 ],
7683 }
7684 }
7685}
7686
7687#[cfg(test)]
7688mod test_helpers {
7689 use super::*;
7690
7691 #[allow(dead_code)]
7693 pub fn test_app() -> App {
7694 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
7695 }
7696
7697 #[allow(dead_code)]
7698 pub fn test_app_no_region() -> App {
7699 App::new_without_client("test".to_string(), None)
7700 }
7701
7702 #[allow(dead_code)]
7703 pub fn test_tab(service: Service) -> Tab {
7704 Tab {
7705 service,
7706 title: service.name().to_string(),
7707 breadcrumb: service.name().to_string(),
7708 }
7709 }
7710
7711 #[allow(dead_code)]
7712 pub fn test_iam_role(name: &str) -> crate::iam::IamRole {
7713 crate::iam::IamRole {
7714 role_name: name.to_string(),
7715 path: "/".to_string(),
7716 description: format!("Test role {}", name),
7717 trusted_entities: "AWS Service: ec2.amazonaws.com".to_string(),
7718 creation_time: "2024-01-01 00:00:00".to_string(),
7719 arn: format!("arn:aws:iam::123456789012:role/{}", name),
7720 max_session_duration: Some(3600),
7721 last_activity: "-".to_string(),
7722 }
7723 }
7724}
7725
7726#[cfg(test)]
7727mod tests {
7728 use super::*;
7729 use crate::keymap::Action;
7730 use test_helpers::*;
7731
7732 #[test]
7733 fn test_next_tab_cycles_forward() {
7734 let mut app = test_app();
7735 app.tabs = vec![
7736 Tab {
7737 service: Service::CloudWatchLogGroups,
7738 title: "CloudWatch > Log Groups".to_string(),
7739 breadcrumb: "CloudWatch > Log Groups".to_string(),
7740 },
7741 Tab {
7742 service: Service::CloudWatchInsights,
7743 title: "CloudWatch > Logs Insights".to_string(),
7744 breadcrumb: "CloudWatch > Logs Insights".to_string(),
7745 },
7746 Tab {
7747 service: Service::CloudWatchAlarms,
7748 title: "CloudWatch > Alarms".to_string(),
7749 breadcrumb: "CloudWatch > Alarms".to_string(),
7750 },
7751 ];
7752 app.current_tab = 0;
7753
7754 app.handle_action(Action::NextTab);
7755 assert_eq!(app.current_tab, 1);
7756 assert_eq!(app.current_service, Service::CloudWatchInsights);
7757
7758 app.handle_action(Action::NextTab);
7759 assert_eq!(app.current_tab, 2);
7760 assert_eq!(app.current_service, Service::CloudWatchAlarms);
7761
7762 app.handle_action(Action::NextTab);
7764 assert_eq!(app.current_tab, 0);
7765 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
7766 }
7767
7768 #[test]
7769 fn test_prev_tab_cycles_backward() {
7770 let mut app = test_app();
7771 app.tabs = vec![
7772 Tab {
7773 service: Service::CloudWatchLogGroups,
7774 title: "CloudWatch > Log Groups".to_string(),
7775 breadcrumb: "CloudWatch > Log Groups".to_string(),
7776 },
7777 Tab {
7778 service: Service::CloudWatchInsights,
7779 title: "CloudWatch > Logs Insights".to_string(),
7780 breadcrumb: "CloudWatch > Logs Insights".to_string(),
7781 },
7782 Tab {
7783 service: Service::CloudWatchAlarms,
7784 title: "CloudWatch > Alarms".to_string(),
7785 breadcrumb: "CloudWatch > Alarms".to_string(),
7786 },
7787 ];
7788 app.current_tab = 2;
7789
7790 app.handle_action(Action::PrevTab);
7791 assert_eq!(app.current_tab, 1);
7792 assert_eq!(app.current_service, Service::CloudWatchInsights);
7793
7794 app.handle_action(Action::PrevTab);
7795 assert_eq!(app.current_tab, 0);
7796 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
7797
7798 app.handle_action(Action::PrevTab);
7800 assert_eq!(app.current_tab, 2);
7801 assert_eq!(app.current_service, Service::CloudWatchAlarms);
7802 }
7803
7804 #[test]
7805 fn test_close_tab_removes_current() {
7806 let mut app = test_app();
7807 app.tabs = vec![
7808 Tab {
7809 service: Service::CloudWatchLogGroups,
7810 title: "CloudWatch > Log Groups".to_string(),
7811 breadcrumb: "CloudWatch > Log Groups".to_string(),
7812 },
7813 Tab {
7814 service: Service::CloudWatchInsights,
7815 title: "CloudWatch > Logs Insights".to_string(),
7816 breadcrumb: "CloudWatch > Logs Insights".to_string(),
7817 },
7818 Tab {
7819 service: Service::CloudWatchAlarms,
7820 title: "CloudWatch > Alarms".to_string(),
7821 breadcrumb: "CloudWatch > Alarms".to_string(),
7822 },
7823 ];
7824 app.current_tab = 1;
7825 app.service_selected = true;
7826
7827 app.handle_action(Action::CloseTab);
7828 assert_eq!(app.tabs.len(), 2);
7829 assert_eq!(app.current_tab, 1);
7830 assert_eq!(app.current_service, Service::CloudWatchAlarms);
7831 }
7832
7833 #[test]
7834 fn test_close_last_tab_exits_service() {
7835 let mut app = test_app();
7836 app.tabs = vec![Tab {
7837 service: Service::CloudWatchLogGroups,
7838 title: "CloudWatch > Log Groups".to_string(),
7839 breadcrumb: "CloudWatch > Log Groups".to_string(),
7840 }];
7841 app.current_tab = 0;
7842 app.service_selected = true;
7843
7844 app.handle_action(Action::CloseTab);
7845 assert_eq!(app.tabs.len(), 0);
7846 assert!(!app.service_selected);
7847 assert_eq!(app.current_tab, 0);
7848 }
7849
7850 #[test]
7851 fn test_close_service_removes_current_tab() {
7852 let mut app = test_app();
7853 app.tabs = vec![
7854 Tab {
7855 service: Service::CloudWatchLogGroups,
7856 title: "CloudWatch > Log Groups".to_string(),
7857 breadcrumb: "CloudWatch > Log Groups".to_string(),
7858 },
7859 Tab {
7860 service: Service::CloudWatchInsights,
7861 title: "CloudWatch > Logs Insights".to_string(),
7862 breadcrumb: "CloudWatch > Logs Insights".to_string(),
7863 },
7864 Tab {
7865 service: Service::CloudWatchAlarms,
7866 title: "CloudWatch > Alarms".to_string(),
7867 breadcrumb: "CloudWatch > Alarms".to_string(),
7868 },
7869 ];
7870 app.current_tab = 1;
7871 app.service_selected = true;
7872
7873 app.handle_action(Action::CloseService);
7874
7875 assert_eq!(app.tabs.len(), 2);
7877 assert_eq!(app.current_tab, 1);
7879 assert_eq!(app.current_service, Service::CloudWatchAlarms);
7880 assert!(app.service_selected);
7882 assert_eq!(app.mode, Mode::Normal);
7883 }
7884
7885 #[test]
7886 fn test_close_service_last_tab_shows_picker() {
7887 let mut app = test_app();
7888 app.tabs = vec![Tab {
7889 service: Service::CloudWatchLogGroups,
7890 title: "CloudWatch > Log Groups".to_string(),
7891 breadcrumb: "CloudWatch > Log Groups".to_string(),
7892 }];
7893 app.current_tab = 0;
7894 app.service_selected = true;
7895
7896 app.handle_action(Action::CloseService);
7897
7898 assert_eq!(app.tabs.len(), 0);
7900 assert!(!app.service_selected);
7902 assert_eq!(app.mode, Mode::ServicePicker);
7903 }
7904
7905 #[test]
7906 fn test_open_tab_picker_with_tabs() {
7907 let mut app = test_app();
7908 app.tabs = vec![
7909 Tab {
7910 service: Service::CloudWatchLogGroups,
7911 title: "CloudWatch > Log Groups".to_string(),
7912 breadcrumb: "CloudWatch > Log Groups".to_string(),
7913 },
7914 Tab {
7915 service: Service::CloudWatchInsights,
7916 title: "CloudWatch > Logs Insights".to_string(),
7917 breadcrumb: "CloudWatch > Logs Insights".to_string(),
7918 },
7919 ];
7920 app.current_tab = 1;
7921
7922 app.handle_action(Action::OpenTabPicker);
7923 assert_eq!(app.mode, Mode::TabPicker);
7924 assert_eq!(app.tab_picker_selected, 1);
7925 }
7926
7927 #[test]
7928 fn test_open_tab_picker_without_tabs() {
7929 let mut app = test_app();
7930 app.tabs = vec![];
7931
7932 app.handle_action(Action::OpenTabPicker);
7933 assert_eq!(app.mode, Mode::Normal);
7934 }
7935
7936 #[test]
7937 fn test_pending_key_state() {
7938 let mut app = test_app();
7939 assert_eq!(app.pending_key, None);
7940
7941 app.pending_key = Some('g');
7942 assert_eq!(app.pending_key, Some('g'));
7943 }
7944
7945 #[test]
7946 fn test_tab_breadcrumb_updates() {
7947 let mut app = test_app();
7948 app.tabs = vec![Tab {
7949 service: Service::CloudWatchLogGroups,
7950 title: "CloudWatch > Log Groups".to_string(),
7951 breadcrumb: "CloudWatch > Log groups".to_string(),
7952 }];
7953 app.current_tab = 0;
7954 app.service_selected = true;
7955 app.current_service = Service::CloudWatchLogGroups;
7956
7957 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
7959
7960 app.log_groups_state
7962 .log_groups
7963 .items
7964 .push(rusticity_core::LogGroup {
7965 name: "/aws/lambda/test".to_string(),
7966 creation_time: None,
7967 stored_bytes: Some(1024),
7968 retention_days: None,
7969 log_class: None,
7970 arn: None,
7971 });
7972 app.log_groups_state.log_groups.reset();
7973 app.view_mode = ViewMode::Detail;
7974 app.update_current_tab_breadcrumb();
7975
7976 assert_eq!(
7978 app.tabs[0].breadcrumb,
7979 "CloudWatch > Log groups > /aws/lambda/test"
7980 );
7981 }
7982
7983 #[test]
7984 fn test_s3_bucket_column_selector_navigation() {
7985 let mut app = test_app();
7986 app.current_service = Service::S3Buckets;
7987 app.mode = Mode::ColumnSelector;
7988 app.column_selector_index = 0;
7989
7990 app.handle_action(Action::NextItem);
7992 assert_eq!(app.column_selector_index, 1);
7993
7994 app.handle_action(Action::NextItem);
7995 assert_eq!(app.column_selector_index, 2);
7996
7997 app.handle_action(Action::NextItem);
7999 assert_eq!(app.column_selector_index, 2);
8000
8001 app.handle_action(Action::PrevItem);
8003 assert_eq!(app.column_selector_index, 1);
8004
8005 app.handle_action(Action::PrevItem);
8006 assert_eq!(app.column_selector_index, 0);
8007
8008 app.handle_action(Action::PrevItem);
8010 assert_eq!(app.column_selector_index, 0);
8011 }
8012
8013 #[test]
8014 fn test_cloudwatch_alarms_state_initialized() {
8015 let app = test_app();
8016
8017 assert_eq!(app.alarms_state.table.items.len(), 0);
8019 assert_eq!(app.alarms_state.table.selected, 0);
8020 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
8021 assert!(!app.alarms_state.table.loading);
8022 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
8023 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
8024 }
8025
8026 #[test]
8027 fn test_cloudwatch_alarms_service_selection() {
8028 let mut app = test_app();
8029
8030 app.current_service = Service::CloudWatchAlarms;
8032 app.service_selected = true;
8033
8034 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8035 assert!(app.service_selected);
8036 }
8037
8038 #[test]
8039 fn test_cloudwatch_alarms_column_preferences() {
8040 let app = test_app();
8041
8042 assert!(!app.cw_alarm_column_ids.is_empty());
8044 assert!(!app.cw_alarm_visible_column_ids.is_empty());
8045
8046 assert!(app
8048 .cw_alarm_visible_column_ids
8049 .contains(&AlarmColumn::Name.id()));
8050 assert!(app
8051 .cw_alarm_visible_column_ids
8052 .contains(&AlarmColumn::State.id()));
8053 }
8054
8055 #[test]
8056 fn test_s3_bucket_navigation_without_expansion() {
8057 let mut app = test_app();
8058 app.current_service = Service::S3Buckets;
8059 app.service_selected = true;
8060 app.mode = Mode::Normal;
8061
8062 app.s3_state.buckets.items = vec![
8064 S3Bucket {
8065 name: "bucket1".to_string(),
8066 region: "us-east-1".to_string(),
8067 creation_date: "2024-01-01T00:00:00Z".to_string(),
8068 },
8069 S3Bucket {
8070 name: "bucket2".to_string(),
8071 region: "us-east-1".to_string(),
8072 creation_date: "2024-01-02T00:00:00Z".to_string(),
8073 },
8074 S3Bucket {
8075 name: "bucket3".to_string(),
8076 region: "us-east-1".to_string(),
8077 creation_date: "2024-01-03T00:00:00Z".to_string(),
8078 },
8079 ];
8080 app.s3_state.selected_row = 0;
8081
8082 app.handle_action(Action::NextItem);
8084 assert_eq!(app.s3_state.selected_row, 1);
8085
8086 app.handle_action(Action::NextItem);
8087 assert_eq!(app.s3_state.selected_row, 2);
8088
8089 app.handle_action(Action::NextItem);
8091 assert_eq!(app.s3_state.selected_row, 2);
8092
8093 app.handle_action(Action::PrevItem);
8095 assert_eq!(app.s3_state.selected_row, 1);
8096
8097 app.handle_action(Action::PrevItem);
8098 assert_eq!(app.s3_state.selected_row, 0);
8099
8100 app.handle_action(Action::PrevItem);
8102 assert_eq!(app.s3_state.selected_row, 0);
8103 }
8104
8105 #[test]
8106 fn test_s3_bucket_navigation_with_expansion() {
8107 let mut app = test_app();
8108 app.current_service = Service::S3Buckets;
8109 app.service_selected = true;
8110 app.mode = Mode::Normal;
8111
8112 app.s3_state.buckets.items = vec![
8114 S3Bucket {
8115 name: "bucket1".to_string(),
8116 region: "us-east-1".to_string(),
8117 creation_date: "2024-01-01T00:00:00Z".to_string(),
8118 },
8119 S3Bucket {
8120 name: "bucket2".to_string(),
8121 region: "us-east-1".to_string(),
8122 creation_date: "2024-01-02T00:00:00Z".to_string(),
8123 },
8124 ];
8125
8126 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
8128 app.s3_state.bucket_preview.insert(
8129 "bucket1".to_string(),
8130 vec![
8131 S3Object {
8132 key: "file1.txt".to_string(),
8133 size: 100,
8134 last_modified: "2024-01-01T00:00:00Z".to_string(),
8135 is_prefix: false,
8136 storage_class: "STANDARD".to_string(),
8137 },
8138 S3Object {
8139 key: "folder/".to_string(),
8140 size: 0,
8141 last_modified: "2024-01-01T00:00:00Z".to_string(),
8142 is_prefix: true,
8143 storage_class: String::new(),
8144 },
8145 ],
8146 );
8147
8148 app.s3_state.selected_row = 0;
8149
8150 app.handle_action(Action::NextItem);
8153 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
8156 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
8159 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
8163 assert_eq!(app.s3_state.selected_row, 3);
8164 }
8165
8166 #[test]
8167 fn test_s3_bucket_navigation_with_nested_expansion() {
8168 let mut app = test_app();
8169 app.current_service = Service::S3Buckets;
8170 app.service_selected = true;
8171 app.mode = Mode::Normal;
8172
8173 app.s3_state.buckets.items = vec![S3Bucket {
8175 name: "bucket1".to_string(),
8176 region: "us-east-1".to_string(),
8177 creation_date: "2024-01-01T00:00:00Z".to_string(),
8178 }];
8179
8180 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
8182 app.s3_state.bucket_preview.insert(
8183 "bucket1".to_string(),
8184 vec![S3Object {
8185 key: "folder/".to_string(),
8186 size: 0,
8187 last_modified: "2024-01-01T00:00:00Z".to_string(),
8188 is_prefix: true,
8189 storage_class: String::new(),
8190 }],
8191 );
8192
8193 app.s3_state.expanded_prefixes.insert("folder/".to_string());
8195 app.s3_state.prefix_preview.insert(
8196 "folder/".to_string(),
8197 vec![
8198 S3Object {
8199 key: "folder/file1.txt".to_string(),
8200 size: 100,
8201 last_modified: "2024-01-01T00:00:00Z".to_string(),
8202 is_prefix: false,
8203 storage_class: "STANDARD".to_string(),
8204 },
8205 S3Object {
8206 key: "folder/file2.txt".to_string(),
8207 size: 200,
8208 last_modified: "2024-01-01T00:00:00Z".to_string(),
8209 is_prefix: false,
8210 storage_class: "STANDARD".to_string(),
8211 },
8212 ],
8213 );
8214
8215 app.s3_state.selected_row = 0;
8216
8217 app.handle_action(Action::NextItem);
8219 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
8222 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
8225 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
8229 assert_eq!(app.s3_state.selected_row, 3);
8230 }
8231
8232 #[test]
8233 fn test_calculate_total_bucket_rows() {
8234 let mut app = test_app();
8235
8236 assert_eq!(app.calculate_total_bucket_rows(), 0);
8238
8239 app.s3_state.buckets.items = vec![
8241 S3Bucket {
8242 name: "bucket1".to_string(),
8243 region: "us-east-1".to_string(),
8244 creation_date: "2024-01-01T00:00:00Z".to_string(),
8245 },
8246 S3Bucket {
8247 name: "bucket2".to_string(),
8248 region: "us-east-1".to_string(),
8249 creation_date: "2024-01-02T00:00:00Z".to_string(),
8250 },
8251 ];
8252 assert_eq!(app.calculate_total_bucket_rows(), 2);
8253
8254 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
8256 app.s3_state.bucket_preview.insert(
8257 "bucket1".to_string(),
8258 vec![
8259 S3Object {
8260 key: "file1.txt".to_string(),
8261 size: 100,
8262 last_modified: "2024-01-01T00:00:00Z".to_string(),
8263 is_prefix: false,
8264 storage_class: "STANDARD".to_string(),
8265 },
8266 S3Object {
8267 key: "file2.txt".to_string(),
8268 size: 200,
8269 last_modified: "2024-01-01T00:00:00Z".to_string(),
8270 is_prefix: false,
8271 storage_class: "STANDARD".to_string(),
8272 },
8273 S3Object {
8274 key: "folder/".to_string(),
8275 size: 0,
8276 last_modified: "2024-01-01T00:00:00Z".to_string(),
8277 is_prefix: true,
8278 storage_class: String::new(),
8279 },
8280 ],
8281 );
8282 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
8286 app.s3_state.prefix_preview.insert(
8287 "folder/".to_string(),
8288 vec![
8289 S3Object {
8290 key: "folder/nested1.txt".to_string(),
8291 size: 50,
8292 last_modified: "2024-01-01T00:00:00Z".to_string(),
8293 is_prefix: false,
8294 storage_class: "STANDARD".to_string(),
8295 },
8296 S3Object {
8297 key: "folder/nested2.txt".to_string(),
8298 size: 75,
8299 last_modified: "2024-01-01T00:00:00Z".to_string(),
8300 is_prefix: false,
8301 storage_class: "STANDARD".to_string(),
8302 },
8303 ],
8304 );
8305 assert_eq!(app.calculate_total_bucket_rows(), 7); }
8307
8308 #[test]
8309 fn test_calculate_total_object_rows() {
8310 let mut app = test_app();
8311 app.s3_state.current_bucket = Some("test-bucket".to_string());
8312
8313 assert_eq!(app.calculate_total_object_rows(), 0);
8315
8316 app.s3_state.objects = vec![
8318 S3Object {
8319 key: "file1.txt".to_string(),
8320 size: 100,
8321 last_modified: "2024-01-01T00:00:00Z".to_string(),
8322 is_prefix: false,
8323 storage_class: "STANDARD".to_string(),
8324 },
8325 S3Object {
8326 key: "folder/".to_string(),
8327 size: 0,
8328 last_modified: "2024-01-01T00:00:00Z".to_string(),
8329 is_prefix: true,
8330 storage_class: String::new(),
8331 },
8332 ];
8333 assert_eq!(app.calculate_total_object_rows(), 2);
8334
8335 app.s3_state.expanded_prefixes.insert("folder/".to_string());
8337 app.s3_state.prefix_preview.insert(
8338 "folder/".to_string(),
8339 vec![
8340 S3Object {
8341 key: "folder/file2.txt".to_string(),
8342 size: 200,
8343 last_modified: "2024-01-01T00:00:00Z".to_string(),
8344 is_prefix: false,
8345 storage_class: "STANDARD".to_string(),
8346 },
8347 S3Object {
8348 key: "folder/subfolder/".to_string(),
8349 size: 0,
8350 last_modified: "2024-01-01T00:00:00Z".to_string(),
8351 is_prefix: true,
8352 storage_class: String::new(),
8353 },
8354 ],
8355 );
8356 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
8360 .expanded_prefixes
8361 .insert("folder/subfolder/".to_string());
8362 app.s3_state.prefix_preview.insert(
8363 "folder/subfolder/".to_string(),
8364 vec![S3Object {
8365 key: "folder/subfolder/deep.txt".to_string(),
8366 size: 50,
8367 last_modified: "2024-01-01T00:00:00Z".to_string(),
8368 is_prefix: false,
8369 storage_class: "STANDARD".to_string(),
8370 }],
8371 );
8372 assert_eq!(app.calculate_total_object_rows(), 5); }
8374
8375 #[test]
8376 fn test_s3_object_navigation_with_deep_nesting() {
8377 let mut app = test_app();
8378 app.current_service = Service::S3Buckets;
8379 app.service_selected = true;
8380 app.mode = Mode::Normal;
8381 app.s3_state.current_bucket = Some("test-bucket".to_string());
8382
8383 app.s3_state.objects = vec![S3Object {
8385 key: "folder1/".to_string(),
8386 size: 0,
8387 last_modified: "2024-01-01T00:00:00Z".to_string(),
8388 is_prefix: true,
8389 storage_class: String::new(),
8390 }];
8391
8392 app.s3_state
8394 .expanded_prefixes
8395 .insert("folder1/".to_string());
8396 app.s3_state.prefix_preview.insert(
8397 "folder1/".to_string(),
8398 vec![S3Object {
8399 key: "folder1/folder2/".to_string(),
8400 size: 0,
8401 last_modified: "2024-01-01T00:00:00Z".to_string(),
8402 is_prefix: true,
8403 storage_class: String::new(),
8404 }],
8405 );
8406
8407 app.s3_state
8409 .expanded_prefixes
8410 .insert("folder1/folder2/".to_string());
8411 app.s3_state.prefix_preview.insert(
8412 "folder1/folder2/".to_string(),
8413 vec![S3Object {
8414 key: "folder1/folder2/file.txt".to_string(),
8415 size: 100,
8416 last_modified: "2024-01-01T00:00:00Z".to_string(),
8417 is_prefix: false,
8418 storage_class: "STANDARD".to_string(),
8419 }],
8420 );
8421
8422 app.s3_state.selected_object = 0;
8423
8424 app.handle_action(Action::NextItem);
8426 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
8429 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
8433 assert_eq!(app.s3_state.selected_object, 2);
8434 }
8435
8436 #[test]
8437 fn test_s3_expand_nested_folder_in_objects_view() {
8438 let mut app = test_app();
8439 app.current_service = Service::S3Buckets;
8440 app.service_selected = true;
8441 app.mode = Mode::Normal;
8442 app.s3_state.current_bucket = Some("test-bucket".to_string());
8443
8444 app.s3_state.objects = vec![S3Object {
8446 key: "parent/".to_string(),
8447 size: 0,
8448 last_modified: "2024-01-01T00:00:00Z".to_string(),
8449 is_prefix: true,
8450 storage_class: String::new(),
8451 }];
8452
8453 app.s3_state.expanded_prefixes.insert("parent/".to_string());
8455 app.s3_state.prefix_preview.insert(
8456 "parent/".to_string(),
8457 vec![S3Object {
8458 key: "parent/child/".to_string(),
8459 size: 0,
8460 last_modified: "2024-01-01T00:00:00Z".to_string(),
8461 is_prefix: true,
8462 storage_class: String::new(),
8463 }],
8464 );
8465
8466 app.s3_state.selected_object = 1;
8468
8469 app.handle_action(Action::NextPane);
8471
8472 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
8474 assert!(app.s3_state.buckets.loading); }
8476
8477 #[test]
8478 fn test_s3_drill_into_nested_folder() {
8479 let mut app = test_app();
8480 app.current_service = Service::S3Buckets;
8481 app.service_selected = true;
8482 app.mode = Mode::Normal;
8483 app.s3_state.current_bucket = Some("test-bucket".to_string());
8484
8485 app.s3_state.objects = vec![S3Object {
8487 key: "parent/".to_string(),
8488 size: 0,
8489 last_modified: "2024-01-01T00:00:00Z".to_string(),
8490 is_prefix: true,
8491 storage_class: String::new(),
8492 }];
8493
8494 app.s3_state.expanded_prefixes.insert("parent/".to_string());
8496 app.s3_state.prefix_preview.insert(
8497 "parent/".to_string(),
8498 vec![S3Object {
8499 key: "parent/child/".to_string(),
8500 size: 0,
8501 last_modified: "2024-01-01T00:00:00Z".to_string(),
8502 is_prefix: true,
8503 storage_class: String::new(),
8504 }],
8505 );
8506
8507 app.s3_state.selected_object = 1;
8509
8510 app.handle_action(Action::Select);
8512
8513 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
8515 assert!(app.s3_state.buckets.loading); }
8517
8518 #[test]
8519 fn test_s3_esc_pops_navigation_stack() {
8520 let mut app = test_app();
8521 app.current_service = Service::S3Buckets;
8522 app.s3_state.current_bucket = Some("test-bucket".to_string());
8523 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
8524
8525 app.handle_action(Action::GoBack);
8527 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
8528 assert!(app.s3_state.buckets.loading);
8529
8530 app.s3_state.buckets.loading = false;
8532 app.handle_action(Action::GoBack);
8533 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
8534 assert!(app.s3_state.buckets.loading);
8535
8536 app.s3_state.buckets.loading = false;
8538 app.handle_action(Action::GoBack);
8539 assert_eq!(app.s3_state.current_bucket, None);
8540 }
8541
8542 #[test]
8543 fn test_s3_esc_from_bucket_root_exits() {
8544 let mut app = test_app();
8545 app.current_service = Service::S3Buckets;
8546 app.s3_state.current_bucket = Some("test-bucket".to_string());
8547 app.s3_state.prefix_stack = vec![];
8548
8549 app.handle_action(Action::GoBack);
8551 assert_eq!(app.s3_state.current_bucket, None);
8552 assert_eq!(app.s3_state.objects.len(), 0);
8553 }
8554
8555 #[test]
8556 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
8557 let mut app = test_app();
8558 app.current_service = Service::S3Buckets;
8559 app.service_selected = true;
8560 app.mode = Mode::Normal;
8561
8562 app.s3_state.buckets.items = vec![S3Bucket {
8564 name: "test-bucket".to_string(),
8565 region: "us-east-1".to_string(),
8566 creation_date: "2024-01-01".to_string(),
8567 }];
8568
8569 app.s3_state
8571 .expanded_prefixes
8572 .insert("test-bucket".to_string());
8573 app.s3_state.bucket_preview.insert(
8574 "test-bucket".to_string(),
8575 vec![S3Object {
8576 key: "parent/".to_string(),
8577 size: 0,
8578 last_modified: "2024-01-01".to_string(),
8579 is_prefix: true,
8580 storage_class: String::new(),
8581 }],
8582 );
8583
8584 app.s3_state.expanded_prefixes.insert("parent/".to_string());
8586 app.s3_state.prefix_preview.insert(
8587 "parent/".to_string(),
8588 vec![S3Object {
8589 key: "parent/child/".to_string(),
8590 size: 0,
8591 last_modified: "2024-01-01".to_string(),
8592 is_prefix: true,
8593 storage_class: String::new(),
8594 }],
8595 );
8596
8597 app.s3_state.selected_row = 2;
8599
8600 app.handle_action(Action::Select);
8602
8603 assert_eq!(
8605 app.s3_state.prefix_stack,
8606 vec!["parent/".to_string(), "parent/child/".to_string()]
8607 );
8608 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
8609 assert!(app.s3_state.buckets.loading);
8610
8611 app.s3_state.buckets.loading = false;
8613 app.handle_action(Action::GoBack);
8614 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
8615 assert!(app.s3_state.buckets.loading);
8616
8617 app.s3_state.buckets.loading = false;
8619 app.handle_action(Action::GoBack);
8620 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
8621 assert!(app.s3_state.buckets.loading);
8622
8623 app.s3_state.buckets.loading = false;
8625 app.handle_action(Action::GoBack);
8626 assert_eq!(app.s3_state.current_bucket, None);
8627 }
8628
8629 #[test]
8630 fn test_region_picker_fuzzy_filter() {
8631 let mut app = test_app();
8632 app.region_latencies.insert("us-east-1".to_string(), 10);
8633 app.region_filter = "vir".to_string();
8634 let filtered = app.get_filtered_regions();
8635 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
8636 }
8637
8638 #[test]
8639 fn test_profile_picker_loads_profiles() {
8640 let profiles = App::load_aws_profiles();
8641 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
8643 }
8644
8645 #[test]
8646 fn test_profile_with_region_uses_it() {
8647 let mut app = test_app_no_region();
8648 app.available_profiles = vec![AwsProfile {
8649 name: "test-profile".to_string(),
8650 region: Some("eu-west-1".to_string()),
8651 account: Some("123456789".to_string()),
8652 role_arn: None,
8653 source_profile: None,
8654 }];
8655 app.profile_picker_selected = 0;
8656 app.mode = Mode::ProfilePicker;
8657
8658 let filtered = app.get_filtered_profiles();
8660 if let Some(profile) = filtered.first() {
8661 let profile_name = profile.name.clone();
8662 let profile_region = profile.region.clone();
8663
8664 app.profile = profile_name;
8665 if let Some(region) = profile_region {
8666 app.region = region;
8667 }
8668 }
8669
8670 assert_eq!(app.profile, "test-profile");
8671 assert_eq!(app.region, "eu-west-1");
8672 }
8673
8674 #[test]
8675 fn test_profile_without_region_keeps_unknown() {
8676 let mut app = test_app_no_region();
8677 let initial_region = app.region.clone();
8678
8679 app.available_profiles = vec![AwsProfile {
8680 name: "test-profile".to_string(),
8681 region: None,
8682 account: None,
8683 role_arn: None,
8684 source_profile: None,
8685 }];
8686 app.profile_picker_selected = 0;
8687 app.mode = Mode::ProfilePicker;
8688
8689 let filtered = app.get_filtered_profiles();
8690 if let Some(profile) = filtered.first() {
8691 let profile_name = profile.name.clone();
8692 let profile_region = profile.region.clone();
8693
8694 app.profile = profile_name;
8695 if let Some(region) = profile_region {
8696 app.region = region;
8697 }
8698 }
8699
8700 assert_eq!(app.profile, "test-profile");
8701 assert_eq!(app.region, initial_region); }
8703
8704 #[test]
8705 fn test_region_selection_closes_all_tabs() {
8706 let mut app = test_app();
8707
8708 app.tabs.push(Tab {
8710 service: Service::CloudWatchLogGroups,
8711 title: "CloudWatch".to_string(),
8712 breadcrumb: "CloudWatch".to_string(),
8713 });
8714 app.tabs.push(Tab {
8715 service: Service::S3Buckets,
8716 title: "S3".to_string(),
8717 breadcrumb: "S3".to_string(),
8718 });
8719 app.service_selected = true;
8720 app.current_tab = 1;
8721
8722 app.region_latencies.insert("eu-west-1".to_string(), 50);
8724
8725 app.mode = Mode::RegionPicker;
8727 app.region_picker_selected = 0;
8728
8729 let filtered = app.get_filtered_regions();
8730 if let Some(region) = filtered.first() {
8731 app.region = region.code.to_string();
8732 app.tabs.clear();
8733 app.current_tab = 0;
8734 app.service_selected = false;
8735 app.mode = Mode::Normal;
8736 }
8737
8738 assert_eq!(app.tabs.len(), 0);
8739 assert_eq!(app.current_tab, 0);
8740 assert!(!app.service_selected);
8741 assert_eq!(app.region, "eu-west-1");
8742 }
8743
8744 #[test]
8745 fn test_region_picker_can_be_closed_without_selection() {
8746 let mut app = test_app();
8747 let initial_region = app.region.clone();
8748
8749 app.mode = Mode::RegionPicker;
8750
8751 app.mode = Mode::Normal;
8753
8754 assert_eq!(app.region, initial_region);
8756 }
8757
8758 #[test]
8759 fn test_session_filter_works() {
8760 let mut app = test_app();
8761
8762 app.sessions = vec![
8763 Session {
8764 id: "1".to_string(),
8765 timestamp: "2024-01-01".to_string(),
8766 profile: "prod-profile".to_string(),
8767 region: "us-east-1".to_string(),
8768 account_id: "123456789".to_string(),
8769 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
8770 tabs: vec![],
8771 },
8772 Session {
8773 id: "2".to_string(),
8774 timestamp: "2024-01-02".to_string(),
8775 profile: "dev-profile".to_string(),
8776 region: "eu-west-1".to_string(),
8777 account_id: "987654321".to_string(),
8778 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
8779 tabs: vec![],
8780 },
8781 ];
8782
8783 app.session_filter = "prod".to_string();
8785 let filtered = app.get_filtered_sessions();
8786 assert_eq!(filtered.len(), 1);
8787 assert_eq!(filtered[0].profile, "prod-profile");
8788
8789 app.session_filter = "eu".to_string();
8791 let filtered = app.get_filtered_sessions();
8792 assert_eq!(filtered.len(), 1);
8793 assert_eq!(filtered[0].region, "eu-west-1");
8794
8795 app.session_filter.clear();
8797 let filtered = app.get_filtered_sessions();
8798 assert_eq!(filtered.len(), 2);
8799 }
8800
8801 #[test]
8802 fn test_profile_picker_shows_account() {
8803 let mut app = test_app_no_region();
8804 app.available_profiles = vec![AwsProfile {
8805 name: "test-profile".to_string(),
8806 region: Some("us-east-1".to_string()),
8807 account: Some("123456789".to_string()),
8808 role_arn: None,
8809 source_profile: None,
8810 }];
8811
8812 let filtered = app.get_filtered_profiles();
8813 assert_eq!(filtered.len(), 1);
8814 assert_eq!(filtered[0].account, Some("123456789".to_string()));
8815 }
8816
8817 #[test]
8818 fn test_profile_without_account() {
8819 let mut app = test_app_no_region();
8820 app.available_profiles = vec![AwsProfile {
8821 name: "test-profile".to_string(),
8822 region: Some("us-east-1".to_string()),
8823 account: None,
8824 role_arn: None,
8825 source_profile: None,
8826 }];
8827
8828 let filtered = app.get_filtered_profiles();
8829 assert_eq!(filtered.len(), 1);
8830 assert_eq!(filtered[0].account, None);
8831 }
8832
8833 #[test]
8834 fn test_profile_with_all_fields() {
8835 let mut app = test_app_no_region();
8836 app.available_profiles = vec![AwsProfile {
8837 name: "prod-profile".to_string(),
8838 region: Some("us-west-2".to_string()),
8839 account: Some("123456789".to_string()),
8840 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
8841 source_profile: Some("base-profile".to_string()),
8842 }];
8843
8844 let filtered = app.get_filtered_profiles();
8845 assert_eq!(filtered.len(), 1);
8846 assert_eq!(filtered[0].name, "prod-profile");
8847 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
8848 assert_eq!(filtered[0].account, Some("123456789".to_string()));
8849 assert_eq!(
8850 filtered[0].role_arn,
8851 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
8852 );
8853 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
8854 }
8855
8856 #[test]
8857 fn test_profile_filter_by_source_profile() {
8858 let mut app = test_app_no_region();
8859 app.available_profiles = vec![
8860 AwsProfile {
8861 name: "profile1".to_string(),
8862 region: None,
8863 account: None,
8864 role_arn: None,
8865 source_profile: Some("base".to_string()),
8866 },
8867 AwsProfile {
8868 name: "profile2".to_string(),
8869 region: None,
8870 account: None,
8871 role_arn: None,
8872 source_profile: Some("other".to_string()),
8873 },
8874 ];
8875
8876 app.profile_filter = "base".to_string();
8877 let filtered = app.get_filtered_profiles();
8878 assert_eq!(filtered.len(), 1);
8879 assert_eq!(filtered[0].name, "profile1");
8880 }
8881
8882 #[test]
8883 fn test_profile_filter_by_role() {
8884 let mut app = test_app_no_region();
8885 app.available_profiles = vec![
8886 AwsProfile {
8887 name: "admin-profile".to_string(),
8888 region: None,
8889 account: None,
8890 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
8891 source_profile: None,
8892 },
8893 AwsProfile {
8894 name: "dev-profile".to_string(),
8895 region: None,
8896 account: None,
8897 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
8898 source_profile: None,
8899 },
8900 ];
8901
8902 app.profile_filter = "Admin".to_string();
8903 let filtered = app.get_filtered_profiles();
8904 assert_eq!(filtered.len(), 1);
8905 assert_eq!(filtered[0].name, "admin-profile");
8906 }
8907
8908 #[test]
8909 fn test_profiles_sorted_by_name() {
8910 let mut app = test_app_no_region();
8911 app.available_profiles = vec![
8912 AwsProfile {
8913 name: "zebra-profile".to_string(),
8914 region: None,
8915 account: None,
8916 role_arn: None,
8917 source_profile: None,
8918 },
8919 AwsProfile {
8920 name: "alpha-profile".to_string(),
8921 region: None,
8922 account: None,
8923 role_arn: None,
8924 source_profile: None,
8925 },
8926 AwsProfile {
8927 name: "beta-profile".to_string(),
8928 region: None,
8929 account: None,
8930 role_arn: None,
8931 source_profile: None,
8932 },
8933 ];
8934
8935 let filtered = app.get_filtered_profiles();
8936 assert_eq!(filtered.len(), 3);
8937 assert_eq!(filtered[0].name, "alpha-profile");
8938 assert_eq!(filtered[1].name, "beta-profile");
8939 assert_eq!(filtered[2].name, "zebra-profile");
8940 }
8941
8942 #[test]
8943 fn test_profile_with_role_arn() {
8944 let mut app = test_app_no_region();
8945 app.available_profiles = vec![AwsProfile {
8946 name: "role-profile".to_string(),
8947 region: Some("us-east-1".to_string()),
8948 account: Some("123456789".to_string()),
8949 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
8950 source_profile: None,
8951 }];
8952
8953 let filtered = app.get_filtered_profiles();
8954 assert_eq!(filtered.len(), 1);
8955 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
8956 }
8957
8958 #[test]
8959 fn test_profile_with_user_arn() {
8960 let mut app = test_app_no_region();
8961 app.available_profiles = vec![AwsProfile {
8962 name: "user-profile".to_string(),
8963 region: Some("us-east-1".to_string()),
8964 account: Some("123456789".to_string()),
8965 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
8966 source_profile: None,
8967 }];
8968
8969 let filtered = app.get_filtered_profiles();
8970 assert_eq!(filtered.len(), 1);
8971 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
8972 }
8973
8974 #[test]
8975 fn test_filtered_profiles_also_sorted() {
8976 let mut app = test_app_no_region();
8977 app.available_profiles = vec![
8978 AwsProfile {
8979 name: "prod-zebra".to_string(),
8980 region: Some("us-east-1".to_string()),
8981 account: None,
8982 role_arn: None,
8983 source_profile: None,
8984 },
8985 AwsProfile {
8986 name: "prod-alpha".to_string(),
8987 region: Some("us-east-1".to_string()),
8988 account: None,
8989 role_arn: None,
8990 source_profile: None,
8991 },
8992 AwsProfile {
8993 name: "dev-profile".to_string(),
8994 region: Some("us-west-2".to_string()),
8995 account: None,
8996 role_arn: None,
8997 source_profile: None,
8998 },
8999 ];
9000
9001 app.profile_filter = "prod".to_string();
9002 let filtered = app.get_filtered_profiles();
9003 assert_eq!(filtered.len(), 2);
9004 assert_eq!(filtered[0].name, "prod-alpha");
9005 assert_eq!(filtered[1].name, "prod-zebra");
9006 }
9007
9008 #[test]
9009 fn test_profile_picker_has_all_columns() {
9010 let mut app = test_app_no_region();
9011 app.available_profiles = vec![AwsProfile {
9012 name: "test".to_string(),
9013 region: Some("us-east-1".to_string()),
9014 account: Some("123456789".to_string()),
9015 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
9016 source_profile: Some("base".to_string()),
9017 }];
9018
9019 let filtered = app.get_filtered_profiles();
9020 assert_eq!(filtered.len(), 1);
9021 assert!(filtered[0].name == "test");
9022 assert!(filtered[0].region.is_some());
9023 assert!(filtered[0].account.is_some());
9024 assert!(filtered[0].role_arn.is_some());
9025 assert!(filtered[0].source_profile.is_some());
9026 }
9027
9028 #[test]
9029 fn test_session_picker_shows_tab_count() {
9030 let mut app = test_app_no_region();
9031 app.sessions = vec![Session {
9032 id: "1".to_string(),
9033 timestamp: "2024-01-01".to_string(),
9034 profile: "test".to_string(),
9035 region: "us-east-1".to_string(),
9036 account_id: "123".to_string(),
9037 role_arn: String::new(),
9038 tabs: vec![
9039 SessionTab {
9040 service: "CloudWatch".to_string(),
9041 title: "Logs".to_string(),
9042 breadcrumb: String::new(),
9043 filter: None,
9044 selected_item: None,
9045 },
9046 SessionTab {
9047 service: "S3".to_string(),
9048 title: "Buckets".to_string(),
9049 breadcrumb: String::new(),
9050 filter: None,
9051 selected_item: None,
9052 },
9053 ],
9054 }];
9055
9056 let filtered = app.get_filtered_sessions();
9057 assert_eq!(filtered.len(), 1);
9058 assert_eq!(filtered[0].tabs.len(), 2);
9059 }
9060
9061 #[test]
9062 fn test_start_background_data_fetch_loads_profiles() {
9063 let mut app = test_app_no_region();
9064 assert!(app.available_profiles.is_empty());
9065
9066 app.available_profiles = App::load_aws_profiles();
9068
9069 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
9071 }
9072
9073 #[test]
9074 fn test_refresh_in_profile_picker() {
9075 let mut app = test_app_no_region();
9076 app.mode = Mode::ProfilePicker;
9077 app.available_profiles = vec![AwsProfile {
9078 name: "test".to_string(),
9079 region: None,
9080 account: None,
9081 role_arn: None,
9082 source_profile: None,
9083 }];
9084
9085 app.handle_action(Action::Refresh);
9086
9087 assert!(app.log_groups_state.loading);
9089 assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
9090 }
9091
9092 #[test]
9093 fn test_refresh_sets_loading_for_profile_picker() {
9094 let mut app = test_app_no_region();
9095 app.mode = Mode::ProfilePicker;
9096
9097 assert!(!app.log_groups_state.loading);
9098
9099 app.handle_action(Action::Refresh);
9100
9101 assert!(app.log_groups_state.loading);
9102 }
9103
9104 #[test]
9105 fn test_profiles_loaded_on_demand() {
9106 let mut app = test_app_no_region();
9107
9108 assert!(app.available_profiles.is_empty());
9110
9111 app.available_profiles = App::load_aws_profiles();
9113
9114 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
9116 }
9117
9118 #[test]
9119 fn test_profile_accounts_not_fetched_automatically() {
9120 let mut app = test_app_no_region();
9121 app.available_profiles = App::load_aws_profiles();
9122
9123 for profile in &app.available_profiles {
9125 assert!(profile.account.is_none() || profile.account.is_some());
9128 }
9129 }
9130
9131 #[test]
9132 fn test_ctrl_r_triggers_account_fetch() {
9133 let mut app = test_app_no_region();
9134 app.mode = Mode::ProfilePicker;
9135 app.available_profiles = vec![AwsProfile {
9136 name: "test".to_string(),
9137 region: Some("us-east-1".to_string()),
9138 account: None,
9139 role_arn: None,
9140 source_profile: None,
9141 }];
9142
9143 assert!(app.available_profiles[0].account.is_none());
9145
9146 app.handle_action(Action::Refresh);
9148
9149 assert!(app.log_groups_state.loading);
9151 }
9152
9153 #[test]
9154 fn test_refresh_in_region_picker() {
9155 let mut app = test_app_no_region();
9156 app.mode = Mode::RegionPicker;
9157
9158 let initial_latencies = app.region_latencies.len();
9159 app.handle_action(Action::Refresh);
9160
9161 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
9163 }
9164
9165 #[test]
9166 fn test_refresh_in_session_picker() {
9167 let mut app = test_app_no_region();
9168 app.mode = Mode::SessionPicker;
9169 app.sessions = vec![];
9170
9171 app.handle_action(Action::Refresh);
9172
9173 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
9175 }
9176
9177 #[test]
9178 fn test_session_picker_selection() {
9179 let mut app = test_app();
9180
9181 app.sessions = vec![Session {
9182 id: "1".to_string(),
9183 timestamp: "2024-01-01".to_string(),
9184 profile: "prod-profile".to_string(),
9185 region: "us-west-2".to_string(),
9186 account_id: "123456789".to_string(),
9187 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
9188 tabs: vec![SessionTab {
9189 service: "CloudWatchLogGroups".to_string(),
9190 title: "Log Groups".to_string(),
9191 breadcrumb: "CloudWatch > Log Groups".to_string(),
9192 filter: Some("test".to_string()),
9193 selected_item: None,
9194 }],
9195 }];
9196
9197 app.mode = Mode::SessionPicker;
9198 app.session_picker_selected = 0;
9199
9200 app.handle_action(Action::Select);
9202
9203 assert_eq!(app.mode, Mode::Normal);
9204 assert_eq!(app.profile, "prod-profile");
9205 assert_eq!(app.region, "us-west-2");
9206 assert_eq!(app.config.account_id, "123456789");
9207 assert_eq!(app.tabs.len(), 1);
9208 assert_eq!(app.tabs[0].title, "Log Groups");
9209 }
9210
9211 #[test]
9212 fn test_save_session_creates_session() {
9213 let mut app =
9214 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
9215 app.config.account_id = "123456789".to_string();
9216 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
9217
9218 app.tabs.push(Tab {
9219 service: Service::CloudWatchLogGroups,
9220 title: "Log Groups".to_string(),
9221 breadcrumb: "CloudWatch > Log Groups".to_string(),
9222 });
9223
9224 app.save_current_session();
9225
9226 assert!(app.current_session.is_some());
9227 let session = app.current_session.clone().unwrap();
9228 assert_eq!(session.profile, "test-profile");
9229 assert_eq!(session.region, "us-east-1");
9230 assert_eq!(session.account_id, "123456789");
9231 assert_eq!(session.tabs.len(), 1);
9232
9233 let _ = session.delete();
9235 }
9236
9237 #[test]
9238 fn test_save_session_updates_existing() {
9239 let mut app =
9240 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
9241 app.config.account_id = "123456789".to_string();
9242 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
9243
9244 app.current_session = Some(Session {
9245 id: "existing".to_string(),
9246 timestamp: "2024-01-01".to_string(),
9247 profile: "test-profile".to_string(),
9248 region: "us-east-1".to_string(),
9249 account_id: "123456789".to_string(),
9250 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
9251 tabs: vec![],
9252 });
9253
9254 app.tabs.push(Tab {
9255 service: Service::CloudWatchLogGroups,
9256 title: "Log Groups".to_string(),
9257 breadcrumb: "CloudWatch > Log Groups".to_string(),
9258 });
9259
9260 app.save_current_session();
9261
9262 let session = app.current_session.clone().unwrap();
9263 assert_eq!(session.id, "existing");
9264 assert_eq!(session.tabs.len(), 1);
9265
9266 let _ = session.delete();
9268 }
9269
9270 #[test]
9271 fn test_save_session_skips_empty_tabs() {
9272 let mut app =
9273 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
9274 app.config.account_id = "123456789".to_string();
9275
9276 app.save_current_session();
9277
9278 assert!(app.current_session.is_none());
9279 }
9280
9281 #[test]
9282 fn test_save_session_deletes_when_tabs_closed() {
9283 let mut app =
9284 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
9285 app.config.account_id = "123456789".to_string();
9286 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
9287
9288 app.current_session = Some(Session {
9290 id: "test_delete".to_string(),
9291 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
9292 profile: "test-profile".to_string(),
9293 region: "us-east-1".to_string(),
9294 account_id: "123456789".to_string(),
9295 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
9296 tabs: vec![],
9297 });
9298
9299 app.save_current_session();
9301
9302 assert!(app.current_session.is_none());
9303 }
9304
9305 #[test]
9306 fn test_closing_all_tabs_deletes_session() {
9307 let mut app =
9308 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
9309 app.config.account_id = "123456789".to_string();
9310 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
9311
9312 app.tabs.push(Tab {
9314 service: Service::CloudWatchLogGroups,
9315 title: "Log Groups".to_string(),
9316 breadcrumb: "CloudWatch > Log Groups".to_string(),
9317 });
9318
9319 app.save_current_session();
9321 assert!(app.current_session.is_some());
9322 let session_id = app.current_session.as_ref().unwrap().id.clone();
9323
9324 app.tabs.clear();
9326
9327 app.save_current_session();
9329 assert!(app.current_session.is_none());
9330
9331 let _ = Session::load(&session_id).map(|s| s.delete());
9333 }
9334
9335 #[test]
9336 fn test_credential_error_opens_profile_picker() {
9337 let mut app = App::new_without_client("default".to_string(), None);
9339 let error_str = "Unable to load credentials from any source";
9340
9341 if error_str.contains("credentials") {
9342 app.available_profiles = App::load_aws_profiles();
9343 app.mode = Mode::ProfilePicker;
9344 }
9345
9346 assert_eq!(app.mode, Mode::ProfilePicker);
9347 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
9349 }
9350
9351 #[test]
9352 fn test_non_credential_error_shows_error_modal() {
9353 let mut app = App::new_without_client("default".to_string(), None);
9354 let error_str = "Network timeout";
9355
9356 if !error_str.contains("credentials") {
9357 app.error_message = Some(error_str.to_string());
9358 app.mode = Mode::ErrorModal;
9359 }
9360
9361 assert_eq!(app.mode, Mode::ErrorModal);
9362 assert!(app.error_message.is_some());
9363 }
9364
9365 #[tokio::test]
9366 async fn test_profile_selection_loads_credentials() {
9367 std::env::set_var("AWS_PROFILE", "default");
9369
9370 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
9372
9373 if let Ok(app) = result {
9374 assert!(!app.config.account_id.is_empty());
9376 assert!(!app.config.role_arn.is_empty());
9377 assert_eq!(app.profile, "default");
9378 assert_eq!(app.config.region, "us-east-1");
9379 }
9380 }
9382
9383 #[test]
9384 fn test_new_app_shows_service_picker_with_no_tabs() {
9385 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
9386
9387 assert!(!app.service_selected);
9389 assert_eq!(app.mode, Mode::ServicePicker);
9391 assert!(app.tabs.is_empty());
9393 }
9394
9395 #[tokio::test]
9396 async fn test_aws_profile_env_var_read_before_config_load() {
9397 std::env::set_var("AWS_PROFILE", "test-profile");
9399
9400 let profile_name = None
9402 .or_else(|| std::env::var("AWS_PROFILE").ok())
9403 .unwrap_or_else(|| "default".to_string());
9404
9405 assert_eq!(profile_name, "test-profile");
9407
9408 std::env::set_var("AWS_PROFILE", &profile_name);
9410
9411 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
9413
9414 std::env::remove_var("AWS_PROFILE");
9415 }
9416
9417 #[test]
9418 fn test_next_preferences_cloudformation() {
9419 let mut app = test_app();
9420 app.current_service = Service::CloudFormationStacks;
9421 app.mode = Mode::ColumnSelector;
9422 app.column_selector_index = 0;
9423
9424 let page_size_idx = app.cfn_column_ids.len() + 2;
9426 app.handle_action(Action::NextPreferences);
9427 assert_eq!(app.column_selector_index, page_size_idx);
9428
9429 app.handle_action(Action::NextPreferences);
9431 assert_eq!(app.column_selector_index, 0);
9432 }
9433
9434 #[test]
9435 fn test_next_preferences_lambda_functions() {
9436 let mut app = test_app();
9437 app.current_service = Service::LambdaFunctions;
9438 app.mode = Mode::ColumnSelector;
9439 app.column_selector_index = 0;
9440
9441 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
9442 app.handle_action(Action::NextPreferences);
9443 assert_eq!(app.column_selector_index, page_size_idx);
9444
9445 app.handle_action(Action::NextPreferences);
9446 assert_eq!(app.column_selector_index, 0);
9447 }
9448
9449 #[test]
9450 fn test_next_preferences_lambda_applications() {
9451 let mut app = test_app();
9452 app.current_service = Service::LambdaApplications;
9453 app.mode = Mode::ColumnSelector;
9454 app.column_selector_index = 0;
9455
9456 let page_size_idx = app.lambda_application_column_ids.len() + 2;
9457 app.handle_action(Action::NextPreferences);
9458 assert_eq!(app.column_selector_index, page_size_idx);
9459
9460 app.handle_action(Action::NextPreferences);
9461 assert_eq!(app.column_selector_index, 0);
9462 }
9463
9464 #[test]
9465 fn test_next_preferences_ecr_images() {
9466 let mut app = test_app();
9467 app.current_service = Service::EcrRepositories;
9468 app.ecr_state.current_repository = Some("test-repo".to_string());
9469 app.mode = Mode::ColumnSelector;
9470 app.column_selector_index = 0;
9471
9472 let page_size_idx = app.ecr_image_column_ids.len() + 2;
9473 app.handle_action(Action::NextPreferences);
9474 assert_eq!(app.column_selector_index, page_size_idx);
9475
9476 app.handle_action(Action::NextPreferences);
9477 assert_eq!(app.column_selector_index, 0);
9478 }
9479
9480 #[test]
9481 fn test_cloudformation_next_item() {
9482 let mut app = test_app();
9483 app.current_service = Service::CloudFormationStacks;
9484 app.service_selected = true;
9485 app.mode = Mode::Normal;
9486 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9487 app.cfn_state.table.items = vec![
9488 CfnStack {
9489 name: "stack1".to_string(),
9490 stack_id: "id1".to_string(),
9491 status: "CREATE_COMPLETE".to_string(),
9492 created_time: "2024-01-01".to_string(),
9493 updated_time: String::new(),
9494 deleted_time: String::new(),
9495 drift_status: String::new(),
9496 last_drift_check_time: String::new(),
9497 status_reason: String::new(),
9498 description: String::new(),
9499 detailed_status: String::new(),
9500 root_stack: String::new(),
9501 parent_stack: String::new(),
9502 termination_protection: false,
9503 iam_role: String::new(),
9504 tags: Vec::new(),
9505 stack_policy: String::new(),
9506 rollback_monitoring_time: String::new(),
9507 rollback_alarms: Vec::new(),
9508 notification_arns: Vec::new(),
9509 },
9510 CfnStack {
9511 name: "stack2".to_string(),
9512 stack_id: "id2".to_string(),
9513 status: "UPDATE_COMPLETE".to_string(),
9514 created_time: "2024-01-02".to_string(),
9515 updated_time: String::new(),
9516 deleted_time: String::new(),
9517 drift_status: String::new(),
9518 last_drift_check_time: String::new(),
9519 status_reason: String::new(),
9520 description: String::new(),
9521 detailed_status: String::new(),
9522 root_stack: String::new(),
9523 parent_stack: String::new(),
9524 termination_protection: false,
9525 iam_role: String::new(),
9526 tags: Vec::new(),
9527 stack_policy: String::new(),
9528 rollback_monitoring_time: String::new(),
9529 rollback_alarms: Vec::new(),
9530 notification_arns: Vec::new(),
9531 },
9532 ];
9533 app.cfn_state.table.reset();
9534
9535 app.handle_action(Action::NextItem);
9536 assert_eq!(app.cfn_state.table.selected, 1);
9537
9538 app.handle_action(Action::NextItem);
9539 assert_eq!(app.cfn_state.table.selected, 1); }
9541
9542 #[test]
9543 fn test_cloudformation_prev_item() {
9544 let mut app = test_app();
9545 app.current_service = Service::CloudFormationStacks;
9546 app.service_selected = true;
9547 app.mode = Mode::Normal;
9548 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9549 app.cfn_state.table.items = vec![
9550 CfnStack {
9551 name: "stack1".to_string(),
9552 stack_id: "id1".to_string(),
9553 status: "CREATE_COMPLETE".to_string(),
9554 created_time: "2024-01-01".to_string(),
9555 updated_time: String::new(),
9556 deleted_time: String::new(),
9557 drift_status: String::new(),
9558 last_drift_check_time: String::new(),
9559 status_reason: String::new(),
9560 description: String::new(),
9561 detailed_status: String::new(),
9562 root_stack: String::new(),
9563 parent_stack: String::new(),
9564 termination_protection: false,
9565 iam_role: String::new(),
9566 tags: Vec::new(),
9567 stack_policy: String::new(),
9568 rollback_monitoring_time: String::new(),
9569 rollback_alarms: Vec::new(),
9570 notification_arns: Vec::new(),
9571 },
9572 CfnStack {
9573 name: "stack2".to_string(),
9574 stack_id: "id2".to_string(),
9575 status: "UPDATE_COMPLETE".to_string(),
9576 created_time: "2024-01-02".to_string(),
9577 updated_time: String::new(),
9578 deleted_time: String::new(),
9579 drift_status: String::new(),
9580 last_drift_check_time: String::new(),
9581 status_reason: String::new(),
9582 description: String::new(),
9583 detailed_status: String::new(),
9584 root_stack: String::new(),
9585 parent_stack: String::new(),
9586 termination_protection: false,
9587 iam_role: String::new(),
9588 tags: Vec::new(),
9589 stack_policy: String::new(),
9590 rollback_monitoring_time: String::new(),
9591 rollback_alarms: Vec::new(),
9592 notification_arns: Vec::new(),
9593 },
9594 ];
9595 app.cfn_state.table.selected = 1;
9596
9597 app.handle_action(Action::PrevItem);
9598 assert_eq!(app.cfn_state.table.selected, 0);
9599
9600 app.handle_action(Action::PrevItem);
9601 assert_eq!(app.cfn_state.table.selected, 0); }
9603
9604 #[test]
9605 fn test_cloudformation_page_down() {
9606 let mut app = test_app();
9607 app.current_service = Service::CloudFormationStacks;
9608 app.service_selected = true;
9609 app.mode = Mode::Normal;
9610 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9611
9612 for i in 0..20 {
9614 app.cfn_state.table.items.push(CfnStack {
9615 name: format!("stack{}", i),
9616 stack_id: format!("id{}", i),
9617 status: "CREATE_COMPLETE".to_string(),
9618 created_time: format!("2024-01-{:02}", i + 1),
9619 updated_time: String::new(),
9620 deleted_time: String::new(),
9621 drift_status: String::new(),
9622 last_drift_check_time: String::new(),
9623 status_reason: String::new(),
9624 description: String::new(),
9625 detailed_status: String::new(),
9626 root_stack: String::new(),
9627 parent_stack: String::new(),
9628 termination_protection: false,
9629 iam_role: String::new(),
9630 tags: Vec::new(),
9631 stack_policy: String::new(),
9632 rollback_monitoring_time: String::new(),
9633 rollback_alarms: Vec::new(),
9634 notification_arns: Vec::new(),
9635 });
9636 }
9637 app.cfn_state.table.reset();
9638
9639 app.handle_action(Action::PageDown);
9640 assert_eq!(app.cfn_state.table.selected, 10);
9641
9642 app.handle_action(Action::PageDown);
9643 assert_eq!(app.cfn_state.table.selected, 19); }
9645
9646 #[test]
9647 fn test_cloudformation_page_up() {
9648 let mut app = test_app();
9649 app.current_service = Service::CloudFormationStacks;
9650 app.service_selected = true;
9651 app.mode = Mode::Normal;
9652 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9653
9654 for i in 0..20 {
9656 app.cfn_state.table.items.push(CfnStack {
9657 name: format!("stack{}", i),
9658 stack_id: format!("id{}", i),
9659 status: "CREATE_COMPLETE".to_string(),
9660 created_time: format!("2024-01-{:02}", i + 1),
9661 updated_time: String::new(),
9662 deleted_time: String::new(),
9663 drift_status: String::new(),
9664 last_drift_check_time: String::new(),
9665 status_reason: String::new(),
9666 description: String::new(),
9667 detailed_status: String::new(),
9668 root_stack: String::new(),
9669 parent_stack: String::new(),
9670 termination_protection: false,
9671 iam_role: String::new(),
9672 tags: Vec::new(),
9673 stack_policy: String::new(),
9674 rollback_monitoring_time: String::new(),
9675 rollback_alarms: Vec::new(),
9676 notification_arns: Vec::new(),
9677 });
9678 }
9679 app.cfn_state.table.selected = 15;
9680
9681 app.handle_action(Action::PageUp);
9682 assert_eq!(app.cfn_state.table.selected, 5);
9683
9684 app.handle_action(Action::PageUp);
9685 assert_eq!(app.cfn_state.table.selected, 0); }
9687
9688 #[test]
9689 fn test_cloudformation_filter_input() {
9690 let mut app = test_app();
9691 app.current_service = Service::CloudFormationStacks;
9692 app.service_selected = true;
9693 app.mode = Mode::Normal;
9694
9695 app.handle_action(Action::StartFilter);
9696 assert_eq!(app.mode, Mode::FilterInput);
9697
9698 app.cfn_state.table.filter = "test".to_string();
9700 assert_eq!(app.cfn_state.table.filter, "test");
9701 }
9702
9703 #[test]
9704 fn test_cloudformation_filter_applies() {
9705 let mut app = test_app();
9706 app.current_service = Service::CloudFormationStacks;
9707 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9708 app.cfn_state.table.items = vec![
9709 CfnStack {
9710 name: "prod-stack".to_string(),
9711 stack_id: "id1".to_string(),
9712 status: "CREATE_COMPLETE".to_string(),
9713 created_time: "2024-01-01".to_string(),
9714 updated_time: String::new(),
9715 deleted_time: String::new(),
9716 drift_status: String::new(),
9717 last_drift_check_time: String::new(),
9718 status_reason: String::new(),
9719 description: "Production stack".to_string(),
9720 detailed_status: String::new(),
9721 root_stack: String::new(),
9722 parent_stack: String::new(),
9723 termination_protection: false,
9724 iam_role: String::new(),
9725 tags: Vec::new(),
9726 stack_policy: String::new(),
9727 rollback_monitoring_time: String::new(),
9728 rollback_alarms: Vec::new(),
9729 notification_arns: Vec::new(),
9730 },
9731 CfnStack {
9732 name: "dev-stack".to_string(),
9733 stack_id: "id2".to_string(),
9734 status: "UPDATE_COMPLETE".to_string(),
9735 created_time: "2024-01-02".to_string(),
9736 updated_time: String::new(),
9737 deleted_time: String::new(),
9738 drift_status: String::new(),
9739 last_drift_check_time: String::new(),
9740 status_reason: String::new(),
9741 description: "Development stack".to_string(),
9742 detailed_status: String::new(),
9743 root_stack: String::new(),
9744 parent_stack: String::new(),
9745 termination_protection: false,
9746 iam_role: String::new(),
9747 tags: Vec::new(),
9748 stack_policy: String::new(),
9749 rollback_monitoring_time: String::new(),
9750 rollback_alarms: Vec::new(),
9751 notification_arns: Vec::new(),
9752 },
9753 ];
9754 app.cfn_state.table.filter = "prod".to_string();
9755
9756 let filtered = app.filtered_cloudformation_stacks();
9757 assert_eq!(filtered.len(), 1);
9758 assert_eq!(filtered[0].name, "prod-stack");
9759 }
9760
9761 #[test]
9762 fn test_cloudformation_right_arrow_expands() {
9763 let mut app = test_app();
9764 app.current_service = Service::CloudFormationStacks;
9765 app.service_selected = true;
9766 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9767 app.cfn_state.table.items = vec![CfnStack {
9768 name: "test-stack".to_string(),
9769 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
9770 .to_string(),
9771 status: "CREATE_COMPLETE".to_string(),
9772 created_time: "2024-01-01".to_string(),
9773 updated_time: String::new(),
9774 deleted_time: String::new(),
9775 drift_status: String::new(),
9776 last_drift_check_time: String::new(),
9777 status_reason: String::new(),
9778 description: "Test stack".to_string(),
9779 detailed_status: String::new(),
9780 root_stack: String::new(),
9781 parent_stack: String::new(),
9782 termination_protection: false,
9783 iam_role: String::new(),
9784 tags: Vec::new(),
9785 stack_policy: String::new(),
9786 rollback_monitoring_time: String::new(),
9787 rollback_alarms: Vec::new(),
9788 notification_arns: Vec::new(),
9789 }];
9790 app.cfn_state.table.reset();
9791
9792 assert_eq!(app.cfn_state.table.expanded_item, None);
9793
9794 app.handle_action(Action::NextPane);
9795 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
9796 }
9797
9798 #[test]
9799 fn test_cloudformation_left_arrow_collapses() {
9800 let mut app = test_app();
9801 app.current_service = Service::CloudFormationStacks;
9802 app.service_selected = true;
9803 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9804 app.cfn_state.table.items = vec![CfnStack {
9805 name: "test-stack".to_string(),
9806 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
9807 .to_string(),
9808 status: "CREATE_COMPLETE".to_string(),
9809 created_time: "2024-01-01".to_string(),
9810 updated_time: String::new(),
9811 deleted_time: String::new(),
9812 drift_status: String::new(),
9813 last_drift_check_time: String::new(),
9814 status_reason: String::new(),
9815 description: "Test stack".to_string(),
9816 detailed_status: String::new(),
9817 root_stack: String::new(),
9818 parent_stack: String::new(),
9819 termination_protection: false,
9820 iam_role: String::new(),
9821 tags: Vec::new(),
9822 stack_policy: String::new(),
9823 rollback_monitoring_time: String::new(),
9824 rollback_alarms: Vec::new(),
9825 notification_arns: Vec::new(),
9826 }];
9827 app.cfn_state.table.reset();
9828 app.cfn_state.table.expanded_item = Some(0);
9829
9830 app.handle_action(Action::PrevPane);
9831 assert_eq!(app.cfn_state.table.expanded_item, None);
9832 }
9833
9834 #[test]
9835 fn test_cloudformation_enter_drills_into_stack() {
9836 let mut app = test_app();
9837 app.current_service = Service::CloudFormationStacks;
9838 app.service_selected = true;
9839 app.mode = Mode::Normal;
9840 app.tabs = vec![Tab {
9841 service: Service::CloudFormationStacks,
9842 title: "CloudFormation > Stacks".to_string(),
9843 breadcrumb: "CloudFormation > Stacks".to_string(),
9844 }];
9845 app.current_tab = 0;
9846 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9847 app.cfn_state.table.items = vec![CfnStack {
9848 name: "test-stack".to_string(),
9849 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
9850 .to_string(),
9851 status: "CREATE_COMPLETE".to_string(),
9852 created_time: "2024-01-01".to_string(),
9853 updated_time: String::new(),
9854 deleted_time: String::new(),
9855 drift_status: String::new(),
9856 last_drift_check_time: String::new(),
9857 status_reason: String::new(),
9858 description: "Test stack".to_string(),
9859 detailed_status: String::new(),
9860 root_stack: String::new(),
9861 parent_stack: String::new(),
9862 termination_protection: false,
9863 iam_role: String::new(),
9864 tags: Vec::new(),
9865 stack_policy: String::new(),
9866 rollback_monitoring_time: String::new(),
9867 rollback_alarms: Vec::new(),
9868 notification_arns: Vec::new(),
9869 }];
9870 app.cfn_state.table.reset();
9871
9872 let filtered = app.filtered_cloudformation_stacks();
9874 assert_eq!(filtered.len(), 1);
9875 assert_eq!(filtered[0].name, "test-stack");
9876
9877 assert_eq!(app.cfn_state.current_stack, None);
9878
9879 app.handle_action(Action::Select);
9881 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
9882 }
9883
9884 #[test]
9885 fn test_cloudformation_copy_to_clipboard() {
9886 let mut app = test_app();
9887 app.current_service = Service::CloudFormationStacks;
9888 app.service_selected = true;
9889 app.mode = Mode::Normal;
9890 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9891 app.cfn_state.table.items = vec![
9892 CfnStack {
9893 name: "stack1".to_string(),
9894 stack_id: "id1".to_string(),
9895 status: "CREATE_COMPLETE".to_string(),
9896 created_time: "2024-01-01".to_string(),
9897 updated_time: String::new(),
9898 deleted_time: String::new(),
9899 drift_status: String::new(),
9900 last_drift_check_time: String::new(),
9901 status_reason: String::new(),
9902 description: String::new(),
9903 detailed_status: String::new(),
9904 root_stack: String::new(),
9905 parent_stack: String::new(),
9906 termination_protection: false,
9907 iam_role: String::new(),
9908 tags: Vec::new(),
9909 stack_policy: String::new(),
9910 rollback_monitoring_time: String::new(),
9911 rollback_alarms: Vec::new(),
9912 notification_arns: Vec::new(),
9913 },
9914 CfnStack {
9915 name: "stack2".to_string(),
9916 stack_id: "id2".to_string(),
9917 status: "UPDATE_COMPLETE".to_string(),
9918 created_time: "2024-01-02".to_string(),
9919 updated_time: String::new(),
9920 deleted_time: String::new(),
9921 drift_status: String::new(),
9922 last_drift_check_time: String::new(),
9923 status_reason: String::new(),
9924 description: String::new(),
9925 detailed_status: String::new(),
9926 root_stack: String::new(),
9927 parent_stack: String::new(),
9928 termination_protection: false,
9929 iam_role: String::new(),
9930 tags: Vec::new(),
9931 stack_policy: String::new(),
9932 rollback_monitoring_time: String::new(),
9933 rollback_alarms: Vec::new(),
9934 notification_arns: Vec::new(),
9935 },
9936 ];
9937
9938 assert!(!app.snapshot_requested);
9939 app.handle_action(Action::CopyToClipboard);
9940
9941 assert!(app.snapshot_requested);
9943 }
9944
9945 #[test]
9946 fn test_cloudformation_expansion_shows_all_visible_columns() {
9947 let mut app = test_app();
9948 app.current_service = Service::CloudFormationStacks;
9949 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9950 app.cfn_state.table.items = vec![CfnStack {
9951 name: "test-stack".to_string(),
9952 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
9953 .to_string(),
9954 status: "CREATE_COMPLETE".to_string(),
9955 created_time: "2024-01-01".to_string(),
9956 updated_time: "2024-01-02".to_string(),
9957 deleted_time: String::new(),
9958 drift_status: "IN_SYNC".to_string(),
9959 last_drift_check_time: "2024-01-03".to_string(),
9960 status_reason: String::new(),
9961 description: "Test description".to_string(),
9962 detailed_status: String::new(),
9963 root_stack: String::new(),
9964 parent_stack: String::new(),
9965 termination_protection: false,
9966 iam_role: String::new(),
9967 tags: Vec::new(),
9968 stack_policy: String::new(),
9969 rollback_monitoring_time: String::new(),
9970 rollback_alarms: Vec::new(),
9971 notification_arns: Vec::new(),
9972 }];
9973
9974 app.cfn_visible_column_ids = [
9976 CfnColumn::Name,
9977 CfnColumn::Status,
9978 CfnColumn::CreatedTime,
9979 CfnColumn::Description,
9980 ]
9981 .iter()
9982 .map(|c| c.id())
9983 .collect();
9984
9985 app.cfn_state.table.expanded_item = Some(0);
9986
9987 assert_eq!(app.cfn_visible_column_ids.len(), 4);
9990 assert!(app.cfn_state.table.has_expanded_item());
9991 }
9992
9993 #[test]
9994 fn test_cloudformation_empty_list_shows_page_1() {
9995 let mut app = test_app();
9996 app.current_service = Service::CloudFormationStacks;
9997 app.cfn_state.table.items = vec![];
9998
9999 let filtered = app.filtered_cloudformation_stacks();
10000 assert_eq!(filtered.len(), 0);
10001
10002 let page_size = app.cfn_state.table.page_size.value();
10004 let total_pages = filtered.len().div_ceil(page_size);
10005 assert_eq!(total_pages, 0);
10006
10007 }
10010}
10011
10012impl App {
10013 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
10014 let mut all = AwsRegion::all();
10015
10016 for region in &mut all {
10018 region.latency_ms = self.region_latencies.get(region.code).copied();
10019 }
10020
10021 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
10023 all
10024 } else {
10025 let filter_lower = self.region_filter.to_lowercase();
10026 all.into_iter()
10027 .filter(|r| {
10028 r.name.to_lowercase().contains(&filter_lower)
10029 || r.code.to_lowercase().contains(&filter_lower)
10030 || r.group.to_lowercase().contains(&filter_lower)
10031 })
10032 .collect()
10033 };
10034
10035 let mut sorted = filtered;
10037 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
10038 sorted
10039 }
10040
10041 pub fn measure_region_latencies(&mut self) {
10042 use std::time::Instant;
10043 self.region_latencies.clear();
10044
10045 let regions = AwsRegion::all();
10046 let start_all = Instant::now();
10047 tracing::info!("Starting latency measurement for {} regions", regions.len());
10048
10049 let handles: Vec<_> = regions
10050 .iter()
10051 .map(|region| {
10052 let code = region.code.to_string();
10053 std::thread::spawn(move || {
10054 let endpoint = format!("https://sts.{}.amazonaws.com", code);
10056 let start = Instant::now();
10057
10058 match ureq::get(&endpoint)
10059 .timeout(std::time::Duration::from_secs(2))
10060 .call()
10061 {
10062 Ok(_) => {
10063 let latency = start.elapsed().as_millis() as u64;
10064 Some((code, latency))
10065 }
10066 Err(e) => {
10067 tracing::debug!("Failed to measure {}: {}", code, e);
10068 Some((code, 9999))
10069 }
10070 }
10071 })
10072 })
10073 .collect();
10074
10075 for handle in handles {
10076 if let Ok(Some((code, latency))) = handle.join() {
10077 self.region_latencies.insert(code, latency);
10078 }
10079 }
10080
10081 tracing::info!(
10082 "Measured {} regions in {:?}",
10083 self.region_latencies.len(),
10084 start_all.elapsed()
10085 );
10086 }
10087
10088 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
10089 crate::aws::filter_profiles(&self.available_profiles, &self.profile_filter)
10090 }
10091
10092 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
10093 if self.session_filter.is_empty() {
10094 return self.sessions.iter().collect();
10095 }
10096 let filter_lower = self.session_filter.to_lowercase();
10097 self.sessions
10098 .iter()
10099 .filter(|s| {
10100 s.profile.to_lowercase().contains(&filter_lower)
10101 || s.region.to_lowercase().contains(&filter_lower)
10102 || s.account_id.to_lowercase().contains(&filter_lower)
10103 || s.role_arn.to_lowercase().contains(&filter_lower)
10104 })
10105 .collect()
10106 }
10107
10108 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
10109 if self.tab_filter.is_empty() {
10110 return self.tabs.iter().enumerate().collect();
10111 }
10112 let filter_lower = self.tab_filter.to_lowercase();
10113 self.tabs
10114 .iter()
10115 .enumerate()
10116 .filter(|(_, tab)| {
10117 tab.title.to_lowercase().contains(&filter_lower)
10118 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
10119 })
10120 .collect()
10121 }
10122
10123 pub fn load_aws_profiles() -> Vec<AwsProfile> {
10124 AwsProfile::load_all()
10125 }
10126
10127 pub async fn fetch_profile_accounts(&mut self) {
10128 for profile in &mut self.available_profiles {
10129 if profile.account.is_none() {
10130 let region = profile
10131 .region
10132 .clone()
10133 .unwrap_or_else(|| "us-east-1".to_string());
10134 if let Ok(account) =
10135 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
10136 {
10137 profile.account = Some(account);
10138 }
10139 }
10140 }
10141 }
10142
10143 fn save_current_session(&mut self) {
10144 if self.tabs.is_empty() {
10146 if let Some(ref session) = self.current_session {
10147 let _ = session.delete();
10148 self.current_session = None;
10149 }
10150 return;
10151 }
10152
10153 let session = if let Some(ref mut current) = self.current_session {
10154 current.tabs = self
10156 .tabs
10157 .iter()
10158 .map(|t| SessionTab {
10159 service: format!("{:?}", t.service),
10160 title: t.title.clone(),
10161 breadcrumb: t.breadcrumb.clone(),
10162 filter: match t.service {
10163 Service::CloudWatchLogGroups => {
10164 Some(self.log_groups_state.log_groups.filter.clone())
10165 }
10166 _ => None,
10167 },
10168 selected_item: None,
10169 })
10170 .collect();
10171 current.clone()
10172 } else {
10173 let mut session = Session::new(
10175 self.profile.clone(),
10176 self.region.clone(),
10177 self.config.account_id.clone(),
10178 self.config.role_arn.clone(),
10179 );
10180 session.tabs = self
10181 .tabs
10182 .iter()
10183 .map(|t| SessionTab {
10184 service: format!("{:?}", t.service),
10185 title: t.title.clone(),
10186 breadcrumb: t.breadcrumb.clone(),
10187 filter: match t.service {
10188 Service::CloudWatchLogGroups => {
10189 Some(self.log_groups_state.log_groups.filter.clone())
10190 }
10191 _ => None,
10192 },
10193 selected_item: None,
10194 })
10195 .collect();
10196 self.current_session = Some(session.clone());
10197 session
10198 };
10199
10200 let _ = session.save();
10201 }
10202}
10203
10204#[cfg(test)]
10205mod iam_policy_view_tests {
10206 use super::*;
10207 use test_helpers::*;
10208
10209 #[test]
10210 fn test_enter_opens_policy_view() {
10211 let mut app = test_app();
10212 app.current_service = Service::IamRoles;
10213 app.service_selected = true;
10214 app.mode = Mode::Normal;
10215 app.view_mode = ViewMode::Detail;
10216 app.iam_state.current_role = Some("TestRole".to_string());
10217 app.iam_state.policies.items = vec![crate::iam::Policy {
10218 policy_name: "TestPolicy".to_string(),
10219 policy_type: "Inline".to_string(),
10220 attached_via: "Direct".to_string(),
10221 attached_entities: "1".to_string(),
10222 description: "Test".to_string(),
10223 creation_time: "2023-01-01".to_string(),
10224 edited_time: "2023-01-01".to_string(),
10225 policy_arn: None,
10226 }];
10227 app.iam_state.policies.reset();
10228
10229 app.handle_action(Action::Select);
10230
10231 assert_eq!(app.view_mode, ViewMode::PolicyView);
10232 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
10233 assert_eq!(app.iam_state.policy_scroll, 0);
10234 assert!(app.iam_state.policies.loading);
10235 }
10236
10237 #[test]
10238 fn test_escape_closes_policy_view() {
10239 let mut app = test_app();
10240 app.current_service = Service::IamRoles;
10241 app.service_selected = true;
10242 app.mode = Mode::Normal;
10243 app.view_mode = ViewMode::PolicyView;
10244 app.iam_state.current_role = Some("TestRole".to_string());
10245 app.iam_state.current_policy = Some("TestPolicy".to_string());
10246 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
10247 app.iam_state.policy_scroll = 5;
10248
10249 app.handle_action(Action::PrevPane);
10250
10251 assert_eq!(app.view_mode, ViewMode::Detail);
10252 assert_eq!(app.iam_state.current_policy, None);
10253 assert_eq!(app.iam_state.policy_document, "");
10254 assert_eq!(app.iam_state.policy_scroll, 0);
10255 }
10256
10257 #[test]
10258 fn test_ctrl_d_scrolls_down_in_policy_view() {
10259 let mut app = test_app();
10260 app.current_service = Service::IamRoles;
10261 app.service_selected = true;
10262 app.mode = Mode::Normal;
10263 app.view_mode = ViewMode::PolicyView;
10264 app.iam_state.current_role = Some("TestRole".to_string());
10265 app.iam_state.current_policy = Some("TestPolicy".to_string());
10266 app.iam_state.policy_document = (0..100)
10267 .map(|i| format!("line {}", i))
10268 .collect::<Vec<_>>()
10269 .join("\n");
10270 app.iam_state.policy_scroll = 0;
10271
10272 app.handle_action(Action::ScrollDown);
10273
10274 assert_eq!(app.iam_state.policy_scroll, 10);
10275
10276 app.handle_action(Action::ScrollDown);
10277
10278 assert_eq!(app.iam_state.policy_scroll, 20);
10279 }
10280
10281 #[test]
10282 fn test_ctrl_u_scrolls_up_in_policy_view() {
10283 let mut app = test_app();
10284 app.current_service = Service::IamRoles;
10285 app.service_selected = true;
10286 app.mode = Mode::Normal;
10287 app.view_mode = ViewMode::PolicyView;
10288 app.iam_state.current_role = Some("TestRole".to_string());
10289 app.iam_state.current_policy = Some("TestPolicy".to_string());
10290 app.iam_state.policy_document = (0..100)
10291 .map(|i| format!("line {}", i))
10292 .collect::<Vec<_>>()
10293 .join("\n");
10294 app.iam_state.policy_scroll = 30;
10295
10296 app.handle_action(Action::ScrollUp);
10297
10298 assert_eq!(app.iam_state.policy_scroll, 20);
10299
10300 app.handle_action(Action::ScrollUp);
10301
10302 assert_eq!(app.iam_state.policy_scroll, 10);
10303 }
10304
10305 #[test]
10306 fn test_scroll_does_not_go_negative() {
10307 let mut app = test_app();
10308 app.current_service = Service::IamRoles;
10309 app.service_selected = true;
10310 app.mode = Mode::Normal;
10311 app.view_mode = ViewMode::PolicyView;
10312 app.iam_state.current_role = Some("TestRole".to_string());
10313 app.iam_state.current_policy = Some("TestPolicy".to_string());
10314 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
10315 app.iam_state.policy_scroll = 0;
10316
10317 app.handle_action(Action::ScrollUp);
10318
10319 assert_eq!(app.iam_state.policy_scroll, 0);
10320 }
10321
10322 #[test]
10323 fn test_scroll_does_not_exceed_max() {
10324 let mut app = test_app();
10325 app.current_service = Service::IamRoles;
10326 app.service_selected = true;
10327 app.mode = Mode::Normal;
10328 app.view_mode = ViewMode::PolicyView;
10329 app.iam_state.current_role = Some("TestRole".to_string());
10330 app.iam_state.current_policy = Some("TestPolicy".to_string());
10331 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
10332 app.iam_state.policy_scroll = 0;
10333
10334 app.handle_action(Action::ScrollDown);
10335
10336 assert_eq!(app.iam_state.policy_scroll, 2); }
10338
10339 #[test]
10340 fn test_policy_view_console_url() {
10341 let mut app = test_app();
10342 app.current_service = Service::IamRoles;
10343 app.service_selected = true;
10344 app.view_mode = ViewMode::PolicyView;
10345 app.iam_state.current_role = Some("TestRole".to_string());
10346 app.iam_state.current_policy = Some("TestPolicy".to_string());
10347
10348 let url = app.get_console_url();
10349
10350 assert!(url.contains("us-east-1.console.aws.amazon.com"));
10351 assert!(url.contains("/roles/details/TestRole"));
10352 assert!(url.contains("/editPolicy/TestPolicy"));
10353 assert!(url.contains("step=addPermissions"));
10354 }
10355
10356 #[test]
10357 fn test_esc_from_policy_view_goes_to_role_detail() {
10358 let mut app = test_app();
10359 app.current_service = Service::IamRoles;
10360 app.service_selected = true;
10361 app.mode = Mode::Normal;
10362 app.view_mode = ViewMode::PolicyView;
10363 app.iam_state.current_role = Some("TestRole".to_string());
10364 app.iam_state.current_policy = Some("TestPolicy".to_string());
10365 app.iam_state.policy_document = "test".to_string();
10366 app.iam_state.policy_scroll = 5;
10367
10368 app.handle_action(Action::GoBack);
10369
10370 assert_eq!(app.view_mode, ViewMode::Detail);
10371 assert_eq!(app.iam_state.current_policy, None);
10372 assert_eq!(app.iam_state.policy_document, "");
10373 assert_eq!(app.iam_state.policy_scroll, 0);
10374 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
10375 }
10376
10377 #[test]
10378 fn test_esc_from_role_detail_goes_to_role_list() {
10379 let mut app = test_app();
10380 app.current_service = Service::IamRoles;
10381 app.service_selected = true;
10382 app.mode = Mode::Normal;
10383 app.view_mode = ViewMode::Detail;
10384 app.iam_state.current_role = Some("TestRole".to_string());
10385
10386 app.handle_action(Action::GoBack);
10387
10388 assert_eq!(app.iam_state.current_role, None);
10389 }
10390
10391 #[test]
10392 fn test_right_arrow_expands_policy_row() {
10393 let mut app = test_app();
10394 app.current_service = Service::IamRoles;
10395 app.service_selected = true;
10396 app.mode = Mode::Normal;
10397 app.view_mode = ViewMode::Detail;
10398 app.iam_state.current_role = Some("TestRole".to_string());
10399 app.iam_state.policies.items = vec![crate::iam::Policy {
10400 policy_name: "TestPolicy".to_string(),
10401 policy_type: "Inline".to_string(),
10402 attached_via: "Direct".to_string(),
10403 attached_entities: "1".to_string(),
10404 description: "Test".to_string(),
10405 creation_time: "2023-01-01".to_string(),
10406 edited_time: "2023-01-01".to_string(),
10407 policy_arn: None,
10408 }];
10409 app.iam_state.policies.reset();
10410
10411 app.handle_action(Action::NextPane);
10412
10413 assert_eq!(app.view_mode, ViewMode::Detail);
10415 assert_eq!(app.iam_state.current_policy, None);
10416 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
10417 }
10418}
10419
10420#[cfg(test)]
10421mod tab_filter_tests {
10422 use super::*;
10423 use test_helpers::*;
10424
10425 #[test]
10426 fn test_space_t_opens_tab_picker() {
10427 let mut app = test_app();
10428 app.tabs = vec![
10429 Tab {
10430 service: Service::CloudWatchLogGroups,
10431 title: "Tab 1".to_string(),
10432 breadcrumb: "CloudWatch > Log groups".to_string(),
10433 },
10434 Tab {
10435 service: Service::S3Buckets,
10436 title: "Tab 2".to_string(),
10437 breadcrumb: "S3 > Buckets".to_string(),
10438 },
10439 ];
10440 app.current_tab = 0;
10441
10442 app.handle_action(Action::OpenTabPicker);
10443
10444 assert_eq!(app.mode, Mode::TabPicker);
10445 assert_eq!(app.tab_picker_selected, 0);
10446 }
10447
10448 #[test]
10449 fn test_tab_filter_works() {
10450 let mut app = test_app();
10451 app.tabs = vec![
10452 Tab {
10453 service: Service::CloudWatchLogGroups,
10454 title: "CloudWatch Logs".to_string(),
10455 breadcrumb: "CloudWatch > Log groups".to_string(),
10456 },
10457 Tab {
10458 service: Service::S3Buckets,
10459 title: "S3 Buckets".to_string(),
10460 breadcrumb: "S3 > Buckets".to_string(),
10461 },
10462 Tab {
10463 service: Service::CloudWatchAlarms,
10464 title: "CloudWatch Alarms".to_string(),
10465 breadcrumb: "CloudWatch > Alarms".to_string(),
10466 },
10467 ];
10468 app.mode = Mode::TabPicker;
10469
10470 app.handle_action(Action::FilterInput('s'));
10472 app.handle_action(Action::FilterInput('3'));
10473
10474 let filtered = app.get_filtered_tabs();
10475 assert_eq!(filtered.len(), 1);
10476 assert_eq!(filtered[0].1.title, "S3 Buckets");
10477 }
10478
10479 #[test]
10480 fn test_tab_filter_by_breadcrumb() {
10481 let mut app = test_app();
10482 app.tabs = vec![
10483 Tab {
10484 service: Service::CloudWatchLogGroups,
10485 title: "Tab 1".to_string(),
10486 breadcrumb: "CloudWatch > Log groups".to_string(),
10487 },
10488 Tab {
10489 service: Service::S3Buckets,
10490 title: "Tab 2".to_string(),
10491 breadcrumb: "S3 > Buckets".to_string(),
10492 },
10493 ];
10494 app.mode = Mode::TabPicker;
10495
10496 app.handle_action(Action::FilterInput('c'));
10498 app.handle_action(Action::FilterInput('l'));
10499 app.handle_action(Action::FilterInput('o'));
10500 app.handle_action(Action::FilterInput('u'));
10501 app.handle_action(Action::FilterInput('d'));
10502
10503 let filtered = app.get_filtered_tabs();
10504 assert_eq!(filtered.len(), 1);
10505 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
10506 }
10507
10508 #[test]
10509 fn test_tab_filter_backspace() {
10510 let mut app = test_app();
10511 app.tabs = vec![
10512 Tab {
10513 service: Service::CloudWatchLogGroups,
10514 title: "CloudWatch Logs".to_string(),
10515 breadcrumb: "CloudWatch > Log groups".to_string(),
10516 },
10517 Tab {
10518 service: Service::S3Buckets,
10519 title: "S3 Buckets".to_string(),
10520 breadcrumb: "S3 > Buckets".to_string(),
10521 },
10522 ];
10523 app.mode = Mode::TabPicker;
10524
10525 app.handle_action(Action::FilterInput('s'));
10526 app.handle_action(Action::FilterInput('3'));
10527 assert_eq!(app.tab_filter, "s3");
10528
10529 app.handle_action(Action::FilterBackspace);
10530 assert_eq!(app.tab_filter, "s");
10531
10532 let filtered = app.get_filtered_tabs();
10533 assert_eq!(filtered.len(), 2); }
10535
10536 #[test]
10537 fn test_tab_selection_with_filter() {
10538 let mut app = test_app();
10539 app.tabs = vec![
10540 Tab {
10541 service: Service::CloudWatchLogGroups,
10542 title: "CloudWatch Logs".to_string(),
10543 breadcrumb: "CloudWatch > Log groups".to_string(),
10544 },
10545 Tab {
10546 service: Service::S3Buckets,
10547 title: "S3 Buckets".to_string(),
10548 breadcrumb: "S3 > Buckets".to_string(),
10549 },
10550 ];
10551 app.mode = Mode::TabPicker;
10552 app.current_tab = 0;
10553
10554 app.handle_action(Action::FilterInput('s'));
10556 app.handle_action(Action::FilterInput('3'));
10557
10558 app.handle_action(Action::Select);
10560
10561 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
10563 assert_eq!(app.tab_filter, ""); }
10565}
10566
10567#[cfg(test)]
10568mod region_latency_tests {
10569 use super::*;
10570 use test_helpers::*;
10571
10572 #[test]
10573 fn test_regions_sorted_by_latency() {
10574 let mut app = test_app();
10575
10576 app.region_latencies.insert("us-west-2".to_string(), 50);
10578 app.region_latencies.insert("us-east-1".to_string(), 10);
10579 app.region_latencies.insert("eu-west-1".to_string(), 100);
10580
10581 let filtered = app.get_filtered_regions();
10582
10583 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
10585
10586 assert!(with_latency.len() >= 3);
10587 assert_eq!(with_latency[0].code, "us-east-1");
10588 assert_eq!(with_latency[0].latency_ms, Some(10));
10589 assert_eq!(with_latency[1].code, "us-west-2");
10590 assert_eq!(with_latency[1].latency_ms, Some(50));
10591 assert_eq!(with_latency[2].code, "eu-west-1");
10592 assert_eq!(with_latency[2].latency_ms, Some(100));
10593 }
10594
10595 #[test]
10596 fn test_regions_with_latency_before_without() {
10597 let mut app = test_app();
10598
10599 app.region_latencies.insert("eu-west-1".to_string(), 100);
10601
10602 let filtered = app.get_filtered_regions();
10603
10604 assert_eq!(filtered[0].code, "eu-west-1");
10606 assert_eq!(filtered[0].latency_ms, Some(100));
10607
10608 for region in &filtered[1..] {
10610 assert!(region.latency_ms.is_none());
10611 }
10612 }
10613
10614 #[test]
10615 fn test_region_filter_with_latency() {
10616 let mut app = test_app();
10617
10618 app.region_latencies.insert("us-east-1".to_string(), 10);
10619 app.region_latencies.insert("us-west-2".to_string(), 50);
10620 app.region_filter = "us".to_string();
10621
10622 let filtered = app.get_filtered_regions();
10623
10624 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
10626 assert_eq!(filtered[0].code, "us-east-1");
10627 assert_eq!(filtered[1].code, "us-west-2");
10628 }
10629
10630 #[test]
10631 fn test_latency_persists_across_filters() {
10632 let mut app = test_app();
10633
10634 app.region_latencies.insert("us-east-1".to_string(), 10);
10635
10636 app.region_filter = "eu".to_string();
10638 let filtered = app.get_filtered_regions();
10639 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
10640
10641 app.region_filter.clear();
10643 let all = app.get_filtered_regions();
10644
10645 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
10647 assert_eq!(us_east.latency_ms, Some(10));
10648 }
10649
10650 #[test]
10651 fn test_measure_region_latencies_clears_previous() {
10652 let mut app = test_app();
10653
10654 app.region_latencies.insert("us-east-1".to_string(), 100);
10656 app.region_latencies.insert("eu-west-1".to_string(), 200);
10657
10658 app.measure_region_latencies();
10660
10661 assert!(
10663 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
10664 );
10665 }
10666
10667 #[test]
10668 fn test_regions_with_latency_sorted_first() {
10669 let mut app = test_app();
10670
10671 app.region_latencies.insert("us-east-1".to_string(), 50);
10673 app.region_latencies.insert("eu-west-1".to_string(), 500);
10674
10675 let filtered = app.get_filtered_regions();
10676
10677 assert!(filtered.len() > 2);
10679
10680 assert_eq!(filtered[0].code, "us-east-1");
10682 assert_eq!(filtered[0].latency_ms, Some(50));
10683 assert_eq!(filtered[1].code, "eu-west-1");
10684 assert_eq!(filtered[1].latency_ms, Some(500));
10685
10686 for region in &filtered[2..] {
10688 assert!(region.latency_ms.is_none());
10689 }
10690 }
10691
10692 #[test]
10693 fn test_regions_without_latency_sorted_as_1000ms() {
10694 let mut app = test_app();
10695
10696 app.region_latencies
10698 .insert("ap-southeast-2".to_string(), 1500);
10699 app.region_latencies.insert("us-east-1".to_string(), 50);
10701
10702 let filtered = app.get_filtered_regions();
10703
10704 assert_eq!(filtered[0].code, "us-east-1");
10706 assert_eq!(filtered[0].latency_ms, Some(50));
10707
10708 let slow_region_idx = filtered
10710 .iter()
10711 .position(|r| r.code == "ap-southeast-2")
10712 .unwrap();
10713 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
10717 assert!(region.latency_ms.is_none());
10718 }
10719 }
10720
10721 #[test]
10722 fn test_region_picker_opens_with_latencies() {
10723 let mut app = test_app();
10724
10725 app.region_filter.clear();
10727 app.region_picker_selected = 0;
10728 app.measure_region_latencies();
10729
10730 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
10733 }
10734
10735 #[test]
10736 fn test_ecr_tab_next() {
10737 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
10738 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
10739 }
10740
10741 #[test]
10742 fn test_ecr_tab_switching() {
10743 let mut app = test_app();
10744 app.current_service = Service::EcrRepositories;
10745 app.service_selected = true;
10746 app.ecr_state.tab = EcrTab::Private;
10747
10748 app.handle_action(Action::NextDetailTab);
10749 assert_eq!(app.ecr_state.tab, EcrTab::Public);
10750 assert_eq!(app.ecr_state.repositories.selected, 0);
10751
10752 app.handle_action(Action::NextDetailTab);
10753 assert_eq!(app.ecr_state.tab, EcrTab::Private);
10754 }
10755
10756 #[test]
10757 fn test_ecr_navigation() {
10758 let mut app = test_app();
10759 app.current_service = Service::EcrRepositories;
10760 app.service_selected = true;
10761 app.mode = Mode::Normal;
10762 app.ecr_state.repositories.items = vec![
10763 EcrRepository {
10764 name: "repo1".to_string(),
10765 uri: "uri1".to_string(),
10766 created_at: "2023-01-01".to_string(),
10767 tag_immutability: "MUTABLE".to_string(),
10768 encryption_type: "AES256".to_string(),
10769 },
10770 EcrRepository {
10771 name: "repo2".to_string(),
10772 uri: "uri2".to_string(),
10773 created_at: "2023-01-02".to_string(),
10774 tag_immutability: "IMMUTABLE".to_string(),
10775 encryption_type: "KMS".to_string(),
10776 },
10777 ];
10778
10779 app.handle_action(Action::NextItem);
10780 assert_eq!(app.ecr_state.repositories.selected, 1);
10781
10782 app.handle_action(Action::PrevItem);
10783 assert_eq!(app.ecr_state.repositories.selected, 0);
10784 }
10785
10786 #[test]
10787 fn test_ecr_filter() {
10788 let mut app = test_app();
10789 app.current_service = Service::EcrRepositories;
10790 app.service_selected = true;
10791 app.ecr_state.repositories.items = vec![
10792 EcrRepository {
10793 name: "my-app".to_string(),
10794 uri: "uri1".to_string(),
10795 created_at: "2023-01-01".to_string(),
10796 tag_immutability: "MUTABLE".to_string(),
10797 encryption_type: "AES256".to_string(),
10798 },
10799 EcrRepository {
10800 name: "other-service".to_string(),
10801 uri: "uri2".to_string(),
10802 created_at: "2023-01-02".to_string(),
10803 tag_immutability: "IMMUTABLE".to_string(),
10804 encryption_type: "KMS".to_string(),
10805 },
10806 ];
10807
10808 app.ecr_state.repositories.filter = "app".to_string();
10809 let filtered = app.filtered_ecr_repositories();
10810 assert_eq!(filtered.len(), 1);
10811 assert_eq!(filtered[0].name, "my-app");
10812 }
10813
10814 #[test]
10815 fn test_ecr_filter_input() {
10816 let mut app = test_app();
10817 app.current_service = Service::EcrRepositories;
10818 app.service_selected = true;
10819 app.mode = Mode::FilterInput;
10820
10821 app.handle_action(Action::FilterInput('t'));
10822 app.handle_action(Action::FilterInput('e'));
10823 app.handle_action(Action::FilterInput('s'));
10824 app.handle_action(Action::FilterInput('t'));
10825 assert_eq!(app.ecr_state.repositories.filter, "test");
10826
10827 app.handle_action(Action::FilterBackspace);
10828 assert_eq!(app.ecr_state.repositories.filter, "tes");
10829 }
10830
10831 #[test]
10832 fn test_iam_users_filter_input() {
10833 let mut app = test_app();
10834 app.current_service = Service::IamUsers;
10835 app.service_selected = true;
10836 app.mode = Mode::FilterInput;
10837
10838 app.handle_action(Action::FilterInput('a'));
10839 app.handle_action(Action::FilterInput('d'));
10840 app.handle_action(Action::FilterInput('m'));
10841 app.handle_action(Action::FilterInput('i'));
10842 app.handle_action(Action::FilterInput('n'));
10843 assert_eq!(app.iam_state.users.filter, "admin");
10844
10845 app.handle_action(Action::FilterBackspace);
10846 assert_eq!(app.iam_state.users.filter, "admi");
10847 }
10848
10849 #[test]
10850 fn test_iam_policies_filter_input() {
10851 let mut app = test_app();
10852 app.current_service = Service::IamUsers;
10853 app.service_selected = true;
10854 app.iam_state.current_user = Some("testuser".to_string());
10855 app.mode = Mode::FilterInput;
10856
10857 app.handle_action(Action::FilterInput('r'));
10858 app.handle_action(Action::FilterInput('e'));
10859 app.handle_action(Action::FilterInput('a'));
10860 app.handle_action(Action::FilterInput('d'));
10861 assert_eq!(app.iam_state.policies.filter, "read");
10862
10863 app.handle_action(Action::FilterBackspace);
10864 assert_eq!(app.iam_state.policies.filter, "rea");
10865 }
10866
10867 #[test]
10868 fn test_iam_start_filter() {
10869 let mut app = test_app();
10870 app.current_service = Service::IamUsers;
10871 app.service_selected = true;
10872 app.mode = Mode::Normal;
10873
10874 app.handle_action(Action::StartFilter);
10875 assert_eq!(app.mode, Mode::FilterInput);
10876 }
10877
10878 #[test]
10879 fn test_iam_roles_filter_input() {
10880 let mut app = test_app();
10881 app.current_service = Service::IamRoles;
10882 app.service_selected = true;
10883 app.mode = Mode::FilterInput;
10884
10885 app.handle_action(Action::FilterInput('a'));
10886 app.handle_action(Action::FilterInput('d'));
10887 app.handle_action(Action::FilterInput('m'));
10888 app.handle_action(Action::FilterInput('i'));
10889 app.handle_action(Action::FilterInput('n'));
10890 assert_eq!(app.iam_state.roles.filter, "admin");
10891
10892 app.handle_action(Action::FilterBackspace);
10893 assert_eq!(app.iam_state.roles.filter, "admi");
10894 }
10895
10896 #[test]
10897 fn test_iam_roles_start_filter() {
10898 let mut app = test_app();
10899 app.current_service = Service::IamRoles;
10900 app.service_selected = true;
10901 app.mode = Mode::Normal;
10902
10903 app.handle_action(Action::StartFilter);
10904 assert_eq!(app.mode, Mode::FilterInput);
10905 }
10906
10907 #[test]
10908 fn test_iam_roles_navigation() {
10909 let mut app = test_app();
10910 app.current_service = Service::IamRoles;
10911 app.service_selected = true;
10912 app.mode = Mode::Normal;
10913 app.iam_state.roles.items = (0..10)
10914 .map(|i| crate::iam::IamRole {
10915 role_name: format!("role{}", i),
10916 path: "/".to_string(),
10917 trusted_entities: String::new(),
10918 last_activity: String::new(),
10919 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
10920 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10921 description: String::new(),
10922 max_session_duration: Some(3600),
10923 })
10924 .collect();
10925
10926 assert_eq!(app.iam_state.roles.selected, 0);
10927
10928 app.handle_action(Action::NextItem);
10929 assert_eq!(app.iam_state.roles.selected, 1);
10930
10931 app.handle_action(Action::NextItem);
10932 assert_eq!(app.iam_state.roles.selected, 2);
10933
10934 app.handle_action(Action::PrevItem);
10935 assert_eq!(app.iam_state.roles.selected, 1);
10936 }
10937
10938 #[test]
10939 fn test_iam_roles_page_hotkey() {
10940 let mut app = test_app();
10941 app.current_service = Service::IamRoles;
10942 app.service_selected = true;
10943 app.mode = Mode::Normal;
10944 app.iam_state.roles.page_size = PageSize::Ten;
10945 app.iam_state.roles.items = (0..100)
10946 .map(|i| crate::iam::IamRole {
10947 role_name: format!("role{}", i),
10948 path: "/".to_string(),
10949 trusted_entities: String::new(),
10950 last_activity: String::new(),
10951 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
10952 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10953 description: String::new(),
10954 max_session_duration: Some(3600),
10955 })
10956 .collect();
10957
10958 app.handle_action(Action::FilterInput('2'));
10959 app.handle_action(Action::OpenColumnSelector);
10960 assert_eq!(app.iam_state.roles.selected, 10); }
10962
10963 #[test]
10964 fn test_iam_users_page_hotkey() {
10965 let mut app = test_app();
10966 app.current_service = Service::IamUsers;
10967 app.service_selected = true;
10968 app.mode = Mode::Normal;
10969 app.iam_state.users.page_size = PageSize::Ten;
10970 app.iam_state.users.items = (0..100)
10971 .map(|i| crate::iam::IamUser {
10972 user_name: format!("user{}", i),
10973 path: "/".to_string(),
10974 groups: String::new(),
10975 last_activity: String::new(),
10976 mfa: String::new(),
10977 password_age: String::new(),
10978 console_last_sign_in: String::new(),
10979 access_key_id: String::new(),
10980 active_key_age: String::new(),
10981 access_key_last_used: String::new(),
10982 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
10983 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10984 console_access: String::new(),
10985 signing_certs: String::new(),
10986 })
10987 .collect();
10988
10989 app.handle_action(Action::FilterInput('3'));
10990 app.handle_action(Action::OpenColumnSelector);
10991 assert_eq!(app.iam_state.users.selected, 20); }
10993
10994 #[test]
10995 fn test_ecr_scroll_navigation() {
10996 let mut app = test_app();
10997 app.current_service = Service::EcrRepositories;
10998 app.service_selected = true;
10999 app.ecr_state.repositories.items = (0..20)
11000 .map(|i| EcrRepository {
11001 name: format!("repo{}", i),
11002 uri: format!("uri{}", i),
11003 created_at: "2023-01-01".to_string(),
11004 tag_immutability: "MUTABLE".to_string(),
11005 encryption_type: "AES256".to_string(),
11006 })
11007 .collect();
11008
11009 app.handle_action(Action::ScrollDown);
11010 assert_eq!(app.ecr_state.repositories.selected, 10);
11011
11012 app.handle_action(Action::ScrollUp);
11013 assert_eq!(app.ecr_state.repositories.selected, 0);
11014 }
11015
11016 #[test]
11017 fn test_ecr_tab_switching_triggers_reload() {
11018 let mut app = test_app();
11019 app.current_service = Service::EcrRepositories;
11020 app.service_selected = true;
11021 app.ecr_state.tab = EcrTab::Private;
11022 app.ecr_state.repositories.loading = false;
11023 app.ecr_state.repositories.items = vec![EcrRepository {
11024 name: "private-repo".to_string(),
11025 uri: "uri".to_string(),
11026 created_at: "2023-01-01".to_string(),
11027 tag_immutability: "MUTABLE".to_string(),
11028 encryption_type: "AES256".to_string(),
11029 }];
11030
11031 app.handle_action(Action::NextDetailTab);
11032 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11033 assert!(app.ecr_state.repositories.loading);
11034 assert_eq!(app.ecr_state.repositories.selected, 0);
11035 }
11036
11037 #[test]
11038 fn test_ecr_tab_cycles_between_private_and_public() {
11039 let mut app = test_app();
11040 app.current_service = Service::EcrRepositories;
11041 app.service_selected = true;
11042 app.ecr_state.tab = EcrTab::Private;
11043
11044 app.handle_action(Action::NextDetailTab);
11045 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11046
11047 app.handle_action(Action::NextDetailTab);
11048 assert_eq!(app.ecr_state.tab, EcrTab::Private);
11049 }
11050
11051 #[test]
11052 fn test_page_size_values() {
11053 assert_eq!(PageSize::Ten.value(), 10);
11054 assert_eq!(PageSize::TwentyFive.value(), 25);
11055 assert_eq!(PageSize::Fifty.value(), 50);
11056 assert_eq!(PageSize::OneHundred.value(), 100);
11057 }
11058
11059 #[test]
11060 fn test_page_size_next() {
11061 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
11062 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
11063 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
11064 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
11065 }
11066
11067 #[test]
11068 fn test_ecr_enter_drills_into_repository() {
11069 let mut app = test_app();
11070 app.current_service = Service::EcrRepositories;
11071 app.service_selected = true;
11072 app.mode = Mode::Normal;
11073 app.ecr_state.repositories.items = vec![EcrRepository {
11074 name: "my-repo".to_string(),
11075 uri: "uri".to_string(),
11076 created_at: "2023-01-01".to_string(),
11077 tag_immutability: "MUTABLE".to_string(),
11078 encryption_type: "AES256".to_string(),
11079 }];
11080
11081 app.handle_action(Action::Select);
11082 assert_eq!(
11083 app.ecr_state.current_repository,
11084 Some("my-repo".to_string())
11085 );
11086 assert!(app.ecr_state.repositories.loading);
11087 }
11088
11089 #[test]
11090 fn test_ecr_repository_expansion() {
11091 let mut app = test_app();
11092 app.current_service = Service::EcrRepositories;
11093 app.service_selected = true;
11094 app.ecr_state.repositories.items = vec![EcrRepository {
11095 name: "my-repo".to_string(),
11096 uri: "uri".to_string(),
11097 created_at: "2023-01-01".to_string(),
11098 tag_immutability: "MUTABLE".to_string(),
11099 encryption_type: "AES256".to_string(),
11100 }];
11101 app.ecr_state.repositories.selected = 0;
11102
11103 assert_eq!(app.ecr_state.repositories.expanded_item, None);
11104
11105 app.handle_action(Action::NextPane);
11106 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
11107
11108 app.handle_action(Action::PrevPane);
11109 assert_eq!(app.ecr_state.repositories.expanded_item, None);
11110 }
11111
11112 #[test]
11113 fn test_ecr_ctrl_d_scrolls_down() {
11114 let mut app = test_app();
11115 app.current_service = Service::EcrRepositories;
11116 app.service_selected = true;
11117 app.mode = Mode::Normal;
11118 app.ecr_state.repositories.items = (0..30)
11119 .map(|i| EcrRepository {
11120 name: format!("repo{}", i),
11121 uri: format!("uri{}", i),
11122 created_at: "2023-01-01".to_string(),
11123 tag_immutability: "MUTABLE".to_string(),
11124 encryption_type: "AES256".to_string(),
11125 })
11126 .collect();
11127 app.ecr_state.repositories.selected = 0;
11128
11129 app.handle_action(Action::PageDown);
11130 assert_eq!(app.ecr_state.repositories.selected, 10);
11131 }
11132
11133 #[test]
11134 fn test_ecr_ctrl_u_scrolls_up() {
11135 let mut app = test_app();
11136 app.current_service = Service::EcrRepositories;
11137 app.service_selected = true;
11138 app.mode = Mode::Normal;
11139 app.ecr_state.repositories.items = (0..30)
11140 .map(|i| EcrRepository {
11141 name: format!("repo{}", i),
11142 uri: format!("uri{}", i),
11143 created_at: "2023-01-01".to_string(),
11144 tag_immutability: "MUTABLE".to_string(),
11145 encryption_type: "AES256".to_string(),
11146 })
11147 .collect();
11148 app.ecr_state.repositories.selected = 15;
11149
11150 app.handle_action(Action::PageUp);
11151 assert_eq!(app.ecr_state.repositories.selected, 5);
11152 }
11153
11154 #[test]
11155 fn test_ecr_images_ctrl_d_scrolls_down() {
11156 let mut app = test_app();
11157 app.current_service = Service::EcrRepositories;
11158 app.service_selected = true;
11159 app.mode = Mode::Normal;
11160 app.ecr_state.current_repository = Some("repo".to_string());
11161 app.ecr_state.images.items = (0..30)
11162 .map(|i| EcrImage {
11163 tag: format!("tag{}", i),
11164 artifact_type: "container".to_string(),
11165 pushed_at: "2023-01-01T12:00:00Z".to_string(),
11166 size_bytes: 104857600,
11167 uri: format!("uri{}", i),
11168 digest: format!("sha256:{}", i),
11169 last_pull_time: String::new(),
11170 })
11171 .collect();
11172 app.ecr_state.images.selected = 0;
11173
11174 app.handle_action(Action::PageDown);
11175 assert_eq!(app.ecr_state.images.selected, 10);
11176 }
11177
11178 #[test]
11179 fn test_ecr_esc_goes_back_from_images_to_repos() {
11180 let mut app = test_app();
11181 app.current_service = Service::EcrRepositories;
11182 app.service_selected = true;
11183 app.mode = Mode::Normal;
11184 app.ecr_state.current_repository = Some("my-repo".to_string());
11185 app.ecr_state.images.items = vec![EcrImage {
11186 tag: "latest".to_string(),
11187 artifact_type: "container".to_string(),
11188 pushed_at: "2023-01-01T12:00:00Z".to_string(),
11189 size_bytes: 104857600,
11190 uri: "uri".to_string(),
11191 digest: "sha256:abc".to_string(),
11192 last_pull_time: String::new(),
11193 }];
11194
11195 app.handle_action(Action::GoBack);
11196 assert_eq!(app.ecr_state.current_repository, None);
11197 assert!(app.ecr_state.images.items.is_empty());
11198 }
11199
11200 #[test]
11201 fn test_ecr_esc_collapses_expanded_image_first() {
11202 let mut app = test_app();
11203 app.current_service = Service::EcrRepositories;
11204 app.service_selected = true;
11205 app.mode = Mode::Normal;
11206 app.ecr_state.current_repository = Some("my-repo".to_string());
11207 app.ecr_state.images.expanded_item = Some(0);
11208
11209 app.handle_action(Action::GoBack);
11210 assert_eq!(app.ecr_state.images.expanded_item, None);
11211 assert_eq!(
11212 app.ecr_state.current_repository,
11213 Some("my-repo".to_string())
11214 );
11215 }
11216
11217 #[test]
11218 fn test_pagination_with_lowercase_p() {
11219 let mut app = test_app();
11220 app.current_service = Service::EcrRepositories;
11221 app.service_selected = true;
11222 app.mode = Mode::Normal;
11223 app.ecr_state.repositories.items = (0..100)
11224 .map(|i| EcrRepository {
11225 name: format!("repo{}", i),
11226 uri: format!("uri{}", i),
11227 created_at: "2023-01-01".to_string(),
11228 tag_immutability: "MUTABLE".to_string(),
11229 encryption_type: "AES256".to_string(),
11230 })
11231 .collect();
11232
11233 app.handle_action(Action::FilterInput('2'));
11235 assert_eq!(app.page_input, "2");
11236
11237 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
11241
11242 #[test]
11243 fn test_lowercase_p_without_number_opens_preferences() {
11244 let mut app = test_app();
11245 app.current_service = Service::EcrRepositories;
11246 app.service_selected = true;
11247 app.mode = Mode::Normal;
11248
11249 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
11251 }
11252
11253 #[test]
11254 fn test_ctrl_o_generates_correct_console_url() {
11255 let mut app = test_app();
11256 app.current_service = Service::EcrRepositories;
11257 app.service_selected = true;
11258 app.mode = Mode::Normal;
11259 app.config.account_id = "123456789012".to_string();
11260
11261 let url = app.get_console_url();
11263 assert!(url.contains("ecr/private-registry/repositories"));
11264 assert!(url.contains("region=us-east-1"));
11265
11266 app.ecr_state.current_repository = Some("my-repo".to_string());
11268 let url = app.get_console_url();
11269 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
11270 assert!(url.contains("region=us-east-1"));
11271 }
11272
11273 #[test]
11274 fn test_page_input_display_and_reset() {
11275 let mut app = test_app();
11276 app.current_service = Service::EcrRepositories;
11277 app.service_selected = true;
11278 app.mode = Mode::Normal;
11279 app.ecr_state.repositories.items = (0..100)
11280 .map(|i| EcrRepository {
11281 name: format!("repo{}", i),
11282 uri: format!("uri{}", i),
11283 created_at: "2023-01-01".to_string(),
11284 tag_immutability: "MUTABLE".to_string(),
11285 encryption_type: "AES256".to_string(),
11286 })
11287 .collect();
11288
11289 app.handle_action(Action::FilterInput('2'));
11291 assert_eq!(app.page_input, "2");
11292
11293 app.handle_action(Action::OpenColumnSelector);
11295 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
11298
11299 #[test]
11300 fn test_page_navigation_updates_scroll_offset_for_cfn() {
11301 let mut app = test_app();
11302 app.current_service = Service::CloudFormationStacks;
11303 app.service_selected = true;
11304 app.mode = Mode::Normal;
11305 app.cfn_state.table.items = (0..100)
11306 .map(|i| crate::cfn::Stack {
11307 name: format!("stack-{}", i),
11308 stack_id: format!(
11309 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
11310 i
11311 ),
11312 status: "CREATE_COMPLETE".to_string(),
11313 created_time: "2023-01-01T00:00:00Z".to_string(),
11314 updated_time: "2023-01-01T00:00:00Z".to_string(),
11315 deleted_time: String::new(),
11316 drift_status: "IN_SYNC".to_string(),
11317 last_drift_check_time: String::new(),
11318 status_reason: String::new(),
11319 description: String::new(),
11320 detailed_status: String::new(),
11321 root_stack: String::new(),
11322 parent_stack: String::new(),
11323 termination_protection: false,
11324 iam_role: String::new(),
11325 tags: vec![],
11326 stack_policy: String::new(),
11327 rollback_monitoring_time: String::new(),
11328 rollback_alarms: vec![],
11329 notification_arns: vec![],
11330 })
11331 .collect();
11332
11333 app.handle_action(Action::FilterInput('2'));
11335 assert_eq!(app.page_input, "2");
11336
11337 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
11342 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
11344 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
11345
11346 let current_page = app.cfn_state.table.scroll_offset / page_size;
11348 assert_eq!(
11349 current_page, 1,
11350 "2p should go to page 2 (0-indexed as 1), not page 3"
11351 ); }
11353
11354 #[test]
11355 fn test_3p_goes_to_page_3_not_page_5() {
11356 let mut app = test_app();
11357 app.current_service = Service::CloudFormationStacks;
11358 app.service_selected = true;
11359 app.mode = Mode::Normal;
11360 app.cfn_state.table.items = (0..200)
11361 .map(|i| crate::cfn::Stack {
11362 name: format!("stack-{}", i),
11363 stack_id: format!(
11364 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
11365 i
11366 ),
11367 status: "CREATE_COMPLETE".to_string(),
11368 created_time: "2023-01-01T00:00:00Z".to_string(),
11369 updated_time: "2023-01-01T00:00:00Z".to_string(),
11370 deleted_time: String::new(),
11371 drift_status: "IN_SYNC".to_string(),
11372 last_drift_check_time: String::new(),
11373 status_reason: String::new(),
11374 description: String::new(),
11375 detailed_status: String::new(),
11376 root_stack: String::new(),
11377 parent_stack: String::new(),
11378 termination_protection: false,
11379 iam_role: String::new(),
11380 tags: vec![],
11381 stack_policy: String::new(),
11382 rollback_monitoring_time: String::new(),
11383 rollback_alarms: vec![],
11384 notification_arns: vec![],
11385 })
11386 .collect();
11387
11388 app.handle_action(Action::FilterInput('3'));
11390 app.handle_action(Action::OpenColumnSelector);
11391
11392 let page_size = app.cfn_state.table.page_size.value();
11393 let current_page = app.cfn_state.table.scroll_offset / page_size;
11394 assert_eq!(
11395 current_page, 2,
11396 "3p should go to page 3 (0-indexed as 2), not page 5"
11397 );
11398 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
11399 }
11400
11401 #[test]
11402 fn test_log_streams_page_navigation_uses_correct_page_size() {
11403 let mut app = test_app();
11404 app.current_service = Service::CloudWatchLogGroups;
11405 app.view_mode = ViewMode::Detail;
11406 app.service_selected = true;
11407 app.mode = Mode::Normal;
11408 app.log_groups_state.log_streams = (0..100)
11409 .map(|i| LogStream {
11410 name: format!("stream-{}", i),
11411 creation_time: None,
11412 last_event_time: None,
11413 })
11414 .collect();
11415
11416 app.handle_action(Action::FilterInput('2'));
11418 app.handle_action(Action::OpenColumnSelector);
11419
11420 assert_eq!(app.log_groups_state.selected_stream, 20);
11422
11423 let page_size = 20;
11425 let current_page = app.log_groups_state.selected_stream / page_size;
11426 assert_eq!(
11427 current_page, 1,
11428 "2p should go to page 2 (0-indexed as 1), not page 3"
11429 );
11430 }
11431
11432 #[test]
11433 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
11434 let mut app = test_app();
11435 app.current_service = Service::EcrRepositories;
11436 app.service_selected = true;
11437 app.mode = Mode::Normal;
11438 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
11440 .map(|i| EcrRepository {
11441 name: format!("repo{}", i),
11442 uri: format!("uri{}", i),
11443 created_at: "2023-01-01".to_string(),
11444 tag_immutability: "MUTABLE".to_string(),
11445 encryption_type: "AES256".to_string(),
11446 })
11447 .collect();
11448
11449 app.handle_action(Action::FilterInput('3'));
11451 app.handle_action(Action::OpenColumnSelector);
11452
11453 assert_eq!(app.ecr_state.repositories.selected, 50);
11455
11456 let page_size = app.ecr_state.repositories.page_size.value();
11457 let current_page = app.ecr_state.repositories.selected / page_size;
11458 assert_eq!(
11459 current_page, 2,
11460 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
11461 );
11462 }
11463
11464 #[test]
11465 fn test_page_navigation_updates_scroll_offset_for_alarms() {
11466 let mut app = test_app();
11467 app.current_service = Service::CloudWatchAlarms;
11468 app.service_selected = true;
11469 app.mode = Mode::Normal;
11470 app.alarms_state.table.items = (0..100)
11471 .map(|i| crate::cw::alarms::Alarm {
11472 name: format!("alarm-{}", i),
11473 state: "OK".to_string(),
11474 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
11475 description: String::new(),
11476 metric_name: "CPUUtilization".to_string(),
11477 namespace: "AWS/EC2".to_string(),
11478 statistic: "Average".to_string(),
11479 period: 300,
11480 comparison_operator: "GreaterThanThreshold".to_string(),
11481 threshold: 80.0,
11482 actions_enabled: true,
11483 state_reason: String::new(),
11484 resource: String::new(),
11485 dimensions: String::new(),
11486 expression: String::new(),
11487 alarm_type: "MetricAlarm".to_string(),
11488 cross_account: String::new(),
11489 })
11490 .collect();
11491
11492 app.handle_action(Action::FilterInput('2'));
11494 app.handle_action(Action::OpenColumnSelector);
11495
11496 let page_size = app.alarms_state.table.page_size.value();
11498 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
11500 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
11501 }
11502
11503 #[test]
11504 fn test_ecr_pagination_with_65_repos() {
11505 let mut app = test_app();
11506 app.current_service = Service::EcrRepositories;
11507 app.service_selected = true;
11508 app.mode = Mode::Normal;
11509 app.ecr_state.repositories.items = (0..65)
11510 .map(|i| EcrRepository {
11511 name: format!("repo{:02}", i),
11512 uri: format!("uri{}", i),
11513 created_at: "2023-01-01".to_string(),
11514 tag_immutability: "MUTABLE".to_string(),
11515 encryption_type: "AES256".to_string(),
11516 })
11517 .collect();
11518
11519 assert_eq!(app.ecr_state.repositories.selected, 0);
11521 let page_size = 50;
11522 let current_page = app.ecr_state.repositories.selected / page_size;
11523 assert_eq!(current_page, 0);
11524
11525 app.handle_action(Action::FilterInput('2'));
11527 app.handle_action(Action::OpenColumnSelector);
11528 assert_eq!(app.ecr_state.repositories.selected, 50);
11529
11530 let current_page = app.ecr_state.repositories.selected / page_size;
11532 assert_eq!(current_page, 1);
11533 }
11534
11535 #[test]
11536 fn test_ecr_repos_input_focus_tab_cycling() {
11537 let mut app = test_app();
11538 app.current_service = Service::EcrRepositories;
11539 app.service_selected = true;
11540 app.mode = Mode::FilterInput;
11541 app.ecr_state.input_focus = InputFocus::Filter;
11542
11543 app.handle_action(Action::NextFilterFocus);
11545 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
11546
11547 app.handle_action(Action::NextFilterFocus);
11549 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
11550
11551 app.handle_action(Action::PrevFilterFocus);
11553 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
11554
11555 app.handle_action(Action::PrevFilterFocus);
11557 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
11558 }
11559
11560 #[test]
11561 fn test_ecr_images_column_toggle_not_off_by_one() {
11562 use crate::ecr::image::Column as ImageColumn;
11563 let mut app = test_app();
11564 app.current_service = Service::EcrRepositories;
11565 app.service_selected = true;
11566 app.mode = Mode::ColumnSelector;
11567 app.ecr_state.current_repository = Some("test-repo".to_string());
11568
11569 app.ecr_image_visible_column_ids = ImageColumn::ids();
11571 let initial_count = app.ecr_image_visible_column_ids.len();
11572
11573 app.column_selector_index = 0;
11575 app.handle_action(Action::ToggleColumn);
11576
11577 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
11579 assert!(!app
11580 .ecr_image_visible_column_ids
11581 .contains(&ImageColumn::Tag.id()));
11582
11583 app.handle_action(Action::ToggleColumn);
11585 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
11586 assert!(app
11587 .ecr_image_visible_column_ids
11588 .contains(&ImageColumn::Tag.id()));
11589 }
11590
11591 #[test]
11592 fn test_ecr_repos_column_toggle_works() {
11593 let mut app = test_app();
11594 app.current_service = Service::EcrRepositories;
11595 app.service_selected = true;
11596 app.mode = Mode::ColumnSelector;
11597 app.ecr_state.current_repository = None;
11598
11599 app.ecr_repo_visible_column_ids = EcrColumn::ids();
11601 let initial_count = app.ecr_repo_visible_column_ids.len();
11602
11603 app.column_selector_index = 0;
11605 app.handle_action(Action::ToggleColumn);
11606
11607 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
11609 assert!(!app
11610 .ecr_repo_visible_column_ids
11611 .contains(&EcrColumn::Name.id()));
11612
11613 app.handle_action(Action::ToggleColumn);
11615 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
11616 assert!(app
11617 .ecr_repo_visible_column_ids
11618 .contains(&EcrColumn::Name.id()));
11619 }
11620
11621 #[test]
11622 fn test_ecr_repos_pagination_left_right_navigation() {
11623 use crate::ecr::repo::Repository as EcrRepository;
11624 let mut app = test_app();
11625 app.current_service = Service::EcrRepositories;
11626 app.service_selected = true;
11627 app.mode = Mode::FilterInput;
11628 app.ecr_state.input_focus = InputFocus::Pagination;
11629
11630 app.ecr_state.repositories.items = (0..150)
11632 .map(|i| EcrRepository {
11633 name: format!("repo{:03}", i),
11634 uri: format!("uri{}", i),
11635 created_at: "2023-01-01".to_string(),
11636 tag_immutability: "MUTABLE".to_string(),
11637 encryption_type: "AES256".to_string(),
11638 })
11639 .collect();
11640
11641 app.ecr_state.repositories.selected = 0;
11643 eprintln!(
11644 "Initial: selected={}, focus={:?}, mode={:?}",
11645 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
11646 );
11647
11648 app.handle_action(Action::PageDown);
11650 eprintln!(
11651 "After PageDown: selected={}",
11652 app.ecr_state.repositories.selected
11653 );
11654 assert_eq!(app.ecr_state.repositories.selected, 50);
11655
11656 app.handle_action(Action::PageDown);
11658 eprintln!(
11659 "After 2nd PageDown: selected={}",
11660 app.ecr_state.repositories.selected
11661 );
11662 assert_eq!(app.ecr_state.repositories.selected, 100);
11663
11664 app.handle_action(Action::PageDown);
11666 eprintln!(
11667 "After 3rd PageDown: selected={}",
11668 app.ecr_state.repositories.selected
11669 );
11670 assert_eq!(app.ecr_state.repositories.selected, 100);
11671
11672 app.handle_action(Action::PageUp);
11674 eprintln!(
11675 "After PageUp: selected={}",
11676 app.ecr_state.repositories.selected
11677 );
11678 assert_eq!(app.ecr_state.repositories.selected, 50);
11679
11680 app.handle_action(Action::PageUp);
11682 eprintln!(
11683 "After 2nd PageUp: selected={}",
11684 app.ecr_state.repositories.selected
11685 );
11686 assert_eq!(app.ecr_state.repositories.selected, 0);
11687
11688 app.handle_action(Action::PageUp);
11690 eprintln!(
11691 "After 3rd PageUp: selected={}",
11692 app.ecr_state.repositories.selected
11693 );
11694 assert_eq!(app.ecr_state.repositories.selected, 0);
11695 }
11696
11697 #[test]
11698 fn test_ecr_repos_filter_input_when_input_focused() {
11699 use crate::ecr::repo::Repository as EcrRepository;
11700 let mut app = test_app();
11701 app.current_service = Service::EcrRepositories;
11702 app.service_selected = true;
11703 app.mode = Mode::FilterInput;
11704 app.ecr_state.input_focus = InputFocus::Filter;
11705
11706 app.ecr_state.repositories.items = vec![
11708 EcrRepository {
11709 name: "test-repo".to_string(),
11710 uri: "uri1".to_string(),
11711 created_at: "2023-01-01".to_string(),
11712 tag_immutability: "MUTABLE".to_string(),
11713 encryption_type: "AES256".to_string(),
11714 },
11715 EcrRepository {
11716 name: "prod-repo".to_string(),
11717 uri: "uri2".to_string(),
11718 created_at: "2023-01-01".to_string(),
11719 tag_immutability: "MUTABLE".to_string(),
11720 encryption_type: "AES256".to_string(),
11721 },
11722 ];
11723
11724 assert_eq!(app.ecr_state.repositories.filter, "");
11726 app.handle_action(Action::FilterInput('t'));
11727 assert_eq!(app.ecr_state.repositories.filter, "t");
11728 app.handle_action(Action::FilterInput('e'));
11729 assert_eq!(app.ecr_state.repositories.filter, "te");
11730 app.handle_action(Action::FilterInput('s'));
11731 assert_eq!(app.ecr_state.repositories.filter, "tes");
11732 app.handle_action(Action::FilterInput('t'));
11733 assert_eq!(app.ecr_state.repositories.filter, "test");
11734 }
11735
11736 #[test]
11737 fn test_ecr_repos_digit_input_when_pagination_focused() {
11738 use crate::ecr::repo::Repository as EcrRepository;
11739 let mut app = test_app();
11740 app.current_service = Service::EcrRepositories;
11741 app.service_selected = true;
11742 app.mode = Mode::FilterInput;
11743 app.ecr_state.input_focus = InputFocus::Pagination;
11744
11745 app.ecr_state.repositories.items = vec![EcrRepository {
11747 name: "test-repo".to_string(),
11748 uri: "uri1".to_string(),
11749 created_at: "2023-01-01".to_string(),
11750 tag_immutability: "MUTABLE".to_string(),
11751 encryption_type: "AES256".to_string(),
11752 }];
11753
11754 assert_eq!(app.ecr_state.repositories.filter, "");
11756 assert_eq!(app.page_input, "");
11757 app.handle_action(Action::FilterInput('2'));
11758 assert_eq!(app.ecr_state.repositories.filter, "");
11759 assert_eq!(app.page_input, "2");
11760
11761 app.handle_action(Action::FilterInput('a'));
11763 assert_eq!(app.ecr_state.repositories.filter, "");
11764 assert_eq!(app.page_input, "2");
11765 }
11766
11767 #[test]
11768 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
11769 use crate::ecr::repo::Repository as EcrRepository;
11770 let mut app = test_app();
11771 app.current_service = Service::EcrRepositories;
11772 app.service_selected = true;
11773 app.mode = Mode::FilterInput;
11774 app.ecr_state.input_focus = InputFocus::Filter;
11775
11776 app.ecr_state.repositories.items = (0..150)
11778 .map(|i| EcrRepository {
11779 name: format!("repo{:03}", i),
11780 uri: format!("uri{}", i),
11781 created_at: "2023-01-01".to_string(),
11782 tag_immutability: "MUTABLE".to_string(),
11783 encryption_type: "AES256".to_string(),
11784 })
11785 .collect();
11786
11787 app.ecr_state.repositories.selected = 0;
11789
11790 app.handle_action(Action::PageDown);
11792 assert_eq!(
11793 app.ecr_state.repositories.selected, 10,
11794 "Should scroll down by 10"
11795 );
11796
11797 app.handle_action(Action::PageUp);
11798 assert_eq!(
11799 app.ecr_state.repositories.selected, 0,
11800 "Should scroll back up"
11801 );
11802 }
11803
11804 #[test]
11805 fn test_ecr_repos_pagination_control_actually_works() {
11806 use crate::ecr::repo::Repository as EcrRepository;
11807
11808 let mut app = test_app();
11810 app.current_service = Service::EcrRepositories;
11811 app.service_selected = true;
11812 app.mode = Mode::FilterInput;
11813 app.ecr_state.current_repository = None;
11814 app.ecr_state.input_focus = InputFocus::Pagination;
11815
11816 app.ecr_state.repositories.items = (0..100)
11818 .map(|i| EcrRepository {
11819 name: format!("repo{:03}", i),
11820 uri: format!("uri{}", i),
11821 created_at: "2023-01-01".to_string(),
11822 tag_immutability: "MUTABLE".to_string(),
11823 encryption_type: "AES256".to_string(),
11824 })
11825 .collect();
11826
11827 app.ecr_state.repositories.selected = 0;
11828
11829 assert_eq!(app.mode, Mode::FilterInput);
11831 assert_eq!(app.current_service, Service::EcrRepositories);
11832 assert_eq!(app.ecr_state.current_repository, None);
11833 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
11834
11835 app.handle_action(Action::PageDown);
11837 assert_eq!(
11838 app.ecr_state.repositories.selected, 50,
11839 "PageDown should move to page 2"
11840 );
11841
11842 app.handle_action(Action::PageUp);
11843 assert_eq!(
11844 app.ecr_state.repositories.selected, 0,
11845 "PageUp should move back to page 1"
11846 );
11847 }
11848
11849 #[test]
11850 fn test_ecr_repos_start_filter_resets_focus_to_input() {
11851 let mut app = test_app();
11852 app.current_service = Service::EcrRepositories;
11853 app.service_selected = true;
11854 app.mode = Mode::Normal;
11855 app.ecr_state.current_repository = None;
11856
11857 app.ecr_state.input_focus = InputFocus::Pagination;
11859
11860 app.handle_action(Action::StartFilter);
11862
11863 assert_eq!(app.mode, Mode::FilterInput);
11865 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
11866 }
11867
11868 #[test]
11869 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
11870 use crate::ecr::repo::Repository as EcrRepository;
11871
11872 let mut app = test_app();
11873 app.current_service = Service::EcrRepositories;
11874 app.service_selected = true;
11875 app.mode = Mode::Normal;
11876 app.ecr_state.current_repository = None;
11877
11878 app.ecr_state.repositories.items = (0..100)
11880 .map(|i| EcrRepository {
11881 name: format!("repo{:03}", i),
11882 uri: format!("uri{}", i),
11883 created_at: "2023-01-01".to_string(),
11884 tag_immutability: "MUTABLE".to_string(),
11885 encryption_type: "AES256".to_string(),
11886 })
11887 .collect();
11888
11889 app.ecr_state.repositories.selected = 0;
11890
11891 app.handle_action(Action::StartFilter);
11893 assert_eq!(app.mode, Mode::FilterInput);
11894 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
11895
11896 app.handle_action(Action::NextFilterFocus);
11898 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
11899
11900 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
11902 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
11903 app.handle_action(Action::PageDown);
11904 eprintln!(
11905 "After PageDown: selected={}",
11906 app.ecr_state.repositories.selected
11907 );
11908
11909 assert_eq!(
11911 app.ecr_state.repositories.selected, 50,
11912 "Right arrow should move to page 2"
11913 );
11914
11915 app.handle_action(Action::PageUp);
11917 assert_eq!(
11918 app.ecr_state.repositories.selected, 0,
11919 "Left arrow should move back to page 1"
11920 );
11921 }
11922
11923 #[test]
11924 fn test_service_picker_i_key_activates_filter() {
11925 let mut app = test_app();
11926
11927 assert_eq!(app.mode, Mode::ServicePicker);
11929 assert!(app.service_picker.filter.is_empty());
11930
11931 app.handle_action(Action::FilterInput('i'));
11933
11934 assert_eq!(app.mode, Mode::ServicePicker);
11936 assert_eq!(app.service_picker.filter, "i");
11937 }
11938
11939 #[test]
11940 fn test_service_picker_typing_filters_services() {
11941 let mut app = test_app();
11942
11943 assert_eq!(app.mode, Mode::ServicePicker);
11945
11946 app.handle_action(Action::FilterInput('s'));
11948 app.handle_action(Action::FilterInput('3'));
11949
11950 assert_eq!(app.service_picker.filter, "s3");
11951 assert_eq!(app.mode, Mode::ServicePicker);
11952 }
11953
11954 #[test]
11955 fn test_service_picker_resets_on_open() {
11956 let mut app = test_app();
11957
11958 app.service_selected = true;
11960 app.mode = Mode::Normal;
11961
11962 app.service_picker.filter = "previous".to_string();
11964 app.service_picker.selected = 5;
11965
11966 app.handle_action(Action::OpenSpaceMenu);
11968
11969 assert_eq!(app.mode, Mode::SpaceMenu);
11971 assert!(app.service_picker.filter.is_empty());
11972 assert_eq!(app.service_picker.selected, 0);
11973 }
11974
11975 #[test]
11976 fn test_no_pii_in_test_data() {
11977 let test_repo = EcrRepository {
11979 name: "test-repo".to_string(),
11980 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
11981 created_at: "2024-01-01".to_string(),
11982 tag_immutability: "MUTABLE".to_string(),
11983 encryption_type: "AES256".to_string(),
11984 };
11985
11986 assert!(test_repo.uri.starts_with("123456789012"));
11988 assert!(!test_repo.uri.contains("123456789013")); }
11990
11991 #[test]
11992 fn test_lambda_versions_tab_triggers_loading() {
11993 let mut app = test_app();
11994 app.current_service = Service::LambdaFunctions;
11995 app.service_selected = true;
11996
11997 app.lambda_state.current_function = Some("test-function".to_string());
11999 app.lambda_state.detail_tab = LambdaDetailTab::Code;
12000
12001 assert!(app.lambda_state.version_table.items.is_empty());
12003
12004 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12006
12007 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
12010 assert!(app.lambda_state.current_function.is_some());
12011 }
12012
12013 #[test]
12014 fn test_lambda_versions_navigation() {
12015 use crate::lambda::Version;
12016
12017 let mut app = test_app();
12018 app.current_service = Service::LambdaFunctions;
12019 app.service_selected = true;
12020 app.lambda_state.current_function = Some("test-function".to_string());
12021 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12022
12023 app.lambda_state.version_table.items = vec![
12025 Version {
12026 version: "3".to_string(),
12027 aliases: "prod".to_string(),
12028 description: "".to_string(),
12029 last_modified: "".to_string(),
12030 architecture: "X86_64".to_string(),
12031 },
12032 Version {
12033 version: "2".to_string(),
12034 aliases: "".to_string(),
12035 description: "".to_string(),
12036 last_modified: "".to_string(),
12037 architecture: "X86_64".to_string(),
12038 },
12039 Version {
12040 version: "1".to_string(),
12041 aliases: "".to_string(),
12042 description: "".to_string(),
12043 last_modified: "".to_string(),
12044 architecture: "X86_64".to_string(),
12045 },
12046 ];
12047
12048 assert_eq!(app.lambda_state.version_table.items.len(), 3);
12050 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
12051 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
12052
12053 app.lambda_state.version_table.selected = 1;
12055 assert_eq!(app.lambda_state.version_table.selected, 1);
12056 }
12057
12058 #[test]
12059 fn test_lambda_versions_with_aliases() {
12060 use crate::lambda::Version;
12061
12062 let version = Version {
12063 version: "35".to_string(),
12064 aliases: "prod, staging".to_string(),
12065 description: "Production version".to_string(),
12066 last_modified: "2024-01-01".to_string(),
12067 architecture: "X86_64".to_string(),
12068 };
12069
12070 assert_eq!(version.aliases, "prod, staging");
12071 assert!(!version.aliases.is_empty());
12072 }
12073
12074 #[test]
12075 fn test_lambda_versions_expansion() {
12076 use crate::lambda::Version;
12077
12078 let mut app = test_app();
12079 app.current_service = Service::LambdaFunctions;
12080 app.service_selected = true;
12081 app.lambda_state.current_function = Some("test-function".to_string());
12082 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12083
12084 app.lambda_state.version_table.items = vec![
12086 Version {
12087 version: "2".to_string(),
12088 aliases: "prod".to_string(),
12089 description: "Production".to_string(),
12090 last_modified: "2024-01-01".to_string(),
12091 architecture: "X86_64".to_string(),
12092 },
12093 Version {
12094 version: "1".to_string(),
12095 aliases: "".to_string(),
12096 description: "".to_string(),
12097 last_modified: "2024-01-01".to_string(),
12098 architecture: "Arm64".to_string(),
12099 },
12100 ];
12101
12102 app.lambda_state.version_table.selected = 0;
12103
12104 app.lambda_state.version_table.expanded_item = Some(0);
12106 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
12107
12108 app.lambda_state.version_table.selected = 1;
12110 app.lambda_state.version_table.expanded_item = Some(1);
12111 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
12112 }
12113
12114 #[test]
12115 fn test_lambda_versions_page_navigation() {
12116 use crate::lambda::Version;
12117
12118 let mut app = test_app();
12119 app.current_service = Service::LambdaFunctions;
12120 app.service_selected = true;
12121 app.lambda_state.current_function = Some("test-function".to_string());
12122 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12123
12124 app.lambda_state.version_table.items = (1..=30)
12126 .map(|i| Version {
12127 version: i.to_string(),
12128 aliases: "".to_string(),
12129 description: "".to_string(),
12130 last_modified: "".to_string(),
12131 architecture: "X86_64".to_string(),
12132 })
12133 .collect();
12134
12135 app.lambda_state.version_table.page_size = PageSize::Ten;
12136 app.lambda_state.version_table.selected = 0;
12137
12138 app.page_input = "2".to_string();
12140 app.handle_action(Action::OpenColumnSelector);
12141
12142 assert_eq!(app.lambda_state.version_table.selected, 10);
12144 }
12145
12146 #[test]
12147 fn test_lambda_versions_pagination_arrow_keys() {
12148 use crate::lambda::Version;
12149
12150 let mut app = test_app();
12151 app.current_service = Service::LambdaFunctions;
12152 app.service_selected = true;
12153 app.lambda_state.current_function = Some("test-function".to_string());
12154 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12155 app.mode = Mode::FilterInput;
12156 app.lambda_state.version_input_focus = InputFocus::Pagination;
12157
12158 app.lambda_state.version_table.items = (1..=30)
12160 .map(|i| Version {
12161 version: i.to_string(),
12162 aliases: "".to_string(),
12163 description: "".to_string(),
12164 last_modified: "".to_string(),
12165 architecture: "X86_64".to_string(),
12166 })
12167 .collect();
12168
12169 app.lambda_state.version_table.page_size = PageSize::Ten;
12170 app.lambda_state.version_table.selected = 0;
12171
12172 app.handle_action(Action::PageDown);
12174 assert_eq!(app.lambda_state.version_table.selected, 10);
12175
12176 app.handle_action(Action::PageUp);
12178 assert_eq!(app.lambda_state.version_table.selected, 0);
12179 }
12180
12181 #[test]
12182 fn test_lambda_versions_page_input_in_filter_mode() {
12183 use crate::lambda::Version;
12184
12185 let mut app = test_app();
12186 app.current_service = Service::LambdaFunctions;
12187 app.service_selected = true;
12188 app.lambda_state.current_function = Some("test-function".to_string());
12189 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12190 app.mode = Mode::FilterInput;
12191 app.lambda_state.version_input_focus = InputFocus::Pagination;
12192
12193 app.lambda_state.version_table.items = (1..=30)
12195 .map(|i| Version {
12196 version: i.to_string(),
12197 aliases: "".to_string(),
12198 description: "".to_string(),
12199 last_modified: "".to_string(),
12200 architecture: "X86_64".to_string(),
12201 })
12202 .collect();
12203
12204 app.lambda_state.version_table.page_size = PageSize::Ten;
12205 app.lambda_state.version_table.selected = 0;
12206
12207 app.handle_action(Action::FilterInput('2'));
12209 assert_eq!(app.page_input, "2");
12210 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
12214 assert_eq!(app.lambda_state.version_table.selected, 10);
12215 assert_eq!(app.page_input, ""); }
12217
12218 #[test]
12219 fn test_lambda_versions_filter_input() {
12220 use crate::lambda::Version;
12221
12222 let mut app = test_app();
12223 app.current_service = Service::LambdaFunctions;
12224 app.service_selected = true;
12225 app.lambda_state.current_function = Some("test-function".to_string());
12226 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12227 app.mode = Mode::FilterInput;
12228 app.lambda_state.version_input_focus = InputFocus::Filter;
12229
12230 app.lambda_state.version_table.items = vec![
12232 Version {
12233 version: "1".to_string(),
12234 aliases: "prod".to_string(),
12235 description: "Production".to_string(),
12236 last_modified: "".to_string(),
12237 architecture: "X86_64".to_string(),
12238 },
12239 Version {
12240 version: "2".to_string(),
12241 aliases: "staging".to_string(),
12242 description: "Staging".to_string(),
12243 last_modified: "".to_string(),
12244 architecture: "X86_64".to_string(),
12245 },
12246 ];
12247
12248 app.handle_action(Action::FilterInput('p'));
12250 app.handle_action(Action::FilterInput('r'));
12251 app.handle_action(Action::FilterInput('o'));
12252 app.handle_action(Action::FilterInput('d'));
12253 assert_eq!(app.lambda_state.version_table.filter, "prod");
12254
12255 app.handle_action(Action::FilterBackspace);
12257 assert_eq!(app.lambda_state.version_table.filter, "pro");
12258 }
12259
12260 #[test]
12261 fn test_lambda_aliases_table_expansion() {
12262 use crate::lambda::Alias;
12263
12264 let mut app = test_app();
12265 app.current_service = Service::LambdaFunctions;
12266 app.service_selected = true;
12267 app.lambda_state.current_function = Some("test-function".to_string());
12268 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
12269 app.mode = Mode::Normal;
12270
12271 app.lambda_state.alias_table.items = vec![
12272 Alias {
12273 name: "prod".to_string(),
12274 versions: "1".to_string(),
12275 description: "Production alias".to_string(),
12276 },
12277 Alias {
12278 name: "staging".to_string(),
12279 versions: "2".to_string(),
12280 description: "Staging alias".to_string(),
12281 },
12282 ];
12283
12284 app.lambda_state.alias_table.selected = 0;
12285
12286 app.handle_action(Action::Select);
12288 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
12289 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
12290
12291 app.handle_action(Action::GoBack);
12293 assert_eq!(app.lambda_state.current_alias, None);
12294 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
12295
12296 app.lambda_state.alias_table.selected = 1;
12298 app.handle_action(Action::Select);
12299 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
12300 }
12301
12302 #[test]
12303 fn test_lambda_versions_arrow_key_expansion() {
12304 use crate::lambda::Version;
12305
12306 let mut app = test_app();
12307 app.current_service = Service::LambdaFunctions;
12308 app.service_selected = true;
12309 app.lambda_state.current_function = Some("test-function".to_string());
12310 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12311 app.mode = Mode::Normal;
12312
12313 app.lambda_state.version_table.items = vec![Version {
12314 version: "1".to_string(),
12315 aliases: "prod".to_string(),
12316 description: "Production".to_string(),
12317 last_modified: "2024-01-01".to_string(),
12318 architecture: "X86_64".to_string(),
12319 }];
12320
12321 app.lambda_state.version_table.selected = 0;
12322
12323 app.handle_action(Action::NextPane);
12325 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
12326
12327 app.handle_action(Action::PrevPane);
12329 assert_eq!(app.lambda_state.version_table.expanded_item, None);
12330 }
12331
12332 #[test]
12333 fn test_lambda_version_detail_view() {
12334 use crate::lambda::Function;
12335
12336 let mut app = test_app();
12337 app.current_service = Service::LambdaFunctions;
12338 app.service_selected = true;
12339 app.lambda_state.current_function = Some("test-function".to_string());
12340 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
12341 app.mode = Mode::Normal;
12342
12343 app.lambda_state.table.items = vec![Function {
12344 name: "test-function".to_string(),
12345 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
12346 application: None,
12347 description: "Test".to_string(),
12348 package_type: "Zip".to_string(),
12349 runtime: "python3.12".to_string(),
12350 architecture: "X86_64".to_string(),
12351 code_size: 1024,
12352 code_sha256: "hash".to_string(),
12353 memory_mb: 128,
12354 timeout_seconds: 30,
12355 last_modified: "2024-01-01".to_string(),
12356 layers: vec![],
12357 }];
12358
12359 app.lambda_state.version_table.items = vec![crate::lambda::Version {
12360 version: "1".to_string(),
12361 aliases: "prod".to_string(),
12362 description: "Production".to_string(),
12363 last_modified: "2024-01-01".to_string(),
12364 architecture: "X86_64".to_string(),
12365 }];
12366
12367 app.lambda_state.version_table.selected = 0;
12368
12369 app.handle_action(Action::Select);
12371 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
12372 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
12373
12374 app.handle_action(Action::GoBack);
12376 assert_eq!(app.lambda_state.current_version, None);
12377 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
12378 }
12379
12380 #[test]
12381 fn test_lambda_version_detail_tabs() {
12382 use crate::lambda::Function;
12383
12384 let mut app = test_app();
12385 app.current_service = Service::LambdaFunctions;
12386 app.service_selected = true;
12387 app.lambda_state.current_function = Some("test-function".to_string());
12388 app.lambda_state.current_version = Some("1".to_string());
12389 app.lambda_state.detail_tab = LambdaDetailTab::Code;
12390 app.mode = Mode::Normal;
12391
12392 app.lambda_state.table.items = vec![Function {
12393 name: "test-function".to_string(),
12394 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
12395 application: None,
12396 description: "Test".to_string(),
12397 package_type: "Zip".to_string(),
12398 runtime: "python3.12".to_string(),
12399 architecture: "X86_64".to_string(),
12400 code_size: 1024,
12401 code_sha256: "hash".to_string(),
12402 memory_mb: 128,
12403 timeout_seconds: 30,
12404 last_modified: "2024-01-01".to_string(),
12405 layers: vec![],
12406 }];
12407
12408 app.handle_action(Action::NextDetailTab);
12410 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
12411
12412 app.handle_action(Action::NextDetailTab);
12413 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
12414
12415 app.handle_action(Action::NextDetailTab);
12416 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
12417
12418 app.handle_action(Action::PrevDetailTab);
12420 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
12421
12422 app.handle_action(Action::PrevDetailTab);
12423 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
12424 }
12425
12426 #[test]
12427 fn test_lambda_aliases_arrow_key_expansion() {
12428 use crate::lambda::Alias;
12429
12430 let mut app = test_app();
12431 app.current_service = Service::LambdaFunctions;
12432 app.service_selected = true;
12433 app.lambda_state.current_function = Some("test-function".to_string());
12434 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
12435 app.mode = Mode::Normal;
12436
12437 app.lambda_state.alias_table.items = vec![Alias {
12438 name: "prod".to_string(),
12439 versions: "1".to_string(),
12440 description: "Production alias".to_string(),
12441 }];
12442
12443 app.lambda_state.alias_table.selected = 0;
12444
12445 app.handle_action(Action::NextPane);
12447 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
12448
12449 app.handle_action(Action::PrevPane);
12451 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
12452 }
12453
12454 #[test]
12455 fn test_lambda_functions_arrow_key_expansion() {
12456 use crate::lambda::Function;
12457
12458 let mut app = test_app();
12459 app.current_service = Service::LambdaFunctions;
12460 app.service_selected = true;
12461 app.mode = Mode::Normal;
12462
12463 app.lambda_state.table.items = vec![Function {
12464 name: "test-function".to_string(),
12465 arn: "arn".to_string(),
12466 application: None,
12467 description: "Test".to_string(),
12468 package_type: "Zip".to_string(),
12469 runtime: "python3.12".to_string(),
12470 architecture: "X86_64".to_string(),
12471 code_size: 1024,
12472 code_sha256: "hash".to_string(),
12473 memory_mb: 128,
12474 timeout_seconds: 30,
12475 last_modified: "2024-01-01".to_string(),
12476 layers: vec![],
12477 }];
12478
12479 app.lambda_state.table.selected = 0;
12480
12481 app.handle_action(Action::NextPane);
12483 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
12484
12485 app.handle_action(Action::PrevPane);
12487 assert_eq!(app.lambda_state.table.expanded_item, None);
12488 }
12489
12490 #[test]
12491 fn test_lambda_version_detail_with_application() {
12492 use crate::lambda::Function;
12493
12494 let mut app = test_app();
12495 app.current_service = Service::LambdaFunctions;
12496 app.service_selected = true;
12497 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
12498 app.lambda_state.current_version = Some("1".to_string());
12499 app.lambda_state.detail_tab = LambdaDetailTab::Code;
12500 app.mode = Mode::Normal;
12501
12502 app.lambda_state.table.items = vec![Function {
12503 name: "storefront-studio-beta-api".to_string(),
12504 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
12505 .to_string(),
12506 application: Some("storefront-studio-beta".to_string()),
12507 description: "API function".to_string(),
12508 package_type: "Zip".to_string(),
12509 runtime: "python3.12".to_string(),
12510 architecture: "X86_64".to_string(),
12511 code_size: 1024,
12512 code_sha256: "hash".to_string(),
12513 memory_mb: 128,
12514 timeout_seconds: 30,
12515 last_modified: "2024-01-01".to_string(),
12516 layers: vec![],
12517 }];
12518
12519 assert_eq!(
12521 app.lambda_state.table.items[0].application,
12522 Some("storefront-studio-beta".to_string())
12523 );
12524 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
12525 }
12526
12527 #[test]
12528 fn test_lambda_layer_navigation() {
12529 use crate::lambda::{Function, Layer};
12530
12531 let mut app = test_app();
12532 app.current_service = Service::LambdaFunctions;
12533 app.service_selected = true;
12534 app.lambda_state.current_function = Some("test-function".to_string());
12535 app.lambda_state.detail_tab = LambdaDetailTab::Code;
12536 app.mode = Mode::Normal;
12537
12538 app.lambda_state.table.items = vec![Function {
12539 name: "test-function".to_string(),
12540 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
12541 application: None,
12542 description: "Test".to_string(),
12543 package_type: "Zip".to_string(),
12544 runtime: "python3.12".to_string(),
12545 architecture: "X86_64".to_string(),
12546 code_size: 1024,
12547 code_sha256: "hash".to_string(),
12548 memory_mb: 128,
12549 timeout_seconds: 30,
12550 last_modified: "2024-01-01".to_string(),
12551 layers: vec![
12552 Layer {
12553 merge_order: "1".to_string(),
12554 name: "layer1".to_string(),
12555 layer_version: "1".to_string(),
12556 compatible_runtimes: "python3.9".to_string(),
12557 compatible_architectures: "x86_64".to_string(),
12558 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
12559 },
12560 Layer {
12561 merge_order: "2".to_string(),
12562 name: "layer2".to_string(),
12563 layer_version: "2".to_string(),
12564 compatible_runtimes: "python3.9".to_string(),
12565 compatible_architectures: "x86_64".to_string(),
12566 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
12567 },
12568 Layer {
12569 merge_order: "3".to_string(),
12570 name: "layer3".to_string(),
12571 layer_version: "3".to_string(),
12572 compatible_runtimes: "python3.9".to_string(),
12573 compatible_architectures: "x86_64".to_string(),
12574 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
12575 },
12576 ],
12577 }];
12578
12579 assert_eq!(app.lambda_state.layer_selected, 0);
12580
12581 app.handle_action(Action::NextItem);
12582 assert_eq!(app.lambda_state.layer_selected, 1);
12583
12584 app.handle_action(Action::NextItem);
12585 assert_eq!(app.lambda_state.layer_selected, 2);
12586
12587 app.handle_action(Action::NextItem);
12588 assert_eq!(app.lambda_state.layer_selected, 2);
12589
12590 app.handle_action(Action::PrevItem);
12591 assert_eq!(app.lambda_state.layer_selected, 1);
12592
12593 app.handle_action(Action::PrevItem);
12594 assert_eq!(app.lambda_state.layer_selected, 0);
12595
12596 app.handle_action(Action::PrevItem);
12597 assert_eq!(app.lambda_state.layer_selected, 0);
12598 }
12599
12600 #[test]
12601 fn test_lambda_layer_expansion() {
12602 use crate::lambda::{Function, Layer};
12603
12604 let mut app = test_app();
12605 app.current_service = Service::LambdaFunctions;
12606 app.service_selected = true;
12607 app.lambda_state.current_function = Some("test-function".to_string());
12608 app.lambda_state.detail_tab = LambdaDetailTab::Code;
12609 app.mode = Mode::Normal;
12610
12611 app.lambda_state.table.items = vec![Function {
12612 name: "test-function".to_string(),
12613 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
12614 application: None,
12615 description: "Test".to_string(),
12616 package_type: "Zip".to_string(),
12617 runtime: "python3.12".to_string(),
12618 architecture: "X86_64".to_string(),
12619 code_size: 1024,
12620 code_sha256: "hash".to_string(),
12621 memory_mb: 128,
12622 timeout_seconds: 30,
12623 last_modified: "2024-01-01".to_string(),
12624 layers: vec![Layer {
12625 merge_order: "1".to_string(),
12626 name: "test-layer".to_string(),
12627 layer_version: "1".to_string(),
12628 compatible_runtimes: "python3.9".to_string(),
12629 compatible_architectures: "x86_64".to_string(),
12630 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
12631 }],
12632 }];
12633
12634 assert_eq!(app.lambda_state.layer_expanded, None);
12635
12636 app.handle_action(Action::NextPane);
12637 assert_eq!(app.lambda_state.layer_expanded, Some(0));
12638
12639 app.handle_action(Action::PrevPane);
12640 assert_eq!(app.lambda_state.layer_expanded, None);
12641
12642 app.handle_action(Action::NextPane);
12643 assert_eq!(app.lambda_state.layer_expanded, Some(0));
12644
12645 app.handle_action(Action::NextPane);
12646 assert_eq!(app.lambda_state.layer_expanded, None);
12647 }
12648
12649 #[test]
12650 fn test_lambda_layer_selection_and_expansion_workflow() {
12651 use crate::lambda::{Function, Layer};
12652
12653 let mut app = test_app();
12654 app.current_service = Service::LambdaFunctions;
12655 app.service_selected = true;
12656 app.lambda_state.current_function = Some("test-function".to_string());
12657 app.lambda_state.detail_tab = LambdaDetailTab::Code;
12658 app.mode = Mode::Normal;
12659
12660 app.lambda_state.table.items = vec![Function {
12661 name: "test-function".to_string(),
12662 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
12663 application: None,
12664 description: "Test".to_string(),
12665 package_type: "Zip".to_string(),
12666 runtime: "python3.12".to_string(),
12667 architecture: "X86_64".to_string(),
12668 code_size: 1024,
12669 code_sha256: "hash".to_string(),
12670 memory_mb: 128,
12671 timeout_seconds: 30,
12672 last_modified: "2024-01-01".to_string(),
12673 layers: vec![
12674 Layer {
12675 merge_order: "1".to_string(),
12676 name: "layer1".to_string(),
12677 layer_version: "1".to_string(),
12678 compatible_runtimes: "python3.9".to_string(),
12679 compatible_architectures: "x86_64".to_string(),
12680 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
12681 },
12682 Layer {
12683 merge_order: "2".to_string(),
12684 name: "layer2".to_string(),
12685 layer_version: "2".to_string(),
12686 compatible_runtimes: "python3.9".to_string(),
12687 compatible_architectures: "x86_64".to_string(),
12688 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
12689 },
12690 ],
12691 }];
12692
12693 assert_eq!(app.lambda_state.layer_selected, 0);
12695 assert_eq!(app.lambda_state.layer_expanded, None);
12696
12697 app.handle_action(Action::NextPane);
12699 assert_eq!(app.lambda_state.layer_selected, 0);
12700 assert_eq!(app.lambda_state.layer_expanded, Some(0));
12701
12702 app.handle_action(Action::NextItem);
12704 assert_eq!(app.lambda_state.layer_selected, 1);
12705 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
12709 assert_eq!(app.lambda_state.layer_selected, 1);
12710 assert_eq!(app.lambda_state.layer_expanded, Some(1));
12711
12712 app.handle_action(Action::PrevPane);
12714 assert_eq!(app.lambda_state.layer_selected, 1);
12715 assert_eq!(app.lambda_state.layer_expanded, None);
12716
12717 app.handle_action(Action::PrevItem);
12719 assert_eq!(app.lambda_state.layer_selected, 0);
12720 assert_eq!(app.lambda_state.layer_expanded, None);
12721 }
12722
12723 #[test]
12724 fn test_backtab_cycles_detail_tabs_backward() {
12725 let mut app = test_app();
12726 app.mode = Mode::Normal;
12727
12728 app.current_service = Service::LambdaFunctions;
12730 app.service_selected = true;
12731 app.lambda_state.current_function = Some("test-function".to_string());
12732 app.lambda_state.detail_tab = LambdaDetailTab::Code;
12733
12734 app.handle_action(Action::PrevDetailTab);
12735 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
12736
12737 app.handle_action(Action::PrevDetailTab);
12738 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
12739
12740 app.current_service = Service::IamRoles;
12742 app.iam_state.current_role = Some("test-role".to_string());
12743 app.iam_state.role_tab = RoleTab::Permissions;
12744
12745 app.handle_action(Action::PrevDetailTab);
12746 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
12747
12748 app.current_service = Service::IamUsers;
12750 app.iam_state.current_user = Some("test-user".to_string());
12751 app.iam_state.user_tab = UserTab::Permissions;
12752
12753 app.handle_action(Action::PrevDetailTab);
12754 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
12755
12756 app.current_service = Service::IamUserGroups;
12758 app.iam_state.current_group = Some("test-group".to_string());
12759 app.iam_state.group_tab = GroupTab::Permissions;
12760
12761 app.handle_action(Action::PrevDetailTab);
12762 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
12763
12764 app.current_service = Service::S3Buckets;
12766 app.s3_state.current_bucket = Some("test-bucket".to_string());
12767 app.s3_state.object_tab = S3ObjectTab::Properties;
12768
12769 app.handle_action(Action::PrevDetailTab);
12770 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
12771
12772 app.current_service = Service::EcrRepositories;
12774 app.ecr_state.current_repository = None;
12775 app.ecr_state.tab = EcrTab::Private;
12776
12777 app.handle_action(Action::PrevDetailTab);
12778 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12779
12780 app.current_service = Service::CloudFormationStacks;
12782 app.cfn_state.current_stack = Some("test-stack".to_string());
12783 app.cfn_state.detail_tab = CfnDetailTab::Resources;
12784 }
12785
12786 #[test]
12787 fn test_cloudformation_status_filter_active() {
12788 use crate::ui::cfn::StatusFilter;
12789 let filter = StatusFilter::Active;
12790 assert!(filter.matches("CREATE_IN_PROGRESS"));
12791 assert!(filter.matches("UPDATE_IN_PROGRESS"));
12792 assert!(!filter.matches("CREATE_COMPLETE"));
12793 assert!(!filter.matches("DELETE_COMPLETE"));
12794 assert!(!filter.matches("CREATE_FAILED"));
12795 }
12796
12797 #[test]
12798 fn test_cloudformation_status_filter_complete() {
12799 use crate::ui::cfn::StatusFilter;
12800 let filter = StatusFilter::Complete;
12801 assert!(filter.matches("CREATE_COMPLETE"));
12802 assert!(filter.matches("UPDATE_COMPLETE"));
12803 assert!(!filter.matches("DELETE_COMPLETE"));
12804 assert!(!filter.matches("CREATE_IN_PROGRESS"));
12805 }
12806
12807 #[test]
12808 fn test_cloudformation_status_filter_failed() {
12809 use crate::ui::cfn::StatusFilter;
12810 let filter = StatusFilter::Failed;
12811 assert!(filter.matches("CREATE_FAILED"));
12812 assert!(filter.matches("UPDATE_FAILED"));
12813 assert!(!filter.matches("CREATE_COMPLETE"));
12814 }
12815
12816 #[test]
12817 fn test_cloudformation_status_filter_deleted() {
12818 use crate::ui::cfn::StatusFilter;
12819 let filter = StatusFilter::Deleted;
12820 assert!(filter.matches("DELETE_COMPLETE"));
12821 assert!(filter.matches("DELETE_IN_PROGRESS"));
12822 assert!(!filter.matches("CREATE_COMPLETE"));
12823 }
12824
12825 #[test]
12826 fn test_cloudformation_status_filter_in_progress() {
12827 use crate::ui::cfn::StatusFilter;
12828 let filter = StatusFilter::InProgress;
12829 assert!(filter.matches("CREATE_IN_PROGRESS"));
12830 assert!(filter.matches("UPDATE_IN_PROGRESS"));
12831 assert!(filter.matches("DELETE_IN_PROGRESS"));
12832 assert!(!filter.matches("CREATE_COMPLETE"));
12833 }
12834
12835 #[test]
12836 fn test_cloudformation_status_filter_cycle() {
12837 use crate::ui::cfn::StatusFilter;
12838 let filter = StatusFilter::All;
12839 assert_eq!(filter.next(), StatusFilter::Active);
12840 assert_eq!(filter.next().next(), StatusFilter::Complete);
12841 assert_eq!(filter.next().next().next(), StatusFilter::Failed);
12842 assert_eq!(filter.next().next().next().next(), StatusFilter::Deleted);
12843 assert_eq!(
12844 filter.next().next().next().next().next(),
12845 StatusFilter::InProgress
12846 );
12847 assert_eq!(
12848 filter.next().next().next().next().next().next(),
12849 StatusFilter::All
12850 );
12851 }
12852
12853 #[test]
12854 fn test_cloudformation_default_columns() {
12855 let app = test_app();
12856 assert_eq!(app.cfn_visible_column_ids.len(), 4);
12857 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
12858 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
12859 assert!(app
12860 .cfn_visible_column_ids
12861 .contains(&CfnColumn::CreatedTime.id()));
12862 assert!(app
12863 .cfn_visible_column_ids
12864 .contains(&CfnColumn::Description.id()));
12865 }
12866
12867 #[test]
12868 fn test_cloudformation_all_columns() {
12869 let app = test_app();
12870 assert_eq!(app.cfn_column_ids.len(), 10);
12871 }
12872
12873 #[test]
12874 fn test_cloudformation_filter_by_name() {
12875 use crate::ui::cfn::StatusFilter;
12876 let mut app = test_app();
12877 app.cfn_state.status_filter = StatusFilter::Complete;
12878 app.cfn_state.table.items = vec![
12879 CfnStack {
12880 name: "my-stack".to_string(),
12881 stack_id: "id1".to_string(),
12882 status: "CREATE_COMPLETE".to_string(),
12883 created_time: "2024-01-01".to_string(),
12884 updated_time: String::new(),
12885 deleted_time: String::new(),
12886 drift_status: String::new(),
12887 last_drift_check_time: String::new(),
12888 status_reason: String::new(),
12889 description: String::new(),
12890 detailed_status: String::new(),
12891 root_stack: String::new(),
12892 parent_stack: String::new(),
12893 termination_protection: false,
12894 iam_role: String::new(),
12895 tags: Vec::new(),
12896 stack_policy: String::new(),
12897 rollback_monitoring_time: String::new(),
12898 rollback_alarms: Vec::new(),
12899 notification_arns: Vec::new(),
12900 },
12901 CfnStack {
12902 name: "other-stack".to_string(),
12903 stack_id: "id2".to_string(),
12904 status: "CREATE_COMPLETE".to_string(),
12905 created_time: "2024-01-02".to_string(),
12906 updated_time: String::new(),
12907 deleted_time: String::new(),
12908 drift_status: String::new(),
12909 last_drift_check_time: String::new(),
12910 status_reason: String::new(),
12911 description: String::new(),
12912 detailed_status: String::new(),
12913 root_stack: String::new(),
12914 parent_stack: String::new(),
12915 termination_protection: false,
12916 iam_role: String::new(),
12917 tags: Vec::new(),
12918 stack_policy: String::new(),
12919 rollback_monitoring_time: String::new(),
12920 rollback_alarms: Vec::new(),
12921 notification_arns: Vec::new(),
12922 },
12923 ];
12924
12925 app.cfn_state.table.filter = "my".to_string();
12926 let filtered = app.filtered_cloudformation_stacks();
12927 assert_eq!(filtered.len(), 1);
12928 assert_eq!(filtered[0].name, "my-stack");
12929 }
12930
12931 #[test]
12932 fn test_cloudformation_filter_by_description() {
12933 use crate::ui::cfn::StatusFilter;
12934 let mut app = test_app();
12935 app.cfn_state.status_filter = StatusFilter::Complete;
12936 app.cfn_state.table.items = vec![CfnStack {
12937 name: "stack1".to_string(),
12938 stack_id: "id1".to_string(),
12939 status: "CREATE_COMPLETE".to_string(),
12940 created_time: "2024-01-01".to_string(),
12941 updated_time: String::new(),
12942 deleted_time: String::new(),
12943 drift_status: String::new(),
12944 last_drift_check_time: String::new(),
12945 status_reason: String::new(),
12946 description: "production stack".to_string(),
12947 detailed_status: String::new(),
12948 root_stack: String::new(),
12949 parent_stack: String::new(),
12950 termination_protection: false,
12951 iam_role: String::new(),
12952 tags: Vec::new(),
12953 stack_policy: String::new(),
12954 rollback_monitoring_time: String::new(),
12955 rollback_alarms: Vec::new(),
12956 notification_arns: Vec::new(),
12957 }];
12958
12959 app.cfn_state.table.filter = "production".to_string();
12960 let filtered = app.filtered_cloudformation_stacks();
12961 assert_eq!(filtered.len(), 1);
12962 }
12963
12964 #[test]
12965 fn test_cloudformation_status_filter_applied() {
12966 use crate::ui::cfn::StatusFilter;
12967 let mut app = test_app();
12968 app.cfn_state.table.items = vec![
12969 CfnStack {
12970 name: "complete-stack".to_string(),
12971 stack_id: "id1".to_string(),
12972 status: "CREATE_COMPLETE".to_string(),
12973 created_time: "2024-01-01".to_string(),
12974 updated_time: String::new(),
12975 deleted_time: String::new(),
12976 drift_status: String::new(),
12977 last_drift_check_time: String::new(),
12978 status_reason: String::new(),
12979 description: String::new(),
12980 detailed_status: String::new(),
12981 root_stack: String::new(),
12982 parent_stack: String::new(),
12983 termination_protection: false,
12984 iam_role: String::new(),
12985 tags: Vec::new(),
12986 stack_policy: String::new(),
12987 rollback_monitoring_time: String::new(),
12988 rollback_alarms: Vec::new(),
12989 notification_arns: Vec::new(),
12990 },
12991 CfnStack {
12992 name: "failed-stack".to_string(),
12993 stack_id: "id2".to_string(),
12994 status: "CREATE_FAILED".to_string(),
12995 created_time: "2024-01-02".to_string(),
12996 updated_time: String::new(),
12997 deleted_time: String::new(),
12998 drift_status: String::new(),
12999 last_drift_check_time: String::new(),
13000 status_reason: String::new(),
13001 description: String::new(),
13002 detailed_status: String::new(),
13003 root_stack: String::new(),
13004 parent_stack: String::new(),
13005 termination_protection: false,
13006 iam_role: String::new(),
13007 tags: Vec::new(),
13008 stack_policy: String::new(),
13009 rollback_monitoring_time: String::new(),
13010 rollback_alarms: Vec::new(),
13011 notification_arns: Vec::new(),
13012 },
13013 ];
13014
13015 app.cfn_state.status_filter = StatusFilter::Complete;
13016 let filtered = app.filtered_cloudformation_stacks();
13017 assert_eq!(filtered.len(), 1);
13018 assert_eq!(filtered[0].name, "complete-stack");
13019
13020 app.cfn_state.status_filter = StatusFilter::Failed;
13021 let filtered = app.filtered_cloudformation_stacks();
13022 assert_eq!(filtered.len(), 1);
13023 assert_eq!(filtered[0].name, "failed-stack");
13024 }
13025
13026 #[test]
13027 fn test_cloudformation_default_page_size() {
13028 let app = test_app();
13029 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
13030 }
13031
13032 #[test]
13033 fn test_cloudformation_default_status_filter() {
13034 use crate::ui::cfn::StatusFilter;
13035 let app = test_app();
13036 assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
13037 }
13038
13039 #[test]
13040 fn test_cloudformation_view_nested_default_false() {
13041 let app = test_app();
13042 assert!(!app.cfn_state.view_nested);
13043 }
13044
13045 #[test]
13046 fn test_cloudformation_pagination_hotkeys() {
13047 use crate::ui::cfn::StatusFilter;
13048 let mut app = test_app();
13049 app.current_service = Service::CloudFormationStacks;
13050 app.service_selected = true;
13051 app.cfn_state.status_filter = StatusFilter::All;
13052
13053 for i in 0..150 {
13055 app.cfn_state.table.items.push(CfnStack {
13056 name: format!("stack-{}", i),
13057 stack_id: format!("id-{}", i),
13058 status: "CREATE_COMPLETE".to_string(),
13059 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13060 updated_time: String::new(),
13061 deleted_time: String::new(),
13062 drift_status: String::new(),
13063 last_drift_check_time: String::new(),
13064 status_reason: String::new(),
13065 description: String::new(),
13066 detailed_status: String::new(),
13067 root_stack: String::new(),
13068 parent_stack: String::new(),
13069 termination_protection: false,
13070 iam_role: String::new(),
13071 tags: vec![],
13072 stack_policy: String::new(),
13073 rollback_monitoring_time: String::new(),
13074 rollback_alarms: vec![],
13075 notification_arns: vec![],
13076 });
13077 }
13078
13079 app.go_to_page(2);
13081 assert_eq!(app.cfn_state.table.selected, 50);
13082
13083 app.go_to_page(3);
13085 assert_eq!(app.cfn_state.table.selected, 100);
13086
13087 app.go_to_page(1);
13089 assert_eq!(app.cfn_state.table.selected, 0);
13090 }
13091
13092 #[test]
13093 fn test_cloudformation_tab_cycling_in_filter_mode() {
13094 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
13095 let mut app = test_app();
13096 app.current_service = Service::CloudFormationStacks;
13097 app.service_selected = true;
13098 app.mode = Mode::FilterInput;
13099 app.cfn_state.input_focus = InputFocus::Filter;
13100
13101 app.handle_action(Action::NextFilterFocus);
13103 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
13104
13105 app.handle_action(Action::NextFilterFocus);
13107 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
13108
13109 app.handle_action(Action::NextFilterFocus);
13111 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
13112
13113 app.handle_action(Action::NextFilterFocus);
13115 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
13116 }
13117
13118 #[test]
13119 fn test_cloudformation_timestamp_format_includes_utc() {
13120 let stack = CfnStack {
13121 name: "test-stack".to_string(),
13122 stack_id: "id-123".to_string(),
13123 status: "CREATE_COMPLETE".to_string(),
13124 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
13125 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
13126 deleted_time: String::new(),
13127 drift_status: String::new(),
13128 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
13129 status_reason: String::new(),
13130 description: String::new(),
13131 detailed_status: String::new(),
13132 root_stack: String::new(),
13133 parent_stack: String::new(),
13134 termination_protection: false,
13135 iam_role: String::new(),
13136 tags: vec![],
13137 stack_policy: String::new(),
13138 rollback_monitoring_time: String::new(),
13139 rollback_alarms: vec![],
13140 notification_arns: vec![],
13141 };
13142
13143 assert!(stack.created_time.contains("(UTC)"));
13144 assert!(stack.updated_time.contains("(UTC)"));
13145 assert!(stack.last_drift_check_time.contains("(UTC)"));
13146 assert_eq!(stack.created_time.len(), 25);
13147 }
13148
13149 #[test]
13150 fn test_cloudformation_enter_drills_into_stack_view() {
13151 use crate::ui::cfn::StatusFilter;
13152 let mut app = test_app();
13153 app.current_service = Service::CloudFormationStacks;
13154 app.service_selected = true;
13155 app.mode = Mode::Normal;
13156 app.cfn_state.status_filter = StatusFilter::All;
13157 app.tabs = vec![Tab {
13158 service: Service::CloudFormationStacks,
13159 title: "CloudFormation > Stacks".to_string(),
13160 breadcrumb: "CloudFormation > Stacks".to_string(),
13161 }];
13162 app.current_tab = 0;
13163
13164 app.cfn_state.table.items.push(CfnStack {
13165 name: "test-stack".to_string(),
13166 stack_id: "id-123".to_string(),
13167 status: "CREATE_COMPLETE".to_string(),
13168 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13169 updated_time: String::new(),
13170 deleted_time: String::new(),
13171 drift_status: String::new(),
13172 last_drift_check_time: String::new(),
13173 status_reason: String::new(),
13174 description: String::new(),
13175 detailed_status: String::new(),
13176 root_stack: String::new(),
13177 parent_stack: String::new(),
13178 termination_protection: false,
13179 iam_role: String::new(),
13180 tags: vec![],
13181 stack_policy: String::new(),
13182 rollback_monitoring_time: String::new(),
13183 rollback_alarms: vec![],
13184 notification_arns: vec![],
13185 });
13186
13187 app.cfn_state.table.reset();
13188 assert_eq!(app.cfn_state.current_stack, None);
13189
13190 app.handle_action(Action::Select);
13192 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
13193 }
13194
13195 #[test]
13196 fn test_cloudformation_arrow_keys_expand_collapse() {
13197 use crate::ui::cfn::StatusFilter;
13198 let mut app = test_app();
13199 app.current_service = Service::CloudFormationStacks;
13200 app.service_selected = true;
13201 app.mode = Mode::Normal;
13202 app.cfn_state.status_filter = StatusFilter::All;
13203
13204 app.cfn_state.table.items.push(CfnStack {
13205 name: "test-stack".to_string(),
13206 stack_id: "id-123".to_string(),
13207 status: "CREATE_COMPLETE".to_string(),
13208 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13209 updated_time: String::new(),
13210 deleted_time: String::new(),
13211 drift_status: String::new(),
13212 last_drift_check_time: String::new(),
13213 status_reason: String::new(),
13214 description: String::new(),
13215 detailed_status: String::new(),
13216 root_stack: String::new(),
13217 parent_stack: String::new(),
13218 termination_protection: false,
13219 iam_role: String::new(),
13220 tags: vec![],
13221 stack_policy: String::new(),
13222 rollback_monitoring_time: String::new(),
13223 rollback_alarms: vec![],
13224 notification_arns: vec![],
13225 });
13226
13227 app.cfn_state.table.reset();
13228 assert_eq!(app.cfn_state.table.expanded_item, None);
13229
13230 app.handle_action(Action::NextPane);
13232 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
13233
13234 app.handle_action(Action::PrevPane);
13236 assert_eq!(app.cfn_state.table.expanded_item, None);
13237
13238 assert_eq!(app.cfn_state.current_stack, None);
13240 }
13241
13242 #[test]
13243 fn test_cloudformation_tab_cycling() {
13244 use crate::ui::cfn::{DetailTab, StatusFilter};
13245 let mut app = test_app();
13246 app.current_service = Service::CloudFormationStacks;
13247 app.service_selected = true;
13248 app.mode = Mode::Normal;
13249 app.cfn_state.status_filter = StatusFilter::All;
13250 app.cfn_state.current_stack = Some("test-stack".to_string());
13251
13252 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
13253 }
13254
13255 #[test]
13256 fn test_cloudformation_console_url() {
13257 use crate::ui::cfn::{DetailTab, StatusFilter};
13258 let mut app = test_app();
13259 app.current_service = Service::CloudFormationStacks;
13260 app.service_selected = true;
13261 app.cfn_state.status_filter = StatusFilter::All;
13262
13263 app.cfn_state.table.items.push(CfnStack {
13264 name: "test-stack".to_string(),
13265 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
13266 .to_string(),
13267 status: "CREATE_COMPLETE".to_string(),
13268 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13269 updated_time: String::new(),
13270 deleted_time: String::new(),
13271 drift_status: String::new(),
13272 last_drift_check_time: String::new(),
13273 status_reason: String::new(),
13274 description: String::new(),
13275 detailed_status: String::new(),
13276 root_stack: String::new(),
13277 parent_stack: String::new(),
13278 termination_protection: false,
13279 iam_role: String::new(),
13280 tags: vec![],
13281 stack_policy: String::new(),
13282 rollback_monitoring_time: String::new(),
13283 rollback_alarms: vec![],
13284 notification_arns: vec![],
13285 });
13286
13287 app.cfn_state.current_stack = Some("test-stack".to_string());
13288
13289 app.cfn_state.detail_tab = DetailTab::StackInfo;
13291 let url = app.get_console_url();
13292 assert!(url.contains("stackinfo"));
13293 assert!(url.contains("arn%3Aaws%3Acloudformation"));
13294
13295 app.cfn_state.detail_tab = DetailTab::Events;
13297 let url = app.get_console_url();
13298 assert!(url.contains("events"));
13299 assert!(url.contains("arn%3Aaws%3Acloudformation"));
13300 }
13301
13302 #[test]
13303 fn test_iam_role_select() {
13304 let mut app = test_app();
13305 app.current_service = Service::IamRoles;
13306 app.service_selected = true;
13307 app.mode = Mode::Normal;
13308
13309 app.iam_state.roles.items = vec![
13310 crate::iam::IamRole {
13311 role_name: "role1".to_string(),
13312 path: "/".to_string(),
13313 trusted_entities: "AWS Service: ec2".to_string(),
13314 last_activity: "-".to_string(),
13315 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
13316 creation_time: "2025-01-01".to_string(),
13317 description: "Test role 1".to_string(),
13318 max_session_duration: Some(3600),
13319 },
13320 crate::iam::IamRole {
13321 role_name: "role2".to_string(),
13322 path: "/".to_string(),
13323 trusted_entities: "AWS Service: lambda".to_string(),
13324 last_activity: "-".to_string(),
13325 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
13326 creation_time: "2025-01-02".to_string(),
13327 description: "Test role 2".to_string(),
13328 max_session_duration: Some(7200),
13329 },
13330 ];
13331
13332 app.iam_state.roles.selected = 0;
13334 app.handle_action(Action::Select);
13335
13336 assert_eq!(
13337 app.iam_state.current_role,
13338 Some("role1".to_string()),
13339 "Should open role detail view"
13340 );
13341 assert_eq!(
13342 app.iam_state.role_tab,
13343 RoleTab::Permissions,
13344 "Should default to Permissions tab"
13345 );
13346 }
13347
13348 #[test]
13349 fn test_iam_role_back_navigation() {
13350 let mut app = test_app();
13351 app.current_service = Service::IamRoles;
13352 app.service_selected = true;
13353 app.iam_state.current_role = Some("test-role".to_string());
13354
13355 app.handle_action(Action::GoBack);
13356
13357 assert_eq!(
13358 app.iam_state.current_role, None,
13359 "Should return to roles list"
13360 );
13361 }
13362
13363 #[test]
13364 fn test_iam_role_tab_navigation() {
13365 let mut app = test_app();
13366 app.current_service = Service::IamRoles;
13367 app.service_selected = true;
13368 app.iam_state.current_role = Some("test-role".to_string());
13369 app.iam_state.role_tab = RoleTab::Permissions;
13370
13371 app.handle_action(Action::NextDetailTab);
13372
13373 assert_eq!(
13374 app.iam_state.role_tab,
13375 RoleTab::TrustRelationships,
13376 "Should move to next tab"
13377 );
13378 }
13379
13380 #[test]
13381 fn test_iam_role_tab_cycle_order() {
13382 let mut app = test_app();
13383 app.current_service = Service::IamRoles;
13384 app.service_selected = true;
13385 app.iam_state.current_role = Some("test-role".to_string());
13386 app.iam_state.role_tab = RoleTab::Permissions;
13387
13388 app.handle_action(Action::NextDetailTab);
13389 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
13390
13391 app.handle_action(Action::NextDetailTab);
13392 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
13393
13394 app.handle_action(Action::NextDetailTab);
13395 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
13396
13397 app.handle_action(Action::NextDetailTab);
13398 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
13399
13400 app.handle_action(Action::NextDetailTab);
13401 assert_eq!(
13402 app.iam_state.role_tab,
13403 RoleTab::Permissions,
13404 "Should cycle back to first tab"
13405 );
13406 }
13407
13408 #[test]
13409 fn test_iam_role_pagination() {
13410 let mut app = test_app();
13411 app.current_service = Service::IamRoles;
13412 app.service_selected = true;
13413 app.iam_state.roles.page_size = crate::common::PageSize::Ten;
13414
13415 app.iam_state.roles.items = (0..25)
13416 .map(|i| crate::iam::IamRole {
13417 role_name: format!("role{}", i),
13418 path: "/".to_string(),
13419 trusted_entities: "AWS Service: ec2".to_string(),
13420 last_activity: "-".to_string(),
13421 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
13422 creation_time: "2025-01-01".to_string(),
13423 description: format!("Test role {}", i),
13424 max_session_duration: Some(3600),
13425 })
13426 .collect();
13427
13428 app.go_to_page(2);
13430
13431 assert_eq!(
13432 app.iam_state.roles.selected, 10,
13433 "Should select first item of page 2"
13434 );
13435 assert_eq!(
13436 app.iam_state.roles.scroll_offset, 10,
13437 "Should update scroll offset"
13438 );
13439 }
13440
13441 #[test]
13442 fn test_tags_table_populated_on_role_detail() {
13443 let mut app = test_app();
13444 app.current_service = Service::IamRoles;
13445 app.service_selected = true;
13446 app.mode = Mode::Normal;
13447 app.iam_state.roles.items = vec![crate::iam::IamRole {
13448 role_name: "TestRole".to_string(),
13449 path: "/".to_string(),
13450 trusted_entities: String::new(),
13451 last_activity: String::new(),
13452 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
13453 creation_time: "2025-01-01".to_string(),
13454 description: String::new(),
13455 max_session_duration: Some(3600),
13456 }];
13457
13458 app.iam_state.tags.items = vec![
13460 crate::iam::RoleTag {
13461 key: "Environment".to_string(),
13462 value: "Production".to_string(),
13463 },
13464 crate::iam::RoleTag {
13465 key: "Team".to_string(),
13466 value: "Platform".to_string(),
13467 },
13468 ];
13469
13470 assert_eq!(app.iam_state.tags.items.len(), 2);
13471 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
13472 assert_eq!(app.iam_state.tags.items[0].value, "Production");
13473 assert_eq!(app.iam_state.tags.selected, 0);
13474 }
13475
13476 #[test]
13477 fn test_tags_table_navigation() {
13478 let mut app = test_app();
13479 app.current_service = Service::IamRoles;
13480 app.service_selected = true;
13481 app.mode = Mode::Normal;
13482 app.iam_state.current_role = Some("TestRole".to_string());
13483 app.iam_state.role_tab = RoleTab::Tags;
13484 app.iam_state.tags.items = vec![
13485 crate::iam::RoleTag {
13486 key: "Tag1".to_string(),
13487 value: "Value1".to_string(),
13488 },
13489 crate::iam::RoleTag {
13490 key: "Tag2".to_string(),
13491 value: "Value2".to_string(),
13492 },
13493 ];
13494
13495 app.handle_action(Action::NextItem);
13496 assert_eq!(app.iam_state.tags.selected, 1);
13497
13498 app.handle_action(Action::PrevItem);
13499 assert_eq!(app.iam_state.tags.selected, 0);
13500 }
13501
13502 #[test]
13503 fn test_last_accessed_table_navigation() {
13504 let mut app = test_app();
13505 app.current_service = Service::IamRoles;
13506 app.service_selected = true;
13507 app.mode = Mode::Normal;
13508 app.iam_state.current_role = Some("TestRole".to_string());
13509 app.iam_state.role_tab = RoleTab::LastAccessed;
13510 app.iam_state.last_accessed_services.items = vec![
13511 crate::iam::LastAccessedService {
13512 service: "S3".to_string(),
13513 policies_granting: "Policy1".to_string(),
13514 last_accessed: "2025-01-01".to_string(),
13515 },
13516 crate::iam::LastAccessedService {
13517 service: "EC2".to_string(),
13518 policies_granting: "Policy2".to_string(),
13519 last_accessed: "2025-01-02".to_string(),
13520 },
13521 ];
13522
13523 app.handle_action(Action::NextItem);
13524 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
13525
13526 app.handle_action(Action::PrevItem);
13527 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
13528 }
13529
13530 #[test]
13531 fn test_cfn_input_focus_next() {
13532 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
13533 let mut app = test_app();
13534 app.current_service = Service::CloudFormationStacks;
13535 app.mode = Mode::FilterInput;
13536 app.cfn_state.input_focus = InputFocus::Filter;
13537
13538 app.handle_action(Action::NextFilterFocus);
13539 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
13540
13541 app.handle_action(Action::NextFilterFocus);
13542 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
13543
13544 app.handle_action(Action::NextFilterFocus);
13545 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
13546
13547 app.handle_action(Action::NextFilterFocus);
13548 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
13549 }
13550
13551 #[test]
13552 fn test_cfn_input_focus_prev() {
13553 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
13554 let mut app = test_app();
13555 app.current_service = Service::CloudFormationStacks;
13556 app.mode = Mode::FilterInput;
13557 app.cfn_state.input_focus = InputFocus::Filter;
13558
13559 app.handle_action(Action::PrevFilterFocus);
13560 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
13561
13562 app.handle_action(Action::PrevFilterFocus);
13563 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
13564
13565 app.handle_action(Action::PrevFilterFocus);
13566 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
13567
13568 app.handle_action(Action::PrevFilterFocus);
13569 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
13570 }
13571
13572 #[test]
13573 fn test_cw_logs_input_focus_prev() {
13574 let mut app = test_app();
13575 app.current_service = Service::CloudWatchLogGroups;
13576 app.mode = Mode::FilterInput;
13577 app.view_mode = ViewMode::Detail;
13578 app.log_groups_state.detail_tab = crate::ui::cw::logs::DetailTab::LogStreams;
13579 app.log_groups_state.input_focus = InputFocus::Filter;
13580
13581 app.handle_action(Action::PrevFilterFocus);
13582 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
13583
13584 app.handle_action(Action::PrevFilterFocus);
13585 assert_eq!(
13586 app.log_groups_state.input_focus,
13587 InputFocus::Checkbox("ShowExpired")
13588 );
13589
13590 app.handle_action(Action::PrevFilterFocus);
13591 assert_eq!(
13592 app.log_groups_state.input_focus,
13593 InputFocus::Checkbox("ExactMatch")
13594 );
13595
13596 app.handle_action(Action::PrevFilterFocus);
13597 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
13598 }
13599
13600 #[test]
13601 fn test_cw_events_input_focus_prev() {
13602 use crate::ui::cw::logs::EventFilterFocus;
13603 let mut app = test_app();
13604 app.mode = Mode::EventFilterInput;
13605 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
13606
13607 app.handle_action(Action::PrevFilterFocus);
13608 assert_eq!(
13609 app.log_groups_state.event_input_focus,
13610 EventFilterFocus::DateRange
13611 );
13612
13613 app.handle_action(Action::PrevFilterFocus);
13614 assert_eq!(
13615 app.log_groups_state.event_input_focus,
13616 EventFilterFocus::Filter
13617 );
13618 }
13619
13620 #[test]
13621 fn test_cfn_input_focus_cycle_complete() {
13622 let mut app = test_app();
13623 app.current_service = Service::CloudFormationStacks;
13624 app.mode = Mode::FilterInput;
13625 app.cfn_state.input_focus = InputFocus::Filter;
13626
13627 for _ in 0..4 {
13629 app.handle_action(Action::NextFilterFocus);
13630 }
13631 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
13632
13633 for _ in 0..4 {
13635 app.handle_action(Action::PrevFilterFocus);
13636 }
13637 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
13638 }
13639
13640 #[test]
13641 fn test_cfn_filter_status_arrow_keys() {
13642 use crate::ui::cfn::{StatusFilter, STATUS_FILTER};
13643 let mut app = test_app();
13644 app.current_service = Service::CloudFormationStacks;
13645 app.mode = Mode::FilterInput;
13646 app.cfn_state.input_focus = STATUS_FILTER;
13647 app.cfn_state.status_filter = StatusFilter::All;
13648
13649 app.handle_action(Action::NextItem);
13650 assert_eq!(app.cfn_state.status_filter, StatusFilter::Active);
13651
13652 app.handle_action(Action::PrevItem);
13653 assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
13654 }
13655
13656 #[test]
13657 fn test_cfn_filter_shift_tab_cycles_backward() {
13658 use crate::ui::cfn::STATUS_FILTER;
13659 let mut app = test_app();
13660 app.current_service = Service::CloudFormationStacks;
13661 app.mode = Mode::FilterInput;
13662 app.cfn_state.input_focus = STATUS_FILTER;
13663
13664 app.handle_action(Action::PrevFilterFocus);
13666 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
13667
13668 app.handle_action(Action::PrevFilterFocus);
13670 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
13671 }
13672
13673 #[test]
13674 fn test_cfn_pagination_arrow_keys() {
13675 let mut app = test_app();
13676 app.current_service = Service::CloudFormationStacks;
13677 app.mode = Mode::FilterInput;
13678 app.cfn_state.input_focus = InputFocus::Pagination;
13679 app.cfn_state.table.scroll_offset = 0;
13680 app.cfn_state.table.page_size = crate::common::PageSize::Ten;
13681
13682 app.cfn_state.table.items = (0..30)
13684 .map(|i| crate::cfn::Stack {
13685 name: format!("stack-{}", i),
13686 stack_id: format!("id-{}", i),
13687 status: "CREATE_COMPLETE".to_string(),
13688 created_time: "2024-01-01".to_string(),
13689 updated_time: String::new(),
13690 deleted_time: String::new(),
13691 drift_status: String::new(),
13692 last_drift_check_time: String::new(),
13693 status_reason: String::new(),
13694 description: String::new(),
13695 detailed_status: String::new(),
13696 root_stack: String::new(),
13697 parent_stack: String::new(),
13698 termination_protection: false,
13699 iam_role: String::new(),
13700 tags: Vec::new(),
13701 stack_policy: String::new(),
13702 rollback_monitoring_time: String::new(),
13703 rollback_alarms: Vec::new(),
13704 notification_arns: Vec::new(),
13705 })
13706 .collect();
13707
13708 app.handle_action(Action::PageDown);
13710 assert_eq!(app.cfn_state.table.scroll_offset, 10);
13711 let page_size = app.cfn_state.table.page_size.value();
13713 let current_page = app.cfn_state.table.scroll_offset / page_size;
13714 assert_eq!(current_page, 1);
13715
13716 app.handle_action(Action::PageUp);
13718 assert_eq!(app.cfn_state.table.scroll_offset, 0);
13719 let current_page = app.cfn_state.table.scroll_offset / page_size;
13720 assert_eq!(current_page, 0);
13721 }
13722
13723 #[test]
13724 fn test_cfn_page_navigation_updates_selection() {
13725 let mut app = test_app();
13726 app.current_service = Service::CloudFormationStacks;
13727 app.mode = Mode::Normal;
13728
13729 app.cfn_state.table.items = (0..30)
13731 .map(|i| crate::cfn::Stack {
13732 name: format!("stack-{}", i),
13733 stack_id: format!("id-{}", i),
13734 status: "CREATE_COMPLETE".to_string(),
13735 created_time: "2024-01-01".to_string(),
13736 updated_time: String::new(),
13737 deleted_time: String::new(),
13738 drift_status: String::new(),
13739 last_drift_check_time: String::new(),
13740 status_reason: String::new(),
13741 description: String::new(),
13742 detailed_status: String::new(),
13743 root_stack: String::new(),
13744 parent_stack: String::new(),
13745 termination_protection: false,
13746 iam_role: String::new(),
13747 tags: Vec::new(),
13748 stack_policy: String::new(),
13749 rollback_monitoring_time: String::new(),
13750 rollback_alarms: Vec::new(),
13751 notification_arns: Vec::new(),
13752 })
13753 .collect();
13754
13755 app.cfn_state.table.reset();
13756 app.cfn_state.table.scroll_offset = 0;
13757
13758 app.handle_action(Action::PageDown);
13760 assert_eq!(app.cfn_state.table.selected, 10);
13761
13762 app.handle_action(Action::PageDown);
13764 assert_eq!(app.cfn_state.table.selected, 20);
13765
13766 app.handle_action(Action::PageUp);
13768 assert_eq!(app.cfn_state.table.selected, 10);
13769 }
13770
13771 #[test]
13772 fn test_cfn_filter_input_only_when_focused() {
13773 use crate::ui::cfn::STATUS_FILTER;
13774 let mut app = test_app();
13775 app.current_service = Service::CloudFormationStacks;
13776 app.mode = Mode::FilterInput;
13777 app.cfn_state.input_focus = STATUS_FILTER;
13778 app.cfn_state.table.filter = String::new();
13779
13780 app.handle_action(Action::FilterInput('t'));
13782 app.handle_action(Action::FilterInput('e'));
13783 app.handle_action(Action::FilterInput('s'));
13784 app.handle_action(Action::FilterInput('t'));
13785 assert_eq!(app.cfn_state.table.filter, "");
13786
13787 app.cfn_state.input_focus = InputFocus::Filter;
13789 app.handle_action(Action::FilterInput('t'));
13790 app.handle_action(Action::FilterInput('e'));
13791 app.handle_action(Action::FilterInput('s'));
13792 app.handle_action(Action::FilterInput('t'));
13793 assert_eq!(app.cfn_state.table.filter, "test");
13794 }
13795
13796 #[test]
13797 fn test_cfn_input_focus_resets_on_start() {
13798 let mut app = test_app();
13799 app.current_service = Service::CloudFormationStacks;
13800 app.service_selected = true;
13801 app.mode = Mode::Normal;
13802 app.cfn_state.input_focus = InputFocus::Pagination;
13803
13804 app.handle_action(Action::StartFilter);
13806 assert_eq!(app.mode, Mode::FilterInput);
13807 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
13808 }
13809
13810 #[test]
13811 fn test_iam_roles_input_focus_cycles_forward() {
13812 let mut app = test_app();
13813 app.current_service = Service::IamRoles;
13814 app.mode = Mode::FilterInput;
13815 app.iam_state.role_input_focus = InputFocus::Filter;
13816
13817 app.handle_action(Action::NextFilterFocus);
13818 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
13819
13820 app.handle_action(Action::NextFilterFocus);
13821 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
13822 }
13823
13824 #[test]
13825 fn test_iam_roles_input_focus_cycles_backward() {
13826 let mut app = test_app();
13827 app.current_service = Service::IamRoles;
13828 app.mode = Mode::FilterInput;
13829 app.iam_state.role_input_focus = InputFocus::Filter;
13830
13831 app.handle_action(Action::PrevFilterFocus);
13832 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
13833
13834 app.handle_action(Action::PrevFilterFocus);
13835 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
13836 }
13837
13838 #[test]
13839 fn test_iam_roles_filter_input_only_when_focused() {
13840 let mut app = test_app();
13841 app.current_service = Service::IamRoles;
13842 app.mode = Mode::FilterInput;
13843 app.iam_state.role_input_focus = InputFocus::Pagination;
13844 app.iam_state.roles.filter = String::new();
13845
13846 app.handle_action(Action::FilterInput('t'));
13848 app.handle_action(Action::FilterInput('e'));
13849 app.handle_action(Action::FilterInput('s'));
13850 app.handle_action(Action::FilterInput('t'));
13851 assert_eq!(app.iam_state.roles.filter, "");
13852
13853 app.iam_state.role_input_focus = InputFocus::Filter;
13855 app.handle_action(Action::FilterInput('t'));
13856 app.handle_action(Action::FilterInput('e'));
13857 app.handle_action(Action::FilterInput('s'));
13858 app.handle_action(Action::FilterInput('t'));
13859 assert_eq!(app.iam_state.roles.filter, "test");
13860 }
13861
13862 #[test]
13863 fn test_iam_roles_page_down_updates_scroll_offset() {
13864 let mut app = test_app();
13865 app.current_service = Service::IamRoles;
13866 app.mode = Mode::Normal;
13867 app.iam_state.roles.items = (0..50)
13868 .map(|i| crate::iam::IamRole {
13869 role_name: format!("role-{}", i),
13870 path: "/".to_string(),
13871 trusted_entities: "AWS Service".to_string(),
13872 last_activity: "N/A".to_string(),
13873 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
13874 creation_time: "2024-01-01".to_string(),
13875 description: String::new(),
13876 max_session_duration: Some(3600),
13877 })
13878 .collect();
13879
13880 app.iam_state.roles.selected = 0;
13881 app.iam_state.roles.scroll_offset = 0;
13882
13883 app.handle_action(Action::PageDown);
13885 assert_eq!(app.iam_state.roles.selected, 10);
13886 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
13888
13889 app.handle_action(Action::PageDown);
13891 assert_eq!(app.iam_state.roles.selected, 20);
13892 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
13893 }
13894
13895 #[test]
13896 fn test_application_selection_and_deployments_tab() {
13897 use crate::lambda::Application as LambdaApplication;
13898 use LambdaApplicationDetailTab;
13899
13900 let mut app = test_app();
13901 app.current_service = Service::LambdaApplications;
13902 app.service_selected = true;
13903 app.mode = Mode::Normal;
13904
13905 app.lambda_application_state.table.items = vec![LambdaApplication {
13906 name: "test-app".to_string(),
13907 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13908 description: "Test application".to_string(),
13909 status: "CREATE_COMPLETE".to_string(),
13910 last_modified: "2024-01-01".to_string(),
13911 }];
13912
13913 app.handle_action(Action::Select);
13915 assert_eq!(
13916 app.lambda_application_state.current_application,
13917 Some("test-app".to_string())
13918 );
13919 assert_eq!(
13920 app.lambda_application_state.detail_tab,
13921 LambdaApplicationDetailTab::Overview
13922 );
13923
13924 app.handle_action(Action::NextDetailTab);
13926 assert_eq!(
13927 app.lambda_application_state.detail_tab,
13928 LambdaApplicationDetailTab::Deployments
13929 );
13930
13931 app.handle_action(Action::GoBack);
13933 assert_eq!(app.lambda_application_state.current_application, None);
13934 }
13935
13936 #[test]
13937 fn test_application_resources_filter_and_pagination() {
13938 use crate::lambda::Application as LambdaApplication;
13939 use LambdaApplicationDetailTab;
13940
13941 let mut app = test_app();
13942 app.current_service = Service::LambdaApplications;
13943 app.service_selected = true;
13944 app.mode = Mode::Normal;
13945
13946 app.lambda_application_state.table.items = vec![LambdaApplication {
13947 name: "test-app".to_string(),
13948 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13949 description: "Test application".to_string(),
13950 status: "CREATE_COMPLETE".to_string(),
13951 last_modified: "2024-01-01".to_string(),
13952 }];
13953
13954 app.handle_action(Action::Select);
13956 assert_eq!(
13957 app.lambda_application_state.detail_tab,
13958 LambdaApplicationDetailTab::Overview
13959 );
13960
13961 assert!(!app.lambda_application_state.resources.items.is_empty());
13963
13964 app.mode = Mode::FilterInput;
13966 assert_eq!(
13967 app.lambda_application_state.resource_input_focus,
13968 InputFocus::Filter
13969 );
13970
13971 app.handle_action(Action::NextFilterFocus);
13972 assert_eq!(
13973 app.lambda_application_state.resource_input_focus,
13974 InputFocus::Pagination
13975 );
13976
13977 app.handle_action(Action::PrevFilterFocus);
13978 assert_eq!(
13979 app.lambda_application_state.resource_input_focus,
13980 InputFocus::Filter
13981 );
13982 }
13983
13984 #[test]
13985 fn test_application_deployments_filter_and_pagination() {
13986 use crate::lambda::Application as LambdaApplication;
13987 use LambdaApplicationDetailTab;
13988
13989 let mut app = test_app();
13990 app.current_service = Service::LambdaApplications;
13991 app.service_selected = true;
13992 app.mode = Mode::Normal;
13993
13994 app.lambda_application_state.table.items = vec![LambdaApplication {
13995 name: "test-app".to_string(),
13996 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13997 description: "Test application".to_string(),
13998 status: "CREATE_COMPLETE".to_string(),
13999 last_modified: "2024-01-01".to_string(),
14000 }];
14001
14002 app.handle_action(Action::Select);
14004 app.handle_action(Action::NextDetailTab);
14005 assert_eq!(
14006 app.lambda_application_state.detail_tab,
14007 LambdaApplicationDetailTab::Deployments
14008 );
14009
14010 assert!(!app.lambda_application_state.deployments.items.is_empty());
14012
14013 app.mode = Mode::FilterInput;
14015 assert_eq!(
14016 app.lambda_application_state.deployment_input_focus,
14017 InputFocus::Filter
14018 );
14019
14020 app.handle_action(Action::NextFilterFocus);
14021 assert_eq!(
14022 app.lambda_application_state.deployment_input_focus,
14023 InputFocus::Pagination
14024 );
14025
14026 app.handle_action(Action::PrevFilterFocus);
14027 assert_eq!(
14028 app.lambda_application_state.deployment_input_focus,
14029 InputFocus::Filter
14030 );
14031 }
14032
14033 #[test]
14034 fn test_application_resource_expansion() {
14035 use crate::lambda::Application as LambdaApplication;
14036 use LambdaApplicationDetailTab;
14037
14038 let mut app = test_app();
14039 app.current_service = Service::LambdaApplications;
14040 app.service_selected = true;
14041 app.mode = Mode::Normal;
14042
14043 app.lambda_application_state.table.items = vec![LambdaApplication {
14044 name: "test-app".to_string(),
14045 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
14046 description: "Test application".to_string(),
14047 status: "CREATE_COMPLETE".to_string(),
14048 last_modified: "2024-01-01".to_string(),
14049 }];
14050
14051 app.handle_action(Action::Select);
14053 assert_eq!(
14054 app.lambda_application_state.detail_tab,
14055 LambdaApplicationDetailTab::Overview
14056 );
14057
14058 app.handle_action(Action::NextPane);
14060 assert_eq!(
14061 app.lambda_application_state.resources.expanded_item,
14062 Some(0)
14063 );
14064
14065 app.handle_action(Action::PrevPane);
14067 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
14068 }
14069
14070 #[test]
14071 fn test_application_deployment_expansion() {
14072 use crate::lambda::Application as LambdaApplication;
14073 use LambdaApplicationDetailTab;
14074
14075 let mut app = test_app();
14076 app.current_service = Service::LambdaApplications;
14077 app.service_selected = true;
14078 app.mode = Mode::Normal;
14079
14080 app.lambda_application_state.table.items = vec![LambdaApplication {
14081 name: "test-app".to_string(),
14082 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
14083 description: "Test application".to_string(),
14084 status: "CREATE_COMPLETE".to_string(),
14085 last_modified: "2024-01-01".to_string(),
14086 }];
14087
14088 app.handle_action(Action::Select);
14090 app.handle_action(Action::NextDetailTab);
14091 assert_eq!(
14092 app.lambda_application_state.detail_tab,
14093 LambdaApplicationDetailTab::Deployments
14094 );
14095
14096 app.handle_action(Action::NextPane);
14098 assert_eq!(
14099 app.lambda_application_state.deployments.expanded_item,
14100 Some(0)
14101 );
14102
14103 app.handle_action(Action::PrevPane);
14105 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
14106 }
14107
14108 #[test]
14109 fn test_s3_nested_prefix_expansion() {
14110 use crate::s3::Bucket;
14111 use crate::s3::Object as S3Object;
14112
14113 let mut app = test_app();
14114 app.current_service = Service::S3Buckets;
14115 app.service_selected = true;
14116 app.mode = Mode::Normal;
14117
14118 app.s3_state.buckets.items = vec![Bucket {
14120 name: "test-bucket".to_string(),
14121 region: "us-east-1".to_string(),
14122 creation_date: "2024-01-01".to_string(),
14123 }];
14124
14125 app.s3_state.bucket_preview.insert(
14127 "test-bucket".to_string(),
14128 vec![S3Object {
14129 key: "level1/".to_string(),
14130 size: 0,
14131 last_modified: "".to_string(),
14132 is_prefix: true,
14133 storage_class: "".to_string(),
14134 }],
14135 );
14136
14137 app.s3_state.prefix_preview.insert(
14139 "level1/".to_string(),
14140 vec![S3Object {
14141 key: "level1/level2/".to_string(),
14142 size: 0,
14143 last_modified: "".to_string(),
14144 is_prefix: true,
14145 storage_class: "".to_string(),
14146 }],
14147 );
14148
14149 app.s3_state.selected_row = 0;
14151 app.handle_action(Action::NextPane);
14152 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
14153
14154 app.s3_state.selected_row = 1;
14156 app.handle_action(Action::NextPane);
14157 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
14158
14159 app.s3_state.selected_row = 2;
14161 app.handle_action(Action::NextPane);
14162 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
14163
14164 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
14166 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
14167 }
14168
14169 #[test]
14170 fn test_s3_nested_prefix_collapse() {
14171 use crate::s3::Bucket;
14172 use crate::s3::Object as S3Object;
14173
14174 let mut app = test_app();
14175 app.current_service = Service::S3Buckets;
14176 app.service_selected = true;
14177 app.mode = Mode::Normal;
14178
14179 app.s3_state.buckets.items = vec![Bucket {
14180 name: "test-bucket".to_string(),
14181 region: "us-east-1".to_string(),
14182 creation_date: "2024-01-01".to_string(),
14183 }];
14184
14185 app.s3_state.bucket_preview.insert(
14186 "test-bucket".to_string(),
14187 vec![S3Object {
14188 key: "level1/".to_string(),
14189 size: 0,
14190 last_modified: "".to_string(),
14191 is_prefix: true,
14192 storage_class: "".to_string(),
14193 }],
14194 );
14195
14196 app.s3_state.prefix_preview.insert(
14197 "level1/".to_string(),
14198 vec![S3Object {
14199 key: "level1/level2/".to_string(),
14200 size: 0,
14201 last_modified: "".to_string(),
14202 is_prefix: true,
14203 storage_class: "".to_string(),
14204 }],
14205 );
14206
14207 app.s3_state
14209 .expanded_prefixes
14210 .insert("test-bucket".to_string());
14211 app.s3_state.expanded_prefixes.insert("level1/".to_string());
14212 app.s3_state
14213 .expanded_prefixes
14214 .insert("level1/level2/".to_string());
14215
14216 app.s3_state.selected_row = 2;
14218 app.handle_action(Action::PrevPane);
14219 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
14220 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
14224 app.handle_action(Action::PrevPane);
14225 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
14226 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
14230 app.handle_action(Action::PrevPane);
14231 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
14232 }
14233}
14234
14235#[cfg(test)]
14236mod sqs_tests {
14237 use super::*;
14238 use test_helpers::*;
14239
14240 #[test]
14241 fn test_sqs_filter_input() {
14242 let mut app = test_app();
14243 app.current_service = Service::SqsQueues;
14244 app.service_selected = true;
14245 app.mode = Mode::FilterInput;
14246
14247 app.handle_action(Action::FilterInput('t'));
14248 app.handle_action(Action::FilterInput('e'));
14249 app.handle_action(Action::FilterInput('s'));
14250 app.handle_action(Action::FilterInput('t'));
14251 assert_eq!(app.sqs_state.queues.filter, "test");
14252
14253 app.handle_action(Action::FilterBackspace);
14254 assert_eq!(app.sqs_state.queues.filter, "tes");
14255 }
14256
14257 #[test]
14258 fn test_sqs_start_filter() {
14259 let mut app = test_app();
14260 app.current_service = Service::SqsQueues;
14261 app.service_selected = true;
14262 app.mode = Mode::Normal;
14263
14264 app.handle_action(Action::StartFilter);
14265 assert_eq!(app.mode, Mode::FilterInput);
14266 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
14267 }
14268
14269 #[test]
14270 fn test_sqs_filter_focus_cycling() {
14271 let mut app = test_app();
14272 app.current_service = Service::SqsQueues;
14273 app.service_selected = true;
14274 app.mode = Mode::FilterInput;
14275 app.sqs_state.input_focus = InputFocus::Filter;
14276
14277 app.handle_action(Action::NextFilterFocus);
14278 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
14279
14280 app.handle_action(Action::NextFilterFocus);
14281 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
14282
14283 app.handle_action(Action::PrevFilterFocus);
14284 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
14285 }
14286
14287 #[test]
14288 fn test_sqs_navigation() {
14289 let mut app = test_app();
14290 app.current_service = Service::SqsQueues;
14291 app.service_selected = true;
14292 app.mode = Mode::Normal;
14293 app.sqs_state.queues.items = (0..10)
14294 .map(|i| crate::sqs::Queue {
14295 name: format!("queue{}", i),
14296 url: String::new(),
14297 queue_type: "Standard".to_string(),
14298 created_timestamp: String::new(),
14299 messages_available: "0".to_string(),
14300 messages_in_flight: "0".to_string(),
14301 encryption: "Disabled".to_string(),
14302 content_based_deduplication: "Disabled".to_string(),
14303 last_modified_timestamp: String::new(),
14304 visibility_timeout: String::new(),
14305 message_retention_period: String::new(),
14306 maximum_message_size: String::new(),
14307 delivery_delay: String::new(),
14308 receive_message_wait_time: String::new(),
14309 high_throughput_fifo: "N/A".to_string(),
14310 deduplication_scope: "N/A".to_string(),
14311 fifo_throughput_limit: "N/A".to_string(),
14312 dead_letter_queue: "-".to_string(),
14313 messages_delayed: "0".to_string(),
14314 redrive_allow_policy: "-".to_string(),
14315 redrive_policy: "".to_string(),
14316 redrive_task_id: "-".to_string(),
14317 redrive_task_start_time: "-".to_string(),
14318 redrive_task_status: "-".to_string(),
14319 redrive_task_percent: "-".to_string(),
14320 redrive_task_destination: "-".to_string(),
14321 })
14322 .collect();
14323
14324 app.handle_action(Action::NextItem);
14325 assert_eq!(app.sqs_state.queues.selected, 1);
14326
14327 app.handle_action(Action::PrevItem);
14328 assert_eq!(app.sqs_state.queues.selected, 0);
14329 }
14330
14331 #[test]
14332 fn test_sqs_page_navigation() {
14333 let mut app = test_app();
14334 app.current_service = Service::SqsQueues;
14335 app.service_selected = true;
14336 app.mode = Mode::Normal;
14337 app.sqs_state.queues.items = (0..100)
14338 .map(|i| crate::sqs::Queue {
14339 name: format!("queue{}", i),
14340 url: String::new(),
14341 queue_type: "Standard".to_string(),
14342 created_timestamp: String::new(),
14343 messages_available: "0".to_string(),
14344 messages_in_flight: "0".to_string(),
14345 encryption: "Disabled".to_string(),
14346 content_based_deduplication: "Disabled".to_string(),
14347 last_modified_timestamp: String::new(),
14348 visibility_timeout: String::new(),
14349 message_retention_period: String::new(),
14350 maximum_message_size: String::new(),
14351 delivery_delay: String::new(),
14352 receive_message_wait_time: String::new(),
14353 high_throughput_fifo: "N/A".to_string(),
14354 deduplication_scope: "N/A".to_string(),
14355 fifo_throughput_limit: "N/A".to_string(),
14356 dead_letter_queue: "-".to_string(),
14357 messages_delayed: "0".to_string(),
14358 redrive_allow_policy: "-".to_string(),
14359 redrive_policy: "".to_string(),
14360 redrive_task_id: "-".to_string(),
14361 redrive_task_start_time: "-".to_string(),
14362 redrive_task_status: "-".to_string(),
14363 redrive_task_percent: "-".to_string(),
14364 redrive_task_destination: "-".to_string(),
14365 })
14366 .collect();
14367
14368 app.handle_action(Action::PageDown);
14369 assert_eq!(app.sqs_state.queues.selected, 10);
14370
14371 app.handle_action(Action::PageUp);
14372 assert_eq!(app.sqs_state.queues.selected, 0);
14373 }
14374
14375 #[test]
14376 fn test_sqs_queue_expansion() {
14377 let mut app = test_app();
14378 app.current_service = Service::SqsQueues;
14379 app.service_selected = true;
14380 app.sqs_state.queues.items = vec![crate::sqs::Queue {
14381 name: "my-queue".to_string(),
14382 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
14383 queue_type: "Standard".to_string(),
14384 created_timestamp: "2023-01-01".to_string(),
14385 messages_available: "5".to_string(),
14386 messages_in_flight: "2".to_string(),
14387 encryption: "Enabled".to_string(),
14388 content_based_deduplication: "Disabled".to_string(),
14389 last_modified_timestamp: "2023-01-02".to_string(),
14390 visibility_timeout: "30".to_string(),
14391 message_retention_period: "345600".to_string(),
14392 maximum_message_size: "262144".to_string(),
14393 delivery_delay: "0".to_string(),
14394 receive_message_wait_time: "0".to_string(),
14395 high_throughput_fifo: "N/A".to_string(),
14396 deduplication_scope: "N/A".to_string(),
14397 fifo_throughput_limit: "N/A".to_string(),
14398 dead_letter_queue: "-".to_string(),
14399 messages_delayed: "0".to_string(),
14400 redrive_allow_policy: "-".to_string(),
14401 redrive_policy: "".to_string(),
14402 redrive_task_id: "-".to_string(),
14403 redrive_task_start_time: "-".to_string(),
14404 redrive_task_status: "-".to_string(),
14405 redrive_task_percent: "-".to_string(),
14406 redrive_task_destination: "-".to_string(),
14407 }];
14408 app.sqs_state.queues.selected = 0;
14409
14410 assert_eq!(app.sqs_state.queues.expanded_item, None);
14411
14412 app.handle_action(Action::NextPane);
14414 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
14415
14416 app.handle_action(Action::NextPane);
14418 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
14419
14420 app.handle_action(Action::PrevPane);
14422 assert_eq!(app.sqs_state.queues.expanded_item, None);
14423
14424 app.handle_action(Action::PrevPane);
14426 assert_eq!(app.sqs_state.queues.expanded_item, None);
14427 }
14428
14429 #[test]
14430 fn test_sqs_column_toggle() {
14431 use crate::sqs::queue::Column as SqsColumn;
14432 let mut app = test_app();
14433 app.current_service = Service::SqsQueues;
14434 app.service_selected = true;
14435 app.mode = Mode::ColumnSelector;
14436
14437 app.sqs_visible_column_ids = SqsColumn::ids();
14439 let initial_count = app.sqs_visible_column_ids.len();
14440
14441 app.column_selector_index = 0;
14443 app.handle_action(Action::ToggleColumn);
14444
14445 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
14447 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
14448
14449 app.handle_action(Action::ToggleColumn);
14451 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
14452 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
14453 }
14454
14455 #[test]
14456 fn test_sqs_column_selector_navigation() {
14457 let mut app = test_app();
14458 app.current_service = Service::SqsQueues;
14459 app.service_selected = true;
14460 app.mode = Mode::ColumnSelector;
14461 app.column_selector_index = 0;
14462
14463 let max_index = app.sqs_column_ids.len() - 1;
14465
14466 for _ in 0..max_index {
14468 app.handle_action(Action::NextItem);
14469 }
14470 assert_eq!(app.column_selector_index, max_index);
14471
14472 for _ in 0..max_index {
14474 app.handle_action(Action::PrevItem);
14475 }
14476 assert_eq!(app.column_selector_index, 0);
14477 }
14478
14479 #[test]
14480 fn test_sqs_queue_selection() {
14481 let mut app = test_app();
14482 app.current_service = Service::SqsQueues;
14483 app.service_selected = true;
14484 app.mode = Mode::Normal;
14485 app.sqs_state.queues.items = vec![crate::sqs::Queue {
14486 name: "my-queue".to_string(),
14487 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
14488 queue_type: "Standard".to_string(),
14489 created_timestamp: "2023-01-01".to_string(),
14490 messages_available: "5".to_string(),
14491 messages_in_flight: "2".to_string(),
14492 encryption: "Enabled".to_string(),
14493 content_based_deduplication: "Disabled".to_string(),
14494 last_modified_timestamp: "2023-01-02".to_string(),
14495 visibility_timeout: "30".to_string(),
14496 message_retention_period: "345600".to_string(),
14497 maximum_message_size: "262144".to_string(),
14498 delivery_delay: "0".to_string(),
14499 receive_message_wait_time: "0".to_string(),
14500 high_throughput_fifo: "N/A".to_string(),
14501 deduplication_scope: "N/A".to_string(),
14502 fifo_throughput_limit: "N/A".to_string(),
14503 dead_letter_queue: "-".to_string(),
14504 messages_delayed: "0".to_string(),
14505 redrive_allow_policy: "-".to_string(),
14506 redrive_policy: "".to_string(),
14507 redrive_task_id: "-".to_string(),
14508 redrive_task_start_time: "-".to_string(),
14509 redrive_task_status: "-".to_string(),
14510 redrive_task_percent: "-".to_string(),
14511 redrive_task_destination: "-".to_string(),
14512 }];
14513 app.sqs_state.queues.selected = 0;
14514
14515 assert_eq!(app.sqs_state.current_queue, None);
14516
14517 app.handle_action(Action::Select);
14519 assert_eq!(
14520 app.sqs_state.current_queue,
14521 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
14522 );
14523
14524 app.handle_action(Action::GoBack);
14526 assert_eq!(app.sqs_state.current_queue, None);
14527 }
14528
14529 #[test]
14530 fn test_sqs_lambda_triggers_expand_collapse() {
14531 let mut app = test_app();
14532 app.current_service = Service::SqsQueues;
14533 app.service_selected = true;
14534 app.sqs_state.current_queue =
14535 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
14536 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
14537 app.sqs_state.triggers.items = vec![crate::sqs::LambdaTrigger {
14538 uuid: "test-uuid".to_string(),
14539 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
14540 status: "Enabled".to_string(),
14541 last_modified: "2024-01-01T00:00:00Z".to_string(),
14542 }];
14543 app.sqs_state.triggers.selected = 0;
14544
14545 assert_eq!(app.sqs_state.triggers.expanded_item, None);
14546
14547 app.handle_action(Action::NextPane);
14549 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
14550
14551 app.handle_action(Action::PrevPane);
14553 assert_eq!(app.sqs_state.triggers.expanded_item, None);
14554 }
14555
14556 #[test]
14557 fn test_sqs_lambda_triggers_expand_toggle() {
14558 let mut app = test_app();
14559 app.current_service = Service::SqsQueues;
14560 app.service_selected = true;
14561 app.sqs_state.current_queue =
14562 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
14563 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
14564 app.sqs_state.triggers.items = vec![crate::sqs::LambdaTrigger {
14565 uuid: "test-uuid".to_string(),
14566 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
14567 status: "Enabled".to_string(),
14568 last_modified: "2024-01-01T00:00:00Z".to_string(),
14569 }];
14570 app.sqs_state.triggers.selected = 0;
14571
14572 app.handle_action(Action::NextPane);
14574 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
14575
14576 app.handle_action(Action::NextPane);
14578 assert_eq!(app.sqs_state.triggers.expanded_item, None);
14579
14580 app.handle_action(Action::NextPane);
14582 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
14583 }
14584
14585 #[test]
14586 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
14587 use crate::ui::sqs::filtered_lambda_triggers;
14588
14589 let mut app = test_app();
14590 app.current_service = Service::SqsQueues;
14591 app.service_selected = true;
14592 app.sqs_state.current_queue =
14593 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
14594 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
14595 app.sqs_state.triggers.items = vec![
14596 crate::sqs::LambdaTrigger {
14597 uuid: "uuid-3".to_string(),
14598 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
14599 status: "Enabled".to_string(),
14600 last_modified: "2024-03-01T00:00:00Z".to_string(),
14601 },
14602 crate::sqs::LambdaTrigger {
14603 uuid: "uuid-1".to_string(),
14604 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
14605 status: "Enabled".to_string(),
14606 last_modified: "2024-01-01T00:00:00Z".to_string(),
14607 },
14608 crate::sqs::LambdaTrigger {
14609 uuid: "uuid-2".to_string(),
14610 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
14611 status: "Enabled".to_string(),
14612 last_modified: "2024-02-01T00:00:00Z".to_string(),
14613 },
14614 ];
14615
14616 let sorted = filtered_lambda_triggers(&app);
14617
14618 assert_eq!(sorted.len(), 3);
14620 assert_eq!(sorted[0].uuid, "uuid-1");
14621 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
14622 assert_eq!(sorted[1].uuid, "uuid-2");
14623 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
14624 assert_eq!(sorted[2].uuid, "uuid-3");
14625 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
14626 }
14627
14628 #[test]
14629 fn test_sqs_lambda_triggers_filter_input() {
14630 let mut app = test_app();
14631 app.current_service = Service::SqsQueues;
14632 app.service_selected = true;
14633 app.mode = Mode::FilterInput;
14634 app.sqs_state.current_queue =
14635 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
14636 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
14637 app.sqs_state.input_focus = InputFocus::Filter;
14638
14639 assert_eq!(app.sqs_state.triggers.filter, "");
14640
14641 app.handle_action(Action::FilterInput('t'));
14643 assert_eq!(app.sqs_state.triggers.filter, "t");
14644
14645 app.handle_action(Action::FilterInput('e'));
14646 assert_eq!(app.sqs_state.triggers.filter, "te");
14647
14648 app.handle_action(Action::FilterInput('s'));
14649 assert_eq!(app.sqs_state.triggers.filter, "tes");
14650
14651 app.handle_action(Action::FilterInput('t'));
14652 assert_eq!(app.sqs_state.triggers.filter, "test");
14653
14654 app.handle_action(Action::FilterBackspace);
14656 assert_eq!(app.sqs_state.triggers.filter, "tes");
14657 }
14658
14659 #[test]
14660 fn test_sqs_lambda_triggers_filter_applied() {
14661 use crate::ui::sqs::filtered_lambda_triggers;
14662
14663 let mut app = test_app();
14664 app.current_service = Service::SqsQueues;
14665 app.service_selected = true;
14666 app.sqs_state.current_queue =
14667 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
14668 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
14669 app.sqs_state.triggers.items = vec![
14670 crate::sqs::LambdaTrigger {
14671 uuid: "uuid-1".to_string(),
14672 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
14673 status: "Enabled".to_string(),
14674 last_modified: "2024-01-01T00:00:00Z".to_string(),
14675 },
14676 crate::sqs::LambdaTrigger {
14677 uuid: "uuid-2".to_string(),
14678 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
14679 status: "Enabled".to_string(),
14680 last_modified: "2024-02-01T00:00:00Z".to_string(),
14681 },
14682 crate::sqs::LambdaTrigger {
14683 uuid: "uuid-3".to_string(),
14684 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
14685 status: "Enabled".to_string(),
14686 last_modified: "2024-03-01T00:00:00Z".to_string(),
14687 },
14688 ];
14689
14690 let filtered = filtered_lambda_triggers(&app);
14692 assert_eq!(filtered.len(), 3);
14693
14694 app.sqs_state.triggers.filter = "alpha".to_string();
14696 let filtered = filtered_lambda_triggers(&app);
14697 assert_eq!(filtered.len(), 1);
14698 assert_eq!(
14699 filtered[0].arn,
14700 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
14701 );
14702
14703 app.sqs_state.triggers.filter = "test".to_string();
14705 let filtered = filtered_lambda_triggers(&app);
14706 assert_eq!(filtered.len(), 2);
14707 assert_eq!(
14708 filtered[0].arn,
14709 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
14710 );
14711 assert_eq!(
14712 filtered[1].arn,
14713 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
14714 );
14715
14716 app.sqs_state.triggers.filter = "uuid-3".to_string();
14718 let filtered = filtered_lambda_triggers(&app);
14719 assert_eq!(filtered.len(), 1);
14720 assert_eq!(filtered[0].uuid, "uuid-3");
14721 }
14722
14723 #[test]
14724 fn test_sqs_triggers_navigation() {
14725 let mut app = test_app();
14726 app.service_selected = true;
14727 app.mode = Mode::Normal;
14728 app.current_service = Service::SqsQueues;
14729 app.sqs_state.current_queue = Some("test-queue".to_string());
14730 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
14731 app.sqs_state.triggers.items = vec![
14732 crate::sqs::LambdaTrigger {
14733 uuid: "1".to_string(),
14734 arn: "arn1".to_string(),
14735 status: "Enabled".to_string(),
14736 last_modified: "2024-01-01".to_string(),
14737 },
14738 crate::sqs::LambdaTrigger {
14739 uuid: "2".to_string(),
14740 arn: "arn2".to_string(),
14741 status: "Enabled".to_string(),
14742 last_modified: "2024-01-02".to_string(),
14743 },
14744 ];
14745
14746 assert_eq!(app.sqs_state.triggers.selected, 0);
14747 app.next_item();
14748 assert_eq!(app.sqs_state.triggers.selected, 1);
14749 app.prev_item();
14750 assert_eq!(app.sqs_state.triggers.selected, 0);
14751 }
14752
14753 #[test]
14754 fn test_sqs_pipes_navigation() {
14755 let mut app = test_app();
14756 app.service_selected = true;
14757 app.mode = Mode::Normal;
14758 app.current_service = Service::SqsQueues;
14759 app.sqs_state.current_queue = Some("test-queue".to_string());
14760 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
14761 app.sqs_state.pipes.items = vec![
14762 crate::sqs::EventBridgePipe {
14763 name: "pipe1".to_string(),
14764 status: "RUNNING".to_string(),
14765 target: "target1".to_string(),
14766 last_modified: "2024-01-01".to_string(),
14767 },
14768 crate::sqs::EventBridgePipe {
14769 name: "pipe2".to_string(),
14770 status: "RUNNING".to_string(),
14771 target: "target2".to_string(),
14772 last_modified: "2024-01-02".to_string(),
14773 },
14774 ];
14775
14776 assert_eq!(app.sqs_state.pipes.selected, 0);
14777 app.next_item();
14778 assert_eq!(app.sqs_state.pipes.selected, 1);
14779 app.prev_item();
14780 assert_eq!(app.sqs_state.pipes.selected, 0);
14781 }
14782
14783 #[test]
14784 fn test_sqs_tags_navigation() {
14785 let mut app = test_app();
14786 app.service_selected = true;
14787 app.mode = Mode::Normal;
14788 app.current_service = Service::SqsQueues;
14789 app.sqs_state.current_queue = Some("test-queue".to_string());
14790 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
14791 app.sqs_state.tags.items = vec![
14792 crate::sqs::QueueTag {
14793 key: "Env".to_string(),
14794 value: "prod".to_string(),
14795 },
14796 crate::sqs::QueueTag {
14797 key: "Team".to_string(),
14798 value: "backend".to_string(),
14799 },
14800 ];
14801
14802 assert_eq!(app.sqs_state.tags.selected, 0);
14803 app.next_item();
14804 assert_eq!(app.sqs_state.tags.selected, 1);
14805 app.prev_item();
14806 assert_eq!(app.sqs_state.tags.selected, 0);
14807 }
14808
14809 #[test]
14810 fn test_sqs_queues_navigation() {
14811 let mut app = test_app();
14812 app.service_selected = true;
14813 app.mode = Mode::Normal;
14814 app.current_service = Service::SqsQueues;
14815 app.sqs_state.queues.items = vec![
14816 crate::sqs::Queue {
14817 name: "queue1".to_string(),
14818 url: "url1".to_string(),
14819 queue_type: "Standard".to_string(),
14820 created_timestamp: "".to_string(),
14821 messages_available: "0".to_string(),
14822 messages_in_flight: "0".to_string(),
14823 encryption: "Disabled".to_string(),
14824 content_based_deduplication: "Disabled".to_string(),
14825 last_modified_timestamp: "".to_string(),
14826 visibility_timeout: "".to_string(),
14827 message_retention_period: "".to_string(),
14828 maximum_message_size: "".to_string(),
14829 delivery_delay: "".to_string(),
14830 receive_message_wait_time: "".to_string(),
14831 high_throughput_fifo: "-".to_string(),
14832 deduplication_scope: "-".to_string(),
14833 fifo_throughput_limit: "-".to_string(),
14834 dead_letter_queue: "-".to_string(),
14835 messages_delayed: "0".to_string(),
14836 redrive_allow_policy: "-".to_string(),
14837 redrive_policy: "".to_string(),
14838 redrive_task_id: "-".to_string(),
14839 redrive_task_start_time: "-".to_string(),
14840 redrive_task_status: "-".to_string(),
14841 redrive_task_percent: "-".to_string(),
14842 redrive_task_destination: "-".to_string(),
14843 },
14844 crate::sqs::Queue {
14845 name: "queue2".to_string(),
14846 url: "url2".to_string(),
14847 queue_type: "Standard".to_string(),
14848 created_timestamp: "".to_string(),
14849 messages_available: "0".to_string(),
14850 messages_in_flight: "0".to_string(),
14851 encryption: "Disabled".to_string(),
14852 content_based_deduplication: "Disabled".to_string(),
14853 last_modified_timestamp: "".to_string(),
14854 visibility_timeout: "".to_string(),
14855 message_retention_period: "".to_string(),
14856 maximum_message_size: "".to_string(),
14857 delivery_delay: "".to_string(),
14858 receive_message_wait_time: "".to_string(),
14859 high_throughput_fifo: "-".to_string(),
14860 deduplication_scope: "-".to_string(),
14861 fifo_throughput_limit: "-".to_string(),
14862 dead_letter_queue: "-".to_string(),
14863 messages_delayed: "0".to_string(),
14864 redrive_allow_policy: "-".to_string(),
14865 redrive_policy: "".to_string(),
14866 redrive_task_id: "-".to_string(),
14867 redrive_task_start_time: "-".to_string(),
14868 redrive_task_status: "-".to_string(),
14869 redrive_task_percent: "-".to_string(),
14870 redrive_task_destination: "-".to_string(),
14871 },
14872 ];
14873
14874 assert_eq!(app.sqs_state.queues.selected, 0);
14875 app.next_item();
14876 assert_eq!(app.sqs_state.queues.selected, 1);
14877 app.prev_item();
14878 assert_eq!(app.sqs_state.queues.selected, 0);
14879 }
14880
14881 #[test]
14882 fn test_sqs_subscriptions_navigation() {
14883 let mut app = test_app();
14884 app.service_selected = true;
14885 app.mode = Mode::Normal;
14886 app.current_service = Service::SqsQueues;
14887 app.sqs_state.current_queue = Some("test-queue".to_string());
14888 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14889 app.sqs_state.subscriptions.items = vec![
14890 crate::sqs::SnsSubscription {
14891 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
14892 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
14893 },
14894 crate::sqs::SnsSubscription {
14895 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
14896 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
14897 },
14898 ];
14899
14900 assert_eq!(app.sqs_state.subscriptions.selected, 0);
14901 app.next_item();
14902 assert_eq!(app.sqs_state.subscriptions.selected, 1);
14903 app.prev_item();
14904 assert_eq!(app.sqs_state.subscriptions.selected, 0);
14905 }
14906
14907 #[test]
14908 fn test_sqs_subscription_region_dropdown_navigation() {
14909 let mut app = test_app();
14910 app.service_selected = true;
14911 app.mode = Mode::FilterInput;
14912 app.current_service = Service::SqsQueues;
14913 app.sqs_state.current_queue = Some("test-queue".to_string());
14914 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14915 app.sqs_state.input_focus = crate::common::InputFocus::Dropdown("SubscriptionRegion");
14916
14917 assert_eq!(app.sqs_state.subscription_region_selected, 0);
14918 app.next_item();
14919 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14920 app.next_item();
14921 assert_eq!(app.sqs_state.subscription_region_selected, 2);
14922 app.prev_item();
14923 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14924 app.prev_item();
14925 assert_eq!(app.sqs_state.subscription_region_selected, 0);
14926 }
14927
14928 #[test]
14929 fn test_sqs_subscription_region_selection() {
14930 let mut app = test_app();
14931 app.service_selected = true;
14932 app.mode = Mode::FilterInput;
14933 app.current_service = Service::SqsQueues;
14934 app.sqs_state.current_queue = Some("test-queue".to_string());
14935 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14936 app.sqs_state.input_focus = crate::common::InputFocus::Dropdown("SubscriptionRegion");
14937 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
14940 app.handle_action(Action::ApplyFilter);
14941 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
14942 assert_eq!(app.mode, Mode::Normal);
14943 }
14944
14945 #[test]
14946 fn test_sqs_subscription_region_change_resets_selection() {
14947 let mut app = test_app();
14948 app.service_selected = true;
14949 app.mode = Mode::FilterInput;
14950 app.current_service = Service::SqsQueues;
14951 app.sqs_state.current_queue = Some("test-queue".to_string());
14952 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14953 app.sqs_state.input_focus = crate::common::InputFocus::Dropdown("SubscriptionRegion");
14954 app.sqs_state.subscription_region_selected = 0;
14955 app.sqs_state.subscriptions.selected = 5;
14956
14957 app.handle_action(Action::NextItem);
14958
14959 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14960 assert_eq!(app.sqs_state.subscriptions.selected, 0);
14961 }
14962
14963 #[test]
14964 fn test_s3_object_filter_resets_selection() {
14965 let mut app = test_app();
14966 app.service_selected = true;
14967 app.current_service = Service::S3Buckets;
14968 app.s3_state.current_bucket = Some("test-bucket".to_string());
14969 app.s3_state.selected_row = 5;
14970 app.mode = Mode::FilterInput;
14971
14972 app.handle_action(Action::CloseMenu);
14973
14974 assert_eq!(app.s3_state.selected_row, 0);
14975 assert_eq!(app.mode, Mode::Normal);
14976 }
14977
14978 #[test]
14979 fn test_s3_bucket_filter_resets_selection() {
14980 let mut app = test_app();
14981 app.service_selected = true;
14982 app.current_service = Service::S3Buckets;
14983 app.s3_state.selected_row = 10;
14984 app.mode = Mode::FilterInput;
14985
14986 app.handle_action(Action::CloseMenu);
14987
14988 assert_eq!(app.s3_state.selected_row, 0);
14989 assert_eq!(app.mode, Mode::Normal);
14990 }
14991
14992 #[test]
14993 fn test_s3_selection_stays_in_bounds() {
14994 let mut app = test_app();
14995 app.service_selected = true;
14996 app.current_service = Service::S3Buckets;
14997 app.s3_state.selected_row = 0;
14998 app.s3_state.selected_object = 0;
14999
15000 app.prev_item();
15002
15003 assert_eq!(app.s3_state.selected_row, 0);
15005 assert_eq!(app.s3_state.selected_object, 0);
15006 }
15007
15008 #[test]
15009 fn test_cfn_filter_resets_selection() {
15010 let mut app = test_app();
15011 app.service_selected = true;
15012 app.current_service = Service::CloudFormationStacks;
15013 app.cfn_state.table.selected = 10;
15014 app.mode = Mode::FilterInput;
15015
15016 app.handle_action(Action::CloseMenu);
15017
15018 assert_eq!(app.cfn_state.table.selected, 0);
15019 assert_eq!(app.mode, Mode::Normal);
15020 }
15021
15022 #[test]
15023 fn test_lambda_filter_resets_selection() {
15024 let mut app = test_app();
15025 app.service_selected = true;
15026 app.current_service = Service::LambdaFunctions;
15027 app.lambda_state.table.selected = 8;
15028 app.mode = Mode::FilterInput;
15029
15030 app.handle_action(Action::CloseMenu);
15031
15032 assert_eq!(app.lambda_state.table.selected, 0);
15033 assert_eq!(app.mode, Mode::Normal);
15034 }
15035
15036 #[test]
15037 fn test_sqs_filter_resets_selection() {
15038 let mut app = test_app();
15039 app.service_selected = true;
15040 app.current_service = Service::SqsQueues;
15041 app.sqs_state.queues.selected = 7;
15042 app.mode = Mode::FilterInput;
15043
15044 app.handle_action(Action::CloseMenu);
15045
15046 assert_eq!(app.sqs_state.queues.selected, 0);
15047 assert_eq!(app.mode, Mode::Normal);
15048 }
15049
15050 #[test]
15051 fn test_cfn_status_filter_change_resets_selection() {
15052 use crate::ui::cfn::{StatusFilter, STATUS_FILTER};
15053 let mut app = test_app();
15054 app.service_selected = true;
15055 app.current_service = Service::CloudFormationStacks;
15056 app.mode = Mode::FilterInput;
15057 app.cfn_state.input_focus = STATUS_FILTER;
15058 app.cfn_state.status_filter = StatusFilter::All;
15059 app.cfn_state.table.items = vec![
15060 CfnStack {
15061 name: "stack1".to_string(),
15062 stack_id: "id1".to_string(),
15063 status: "CREATE_COMPLETE".to_string(),
15064 created_time: "2024-01-01".to_string(),
15065 updated_time: String::new(),
15066 deleted_time: String::new(),
15067 drift_status: String::new(),
15068 last_drift_check_time: String::new(),
15069 status_reason: String::new(),
15070 description: String::new(),
15071 detailed_status: String::new(),
15072 root_stack: String::new(),
15073 parent_stack: String::new(),
15074 termination_protection: false,
15075 iam_role: String::new(),
15076 tags: Vec::new(),
15077 stack_policy: String::new(),
15078 rollback_monitoring_time: String::new(),
15079 rollback_alarms: Vec::new(),
15080 notification_arns: Vec::new(),
15081 },
15082 CfnStack {
15083 name: "stack2".to_string(),
15084 stack_id: "id2".to_string(),
15085 status: "UPDATE_IN_PROGRESS".to_string(),
15086 created_time: "2024-01-02".to_string(),
15087 updated_time: String::new(),
15088 deleted_time: String::new(),
15089 drift_status: String::new(),
15090 last_drift_check_time: String::new(),
15091 status_reason: String::new(),
15092 description: String::new(),
15093 detailed_status: String::new(),
15094 root_stack: String::new(),
15095 parent_stack: String::new(),
15096 termination_protection: false,
15097 iam_role: String::new(),
15098 tags: Vec::new(),
15099 stack_policy: String::new(),
15100 rollback_monitoring_time: String::new(),
15101 rollback_alarms: Vec::new(),
15102 notification_arns: Vec::new(),
15103 },
15104 ];
15105 app.cfn_state.table.selected = 1;
15106
15107 app.handle_action(Action::NextItem);
15108
15109 assert_eq!(app.cfn_state.status_filter, StatusFilter::Active);
15110 assert_eq!(app.cfn_state.table.selected, 0);
15111 }
15112
15113 #[test]
15114 fn test_cfn_view_nested_toggle_resets_selection() {
15115 use crate::ui::cfn::VIEW_NESTED;
15116 let mut app = test_app();
15117 app.service_selected = true;
15118 app.current_service = Service::CloudFormationStacks;
15119 app.mode = Mode::FilterInput;
15120 app.cfn_state.input_focus = VIEW_NESTED;
15121 app.cfn_state.view_nested = false;
15122 app.cfn_state.table.items = vec![CfnStack {
15123 name: "stack1".to_string(),
15124 stack_id: "id1".to_string(),
15125 status: "CREATE_COMPLETE".to_string(),
15126 created_time: "2024-01-01".to_string(),
15127 updated_time: String::new(),
15128 deleted_time: String::new(),
15129 drift_status: String::new(),
15130 last_drift_check_time: String::new(),
15131 status_reason: String::new(),
15132 description: String::new(),
15133 detailed_status: String::new(),
15134 root_stack: String::new(),
15135 parent_stack: String::new(),
15136 termination_protection: false,
15137 iam_role: String::new(),
15138 tags: Vec::new(),
15139 stack_policy: String::new(),
15140 rollback_monitoring_time: String::new(),
15141 rollback_alarms: Vec::new(),
15142 notification_arns: Vec::new(),
15143 }];
15144 app.cfn_state.table.selected = 5;
15145
15146 app.handle_action(Action::ToggleFilterCheckbox);
15147
15148 assert!(app.cfn_state.view_nested);
15149 assert_eq!(app.cfn_state.table.selected, 0);
15150 }
15151
15152 #[test]
15153 fn test_cfn_template_scroll_up() {
15154 let mut app = test_app();
15155 app.service_selected = true;
15156 app.current_service = Service::CloudFormationStacks;
15157 app.cfn_state.current_stack = Some("test-stack".to_string());
15158 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Template;
15159 app.cfn_state.template_scroll = 20;
15160
15161 app.page_up();
15162
15163 assert_eq!(app.cfn_state.template_scroll, 10);
15164 }
15165
15166 #[test]
15167 fn test_cfn_template_scroll_down() {
15168 let mut app = test_app();
15169 app.service_selected = true;
15170 app.current_service = Service::CloudFormationStacks;
15171 app.cfn_state.current_stack = Some("test-stack".to_string());
15172 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Template;
15173 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
15174 app.cfn_state.template_scroll = 0;
15175
15176 app.page_down();
15177
15178 assert_eq!(app.cfn_state.template_scroll, 10);
15179 }
15180
15181 #[test]
15182 fn test_cfn_template_scroll_down_respects_max() {
15183 let mut app = test_app();
15184 app.service_selected = true;
15185 app.current_service = Service::CloudFormationStacks;
15186 app.cfn_state.current_stack = Some("test-stack".to_string());
15187 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Template;
15188 app.cfn_state.template_body = "line1\nline2\nline3".to_string();
15189 app.cfn_state.template_scroll = 0;
15190
15191 app.page_down();
15192
15193 assert_eq!(app.cfn_state.template_scroll, 2);
15195 }
15196
15197 #[test]
15198 fn test_cfn_template_arrow_up() {
15199 let mut app = test_app();
15200 app.service_selected = true;
15201 app.current_service = Service::CloudFormationStacks;
15202 app.mode = Mode::Normal;
15203 app.cfn_state.current_stack = Some("test-stack".to_string());
15204 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Template;
15205 app.cfn_state.template_scroll = 5;
15206
15207 app.prev_item();
15208
15209 assert_eq!(app.cfn_state.template_scroll, 4);
15210 }
15211
15212 #[test]
15213 fn test_cfn_template_arrow_down() {
15214 let mut app = test_app();
15215 app.service_selected = true;
15216 app.current_service = Service::CloudFormationStacks;
15217 app.mode = Mode::Normal;
15218 app.cfn_state.current_stack = Some("test-stack".to_string());
15219 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Template;
15220 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
15221 app.cfn_state.template_scroll = 2;
15222
15223 app.next_item();
15224
15225 assert_eq!(app.cfn_state.template_scroll, 3);
15226 }
15227
15228 #[test]
15229 fn test_cfn_template_arrow_down_respects_max() {
15230 let mut app = test_app();
15231 app.service_selected = true;
15232 app.current_service = Service::CloudFormationStacks;
15233 app.mode = Mode::Normal;
15234 app.cfn_state.current_stack = Some("test-stack".to_string());
15235 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Template;
15236 app.cfn_state.template_body = "line1\nline2".to_string();
15237 app.cfn_state.template_scroll = 1;
15238
15239 app.next_item();
15240
15241 assert_eq!(app.cfn_state.template_scroll, 1);
15243 }
15244}
15245
15246#[cfg(test)]
15247mod lambda_version_tab_tests {
15248 use super::*;
15249 use test_helpers::*;
15250
15251 #[test]
15252 fn test_lambda_version_tab_cycling_next() {
15253 let mut app = test_app();
15254 app.current_service = Service::LambdaFunctions;
15255 app.lambda_state.current_function = Some("test-function".to_string());
15256 app.lambda_state.current_version = Some("1".to_string());
15257 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15258
15259 app.handle_action(Action::NextDetailTab);
15261 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
15262 assert!(app.lambda_state.metrics_loading);
15263
15264 app.lambda_state.metrics_loading = false;
15266 app.handle_action(Action::NextDetailTab);
15267 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
15268
15269 app.handle_action(Action::NextDetailTab);
15271 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
15272 }
15273
15274 #[test]
15275 fn test_lambda_version_tab_cycling_prev() {
15276 let mut app = test_app();
15277 app.current_service = Service::LambdaFunctions;
15278 app.lambda_state.current_function = Some("test-function".to_string());
15279 app.lambda_state.current_version = Some("1".to_string());
15280 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15281
15282 app.handle_action(Action::PrevDetailTab);
15284 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
15285
15286 app.handle_action(Action::PrevDetailTab);
15288 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
15289 assert!(app.lambda_state.metrics_loading);
15290
15291 app.lambda_state.metrics_loading = false;
15293 app.handle_action(Action::PrevDetailTab);
15294 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
15295 }
15296
15297 #[test]
15298 fn test_lambda_version_monitor_clears_metrics() {
15299 let mut app = test_app();
15300 app.current_service = Service::LambdaFunctions;
15301 app.lambda_state.current_function = Some("test-function".to_string());
15302 app.lambda_state.current_version = Some("1".to_string());
15303 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15304
15305 app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
15307 app.lambda_state.monitoring_scroll = 5;
15308
15309 app.handle_action(Action::NextDetailTab);
15311
15312 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
15313 assert!(app.lambda_state.metrics_loading);
15314 assert_eq!(app.lambda_state.monitoring_scroll, 0);
15315 assert!(app.lambda_state.metric_data_invocations.is_empty());
15316 }
15317
15318 #[test]
15319 fn test_cfn_parameters_expand_collapse() {
15320 let mut app = test_app();
15321 app.current_service = Service::CloudFormationStacks;
15322 app.service_selected = true;
15323 app.cfn_state.current_stack = Some("test-stack".to_string());
15324 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Parameters;
15325 app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
15326 key: "Param1".to_string(),
15327 value: "Value1".to_string(),
15328 resolved_value: "Resolved1".to_string(),
15329 }];
15330 app.cfn_state.parameters.reset();
15331
15332 assert_eq!(app.cfn_state.parameters.expanded_item, None);
15333
15334 app.handle_action(Action::NextPane);
15336 assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
15337
15338 app.handle_action(Action::PrevPane);
15340 assert_eq!(app.cfn_state.parameters.expanded_item, None);
15341 }
15342
15343 #[test]
15344 fn test_cfn_parameters_filter_resets_selection() {
15345 let mut app = test_app();
15346 app.current_service = Service::CloudFormationStacks;
15347 app.service_selected = true;
15348 app.cfn_state.current_stack = Some("test-stack".to_string());
15349 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Parameters;
15350 app.cfn_state.parameters.items = vec![
15351 rusticity_core::cfn::StackParameter {
15352 key: "DatabaseName".to_string(),
15353 value: "mydb".to_string(),
15354 resolved_value: "mydb".to_string(),
15355 },
15356 rusticity_core::cfn::StackParameter {
15357 key: "InstanceType".to_string(),
15358 value: "t2.micro".to_string(),
15359 resolved_value: "t2.micro".to_string(),
15360 },
15361 rusticity_core::cfn::StackParameter {
15362 key: "Environment".to_string(),
15363 value: "production".to_string(),
15364 resolved_value: "production".to_string(),
15365 },
15366 ];
15367 app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
15369 app.cfn_state.parameters_input_focus = InputFocus::Filter;
15370
15371 app.handle_action(Action::FilterInput('D'));
15373 assert_eq!(app.cfn_state.parameters.selected, 0);
15374 assert_eq!(app.cfn_state.parameters.filter, "D");
15375
15376 app.cfn_state.parameters.selected = 1;
15378
15379 app.handle_action(Action::FilterInput('a'));
15381 assert_eq!(app.cfn_state.parameters.selected, 0);
15382 assert_eq!(app.cfn_state.parameters.filter, "Da");
15383
15384 app.cfn_state.parameters.selected = 1;
15386
15387 app.handle_action(Action::FilterBackspace);
15389 assert_eq!(app.cfn_state.parameters.selected, 0);
15390 assert_eq!(app.cfn_state.parameters.filter, "D");
15391 }
15392
15393 #[test]
15394 fn test_cfn_template_tab_no_preferences() {
15395 let mut app = test_app();
15396 app.current_service = Service::CloudFormationStacks;
15397 app.service_selected = true;
15398 app.cfn_state.current_stack = Some("test-stack".to_string());
15399 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Template;
15400 app.mode = Mode::Normal;
15401
15402 app.handle_action(Action::OpenColumnSelector);
15404 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::GitSync;
15408 app.handle_action(Action::OpenColumnSelector);
15409 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Parameters;
15413 app.handle_action(Action::OpenColumnSelector);
15414 assert_eq!(app.mode, Mode::ColumnSelector); app.mode = Mode::Normal;
15418 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Outputs;
15419 app.handle_action(Action::OpenColumnSelector);
15420 assert_eq!(app.mode, Mode::ColumnSelector); }
15422
15423 #[test]
15424 fn test_cfn_outputs_expand_collapse() {
15425 let mut app = test_app();
15426 app.current_service = Service::CloudFormationStacks;
15427 app.service_selected = true;
15428 app.cfn_state.current_stack = Some("test-stack".to_string());
15429 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Outputs;
15430 app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
15431 key: "Output1".to_string(),
15432 value: "Value1".to_string(),
15433 description: "Description1".to_string(),
15434 export_name: "Export1".to_string(),
15435 }];
15436 app.cfn_state.outputs.reset();
15437
15438 assert_eq!(app.cfn_state.outputs.expanded_item, None);
15439
15440 app.handle_action(Action::NextPane);
15442 assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
15443
15444 app.handle_action(Action::PrevPane);
15446 assert_eq!(app.cfn_state.outputs.expanded_item, None);
15447 }
15448
15449 #[test]
15450 fn test_cfn_outputs_filter_resets_selection() {
15451 let mut app = test_app();
15452 app.current_service = Service::CloudFormationStacks;
15453 app.service_selected = true;
15454 app.cfn_state.current_stack = Some("test-stack".to_string());
15455 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Outputs;
15456 app.cfn_state.outputs.items = vec![
15457 rusticity_core::cfn::StackOutput {
15458 key: "ApiUrl".to_string(),
15459 value: "https://api.example.com".to_string(),
15460 description: "API endpoint".to_string(),
15461 export_name: "MyApiUrl".to_string(),
15462 },
15463 rusticity_core::cfn::StackOutput {
15464 key: "BucketName".to_string(),
15465 value: "my-bucket".to_string(),
15466 description: "S3 bucket".to_string(),
15467 export_name: "MyBucket".to_string(),
15468 },
15469 ];
15470 app.cfn_state.outputs.reset();
15471 app.cfn_state.outputs.selected = 1;
15472
15473 app.handle_action(Action::StartFilter);
15475 assert_eq!(app.mode, Mode::FilterInput);
15476
15477 app.handle_action(Action::FilterInput('A'));
15479 assert_eq!(app.cfn_state.outputs.selected, 0);
15480 assert_eq!(app.cfn_state.outputs.filter, "A");
15481
15482 app.cfn_state.outputs.selected = 1;
15484 app.handle_action(Action::FilterInput('p'));
15485 assert_eq!(app.cfn_state.outputs.selected, 0);
15486
15487 app.cfn_state.outputs.selected = 1;
15489 app.handle_action(Action::FilterBackspace);
15490 assert_eq!(app.cfn_state.outputs.selected, 0);
15491 }
15492
15493 #[test]
15494 fn test_ec2_service_in_picker() {
15495 let app = test_app();
15496 assert!(app.service_picker.services.contains(&"EC2 > Instances"));
15497 }
15498
15499 #[test]
15500 fn test_ec2_state_filter_cycles() {
15501 let mut app = test_app();
15502 app.current_service = Service::Ec2Instances;
15503 app.service_selected = true;
15504 app.mode = Mode::FilterInput;
15505 app.ec2_state.input_focus = EC2_STATE_FILTER;
15506
15507 let initial = app.ec2_state.state_filter;
15508 assert_eq!(initial, Ec2StateFilter::AllStates);
15509
15510 app.handle_action(Action::ToggleFilterCheckbox);
15512 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15513
15514 app.handle_action(Action::ToggleFilterCheckbox);
15515 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15516
15517 app.handle_action(Action::ToggleFilterCheckbox);
15518 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
15519
15520 app.handle_action(Action::ToggleFilterCheckbox);
15521 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
15522
15523 app.handle_action(Action::ToggleFilterCheckbox);
15524 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
15525
15526 app.handle_action(Action::ToggleFilterCheckbox);
15527 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15528
15529 app.handle_action(Action::ToggleFilterCheckbox);
15530 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15531 }
15532
15533 #[test]
15534 fn test_ec2_filter_resets_table() {
15535 let mut app = test_app();
15536 app.current_service = Service::Ec2Instances;
15537 app.service_selected = true;
15538 app.mode = Mode::FilterInput;
15539 app.ec2_state.input_focus = EC2_STATE_FILTER;
15540 app.ec2_state.table.selected = 5;
15541
15542 app.handle_action(Action::ToggleFilterCheckbox);
15543 assert_eq!(app.ec2_state.table.selected, 0);
15544 }
15545
15546 #[test]
15547 fn test_ec2_columns_visible() {
15548 let app = test_app();
15549 assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
15552
15553 #[test]
15554 fn test_ec2_breadcrumbs() {
15555 let mut app = test_app();
15556 app.current_service = Service::Ec2Instances;
15557 app.service_selected = true;
15558 let breadcrumb = app.breadcrumbs();
15559 assert_eq!(breadcrumb, "EC2 > Instances");
15560 }
15561
15562 #[test]
15563 fn test_ec2_console_url() {
15564 let mut app = test_app();
15565 app.current_service = Service::Ec2Instances;
15566 app.service_selected = true;
15567 let url = app.get_console_url();
15568 assert!(url.contains("ec2"));
15569 assert!(url.contains("Instances"));
15570 }
15571
15572 #[test]
15573 fn test_ec2_filter_handling() {
15574 let mut app = test_app();
15575 app.current_service = Service::Ec2Instances;
15576 app.service_selected = true;
15577 app.mode = Mode::FilterInput;
15578
15579 app.handle_action(Action::FilterInput('t'));
15580 app.handle_action(Action::FilterInput('e'));
15581 app.handle_action(Action::FilterInput('s'));
15582 app.handle_action(Action::FilterInput('t'));
15583
15584 assert_eq!(app.ec2_state.table.filter, "test");
15585
15586 app.handle_action(Action::FilterBackspace);
15587 assert_eq!(app.ec2_state.table.filter, "tes");
15588 }
15589
15590 #[test]
15591 fn test_column_selector_page_down_ec2() {
15592 let mut app = test_app();
15593 app.current_service = Service::Ec2Instances;
15594 app.service_selected = true;
15595 app.mode = Mode::ColumnSelector;
15596 app.column_selector_index = 0;
15597
15598 app.handle_action(Action::PageDown);
15599 assert_eq!(app.column_selector_index, 10);
15600
15601 app.handle_action(Action::PageDown);
15602 assert_eq!(app.column_selector_index, 20);
15603 }
15604
15605 #[test]
15606 fn test_column_selector_page_up_ec2() {
15607 let mut app = test_app();
15608 app.current_service = Service::Ec2Instances;
15609 app.service_selected = true;
15610 app.mode = Mode::ColumnSelector;
15611 app.column_selector_index = 30;
15612
15613 app.handle_action(Action::PageUp);
15614 assert_eq!(app.column_selector_index, 20);
15615
15616 app.handle_action(Action::PageUp);
15617 assert_eq!(app.column_selector_index, 10);
15618 }
15619
15620 #[test]
15621 fn test_ec2_state_filter_dropdown_focus() {
15622 let mut app = test_app();
15623 app.current_service = Service::Ec2Instances;
15624 app.service_selected = true;
15625 app.mode = Mode::FilterInput;
15626
15627 app.handle_action(Action::NextFilterFocus);
15629 assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
15630
15631 app.handle_action(Action::ToggleFilterCheckbox);
15634 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15635 }
15636
15637 #[test]
15638 fn test_column_selector_ctrl_d_scrolling() {
15639 let mut app = test_app();
15640 app.current_service = Service::LambdaFunctions;
15641 app.mode = Mode::ColumnSelector;
15642 app.column_selector_index = 0;
15643
15644 app.handle_action(Action::PageDown);
15645 assert_eq!(app.column_selector_index, 10);
15646
15647 let max = app.get_column_selector_max();
15649 app.handle_action(Action::PageDown);
15650 assert_eq!(app.column_selector_index, max);
15651 }
15652
15653 #[test]
15654 fn test_column_selector_ctrl_u_scrolling() {
15655 let mut app = test_app();
15656 app.current_service = Service::CloudFormationStacks;
15657 app.mode = Mode::ColumnSelector;
15658 app.column_selector_index = 25;
15659
15660 app.handle_action(Action::PageUp);
15661 assert_eq!(app.column_selector_index, 15);
15662
15663 app.handle_action(Action::PageUp);
15664 assert_eq!(app.column_selector_index, 5);
15665 }
15666
15667 #[test]
15668 fn test_prev_preferences_lambda() {
15669 let mut app = test_app();
15670 app.current_service = Service::LambdaFunctions;
15671 app.mode = Mode::ColumnSelector;
15672 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
15673 app.column_selector_index = page_size_idx;
15674
15675 app.handle_action(Action::PrevPreferences);
15676 assert_eq!(app.column_selector_index, 0);
15677
15678 app.handle_action(Action::PrevPreferences);
15679 assert_eq!(app.column_selector_index, page_size_idx);
15680 }
15681
15682 #[test]
15683 fn test_prev_preferences_cloudformation() {
15684 let mut app = test_app();
15685 app.current_service = Service::CloudFormationStacks;
15686 app.mode = Mode::ColumnSelector;
15687 let page_size_idx = app.cfn_column_ids.len() + 2;
15688 app.column_selector_index = page_size_idx;
15689
15690 app.handle_action(Action::PrevPreferences);
15691 assert_eq!(app.column_selector_index, 0);
15692
15693 app.handle_action(Action::PrevPreferences);
15694 assert_eq!(app.column_selector_index, page_size_idx);
15695 }
15696
15697 #[test]
15698 fn test_prev_preferences_alarms() {
15699 let mut app = test_app();
15700 app.current_service = Service::CloudWatchAlarms;
15701 app.mode = Mode::ColumnSelector;
15702 app.column_selector_index = 28; app.handle_action(Action::PrevPreferences);
15705 assert_eq!(app.column_selector_index, 22); app.handle_action(Action::PrevPreferences);
15708 assert_eq!(app.column_selector_index, 18); app.handle_action(Action::PrevPreferences);
15711 assert_eq!(app.column_selector_index, 0); app.handle_action(Action::PrevPreferences);
15714 assert_eq!(app.column_selector_index, 28); }
15716
15717 #[test]
15718 fn test_ec2_page_size_in_preferences() {
15719 let mut app = test_app();
15720 app.current_service = Service::Ec2Instances;
15721 app.mode = Mode::ColumnSelector;
15722 app.ec2_state.table.page_size = PageSize::Fifty;
15723
15724 let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
15727 app.handle_action(Action::ToggleColumn);
15728
15729 assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
15730 }
15731
15732 #[test]
15733 fn test_ec2_next_preferences_with_page_size() {
15734 let mut app = test_app();
15735 app.current_service = Service::Ec2Instances;
15736 app.mode = Mode::ColumnSelector;
15737 app.column_selector_index = 0;
15738
15739 let page_size_idx = app.ec2_column_ids.len() + 2;
15740 app.handle_action(Action::NextPreferences);
15741 assert_eq!(app.column_selector_index, page_size_idx);
15742
15743 app.handle_action(Action::NextPreferences);
15744 assert_eq!(app.column_selector_index, 0);
15745 }
15746
15747 #[test]
15748 fn test_ec2_dropdown_next_item() {
15749 let mut app = test_app();
15750 app.current_service = Service::Ec2Instances;
15751 app.mode = Mode::FilterInput;
15752 app.ec2_state.input_focus = EC2_STATE_FILTER;
15753 app.ec2_state.state_filter = Ec2StateFilter::AllStates;
15754
15755 app.handle_action(Action::NextItem);
15756 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15757
15758 app.handle_action(Action::NextItem);
15759 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15760 }
15761
15762 #[test]
15763 fn test_ec2_dropdown_prev_item() {
15764 let mut app = test_app();
15765 app.current_service = Service::Ec2Instances;
15766 app.mode = Mode::FilterInput;
15767 app.ec2_state.input_focus = EC2_STATE_FILTER;
15768 app.ec2_state.state_filter = Ec2StateFilter::Stopped;
15769
15770 app.handle_action(Action::PrevItem);
15771 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15772
15773 app.handle_action(Action::PrevItem);
15774 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15775 }
15776
15777 #[test]
15778 fn test_ec2_dropdown_cycles_with_arrows() {
15779 let mut app = test_app();
15780 app.current_service = Service::Ec2Instances;
15781 app.mode = Mode::FilterInput;
15782 app.ec2_state.input_focus = EC2_STATE_FILTER;
15783 app.ec2_state.state_filter = Ec2StateFilter::Stopping;
15784
15785 app.handle_action(Action::NextItem);
15787 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15788
15789 app.handle_action(Action::PrevItem);
15791 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15792 }
15793}