1pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
2use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
3use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
4pub use crate::cw::insights::InsightsFocus;
5use crate::cw::insights::InsightsState;
6pub use crate::cw::{Alarm, AlarmColumn};
7pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
8use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
9use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
10use crate::iam::{
11 self, GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag,
12 UserColumn, UserTag as IamUserTag,
13};
14#[cfg(test)]
15use crate::iam::{IamRole, IamUser, LastAccessedService};
16use crate::keymap::{Action, Mode};
17pub use crate::lambda::{
18 Alias as LambdaAlias, Application as LambdaApplication,
19 ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
20 Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
21 ResourceColumn, Version as LambdaVersion,
22};
23pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
24use crate::session::{Session, SessionTab};
25pub use crate::sqs::queue::Column as SqsColumn;
26pub use crate::sqs::trigger::Column as SqsTriggerColumn;
27use crate::sqs::{console_url_queue_detail, console_url_queues};
28#[cfg(test)]
29use crate::sqs::{
30 EventBridgePipe, LambdaTrigger, Queue as SqsQueue, QueueTag as SqsQueueTag, SnsSubscription,
31};
32use crate::table::TableState;
33use crate::ui::cfn::State as CfnStateConstants;
34pub use crate::ui::cfn::{
35 filtered_cloudformation_stacks, filtered_outputs, filtered_parameters, filtered_resources,
36 output_column_ids, parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
37 State as CfnState, StatusFilter as CfnStatusFilter,
38};
39pub use crate::ui::cw::alarms::{
40 AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
41};
42pub use crate::ui::cw::logs::{
43 filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
44 DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
45};
46use crate::ui::ec2;
47use crate::ui::ec2::filtered_ec2_instances;
48pub use crate::ui::ec2::{
49 DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
50 STATE_FILTER as EC2_STATE_FILTER,
51};
52pub use crate::ui::ecr::{
53 filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
54 FILTER_CONTROLS as ECR_FILTER_CONTROLS,
55};
56use crate::ui::iam::{
57 filtered_iam_policies, filtered_iam_roles, filtered_iam_users, filtered_last_accessed,
58 filtered_tags as filtered_iam_tags, filtered_user_tags, GroupTab, RoleTab, State as IamState,
59 UserTab,
60};
61pub use crate::ui::lambda::{
62 filtered_lambda_applications, filtered_lambda_functions,
63 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
64 DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
65};
66use crate::ui::monitoring::MonitoringState;
67pub use crate::ui::s3::{
68 calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
69 ObjectTab as S3ObjectTab, State as S3State,
70};
71pub use crate::ui::sqs::{
72 extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
73 filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
74 State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
75 SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
76};
77pub use crate::ui::{
78 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
79 StreamColumn, StreamSort, TimeUnit,
80};
81#[cfg(test)]
82use rusticity_core::LogStream;
83use rusticity_core::{
84 AlarmsClient, AwsConfig, CloudFormationClient, CloudWatchClient, Ec2Client, EcrClient,
85 IamClient, LambdaClient, S3Client, SqsClient,
86};
87
88#[derive(Clone)]
89pub struct Tab {
90 pub service: Service,
91 pub title: String,
92 pub breadcrumb: String,
93}
94
95pub struct App {
96 pub running: bool,
97 pub mode: Mode,
98 pub config: AwsConfig,
99 pub cloudwatch_client: CloudWatchClient,
100 pub s3_client: S3Client,
101 pub sqs_client: SqsClient,
102 pub alarms_client: AlarmsClient,
103 pub ec2_client: Ec2Client,
104 pub ecr_client: EcrClient,
105 pub iam_client: IamClient,
106 pub lambda_client: LambdaClient,
107 pub cloudformation_client: CloudFormationClient,
108 pub current_service: Service,
109 pub tabs: Vec<Tab>,
110 pub current_tab: usize,
111 pub tab_picker_selected: usize,
112 pub tab_filter: String,
113 pub pending_key: Option<char>,
114 pub log_groups_state: CloudWatchLogGroupsState,
115 pub insights_state: CloudWatchInsightsState,
116 pub alarms_state: CloudWatchAlarmsState,
117 pub s3_state: S3State,
118 pub sqs_state: SqsState,
119 pub ec2_state: Ec2State,
120 pub ecr_state: EcrState,
121 pub lambda_state: LambdaState,
122 pub lambda_application_state: LambdaApplicationState,
123 pub cfn_state: CfnState,
124 pub iam_state: IamState,
125 pub service_picker: ServicePickerState,
126 pub service_selected: bool,
127 pub profile: String,
128 pub region: String,
129 pub region_selector_index: usize,
130 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
131 pub cw_log_group_column_ids: Vec<ColumnId>,
132 pub column_selector_index: usize,
133 pub preference_section: Preferences,
134 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
135 pub cw_log_stream_column_ids: Vec<ColumnId>,
136 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
137 pub cw_log_event_column_ids: Vec<ColumnId>,
138 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
139 pub cw_alarm_column_ids: Vec<ColumnId>,
140 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
141 pub s3_bucket_column_ids: Vec<ColumnId>,
142 pub sqs_visible_column_ids: Vec<ColumnId>,
143 pub sqs_column_ids: Vec<ColumnId>,
144 pub ec2_visible_column_ids: Vec<ColumnId>,
145 pub ec2_column_ids: Vec<ColumnId>,
146 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
147 pub ecr_repo_column_ids: Vec<ColumnId>,
148 pub ecr_image_visible_column_ids: Vec<ColumnId>,
149 pub ecr_image_column_ids: Vec<ColumnId>,
150 pub lambda_application_visible_column_ids: Vec<ColumnId>,
151 pub lambda_application_column_ids: Vec<ColumnId>,
152 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
153 pub lambda_deployment_column_ids: Vec<ColumnId>,
154 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
155 pub lambda_resource_column_ids: Vec<ColumnId>,
156 pub cfn_visible_column_ids: Vec<ColumnId>,
157 pub cfn_column_ids: Vec<ColumnId>,
158 pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
159 pub cfn_parameter_column_ids: Vec<ColumnId>,
160 pub cfn_output_visible_column_ids: Vec<ColumnId>,
161 pub cfn_output_column_ids: Vec<ColumnId>,
162 pub cfn_resource_visible_column_ids: Vec<ColumnId>,
163 pub cfn_resource_column_ids: Vec<ColumnId>,
164 pub iam_user_visible_column_ids: Vec<ColumnId>,
165 pub iam_user_column_ids: Vec<ColumnId>,
166 pub iam_role_visible_column_ids: Vec<ColumnId>,
167 pub iam_role_column_ids: Vec<ColumnId>,
168 pub iam_group_visible_column_ids: Vec<String>,
169 pub iam_group_column_ids: Vec<String>,
170 pub iam_policy_visible_column_ids: Vec<String>,
171 pub iam_policy_column_ids: Vec<String>,
172 pub view_mode: ViewMode,
173 pub error_message: Option<String>,
174 pub error_scroll: usize,
175 pub page_input: String,
176 pub calendar_date: Option<time::Date>,
177 pub calendar_selecting: CalendarField,
178 pub cursor_pos: usize,
179 pub current_session: Option<Session>,
180 pub sessions: Vec<Session>,
181 pub session_picker_selected: usize,
182 pub session_filter: String,
183 pub region_filter: String,
184 pub region_picker_selected: usize,
185 pub region_latencies: std::collections::HashMap<String, u64>,
186 pub profile_filter: String,
187 pub profile_picker_selected: usize,
188 pub available_profiles: Vec<AwsProfile>,
189 pub snapshot_requested: bool,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq)]
193pub enum CalendarField {
194 StartDate,
195 EndDate,
196}
197
198pub struct CloudWatchInsightsState {
199 pub insights: InsightsState,
200 pub loading: bool,
201}
202
203pub struct CloudWatchAlarmsState {
204 pub table: TableState<Alarm>,
205 pub alarm_tab: AlarmTab,
206 pub view_as: AlarmViewMode,
207 pub wrap_lines: bool,
208 pub sort_column: String,
209 pub sort_direction: SortDirection,
210 pub input_focus: InputFocus,
211}
212
213impl PageSize {
214 pub fn value(&self) -> usize {
215 match self {
216 PageSize::Ten => 10,
217 PageSize::TwentyFive => 25,
218 PageSize::Fifty => 50,
219 PageSize::OneHundred => 100,
220 }
221 }
222
223 pub fn next(&self) -> Self {
224 match self {
225 PageSize::Ten => PageSize::TwentyFive,
226 PageSize::TwentyFive => PageSize::Fifty,
227 PageSize::Fifty => PageSize::OneHundred,
228 PageSize::OneHundred => PageSize::Ten,
229 }
230 }
231}
232
233pub struct ServicePickerState {
234 pub filter: String,
235 pub selected: usize,
236 pub services: Vec<&'static str>,
237}
238
239#[derive(Debug, Clone, Copy, PartialEq)]
240pub enum ViewMode {
241 List,
242 Detail,
243 Events,
244 InsightsResults,
245 PolicyView,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq)]
249pub enum Service {
250 CloudWatchLogGroups,
251 CloudWatchInsights,
252 CloudWatchAlarms,
253 S3Buckets,
254 SqsQueues,
255 Ec2Instances,
256 EcrRepositories,
257 LambdaFunctions,
258 LambdaApplications,
259 CloudFormationStacks,
260 IamUsers,
261 IamRoles,
262 IamUserGroups,
263}
264
265impl Service {
266 pub fn name(&self) -> &str {
267 match self {
268 Service::CloudWatchLogGroups => "CloudWatch > Log Groups",
269 Service::CloudWatchInsights => "CloudWatch > Logs Insights",
270 Service::CloudWatchAlarms => "CloudWatch > Alarms",
271 Service::S3Buckets => "S3 > Buckets",
272 Service::SqsQueues => "SQS > Queues",
273 Service::Ec2Instances => "EC2 > Instances",
274 Service::EcrRepositories => "ECR > Repositories",
275 Service::LambdaFunctions => "Lambda > Functions",
276 Service::LambdaApplications => "Lambda > Applications",
277 Service::CloudFormationStacks => "CloudFormation > Stacks",
278 Service::IamUsers => "IAM > Users",
279 Service::IamRoles => "IAM > Roles",
280 Service::IamUserGroups => "IAM > User Groups",
281 }
282 }
283}
284
285fn copy_to_clipboard(text: &str) {
286 use std::io::Write;
287 use std::process::{Command, Stdio};
288 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
289 if let Some(mut stdin) = child.stdin.take() {
290 let _ = stdin.write_all(text.as_bytes());
291 }
292 let _ = child.wait();
293 }
294}
295
296fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
297 if max > 0 {
298 *selected = (*selected + page_size).min(max - 1);
299 }
300}
301
302fn toggle_iam_preference(
303 idx: usize,
304 column_ids: &[String],
305 visible_column_ids: &mut Vec<String>,
306 page_size: &mut PageSize,
307) {
308 if idx > 0 && idx <= column_ids.len() {
309 if let Some(col) = column_ids.get(idx - 1) {
310 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
311 visible_column_ids.remove(pos);
312 } else {
313 visible_column_ids.push(col.clone());
314 }
315 }
316 } else if idx == column_ids.len() + 3 {
317 *page_size = PageSize::Ten;
318 } else if idx == column_ids.len() + 4 {
319 *page_size = PageSize::TwentyFive;
320 } else if idx == column_ids.len() + 5 {
321 *page_size = PageSize::Fifty;
322 }
323}
324
325fn toggle_iam_preference_static(
326 idx: usize,
327 column_ids: &[ColumnId],
328 visible_column_ids: &mut Vec<ColumnId>,
329 page_size: &mut PageSize,
330) {
331 if idx > 0 && idx <= column_ids.len() {
332 if let Some(col) = column_ids.get(idx - 1) {
333 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
334 visible_column_ids.remove(pos);
335 } else {
336 visible_column_ids.push(*col);
337 }
338 }
339 } else if idx == column_ids.len() + 3 {
340 *page_size = PageSize::Ten;
341 } else if idx == column_ids.len() + 4 {
342 *page_size = PageSize::TwentyFive;
343 } else if idx == column_ids.len() + 5 {
344 *page_size = PageSize::Fifty;
345 }
346}
347
348fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
349 if idx == base_idx {
350 *page_size = PageSize::Ten;
351 } else if idx == base_idx + 1 {
352 *page_size = PageSize::TwentyFive;
353 } else if idx == base_idx + 2 {
354 *page_size = PageSize::Fifty;
355 }
356}
357
358impl App {
359 pub fn get_input_focus(&self) -> InputFocus {
360 InputFocus::Filter
361 }
362
363 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
364 if self.current_service == Service::CloudWatchAlarms {
365 Some(&mut self.alarms_state.table.filter)
366 } else if self.current_service == Service::Ec2Instances {
367 if self.ec2_state.current_instance.is_some()
368 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
369 {
370 Some(&mut self.ec2_state.tags.filter)
371 } else {
372 Some(&mut self.ec2_state.table.filter)
373 }
374 } else if self.current_service == Service::S3Buckets {
375 if self.s3_state.current_bucket.is_some() {
376 Some(&mut self.s3_state.object_filter)
377 } else {
378 Some(&mut self.s3_state.buckets.filter)
379 }
380 } else if self.current_service == Service::EcrRepositories {
381 if self.ecr_state.current_repository.is_some() {
382 Some(&mut self.ecr_state.images.filter)
383 } else {
384 Some(&mut self.ecr_state.repositories.filter)
385 }
386 } else if self.current_service == Service::SqsQueues {
387 if self.sqs_state.current_queue.is_some()
388 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
389 {
390 Some(&mut self.sqs_state.triggers.filter)
391 } else if self.sqs_state.current_queue.is_some()
392 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
393 {
394 Some(&mut self.sqs_state.pipes.filter)
395 } else if self.sqs_state.current_queue.is_some()
396 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
397 {
398 Some(&mut self.sqs_state.tags.filter)
399 } else if self.sqs_state.current_queue.is_some()
400 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
401 {
402 Some(&mut self.sqs_state.subscriptions.filter)
403 } else {
404 Some(&mut self.sqs_state.queues.filter)
405 }
406 } else if self.current_service == Service::LambdaFunctions {
407 if self.lambda_state.current_version.is_some()
408 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
409 {
410 Some(&mut self.lambda_state.alias_table.filter)
411 } else if self.lambda_state.current_function.is_some()
412 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
413 {
414 Some(&mut self.lambda_state.version_table.filter)
415 } else if self.lambda_state.current_function.is_some()
416 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
417 {
418 Some(&mut self.lambda_state.alias_table.filter)
419 } else {
420 Some(&mut self.lambda_state.table.filter)
421 }
422 } else if self.current_service == Service::LambdaApplications {
423 if self.lambda_application_state.current_application.is_some() {
424 if self.lambda_application_state.detail_tab
425 == LambdaApplicationDetailTab::Deployments
426 {
427 Some(&mut self.lambda_application_state.deployments.filter)
428 } else {
429 Some(&mut self.lambda_application_state.resources.filter)
430 }
431 } else {
432 Some(&mut self.lambda_application_state.table.filter)
433 }
434 } else if self.current_service == Service::CloudFormationStacks {
435 if self.cfn_state.current_stack.is_some()
436 && self.cfn_state.detail_tab == CfnDetailTab::Resources
437 {
438 Some(&mut self.cfn_state.resources.filter)
439 } else {
440 Some(&mut self.cfn_state.table.filter)
441 }
442 } else if self.current_service == Service::IamUsers {
443 if self.iam_state.current_user.is_some() {
444 if self.iam_state.user_tab == UserTab::Tags {
445 Some(&mut self.iam_state.user_tags.filter)
446 } else {
447 Some(&mut self.iam_state.policies.filter)
448 }
449 } else {
450 Some(&mut self.iam_state.users.filter)
451 }
452 } else if self.current_service == Service::IamRoles {
453 if self.iam_state.current_role.is_some() {
454 if self.iam_state.role_tab == RoleTab::Tags {
455 Some(&mut self.iam_state.tags.filter)
456 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
457 Some(&mut self.iam_state.last_accessed_filter)
458 } else {
459 Some(&mut self.iam_state.policies.filter)
460 }
461 } else {
462 Some(&mut self.iam_state.roles.filter)
463 }
464 } else if self.current_service == Service::IamUserGroups {
465 if self.iam_state.current_group.is_some() {
466 if self.iam_state.group_tab == GroupTab::Permissions {
467 Some(&mut self.iam_state.policies.filter)
468 } else if self.iam_state.group_tab == GroupTab::Users {
469 Some(&mut self.iam_state.group_users.filter)
470 } else {
471 None
472 }
473 } else {
474 Some(&mut self.iam_state.groups.filter)
475 }
476 } else if self.view_mode == ViewMode::List {
477 Some(&mut self.log_groups_state.log_groups.filter)
478 } else if self.view_mode == ViewMode::Detail
479 && self.log_groups_state.detail_tab == DetailTab::LogStreams
480 {
481 Some(&mut self.log_groups_state.stream_filter)
482 } else {
483 None
484 }
485 }
486
487 fn apply_filter_operation<F>(&mut self, op: F)
488 where
489 F: FnOnce(&mut String),
490 {
491 if let Some(filter) = self.get_active_filter_mut() {
492 op(filter);
493 if self.current_service == Service::CloudWatchAlarms {
495 self.alarms_state.table.reset();
496 } else if self.current_service == Service::Ec2Instances {
497 self.ec2_state.table.reset();
498 } else if self.current_service == Service::S3Buckets {
499 if self.s3_state.current_bucket.is_some() {
500 self.s3_state.selected_object = 0;
501 } else {
502 self.s3_state.buckets.reset();
503 self.s3_state.selected_row = 0;
504 self.s3_state.bucket_scroll_offset = 0;
505 }
506 } else if self.current_service == Service::EcrRepositories {
507 if self.ecr_state.current_repository.is_some() {
508 self.ecr_state.images.reset();
509 } else {
510 self.ecr_state.repositories.reset();
511 }
512 } else if self.current_service == Service::SqsQueues {
513 self.sqs_state.queues.reset();
514 } else if self.current_service == Service::LambdaFunctions {
515 if self.lambda_state.current_version.is_some()
516 || self.lambda_state.current_function.is_some()
517 {
518 self.lambda_state.version_table.reset();
519 self.lambda_state.alias_table.reset();
520 } else {
521 self.lambda_state.table.reset();
522 }
523 } else if self.current_service == Service::LambdaApplications {
524 if self.lambda_application_state.current_application.is_some() {
525 self.lambda_application_state.deployments.reset();
526 self.lambda_application_state.resources.reset();
527 } else {
528 self.lambda_application_state.table.reset();
529 }
530 } else if self.current_service == Service::CloudFormationStacks {
531 self.cfn_state.table.reset();
532 } else if self.current_service == Service::IamUsers {
533 if self.iam_state.current_user.is_some() {
534 self.iam_state.user_tags.reset();
535 self.iam_state.policies.reset();
536 } else {
537 self.iam_state.users.reset();
538 }
539 } else if self.current_service == Service::IamRoles {
540 if self.iam_state.current_role.is_some() {
541 self.iam_state.tags.reset();
542 self.iam_state.policies.reset();
543 } else {
544 self.iam_state.roles.reset();
545 }
546 } else if self.current_service == Service::IamUserGroups {
547 if self.iam_state.current_group.is_some() {
548 self.iam_state.policies.reset();
549 self.iam_state.group_users.reset();
550 } else {
551 self.iam_state.groups.reset();
552 }
553 } else if self.current_service == Service::CloudWatchLogGroups {
554 if self.view_mode == ViewMode::List {
555 self.log_groups_state.log_groups.reset();
556 } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
557 self.log_groups_state.selected_stream = 0;
558 }
559 }
560 }
561 }
562
563 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
564 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
565 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
566
567 std::env::set_var("AWS_PROFILE", &profile_name);
568
569 let config = AwsConfig::new(region).await?;
570 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
571 let s3_client = S3Client::new(config.clone());
572 let sqs_client = SqsClient::new(config.clone());
573 let alarms_client = AlarmsClient::new(config.clone());
574 let ec2_client = Ec2Client::new(config.clone());
575 let ecr_client = EcrClient::new(config.clone());
576 let iam_client = IamClient::new(config.clone());
577 let lambda_client = LambdaClient::new(config.clone());
578 let cloudformation_client = CloudFormationClient::new(config.clone());
579 let region_name = config.region.clone();
580
581 Ok(Self {
582 running: true,
583 mode: Mode::ServicePicker,
584 config,
585 cloudwatch_client,
586 s3_client,
587 sqs_client,
588 alarms_client,
589 ec2_client,
590 ecr_client,
591 iam_client,
592 lambda_client,
593 cloudformation_client,
594 current_service: Service::CloudWatchLogGroups,
595 tabs: Vec::new(),
596 current_tab: 0,
597 tab_picker_selected: 0,
598 tab_filter: String::new(),
599 pending_key: None,
600 log_groups_state: CloudWatchLogGroupsState::new(),
601 insights_state: CloudWatchInsightsState::new(),
602 alarms_state: CloudWatchAlarmsState::new(),
603 s3_state: S3State::new(),
604 sqs_state: SqsState::new(),
605 ec2_state: Ec2State::default(),
606 ecr_state: EcrState::new(),
607 lambda_state: LambdaState::new(),
608 lambda_application_state: LambdaApplicationState::new(),
609 cfn_state: CfnState::new(),
610 iam_state: IamState::new(),
611 service_picker: ServicePickerState::new(),
612 service_selected: false,
613 profile: profile_name,
614 region: region_name,
615 region_selector_index: 0,
616 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
617 cw_log_group_column_ids: LogGroupColumn::ids(),
618 column_selector_index: 0,
619 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
620 cw_log_stream_column_ids: StreamColumn::ids(),
621 cw_log_event_visible_column_ids: EventColumn::default_visible(),
622 cw_log_event_column_ids: EventColumn::ids(),
623 cw_alarm_visible_column_ids: [
624 AlarmColumn::Name,
625 AlarmColumn::State,
626 AlarmColumn::LastStateUpdate,
627 AlarmColumn::Conditions,
628 AlarmColumn::Actions,
629 ]
630 .iter()
631 .map(|c| c.id())
632 .collect(),
633 cw_alarm_column_ids: AlarmColumn::ids(),
634 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
635 s3_bucket_column_ids: S3BucketColumn::ids(),
636 sqs_visible_column_ids: [
637 SqsColumn::Name,
638 SqsColumn::Type,
639 SqsColumn::Created,
640 SqsColumn::MessagesAvailable,
641 SqsColumn::MessagesInFlight,
642 SqsColumn::Encryption,
643 SqsColumn::ContentBasedDeduplication,
644 ]
645 .iter()
646 .map(|c| c.id())
647 .collect(),
648 sqs_column_ids: SqsColumn::ids(),
649 ec2_visible_column_ids: [
650 Ec2Column::Name,
651 Ec2Column::InstanceId,
652 Ec2Column::InstanceState,
653 Ec2Column::InstanceType,
654 Ec2Column::StatusCheck,
655 Ec2Column::AlarmStatus,
656 Ec2Column::AvailabilityZone,
657 Ec2Column::PublicIpv4Dns,
658 Ec2Column::PublicIpv4Address,
659 Ec2Column::ElasticIp,
660 Ec2Column::Ipv6Ips,
661 Ec2Column::Monitoring,
662 Ec2Column::SecurityGroupName,
663 Ec2Column::KeyName,
664 Ec2Column::LaunchTime,
665 Ec2Column::PlatformDetails,
666 ]
667 .iter()
668 .map(|c| c.id())
669 .collect(),
670 ec2_column_ids: Ec2Column::ids(),
671 ecr_repo_visible_column_ids: EcrColumn::ids(),
672 ecr_repo_column_ids: EcrColumn::ids(),
673 ecr_image_visible_column_ids: EcrImageColumn::ids(),
674 ecr_image_column_ids: EcrImageColumn::ids(),
675 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
676 lambda_application_column_ids: LambdaApplicationColumn::ids(),
677 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
678 lambda_deployment_column_ids: DeploymentColumn::ids(),
679 lambda_resource_visible_column_ids: ResourceColumn::ids(),
680 lambda_resource_column_ids: ResourceColumn::ids(),
681 cfn_visible_column_ids: [
682 CfnColumn::Name,
683 CfnColumn::Status,
684 CfnColumn::CreatedTime,
685 CfnColumn::Description,
686 ]
687 .iter()
688 .map(|c| c.id())
689 .collect(),
690 cfn_column_ids: CfnColumn::ids(),
691 cfn_parameter_visible_column_ids: parameter_column_ids(),
692 cfn_parameter_column_ids: parameter_column_ids(),
693 cfn_output_visible_column_ids: output_column_ids(),
694 cfn_output_column_ids: output_column_ids(),
695 cfn_resource_visible_column_ids: resource_column_ids(),
696 cfn_resource_column_ids: resource_column_ids(),
697 iam_user_visible_column_ids: UserColumn::visible(),
698 iam_user_column_ids: UserColumn::ids(),
699 iam_role_visible_column_ids: RoleColumn::visible(),
700 iam_role_column_ids: RoleColumn::ids(),
701 iam_group_visible_column_ids: vec![
702 "Group name".to_string(),
703 "Users".to_string(),
704 "Permissions".to_string(),
705 "Creation time".to_string(),
706 ],
707 iam_group_column_ids: vec![
708 "Group name".to_string(),
709 "Path".to_string(),
710 "Users".to_string(),
711 "Permissions".to_string(),
712 "Creation time".to_string(),
713 ],
714 iam_policy_visible_column_ids: vec![
715 "Policy name".to_string(),
716 "Type".to_string(),
717 "Attached via".to_string(),
718 ],
719 iam_policy_column_ids: vec![
720 "Policy name".to_string(),
721 "Type".to_string(),
722 "Attached via".to_string(),
723 "Attached entities".to_string(),
724 "Description".to_string(),
725 "Creation time".to_string(),
726 "Edited time".to_string(),
727 ],
728 preference_section: Preferences::Columns,
729 view_mode: ViewMode::List,
730 error_message: None,
731 error_scroll: 0,
732 page_input: String::new(),
733 calendar_date: None,
734 calendar_selecting: CalendarField::StartDate,
735 cursor_pos: 0,
736 current_session: None,
737 sessions: Vec::new(),
738 session_picker_selected: 0,
739 session_filter: String::new(),
740 region_filter: String::new(),
741 region_picker_selected: 0,
742 region_latencies: std::collections::HashMap::new(),
743 profile_filter: String::new(),
744 profile_picker_selected: 0,
745 available_profiles: Vec::new(),
746 snapshot_requested: false,
747 })
748 }
749
750 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
751 let config = AwsConfig::dummy(region.clone());
752 Self {
753 running: true,
754 mode: Mode::ServicePicker,
755 config: config.clone(),
756 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
757 s3_client: S3Client::new(config.clone()),
758 sqs_client: SqsClient::new(config.clone()),
759 alarms_client: AlarmsClient::new(config.clone()),
760 ec2_client: Ec2Client::new(config.clone()),
761 ecr_client: EcrClient::new(config.clone()),
762 iam_client: IamClient::new(config.clone()),
763 lambda_client: LambdaClient::new(config.clone()),
764 cloudformation_client: CloudFormationClient::new(config.clone()),
765 current_service: Service::CloudWatchLogGroups,
766 tabs: Vec::new(),
767 current_tab: 0,
768 tab_picker_selected: 0,
769 tab_filter: String::new(),
770 pending_key: None,
771 log_groups_state: CloudWatchLogGroupsState::new(),
772 insights_state: CloudWatchInsightsState::new(),
773 alarms_state: CloudWatchAlarmsState::new(),
774 s3_state: S3State::new(),
775 sqs_state: SqsState::new(),
776 ec2_state: Ec2State::default(),
777 ecr_state: EcrState::new(),
778 lambda_state: LambdaState::new(),
779 lambda_application_state: LambdaApplicationState::new(),
780 cfn_state: CfnState::new(),
781 iam_state: IamState::new(),
782 service_picker: ServicePickerState::new(),
783 service_selected: false,
784 profile,
785 region: region.unwrap_or_default(),
786 region_selector_index: 0,
787 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
788 cw_log_group_column_ids: LogGroupColumn::ids(),
789 column_selector_index: 0,
790 preference_section: Preferences::Columns,
791 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
792 cw_log_stream_column_ids: StreamColumn::ids(),
793 cw_log_event_visible_column_ids: EventColumn::default_visible(),
794 cw_log_event_column_ids: EventColumn::ids(),
795 cw_alarm_visible_column_ids: [
796 AlarmColumn::Name,
797 AlarmColumn::State,
798 AlarmColumn::LastStateUpdate,
799 AlarmColumn::Conditions,
800 AlarmColumn::Actions,
801 ]
802 .iter()
803 .map(|c| c.id())
804 .collect(),
805 cw_alarm_column_ids: AlarmColumn::ids(),
806 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
807 s3_bucket_column_ids: S3BucketColumn::ids(),
808 sqs_visible_column_ids: [
809 SqsColumn::Name,
810 SqsColumn::Type,
811 SqsColumn::Created,
812 SqsColumn::MessagesAvailable,
813 SqsColumn::MessagesInFlight,
814 SqsColumn::Encryption,
815 SqsColumn::ContentBasedDeduplication,
816 ]
817 .iter()
818 .map(|c| c.id())
819 .collect(),
820 sqs_column_ids: SqsColumn::ids(),
821 ec2_visible_column_ids: [
822 Ec2Column::Name,
823 Ec2Column::InstanceId,
824 Ec2Column::InstanceState,
825 Ec2Column::InstanceType,
826 Ec2Column::StatusCheck,
827 Ec2Column::AlarmStatus,
828 Ec2Column::AvailabilityZone,
829 Ec2Column::PublicIpv4Dns,
830 Ec2Column::PublicIpv4Address,
831 Ec2Column::ElasticIp,
832 Ec2Column::Ipv6Ips,
833 Ec2Column::Monitoring,
834 Ec2Column::SecurityGroupName,
835 Ec2Column::KeyName,
836 Ec2Column::LaunchTime,
837 Ec2Column::PlatformDetails,
838 ]
839 .iter()
840 .map(|c| c.id())
841 .collect(),
842 ec2_column_ids: Ec2Column::ids(),
843 ecr_repo_visible_column_ids: EcrColumn::ids(),
844 ecr_repo_column_ids: EcrColumn::ids(),
845 ecr_image_visible_column_ids: EcrImageColumn::ids(),
846 ecr_image_column_ids: EcrImageColumn::ids(),
847 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
848 lambda_application_column_ids: LambdaApplicationColumn::ids(),
849 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
850 lambda_deployment_column_ids: DeploymentColumn::ids(),
851 lambda_resource_visible_column_ids: ResourceColumn::ids(),
852 lambda_resource_column_ids: ResourceColumn::ids(),
853 cfn_visible_column_ids: [
854 CfnColumn::Name,
855 CfnColumn::Status,
856 CfnColumn::CreatedTime,
857 CfnColumn::Description,
858 ]
859 .iter()
860 .map(|c| c.id())
861 .collect(),
862 cfn_column_ids: CfnColumn::ids(),
863 iam_user_visible_column_ids: UserColumn::visible(),
864 cfn_parameter_visible_column_ids: parameter_column_ids(),
865 cfn_parameter_column_ids: parameter_column_ids(),
866 cfn_output_visible_column_ids: output_column_ids(),
867 cfn_output_column_ids: output_column_ids(),
868 cfn_resource_visible_column_ids: resource_column_ids(),
869 cfn_resource_column_ids: resource_column_ids(),
870 iam_user_column_ids: UserColumn::ids(),
871 iam_role_visible_column_ids: RoleColumn::visible(),
872 iam_role_column_ids: RoleColumn::ids(),
873 iam_group_visible_column_ids: vec![
874 "Group name".to_string(),
875 "Users".to_string(),
876 "Permissions".to_string(),
877 "Creation time".to_string(),
878 ],
879 iam_group_column_ids: vec![
880 "Group name".to_string(),
881 "Path".to_string(),
882 "Users".to_string(),
883 "Permissions".to_string(),
884 "Creation time".to_string(),
885 ],
886 iam_policy_visible_column_ids: vec![
887 "Policy name".to_string(),
888 "Type".to_string(),
889 "Attached via".to_string(),
890 ],
891 iam_policy_column_ids: vec![
892 "Policy name".to_string(),
893 "Type".to_string(),
894 "Attached via".to_string(),
895 "Attached entities".to_string(),
896 "Description".to_string(),
897 "Creation time".to_string(),
898 "Edited time".to_string(),
899 ],
900 view_mode: ViewMode::List,
901 error_message: None,
902 error_scroll: 0,
903 page_input: String::new(),
904 calendar_date: None,
905 calendar_selecting: CalendarField::StartDate,
906 cursor_pos: 0,
907 current_session: None,
908 sessions: Vec::new(),
909 session_picker_selected: 0,
910 session_filter: String::new(),
911 region_filter: String::new(),
912 region_picker_selected: 0,
913 region_latencies: std::collections::HashMap::new(),
914 profile_filter: String::new(),
915 profile_picker_selected: 0,
916 available_profiles: Vec::new(),
917 snapshot_requested: false,
918 }
919 }
920
921 pub fn handle_action(&mut self, action: Action) {
922 match action {
923 Action::Quit => {
924 self.save_current_session();
925 self.running = false;
926 }
927 Action::CloseService => {
928 if !self.tabs.is_empty() {
929 self.tabs.remove(self.current_tab);
931
932 if self.tabs.is_empty() {
933 self.service_selected = false;
935 self.current_tab = 0;
936 self.mode = Mode::ServicePicker;
937 } else {
938 if self.current_tab >= self.tabs.len() {
940 self.current_tab = self.tabs.len() - 1;
941 }
942 self.current_service = self.tabs[self.current_tab].service;
943 self.service_selected = true;
944 self.mode = Mode::Normal;
945 }
946 } else {
947 self.service_selected = false;
949 self.mode = Mode::Normal;
950 }
951 self.service_picker.filter.clear();
952 self.service_picker.selected = 0;
953 }
954 Action::NextItem => self.next_item(),
955 Action::PrevItem => self.prev_item(),
956 Action::PageUp => self.page_up(),
957 Action::PageDown => self.page_down(),
958 Action::NextPane => self.next_pane(),
959 Action::PrevPane => self.prev_pane(),
960 Action::CollapseRow => self.collapse_row(),
961 Action::ExpandRow => self.expand_row(),
962 Action::Select => self.select_item(),
963 Action::OpenSpaceMenu => {
964 self.mode = Mode::SpaceMenu;
965 self.service_picker.filter.clear();
966 self.service_picker.selected = 0;
967 }
968 Action::CloseMenu => {
969 self.mode = Mode::Normal;
970 self.service_picker.filter.clear();
971 match self.current_service {
973 Service::S3Buckets => {
974 self.s3_state.selected_row = 0;
975 self.s3_state.selected_object = 0;
976 }
977 Service::CloudFormationStacks => {
978 if self.cfn_state.current_stack.is_some()
979 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
980 {
981 self.cfn_state.parameters.reset();
982 } else if self.cfn_state.current_stack.is_some()
983 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
984 {
985 self.cfn_state.outputs.reset();
986 } else {
987 self.cfn_state.table.reset();
988 }
989 }
990 Service::LambdaFunctions => {
991 self.lambda_state.table.reset();
992 }
993 Service::SqsQueues => {
994 self.sqs_state.queues.reset();
995 }
996 Service::IamRoles => {
997 self.iam_state.roles.reset();
998 }
999 Service::IamUsers => {
1000 self.iam_state.users.reset();
1001 }
1002 Service::IamUserGroups => {
1003 self.iam_state.groups.reset();
1004 }
1005 Service::CloudWatchAlarms => {
1006 self.alarms_state.table.reset();
1007 }
1008 Service::Ec2Instances => {
1009 self.ec2_state.table.reset();
1010 }
1011 Service::EcrRepositories => {
1012 self.ecr_state.repositories.reset();
1013 }
1014 Service::LambdaApplications => {
1015 self.lambda_application_state.table.reset();
1016 }
1017 _ => {}
1018 }
1019 }
1020 Action::NextTab => {
1021 if !self.tabs.is_empty() {
1022 self.current_tab = (self.current_tab + 1) % self.tabs.len();
1023 self.current_service = self.tabs[self.current_tab].service;
1024 }
1025 }
1026 Action::PrevTab => {
1027 if !self.tabs.is_empty() {
1028 self.current_tab = if self.current_tab == 0 {
1029 self.tabs.len() - 1
1030 } else {
1031 self.current_tab - 1
1032 };
1033 self.current_service = self.tabs[self.current_tab].service;
1034 }
1035 }
1036 Action::CloseTab => {
1037 if !self.tabs.is_empty() {
1038 self.tabs.remove(self.current_tab);
1039 if self.tabs.is_empty() {
1040 self.service_selected = false;
1042 self.current_tab = 0;
1043 self.service_picker.filter.clear();
1044 self.service_picker.selected = 0;
1045 self.mode = Mode::ServicePicker;
1046 } else {
1047 if self.current_tab >= self.tabs.len() {
1050 self.current_tab = self.tabs.len() - 1;
1051 }
1052 self.current_service = self.tabs[self.current_tab].service;
1053 self.service_selected = true;
1054 self.mode = Mode::Normal;
1055 }
1056 }
1057 }
1058 Action::OpenTabPicker => {
1059 if !self.tabs.is_empty() {
1060 self.tab_picker_selected = self.current_tab;
1061 self.mode = Mode::TabPicker;
1062 } else {
1063 self.mode = Mode::Normal;
1064 }
1065 }
1066 Action::OpenSessionPicker => {
1067 self.save_current_session();
1068 self.sessions = Session::list_all().unwrap_or_default();
1069 self.session_picker_selected = 0;
1070 self.mode = Mode::SessionPicker;
1071 }
1072 Action::LoadSession => {
1073 let filtered_sessions = self.get_filtered_sessions();
1074 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1075 let session = session.clone();
1076 self.profile = session.profile.clone();
1078 self.region = session.region.clone();
1079 self.config.account_id = session.account_id.clone();
1080 self.config.role_arn = session.role_arn.clone();
1081
1082 self.tabs.clear();
1084 for session_tab in &session.tabs {
1085 let service = match session_tab.service.as_str() {
1087 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1088 "CloudWatchInsights" => Service::CloudWatchInsights,
1089 "CloudWatchAlarms" => Service::CloudWatchAlarms,
1090 "S3Buckets" => Service::S3Buckets,
1091 "SqsQueues" => Service::SqsQueues,
1092 "Ec2Instances" => Service::Ec2Instances,
1093 "EcrRepositories" => Service::EcrRepositories,
1094 "LambdaFunctions" => Service::LambdaFunctions,
1095 "LambdaApplications" => Service::LambdaApplications,
1096 "CloudFormationStacks" => Service::CloudFormationStacks,
1097 "IamUsers" => Service::IamUsers,
1098 "IamRoles" => Service::IamRoles,
1099 "IamUserGroups" => Service::IamUserGroups,
1100 _ => continue,
1101 };
1102
1103 self.tabs.push(Tab {
1104 service,
1105 title: session_tab.title.clone(),
1106 breadcrumb: session_tab.breadcrumb.clone(),
1107 });
1108
1109 if let Some(filter) = &session_tab.filter {
1111 if service == Service::CloudWatchLogGroups {
1112 self.log_groups_state.log_groups.filter = filter.clone();
1113 }
1114 }
1115 }
1116
1117 if !self.tabs.is_empty() {
1118 self.current_tab = 0;
1119 self.current_service = self.tabs[0].service;
1120 self.service_selected = true;
1121 self.current_session = Some(session.clone());
1122 }
1123 }
1124 self.mode = Mode::Normal;
1125 }
1126 Action::SaveSession => {
1127 }
1129 Action::OpenServicePicker => {
1130 if self.mode == Mode::ServicePicker {
1131 self.tabs.push(Tab {
1132 service: Service::S3Buckets,
1133 title: "S3 > Buckets".to_string(),
1134 breadcrumb: "S3 > Buckets".to_string(),
1135 });
1136 self.current_tab = self.tabs.len() - 1;
1137 self.current_service = Service::S3Buckets;
1138 self.view_mode = ViewMode::List;
1139 self.service_selected = true;
1140 self.mode = Mode::Normal;
1141 } else {
1142 self.mode = Mode::ServicePicker;
1143 self.service_picker.filter.clear();
1144 self.service_picker.selected = 0;
1145 }
1146 }
1147 Action::OpenCloudWatch => {
1148 self.current_service = Service::CloudWatchLogGroups;
1149 self.view_mode = ViewMode::List;
1150 self.service_selected = true;
1151 self.mode = Mode::Normal;
1152 }
1153 Action::OpenCloudWatchSplit => {
1154 self.current_service = Service::CloudWatchInsights;
1155 self.view_mode = ViewMode::InsightsResults;
1156 self.service_selected = true;
1157 self.mode = Mode::Normal;
1158 }
1159 Action::OpenCloudWatchAlarms => {
1160 self.current_service = Service::CloudWatchAlarms;
1161 self.view_mode = ViewMode::List;
1162 self.service_selected = true;
1163 self.mode = Mode::Normal;
1164 }
1165 Action::FilterInput(c) => {
1166 if self.mode == Mode::TabPicker {
1167 self.tab_filter.push(c);
1168 self.tab_picker_selected = 0;
1169 } else if self.mode == Mode::RegionPicker {
1170 self.region_filter.push(c);
1171 self.region_picker_selected = 0;
1172 } else if self.mode == Mode::ProfilePicker {
1173 self.profile_filter.push(c);
1174 self.profile_picker_selected = 0;
1175 } else if self.mode == Mode::SessionPicker {
1176 self.session_filter.push(c);
1177 self.session_picker_selected = 0;
1178 } else if self.mode == Mode::ServicePicker {
1179 self.service_picker.filter.push(c);
1180 self.service_picker.selected = 0;
1181 } else if self.mode == Mode::InsightsInput {
1182 match self.insights_state.insights.insights_focus {
1183 InsightsFocus::Query => {
1184 self.insights_state.insights.query_text.push(c);
1185 }
1186 InsightsFocus::LogGroupSearch => {
1187 self.insights_state.insights.log_group_search.push(c);
1188 if !self.insights_state.insights.log_group_search.is_empty() {
1190 self.insights_state.insights.log_group_matches = self
1191 .log_groups_state
1192 .log_groups
1193 .items
1194 .iter()
1195 .filter(|g| {
1196 g.name.to_lowercase().contains(
1197 &self
1198 .insights_state
1199 .insights
1200 .log_group_search
1201 .to_lowercase(),
1202 )
1203 })
1204 .take(50)
1205 .map(|g| g.name.clone())
1206 .collect();
1207 self.insights_state.insights.show_dropdown = true;
1208 } else {
1209 self.insights_state.insights.log_group_matches.clear();
1210 self.insights_state.insights.show_dropdown = false;
1211 }
1212 }
1213 _ => {}
1214 }
1215 } else if self.mode == Mode::FilterInput {
1216 let is_pagination_focused = if self.current_service
1218 == Service::LambdaApplications
1219 {
1220 if self.lambda_application_state.current_application.is_some() {
1221 if self.lambda_application_state.detail_tab
1222 == LambdaApplicationDetailTab::Deployments
1223 {
1224 self.lambda_application_state.deployment_input_focus
1225 == InputFocus::Pagination
1226 } else {
1227 self.lambda_application_state.resource_input_focus
1228 == InputFocus::Pagination
1229 }
1230 } else {
1231 self.lambda_application_state.input_focus == InputFocus::Pagination
1232 }
1233 } else if self.current_service == Service::CloudFormationStacks {
1234 self.cfn_state.input_focus == InputFocus::Pagination
1235 } else if self.current_service == Service::IamRoles
1236 && self.iam_state.current_role.is_none()
1237 {
1238 self.iam_state.role_input_focus == InputFocus::Pagination
1239 } else if self.view_mode == ViewMode::PolicyView {
1240 self.iam_state.policy_input_focus == InputFocus::Pagination
1241 } else if self.current_service == Service::CloudWatchAlarms {
1242 self.alarms_state.input_focus == InputFocus::Pagination
1243 } else if self.current_service == Service::Ec2Instances {
1244 self.ec2_state.input_focus == InputFocus::Pagination
1245 } else if self.current_service == Service::CloudWatchLogGroups {
1246 self.log_groups_state.input_focus == InputFocus::Pagination
1247 } else if self.current_service == Service::EcrRepositories
1248 && self.ecr_state.current_repository.is_none()
1249 {
1250 self.ecr_state.input_focus == InputFocus::Pagination
1251 } else if self.current_service == Service::LambdaFunctions {
1252 if self.lambda_state.current_function.is_some()
1253 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1254 {
1255 self.lambda_state.version_input_focus == InputFocus::Pagination
1256 } else if self.lambda_state.current_function.is_none() {
1257 self.lambda_state.input_focus == InputFocus::Pagination
1258 } else {
1259 false
1260 }
1261 } else if self.current_service == Service::SqsQueues {
1262 if self.sqs_state.current_queue.is_some()
1263 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1264 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1265 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1266 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1267 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1268 {
1269 self.sqs_state.input_focus == InputFocus::Pagination
1270 } else {
1271 false
1272 }
1273 } else {
1274 false
1275 };
1276
1277 if is_pagination_focused && c.is_ascii_digit() {
1278 self.page_input.push(c);
1279 } else if self.current_service == Service::LambdaApplications {
1280 let is_input_focused =
1281 if self.lambda_application_state.current_application.is_some() {
1282 if self.lambda_application_state.detail_tab
1283 == LambdaApplicationDetailTab::Deployments
1284 {
1285 self.lambda_application_state.deployment_input_focus
1286 == InputFocus::Filter
1287 } else {
1288 self.lambda_application_state.resource_input_focus
1289 == InputFocus::Filter
1290 }
1291 } else {
1292 self.lambda_application_state.input_focus == InputFocus::Filter
1293 };
1294 if is_input_focused {
1295 self.apply_filter_operation(|f| f.push(c));
1296 }
1297 } else if self.current_service == Service::CloudFormationStacks {
1298 if self.cfn_state.current_stack.is_some()
1299 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1300 {
1301 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1302 self.cfn_state.parameters.filter.push(c);
1303 self.cfn_state.parameters.selected = 0;
1304 }
1305 } else if self.cfn_state.current_stack.is_some()
1306 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1307 {
1308 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1309 self.cfn_state.outputs.filter.push(c);
1310 self.cfn_state.outputs.selected = 0;
1311 }
1312 } else if self.cfn_state.current_stack.is_some()
1313 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1314 {
1315 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1316 self.cfn_state.resources.filter.push(c);
1317 self.cfn_state.resources.selected = 0;
1318 }
1319 } else if self.cfn_state.input_focus == InputFocus::Filter {
1320 self.apply_filter_operation(|f| f.push(c));
1321 }
1322 } else if self.current_service == Service::EcrRepositories
1323 && self.ecr_state.current_repository.is_none()
1324 {
1325 if self.ecr_state.input_focus == InputFocus::Filter {
1326 self.apply_filter_operation(|f| f.push(c));
1327 }
1328 } else if self.current_service == Service::IamRoles
1329 && self.iam_state.current_role.is_none()
1330 {
1331 if self.iam_state.role_input_focus == InputFocus::Filter {
1332 self.apply_filter_operation(|f| f.push(c));
1333 }
1334 } else if self.view_mode == ViewMode::PolicyView {
1335 if self.iam_state.policy_input_focus == InputFocus::Filter {
1336 self.apply_filter_operation(|f| f.push(c));
1337 }
1338 } else if self.current_service == Service::LambdaFunctions
1339 && self.lambda_state.current_version.is_some()
1340 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1341 {
1342 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1343 self.apply_filter_operation(|f| f.push(c));
1344 }
1345 } else if self.current_service == Service::LambdaFunctions
1346 && self.lambda_state.current_function.is_some()
1347 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1348 {
1349 if self.lambda_state.version_input_focus == InputFocus::Filter {
1350 self.apply_filter_operation(|f| f.push(c));
1351 }
1352 } else if self.current_service == Service::LambdaFunctions
1353 && self.lambda_state.current_function.is_some()
1354 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1355 {
1356 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1357 self.apply_filter_operation(|f| f.push(c));
1358 }
1359 } else if self.current_service == Service::SqsQueues
1360 && self.sqs_state.current_queue.is_some()
1361 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1362 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1363 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1364 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1365 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1366 {
1367 if self.sqs_state.input_focus == InputFocus::Filter {
1368 self.apply_filter_operation(|f| f.push(c));
1369 }
1370 } else if self.current_service == Service::Ec2Instances
1371 && self.ec2_state.current_instance.is_some()
1372 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1373 {
1374 if self.ec2_state.input_focus == InputFocus::Filter {
1375 self.ec2_state.tags.filter.push(c);
1376 self.ec2_state.tags.selected = 0;
1377 }
1378 } else if self.current_service == Service::CloudWatchLogGroups {
1379 if self.log_groups_state.input_focus == InputFocus::Filter {
1380 self.apply_filter_operation(|f| f.push(c));
1381 }
1382 } else {
1383 self.apply_filter_operation(|f| f.push(c));
1384 }
1385 } else if self.mode == Mode::EventFilterInput {
1386 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1387 self.log_groups_state.event_filter.push(c);
1388 } else if c.is_ascii_digit() {
1389 self.log_groups_state.relative_amount.push(c);
1390 }
1391 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1392 self.page_input.push(c);
1393 }
1394 }
1395 Action::FilterBackspace => {
1396 if self.mode == Mode::ServicePicker {
1397 self.service_picker.filter.pop();
1398 self.service_picker.selected = 0;
1399 } else if self.mode == Mode::TabPicker {
1400 self.tab_filter.pop();
1401 self.tab_picker_selected = 0;
1402 } else if self.mode == Mode::RegionPicker {
1403 self.region_filter.pop();
1404 self.region_picker_selected = 0;
1405 } else if self.mode == Mode::ProfilePicker {
1406 self.profile_filter.pop();
1407 self.profile_picker_selected = 0;
1408 } else if self.mode == Mode::SessionPicker {
1409 self.session_filter.pop();
1410 self.session_picker_selected = 0;
1411 } else if self.mode == Mode::InsightsInput {
1412 match self.insights_state.insights.insights_focus {
1413 InsightsFocus::Query => {
1414 self.insights_state.insights.query_text.pop();
1415 }
1416 InsightsFocus::LogGroupSearch => {
1417 self.insights_state.insights.log_group_search.pop();
1418 if !self.insights_state.insights.log_group_search.is_empty() {
1420 self.insights_state.insights.log_group_matches = self
1421 .log_groups_state
1422 .log_groups
1423 .items
1424 .iter()
1425 .filter(|g| {
1426 g.name.to_lowercase().contains(
1427 &self
1428 .insights_state
1429 .insights
1430 .log_group_search
1431 .to_lowercase(),
1432 )
1433 })
1434 .take(50)
1435 .map(|g| g.name.clone())
1436 .collect();
1437 self.insights_state.insights.show_dropdown = true;
1438 } else {
1439 self.insights_state.insights.log_group_matches.clear();
1440 self.insights_state.insights.show_dropdown = false;
1441 }
1442 }
1443 _ => {}
1444 }
1445 } else if self.mode == Mode::FilterInput {
1446 if self.current_service == Service::CloudFormationStacks {
1448 if self.cfn_state.current_stack.is_some()
1449 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1450 {
1451 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1452 self.cfn_state.parameters.filter.pop();
1453 self.cfn_state.parameters.selected = 0;
1454 }
1455 } else if self.cfn_state.current_stack.is_some()
1456 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1457 {
1458 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1459 self.cfn_state.outputs.filter.pop();
1460 self.cfn_state.outputs.selected = 0;
1461 }
1462 } else if self.cfn_state.current_stack.is_some()
1463 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1464 {
1465 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1466 self.cfn_state.resources.filter.pop();
1467 self.cfn_state.resources.selected = 0;
1468 }
1469 } else if self.cfn_state.input_focus == InputFocus::Filter {
1470 self.apply_filter_operation(|f| {
1471 f.pop();
1472 });
1473 }
1474 } else if self.current_service == Service::Ec2Instances
1475 && self.ec2_state.current_instance.is_some()
1476 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1477 {
1478 if self.ec2_state.input_focus == InputFocus::Filter {
1479 self.ec2_state.tags.filter.pop();
1480 self.ec2_state.tags.selected = 0;
1481 }
1482 } else if self.current_service == Service::CloudWatchLogGroups {
1483 if self.log_groups_state.input_focus == InputFocus::Filter {
1484 self.apply_filter_operation(|f| {
1485 f.pop();
1486 });
1487 }
1488 } else {
1489 self.apply_filter_operation(|f| {
1490 f.pop();
1491 });
1492 }
1493 } else if self.mode == Mode::EventFilterInput {
1494 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1495 self.log_groups_state.event_filter.pop();
1496 } else {
1497 self.log_groups_state.relative_amount.pop();
1498 }
1499 }
1500 }
1501 Action::DeleteWord => {
1502 let text = if self.mode == Mode::ServicePicker {
1503 &mut self.service_picker.filter
1504 } else if self.mode == Mode::InsightsInput {
1505 use crate::app::InsightsFocus;
1506 match self.insights_state.insights.insights_focus {
1507 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1508 InsightsFocus::LogGroupSearch => {
1509 &mut self.insights_state.insights.log_group_search
1510 }
1511 _ => return,
1512 }
1513 } else if self.mode == Mode::FilterInput {
1514 if let Some(filter) = self.get_active_filter_mut() {
1515 filter
1516 } else {
1517 return;
1518 }
1519 } else if self.mode == Mode::EventFilterInput {
1520 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1521 &mut self.log_groups_state.event_filter
1522 } else {
1523 &mut self.log_groups_state.relative_amount
1524 }
1525 } else {
1526 return;
1527 };
1528
1529 if text.is_empty() {
1530 return;
1531 }
1532
1533 let mut chars: Vec<char> = text.chars().collect();
1534 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1535 chars.pop();
1536 }
1537 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1538 chars.pop();
1539 }
1540 *text = chars.into_iter().collect();
1541 }
1542 Action::WordLeft => {
1543 }
1545 Action::WordRight => {
1546 }
1548 Action::OpenColumnSelector => {
1549 if self.current_service == Service::CloudFormationStacks
1551 && self.cfn_state.current_stack.is_some()
1552 && (self.cfn_state.detail_tab == CfnDetailTab::Template
1553 || self.cfn_state.detail_tab == CfnDetailTab::GitSync)
1554 {
1555 return;
1556 }
1557
1558 if self.current_service == Service::IamUsers
1560 && self.iam_state.current_user.is_some()
1561 && self.iam_state.user_tab == UserTab::SecurityCredentials
1562 {
1563 return;
1564 }
1565
1566 if self.current_service == Service::IamRoles
1568 && self.iam_state.current_role.is_some()
1569 && (self.iam_state.role_tab == RoleTab::TrustRelationships
1570 || self.iam_state.role_tab == RoleTab::RevokeSessions)
1571 {
1572 return;
1573 }
1574
1575 if self.current_service == Service::SqsQueues
1577 && self.sqs_state.current_queue.is_some()
1578 && matches!(
1579 self.sqs_state.detail_tab,
1580 SqsQueueDetailTab::QueuePolicies
1581 | SqsQueueDetailTab::Monitoring
1582 | SqsQueueDetailTab::DeadLetterQueue
1583 | SqsQueueDetailTab::Encryption
1584 | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1585 )
1586 {
1587 return;
1588 }
1589
1590 if self.current_service == Service::Ec2Instances
1592 && self.ec2_state.table.expanded_item.is_some()
1593 && self.ec2_state.detail_tab != Ec2DetailTab::Tags
1594 {
1595 return;
1596 }
1597
1598 if !self.page_input.is_empty() {
1600 if let Ok(page) = self.page_input.parse::<usize>() {
1601 self.go_to_page(page);
1602 }
1603 self.page_input.clear();
1604 } else {
1605 self.mode = Mode::ColumnSelector;
1606 self.column_selector_index = 0;
1607 }
1608 }
1609 Action::ToggleColumn => {
1610 if self.current_service == Service::S3Buckets
1611 && self.s3_state.current_bucket.is_none()
1612 {
1613 let idx = self.column_selector_index;
1614 if idx > 0 && idx <= self.s3_bucket_column_ids.len() {
1615 if let Some(col) = self.s3_bucket_column_ids.get(idx - 1) {
1616 if let Some(pos) = self
1617 .s3_bucket_visible_column_ids
1618 .iter()
1619 .position(|c| c == col)
1620 {
1621 self.s3_bucket_visible_column_ids.remove(pos);
1622 } else {
1623 self.s3_bucket_visible_column_ids.push(*col);
1624 }
1625 }
1626 } else if idx == self.s3_bucket_column_ids.len() + 3 {
1627 self.s3_state.buckets.page_size = PageSize::Ten;
1628 } else if idx == self.s3_bucket_column_ids.len() + 4 {
1629 self.s3_state.buckets.page_size = PageSize::TwentyFive;
1630 } else if idx == self.s3_bucket_column_ids.len() + 5 {
1631 self.s3_state.buckets.page_size = PageSize::Fifty;
1632 } else if idx == self.s3_bucket_column_ids.len() + 6 {
1633 self.s3_state.buckets.page_size = PageSize::OneHundred;
1634 }
1635 } else if self.current_service == Service::CloudWatchAlarms {
1636 let idx = self.column_selector_index;
1640 if (1..=16).contains(&idx) {
1641 if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1643 if let Some(pos) = self
1644 .cw_alarm_visible_column_ids
1645 .iter()
1646 .position(|c| c == col)
1647 {
1648 self.cw_alarm_visible_column_ids.remove(pos);
1649 } else {
1650 self.cw_alarm_visible_column_ids.push(*col);
1651 }
1652 }
1653 } else if idx == 19 {
1654 self.alarms_state.view_as = AlarmViewMode::Table;
1655 } else if idx == 20 {
1656 self.alarms_state.view_as = AlarmViewMode::Cards;
1657 } else if idx == 23 {
1658 self.alarms_state.table.page_size = PageSize::Ten;
1659 } else if idx == 24 {
1660 self.alarms_state.table.page_size = PageSize::TwentyFive;
1661 } else if idx == 25 {
1662 self.alarms_state.table.page_size = PageSize::Fifty;
1663 } else if idx == 26 {
1664 self.alarms_state.table.page_size = PageSize::OneHundred;
1665 } else if idx == 29 {
1666 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1667 }
1668 } else if self.current_service == Service::EcrRepositories {
1669 if self.ecr_state.current_repository.is_some() {
1670 let idx = self.column_selector_index;
1672 if let Some(col) = self.ecr_image_column_ids.get(idx) {
1673 if let Some(pos) = self
1674 .ecr_image_visible_column_ids
1675 .iter()
1676 .position(|c| c == col)
1677 {
1678 self.ecr_image_visible_column_ids.remove(pos);
1679 } else {
1680 self.ecr_image_visible_column_ids.push(*col);
1681 }
1682 }
1683 } else {
1684 let idx = self.column_selector_index;
1686 if idx > 0 && idx <= self.ecr_repo_column_ids.len() {
1687 if let Some(col) = self.ecr_repo_column_ids.get(idx - 1) {
1688 if let Some(pos) = self
1689 .ecr_repo_visible_column_ids
1690 .iter()
1691 .position(|c| c == col)
1692 {
1693 self.ecr_repo_visible_column_ids.remove(pos);
1694 } else {
1695 self.ecr_repo_visible_column_ids.push(*col);
1696 }
1697 }
1698 } else if idx == self.ecr_repo_column_ids.len() + 3 {
1699 self.ecr_state.repositories.page_size = PageSize::Ten;
1700 } else if idx == self.ecr_repo_column_ids.len() + 4 {
1701 self.ecr_state.repositories.page_size = PageSize::TwentyFive;
1702 } else if idx == self.ecr_repo_column_ids.len() + 5 {
1703 self.ecr_state.repositories.page_size = PageSize::Fifty;
1704 } else if idx == self.ecr_repo_column_ids.len() + 6 {
1705 self.ecr_state.repositories.page_size = PageSize::OneHundred;
1706 }
1707 }
1708 } else if self.current_service == Service::Ec2Instances {
1709 if self.ec2_state.current_instance.is_some()
1710 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1711 {
1712 let idx = self.column_selector_index;
1713 if idx > 0 && idx <= self.ec2_state.tag_column_ids.len() {
1714 if let Some(col) = self.ec2_state.tag_column_ids.get(idx - 1) {
1715 if let Some(pos) = self
1716 .ec2_state
1717 .tag_visible_column_ids
1718 .iter()
1719 .position(|c| c == col)
1720 {
1721 self.ec2_state.tag_visible_column_ids.remove(pos);
1722 } else {
1723 self.ec2_state.tag_visible_column_ids.push(col.clone());
1724 }
1725 }
1726 } else if idx == self.ec2_state.tag_column_ids.len() + 3 {
1727 self.ec2_state.tags.page_size = PageSize::Ten;
1728 } else if idx == self.ec2_state.tag_column_ids.len() + 4 {
1729 self.ec2_state.tags.page_size = PageSize::TwentyFive;
1730 } else if idx == self.ec2_state.tag_column_ids.len() + 5 {
1731 self.ec2_state.tags.page_size = PageSize::Fifty;
1732 } else if idx == self.ec2_state.tag_column_ids.len() + 6 {
1733 self.ec2_state.tags.page_size = PageSize::OneHundred;
1734 }
1735 } else {
1736 let idx = self.column_selector_index;
1737 if idx > 0 && idx <= self.ec2_column_ids.len() {
1738 if let Some(col) = self.ec2_column_ids.get(idx - 1) {
1739 if let Some(pos) =
1740 self.ec2_visible_column_ids.iter().position(|c| c == col)
1741 {
1742 self.ec2_visible_column_ids.remove(pos);
1743 } else {
1744 self.ec2_visible_column_ids.push(*col);
1745 }
1746 }
1747 } else if idx == self.ec2_column_ids.len() + 3 {
1748 self.ec2_state.table.page_size = PageSize::Ten;
1749 } else if idx == self.ec2_column_ids.len() + 4 {
1750 self.ec2_state.table.page_size = PageSize::TwentyFive;
1751 } else if idx == self.ec2_column_ids.len() + 5 {
1752 self.ec2_state.table.page_size = PageSize::Fifty;
1753 } else if idx == self.ec2_column_ids.len() + 6 {
1754 self.ec2_state.table.page_size = PageSize::OneHundred;
1755 }
1756 }
1757 } else if self.current_service == Service::SqsQueues {
1758 if self.sqs_state.current_queue.is_some()
1759 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1760 {
1761 let idx = self.column_selector_index;
1763 if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
1764 if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
1765 if let Some(pos) = self
1766 .sqs_state
1767 .trigger_visible_column_ids
1768 .iter()
1769 .position(|c| c == col)
1770 {
1771 self.sqs_state.trigger_visible_column_ids.remove(pos);
1772 } else {
1773 self.sqs_state.trigger_visible_column_ids.push(col.clone());
1774 }
1775 }
1776 } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
1777 self.sqs_state.triggers.page_size = PageSize::Ten;
1778 } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
1779 self.sqs_state.triggers.page_size = PageSize::TwentyFive;
1780 } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
1781 self.sqs_state.triggers.page_size = PageSize::Fifty;
1782 } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
1783 self.sqs_state.triggers.page_size = PageSize::OneHundred;
1784 }
1785 } else if self.sqs_state.current_queue.is_some()
1786 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1787 {
1788 let idx = self.column_selector_index;
1790 if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
1791 if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
1792 if let Some(pos) = self
1793 .sqs_state
1794 .pipe_visible_column_ids
1795 .iter()
1796 .position(|c| c == col)
1797 {
1798 self.sqs_state.pipe_visible_column_ids.remove(pos);
1799 } else {
1800 self.sqs_state.pipe_visible_column_ids.push(col.clone());
1801 }
1802 }
1803 } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
1804 self.sqs_state.pipes.page_size = PageSize::Ten;
1805 } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
1806 self.sqs_state.pipes.page_size = PageSize::TwentyFive;
1807 } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
1808 self.sqs_state.pipes.page_size = PageSize::Fifty;
1809 } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
1810 self.sqs_state.pipes.page_size = PageSize::OneHundred;
1811 }
1812 } else if self.sqs_state.current_queue.is_some()
1813 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1814 {
1815 let idx = self.column_selector_index;
1817 if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
1818 if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
1819 if let Some(pos) = self
1820 .sqs_state
1821 .tag_visible_column_ids
1822 .iter()
1823 .position(|c| c == col)
1824 {
1825 self.sqs_state.tag_visible_column_ids.remove(pos);
1826 } else {
1827 self.sqs_state.tag_visible_column_ids.push(col.clone());
1828 }
1829 }
1830 } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
1831 self.sqs_state.tags.page_size = PageSize::Ten;
1832 } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
1833 self.sqs_state.tags.page_size = PageSize::TwentyFive;
1834 } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
1835 self.sqs_state.tags.page_size = PageSize::Fifty;
1836 } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
1837 self.sqs_state.tags.page_size = PageSize::OneHundred;
1838 }
1839 } else if self.sqs_state.current_queue.is_some()
1840 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
1841 {
1842 let idx = self.column_selector_index;
1844 if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
1845 if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
1846 if let Some(pos) = self
1847 .sqs_state
1848 .subscription_visible_column_ids
1849 .iter()
1850 .position(|c| c == col)
1851 {
1852 self.sqs_state.subscription_visible_column_ids.remove(pos);
1853 } else {
1854 self.sqs_state
1855 .subscription_visible_column_ids
1856 .push(col.clone());
1857 }
1858 }
1859 } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
1860 self.sqs_state.subscriptions.page_size = PageSize::Ten;
1861 } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
1862 self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
1863 } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
1864 self.sqs_state.subscriptions.page_size = PageSize::Fifty;
1865 } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
1866 self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
1867 }
1868 } else {
1869 let idx = self.column_selector_index;
1871 if let Some(col) = self.sqs_column_ids.get(idx) {
1872 if let Some(pos) =
1873 self.sqs_visible_column_ids.iter().position(|c| c == col)
1874 {
1875 self.sqs_visible_column_ids.remove(pos);
1876 } else {
1877 self.sqs_visible_column_ids.push(*col);
1878 }
1879 } else if idx == self.sqs_column_ids.len() + 2 {
1880 self.sqs_state.queues.page_size = PageSize::Ten;
1881 } else if idx == self.sqs_column_ids.len() + 3 {
1882 self.sqs_state.queues.page_size = PageSize::TwentyFive;
1883 } else if idx == self.sqs_column_ids.len() + 4 {
1884 self.sqs_state.queues.page_size = PageSize::Fifty;
1885 } else if idx == self.sqs_column_ids.len() + 5 {
1886 self.sqs_state.queues.page_size = PageSize::OneHundred;
1887 }
1888 }
1889 } else if self.current_service == Service::LambdaFunctions {
1890 let idx = self.column_selector_index;
1891 if self.lambda_state.current_function.is_some()
1893 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1894 {
1895 if idx > 0 && idx <= self.lambda_state.version_column_ids.len() {
1897 if let Some(col) = self.lambda_state.version_column_ids.get(idx - 1) {
1898 if let Some(pos) = self
1899 .lambda_state
1900 .version_visible_column_ids
1901 .iter()
1902 .position(|c| *c == *col)
1903 {
1904 self.lambda_state.version_visible_column_ids.remove(pos);
1905 } else {
1906 self.lambda_state
1907 .version_visible_column_ids
1908 .push(col.clone());
1909 }
1910 }
1911 } else if idx == self.lambda_state.version_column_ids.len() + 3 {
1912 self.lambda_state.version_table.page_size = PageSize::Ten;
1913 } else if idx == self.lambda_state.version_column_ids.len() + 4 {
1914 self.lambda_state.version_table.page_size = PageSize::TwentyFive;
1915 } else if idx == self.lambda_state.version_column_ids.len() + 5 {
1916 self.lambda_state.version_table.page_size = PageSize::Fifty;
1917 } else if idx == self.lambda_state.version_column_ids.len() + 6 {
1918 self.lambda_state.version_table.page_size = PageSize::OneHundred;
1919 }
1920 } else if (self.lambda_state.current_function.is_some()
1921 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases)
1922 || (self.lambda_state.current_version.is_some()
1923 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration)
1924 {
1925 if idx > 0 && idx <= self.lambda_state.alias_column_ids.len() {
1927 if let Some(col) = self.lambda_state.alias_column_ids.get(idx - 1) {
1928 if let Some(pos) = self
1929 .lambda_state
1930 .alias_visible_column_ids
1931 .iter()
1932 .position(|c| *c == *col)
1933 {
1934 self.lambda_state.alias_visible_column_ids.remove(pos);
1935 } else {
1936 self.lambda_state.alias_visible_column_ids.push(col.clone());
1937 }
1938 }
1939 } else if idx == self.lambda_state.alias_column_ids.len() + 3 {
1940 self.lambda_state.alias_table.page_size = PageSize::Ten;
1941 } else if idx == self.lambda_state.alias_column_ids.len() + 4 {
1942 self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
1943 } else if idx == self.lambda_state.alias_column_ids.len() + 5 {
1944 self.lambda_state.alias_table.page_size = PageSize::Fifty;
1945 } else if idx == self.lambda_state.alias_column_ids.len() + 6 {
1946 self.lambda_state.alias_table.page_size = PageSize::OneHundred;
1947 }
1948 } else {
1949 if idx > 0 && idx <= self.lambda_state.function_column_ids.len() {
1951 if let Some(col) = self.lambda_state.function_column_ids.get(idx - 1) {
1952 if let Some(pos) = self
1953 .lambda_state
1954 .function_visible_column_ids
1955 .iter()
1956 .position(|c| *c == *col)
1957 {
1958 self.lambda_state.function_visible_column_ids.remove(pos);
1959 } else {
1960 self.lambda_state.function_visible_column_ids.push(*col);
1961 }
1962 }
1963 } else if idx == self.lambda_state.function_column_ids.len() + 3 {
1964 self.lambda_state.table.page_size = PageSize::Ten;
1965 } else if idx == self.lambda_state.function_column_ids.len() + 4 {
1966 self.lambda_state.table.page_size = PageSize::TwentyFive;
1967 } else if idx == self.lambda_state.function_column_ids.len() + 5 {
1968 self.lambda_state.table.page_size = PageSize::Fifty;
1969 } else if idx == self.lambda_state.function_column_ids.len() + 6 {
1970 self.lambda_state.table.page_size = PageSize::OneHundred;
1971 }
1972 }
1973 } else if self.current_service == Service::LambdaApplications {
1974 if self.lambda_application_state.current_application.is_some() {
1975 if self.lambda_application_state.detail_tab
1977 == LambdaApplicationDetailTab::Overview
1978 {
1979 let idx = self.column_selector_index;
1981 if idx > 0 && idx <= self.lambda_resource_column_ids.len() {
1982 if let Some(col) = self.lambda_resource_column_ids.get(idx - 1) {
1983 if let Some(pos) = self
1984 .lambda_resource_visible_column_ids
1985 .iter()
1986 .position(|c| c == col)
1987 {
1988 self.lambda_resource_visible_column_ids.remove(pos);
1989 } else {
1990 self.lambda_resource_visible_column_ids.push(*col);
1991 }
1992 }
1993 } else if idx == self.lambda_resource_column_ids.len() + 3 {
1994 self.lambda_application_state.resources.page_size = PageSize::Ten;
1995 } else if idx == self.lambda_resource_column_ids.len() + 4 {
1996 self.lambda_application_state.resources.page_size =
1997 PageSize::TwentyFive;
1998 } else if idx == self.lambda_resource_column_ids.len() + 5 {
1999 self.lambda_application_state.resources.page_size = PageSize::Fifty;
2000 }
2001 } else {
2002 let idx = self.column_selector_index;
2004 if idx > 0 && idx <= self.lambda_deployment_column_ids.len() {
2005 if let Some(col) = self.lambda_deployment_column_ids.get(idx - 1) {
2006 if let Some(pos) = self
2007 .lambda_deployment_visible_column_ids
2008 .iter()
2009 .position(|c| c == col)
2010 {
2011 self.lambda_deployment_visible_column_ids.remove(pos);
2012 } else {
2013 self.lambda_deployment_visible_column_ids.push(*col);
2014 }
2015 }
2016 } else if idx == self.lambda_deployment_column_ids.len() + 3 {
2017 self.lambda_application_state.deployments.page_size = PageSize::Ten;
2018 } else if idx == self.lambda_deployment_column_ids.len() + 4 {
2019 self.lambda_application_state.deployments.page_size =
2020 PageSize::TwentyFive;
2021 } else if idx == self.lambda_deployment_column_ids.len() + 5 {
2022 self.lambda_application_state.deployments.page_size =
2023 PageSize::Fifty;
2024 }
2025 }
2026 } else {
2027 let idx = self.column_selector_index;
2029 if idx > 0 && idx <= self.lambda_application_column_ids.len() {
2030 if let Some(col) = self.lambda_application_column_ids.get(idx - 1) {
2031 if let Some(pos) = self
2032 .lambda_application_visible_column_ids
2033 .iter()
2034 .position(|c| *c == *col)
2035 {
2036 self.lambda_application_visible_column_ids.remove(pos);
2037 } else {
2038 self.lambda_application_visible_column_ids.push(*col);
2039 }
2040 }
2041 } else if idx == self.lambda_application_column_ids.len() + 3 {
2042 self.lambda_application_state.table.page_size = PageSize::Ten;
2043 } else if idx == self.lambda_application_column_ids.len() + 4 {
2044 self.lambda_application_state.table.page_size = PageSize::TwentyFive;
2045 } else if idx == self.lambda_application_column_ids.len() + 5 {
2046 self.lambda_application_state.table.page_size = PageSize::Fifty;
2047 }
2048 }
2049 } else if self.view_mode == ViewMode::Events {
2050 if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
2051 {
2052 if let Some(pos) = self
2053 .cw_log_event_visible_column_ids
2054 .iter()
2055 .position(|c| c == col)
2056 {
2057 self.cw_log_event_visible_column_ids.remove(pos);
2058 } else {
2059 self.cw_log_event_visible_column_ids.push(*col);
2060 }
2061 }
2062 } else if self.view_mode == ViewMode::Detail {
2063 let idx = self.column_selector_index;
2064 if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
2065 if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
2066 if let Some(pos) = self
2067 .cw_log_stream_visible_column_ids
2068 .iter()
2069 .position(|c| c == col)
2070 {
2071 self.cw_log_stream_visible_column_ids.remove(pos);
2072 } else {
2073 self.cw_log_stream_visible_column_ids.push(*col);
2074 }
2075 }
2076 } else if idx == self.cw_log_stream_column_ids.len() + 3 {
2077 self.log_groups_state.stream_page_size = 10;
2078 self.log_groups_state.stream_current_page = 0;
2079 } else if idx == self.cw_log_stream_column_ids.len() + 4 {
2080 self.log_groups_state.stream_page_size = 25;
2081 self.log_groups_state.stream_current_page = 0;
2082 } else if idx == self.cw_log_stream_column_ids.len() + 5 {
2083 self.log_groups_state.stream_page_size = 50;
2084 self.log_groups_state.stream_current_page = 0;
2085 } else if idx == self.cw_log_stream_column_ids.len() + 6 {
2086 self.log_groups_state.stream_page_size = 100;
2087 self.log_groups_state.stream_current_page = 0;
2088 }
2089 } else if self.current_service == Service::CloudFormationStacks {
2090 let idx = self.column_selector_index;
2091 if self.cfn_state.current_stack.is_some()
2093 && self.cfn_state.detail_tab == CfnDetailTab::StackInfo
2094 {
2095 if idx == 4 {
2097 self.cfn_state.tags.page_size = PageSize::Ten;
2098 } else if idx == 5 {
2099 self.cfn_state.tags.page_size = PageSize::TwentyFive;
2100 } else if idx == 6 {
2101 self.cfn_state.tags.page_size = PageSize::Fifty;
2102 } else if idx == 7 {
2103 self.cfn_state.tags.page_size = PageSize::OneHundred;
2104 }
2105 } else if self.cfn_state.current_stack.is_some()
2106 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2107 {
2108 if idx > 0 && idx <= self.cfn_parameter_column_ids.len() {
2109 if let Some(col) = self.cfn_parameter_column_ids.get(idx - 1) {
2110 if let Some(pos) = self
2111 .cfn_parameter_visible_column_ids
2112 .iter()
2113 .position(|c| c == col)
2114 {
2115 self.cfn_parameter_visible_column_ids.remove(pos);
2116 } else {
2117 self.cfn_parameter_visible_column_ids.push(col);
2118 }
2119 }
2120 } else if idx == self.cfn_parameter_column_ids.len() + 3 {
2121 self.cfn_state.parameters.page_size = PageSize::Ten;
2122 } else if idx == self.cfn_parameter_column_ids.len() + 4 {
2123 self.cfn_state.parameters.page_size = PageSize::TwentyFive;
2124 } else if idx == self.cfn_parameter_column_ids.len() + 5 {
2125 self.cfn_state.parameters.page_size = PageSize::Fifty;
2126 } else if idx == self.cfn_parameter_column_ids.len() + 6 {
2127 self.cfn_state.parameters.page_size = PageSize::OneHundred;
2128 }
2129 } else if self.cfn_state.current_stack.is_some()
2130 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2131 {
2132 if idx > 0 && idx <= self.cfn_output_column_ids.len() {
2133 if let Some(col) = self.cfn_output_column_ids.get(idx - 1) {
2134 if let Some(pos) = self
2135 .cfn_output_visible_column_ids
2136 .iter()
2137 .position(|c| c == col)
2138 {
2139 self.cfn_output_visible_column_ids.remove(pos);
2140 } else {
2141 self.cfn_output_visible_column_ids.push(col);
2142 }
2143 }
2144 } else if idx == self.cfn_output_column_ids.len() + 3 {
2145 self.cfn_state.outputs.page_size = PageSize::Ten;
2146 } else if idx == self.cfn_output_column_ids.len() + 4 {
2147 self.cfn_state.outputs.page_size = PageSize::TwentyFive;
2148 } else if idx == self.cfn_output_column_ids.len() + 5 {
2149 self.cfn_state.outputs.page_size = PageSize::Fifty;
2150 } else if idx == self.cfn_output_column_ids.len() + 6 {
2151 self.cfn_state.outputs.page_size = PageSize::OneHundred;
2152 }
2153 } else if self.cfn_state.current_stack.is_some()
2154 && self.cfn_state.detail_tab == CfnDetailTab::Resources
2155 {
2156 if idx > 0 && idx <= self.cfn_resource_column_ids.len() {
2157 if let Some(col) = self.cfn_resource_column_ids.get(idx - 1) {
2158 if let Some(pos) = self
2159 .cfn_resource_visible_column_ids
2160 .iter()
2161 .position(|c| c == col)
2162 {
2163 self.cfn_resource_visible_column_ids.remove(pos);
2164 } else {
2165 self.cfn_resource_visible_column_ids.push(col);
2166 }
2167 }
2168 } else if idx == self.cfn_resource_column_ids.len() + 3 {
2169 self.cfn_state.resources.page_size = PageSize::Ten;
2170 } else if idx == self.cfn_resource_column_ids.len() + 4 {
2171 self.cfn_state.resources.page_size = PageSize::TwentyFive;
2172 } else if idx == self.cfn_resource_column_ids.len() + 5 {
2173 self.cfn_state.resources.page_size = PageSize::Fifty;
2174 } else if idx == self.cfn_resource_column_ids.len() + 6 {
2175 self.cfn_state.resources.page_size = PageSize::OneHundred;
2176 }
2177 } else if self.cfn_state.current_stack.is_none() {
2178 if idx > 0 && idx <= self.cfn_column_ids.len() {
2180 if let Some(col) = self.cfn_column_ids.get(idx - 1) {
2181 if let Some(pos) =
2182 self.cfn_visible_column_ids.iter().position(|c| c == col)
2183 {
2184 self.cfn_visible_column_ids.remove(pos);
2185 } else {
2186 self.cfn_visible_column_ids.push(*col);
2187 }
2188 }
2189 } else if idx == self.cfn_column_ids.len() + 3 {
2190 self.cfn_state.table.page_size = PageSize::Ten;
2191 } else if idx == self.cfn_column_ids.len() + 4 {
2192 self.cfn_state.table.page_size = PageSize::TwentyFive;
2193 } else if idx == self.cfn_column_ids.len() + 5 {
2194 self.cfn_state.table.page_size = PageSize::Fifty;
2195 } else if idx == self.cfn_column_ids.len() + 6 {
2196 self.cfn_state.table.page_size = PageSize::OneHundred;
2197 }
2198 }
2199 } else if self.current_service == Service::IamUsers {
2201 let idx = self.column_selector_index;
2202 if self.iam_state.current_user.is_some() {
2203 match self.iam_state.user_tab {
2204 UserTab::Permissions => {
2205 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
2207 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
2208 if let Some(pos) = self
2209 .iam_policy_visible_column_ids
2210 .iter()
2211 .position(|c| c == col)
2212 {
2213 self.iam_policy_visible_column_ids.remove(pos);
2214 } else {
2215 self.iam_policy_visible_column_ids.push(col.clone());
2216 }
2217 }
2218 } else if idx == self.iam_policy_column_ids.len() + 3 {
2219 self.iam_state.policies.page_size = PageSize::Ten;
2220 } else if idx == self.iam_policy_column_ids.len() + 4 {
2221 self.iam_state.policies.page_size = PageSize::TwentyFive;
2222 } else if idx == self.iam_policy_column_ids.len() + 5 {
2223 self.iam_state.policies.page_size = PageSize::Fifty;
2224 }
2225 }
2226 UserTab::Groups => {
2227 toggle_iam_page_size_only(
2228 idx,
2229 5,
2230 &mut self.iam_state.user_group_memberships.page_size,
2231 );
2232 }
2233 UserTab::Tags => {
2234 toggle_iam_page_size_only(
2235 idx,
2236 5,
2237 &mut self.iam_state.user_tags.page_size,
2238 );
2239 }
2240 UserTab::LastAccessed => {
2241 toggle_iam_page_size_only(
2242 idx,
2243 6,
2244 &mut self.iam_state.last_accessed_services.page_size,
2245 );
2246 }
2247 _ => {}
2248 }
2249 } else {
2250 toggle_iam_preference_static(
2252 idx,
2253 &self.iam_user_column_ids,
2254 &mut self.iam_user_visible_column_ids,
2255 &mut self.iam_state.users.page_size,
2256 );
2257 }
2258 } else if self.current_service == Service::IamRoles {
2259 let idx = self.column_selector_index;
2260 if self.iam_state.current_role.is_some() {
2261 match self.iam_state.role_tab {
2262 RoleTab::Permissions => {
2263 toggle_iam_preference(
2265 idx,
2266 &self.iam_policy_column_ids,
2267 &mut self.iam_policy_visible_column_ids,
2268 &mut self.iam_state.policies.page_size,
2269 );
2270 }
2271 RoleTab::LastAccessed => {
2272 toggle_iam_page_size_only(
2274 idx,
2275 6,
2276 &mut self.iam_state.last_accessed_services.page_size,
2277 );
2278 }
2279 _ => {}
2280 }
2281 } else {
2282 toggle_iam_preference_static(
2284 idx,
2285 &self.iam_role_column_ids,
2286 &mut self.iam_role_visible_column_ids,
2287 &mut self.iam_state.roles.page_size,
2288 );
2289 }
2290 } else if self.current_service == Service::IamUserGroups {
2291 toggle_iam_preference(
2292 self.column_selector_index,
2293 &self.iam_group_column_ids,
2294 &mut self.iam_group_visible_column_ids,
2295 &mut self.iam_state.groups.page_size,
2296 );
2297 } else {
2298 let idx = self.column_selector_index;
2299 if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
2300 if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
2301 if let Some(pos) = self
2302 .cw_log_group_visible_column_ids
2303 .iter()
2304 .position(|c| c == col)
2305 {
2306 self.cw_log_group_visible_column_ids.remove(pos);
2307 } else {
2308 self.cw_log_group_visible_column_ids.push(*col);
2309 }
2310 }
2311 } else if idx == self.cw_log_group_column_ids.len() + 3 {
2312 self.log_groups_state.log_groups.page_size = PageSize::Ten;
2313 } else if idx == self.cw_log_group_column_ids.len() + 4 {
2314 self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
2315 } else if idx == self.cw_log_group_column_ids.len() + 5 {
2316 self.log_groups_state.log_groups.page_size = PageSize::Fifty;
2317 } else if idx == self.cw_log_group_column_ids.len() + 6 {
2318 self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2319 }
2320 }
2321 }
2322 Action::NextPreferences => {
2323 if self.current_service == Service::CloudWatchAlarms {
2324 if self.column_selector_index < 18 {
2326 self.column_selector_index = 18; } else if self.column_selector_index < 22 {
2328 self.column_selector_index = 22; } else if self.column_selector_index < 28 {
2330 self.column_selector_index = 28; } else {
2332 self.column_selector_index = 0; }
2334 } else if self.current_service == Service::EcrRepositories
2335 && self.ecr_state.current_repository.is_some()
2336 {
2337 let page_size_idx = self.ecr_image_column_ids.len() + 2;
2339 if self.column_selector_index < page_size_idx {
2340 self.column_selector_index = page_size_idx;
2341 } else {
2342 self.column_selector_index = 0;
2343 }
2344 } else if self.current_service == Service::LambdaFunctions {
2345 let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2347 if self.column_selector_index < page_size_idx {
2348 self.column_selector_index = page_size_idx;
2349 } else {
2350 self.column_selector_index = 0;
2351 }
2352 } else if self.current_service == Service::LambdaApplications {
2353 let page_size_idx = self.lambda_application_column_ids.len() + 2;
2355 if self.column_selector_index < page_size_idx {
2356 self.column_selector_index = page_size_idx;
2357 } else {
2358 self.column_selector_index = 0;
2359 }
2360 } else if self.current_service == Service::CloudFormationStacks {
2361 let page_size_idx = self.cfn_column_ids.len() + 2;
2363 if self.column_selector_index < page_size_idx {
2364 self.column_selector_index = page_size_idx;
2365 } else {
2366 self.column_selector_index = 0;
2367 }
2368 } else if self.current_service == Service::Ec2Instances {
2369 let page_size_idx = self.ec2_column_ids.len() + 2;
2370 if self.column_selector_index < page_size_idx {
2371 self.column_selector_index = page_size_idx;
2372 } else {
2373 self.column_selector_index = 0;
2374 }
2375 } else if self.current_service == Service::IamUsers {
2376 if self.iam_state.current_user.is_some() {
2377 match self.iam_state.user_tab {
2378 UserTab::Permissions => {
2379 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2381 if self.column_selector_index < page_size_idx {
2382 self.column_selector_index = page_size_idx;
2383 } else {
2384 self.column_selector_index = 0;
2385 }
2386 }
2387 UserTab::Groups | UserTab::Tags => {
2388 if self.column_selector_index < 4 {
2390 self.column_selector_index = 4;
2391 } else {
2392 self.column_selector_index = 0;
2393 }
2394 }
2395 UserTab::LastAccessed => {
2396 if self.column_selector_index < 5 {
2398 self.column_selector_index = 5;
2399 } else {
2400 self.column_selector_index = 0;
2401 }
2402 }
2403 _ => {}
2404 }
2405 } else {
2406 let page_size_idx = self.iam_user_column_ids.len() + 2;
2408 if self.column_selector_index < page_size_idx {
2409 self.column_selector_index = page_size_idx;
2410 } else {
2411 self.column_selector_index = 0;
2412 }
2413 }
2414 } else if self.current_service == Service::IamRoles {
2415 if self.iam_state.current_role.is_some() {
2416 match self.iam_state.role_tab {
2417 RoleTab::Permissions => {
2418 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2420 if self.column_selector_index < page_size_idx {
2421 self.column_selector_index = page_size_idx;
2422 } else {
2423 self.column_selector_index = 0;
2424 }
2425 }
2426 RoleTab::Tags => {
2427 if self.column_selector_index < 4 {
2429 self.column_selector_index = 4;
2430 } else {
2431 self.column_selector_index = 0;
2432 }
2433 }
2434 RoleTab::LastAccessed => {
2435 if self.column_selector_index < 5 {
2437 self.column_selector_index = 5;
2438 } else {
2439 self.column_selector_index = 0;
2440 }
2441 }
2442 _ => {}
2443 }
2444 } else {
2445 let page_size_idx = self.iam_role_column_ids.len() + 2;
2447 if self.column_selector_index < page_size_idx {
2448 self.column_selector_index = page_size_idx;
2449 } else {
2450 self.column_selector_index = 0;
2451 }
2452 }
2453 } else if self.current_service == Service::IamUserGroups {
2454 let page_size_idx = self.iam_group_column_ids.len() + 2;
2456 if self.column_selector_index < page_size_idx {
2457 self.column_selector_index = page_size_idx;
2458 } else {
2459 self.column_selector_index = 0;
2460 }
2461 } else if self.current_service == Service::SqsQueues
2462 && self.sqs_state.current_queue.is_some()
2463 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2464 {
2465 let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2467 if self.column_selector_index < page_size_idx {
2468 self.column_selector_index = page_size_idx;
2469 } else {
2470 self.column_selector_index = 0;
2471 }
2472 } else if self.current_service == Service::SqsQueues
2473 && self.sqs_state.current_queue.is_some()
2474 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2475 {
2476 let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2478 if self.column_selector_index < page_size_idx {
2479 self.column_selector_index = page_size_idx;
2480 } else {
2481 self.column_selector_index = 0;
2482 }
2483 } else if self.current_service == Service::SqsQueues
2484 && self.sqs_state.current_queue.is_some()
2485 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2486 {
2487 let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2489 if self.column_selector_index < page_size_idx {
2490 self.column_selector_index = page_size_idx;
2491 } else {
2492 self.column_selector_index = 0;
2493 }
2494 } else if self.current_service == Service::SqsQueues
2495 && self.sqs_state.current_queue.is_some()
2496 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2497 {
2498 let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2500 if self.column_selector_index < page_size_idx {
2501 self.column_selector_index = page_size_idx;
2502 } else {
2503 self.column_selector_index = 0;
2504 }
2505 } else if self.current_service == Service::S3Buckets
2506 && self.s3_state.current_bucket.is_none()
2507 {
2508 let page_size_idx = self.s3_bucket_column_ids.len() + 2;
2509 if self.column_selector_index < page_size_idx {
2510 self.column_selector_index = page_size_idx;
2511 } else {
2512 self.column_selector_index = 0;
2513 }
2514 }
2515 }
2516 Action::PrevPreferences => {
2517 if self.current_service == Service::CloudWatchAlarms {
2518 if self.column_selector_index >= 28 {
2520 self.column_selector_index = 22;
2521 } else if self.column_selector_index >= 22 {
2522 self.column_selector_index = 18;
2523 } else if self.column_selector_index >= 18 {
2524 self.column_selector_index = 0;
2525 } else {
2526 self.column_selector_index = 28;
2527 }
2528 } else if self.current_service == Service::EcrRepositories
2529 && self.ecr_state.current_repository.is_some()
2530 {
2531 let page_size_idx = self.ecr_image_column_ids.len() + 2;
2532 if self.column_selector_index >= page_size_idx {
2533 self.column_selector_index = 0;
2534 } else {
2535 self.column_selector_index = page_size_idx;
2536 }
2537 } else if self.current_service == Service::LambdaFunctions {
2538 let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2539 if self.column_selector_index >= page_size_idx {
2540 self.column_selector_index = 0;
2541 } else {
2542 self.column_selector_index = page_size_idx;
2543 }
2544 } else if self.current_service == Service::LambdaApplications {
2545 let page_size_idx = self.lambda_application_column_ids.len() + 2;
2546 if self.column_selector_index >= page_size_idx {
2547 self.column_selector_index = 0;
2548 } else {
2549 self.column_selector_index = page_size_idx;
2550 }
2551 } else if self.current_service == Service::CloudFormationStacks {
2552 let page_size_idx = self.cfn_column_ids.len() + 2;
2553 if self.column_selector_index >= page_size_idx {
2554 self.column_selector_index = 0;
2555 } else {
2556 self.column_selector_index = page_size_idx;
2557 }
2558 } else if self.current_service == Service::Ec2Instances {
2559 let page_size_idx = self.ec2_column_ids.len() + 2;
2560 if self.column_selector_index >= page_size_idx {
2561 self.column_selector_index = 0;
2562 } else {
2563 self.column_selector_index = page_size_idx;
2564 }
2565 } else if self.current_service == Service::IamUsers {
2566 if self.iam_state.current_user.is_some() {
2567 match self.iam_state.user_tab {
2568 UserTab::Permissions => {
2569 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2570 if self.column_selector_index >= page_size_idx {
2571 self.column_selector_index = 0;
2572 } else {
2573 self.column_selector_index = page_size_idx;
2574 }
2575 }
2576 UserTab::Groups | UserTab::Tags => {
2577 if self.column_selector_index >= 4 {
2578 self.column_selector_index = 0;
2579 } else {
2580 self.column_selector_index = 4;
2581 }
2582 }
2583 UserTab::LastAccessed => {
2584 if self.column_selector_index >= 5 {
2585 self.column_selector_index = 0;
2586 } else {
2587 self.column_selector_index = 5;
2588 }
2589 }
2590 _ => {}
2591 }
2592 } else {
2593 let page_size_idx = self.iam_user_column_ids.len() + 2;
2594 if self.column_selector_index >= page_size_idx {
2595 self.column_selector_index = 0;
2596 } else {
2597 self.column_selector_index = page_size_idx;
2598 }
2599 }
2600 } else if self.current_service == Service::IamRoles {
2601 if self.iam_state.current_role.is_some() {
2602 match self.iam_state.role_tab {
2603 RoleTab::Permissions => {
2604 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2605 if self.column_selector_index >= page_size_idx {
2606 self.column_selector_index = 0;
2607 } else {
2608 self.column_selector_index = page_size_idx;
2609 }
2610 }
2611 RoleTab::Tags => {
2612 if self.column_selector_index >= 4 {
2613 self.column_selector_index = 0;
2614 } else {
2615 self.column_selector_index = 4;
2616 }
2617 }
2618 RoleTab::LastAccessed => {
2619 if self.column_selector_index >= 5 {
2620 self.column_selector_index = 0;
2621 } else {
2622 self.column_selector_index = 5;
2623 }
2624 }
2625 _ => {}
2626 }
2627 } else {
2628 let page_size_idx = self.iam_role_column_ids.len() + 2;
2629 if self.column_selector_index >= page_size_idx {
2630 self.column_selector_index = 0;
2631 } else {
2632 self.column_selector_index = page_size_idx;
2633 }
2634 }
2635 } else if self.current_service == Service::IamUserGroups {
2636 let page_size_idx = self.iam_group_column_ids.len() + 2;
2637 if self.column_selector_index >= page_size_idx {
2638 self.column_selector_index = 0;
2639 } else {
2640 self.column_selector_index = page_size_idx;
2641 }
2642 } else if self.current_service == Service::SqsQueues
2643 && self.sqs_state.current_queue.is_some()
2644 {
2645 let page_size_idx = match self.sqs_state.detail_tab {
2646 SqsQueueDetailTab::LambdaTriggers => {
2647 self.sqs_state.trigger_column_ids.len() + 2
2648 }
2649 SqsQueueDetailTab::EventBridgePipes => {
2650 self.sqs_state.pipe_column_ids.len() + 2
2651 }
2652 SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
2653 SqsQueueDetailTab::SnsSubscriptions => {
2654 self.sqs_state.subscription_column_ids.len() + 2
2655 }
2656 _ => 0,
2657 };
2658 if page_size_idx > 0 {
2659 if self.column_selector_index >= page_size_idx {
2660 self.column_selector_index = 0;
2661 } else {
2662 self.column_selector_index = page_size_idx;
2663 }
2664 }
2665 } else if self.current_service == Service::S3Buckets
2666 && self.s3_state.current_bucket.is_none()
2667 {
2668 let page_size_idx = self.s3_bucket_column_ids.len() + 2;
2669 if self.column_selector_index >= page_size_idx {
2670 self.column_selector_index = 0;
2671 } else {
2672 self.column_selector_index = page_size_idx;
2673 }
2674 }
2675 }
2676 Action::CloseColumnSelector => {
2677 self.mode = Mode::Normal;
2678 self.preference_section = Preferences::Columns;
2679 }
2680 Action::NextDetailTab => {
2681 if self.current_service == Service::SqsQueues
2682 && self.sqs_state.current_queue.is_some()
2683 {
2684 self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
2685 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2686 self.sqs_state.set_metrics_loading(true);
2687 self.sqs_state.set_monitoring_scroll(0);
2688 self.sqs_state.clear_metrics();
2689 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
2690 self.sqs_state.triggers.loading = true;
2691 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
2692 self.sqs_state.pipes.loading = true;
2693 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
2694 self.sqs_state.tags.loading = true;
2695 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
2696 self.sqs_state.subscriptions.loading = true;
2697 }
2698 } else if self.current_service == Service::Ec2Instances
2699 && self.ec2_state.current_instance.is_some()
2700 {
2701 self.ec2_state.detail_tab = self.ec2_state.detail_tab.next();
2702 if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
2703 self.ec2_state.tags.loading = true;
2704 } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
2705 self.ec2_state.set_metrics_loading(true);
2706 self.ec2_state.set_monitoring_scroll(0);
2707 self.ec2_state.clear_metrics();
2708 }
2709 } else if self.current_service == Service::LambdaApplications
2710 && self.lambda_application_state.current_application.is_some()
2711 {
2712 self.lambda_application_state.detail_tab =
2713 self.lambda_application_state.detail_tab.next();
2714 } else if self.current_service == Service::IamRoles
2715 && self.iam_state.current_role.is_some()
2716 {
2717 self.iam_state.role_tab = self.iam_state.role_tab.next();
2718 if self.iam_state.role_tab == RoleTab::Tags {
2719 self.iam_state.tags.loading = true;
2720 }
2721 } else if self.current_service == Service::IamUsers
2722 && self.iam_state.current_user.is_some()
2723 {
2724 self.iam_state.user_tab = self.iam_state.user_tab.next();
2725 if self.iam_state.user_tab == UserTab::Tags {
2726 self.iam_state.user_tags.loading = true;
2727 }
2728 } else if self.current_service == Service::IamUserGroups
2729 && self.iam_state.current_group.is_some()
2730 {
2731 self.iam_state.group_tab = self.iam_state.group_tab.next();
2732 } else if self.view_mode == ViewMode::Detail {
2733 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
2734 } else if self.current_service == Service::S3Buckets {
2735 if self.s3_state.current_bucket.is_some() {
2736 self.s3_state.object_tab = self.s3_state.object_tab.next();
2737 } else {
2738 self.s3_state.bucket_type = match self.s3_state.bucket_type {
2739 S3BucketType::GeneralPurpose => S3BucketType::Directory,
2740 S3BucketType::Directory => S3BucketType::GeneralPurpose,
2741 };
2742 self.s3_state.buckets.reset();
2743 }
2744 } else if self.current_service == Service::CloudWatchAlarms {
2745 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2746 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2747 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2748 };
2749 self.alarms_state.table.reset();
2750 } else if self.current_service == Service::EcrRepositories
2751 && self.ecr_state.current_repository.is_none()
2752 {
2753 self.ecr_state.tab = self.ecr_state.tab.next();
2754 self.ecr_state.repositories.reset();
2755 self.ecr_state.repositories.loading = true;
2756 } else if self.current_service == Service::LambdaFunctions
2757 && self.lambda_state.current_function.is_some()
2758 {
2759 if self.lambda_state.current_version.is_some() {
2760 self.lambda_state.version_detail_tab =
2762 self.lambda_state.version_detail_tab.next();
2763 self.lambda_state.detail_tab =
2764 self.lambda_state.version_detail_tab.to_detail_tab();
2765 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2766 self.lambda_state.set_metrics_loading(true);
2767 self.lambda_state.set_monitoring_scroll(0);
2768 self.lambda_state.clear_metrics();
2769 }
2770 } else {
2771 self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
2772 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2773 self.lambda_state.set_metrics_loading(true);
2774 self.lambda_state.set_monitoring_scroll(0);
2775 self.lambda_state.clear_metrics();
2776 }
2777 }
2778 } else if self.current_service == Service::CloudFormationStacks
2779 && self.cfn_state.current_stack.is_some()
2780 {
2781 self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
2782 }
2783 }
2784 Action::PrevDetailTab => {
2785 if self.current_service == Service::SqsQueues
2786 && self.sqs_state.current_queue.is_some()
2787 {
2788 self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
2789 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2790 self.sqs_state.set_metrics_loading(true);
2791 self.sqs_state.set_monitoring_scroll(0);
2792 self.sqs_state.clear_metrics();
2793 }
2794 } else if self.current_service == Service::Ec2Instances
2795 && self.ec2_state.current_instance.is_some()
2796 {
2797 self.ec2_state.detail_tab = self.ec2_state.detail_tab.prev();
2798 if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
2799 self.ec2_state.tags.loading = true;
2800 } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
2801 self.ec2_state.set_metrics_loading(true);
2802 self.ec2_state.set_monitoring_scroll(0);
2803 self.ec2_state.clear_metrics();
2804 }
2805 } else if self.current_service == Service::LambdaApplications
2806 && self.lambda_application_state.current_application.is_some()
2807 {
2808 self.lambda_application_state.detail_tab =
2809 self.lambda_application_state.detail_tab.prev();
2810 } else if self.current_service == Service::IamRoles
2811 && self.iam_state.current_role.is_some()
2812 {
2813 self.iam_state.role_tab = self.iam_state.role_tab.prev();
2814 } else if self.current_service == Service::IamUsers
2815 && self.iam_state.current_user.is_some()
2816 {
2817 self.iam_state.user_tab = self.iam_state.user_tab.prev();
2818 } else if self.current_service == Service::IamUserGroups
2819 && self.iam_state.current_group.is_some()
2820 {
2821 self.iam_state.group_tab = self.iam_state.group_tab.prev();
2822 } else if self.view_mode == ViewMode::Detail {
2823 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
2824 } else if self.current_service == Service::S3Buckets {
2825 if self.s3_state.current_bucket.is_some() {
2826 self.s3_state.object_tab = self.s3_state.object_tab.prev();
2827 }
2828 } else if self.current_service == Service::CloudWatchAlarms {
2829 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2830 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2831 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2832 };
2833 } else if self.current_service == Service::EcrRepositories
2834 && self.ecr_state.current_repository.is_none()
2835 {
2836 self.ecr_state.tab = self.ecr_state.tab.prev();
2837 self.ecr_state.repositories.reset();
2838 self.ecr_state.repositories.loading = true;
2839 } else if self.current_service == Service::LambdaFunctions
2840 && self.lambda_state.current_function.is_some()
2841 {
2842 if self.lambda_state.current_version.is_some() {
2843 self.lambda_state.version_detail_tab =
2845 self.lambda_state.version_detail_tab.prev();
2846 self.lambda_state.detail_tab =
2847 self.lambda_state.version_detail_tab.to_detail_tab();
2848 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2849 self.lambda_state.set_metrics_loading(true);
2850 self.lambda_state.set_monitoring_scroll(0);
2851 self.lambda_state.clear_metrics();
2852 }
2853 } else {
2854 self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
2855 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
2856 self.lambda_state.set_metrics_loading(true);
2857 self.lambda_state.set_monitoring_scroll(0);
2858 self.lambda_state.clear_metrics();
2859 }
2860 }
2861 } else if self.current_service == Service::CloudFormationStacks
2862 && self.cfn_state.current_stack.is_some()
2863 {
2864 self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
2865 }
2866 }
2867 Action::StartFilter => {
2868 if !self.service_selected && self.tabs.is_empty() {
2870 return;
2871 }
2872
2873 if self.current_service == Service::CloudWatchInsights {
2874 self.mode = Mode::InsightsInput;
2875 } else if self.current_service == Service::CloudWatchAlarms {
2876 self.mode = Mode::FilterInput;
2877 } else if self.current_service == Service::S3Buckets {
2878 self.mode = Mode::FilterInput;
2879 self.log_groups_state.filter_mode = true;
2880 } else if self.current_service == Service::EcrRepositories
2881 || self.current_service == Service::IamUsers
2882 || self.current_service == Service::IamUserGroups
2883 {
2884 self.mode = Mode::FilterInput;
2885 if self.current_service == Service::EcrRepositories
2886 && self.ecr_state.current_repository.is_none()
2887 {
2888 self.ecr_state.input_focus = InputFocus::Filter;
2889 }
2890 } else if self.current_service == Service::LambdaFunctions {
2891 self.mode = Mode::FilterInput;
2892 if self.lambda_state.current_version.is_some()
2893 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2894 {
2895 self.lambda_state.alias_input_focus = InputFocus::Filter;
2896 } else if self.lambda_state.current_function.is_some()
2897 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2898 {
2899 self.lambda_state.version_input_focus = InputFocus::Filter;
2900 } else if self.lambda_state.current_function.is_none() {
2901 self.lambda_state.input_focus = InputFocus::Filter;
2902 }
2903 } else if self.current_service == Service::LambdaApplications {
2904 self.mode = Mode::FilterInput;
2905 if self.lambda_application_state.current_application.is_some() {
2906 if self.lambda_application_state.detail_tab
2908 == LambdaApplicationDetailTab::Overview
2909 {
2910 self.lambda_application_state.resource_input_focus = InputFocus::Filter;
2911 } else {
2912 self.lambda_application_state.deployment_input_focus =
2913 InputFocus::Filter;
2914 }
2915 } else {
2916 self.lambda_application_state.input_focus = InputFocus::Filter;
2917 }
2918 } else if self.current_service == Service::IamRoles {
2919 self.mode = Mode::FilterInput;
2920 } else if self.current_service == Service::CloudFormationStacks {
2921 self.mode = Mode::FilterInput;
2922 if self.cfn_state.current_stack.is_some()
2923 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2924 {
2925 self.cfn_state.parameters_input_focus = InputFocus::Filter;
2926 } else if self.cfn_state.current_stack.is_some()
2927 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2928 {
2929 self.cfn_state.outputs_input_focus = InputFocus::Filter;
2930 } else {
2931 self.cfn_state.input_focus = InputFocus::Filter;
2932 }
2933 } else if self.current_service == Service::SqsQueues {
2934 self.mode = Mode::FilterInput;
2935 self.sqs_state.input_focus = InputFocus::Filter;
2936 } else if self.view_mode == ViewMode::List
2937 || (self.view_mode == ViewMode::Detail
2938 && self.log_groups_state.detail_tab == DetailTab::LogStreams)
2939 {
2940 self.mode = Mode::FilterInput;
2941 self.log_groups_state.filter_mode = true;
2942 self.log_groups_state.input_focus = InputFocus::Filter;
2943 } else if self.view_mode == ViewMode::Events {
2944 self.mode = Mode::EventFilterInput;
2945 }
2946 }
2947 Action::StartEventFilter => {
2948 if self.current_service == Service::CloudWatchInsights {
2949 self.mode = Mode::InsightsInput;
2950 } else if self.view_mode == ViewMode::List {
2951 self.mode = Mode::FilterInput;
2952 self.log_groups_state.filter_mode = true;
2953 self.log_groups_state.input_focus = InputFocus::Filter;
2954 } else if self.view_mode == ViewMode::Events {
2955 self.mode = Mode::EventFilterInput;
2956 } else if self.view_mode == ViewMode::Detail
2957 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2958 {
2959 self.mode = Mode::FilterInput;
2960 self.log_groups_state.filter_mode = true;
2961 self.log_groups_state.input_focus = InputFocus::Filter;
2962 }
2963 }
2964 Action::NextFilterFocus => {
2965 if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
2966 const S3_FILTER_CONTROLS: [InputFocus; 2] =
2967 [InputFocus::Filter, InputFocus::Pagination];
2968 self.s3_state.input_focus = self.s3_state.input_focus.next(&S3_FILTER_CONTROLS);
2969 } else if self.mode == Mode::FilterInput
2970 && self.current_service == Service::Ec2Instances
2971 {
2972 self.ec2_state.input_focus =
2973 self.ec2_state.input_focus.next(&ec2::FILTER_CONTROLS);
2974 } else if self.mode == Mode::FilterInput
2975 && self.current_service == Service::LambdaApplications
2976 {
2977 use crate::ui::lambda::FILTER_CONTROLS;
2978 if self.lambda_application_state.current_application.is_some() {
2979 if self.lambda_application_state.detail_tab
2980 == LambdaApplicationDetailTab::Deployments
2981 {
2982 self.lambda_application_state.deployment_input_focus = self
2983 .lambda_application_state
2984 .deployment_input_focus
2985 .next(&FILTER_CONTROLS);
2986 } else {
2987 self.lambda_application_state.resource_input_focus = self
2988 .lambda_application_state
2989 .resource_input_focus
2990 .next(&FILTER_CONTROLS);
2991 }
2992 } else {
2993 self.lambda_application_state.input_focus = self
2994 .lambda_application_state
2995 .input_focus
2996 .next(&FILTER_CONTROLS);
2997 }
2998 } else if self.mode == Mode::FilterInput
2999 && self.current_service == Service::IamRoles
3000 && self.iam_state.current_role.is_some()
3001 {
3002 use crate::ui::iam::POLICY_FILTER_CONTROLS;
3003 self.iam_state.policy_input_focus = self
3004 .iam_state
3005 .policy_input_focus
3006 .next(&POLICY_FILTER_CONTROLS);
3007 } else if self.mode == Mode::FilterInput
3008 && self.current_service == Service::IamRoles
3009 && self.iam_state.current_role.is_none()
3010 {
3011 use crate::ui::iam::ROLE_FILTER_CONTROLS;
3012 self.iam_state.role_input_focus =
3013 self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
3014 } else if self.mode == Mode::FilterInput
3015 && self.current_service == Service::IamUsers
3016 && self.iam_state.current_user.is_some()
3017 {
3018 use crate::ui::iam::{
3019 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3020 USER_SIMPLE_FILTER_CONTROLS,
3021 };
3022 if self.iam_state.user_tab == UserTab::Permissions {
3023 self.iam_state.policy_input_focus = self
3024 .iam_state
3025 .policy_input_focus
3026 .next(&POLICY_FILTER_CONTROLS);
3027 } else if self.iam_state.user_tab == UserTab::LastAccessed {
3028 self.iam_state.last_accessed_input_focus = self
3029 .iam_state
3030 .last_accessed_input_focus
3031 .next(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3032 } else {
3033 self.iam_state.user_input_focus = self
3034 .iam_state
3035 .user_input_focus
3036 .next(&USER_SIMPLE_FILTER_CONTROLS);
3037 }
3038 } else if self.mode == Mode::FilterInput
3039 && self.current_service == Service::IamUserGroups
3040 {
3041 use crate::ui::iam::GROUP_FILTER_CONTROLS;
3042 self.iam_state.group_input_focus = self
3043 .iam_state
3044 .group_input_focus
3045 .next(&GROUP_FILTER_CONTROLS);
3046 } else if self.mode == Mode::InsightsInput {
3047 use crate::app::InsightsFocus;
3048 self.insights_state.insights.insights_focus =
3049 match self.insights_state.insights.insights_focus {
3050 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
3051 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
3052 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
3053 InsightsFocus::Query => InsightsFocus::QueryLanguage,
3054 };
3055 } else if self.mode == Mode::FilterInput
3056 && self.current_service == Service::CloudFormationStacks
3057 {
3058 if self.cfn_state.current_stack.is_some()
3059 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3060 {
3061 self.cfn_state.parameters_input_focus = self
3062 .cfn_state
3063 .parameters_input_focus
3064 .next(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3065 } else if self.cfn_state.current_stack.is_some()
3066 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3067 {
3068 self.cfn_state.outputs_input_focus = self
3069 .cfn_state
3070 .outputs_input_focus
3071 .next(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3072 } else if self.cfn_state.current_stack.is_some()
3073 && self.cfn_state.detail_tab == CfnDetailTab::Resources
3074 {
3075 self.cfn_state.resources_input_focus = self
3076 .cfn_state
3077 .resources_input_focus
3078 .next(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3079 } else {
3080 self.cfn_state.input_focus = self
3081 .cfn_state
3082 .input_focus
3083 .next(&CfnStateConstants::FILTER_CONTROLS);
3084 }
3085 } else if self.mode == Mode::FilterInput
3086 && self.current_service == Service::SqsQueues
3087 {
3088 if self.sqs_state.current_queue.is_some()
3089 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3090 {
3091 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3092 self.sqs_state.input_focus = self
3093 .sqs_state
3094 .input_focus
3095 .next(SUBSCRIPTION_FILTER_CONTROLS);
3096 } else {
3097 use crate::ui::sqs::FILTER_CONTROLS;
3098 self.sqs_state.input_focus =
3099 self.sqs_state.input_focus.next(FILTER_CONTROLS);
3100 }
3101 } else if self.mode == Mode::FilterInput
3102 && self.current_service == Service::CloudWatchLogGroups
3103 {
3104 use crate::ui::cw::logs::FILTER_CONTROLS;
3105 self.log_groups_state.input_focus =
3106 self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
3107 } else if self.mode == Mode::EventFilterInput {
3108 self.log_groups_state.event_input_focus =
3109 self.log_groups_state.event_input_focus.next();
3110 } else if self.mode == Mode::FilterInput
3111 && self.current_service == Service::CloudWatchAlarms
3112 {
3113 use crate::ui::cw::alarms::FILTER_CONTROLS;
3114 self.alarms_state.input_focus =
3115 self.alarms_state.input_focus.next(&FILTER_CONTROLS);
3116 } else if self.mode == Mode::FilterInput
3117 && self.current_service == Service::EcrRepositories
3118 && self.ecr_state.current_repository.is_none()
3119 {
3120 use crate::ui::ecr::FILTER_CONTROLS;
3121 self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
3122 } else if self.mode == Mode::FilterInput
3123 && self.current_service == Service::LambdaFunctions
3124 {
3125 use crate::ui::lambda::FILTER_CONTROLS;
3126 if self.lambda_state.current_version.is_some()
3127 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3128 {
3129 self.lambda_state.alias_input_focus =
3130 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
3131 } else if self.lambda_state.current_function.is_some()
3132 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3133 {
3134 self.lambda_state.version_input_focus =
3135 self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
3136 } else if self.lambda_state.current_function.is_some()
3137 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3138 {
3139 self.lambda_state.alias_input_focus =
3140 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
3141 } else if self.lambda_state.current_function.is_none() {
3142 self.lambda_state.input_focus =
3143 self.lambda_state.input_focus.next(&FILTER_CONTROLS);
3144 }
3145 }
3146 }
3147 Action::PrevFilterFocus => {
3148 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
3149 self.ec2_state.input_focus =
3150 self.ec2_state.input_focus.prev(&ec2::FILTER_CONTROLS);
3151 } else if self.mode == Mode::FilterInput
3152 && self.current_service == Service::LambdaApplications
3153 {
3154 use crate::ui::lambda::FILTER_CONTROLS;
3155 if self.lambda_application_state.current_application.is_some() {
3156 if self.lambda_application_state.detail_tab
3157 == LambdaApplicationDetailTab::Deployments
3158 {
3159 self.lambda_application_state.deployment_input_focus = self
3160 .lambda_application_state
3161 .deployment_input_focus
3162 .prev(&FILTER_CONTROLS);
3163 } else {
3164 self.lambda_application_state.resource_input_focus = self
3165 .lambda_application_state
3166 .resource_input_focus
3167 .prev(&FILTER_CONTROLS);
3168 }
3169 } else {
3170 self.lambda_application_state.input_focus = self
3171 .lambda_application_state
3172 .input_focus
3173 .prev(&FILTER_CONTROLS);
3174 }
3175 } else if self.mode == Mode::FilterInput
3176 && self.current_service == Service::CloudFormationStacks
3177 {
3178 if self.cfn_state.current_stack.is_some()
3179 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3180 {
3181 self.cfn_state.parameters_input_focus = self
3182 .cfn_state
3183 .parameters_input_focus
3184 .prev(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3185 } else if self.cfn_state.current_stack.is_some()
3186 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3187 {
3188 self.cfn_state.outputs_input_focus = self
3189 .cfn_state
3190 .outputs_input_focus
3191 .prev(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3192 } else if self.cfn_state.current_stack.is_some()
3193 && self.cfn_state.detail_tab == CfnDetailTab::Resources
3194 {
3195 self.cfn_state.resources_input_focus = self
3196 .cfn_state
3197 .resources_input_focus
3198 .prev(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3199 } else {
3200 self.cfn_state.input_focus = self
3201 .cfn_state
3202 .input_focus
3203 .prev(&CfnStateConstants::FILTER_CONTROLS);
3204 }
3205 } else if self.mode == Mode::FilterInput
3206 && self.current_service == Service::SqsQueues
3207 {
3208 if self.sqs_state.current_queue.is_some()
3209 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3210 {
3211 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3212 self.sqs_state.input_focus = self
3213 .sqs_state
3214 .input_focus
3215 .prev(SUBSCRIPTION_FILTER_CONTROLS);
3216 } else {
3217 use crate::ui::sqs::FILTER_CONTROLS;
3218 self.sqs_state.input_focus =
3219 self.sqs_state.input_focus.prev(FILTER_CONTROLS);
3220 }
3221 } else if self.mode == Mode::FilterInput
3222 && self.current_service == Service::IamRoles
3223 && self.iam_state.current_role.is_none()
3224 {
3225 use crate::ui::iam::ROLE_FILTER_CONTROLS;
3226 self.iam_state.role_input_focus =
3227 self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
3228 } else if self.mode == Mode::FilterInput
3229 && self.current_service == Service::IamUsers
3230 && self.iam_state.current_user.is_some()
3231 {
3232 use crate::ui::iam::{
3233 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3234 USER_SIMPLE_FILTER_CONTROLS,
3235 };
3236 if self.iam_state.user_tab == UserTab::Permissions {
3237 self.iam_state.policy_input_focus = self
3238 .iam_state
3239 .policy_input_focus
3240 .prev(&POLICY_FILTER_CONTROLS);
3241 } else if self.iam_state.user_tab == UserTab::LastAccessed {
3242 self.iam_state.last_accessed_input_focus = self
3243 .iam_state
3244 .last_accessed_input_focus
3245 .prev(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3246 } else {
3247 self.iam_state.user_input_focus = self
3248 .iam_state
3249 .user_input_focus
3250 .prev(&USER_SIMPLE_FILTER_CONTROLS);
3251 }
3252 } else if self.mode == Mode::FilterInput
3253 && self.current_service == Service::IamUserGroups
3254 {
3255 use crate::ui::iam::GROUP_FILTER_CONTROLS;
3256 self.iam_state.group_input_focus = self
3257 .iam_state
3258 .group_input_focus
3259 .prev(&GROUP_FILTER_CONTROLS);
3260 } else if self.mode == Mode::FilterInput
3261 && self.current_service == Service::CloudWatchLogGroups
3262 {
3263 use crate::ui::cw::logs::FILTER_CONTROLS;
3264 self.log_groups_state.input_focus =
3265 self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
3266 } else if self.mode == Mode::EventFilterInput {
3267 self.log_groups_state.event_input_focus =
3268 self.log_groups_state.event_input_focus.prev();
3269 } else if self.mode == Mode::FilterInput
3270 && self.current_service == Service::IamRoles
3271 && self.iam_state.current_role.is_some()
3272 {
3273 use crate::ui::iam::POLICY_FILTER_CONTROLS;
3274 self.iam_state.policy_input_focus = self
3275 .iam_state
3276 .policy_input_focus
3277 .prev(&POLICY_FILTER_CONTROLS);
3278 } else if self.mode == Mode::FilterInput
3279 && self.current_service == Service::CloudWatchAlarms
3280 {
3281 use crate::ui::cw::alarms::FILTER_CONTROLS;
3282 self.alarms_state.input_focus =
3283 self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
3284 } else if self.mode == Mode::FilterInput
3285 && self.current_service == Service::EcrRepositories
3286 && self.ecr_state.current_repository.is_none()
3287 {
3288 use crate::ui::ecr::FILTER_CONTROLS;
3289 self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
3290 } else if self.mode == Mode::FilterInput
3291 && self.current_service == Service::LambdaFunctions
3292 {
3293 use crate::ui::lambda::FILTER_CONTROLS;
3294 if self.lambda_state.current_version.is_some()
3295 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3296 {
3297 self.lambda_state.alias_input_focus =
3298 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
3299 } else if self.lambda_state.current_function.is_some()
3300 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3301 {
3302 self.lambda_state.version_input_focus =
3303 self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
3304 } else if self.lambda_state.current_function.is_some()
3305 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3306 {
3307 self.lambda_state.alias_input_focus =
3308 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
3309 } else if self.lambda_state.current_function.is_none() {
3310 self.lambda_state.input_focus =
3311 self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
3312 }
3313 }
3314 }
3315 Action::ToggleFilterCheckbox => {
3316 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
3317 if self.ec2_state.input_focus == EC2_STATE_FILTER {
3318 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
3319 self.ec2_state.table.reset();
3320 }
3321 } else if self.mode == Mode::InsightsInput {
3322 use crate::app::InsightsFocus;
3323 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3324 && self.insights_state.insights.show_dropdown
3325 && !self.insights_state.insights.log_group_matches.is_empty()
3326 {
3327 let selected_idx = self.insights_state.insights.dropdown_selected;
3328 if let Some(group_name) = self
3329 .insights_state
3330 .insights
3331 .log_group_matches
3332 .get(selected_idx)
3333 {
3334 let group_name = group_name.clone();
3335 if let Some(pos) = self
3336 .insights_state
3337 .insights
3338 .selected_log_groups
3339 .iter()
3340 .position(|g| g == &group_name)
3341 {
3342 self.insights_state.insights.selected_log_groups.remove(pos);
3343 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
3344 self.insights_state
3345 .insights
3346 .selected_log_groups
3347 .push(group_name);
3348 }
3349 }
3350 }
3351 } else if self.mode == Mode::FilterInput
3352 && self.current_service == Service::CloudFormationStacks
3353 {
3354 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
3355 match self.cfn_state.input_focus {
3356 STATUS_FILTER => {
3357 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3358 self.cfn_state.table.reset();
3359 }
3360 VIEW_NESTED => {
3361 self.cfn_state.view_nested = !self.cfn_state.view_nested;
3362 self.cfn_state.table.reset();
3363 }
3364 _ => {}
3365 }
3366 } else if self.mode == Mode::FilterInput
3367 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3368 {
3369 match self.log_groups_state.input_focus {
3370 InputFocus::Checkbox("ExactMatch") => {
3371 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
3372 }
3373 InputFocus::Checkbox("ShowExpired") => {
3374 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
3375 }
3376 _ => {}
3377 }
3378 } else if self.mode == Mode::EventFilterInput
3379 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
3380 {
3381 self.log_groups_state.relative_unit =
3382 self.log_groups_state.relative_unit.next();
3383 }
3384 }
3385 Action::CycleSortColumn => {
3386 if self.view_mode == ViewMode::Detail
3387 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3388 {
3389 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
3390 StreamSort::Name => StreamSort::CreationTime,
3391 StreamSort::CreationTime => StreamSort::LastEventTime,
3392 StreamSort::LastEventTime => StreamSort::Name,
3393 };
3394 }
3395 }
3396 Action::ToggleSortDirection => {
3397 if self.view_mode == ViewMode::Detail
3398 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3399 {
3400 self.log_groups_state.stream_sort_desc =
3401 !self.log_groups_state.stream_sort_desc;
3402 }
3403 }
3404 Action::ScrollUp => {
3405 if self.mode == Mode::ErrorModal {
3406 self.error_scroll = self.error_scroll.saturating_sub(1);
3407 } else if self.current_service == Service::LambdaFunctions
3408 && self.lambda_state.current_function.is_some()
3409 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3410 && !self.lambda_state.is_metrics_loading()
3411 {
3412 self.lambda_state.set_monitoring_scroll(
3413 self.lambda_state.monitoring_scroll().saturating_sub(1),
3414 );
3415 } else if self.current_service == Service::Ec2Instances
3416 && self.ec2_state.current_instance.is_some()
3417 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
3418 && !self.ec2_state.is_metrics_loading()
3419 {
3420 self.ec2_state.set_monitoring_scroll(
3421 self.ec2_state.monitoring_scroll().saturating_sub(1),
3422 );
3423 } else if self.current_service == Service::SqsQueues
3424 && self.sqs_state.current_queue.is_some()
3425 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3426 && !self.sqs_state.is_metrics_loading()
3427 {
3428 self.sqs_state.set_monitoring_scroll(
3429 self.sqs_state.monitoring_scroll().saturating_sub(1),
3430 );
3431 } else if self.view_mode == ViewMode::PolicyView {
3432 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
3433 } else if self.current_service == Service::IamRoles
3434 && self.iam_state.current_role.is_some()
3435 && self.iam_state.role_tab == RoleTab::TrustRelationships
3436 {
3437 self.iam_state.trust_policy_scroll =
3438 self.iam_state.trust_policy_scroll.saturating_sub(10);
3439 } else if self.view_mode == ViewMode::Events {
3440 if self.log_groups_state.event_scroll_offset == 0
3441 && self.log_groups_state.has_older_events
3442 {
3443 self.log_groups_state.loading = true;
3444 } else {
3445 self.log_groups_state.event_scroll_offset =
3446 self.log_groups_state.event_scroll_offset.saturating_sub(1);
3447 }
3448 } else if self.view_mode == ViewMode::InsightsResults {
3449 self.insights_state.insights.results_selected = self
3450 .insights_state
3451 .insights
3452 .results_selected
3453 .saturating_sub(1);
3454 } else if self.view_mode == ViewMode::Detail {
3455 self.log_groups_state.selected_stream =
3456 self.log_groups_state.selected_stream.saturating_sub(1);
3457 self.log_groups_state.expanded_stream = None;
3458 } else if self.view_mode == ViewMode::List
3459 && self.current_service == Service::CloudWatchLogGroups
3460 {
3461 self.log_groups_state.log_groups.selected =
3462 self.log_groups_state.log_groups.selected.saturating_sub(1);
3463 self.log_groups_state.log_groups.snap_to_page();
3464 } else if self.current_service == Service::EcrRepositories {
3465 if self.ecr_state.current_repository.is_some() {
3466 self.ecr_state.images.page_up();
3467 } else {
3468 self.ecr_state.repositories.page_up();
3469 }
3470 }
3471 }
3472 Action::ScrollDown => {
3473 if self.mode == Mode::ErrorModal {
3474 if let Some(error_msg) = &self.error_message {
3475 let lines = error_msg.lines().count();
3476 let max_scroll = lines.saturating_sub(1);
3477 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
3478 }
3479 } else if self.current_service == Service::SqsQueues
3480 && self.sqs_state.current_queue.is_some()
3481 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3482 {
3483 self.sqs_state
3484 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
3485 } else if self.view_mode == ViewMode::PolicyView {
3486 let lines = self.iam_state.policy_document.lines().count();
3487 let max_scroll = lines.saturating_sub(1);
3488 self.iam_state.policy_scroll =
3489 (self.iam_state.policy_scroll + 10).min(max_scroll);
3490 } else if self.current_service == Service::IamRoles
3491 && self.iam_state.current_role.is_some()
3492 && self.iam_state.role_tab == RoleTab::TrustRelationships
3493 {
3494 let lines = self.iam_state.trust_policy_document.lines().count();
3495 let max_scroll = lines.saturating_sub(1);
3496 self.iam_state.trust_policy_scroll =
3497 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
3498 } else if self.view_mode == ViewMode::Events {
3499 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3500 if self.log_groups_state.event_scroll_offset >= max_scroll {
3501 } else {
3503 self.log_groups_state.event_scroll_offset =
3504 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3505 }
3506 } else if self.view_mode == ViewMode::InsightsResults {
3507 let max = self
3508 .insights_state
3509 .insights
3510 .query_results
3511 .len()
3512 .saturating_sub(1);
3513 self.insights_state.insights.results_selected =
3514 (self.insights_state.insights.results_selected + 1).min(max);
3515 } else if self.view_mode == ViewMode::Detail {
3516 let filtered_streams = filtered_log_streams(self);
3517 let max = filtered_streams.len().saturating_sub(1);
3518 self.log_groups_state.selected_stream =
3519 (self.log_groups_state.selected_stream + 1).min(max);
3520 } else if self.view_mode == ViewMode::List
3521 && self.current_service == Service::CloudWatchLogGroups
3522 {
3523 let filtered_groups = filtered_log_groups(self);
3524 self.log_groups_state
3525 .log_groups
3526 .next_item(filtered_groups.len());
3527 } else if self.current_service == Service::EcrRepositories {
3528 if self.ecr_state.current_repository.is_some() {
3529 let filtered_images = filtered_ecr_images(self);
3530 self.ecr_state.images.page_down(filtered_images.len());
3531 } else {
3532 let filtered_repos = filtered_ecr_repositories(self);
3533 self.ecr_state.repositories.page_down(filtered_repos.len());
3534 }
3535 }
3536 }
3537
3538 Action::Refresh => {
3539 if self.mode == Mode::ProfilePicker {
3540 self.log_groups_state.loading = true;
3541 self.log_groups_state.loading_message = "Refreshing...".to_string();
3542 } else if self.mode == Mode::RegionPicker {
3543 self.measure_region_latencies();
3544 } else if self.mode == Mode::SessionPicker {
3545 self.sessions = Session::list_all().unwrap_or_default();
3546 } else if self.current_service == Service::CloudWatchInsights
3547 && !self.insights_state.insights.selected_log_groups.is_empty()
3548 {
3549 self.log_groups_state.loading = true;
3550 self.insights_state.insights.query_completed = true;
3551 } else if self.current_service == Service::LambdaFunctions {
3552 self.lambda_state.table.loading = true;
3553 } else if self.current_service == Service::LambdaApplications {
3554 self.lambda_application_state.table.loading = true;
3555 } else if matches!(
3556 self.view_mode,
3557 ViewMode::Events | ViewMode::Detail | ViewMode::List
3558 ) {
3559 self.log_groups_state.loading = true;
3560 }
3561 }
3562 Action::Yank => {
3563 if self.mode == Mode::ErrorModal {
3564 if let Some(error) = &self.error_message {
3566 copy_to_clipboard(error);
3567 }
3568 } else if self.view_mode == ViewMode::Events {
3569 if let Some(event) = self
3570 .log_groups_state
3571 .log_events
3572 .get(self.log_groups_state.event_scroll_offset)
3573 {
3574 copy_to_clipboard(&event.message);
3575 }
3576 } else if self.current_service == Service::EcrRepositories {
3577 if self.ecr_state.current_repository.is_some() {
3578 let filtered_images = filtered_ecr_images(self);
3579 if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
3580 copy_to_clipboard(&image.uri);
3581 }
3582 } else {
3583 let filtered_repos = filtered_ecr_repositories(self);
3584 if let Some(repo) =
3585 self.ecr_state.repositories.get_selected(&filtered_repos)
3586 {
3587 copy_to_clipboard(&repo.uri);
3588 }
3589 }
3590 } else if self.current_service == Service::LambdaFunctions {
3591 let filtered_functions = filtered_lambda_functions(self);
3592 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
3593 copy_to_clipboard(&func.arn);
3594 }
3595 } else if self.current_service == Service::CloudFormationStacks {
3596 if let Some(stack_name) = &self.cfn_state.current_stack {
3597 if let Some(stack) = self
3599 .cfn_state
3600 .table
3601 .items
3602 .iter()
3603 .find(|s| &s.name == stack_name)
3604 {
3605 copy_to_clipboard(&stack.stack_id);
3606 }
3607 } else {
3608 let filtered_stacks = filtered_cloudformation_stacks(self);
3610 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
3611 copy_to_clipboard(&stack.stack_id);
3612 }
3613 }
3614 } else if self.current_service == Service::IamUsers {
3615 if self.iam_state.current_user.is_some() {
3616 if let Some(user_name) = &self.iam_state.current_user {
3617 if let Some(user) = self
3618 .iam_state
3619 .users
3620 .items
3621 .iter()
3622 .find(|u| u.user_name == *user_name)
3623 {
3624 copy_to_clipboard(&user.arn);
3625 }
3626 }
3627 } else {
3628 let filtered_users = filtered_iam_users(self);
3629 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
3630 copy_to_clipboard(&user.arn);
3631 }
3632 }
3633 } else if self.current_service == Service::IamRoles {
3634 if self.iam_state.current_role.is_some() {
3635 if let Some(role_name) = &self.iam_state.current_role {
3636 if let Some(role) = self
3637 .iam_state
3638 .roles
3639 .items
3640 .iter()
3641 .find(|r| r.role_name == *role_name)
3642 {
3643 copy_to_clipboard(&role.arn);
3644 }
3645 }
3646 } else {
3647 let filtered_roles = filtered_iam_roles(self);
3648 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
3649 copy_to_clipboard(&role.arn);
3650 }
3651 }
3652 } else if self.current_service == Service::IamUserGroups {
3653 if self.iam_state.current_group.is_some() {
3654 if let Some(group_name) = &self.iam_state.current_group {
3655 let arn = iam::format_arn(&self.config.account_id, "group", group_name);
3656 copy_to_clipboard(&arn);
3657 }
3658 } else {
3659 let filtered_groups: Vec<_> = self
3660 .iam_state
3661 .groups
3662 .items
3663 .iter()
3664 .filter(|g| {
3665 if self.iam_state.groups.filter.is_empty() {
3666 true
3667 } else {
3668 g.group_name
3669 .to_lowercase()
3670 .contains(&self.iam_state.groups.filter.to_lowercase())
3671 }
3672 })
3673 .collect();
3674 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
3675 let arn = iam::format_arn(
3676 &self.config.account_id,
3677 "group",
3678 &group.group_name,
3679 );
3680 copy_to_clipboard(&arn);
3681 }
3682 }
3683 } else if self.current_service == Service::SqsQueues {
3684 if self.sqs_state.current_queue.is_some() {
3685 if let Some(queue) = self
3687 .sqs_state
3688 .queues
3689 .items
3690 .iter()
3691 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
3692 {
3693 let arn = format!(
3694 "arn:aws:sqs:{}:{}:{}",
3695 extract_region(&queue.url),
3696 extract_account_id(&queue.url),
3697 queue.name
3698 );
3699 copy_to_clipboard(&arn);
3700 }
3701 } else {
3702 let filtered_queues = filtered_queues(
3704 &self.sqs_state.queues.items,
3705 &self.sqs_state.queues.filter,
3706 );
3707 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
3708 let arn = format!(
3709 "arn:aws:sqs:{}:{}:{}",
3710 extract_region(&queue.url),
3711 extract_account_id(&queue.url),
3712 queue.name
3713 );
3714 copy_to_clipboard(&arn);
3715 }
3716 }
3717 }
3718 }
3719 Action::CopyToClipboard => {
3720 self.snapshot_requested = true;
3722 }
3723 Action::RetryLoad => {
3724 self.error_message = None;
3725 self.mode = Mode::Normal;
3726 self.log_groups_state.loading = true;
3727 }
3728 Action::ApplyFilter => {
3729 if self.mode == Mode::FilterInput
3730 && self.current_service == Service::SqsQueues
3731 && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
3732 {
3733 let regions = AwsRegion::all();
3734 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
3735 self.sqs_state.subscription_region_filter = region.code.to_string();
3736 }
3737 self.mode = Mode::Normal;
3738 } else if self.mode == Mode::InsightsInput {
3739 use crate::app::InsightsFocus;
3740 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3741 && self.insights_state.insights.show_dropdown
3742 {
3743 self.insights_state.insights.show_dropdown = false;
3745 self.mode = Mode::Normal;
3746 if !self.insights_state.insights.selected_log_groups.is_empty() {
3747 self.log_groups_state.loading = true;
3748 self.insights_state.insights.query_completed = true;
3749 }
3750 }
3751 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
3752 if let Ok(page) = self.page_input.parse::<usize>() {
3753 self.go_to_page(page);
3754 }
3755 self.page_input.clear();
3756 } else {
3757 self.mode = Mode::Normal;
3758 self.log_groups_state.filter_mode = false;
3759 }
3760 }
3761 Action::ToggleExactMatch => {
3762 if self.view_mode == ViewMode::Detail
3763 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3764 {
3765 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
3766 }
3767 }
3768 Action::ToggleShowExpired => {
3769 if self.view_mode == ViewMode::Detail
3770 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3771 {
3772 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
3773 }
3774 }
3775 Action::GoBack => {
3776 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
3778 self.mode = Mode::Normal;
3779 self.service_picker.filter.clear();
3780 }
3781 else if self.current_service == Service::S3Buckets
3783 && self.s3_state.current_bucket.is_some()
3784 {
3785 if !self.s3_state.prefix_stack.is_empty() {
3786 self.s3_state.prefix_stack.pop();
3787 self.s3_state.buckets.loading = true;
3788 } else {
3789 self.s3_state.current_bucket = None;
3790 self.s3_state.objects.clear();
3791 }
3792 }
3793 else if self.current_service == Service::EcrRepositories
3795 && self.ecr_state.current_repository.is_some()
3796 {
3797 if self.ecr_state.images.has_expanded_item() {
3798 self.ecr_state.images.collapse();
3799 } else {
3800 self.ecr_state.current_repository = None;
3801 self.ecr_state.current_repository_uri = None;
3802 self.ecr_state.images.items.clear();
3803 self.ecr_state.images.reset();
3804 }
3805 }
3806 else if self.current_service == Service::Ec2Instances
3808 && self.ec2_state.current_instance.is_some()
3809 {
3810 self.ec2_state.current_instance = None;
3811 self.view_mode = ViewMode::List;
3812 self.update_current_tab_breadcrumb();
3813 }
3814 else if self.current_service == Service::SqsQueues
3816 && self.sqs_state.current_queue.is_some()
3817 {
3818 self.sqs_state.current_queue = None;
3819 }
3820 else if self.current_service == Service::IamUsers
3822 && self.iam_state.current_user.is_some()
3823 {
3824 self.iam_state.current_user = None;
3825 self.iam_state.policies.items.clear();
3826 self.iam_state.policies.reset();
3827 self.update_current_tab_breadcrumb();
3828 }
3829 else if self.current_service == Service::IamUserGroups
3831 && self.iam_state.current_group.is_some()
3832 {
3833 self.iam_state.current_group = None;
3834 self.update_current_tab_breadcrumb();
3835 }
3836 else if self.current_service == Service::IamRoles {
3838 if self.view_mode == ViewMode::PolicyView {
3839 self.view_mode = ViewMode::Detail;
3841 self.iam_state.current_policy = None;
3842 self.iam_state.policy_document.clear();
3843 self.iam_state.policy_scroll = 0;
3844 self.update_current_tab_breadcrumb();
3845 } else if self.iam_state.current_role.is_some() {
3846 self.iam_state.current_role = None;
3847 self.iam_state.policies.items.clear();
3848 self.iam_state.policies.reset();
3849 self.update_current_tab_breadcrumb();
3850 }
3851 }
3852 else if self.current_service == Service::LambdaFunctions
3854 && self.lambda_state.current_version.is_some()
3855 {
3856 self.lambda_state.current_version = None;
3857 self.lambda_state.detail_tab = LambdaDetailTab::Versions;
3858 }
3859 else if self.current_service == Service::LambdaFunctions
3861 && self.lambda_state.current_alias.is_some()
3862 {
3863 self.lambda_state.current_alias = None;
3864 self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
3865 }
3866 else if self.current_service == Service::LambdaFunctions
3868 && self.lambda_state.current_function.is_some()
3869 {
3870 self.lambda_state.current_function = None;
3871 self.update_current_tab_breadcrumb();
3872 }
3873 else if self.current_service == Service::LambdaApplications
3875 && self.lambda_application_state.current_application.is_some()
3876 {
3877 self.lambda_application_state.current_application = None;
3878 self.update_current_tab_breadcrumb();
3879 }
3880 else if self.current_service == Service::CloudFormationStacks
3882 && self.cfn_state.current_stack.is_some()
3883 {
3884 self.cfn_state.current_stack = None;
3885 self.update_current_tab_breadcrumb();
3886 }
3887 else if self.view_mode == ViewMode::InsightsResults {
3889 if self.insights_state.insights.expanded_result.is_some() {
3890 self.insights_state.insights.expanded_result = None;
3891 }
3892 }
3893 else if self.current_service == Service::CloudWatchAlarms {
3895 if self.alarms_state.table.has_expanded_item() {
3896 self.alarms_state.table.collapse();
3897 }
3898 }
3899 else if self.current_service == Service::Ec2Instances {
3901 if self.ec2_state.current_instance.is_some()
3902 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
3903 {
3904 self.ec2_state.tags.collapse();
3905 } else {
3906 self.ec2_state.table.collapse();
3907 }
3908 }
3909 else if self.view_mode == ViewMode::Events {
3911 if self.log_groups_state.expanded_event.is_some() {
3912 self.log_groups_state.expanded_event = None;
3913 } else {
3914 self.view_mode = ViewMode::Detail;
3915 self.log_groups_state.event_filter.clear();
3916 }
3917 }
3918 else if self.view_mode == ViewMode::Detail {
3920 self.view_mode = ViewMode::List;
3921 self.log_groups_state.stream_filter.clear();
3922 self.log_groups_state.exact_match = false;
3923 self.log_groups_state.show_expired = false;
3924 }
3925 }
3926 Action::OpenInConsole | Action::OpenInBrowser => {
3927 let url = self.get_console_url();
3928 let _ = webbrowser::open(&url);
3929 }
3930 Action::ShowHelp => {
3931 self.mode = Mode::HelpModal;
3932 }
3933 Action::OpenRegionPicker => {
3934 self.region_filter.clear();
3935 self.region_picker_selected = 0;
3936 self.measure_region_latencies();
3937 self.mode = Mode::RegionPicker;
3938 }
3939 Action::OpenProfilePicker => {
3940 self.profile_filter.clear();
3941 self.profile_picker_selected = 0;
3942 self.available_profiles = Self::load_aws_profiles();
3943 self.mode = Mode::ProfilePicker;
3944 }
3945 Action::OpenCalendar => {
3946 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
3947 self.calendar_selecting = CalendarField::StartDate;
3948 self.mode = Mode::CalendarPicker;
3949 }
3950 Action::CloseCalendar => {
3951 self.mode = Mode::Normal;
3952 self.calendar_date = None;
3953 }
3954 Action::CalendarPrevDay => {
3955 if let Some(date) = self.calendar_date {
3956 self.calendar_date = date.checked_sub(time::Duration::days(1));
3957 }
3958 }
3959 Action::CalendarNextDay => {
3960 if let Some(date) = self.calendar_date {
3961 self.calendar_date = date.checked_add(time::Duration::days(1));
3962 }
3963 }
3964 Action::CalendarPrevWeek => {
3965 if let Some(date) = self.calendar_date {
3966 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
3967 }
3968 }
3969 Action::CalendarNextWeek => {
3970 if let Some(date) = self.calendar_date {
3971 self.calendar_date = date.checked_add(time::Duration::weeks(1));
3972 }
3973 }
3974 Action::CalendarPrevMonth => {
3975 if let Some(date) = self.calendar_date {
3976 self.calendar_date = Some(if date.month() == time::Month::January {
3977 date.replace_month(time::Month::December)
3978 .unwrap()
3979 .replace_year(date.year() - 1)
3980 .unwrap()
3981 } else {
3982 date.replace_month(date.month().previous()).unwrap()
3983 });
3984 }
3985 }
3986 Action::CalendarNextMonth => {
3987 if let Some(date) = self.calendar_date {
3988 self.calendar_date = Some(if date.month() == time::Month::December {
3989 date.replace_month(time::Month::January)
3990 .unwrap()
3991 .replace_year(date.year() + 1)
3992 .unwrap()
3993 } else {
3994 date.replace_month(date.month().next()).unwrap()
3995 });
3996 }
3997 }
3998 Action::CalendarSelect => {
3999 if let Some(date) = self.calendar_date {
4000 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
4001 .unix_timestamp()
4002 * 1000;
4003 match self.calendar_selecting {
4004 CalendarField::StartDate => {
4005 self.log_groups_state.start_time = Some(timestamp);
4006 self.calendar_selecting = CalendarField::EndDate;
4007 }
4008 CalendarField::EndDate => {
4009 self.log_groups_state.end_time = Some(timestamp);
4010 self.mode = Mode::Normal;
4011 self.calendar_date = None;
4012 }
4013 }
4014 }
4015 }
4016 }
4017 }
4018
4019 pub fn filtered_services(&self) -> Vec<&'static str> {
4020 let mut services = if self.service_picker.filter.is_empty() {
4021 self.service_picker.services.clone()
4022 } else {
4023 self.service_picker
4024 .services
4025 .iter()
4026 .filter(|s| {
4027 s.to_lowercase()
4028 .contains(&self.service_picker.filter.to_lowercase())
4029 })
4030 .copied()
4031 .collect()
4032 };
4033 services.sort();
4034 services
4035 }
4036
4037 pub fn breadcrumbs(&self) -> String {
4038 if !self.service_selected {
4039 return String::new();
4040 }
4041
4042 let mut parts = vec![];
4043
4044 match self.current_service {
4045 Service::CloudWatchLogGroups => {
4046 parts.push("CloudWatch".to_string());
4047 parts.push("Log groups".to_string());
4048
4049 if self.view_mode != ViewMode::List {
4050 if let Some(group) = selected_log_group(self) {
4051 parts.push(group.name.clone());
4052 }
4053 }
4054
4055 if self.view_mode == ViewMode::Events {
4056 if let Some(stream) = self
4057 .log_groups_state
4058 .log_streams
4059 .get(self.log_groups_state.selected_stream)
4060 {
4061 parts.push(stream.name.clone());
4062 }
4063 }
4064 }
4065 Service::CloudWatchInsights => {
4066 parts.push("CloudWatch".to_string());
4067 parts.push("Insights".to_string());
4068 }
4069 Service::CloudWatchAlarms => {
4070 parts.push("CloudWatch".to_string());
4071 parts.push("Alarms".to_string());
4072 }
4073 Service::S3Buckets => {
4074 parts.push("S3".to_string());
4075 if let Some(bucket) = &self.s3_state.current_bucket {
4076 parts.push(bucket.clone());
4077 if let Some(prefix) = self.s3_state.prefix_stack.last() {
4078 parts.push(prefix.trim_end_matches('/').to_string());
4079 }
4080 } else {
4081 parts.push("Buckets".to_string());
4082 }
4083 }
4084 Service::SqsQueues => {
4085 parts.push("SQS".to_string());
4086 parts.push("Queues".to_string());
4087 }
4088 Service::EcrRepositories => {
4089 parts.push("ECR".to_string());
4090 if let Some(repo) = &self.ecr_state.current_repository {
4091 parts.push(repo.clone());
4092 } else {
4093 parts.push("Repositories".to_string());
4094 }
4095 }
4096 Service::LambdaFunctions => {
4097 parts.push("Lambda".to_string());
4098 if let Some(func) = &self.lambda_state.current_function {
4099 parts.push(func.clone());
4100 } else {
4101 parts.push("Functions".to_string());
4102 }
4103 }
4104 Service::LambdaApplications => {
4105 parts.push("Lambda".to_string());
4106 parts.push("Applications".to_string());
4107 }
4108 Service::CloudFormationStacks => {
4109 parts.push("CloudFormation".to_string());
4110 if let Some(stack_name) = &self.cfn_state.current_stack {
4111 parts.push(stack_name.clone());
4112 } else {
4113 parts.push("Stacks".to_string());
4114 }
4115 }
4116 Service::IamUsers => {
4117 parts.push("IAM".to_string());
4118 parts.push("Users".to_string());
4119 }
4120 Service::IamRoles => {
4121 parts.push("IAM".to_string());
4122 parts.push("Roles".to_string());
4123 if let Some(role_name) = &self.iam_state.current_role {
4124 parts.push(role_name.clone());
4125 if let Some(policy_name) = &self.iam_state.current_policy {
4126 parts.push(policy_name.clone());
4127 }
4128 }
4129 }
4130 Service::IamUserGroups => {
4131 parts.push("IAM".to_string());
4132 parts.push("User Groups".to_string());
4133 if let Some(group_name) = &self.iam_state.current_group {
4134 parts.push(group_name.clone());
4135 }
4136 }
4137 Service::Ec2Instances => {
4138 parts.push("EC2".to_string());
4139 parts.push("Instances".to_string());
4140 }
4141 }
4142
4143 parts.join(" > ")
4144 }
4145
4146 pub fn update_current_tab_breadcrumb(&mut self) {
4147 if !self.tabs.is_empty() {
4148 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
4149 }
4150 }
4151
4152 pub fn get_console_url(&self) -> String {
4153 use crate::{cfn, cw, ecr, iam, lambda, s3};
4154
4155 match self.current_service {
4156 Service::CloudWatchLogGroups => {
4157 if self.view_mode == ViewMode::Events {
4158 if let Some(group) = selected_log_group(self) {
4159 if let Some(stream) = self
4160 .log_groups_state
4161 .log_streams
4162 .get(self.log_groups_state.selected_stream)
4163 {
4164 return cw::logs::console_url_stream(
4165 &self.config.region,
4166 &group.name,
4167 &stream.name,
4168 );
4169 }
4170 }
4171 } else if self.view_mode == ViewMode::Detail {
4172 if let Some(group) = selected_log_group(self) {
4173 return cw::logs::console_url_detail(&self.config.region, &group.name);
4174 }
4175 }
4176 cw::logs::console_url_list(&self.config.region)
4177 }
4178 Service::CloudWatchInsights => cw::insights::console_url(
4179 &self.config.region,
4180 &self.config.account_id,
4181 &self.insights_state.insights.query_text,
4182 &self.insights_state.insights.selected_log_groups,
4183 ),
4184 Service::CloudWatchAlarms => {
4185 let view_type = match self.alarms_state.view_as {
4186 AlarmViewMode::Table | AlarmViewMode::Detail => "table",
4187 AlarmViewMode::Cards => "card",
4188 };
4189 cw::alarms::console_url(
4190 &self.config.region,
4191 view_type,
4192 self.alarms_state.table.page_size.value(),
4193 &self.alarms_state.sort_column,
4194 self.alarms_state.sort_direction.as_str(),
4195 )
4196 }
4197 Service::S3Buckets => {
4198 if let Some(bucket_name) = &self.s3_state.current_bucket {
4199 let prefix = self.s3_state.prefix_stack.join("");
4200 s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
4201 } else {
4202 s3::console_url_buckets(&self.config.region)
4203 }
4204 }
4205 Service::SqsQueues => {
4206 if let Some(queue_url) = &self.sqs_state.current_queue {
4207 console_url_queue_detail(&self.config.region, queue_url)
4208 } else {
4209 console_url_queues(&self.config.region)
4210 }
4211 }
4212 Service::EcrRepositories => {
4213 if let Some(repo_name) = &self.ecr_state.current_repository {
4214 ecr::console_url_private_repository(
4215 &self.config.region,
4216 &self.config.account_id,
4217 repo_name,
4218 )
4219 } else {
4220 ecr::console_url_repositories(&self.config.region)
4221 }
4222 }
4223 Service::LambdaFunctions => {
4224 if let Some(func_name) = &self.lambda_state.current_function {
4225 if let Some(version) = &self.lambda_state.current_version {
4226 lambda::console_url_function_version(
4227 &self.config.region,
4228 func_name,
4229 version,
4230 &self.lambda_state.detail_tab,
4231 )
4232 } else {
4233 lambda::console_url_function_detail(&self.config.region, func_name)
4234 }
4235 } else {
4236 lambda::console_url_functions(&self.config.region)
4237 }
4238 }
4239 Service::LambdaApplications => {
4240 if let Some(app_name) = &self.lambda_application_state.current_application {
4241 lambda::console_url_application_detail(
4242 &self.config.region,
4243 app_name,
4244 &self.lambda_application_state.detail_tab,
4245 )
4246 } else {
4247 lambda::console_url_applications(&self.config.region)
4248 }
4249 }
4250 Service::CloudFormationStacks => {
4251 if let Some(stack_name) = &self.cfn_state.current_stack {
4252 if let Some(stack) = self
4253 .cfn_state
4254 .table
4255 .items
4256 .iter()
4257 .find(|s| &s.name == stack_name)
4258 {
4259 return cfn::console_url_stack_detail_with_tab(
4260 &self.config.region,
4261 &stack.stack_id,
4262 &self.cfn_state.detail_tab,
4263 );
4264 }
4265 }
4266 cfn::console_url_stacks(&self.config.region)
4267 }
4268 Service::IamUsers => {
4269 if let Some(user_name) = &self.iam_state.current_user {
4270 let section = match self.iam_state.user_tab {
4271 UserTab::Permissions => "permissions",
4272 UserTab::Groups => "groups",
4273 UserTab::Tags => "tags",
4274 UserTab::SecurityCredentials => "security_credentials",
4275 UserTab::LastAccessed => "access_advisor",
4276 };
4277 iam::console_url_user_detail(&self.config.region, user_name, section)
4278 } else {
4279 iam::console_url_users(&self.config.region)
4280 }
4281 }
4282 Service::IamRoles => {
4283 if let Some(policy_name) = &self.iam_state.current_policy {
4284 if let Some(role_name) = &self.iam_state.current_role {
4285 return iam::console_url_role_policy(
4286 &self.config.region,
4287 role_name,
4288 policy_name,
4289 );
4290 }
4291 }
4292 if let Some(role_name) = &self.iam_state.current_role {
4293 let section = match self.iam_state.role_tab {
4294 RoleTab::Permissions => "permissions",
4295 RoleTab::TrustRelationships => "trust_relationships",
4296 RoleTab::Tags => "tags",
4297 RoleTab::LastAccessed => "access_advisor",
4298 RoleTab::RevokeSessions => "revoke_sessions",
4299 };
4300 iam::console_url_role_detail(&self.config.region, role_name, section)
4301 } else {
4302 iam::console_url_roles(&self.config.region)
4303 }
4304 }
4305 Service::IamUserGroups => iam::console_url_groups(&self.config.region),
4306 Service::Ec2Instances => {
4307 if let Some(instance_id) = &self.ec2_state.current_instance {
4308 format!(
4309 "https://{}.console.aws.amazon.com/ec2/home?region={}#InstanceDetails:instanceId={}",
4310 self.config.region, self.config.region, instance_id
4311 )
4312 } else {
4313 format!(
4314 "https://{}.console.aws.amazon.com/ec2/home?region={}#Instances:",
4315 self.config.region, self.config.region
4316 )
4317 }
4318 }
4319 }
4320 }
4321
4322 pub fn calculate_total_bucket_rows(&self) -> usize {
4323 calculate_total_bucket_rows(self)
4324 }
4325
4326 fn calculate_total_object_rows(&self) -> usize {
4327 calculate_total_object_rows(self)
4328 }
4329
4330 fn get_column_selector_max(&self) -> usize {
4331 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none() {
4332 self.s3_bucket_column_ids.len() + 6
4333 } else if self.view_mode == ViewMode::Events {
4334 self.cw_log_event_column_ids.len() - 1
4335 } else if self.view_mode == ViewMode::Detail {
4336 self.cw_log_stream_column_ids.len() + 6
4337 } else if self.current_service == Service::CloudWatchAlarms {
4338 29
4339 } else if self.current_service == Service::Ec2Instances {
4340 if self.ec2_state.current_instance.is_some()
4341 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4342 {
4343 self.ec2_state.tag_column_ids.len() + 6
4344 } else {
4345 self.ec2_column_ids.len() + 6
4346 }
4347 } else if self.current_service == Service::EcrRepositories {
4348 if self.ecr_state.current_repository.is_some() {
4349 self.ecr_image_column_ids.len() + 6
4350 } else {
4351 self.ecr_repo_column_ids.len() + 6
4352 }
4353 } else if self.current_service == Service::SqsQueues {
4354 self.sqs_column_ids.len() - 1
4355 } else if self.current_service == Service::LambdaFunctions {
4356 self.lambda_state.function_column_ids.len() + 6
4357 } else if self.current_service == Service::LambdaApplications {
4358 self.lambda_application_column_ids.len() + 5
4359 } else if self.current_service == Service::CloudFormationStacks {
4360 self.cfn_column_ids.len() + 6
4361 } else if self.current_service == Service::IamUsers {
4362 if self.iam_state.current_user.is_some() {
4363 self.iam_policy_column_ids.len() + 5
4364 } else {
4365 self.iam_user_column_ids.len() + 5
4366 }
4367 } else if self.current_service == Service::IamRoles {
4368 if self.iam_state.current_role.is_some() {
4369 self.iam_policy_column_ids.len() + 5
4370 } else {
4371 self.iam_role_column_ids.len() + 5
4372 }
4373 } else {
4374 self.cw_log_group_column_ids.len() + 6
4375 }
4376 }
4377
4378 fn next_item(&mut self) {
4379 match self.mode {
4380 Mode::FilterInput => {
4381 if self.current_service == Service::S3Buckets
4382 && self.s3_state.input_focus == InputFocus::Pagination
4383 {
4384 let page_size = self.s3_state.buckets.page_size.value();
4386 let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
4387 let max_offset = total_rows.saturating_sub(page_size);
4388 self.s3_state.selected_row =
4389 (self.s3_state.selected_row + page_size).min(max_offset);
4390 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4391 } else if self.current_service == Service::CloudFormationStacks {
4392 use crate::ui::cfn::STATUS_FILTER;
4393 if self.cfn_state.input_focus == STATUS_FILTER {
4394 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
4395 self.cfn_state.table.reset();
4396 }
4397 } else if self.current_service == Service::IamUsers
4398 && self.iam_state.current_user.is_some()
4399 {
4400 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
4401 if self.iam_state.user_tab == UserTab::Permissions
4402 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
4403 {
4404 self.cycle_policy_type_next();
4405 } else if self.iam_state.user_tab == UserTab::LastAccessed
4406 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
4407 {
4408 self.iam_state.last_accessed_history_filter =
4409 self.iam_state.last_accessed_history_filter.next();
4410 self.iam_state.last_accessed_services.reset();
4411 }
4412 } else if self.current_service == Service::IamRoles
4413 && self.iam_state.current_role.is_some()
4414 && self.iam_state.role_tab == RoleTab::Permissions
4415 {
4416 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
4417 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
4418 self.cycle_policy_type_next();
4419 }
4420 } else if self.current_service == Service::Ec2Instances {
4421 if self.ec2_state.input_focus == EC2_STATE_FILTER {
4422 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
4423 self.ec2_state.table.reset();
4424 }
4425 } else if self.current_service == Service::SqsQueues {
4426 use crate::ui::sqs::SUBSCRIPTION_REGION;
4427 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
4428 let regions = AwsRegion::all();
4429 self.sqs_state.subscription_region_selected =
4430 (self.sqs_state.subscription_region_selected + 1)
4431 .min(regions.len() - 1);
4432 self.sqs_state.subscriptions.reset();
4433 }
4434 }
4435 }
4436 Mode::RegionPicker => {
4437 let filtered = self.get_filtered_regions();
4438 if !filtered.is_empty() {
4439 self.region_picker_selected =
4440 (self.region_picker_selected + 1).min(filtered.len() - 1);
4441 }
4442 }
4443 Mode::ProfilePicker => {
4444 let filtered = self.get_filtered_profiles();
4445 if !filtered.is_empty() {
4446 self.profile_picker_selected =
4447 (self.profile_picker_selected + 1).min(filtered.len() - 1);
4448 }
4449 }
4450 Mode::SessionPicker => {
4451 let filtered = self.get_filtered_sessions();
4452 if !filtered.is_empty() {
4453 self.session_picker_selected =
4454 (self.session_picker_selected + 1).min(filtered.len() - 1);
4455 }
4456 }
4457 Mode::InsightsInput => {
4458 use crate::app::InsightsFocus;
4459 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4460 && self.insights_state.insights.show_dropdown
4461 && !self.insights_state.insights.log_group_matches.is_empty()
4462 {
4463 let max = self.insights_state.insights.log_group_matches.len() - 1;
4464 self.insights_state.insights.dropdown_selected =
4465 (self.insights_state.insights.dropdown_selected + 1).min(max);
4466 }
4467 }
4468 Mode::ColumnSelector => {
4469 let max = self.get_column_selector_max();
4470 self.column_selector_index = (self.column_selector_index + 1).min(max);
4471 }
4472 Mode::ServicePicker => {
4473 let filtered = self.filtered_services();
4474 if !filtered.is_empty() {
4475 self.service_picker.selected =
4476 (self.service_picker.selected + 1).min(filtered.len() - 1);
4477 }
4478 }
4479 Mode::TabPicker => {
4480 let filtered = self.get_filtered_tabs();
4481 if !filtered.is_empty() {
4482 self.tab_picker_selected =
4483 (self.tab_picker_selected + 1).min(filtered.len() - 1);
4484 }
4485 }
4486 Mode::Normal => {
4487 if !self.service_selected {
4488 let filtered = self.filtered_services();
4489 if !filtered.is_empty() {
4490 self.service_picker.selected =
4491 (self.service_picker.selected + 1).min(filtered.len() - 1);
4492 }
4493 } else if self.current_service == Service::S3Buckets {
4494 if self.s3_state.current_bucket.is_some() {
4495 if self.s3_state.object_tab == S3ObjectTab::Properties {
4496 self.s3_state.properties_scroll =
4498 self.s3_state.properties_scroll.saturating_add(1);
4499 } else {
4500 let total_rows = self.calculate_total_object_rows();
4502 let max = total_rows.saturating_sub(1);
4503 self.s3_state.selected_object =
4504 (self.s3_state.selected_object + 1).min(max);
4505
4506 let visible_rows = self.s3_state.object_visible_rows.get();
4508 if self.s3_state.selected_object
4509 >= self.s3_state.object_scroll_offset + visible_rows
4510 {
4511 self.s3_state.object_scroll_offset =
4512 self.s3_state.selected_object - visible_rows + 1;
4513 }
4514 }
4515 } else {
4516 let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
4518 if total_rows > 0 {
4519 self.s3_state.selected_row =
4520 (self.s3_state.selected_row + 1).min(total_rows - 1);
4521
4522 let visible_rows = self.s3_state.bucket_visible_rows.get();
4524 if self.s3_state.selected_row
4525 >= self.s3_state.bucket_scroll_offset + visible_rows
4526 {
4527 self.s3_state.bucket_scroll_offset =
4528 self.s3_state.selected_row - visible_rows + 1;
4529 }
4530 }
4531 }
4532 } else if self.view_mode == ViewMode::InsightsResults {
4533 let max = self
4534 .insights_state
4535 .insights
4536 .query_results
4537 .len()
4538 .saturating_sub(1);
4539 if self.insights_state.insights.results_selected < max {
4540 self.insights_state.insights.results_selected += 1;
4541 }
4542 } else if self.view_mode == ViewMode::PolicyView {
4543 let lines = self.iam_state.policy_document.lines().count();
4544 let max_scroll = lines.saturating_sub(1);
4545 self.iam_state.policy_scroll =
4546 (self.iam_state.policy_scroll + 1).min(max_scroll);
4547 } else if self.current_service == Service::CloudFormationStacks
4548 && self.cfn_state.current_stack.is_some()
4549 && self.cfn_state.detail_tab == CfnDetailTab::Template
4550 {
4551 let lines = self.cfn_state.template_body.lines().count();
4552 let max_scroll = lines.saturating_sub(1);
4553 self.cfn_state.template_scroll =
4554 (self.cfn_state.template_scroll + 1).min(max_scroll);
4555 } else if self.current_service == Service::SqsQueues
4556 && self.sqs_state.current_queue.is_some()
4557 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
4558 {
4559 let lines = self.sqs_state.policy_document.lines().count();
4560 let max_scroll = lines.saturating_sub(1);
4561 self.sqs_state.policy_scroll =
4562 (self.sqs_state.policy_scroll + 1).min(max_scroll);
4563 } else if self.current_service == Service::LambdaFunctions
4564 && self.lambda_state.current_function.is_some()
4565 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4566 && !self.lambda_state.is_metrics_loading()
4567 {
4568 self.lambda_state
4569 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
4570 } else if self.current_service == Service::Ec2Instances
4571 && self.ec2_state.current_instance.is_some()
4572 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
4573 && !self.ec2_state.is_metrics_loading()
4574 {
4575 self.ec2_state
4576 .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
4577 } else if self.current_service == Service::SqsQueues
4578 && self.sqs_state.current_queue.is_some()
4579 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4580 && !self.sqs_state.is_metrics_loading()
4581 {
4582 self.sqs_state
4583 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
4584 } else if self.view_mode == ViewMode::Events {
4585 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
4586 if self.log_groups_state.event_scroll_offset >= max_scroll {
4587 } else {
4589 self.log_groups_state.event_scroll_offset =
4590 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
4591 }
4592 } else if self.current_service == Service::CloudWatchLogGroups {
4593 if self.view_mode == ViewMode::List {
4594 let filtered_groups = filtered_log_groups(self);
4595 self.log_groups_state
4596 .log_groups
4597 .next_item(filtered_groups.len());
4598 } else if self.view_mode == ViewMode::Detail {
4599 let filtered_streams = filtered_log_streams(self);
4600 if !filtered_streams.is_empty() {
4601 let max = filtered_streams.len() - 1;
4602 if self.log_groups_state.selected_stream >= max {
4603 } else {
4605 self.log_groups_state.selected_stream =
4606 (self.log_groups_state.selected_stream + 1).min(max);
4607 self.log_groups_state.expanded_stream = None;
4608 }
4609 }
4610 }
4611 } else if self.current_service == Service::CloudWatchAlarms {
4612 let filtered_alarms = match self.alarms_state.alarm_tab {
4613 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4614 AlarmTab::InAlarm => self
4615 .alarms_state
4616 .table
4617 .items
4618 .iter()
4619 .filter(|a| a.state.to_uppercase() == "ALARM")
4620 .count(),
4621 };
4622 if filtered_alarms > 0 {
4623 self.alarms_state.table.next_item(filtered_alarms);
4624 }
4625 } else if self.current_service == Service::Ec2Instances {
4626 if self.ec2_state.current_instance.is_some()
4627 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4628 {
4629 let filtered = crate::ui::ec2::filtered_tags(self);
4630 if !filtered.is_empty() {
4631 self.ec2_state.tags.next_item(filtered.len());
4632 }
4633 } else {
4634 let filtered: Vec<_> = self
4635 .ec2_state
4636 .table
4637 .items
4638 .iter()
4639 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
4640 .filter(|i| {
4641 if self.ec2_state.table.filter.is_empty() {
4642 return true;
4643 }
4644 i.name.contains(&self.ec2_state.table.filter)
4645 || i.instance_id.contains(&self.ec2_state.table.filter)
4646 || i.state.contains(&self.ec2_state.table.filter)
4647 || i.instance_type.contains(&self.ec2_state.table.filter)
4648 || i.availability_zone.contains(&self.ec2_state.table.filter)
4649 || i.security_groups.contains(&self.ec2_state.table.filter)
4650 || i.key_name.contains(&self.ec2_state.table.filter)
4651 })
4652 .collect();
4653 if !filtered.is_empty() {
4654 self.ec2_state.table.next_item(filtered.len());
4655 }
4656 }
4657 } else if self.current_service == Service::EcrRepositories {
4658 if self.ecr_state.current_repository.is_some() {
4659 let filtered_images = filtered_ecr_images(self);
4660 if !filtered_images.is_empty() {
4661 self.ecr_state.images.next_item(filtered_images.len());
4662 }
4663 } else {
4664 let filtered_repos = filtered_ecr_repositories(self);
4665 if !filtered_repos.is_empty() {
4666 self.ecr_state.repositories.selected =
4667 (self.ecr_state.repositories.selected + 1)
4668 .min(filtered_repos.len() - 1);
4669 self.ecr_state.repositories.snap_to_page();
4670 }
4671 }
4672 } else if self.current_service == Service::SqsQueues {
4673 if self.sqs_state.current_queue.is_some()
4674 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4675 {
4676 let filtered = filtered_lambda_triggers(self);
4677 if !filtered.is_empty() {
4678 self.sqs_state.triggers.next_item(filtered.len());
4679 }
4680 } else if self.sqs_state.current_queue.is_some()
4681 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4682 {
4683 let filtered = filtered_eventbridge_pipes(self);
4684 if !filtered.is_empty() {
4685 self.sqs_state.pipes.next_item(filtered.len());
4686 }
4687 } else if self.sqs_state.current_queue.is_some()
4688 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4689 {
4690 let filtered = filtered_tags(self);
4691 if !filtered.is_empty() {
4692 self.sqs_state.tags.next_item(filtered.len());
4693 }
4694 } else if self.sqs_state.current_queue.is_some()
4695 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4696 {
4697 let filtered = filtered_subscriptions(self);
4698 if !filtered.is_empty() {
4699 self.sqs_state.subscriptions.next_item(filtered.len());
4700 }
4701 } else {
4702 let filtered_queues = filtered_queues(
4703 &self.sqs_state.queues.items,
4704 &self.sqs_state.queues.filter,
4705 );
4706 if !filtered_queues.is_empty() {
4707 self.sqs_state.queues.next_item(filtered_queues.len());
4708 }
4709 }
4710 } else if self.current_service == Service::LambdaFunctions {
4711 if self.lambda_state.current_function.is_some()
4712 && self.lambda_state.detail_tab == LambdaDetailTab::Code
4713 {
4714 if let Some(func_name) = &self.lambda_state.current_function {
4716 if let Some(func) = self
4717 .lambda_state
4718 .table
4719 .items
4720 .iter()
4721 .find(|f| f.name == *func_name)
4722 {
4723 let max = func.layers.len().saturating_sub(1);
4724 if !func.layers.is_empty() {
4725 self.lambda_state.layer_selected =
4726 (self.lambda_state.layer_selected + 1).min(max);
4727 }
4728 }
4729 }
4730 } else if self.lambda_state.current_function.is_some()
4731 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4732 {
4733 let filtered: Vec<_> = self
4735 .lambda_state
4736 .version_table
4737 .items
4738 .iter()
4739 .filter(|v| {
4740 self.lambda_state.version_table.filter.is_empty()
4741 || v.version.to_lowercase().contains(
4742 &self.lambda_state.version_table.filter.to_lowercase(),
4743 )
4744 || v.aliases.to_lowercase().contains(
4745 &self.lambda_state.version_table.filter.to_lowercase(),
4746 )
4747 || v.description.to_lowercase().contains(
4748 &self.lambda_state.version_table.filter.to_lowercase(),
4749 )
4750 })
4751 .collect();
4752 if !filtered.is_empty() {
4753 self.lambda_state.version_table.selected =
4754 (self.lambda_state.version_table.selected + 1)
4755 .min(filtered.len() - 1);
4756 self.lambda_state.version_table.snap_to_page();
4757 }
4758 } else if self.lambda_state.current_function.is_some()
4759 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4760 || (self.lambda_state.current_version.is_some()
4761 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4762 {
4763 let version_filter = self.lambda_state.current_version.clone();
4765 let filtered: Vec<_> = self
4766 .lambda_state
4767 .alias_table
4768 .items
4769 .iter()
4770 .filter(|a| {
4771 (version_filter.is_none()
4772 || a.versions.contains(version_filter.as_ref().unwrap()))
4773 && (self.lambda_state.alias_table.filter.is_empty()
4774 || a.name.to_lowercase().contains(
4775 &self.lambda_state.alias_table.filter.to_lowercase(),
4776 )
4777 || a.versions.to_lowercase().contains(
4778 &self.lambda_state.alias_table.filter.to_lowercase(),
4779 )
4780 || a.description.to_lowercase().contains(
4781 &self.lambda_state.alias_table.filter.to_lowercase(),
4782 ))
4783 })
4784 .collect();
4785 if !filtered.is_empty() {
4786 self.lambda_state.alias_table.selected =
4787 (self.lambda_state.alias_table.selected + 1)
4788 .min(filtered.len() - 1);
4789 self.lambda_state.alias_table.snap_to_page();
4790 }
4791 } else if self.lambda_state.current_function.is_none() {
4792 let filtered = filtered_lambda_functions(self);
4793 if !filtered.is_empty() {
4794 self.lambda_state.table.next_item(filtered.len());
4795 self.lambda_state.table.snap_to_page();
4796 }
4797 }
4798 } else if self.current_service == Service::LambdaApplications {
4799 if self.lambda_application_state.current_application.is_some() {
4800 if self.lambda_application_state.detail_tab
4801 == LambdaApplicationDetailTab::Overview
4802 {
4803 let len = self.lambda_application_state.resources.items.len();
4804 if len > 0 {
4805 self.lambda_application_state.resources.next_item(len);
4806 }
4807 } else {
4808 let len = self.lambda_application_state.deployments.items.len();
4809 if len > 0 {
4810 self.lambda_application_state.deployments.next_item(len);
4811 }
4812 }
4813 } else {
4814 let filtered = filtered_lambda_applications(self);
4815 if !filtered.is_empty() {
4816 self.lambda_application_state.table.selected =
4817 (self.lambda_application_state.table.selected + 1)
4818 .min(filtered.len() - 1);
4819 self.lambda_application_state.table.snap_to_page();
4820 }
4821 }
4822 } else if self.current_service == Service::CloudFormationStacks {
4823 if self.cfn_state.current_stack.is_some()
4824 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4825 {
4826 let filtered = filtered_parameters(self);
4827 self.cfn_state.parameters.next_item(filtered.len());
4828 } else if self.cfn_state.current_stack.is_some()
4829 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4830 {
4831 let filtered = filtered_outputs(self);
4832 self.cfn_state.outputs.next_item(filtered.len());
4833 } else if self.cfn_state.current_stack.is_some()
4834 && self.cfn_state.detail_tab == CfnDetailTab::Resources
4835 {
4836 let filtered = filtered_resources(self);
4837 self.cfn_state.resources.next_item(filtered.len());
4838 } else {
4839 let filtered = filtered_cloudformation_stacks(self);
4840 self.cfn_state.table.next_item(filtered.len());
4841 }
4842 } else if self.current_service == Service::IamUsers {
4843 if self.iam_state.current_user.is_some() {
4844 if self.iam_state.user_tab == UserTab::Tags {
4845 let filtered = filtered_user_tags(self);
4846 if !filtered.is_empty() {
4847 self.iam_state.user_tags.next_item(filtered.len());
4848 }
4849 } else {
4850 let filtered = filtered_iam_policies(self);
4851 if !filtered.is_empty() {
4852 self.iam_state.policies.next_item(filtered.len());
4853 }
4854 }
4855 } else {
4856 let filtered = filtered_iam_users(self);
4857 if !filtered.is_empty() {
4858 self.iam_state.users.next_item(filtered.len());
4859 }
4860 }
4861 } else if self.current_service == Service::IamRoles {
4862 if self.iam_state.current_role.is_some() {
4863 if self.iam_state.role_tab == RoleTab::TrustRelationships {
4864 let lines = self.iam_state.trust_policy_document.lines().count();
4865 let max_scroll = lines.saturating_sub(1);
4866 self.iam_state.trust_policy_scroll =
4867 (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
4868 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4869 self.iam_state.revoke_sessions_scroll =
4870 (self.iam_state.revoke_sessions_scroll + 1).min(19);
4871 } else if self.iam_state.role_tab == RoleTab::Tags {
4872 let filtered = filtered_iam_tags(self);
4873 if !filtered.is_empty() {
4874 self.iam_state.tags.next_item(filtered.len());
4875 }
4876 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4877 let filtered = filtered_last_accessed(self);
4878 if !filtered.is_empty() {
4879 self.iam_state
4880 .last_accessed_services
4881 .next_item(filtered.len());
4882 }
4883 } else {
4884 let filtered = filtered_iam_policies(self);
4885 if !filtered.is_empty() {
4886 self.iam_state.policies.next_item(filtered.len());
4887 }
4888 }
4889 } else {
4890 let filtered = filtered_iam_roles(self);
4891 if !filtered.is_empty() {
4892 self.iam_state.roles.next_item(filtered.len());
4893 }
4894 }
4895 } else if self.current_service == Service::IamUserGroups {
4896 if self.iam_state.current_group.is_some() {
4897 if self.iam_state.group_tab == GroupTab::Users {
4898 let filtered: Vec<_> = self
4899 .iam_state
4900 .group_users
4901 .items
4902 .iter()
4903 .filter(|u| {
4904 if self.iam_state.group_users.filter.is_empty() {
4905 true
4906 } else {
4907 u.user_name.to_lowercase().contains(
4908 &self.iam_state.group_users.filter.to_lowercase(),
4909 )
4910 }
4911 })
4912 .collect();
4913 if !filtered.is_empty() {
4914 self.iam_state.group_users.next_item(filtered.len());
4915 }
4916 } else if self.iam_state.group_tab == GroupTab::Permissions {
4917 let filtered = filtered_iam_policies(self);
4918 if !filtered.is_empty() {
4919 self.iam_state.policies.next_item(filtered.len());
4920 }
4921 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4922 let filtered = filtered_last_accessed(self);
4923 if !filtered.is_empty() {
4924 self.iam_state
4925 .last_accessed_services
4926 .next_item(filtered.len());
4927 }
4928 }
4929 } else {
4930 let filtered: Vec<_> = self
4931 .iam_state
4932 .groups
4933 .items
4934 .iter()
4935 .filter(|g| {
4936 if self.iam_state.groups.filter.is_empty() {
4937 true
4938 } else {
4939 g.group_name
4940 .to_lowercase()
4941 .contains(&self.iam_state.groups.filter.to_lowercase())
4942 }
4943 })
4944 .collect();
4945 if !filtered.is_empty() {
4946 self.iam_state.groups.next_item(filtered.len());
4947 }
4948 }
4949 }
4950 }
4951 _ => {}
4952 }
4953 }
4954
4955 fn prev_item(&mut self) {
4956 match self.mode {
4957 Mode::FilterInput => {
4958 if self.current_service == Service::S3Buckets
4959 && self.s3_state.input_focus == InputFocus::Pagination
4960 {
4961 let page_size = self.s3_state.buckets.page_size.value();
4963 self.s3_state.selected_row =
4964 self.s3_state.selected_row.saturating_sub(page_size);
4965 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4966 } else if self.current_service == Service::CloudFormationStacks {
4967 use crate::ui::cfn::STATUS_FILTER;
4968 if self.cfn_state.input_focus == STATUS_FILTER {
4969 self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
4970 self.cfn_state.table.reset();
4971 }
4972 } else if self.current_service == Service::IamUsers
4973 && self.iam_state.current_user.is_some()
4974 {
4975 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
4976 if self.iam_state.user_tab == UserTab::Permissions
4977 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
4978 {
4979 self.cycle_policy_type_prev();
4980 } else if self.iam_state.user_tab == UserTab::LastAccessed
4981 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
4982 {
4983 self.iam_state.last_accessed_history_filter =
4984 self.iam_state.last_accessed_history_filter.prev();
4985 self.iam_state.last_accessed_services.reset();
4986 }
4987 } else if self.current_service == Service::IamRoles
4988 && self.iam_state.current_role.is_some()
4989 && self.iam_state.role_tab == RoleTab::Permissions
4990 {
4991 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
4992 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
4993 self.cycle_policy_type_prev();
4994 }
4995 } else if self.current_service == Service::Ec2Instances {
4996 if self.ec2_state.input_focus == EC2_STATE_FILTER {
4997 self.ec2_state.state_filter = self.ec2_state.state_filter.prev();
4998 self.ec2_state.table.reset();
4999 }
5000 } else if self.current_service == Service::SqsQueues {
5001 use crate::ui::sqs::SUBSCRIPTION_REGION;
5002 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
5003 self.sqs_state.subscription_region_selected = self
5004 .sqs_state
5005 .subscription_region_selected
5006 .saturating_sub(1);
5007 self.sqs_state.subscriptions.reset();
5008 }
5009 }
5010 }
5011 Mode::RegionPicker => {
5012 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
5013 }
5014 Mode::ProfilePicker => {
5015 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
5016 }
5017 Mode::SessionPicker => {
5018 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
5019 }
5020 Mode::InsightsInput => {
5021 use crate::app::InsightsFocus;
5022 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
5023 && self.insights_state.insights.show_dropdown
5024 && !self.insights_state.insights.log_group_matches.is_empty()
5025 {
5026 self.insights_state.insights.dropdown_selected = self
5027 .insights_state
5028 .insights
5029 .dropdown_selected
5030 .saturating_sub(1);
5031 }
5032 }
5033 Mode::ColumnSelector => {
5034 self.column_selector_index = self.column_selector_index.saturating_sub(1);
5035 }
5036 Mode::ServicePicker => {
5037 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5038 }
5039 Mode::TabPicker => {
5040 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
5041 }
5042 Mode::Normal => {
5043 if !self.service_selected {
5044 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5045 } else if self.current_service == Service::S3Buckets {
5046 if self.s3_state.current_bucket.is_some() {
5047 if self.s3_state.object_tab == S3ObjectTab::Properties {
5048 self.s3_state.properties_scroll =
5049 self.s3_state.properties_scroll.saturating_sub(1);
5050 } else {
5051 self.s3_state.selected_object =
5052 self.s3_state.selected_object.saturating_sub(1);
5053
5054 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5056 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5057 }
5058 }
5059 } else {
5060 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
5061
5062 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5064 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5065 }
5066 }
5067 } else if self.view_mode == ViewMode::InsightsResults {
5068 if self.insights_state.insights.results_selected > 0 {
5069 self.insights_state.insights.results_selected -= 1;
5070 }
5071 } else if self.view_mode == ViewMode::PolicyView {
5072 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
5073 } else if self.current_service == Service::CloudFormationStacks
5074 && self.cfn_state.current_stack.is_some()
5075 && self.cfn_state.detail_tab == CfnDetailTab::Template
5076 {
5077 self.cfn_state.template_scroll =
5078 self.cfn_state.template_scroll.saturating_sub(1);
5079 } else if self.current_service == Service::SqsQueues
5080 && self.sqs_state.current_queue.is_some()
5081 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
5082 {
5083 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(1);
5084 } else if self.current_service == Service::LambdaFunctions
5085 && self.lambda_state.current_function.is_some()
5086 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5087 && !self.lambda_state.is_metrics_loading()
5088 {
5089 self.lambda_state.set_monitoring_scroll(
5090 self.lambda_state.monitoring_scroll().saturating_sub(1),
5091 );
5092 } else if self.current_service == Service::Ec2Instances
5093 && self.ec2_state.current_instance.is_some()
5094 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5095 && !self.ec2_state.is_metrics_loading()
5096 {
5097 self.ec2_state.set_monitoring_scroll(
5098 self.ec2_state.monitoring_scroll().saturating_sub(1),
5099 );
5100 } else if self.current_service == Service::SqsQueues
5101 && self.sqs_state.current_queue.is_some()
5102 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
5103 && !self.sqs_state.is_metrics_loading()
5104 {
5105 self.sqs_state.set_monitoring_scroll(
5106 self.sqs_state.monitoring_scroll().saturating_sub(1),
5107 );
5108 } else if self.view_mode == ViewMode::Events {
5109 if self.log_groups_state.event_scroll_offset == 0 {
5110 if self.log_groups_state.has_older_events {
5111 self.log_groups_state.loading = true;
5112 }
5113 } else {
5115 self.log_groups_state.event_scroll_offset =
5116 self.log_groups_state.event_scroll_offset.saturating_sub(1);
5117 }
5118 } else if self.current_service == Service::CloudWatchLogGroups {
5119 if self.view_mode == ViewMode::List {
5120 self.log_groups_state.log_groups.prev_item();
5121 } else if self.view_mode == ViewMode::Detail
5122 && self.log_groups_state.selected_stream > 0
5123 {
5124 self.log_groups_state.selected_stream =
5125 self.log_groups_state.selected_stream.saturating_sub(1);
5126 self.log_groups_state.expanded_stream = None;
5127 }
5128 } else if self.current_service == Service::CloudWatchAlarms {
5129 self.alarms_state.table.prev_item();
5130 } else if self.current_service == Service::Ec2Instances {
5131 if self.ec2_state.current_instance.is_some()
5132 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5133 {
5134 self.ec2_state.tags.prev_item();
5135 } else {
5136 self.ec2_state.table.prev_item();
5137 }
5138 } else if self.current_service == Service::EcrRepositories {
5139 if self.ecr_state.current_repository.is_some() {
5140 self.ecr_state.images.prev_item();
5141 } else {
5142 self.ecr_state.repositories.prev_item();
5143 }
5144 } else if self.current_service == Service::SqsQueues {
5145 if self.sqs_state.current_queue.is_some()
5146 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5147 {
5148 self.sqs_state.triggers.prev_item();
5149 } else if self.sqs_state.current_queue.is_some()
5150 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5151 {
5152 self.sqs_state.pipes.prev_item();
5153 } else if self.sqs_state.current_queue.is_some()
5154 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5155 {
5156 self.sqs_state.tags.prev_item();
5157 } else if self.sqs_state.current_queue.is_some()
5158 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5159 {
5160 self.sqs_state.subscriptions.prev_item();
5161 } else {
5162 self.sqs_state.queues.prev_item();
5163 }
5164 } else if self.current_service == Service::LambdaFunctions {
5165 if self.lambda_state.current_function.is_some()
5166 && self.lambda_state.detail_tab == LambdaDetailTab::Code
5167 {
5168 self.lambda_state.layer_selected =
5170 self.lambda_state.layer_selected.saturating_sub(1);
5171 } else if self.lambda_state.current_function.is_some()
5172 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5173 {
5174 self.lambda_state.version_table.prev_item();
5175 } else if self.lambda_state.current_function.is_some()
5176 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5177 || (self.lambda_state.current_version.is_some()
5178 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5179 {
5180 self.lambda_state.alias_table.prev_item();
5181 } else if self.lambda_state.current_function.is_none() {
5182 self.lambda_state.table.prev_item();
5183 }
5184 } else if self.current_service == Service::LambdaApplications {
5185 if self.lambda_application_state.current_application.is_some()
5186 && self.lambda_application_state.detail_tab
5187 == LambdaApplicationDetailTab::Overview
5188 {
5189 self.lambda_application_state.resources.selected = self
5190 .lambda_application_state
5191 .resources
5192 .selected
5193 .saturating_sub(1);
5194 } else if self.lambda_application_state.current_application.is_some()
5195 && self.lambda_application_state.detail_tab
5196 == LambdaApplicationDetailTab::Deployments
5197 {
5198 self.lambda_application_state.deployments.selected = self
5199 .lambda_application_state
5200 .deployments
5201 .selected
5202 .saturating_sub(1);
5203 } else {
5204 self.lambda_application_state.table.selected = self
5205 .lambda_application_state
5206 .table
5207 .selected
5208 .saturating_sub(1);
5209 self.lambda_application_state.table.snap_to_page();
5210 }
5211 } else if self.current_service == Service::CloudFormationStacks {
5212 if self.cfn_state.current_stack.is_some()
5213 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5214 {
5215 self.cfn_state.parameters.prev_item();
5216 } else if self.cfn_state.current_stack.is_some()
5217 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5218 {
5219 self.cfn_state.outputs.prev_item();
5220 } else if self.cfn_state.current_stack.is_some()
5221 && self.cfn_state.detail_tab == CfnDetailTab::Resources
5222 {
5223 self.cfn_state.resources.prev_item();
5224 } else {
5225 self.cfn_state.table.prev_item();
5226 }
5227 } else if self.current_service == Service::IamUsers {
5228 self.iam_state.users.prev_item();
5229 } else if self.current_service == Service::IamRoles {
5230 if self.iam_state.current_role.is_some() {
5231 if self.iam_state.role_tab == RoleTab::TrustRelationships {
5232 self.iam_state.trust_policy_scroll =
5233 self.iam_state.trust_policy_scroll.saturating_sub(1);
5234 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
5235 self.iam_state.revoke_sessions_scroll =
5236 self.iam_state.revoke_sessions_scroll.saturating_sub(1);
5237 } else if self.iam_state.role_tab == RoleTab::Tags {
5238 self.iam_state.tags.prev_item();
5239 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5240 self.iam_state.last_accessed_services.prev_item();
5241 } else {
5242 self.iam_state.policies.prev_item();
5243 }
5244 } else {
5245 self.iam_state.roles.prev_item();
5246 }
5247 } else if self.current_service == Service::IamUserGroups {
5248 if self.iam_state.current_group.is_some() {
5249 if self.iam_state.group_tab == GroupTab::Users {
5250 self.iam_state.group_users.prev_item();
5251 } else if self.iam_state.group_tab == GroupTab::Permissions {
5252 self.iam_state.policies.prev_item();
5253 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5254 self.iam_state.last_accessed_services.prev_item();
5255 }
5256 } else {
5257 self.iam_state.groups.prev_item();
5258 }
5259 }
5260 }
5261 _ => {}
5262 }
5263 }
5264
5265 fn page_down(&mut self) {
5266 if self.mode == Mode::ColumnSelector {
5267 let max = self.get_column_selector_max();
5268 self.column_selector_index = (self.column_selector_index + 10).min(max);
5269 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
5270 if self.s3_state.input_focus == InputFocus::Pagination {
5271 let page_size = self.s3_state.buckets.page_size.value();
5273 let total_rows = self.calculate_total_bucket_rows();
5274 let max_offset = total_rows.saturating_sub(page_size);
5275 self.s3_state.selected_row =
5276 (self.s3_state.selected_row + page_size).min(max_offset);
5277 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5278 }
5279 } else if self.mode == Mode::FilterInput
5280 && self.current_service == Service::CloudFormationStacks
5281 {
5282 if self.cfn_state.current_stack.is_some()
5283 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5284 {
5285 let page_size = self.cfn_state.parameters.page_size.value();
5286 let filtered_count = filtered_parameters(self).len();
5287 self.cfn_state.parameters_input_focus.handle_page_down(
5288 &mut self.cfn_state.parameters.selected,
5289 &mut self.cfn_state.parameters.scroll_offset,
5290 page_size,
5291 filtered_count,
5292 );
5293 } else if self.cfn_state.current_stack.is_some()
5294 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5295 {
5296 let page_size = self.cfn_state.outputs.page_size.value();
5297 let filtered_count = filtered_outputs(self).len();
5298 self.cfn_state.outputs_input_focus.handle_page_down(
5299 &mut self.cfn_state.outputs.selected,
5300 &mut self.cfn_state.outputs.scroll_offset,
5301 page_size,
5302 filtered_count,
5303 );
5304 } else {
5305 use crate::ui::cfn::filtered_cloudformation_stacks;
5306 let page_size = self.cfn_state.table.page_size.value();
5307 let filtered_count = filtered_cloudformation_stacks(self).len();
5308 self.cfn_state.input_focus.handle_page_down(
5309 &mut self.cfn_state.table.selected,
5310 &mut self.cfn_state.table.scroll_offset,
5311 page_size,
5312 filtered_count,
5313 );
5314 }
5315 } else if self.mode == Mode::FilterInput
5316 && self.current_service == Service::IamRoles
5317 && self.iam_state.current_role.is_none()
5318 {
5319 let page_size = self.iam_state.roles.page_size.value();
5320 let filtered_count = filtered_iam_roles(self).len();
5321 self.iam_state.role_input_focus.handle_page_down(
5322 &mut self.iam_state.roles.selected,
5323 &mut self.iam_state.roles.scroll_offset,
5324 page_size,
5325 filtered_count,
5326 );
5327 } else if self.mode == Mode::FilterInput
5328 && self.current_service == Service::CloudWatchAlarms
5329 {
5330 let page_size = self.alarms_state.table.page_size.value();
5331 let filtered_count = self.alarms_state.table.items.len();
5332 self.alarms_state.input_focus.handle_page_down(
5333 &mut self.alarms_state.table.selected,
5334 &mut self.alarms_state.table.scroll_offset,
5335 page_size,
5336 filtered_count,
5337 );
5338 } else if self.mode == Mode::FilterInput
5339 && self.current_service == Service::CloudWatchLogGroups
5340 {
5341 if self.view_mode == ViewMode::List {
5342 let filtered = filtered_log_groups(self);
5344 let page_size = self.log_groups_state.log_groups.page_size.value();
5345 let filtered_count = filtered.len();
5346 self.log_groups_state.input_focus.handle_page_down(
5347 &mut self.log_groups_state.log_groups.selected,
5348 &mut self.log_groups_state.log_groups.scroll_offset,
5349 page_size,
5350 filtered_count,
5351 );
5352 } else {
5353 let filtered = filtered_log_streams(self);
5355 let page_size = self.log_groups_state.stream_page_size;
5356 let filtered_count = filtered.len();
5357 self.log_groups_state.input_focus.handle_page_down(
5358 &mut self.log_groups_state.selected_stream,
5359 &mut self.log_groups_state.stream_current_page,
5360 page_size,
5361 filtered_count,
5362 );
5363 self.log_groups_state.expanded_stream = None;
5364 }
5365 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5366 {
5367 if self.lambda_state.current_function.is_some()
5368 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5369 && self.lambda_state.version_input_focus == InputFocus::Pagination
5370 {
5371 let page_size = self.lambda_state.version_table.page_size.value();
5372 let filtered_count: usize = self
5373 .lambda_state
5374 .version_table
5375 .items
5376 .iter()
5377 .filter(|v| {
5378 self.lambda_state.version_table.filter.is_empty()
5379 || v.version
5380 .to_lowercase()
5381 .contains(&self.lambda_state.version_table.filter.to_lowercase())
5382 || v.aliases
5383 .to_lowercase()
5384 .contains(&self.lambda_state.version_table.filter.to_lowercase())
5385 || v.description
5386 .to_lowercase()
5387 .contains(&self.lambda_state.version_table.filter.to_lowercase())
5388 })
5389 .count();
5390 let target = self.lambda_state.version_table.selected + page_size;
5391 self.lambda_state.version_table.selected =
5392 target.min(filtered_count.saturating_sub(1));
5393 } else if self.lambda_state.current_function.is_some()
5394 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5395 || (self.lambda_state.current_version.is_some()
5396 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5397 && self.lambda_state.alias_input_focus == InputFocus::Pagination
5398 {
5399 let page_size = self.lambda_state.alias_table.page_size.value();
5400 let version_filter = self.lambda_state.current_version.clone();
5401 let filtered_count = self
5402 .lambda_state
5403 .alias_table
5404 .items
5405 .iter()
5406 .filter(|a| {
5407 (version_filter.is_none()
5408 || a.versions.contains(version_filter.as_ref().unwrap()))
5409 && (self.lambda_state.alias_table.filter.is_empty()
5410 || a.name
5411 .to_lowercase()
5412 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
5413 || a.versions
5414 .to_lowercase()
5415 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
5416 || a.description
5417 .to_lowercase()
5418 .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
5419 })
5420 .count();
5421 let target = self.lambda_state.alias_table.selected + page_size;
5422 self.lambda_state.alias_table.selected =
5423 target.min(filtered_count.saturating_sub(1));
5424 } else if self.lambda_state.current_function.is_none() {
5425 let page_size = self.lambda_state.table.page_size.value();
5426 let filtered_count = filtered_lambda_functions(self).len();
5427 self.lambda_state.input_focus.handle_page_down(
5428 &mut self.lambda_state.table.selected,
5429 &mut self.lambda_state.table.scroll_offset,
5430 page_size,
5431 filtered_count,
5432 );
5433 }
5434 } else if self.mode == Mode::FilterInput
5435 && self.current_service == Service::LambdaApplications
5436 {
5437 if self.lambda_application_state.current_application.is_some() {
5438 if self.lambda_application_state.detail_tab
5439 == LambdaApplicationDetailTab::Deployments
5440 {
5441 let page_size = self.lambda_application_state.deployments.page_size.value();
5442 let filtered_count = self.lambda_application_state.deployments.items.len();
5443 self.lambda_application_state
5444 .deployment_input_focus
5445 .handle_page_down(
5446 &mut self.lambda_application_state.deployments.selected,
5447 &mut self.lambda_application_state.deployments.scroll_offset,
5448 page_size,
5449 filtered_count,
5450 );
5451 } else {
5452 let page_size = self.lambda_application_state.resources.page_size.value();
5453 let filtered_count = self.lambda_application_state.resources.items.len();
5454 self.lambda_application_state
5455 .resource_input_focus
5456 .handle_page_down(
5457 &mut self.lambda_application_state.resources.selected,
5458 &mut self.lambda_application_state.resources.scroll_offset,
5459 page_size,
5460 filtered_count,
5461 );
5462 }
5463 } else {
5464 let page_size = self.lambda_application_state.table.page_size.value();
5465 let filtered_count = filtered_lambda_applications(self).len();
5466 self.lambda_application_state.input_focus.handle_page_down(
5467 &mut self.lambda_application_state.table.selected,
5468 &mut self.lambda_application_state.table.scroll_offset,
5469 page_size,
5470 filtered_count,
5471 );
5472 }
5473 } else if self.mode == Mode::FilterInput
5474 && self.current_service == Service::EcrRepositories
5475 && self.ecr_state.current_repository.is_none()
5476 && self.ecr_state.input_focus == InputFocus::Filter
5477 {
5478 let filtered = filtered_ecr_repositories(self);
5480 self.ecr_state.repositories.page_down(filtered.len());
5481 } else if self.mode == Mode::FilterInput
5482 && self.current_service == Service::EcrRepositories
5483 && self.ecr_state.current_repository.is_none()
5484 {
5485 let page_size = self.ecr_state.repositories.page_size.value();
5486 let filtered_count = filtered_ecr_repositories(self).len();
5487 self.ecr_state.input_focus.handle_page_down(
5488 &mut self.ecr_state.repositories.selected,
5489 &mut self.ecr_state.repositories.scroll_offset,
5490 page_size,
5491 filtered_count,
5492 );
5493 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5494 let page_size = self.iam_state.policies.page_size.value();
5495 let filtered_count = filtered_iam_policies(self).len();
5496 self.iam_state.policy_input_focus.handle_page_down(
5497 &mut self.iam_state.policies.selected,
5498 &mut self.iam_state.policies.scroll_offset,
5499 page_size,
5500 filtered_count,
5501 );
5502 } else if self.view_mode == ViewMode::PolicyView {
5503 let lines = self.iam_state.policy_document.lines().count();
5504 let max_scroll = lines.saturating_sub(1);
5505 self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
5506 } else if self.current_service == Service::CloudFormationStacks
5507 && self.cfn_state.current_stack.is_some()
5508 && self.cfn_state.detail_tab == CfnDetailTab::Template
5509 {
5510 let lines = self.cfn_state.template_body.lines().count();
5511 let max_scroll = lines.saturating_sub(1);
5512 self.cfn_state.template_scroll = (self.cfn_state.template_scroll + 10).min(max_scroll);
5513 } else if self.current_service == Service::LambdaFunctions
5514 && self.lambda_state.current_function.is_some()
5515 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5516 && !self.lambda_state.is_metrics_loading()
5517 {
5518 self.lambda_state
5519 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
5520 } else if self.current_service == Service::Ec2Instances
5521 && self.ec2_state.current_instance.is_some()
5522 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5523 && !self.ec2_state.is_metrics_loading()
5524 {
5525 self.ec2_state
5526 .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
5527 } else if self.current_service == Service::SqsQueues
5528 && self.sqs_state.current_queue.is_some()
5529 {
5530 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5531 self.sqs_state
5532 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
5533 } else {
5534 let lines = self.sqs_state.policy_document.lines().count();
5535 let max_scroll = lines.saturating_sub(1);
5536 self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
5537 }
5538 } else if self.current_service == Service::IamRoles
5539 && self.iam_state.current_role.is_some()
5540 && self.iam_state.role_tab == RoleTab::TrustRelationships
5541 {
5542 let lines = self.iam_state.trust_policy_document.lines().count();
5543 let max_scroll = lines.saturating_sub(1);
5544 self.iam_state.trust_policy_scroll =
5545 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
5546 } else if self.current_service == Service::IamRoles
5547 && self.iam_state.current_role.is_some()
5548 && self.iam_state.role_tab == RoleTab::RevokeSessions
5549 {
5550 self.iam_state.revoke_sessions_scroll =
5551 (self.iam_state.revoke_sessions_scroll + 10).min(19);
5552 } else if self.mode == Mode::Normal {
5553 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5554 {
5555 let total_rows = self.calculate_total_bucket_rows();
5556 self.s3_state.selected_row = self
5557 .s3_state
5558 .selected_row
5559 .saturating_add(10)
5560 .min(total_rows.saturating_sub(1));
5561
5562 let visible_rows = self.s3_state.bucket_visible_rows.get();
5564 if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
5565 self.s3_state.bucket_scroll_offset =
5566 self.s3_state.selected_row - visible_rows + 1;
5567 }
5568 } else if self.current_service == Service::S3Buckets
5569 && self.s3_state.current_bucket.is_some()
5570 {
5571 let total_rows = self.calculate_total_object_rows();
5572 self.s3_state.selected_object = self
5573 .s3_state
5574 .selected_object
5575 .saturating_add(10)
5576 .min(total_rows.saturating_sub(1));
5577
5578 let visible_rows = self.s3_state.object_visible_rows.get();
5580 if self.s3_state.selected_object
5581 >= self.s3_state.object_scroll_offset + visible_rows
5582 {
5583 self.s3_state.object_scroll_offset =
5584 self.s3_state.selected_object - visible_rows + 1;
5585 }
5586 } else if self.current_service == Service::CloudWatchLogGroups
5587 && self.view_mode == ViewMode::List
5588 {
5589 let filtered = filtered_log_groups(self);
5590 self.log_groups_state.log_groups.page_down(filtered.len());
5591 } else if self.current_service == Service::CloudWatchLogGroups
5592 && self.view_mode == ViewMode::Detail
5593 {
5594 let len = filtered_log_streams(self).len();
5595 nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
5596 } else if self.view_mode == ViewMode::Events {
5597 let max = self.log_groups_state.log_events.len();
5598 nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
5599 } else if self.view_mode == ViewMode::InsightsResults {
5600 let max = self.insights_state.insights.query_results.len();
5601 nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
5602 } else if self.current_service == Service::CloudWatchAlarms {
5603 let filtered = match self.alarms_state.alarm_tab {
5604 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5605 AlarmTab::InAlarm => self
5606 .alarms_state
5607 .table
5608 .items
5609 .iter()
5610 .filter(|a| a.state.to_uppercase() == "ALARM")
5611 .count(),
5612 };
5613 if filtered > 0 {
5614 self.alarms_state.table.page_down(filtered);
5615 }
5616 } else if self.current_service == Service::Ec2Instances {
5617 let filtered: Vec<_> = self
5618 .ec2_state
5619 .table
5620 .items
5621 .iter()
5622 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
5623 .filter(|i| {
5624 if self.ec2_state.table.filter.is_empty() {
5625 return true;
5626 }
5627 i.name.contains(&self.ec2_state.table.filter)
5628 || i.instance_id.contains(&self.ec2_state.table.filter)
5629 || i.state.contains(&self.ec2_state.table.filter)
5630 || i.instance_type.contains(&self.ec2_state.table.filter)
5631 || i.availability_zone.contains(&self.ec2_state.table.filter)
5632 || i.security_groups.contains(&self.ec2_state.table.filter)
5633 || i.key_name.contains(&self.ec2_state.table.filter)
5634 })
5635 .collect();
5636 if !filtered.is_empty() {
5637 self.ec2_state.table.page_down(filtered.len());
5638 }
5639 } else if self.current_service == Service::EcrRepositories {
5640 if self.ecr_state.current_repository.is_some() {
5641 let filtered = filtered_ecr_images(self);
5642 self.ecr_state.images.page_down(filtered.len());
5643 } else {
5644 let filtered = filtered_ecr_repositories(self);
5645 self.ecr_state.repositories.page_down(filtered.len());
5646 }
5647 } else if self.current_service == Service::SqsQueues {
5648 let filtered =
5649 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter);
5650 self.sqs_state.queues.page_down(filtered.len());
5651 } else if self.current_service == Service::LambdaFunctions {
5652 let len = filtered_lambda_functions(self).len();
5653 self.lambda_state.table.page_down(len);
5654 } else if self.current_service == Service::LambdaApplications {
5655 let len = filtered_lambda_applications(self).len();
5656 self.lambda_application_state.table.page_down(len);
5657 } else if self.current_service == Service::CloudFormationStacks {
5658 if self.cfn_state.current_stack.is_some()
5659 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5660 {
5661 let filtered = filtered_parameters(self);
5662 self.cfn_state.parameters.page_down(filtered.len());
5663 } else if self.cfn_state.current_stack.is_some()
5664 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5665 {
5666 let filtered = filtered_outputs(self);
5667 self.cfn_state.outputs.page_down(filtered.len());
5668 } else {
5669 let filtered = filtered_cloudformation_stacks(self);
5670 self.cfn_state.table.page_down(filtered.len());
5671 }
5672 } else if self.current_service == Service::IamUsers {
5673 let len = filtered_iam_users(self).len();
5674 nav_page_down(&mut self.iam_state.users.selected, len, 10);
5675 } else if self.current_service == Service::IamRoles {
5676 if self.iam_state.current_role.is_some() {
5677 let filtered = filtered_iam_policies(self);
5678 if !filtered.is_empty() {
5679 self.iam_state.policies.page_down(filtered.len());
5680 }
5681 } else {
5682 let filtered = filtered_iam_roles(self);
5683 self.iam_state.roles.page_down(filtered.len());
5684 }
5685 } else if self.current_service == Service::IamUserGroups {
5686 if self.iam_state.current_group.is_some() {
5687 if self.iam_state.group_tab == GroupTab::Users {
5688 let filtered: Vec<_> = self
5689 .iam_state
5690 .group_users
5691 .items
5692 .iter()
5693 .filter(|u| {
5694 if self.iam_state.group_users.filter.is_empty() {
5695 true
5696 } else {
5697 u.user_name
5698 .to_lowercase()
5699 .contains(&self.iam_state.group_users.filter.to_lowercase())
5700 }
5701 })
5702 .collect();
5703 if !filtered.is_empty() {
5704 self.iam_state.group_users.page_down(filtered.len());
5705 }
5706 } else if self.iam_state.group_tab == GroupTab::Permissions {
5707 let filtered = filtered_iam_policies(self);
5708 if !filtered.is_empty() {
5709 self.iam_state.policies.page_down(filtered.len());
5710 }
5711 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5712 let filtered = filtered_last_accessed(self);
5713 if !filtered.is_empty() {
5714 self.iam_state
5715 .last_accessed_services
5716 .page_down(filtered.len());
5717 }
5718 }
5719 } else {
5720 let filtered: Vec<_> = self
5721 .iam_state
5722 .groups
5723 .items
5724 .iter()
5725 .filter(|g| {
5726 if self.iam_state.groups.filter.is_empty() {
5727 true
5728 } else {
5729 g.group_name
5730 .to_lowercase()
5731 .contains(&self.iam_state.groups.filter.to_lowercase())
5732 }
5733 })
5734 .collect();
5735 if !filtered.is_empty() {
5736 self.iam_state.groups.page_down(filtered.len());
5737 }
5738 }
5739 }
5740 }
5741 }
5742
5743 fn cycle_policy_type_next(&mut self) {
5744 let types = ["All types", "AWS managed", "Customer managed"];
5745 let current_idx = types
5746 .iter()
5747 .position(|&t| t == self.iam_state.policy_type_filter)
5748 .unwrap_or(0);
5749 let next_idx = (current_idx + 1) % types.len();
5750 self.iam_state.policy_type_filter = types[next_idx].to_string();
5751 self.iam_state.policies.reset();
5752 }
5753
5754 fn cycle_policy_type_prev(&mut self) {
5755 let types = ["All types", "AWS managed", "Customer managed"];
5756 let current_idx = types
5757 .iter()
5758 .position(|&t| t == self.iam_state.policy_type_filter)
5759 .unwrap_or(0);
5760 let prev_idx = if current_idx == 0 {
5761 types.len() - 1
5762 } else {
5763 current_idx - 1
5764 };
5765 self.iam_state.policy_type_filter = types[prev_idx].to_string();
5766 self.iam_state.policies.reset();
5767 }
5768
5769 fn page_up(&mut self) {
5770 if self.mode == Mode::ColumnSelector {
5771 self.column_selector_index = self.column_selector_index.saturating_sub(10);
5772 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
5773 if self.s3_state.input_focus == InputFocus::Pagination {
5774 let page_size = self.s3_state.buckets.page_size.value();
5776 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(page_size);
5777 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5778 }
5779 } else if self.mode == Mode::FilterInput
5780 && self.current_service == Service::CloudFormationStacks
5781 {
5782 if self.cfn_state.current_stack.is_some()
5783 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5784 {
5785 let page_size = self.cfn_state.parameters.page_size.value();
5786 self.cfn_state.parameters_input_focus.handle_page_up(
5787 &mut self.cfn_state.parameters.selected,
5788 &mut self.cfn_state.parameters.scroll_offset,
5789 page_size,
5790 );
5791 } else if self.cfn_state.current_stack.is_some()
5792 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5793 {
5794 let page_size = self.cfn_state.outputs.page_size.value();
5795 self.cfn_state.outputs_input_focus.handle_page_up(
5796 &mut self.cfn_state.outputs.selected,
5797 &mut self.cfn_state.outputs.scroll_offset,
5798 page_size,
5799 );
5800 } else {
5801 let page_size = self.cfn_state.table.page_size.value();
5802 self.cfn_state.input_focus.handle_page_up(
5803 &mut self.cfn_state.table.selected,
5804 &mut self.cfn_state.table.scroll_offset,
5805 page_size,
5806 );
5807 }
5808 } else if self.mode == Mode::FilterInput
5809 && self.current_service == Service::IamRoles
5810 && self.iam_state.current_role.is_none()
5811 {
5812 let page_size = self.iam_state.roles.page_size.value();
5813 self.iam_state.role_input_focus.handle_page_up(
5814 &mut self.iam_state.roles.selected,
5815 &mut self.iam_state.roles.scroll_offset,
5816 page_size,
5817 );
5818 } else if self.mode == Mode::FilterInput
5819 && self.current_service == Service::CloudWatchAlarms
5820 {
5821 let page_size = self.alarms_state.table.page_size.value();
5822 self.alarms_state.input_focus.handle_page_up(
5823 &mut self.alarms_state.table.selected,
5824 &mut self.alarms_state.table.scroll_offset,
5825 page_size,
5826 );
5827 } else if self.mode == Mode::FilterInput
5828 && self.current_service == Service::CloudWatchLogGroups
5829 {
5830 if self.view_mode == ViewMode::List {
5831 let page_size = self.log_groups_state.log_groups.page_size.value();
5833 self.log_groups_state.input_focus.handle_page_up(
5834 &mut self.log_groups_state.log_groups.selected,
5835 &mut self.log_groups_state.log_groups.scroll_offset,
5836 page_size,
5837 );
5838 } else {
5839 let page_size = self.log_groups_state.stream_page_size;
5841 self.log_groups_state.input_focus.handle_page_up(
5842 &mut self.log_groups_state.selected_stream,
5843 &mut self.log_groups_state.stream_current_page,
5844 page_size,
5845 );
5846 self.log_groups_state.expanded_stream = None;
5847 }
5848 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5849 {
5850 if self.lambda_state.current_function.is_some()
5851 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5852 && self.lambda_state.version_input_focus == InputFocus::Pagination
5853 {
5854 let page_size = self.lambda_state.version_table.page_size.value();
5855 self.lambda_state.version_table.selected = self
5856 .lambda_state
5857 .version_table
5858 .selected
5859 .saturating_sub(page_size);
5860 } else if self.lambda_state.current_function.is_some()
5861 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5862 || (self.lambda_state.current_version.is_some()
5863 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5864 && self.lambda_state.alias_input_focus == InputFocus::Pagination
5865 {
5866 let page_size = self.lambda_state.alias_table.page_size.value();
5867 self.lambda_state.alias_table.selected = self
5868 .lambda_state
5869 .alias_table
5870 .selected
5871 .saturating_sub(page_size);
5872 } else if self.lambda_state.current_function.is_none() {
5873 let page_size = self.lambda_state.table.page_size.value();
5874 self.lambda_state.input_focus.handle_page_up(
5875 &mut self.lambda_state.table.selected,
5876 &mut self.lambda_state.table.scroll_offset,
5877 page_size,
5878 );
5879 }
5880 } else if self.mode == Mode::FilterInput
5881 && self.current_service == Service::LambdaApplications
5882 {
5883 if self.lambda_application_state.current_application.is_some() {
5884 if self.lambda_application_state.detail_tab
5885 == LambdaApplicationDetailTab::Deployments
5886 {
5887 let page_size = self.lambda_application_state.deployments.page_size.value();
5888 self.lambda_application_state
5889 .deployment_input_focus
5890 .handle_page_up(
5891 &mut self.lambda_application_state.deployments.selected,
5892 &mut self.lambda_application_state.deployments.scroll_offset,
5893 page_size,
5894 );
5895 } else {
5896 let page_size = self.lambda_application_state.resources.page_size.value();
5897 self.lambda_application_state
5898 .resource_input_focus
5899 .handle_page_up(
5900 &mut self.lambda_application_state.resources.selected,
5901 &mut self.lambda_application_state.resources.scroll_offset,
5902 page_size,
5903 );
5904 }
5905 } else {
5906 let page_size = self.lambda_application_state.table.page_size.value();
5907 self.lambda_application_state.input_focus.handle_page_up(
5908 &mut self.lambda_application_state.table.selected,
5909 &mut self.lambda_application_state.table.scroll_offset,
5910 page_size,
5911 );
5912 }
5913 } else if self.mode == Mode::FilterInput
5914 && self.current_service == Service::EcrRepositories
5915 && self.ecr_state.current_repository.is_none()
5916 && self.ecr_state.input_focus == InputFocus::Filter
5917 {
5918 self.ecr_state.repositories.page_up();
5920 } else if self.mode == Mode::FilterInput
5921 && self.current_service == Service::EcrRepositories
5922 && self.ecr_state.current_repository.is_none()
5923 {
5924 let page_size = self.ecr_state.repositories.page_size.value();
5925 self.ecr_state.input_focus.handle_page_up(
5926 &mut self.ecr_state.repositories.selected,
5927 &mut self.ecr_state.repositories.scroll_offset,
5928 page_size,
5929 );
5930 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5931 let page_size = self.iam_state.policies.page_size.value();
5932 self.iam_state.policy_input_focus.handle_page_up(
5933 &mut self.iam_state.policies.selected,
5934 &mut self.iam_state.policies.scroll_offset,
5935 page_size,
5936 );
5937 } else if self.view_mode == ViewMode::PolicyView {
5938 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
5939 } else if self.current_service == Service::CloudFormationStacks
5940 && self.cfn_state.current_stack.is_some()
5941 && self.cfn_state.detail_tab == CfnDetailTab::Template
5942 {
5943 self.cfn_state.template_scroll = self.cfn_state.template_scroll.saturating_sub(10);
5944 } else if self.current_service == Service::SqsQueues
5945 && self.sqs_state.current_queue.is_some()
5946 {
5947 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5948 self.sqs_state
5949 .set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
5950 } else {
5951 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
5952 }
5953 } else if self.current_service == Service::IamRoles
5954 && self.iam_state.current_role.is_some()
5955 && self.iam_state.role_tab == RoleTab::TrustRelationships
5956 {
5957 self.iam_state.trust_policy_scroll =
5958 self.iam_state.trust_policy_scroll.saturating_sub(10);
5959 } else if self.current_service == Service::IamRoles
5960 && self.iam_state.current_role.is_some()
5961 && self.iam_state.role_tab == RoleTab::RevokeSessions
5962 {
5963 self.iam_state.revoke_sessions_scroll =
5964 self.iam_state.revoke_sessions_scroll.saturating_sub(10);
5965 } else if self.mode == Mode::Normal {
5966 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5967 {
5968 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
5969
5970 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5972 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5973 }
5974 } else if self.current_service == Service::S3Buckets
5975 && self.s3_state.current_bucket.is_some()
5976 {
5977 self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
5978
5979 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5981 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5982 }
5983 } else if self.current_service == Service::CloudWatchLogGroups
5984 && self.view_mode == ViewMode::List
5985 {
5986 self.log_groups_state.log_groups.page_up();
5987 } else if self.current_service == Service::CloudWatchLogGroups
5988 && self.view_mode == ViewMode::Detail
5989 {
5990 self.log_groups_state.selected_stream =
5991 self.log_groups_state.selected_stream.saturating_sub(10);
5992 } else if self.view_mode == ViewMode::Events {
5993 if self.log_groups_state.event_scroll_offset < 10
5994 && self.log_groups_state.has_older_events
5995 {
5996 self.log_groups_state.loading = true;
5997 }
5998 self.log_groups_state.event_scroll_offset =
5999 self.log_groups_state.event_scroll_offset.saturating_sub(10);
6000 } else if self.view_mode == ViewMode::InsightsResults {
6001 self.insights_state.insights.results_selected = self
6002 .insights_state
6003 .insights
6004 .results_selected
6005 .saturating_sub(10);
6006 } else if self.current_service == Service::CloudWatchAlarms {
6007 self.alarms_state.table.page_up();
6008 } else if self.current_service == Service::Ec2Instances {
6009 self.ec2_state.table.page_up();
6010 } else if self.current_service == Service::EcrRepositories {
6011 if self.ecr_state.current_repository.is_some() {
6012 self.ecr_state.images.page_up();
6013 } else {
6014 self.ecr_state.repositories.page_up();
6015 }
6016 } else if self.current_service == Service::SqsQueues {
6017 self.sqs_state.queues.page_up();
6018 } else if self.current_service == Service::LambdaFunctions {
6019 self.lambda_state.table.page_up();
6020 } else if self.current_service == Service::LambdaApplications {
6021 self.lambda_application_state.table.page_up();
6022 } else if self.current_service == Service::CloudFormationStacks {
6023 if self.cfn_state.current_stack.is_some()
6024 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6025 {
6026 self.cfn_state.parameters.page_up();
6027 } else if self.cfn_state.current_stack.is_some()
6028 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6029 {
6030 self.cfn_state.outputs.page_up();
6031 } else {
6032 self.cfn_state.table.page_up();
6033 }
6034 } else if self.current_service == Service::IamUsers {
6035 self.iam_state.users.page_up();
6036 } else if self.current_service == Service::IamRoles {
6037 if self.iam_state.current_role.is_some() {
6038 self.iam_state.policies.page_up();
6039 } else {
6040 self.iam_state.roles.page_up();
6041 }
6042 }
6043 }
6044 }
6045
6046 fn next_pane(&mut self) {
6047 if self.current_service == Service::S3Buckets {
6048 if self.s3_state.current_bucket.is_some() {
6049 let mut visual_idx = 0;
6052 let mut found_obj: Option<S3Object> = None;
6053
6054 fn check_nested(
6056 obj: &S3Object,
6057 visual_idx: &mut usize,
6058 target_idx: usize,
6059 expanded_prefixes: &std::collections::HashSet<String>,
6060 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6061 found_obj: &mut Option<S3Object>,
6062 ) {
6063 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6064 if let Some(preview) = prefix_preview.get(&obj.key) {
6065 for nested_obj in preview {
6066 if *visual_idx == target_idx {
6067 *found_obj = Some(nested_obj.clone());
6068 return;
6069 }
6070 *visual_idx += 1;
6071
6072 check_nested(
6074 nested_obj,
6075 visual_idx,
6076 target_idx,
6077 expanded_prefixes,
6078 prefix_preview,
6079 found_obj,
6080 );
6081 if found_obj.is_some() {
6082 return;
6083 }
6084 }
6085 } else {
6086 *visual_idx += 1;
6088 }
6089 }
6090 }
6091
6092 for obj in &self.s3_state.objects {
6093 if visual_idx == self.s3_state.selected_object {
6094 found_obj = Some(obj.clone());
6095 break;
6096 }
6097 visual_idx += 1;
6098
6099 check_nested(
6101 obj,
6102 &mut visual_idx,
6103 self.s3_state.selected_object,
6104 &self.s3_state.expanded_prefixes,
6105 &self.s3_state.prefix_preview,
6106 &mut found_obj,
6107 );
6108 if found_obj.is_some() {
6109 break;
6110 }
6111 }
6112
6113 if let Some(obj) = found_obj {
6114 if obj.is_prefix {
6115 if !self.s3_state.expanded_prefixes.contains(&obj.key) {
6116 self.s3_state.expanded_prefixes.insert(obj.key.clone());
6117 if !self.s3_state.prefix_preview.contains_key(&obj.key) {
6119 self.s3_state.buckets.loading = true;
6120 }
6121 }
6122 if self.s3_state.expanded_prefixes.contains(&obj.key) {
6124 if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
6125 if !preview.is_empty() {
6126 self.s3_state.selected_object += 1;
6127 }
6128 }
6129 }
6130 }
6131 }
6132 } else {
6133 let mut row_idx = 0;
6135 let mut found = false;
6136 for bucket in &self.s3_state.buckets.items {
6137 if row_idx == self.s3_state.selected_row {
6138 if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
6140 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
6141 if !self.s3_state.bucket_preview.contains_key(&bucket.name)
6142 && !self.s3_state.bucket_errors.contains_key(&bucket.name)
6143 {
6144 self.s3_state.buckets.loading = true;
6145 }
6146 }
6147 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6149 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6150 if !preview.is_empty() {
6151 self.s3_state.selected_row = row_idx + 1;
6152 }
6153 }
6154 }
6155 break;
6156 }
6157 row_idx += 1;
6158
6159 if self.s3_state.bucket_errors.contains_key(&bucket.name)
6161 && self.s3_state.expanded_prefixes.contains(&bucket.name)
6162 {
6163 continue;
6164 }
6165
6166 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6167 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6168 #[allow(clippy::too_many_arguments)]
6170 fn check_nested_expansion(
6171 objects: &[S3Object],
6172 row_idx: &mut usize,
6173 target_row: usize,
6174 expanded_prefixes: &mut std::collections::HashSet<String>,
6175 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6176 found: &mut bool,
6177 loading: &mut bool,
6178 selected_row: &mut usize,
6179 ) {
6180 for obj in objects {
6181 if *row_idx == target_row {
6182 if obj.is_prefix {
6184 if !expanded_prefixes.contains(&obj.key) {
6185 expanded_prefixes.insert(obj.key.clone());
6186 if !prefix_preview.contains_key(&obj.key) {
6187 *loading = true;
6188 }
6189 }
6190 if expanded_prefixes.contains(&obj.key) {
6192 if let Some(preview) = prefix_preview.get(&obj.key)
6193 {
6194 if !preview.is_empty() {
6195 *selected_row = *row_idx + 1;
6196 }
6197 }
6198 }
6199 }
6200 *found = true;
6201 return;
6202 }
6203 *row_idx += 1;
6204
6205 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6207 if let Some(nested) = prefix_preview.get(&obj.key) {
6208 check_nested_expansion(
6209 nested,
6210 row_idx,
6211 target_row,
6212 expanded_prefixes,
6213 prefix_preview,
6214 found,
6215 loading,
6216 selected_row,
6217 );
6218 if *found {
6219 return;
6220 }
6221 } else {
6222 *row_idx += 1; }
6224 }
6225 }
6226 }
6227
6228 check_nested_expansion(
6229 preview,
6230 &mut row_idx,
6231 self.s3_state.selected_row,
6232 &mut self.s3_state.expanded_prefixes,
6233 &self.s3_state.prefix_preview,
6234 &mut found,
6235 &mut self.s3_state.buckets.loading,
6236 &mut self.s3_state.selected_row,
6237 );
6238 if found || row_idx > self.s3_state.selected_row {
6239 break;
6240 }
6241 } else {
6242 row_idx += 1;
6243 if row_idx > self.s3_state.selected_row {
6244 break;
6245 }
6246 }
6247 }
6248 if found {
6249 break;
6250 }
6251 }
6252 }
6253 } else if self.view_mode == ViewMode::InsightsResults {
6254 let max_cols = self
6256 .insights_state
6257 .insights
6258 .query_results
6259 .first()
6260 .map(|r| r.len())
6261 .unwrap_or(0);
6262 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
6263 self.insights_state.insights.results_horizontal_scroll += 1;
6264 }
6265 } else if self.current_service == Service::CloudWatchLogGroups
6266 && self.view_mode == ViewMode::List
6267 {
6268 if self.log_groups_state.log_groups.expanded_item
6270 != Some(self.log_groups_state.log_groups.selected)
6271 {
6272 self.log_groups_state.log_groups.expanded_item =
6273 Some(self.log_groups_state.log_groups.selected);
6274 }
6275 } else if self.current_service == Service::CloudWatchLogGroups
6276 && self.view_mode == ViewMode::Detail
6277 {
6278 if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
6280 {
6281 self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
6282 }
6283 } else if self.view_mode == ViewMode::Events {
6284 if self.log_groups_state.expanded_event
6287 != Some(self.log_groups_state.event_scroll_offset)
6288 {
6289 self.log_groups_state.expanded_event =
6290 Some(self.log_groups_state.event_scroll_offset);
6291 }
6292 } else if self.current_service == Service::CloudWatchAlarms {
6293 if !self.alarms_state.table.is_expanded() {
6295 self.alarms_state.table.toggle_expand();
6296 }
6297 } else if self.current_service == Service::Ec2Instances {
6298 if self.ec2_state.current_instance.is_some()
6299 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
6300 {
6301 self.ec2_state.tags.toggle_expand();
6302 } else if !self.ec2_state.table.is_expanded() {
6303 self.ec2_state.table.toggle_expand();
6304 }
6305 } else if self.current_service == Service::EcrRepositories {
6306 if self.ecr_state.current_repository.is_some() {
6307 self.ecr_state.images.toggle_expand();
6309 } else {
6310 self.ecr_state.repositories.toggle_expand();
6312 }
6313 } else if self.current_service == Service::SqsQueues {
6314 if self.sqs_state.current_queue.is_some()
6315 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6316 {
6317 self.sqs_state.triggers.toggle_expand();
6318 } else if self.sqs_state.current_queue.is_some()
6319 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6320 {
6321 self.sqs_state.pipes.toggle_expand();
6322 } else if self.sqs_state.current_queue.is_some()
6323 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6324 {
6325 self.sqs_state.tags.toggle_expand();
6326 } else if self.sqs_state.current_queue.is_some()
6327 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6328 {
6329 self.sqs_state.subscriptions.toggle_expand();
6330 } else {
6331 self.sqs_state.queues.expand();
6332 }
6333 } else if self.current_service == Service::LambdaFunctions {
6334 if self.lambda_state.current_function.is_some()
6335 && self.lambda_state.detail_tab == LambdaDetailTab::Code
6336 {
6337 if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
6339 self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
6340 } else {
6341 self.lambda_state.layer_expanded = None;
6342 }
6343 } else if self.lambda_state.current_function.is_some()
6344 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6345 {
6346 self.lambda_state.version_table.toggle_expand();
6348 } else if self.lambda_state.current_function.is_some()
6349 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6350 || (self.lambda_state.current_version.is_some()
6351 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6352 {
6353 self.lambda_state.alias_table.toggle_expand();
6355 } else if self.lambda_state.current_function.is_none() {
6356 self.lambda_state.table.toggle_expand();
6358 }
6359 } else if self.current_service == Service::LambdaApplications {
6360 if self.lambda_application_state.current_application.is_some() {
6361 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
6363 {
6364 self.lambda_application_state.resources.toggle_expand();
6365 } else {
6366 self.lambda_application_state.deployments.toggle_expand();
6367 }
6368 } else {
6369 if self.lambda_application_state.table.expanded_item
6371 != Some(self.lambda_application_state.table.selected)
6372 {
6373 self.lambda_application_state.table.expanded_item =
6374 Some(self.lambda_application_state.table.selected);
6375 }
6376 }
6377 } else if self.current_service == Service::CloudFormationStacks
6378 && self.cfn_state.current_stack.is_none()
6379 {
6380 self.cfn_state.table.toggle_expand();
6381 } else if self.current_service == Service::CloudFormationStacks
6382 && self.cfn_state.current_stack.is_some()
6383 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6384 {
6385 self.cfn_state.parameters.toggle_expand();
6386 } else if self.current_service == Service::CloudFormationStacks
6387 && self.cfn_state.current_stack.is_some()
6388 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6389 {
6390 self.cfn_state.outputs.toggle_expand();
6391 } else if self.current_service == Service::CloudFormationStacks
6392 && self.cfn_state.current_stack.is_some()
6393 && self.cfn_state.detail_tab == CfnDetailTab::Resources
6394 {
6395 self.cfn_state.resources.toggle_expand();
6396 } else if self.current_service == Service::IamUsers {
6397 if self.iam_state.current_user.is_some() {
6398 if self.iam_state.user_tab == UserTab::Tags {
6399 if self.iam_state.user_tags.expanded_item
6400 != Some(self.iam_state.user_tags.selected)
6401 {
6402 self.iam_state.user_tags.expanded_item =
6403 Some(self.iam_state.user_tags.selected);
6404 }
6405 } else if self.iam_state.policies.expanded_item
6406 != Some(self.iam_state.policies.selected)
6407 {
6408 self.iam_state.policies.toggle_expand();
6409 }
6410 } else if !self.iam_state.users.is_expanded() {
6411 self.iam_state.users.toggle_expand();
6412 }
6413 } else if self.current_service == Service::IamRoles {
6414 if self.iam_state.current_role.is_some() {
6415 if self.iam_state.role_tab == RoleTab::Tags {
6417 if !self.iam_state.tags.is_expanded() {
6418 self.iam_state.tags.expand();
6419 }
6420 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
6421 if !self.iam_state.last_accessed_services.is_expanded() {
6422 self.iam_state.last_accessed_services.expand();
6423 }
6424 } else if !self.iam_state.policies.is_expanded() {
6425 self.iam_state.policies.expand();
6426 }
6427 } else if !self.iam_state.roles.is_expanded() {
6428 self.iam_state.roles.expand();
6429 }
6430 } else if self.current_service == Service::IamUserGroups {
6431 if self.iam_state.current_group.is_some() {
6432 if self.iam_state.group_tab == GroupTab::Users {
6433 if !self.iam_state.group_users.is_expanded() {
6434 self.iam_state.group_users.expand();
6435 }
6436 } else if self.iam_state.group_tab == GroupTab::Permissions {
6437 if !self.iam_state.policies.is_expanded() {
6438 self.iam_state.policies.expand();
6439 }
6440 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6441 && !self.iam_state.last_accessed_services.is_expanded()
6442 {
6443 self.iam_state.last_accessed_services.expand();
6444 }
6445 } else if !self.iam_state.groups.is_expanded() {
6446 self.iam_state.groups.expand();
6447 }
6448 }
6449 }
6450
6451 fn go_to_page(&mut self, page: usize) {
6452 if page == 0 {
6453 return;
6454 }
6455
6456 match self.current_service {
6457 Service::CloudWatchAlarms => {
6458 let alarm_page_size = self.alarms_state.table.page_size.value();
6459 let target = (page - 1) * alarm_page_size;
6460 let filtered_count = match self.alarms_state.alarm_tab {
6461 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
6462 AlarmTab::InAlarm => self
6463 .alarms_state
6464 .table
6465 .items
6466 .iter()
6467 .filter(|a| a.state.to_uppercase() == "ALARM")
6468 .count(),
6469 };
6470 let max_offset = filtered_count.saturating_sub(alarm_page_size);
6471 self.alarms_state.table.scroll_offset = target.min(max_offset);
6472 self.alarms_state.table.selected = self
6473 .alarms_state
6474 .table
6475 .scroll_offset
6476 .min(filtered_count.saturating_sub(1));
6477 }
6478 Service::CloudWatchLogGroups => match self.view_mode {
6479 ViewMode::Events => {
6480 let page_size = 20;
6481 let target = (page - 1) * page_size;
6482 let max = self.log_groups_state.log_events.len().saturating_sub(1);
6483 self.log_groups_state.event_scroll_offset = target.min(max);
6484 }
6485 ViewMode::Detail => {
6486 let page_size = self.log_groups_state.stream_page_size;
6487 self.log_groups_state.stream_current_page = (page - 1).min(
6488 self.log_groups_state
6489 .log_streams
6490 .len()
6491 .div_ceil(page_size)
6492 .saturating_sub(1),
6493 );
6494 self.log_groups_state.selected_stream = 0;
6495 }
6496 ViewMode::List => {
6497 let total = self.log_groups_state.log_groups.items.len();
6498 self.log_groups_state.log_groups.goto_page(page, total);
6499 }
6500 _ => {}
6501 },
6502 Service::EcrRepositories => {
6503 if self.ecr_state.current_repository.is_some() {
6504 let filtered_count = self
6505 .ecr_state
6506 .images
6507 .filtered(|img| {
6508 self.ecr_state.images.filter.is_empty()
6509 || img
6510 .tag
6511 .to_lowercase()
6512 .contains(&self.ecr_state.images.filter.to_lowercase())
6513 || img
6514 .digest
6515 .to_lowercase()
6516 .contains(&self.ecr_state.images.filter.to_lowercase())
6517 })
6518 .len();
6519 self.ecr_state.images.goto_page(page, filtered_count);
6520 } else {
6521 let filtered_count = self
6522 .ecr_state
6523 .repositories
6524 .filtered(|r| {
6525 self.ecr_state.repositories.filter.is_empty()
6526 || r.name
6527 .to_lowercase()
6528 .contains(&self.ecr_state.repositories.filter.to_lowercase())
6529 })
6530 .len();
6531 self.ecr_state.repositories.goto_page(page, filtered_count);
6532 }
6533 }
6534 Service::SqsQueues => {
6535 let filtered_count =
6536 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter)
6537 .len();
6538 self.sqs_state.queues.goto_page(page, filtered_count);
6539 }
6540 Service::S3Buckets => {
6541 if self.s3_state.current_bucket.is_some() {
6542 let page_size = 50; let target = (page - 1) * page_size;
6544 let total_rows = self.calculate_total_object_rows();
6545 let max = total_rows.saturating_sub(1);
6546 self.s3_state.selected_object = target.min(max);
6547 } else {
6548 let page_size = self.s3_state.buckets.page_size.value();
6549 let target = (page - 1) * page_size;
6550 let total_rows = self.calculate_total_bucket_rows();
6551 let max = total_rows.saturating_sub(1);
6552 self.s3_state.selected_row = target.min(max);
6553 self.s3_state.bucket_scroll_offset =
6555 target.min(total_rows.saturating_sub(page_size));
6556 }
6557 }
6558 Service::LambdaFunctions => {
6559 if self.lambda_state.current_function.is_some()
6560 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6561 {
6562 let filtered_count = self
6563 .lambda_state
6564 .version_table
6565 .filtered(|v| {
6566 self.lambda_state.version_table.filter.is_empty()
6567 || v.version.to_lowercase().contains(
6568 &self.lambda_state.version_table.filter.to_lowercase(),
6569 )
6570 || v.aliases.to_lowercase().contains(
6571 &self.lambda_state.version_table.filter.to_lowercase(),
6572 )
6573 || v.description.to_lowercase().contains(
6574 &self.lambda_state.version_table.filter.to_lowercase(),
6575 )
6576 })
6577 .len();
6578 self.lambda_state
6579 .version_table
6580 .goto_page(page, filtered_count);
6581 } else {
6582 let filtered_count = filtered_lambda_functions(self).len();
6583 self.lambda_state.table.goto_page(page, filtered_count);
6584 }
6585 }
6586 Service::LambdaApplications => {
6587 let filtered_count = filtered_lambda_applications(self).len();
6588 self.lambda_application_state
6589 .table
6590 .goto_page(page, filtered_count);
6591 }
6592 Service::CloudFormationStacks => {
6593 let filtered_count = filtered_cloudformation_stacks(self).len();
6594 self.cfn_state.table.goto_page(page, filtered_count);
6595 }
6596 Service::IamUsers => {
6597 let filtered_count = filtered_iam_users(self).len();
6598 self.iam_state.users.goto_page(page, filtered_count);
6599 }
6600 Service::IamRoles => {
6601 let filtered_count = filtered_iam_roles(self).len();
6602 self.iam_state.roles.goto_page(page, filtered_count);
6603 }
6604 _ => {}
6605 }
6606 }
6607
6608 fn prev_pane(&mut self) {
6609 if self.current_service == Service::S3Buckets {
6610 if self.s3_state.current_bucket.is_some() {
6611 let mut visual_idx = 0;
6614 let mut found_obj: Option<S3Object> = None;
6615 let mut parent_idx: Option<usize> = None;
6616
6617 #[allow(clippy::too_many_arguments)]
6619 fn find_with_parent(
6620 objects: &[S3Object],
6621 visual_idx: &mut usize,
6622 target_idx: usize,
6623 expanded_prefixes: &std::collections::HashSet<String>,
6624 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6625 found_obj: &mut Option<S3Object>,
6626 parent_idx: &mut Option<usize>,
6627 current_parent: Option<usize>,
6628 ) {
6629 for obj in objects {
6630 if *visual_idx == target_idx {
6631 *found_obj = Some(obj.clone());
6632 *parent_idx = current_parent;
6633 return;
6634 }
6635 let obj_idx = *visual_idx;
6636 *visual_idx += 1;
6637
6638 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6640 if let Some(preview) = prefix_preview.get(&obj.key) {
6641 find_with_parent(
6642 preview,
6643 visual_idx,
6644 target_idx,
6645 expanded_prefixes,
6646 prefix_preview,
6647 found_obj,
6648 parent_idx,
6649 Some(obj_idx),
6650 );
6651 if found_obj.is_some() {
6652 return;
6653 }
6654 }
6655 }
6656 }
6657 }
6658
6659 find_with_parent(
6660 &self.s3_state.objects,
6661 &mut visual_idx,
6662 self.s3_state.selected_object,
6663 &self.s3_state.expanded_prefixes,
6664 &self.s3_state.prefix_preview,
6665 &mut found_obj,
6666 &mut parent_idx,
6667 None,
6668 );
6669
6670 if let Some(obj) = found_obj {
6671 if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
6672 self.s3_state.expanded_prefixes.remove(&obj.key);
6674 if let Some(parent) = parent_idx {
6675 self.s3_state.selected_object = parent;
6676 }
6677 } else if let Some(parent) = parent_idx {
6678 self.s3_state.selected_object = parent;
6680 }
6681 }
6682
6683 let visible_rows = self.s3_state.object_visible_rows.get();
6685 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
6686 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
6687 } else if self.s3_state.selected_object
6688 >= self.s3_state.object_scroll_offset + visible_rows
6689 {
6690 self.s3_state.object_scroll_offset = self
6691 .s3_state
6692 .selected_object
6693 .saturating_sub(visible_rows - 1);
6694 }
6695 } else {
6696 let mut row_idx = 0;
6698 for bucket in &self.s3_state.buckets.items {
6699 if row_idx == self.s3_state.selected_row {
6700 self.s3_state.expanded_prefixes.remove(&bucket.name);
6702 break;
6703 }
6704 row_idx += 1;
6705 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6706 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6707 #[allow(clippy::too_many_arguments)]
6709 fn check_nested_collapse(
6710 objects: &[S3Object],
6711 row_idx: &mut usize,
6712 target_row: usize,
6713 expanded_prefixes: &mut std::collections::HashSet<String>,
6714 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6715 found: &mut bool,
6716 selected_row: &mut usize,
6717 parent_row: usize,
6718 ) {
6719 for obj in objects {
6720 let current_row = *row_idx;
6721 if *row_idx == target_row {
6722 if obj.is_prefix {
6724 if expanded_prefixes.contains(&obj.key) {
6725 expanded_prefixes.remove(&obj.key);
6727 *selected_row = parent_row;
6728 } else {
6729 *selected_row = parent_row;
6731 }
6732 } else {
6733 *selected_row = parent_row;
6735 }
6736 *found = true;
6737 return;
6738 }
6739 *row_idx += 1;
6740
6741 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6743 if let Some(nested) = prefix_preview.get(&obj.key) {
6744 check_nested_collapse(
6745 nested,
6746 row_idx,
6747 target_row,
6748 expanded_prefixes,
6749 prefix_preview,
6750 found,
6751 selected_row,
6752 current_row,
6753 );
6754 if *found {
6755 return;
6756 }
6757 } else {
6758 *row_idx += 1; }
6760 }
6761 }
6762 }
6763
6764 let mut found = false;
6765 let parent_row = row_idx - 1; check_nested_collapse(
6767 preview,
6768 &mut row_idx,
6769 self.s3_state.selected_row,
6770 &mut self.s3_state.expanded_prefixes,
6771 &self.s3_state.prefix_preview,
6772 &mut found,
6773 &mut self.s3_state.selected_row,
6774 parent_row,
6775 );
6776 if found {
6777 let visible_rows = self.s3_state.bucket_visible_rows.get();
6779 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6780 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6781 } else if self.s3_state.selected_row
6782 >= self.s3_state.bucket_scroll_offset + visible_rows
6783 {
6784 self.s3_state.bucket_scroll_offset =
6785 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6786 }
6787 return;
6788 }
6789 } else {
6790 row_idx += 1;
6791 }
6792 }
6793 }
6794
6795 let visible_rows = self.s3_state.bucket_visible_rows.get();
6797 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6798 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6799 } else if self.s3_state.selected_row
6800 >= self.s3_state.bucket_scroll_offset + visible_rows
6801 {
6802 self.s3_state.bucket_scroll_offset =
6803 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6804 }
6805 }
6806 } else if self.view_mode == ViewMode::InsightsResults {
6807 self.insights_state.insights.results_horizontal_scroll = self
6809 .insights_state
6810 .insights
6811 .results_horizontal_scroll
6812 .saturating_sub(1);
6813 } else if self.current_service == Service::CloudWatchLogGroups
6814 && self.view_mode == ViewMode::List
6815 {
6816 if self.log_groups_state.log_groups.has_expanded_item() {
6818 self.log_groups_state.log_groups.collapse();
6819 }
6820 } else if self.current_service == Service::CloudWatchLogGroups
6821 && self.view_mode == ViewMode::Detail
6822 {
6823 if self.log_groups_state.expanded_stream.is_some() {
6825 self.log_groups_state.expanded_stream = None;
6826 }
6827 } else if self.view_mode == ViewMode::Events {
6828 if self.log_groups_state.expanded_event.is_some() {
6830 self.log_groups_state.expanded_event = None;
6831 }
6832 } else if self.current_service == Service::CloudWatchAlarms {
6833 self.alarms_state.table.collapse();
6835 } else if self.current_service == Service::Ec2Instances {
6836 self.ec2_state.table.collapse();
6837 } else if self.current_service == Service::EcrRepositories {
6838 if self.ecr_state.current_repository.is_some() {
6839 self.ecr_state.images.collapse();
6841 } else {
6842 self.ecr_state.repositories.collapse();
6844 }
6845 } else if self.current_service == Service::SqsQueues {
6846 if self.sqs_state.current_queue.is_some()
6847 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6848 {
6849 self.sqs_state.triggers.collapse();
6850 } else if self.sqs_state.current_queue.is_some()
6851 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6852 {
6853 self.sqs_state.pipes.collapse();
6854 } else if self.sqs_state.current_queue.is_some()
6855 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6856 {
6857 self.sqs_state.tags.collapse();
6858 } else if self.sqs_state.current_queue.is_some()
6859 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6860 {
6861 self.sqs_state.subscriptions.collapse();
6862 } else {
6863 self.sqs_state.queues.collapse();
6864 }
6865 } else if self.current_service == Service::LambdaFunctions {
6866 if self.lambda_state.current_function.is_some()
6867 && self.lambda_state.detail_tab == LambdaDetailTab::Code
6868 {
6869 self.lambda_state.layer_expanded = None;
6871 } else if self.lambda_state.current_function.is_some()
6872 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6873 {
6874 self.lambda_state.version_table.collapse();
6876 } else if self.lambda_state.current_function.is_some()
6877 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6878 || (self.lambda_state.current_version.is_some()
6879 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6880 {
6881 self.lambda_state.alias_table.collapse();
6883 } else if self.lambda_state.current_function.is_none() {
6884 self.lambda_state.table.collapse();
6886 }
6887 } else if self.current_service == Service::LambdaApplications {
6888 if self.lambda_application_state.current_application.is_some() {
6889 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
6891 {
6892 self.lambda_application_state.resources.collapse();
6893 } else {
6894 self.lambda_application_state.deployments.collapse();
6895 }
6896 } else {
6897 if self.lambda_application_state.table.has_expanded_item() {
6899 self.lambda_application_state.table.collapse();
6900 }
6901 }
6902 } else if self.current_service == Service::CloudFormationStacks
6903 && self.cfn_state.current_stack.is_none()
6904 {
6905 self.cfn_state.table.collapse();
6906 } else if self.current_service == Service::CloudFormationStacks
6907 && self.cfn_state.current_stack.is_some()
6908 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6909 {
6910 self.cfn_state.parameters.collapse();
6911 } else if self.current_service == Service::CloudFormationStacks
6912 && self.cfn_state.current_stack.is_some()
6913 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6914 {
6915 self.cfn_state.outputs.collapse();
6916 } else if self.current_service == Service::CloudFormationStacks
6917 && self.cfn_state.current_stack.is_some()
6918 && self.cfn_state.detail_tab == CfnDetailTab::Resources
6919 {
6920 self.cfn_state.resources.collapse();
6921 } else if self.current_service == Service::IamUsers {
6922 if self.iam_state.users.has_expanded_item() {
6923 self.iam_state.users.collapse();
6924 }
6925 } else if self.current_service == Service::IamRoles {
6926 if self.view_mode == ViewMode::PolicyView {
6927 self.view_mode = ViewMode::Detail;
6929 self.iam_state.current_policy = None;
6930 self.iam_state.policy_document.clear();
6931 self.iam_state.policy_scroll = 0;
6932 } else if self.iam_state.current_role.is_some() {
6933 if self.iam_state.role_tab == RoleTab::Tags
6934 && self.iam_state.tags.has_expanded_item()
6935 {
6936 self.iam_state.tags.collapse();
6937 } else if self.iam_state.role_tab == RoleTab::LastAccessed
6938 && self
6939 .iam_state
6940 .last_accessed_services
6941 .expanded_item
6942 .is_some()
6943 {
6944 self.iam_state.last_accessed_services.collapse();
6945 } else if self.iam_state.policies.has_expanded_item() {
6946 self.iam_state.policies.collapse();
6947 }
6948 } else if self.iam_state.roles.has_expanded_item() {
6949 self.iam_state.roles.collapse();
6950 }
6951 } else if self.current_service == Service::IamUserGroups {
6952 if self.iam_state.current_group.is_some() {
6953 if self.iam_state.group_tab == GroupTab::Users
6954 && self.iam_state.group_users.has_expanded_item()
6955 {
6956 self.iam_state.group_users.collapse();
6957 } else if self.iam_state.group_tab == GroupTab::Permissions
6958 && self.iam_state.policies.has_expanded_item()
6959 {
6960 self.iam_state.policies.collapse();
6961 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6962 && self
6963 .iam_state
6964 .last_accessed_services
6965 .expanded_item
6966 .is_some()
6967 {
6968 self.iam_state.last_accessed_services.collapse();
6969 }
6970 } else if self.iam_state.groups.has_expanded_item() {
6971 self.iam_state.groups.collapse();
6972 }
6973 }
6974 }
6975
6976 fn collapse_row(&mut self) {
6977 match self.current_service {
6978 Service::S3Buckets => {
6979 if self.s3_state.current_bucket.is_none() {
6980 let filtered_buckets: Vec<_> = self
6982 .s3_state
6983 .buckets
6984 .items
6985 .iter()
6986 .filter(|b| {
6987 if self.s3_state.buckets.filter.is_empty() {
6988 true
6989 } else {
6990 b.name
6991 .to_lowercase()
6992 .contains(&self.s3_state.buckets.filter.to_lowercase())
6993 }
6994 })
6995 .collect();
6996
6997 let mut row_idx = 0;
6999
7000 for bucket in filtered_buckets {
7001 if row_idx == self.s3_state.selected_row {
7002 self.s3_state.expanded_prefixes.remove(&bucket.name);
7004 break;
7006 }
7007 row_idx += 1;
7008 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7009 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
7011 continue;
7013 }
7014 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7015 #[allow(clippy::too_many_arguments)]
7017 fn check_nested_collapse(
7018 objects: &[S3Object],
7019 row_idx: &mut usize,
7020 target_row: usize,
7021 expanded_prefixes: &mut std::collections::HashSet<String>,
7022 prefix_preview: &std::collections::HashMap<
7023 String,
7024 Vec<S3Object>,
7025 >,
7026 found: &mut bool,
7027 selected_row: &mut usize,
7028 parent_row: usize,
7029 ) {
7030 for obj in objects {
7031 let current_row = *row_idx;
7032 if *row_idx == target_row {
7033 if obj.is_prefix {
7035 if expanded_prefixes.contains(&obj.key) {
7036 expanded_prefixes.remove(&obj.key);
7038 *selected_row = parent_row;
7039 } else {
7040 *selected_row = parent_row;
7042 }
7043 } else {
7044 *selected_row = parent_row;
7046 }
7047 *found = true;
7048 return;
7049 }
7050 *row_idx += 1;
7051
7052 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7054 if let Some(nested) = prefix_preview.get(&obj.key) {
7055 check_nested_collapse(
7056 nested,
7057 row_idx,
7058 target_row,
7059 expanded_prefixes,
7060 prefix_preview,
7061 found,
7062 selected_row,
7063 current_row,
7064 );
7065 if *found {
7066 return;
7067 }
7068 } else {
7069 *row_idx += 1; }
7071 }
7072 }
7073 }
7074
7075 let mut found = false;
7076 let parent_row = row_idx - 1; check_nested_collapse(
7078 preview,
7079 &mut row_idx,
7080 self.s3_state.selected_row,
7081 &mut self.s3_state.expanded_prefixes,
7082 &self.s3_state.prefix_preview,
7083 &mut found,
7084 &mut self.s3_state.selected_row,
7085 parent_row,
7086 );
7087 if found {
7088 let visible_rows = self.s3_state.bucket_visible_rows.get();
7090 if self.s3_state.selected_row
7091 < self.s3_state.bucket_scroll_offset
7092 {
7093 self.s3_state.bucket_scroll_offset =
7094 self.s3_state.selected_row;
7095 } else if self.s3_state.selected_row
7096 >= self.s3_state.bucket_scroll_offset + visible_rows
7097 {
7098 self.s3_state.bucket_scroll_offset = self
7099 .s3_state
7100 .selected_row
7101 .saturating_sub(visible_rows - 1);
7102 }
7103 return;
7104 }
7105 } else {
7106 row_idx += 1;
7107 }
7108 }
7109 }
7110
7111 let visible_rows = self.s3_state.bucket_visible_rows.get();
7113 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
7114 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
7115 } else if self.s3_state.selected_row
7116 >= self.s3_state.bucket_scroll_offset + visible_rows
7117 {
7118 self.s3_state.bucket_scroll_offset =
7119 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
7120 }
7121 }
7122 }
7123 Service::CloudWatchLogGroups => {
7124 if self.view_mode == ViewMode::Events {
7125 if let Some(idx) = self.log_groups_state.expanded_event {
7126 self.log_groups_state.expanded_event = None;
7127 self.log_groups_state.selected_event = idx;
7128 }
7129 } else if self.view_mode == ViewMode::Detail {
7130 if let Some(idx) = self.log_groups_state.expanded_stream {
7131 self.log_groups_state.expanded_stream = None;
7132 self.log_groups_state.selected_stream = idx;
7133 }
7134 } else {
7135 self.log_groups_state.log_groups.collapse();
7136 }
7137 }
7138 Service::CloudWatchAlarms => self.alarms_state.table.collapse(),
7139 Service::Ec2Instances => {
7140 if self.ec2_state.current_instance.is_some()
7141 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
7142 {
7143 self.ec2_state.tags.collapse();
7144 } else {
7145 self.ec2_state.table.collapse();
7146 }
7147 }
7148 Service::EcrRepositories => {
7149 if self.ecr_state.current_repository.is_some() {
7150 self.ecr_state.images.collapse();
7151 } else {
7152 self.ecr_state.repositories.collapse();
7153 }
7154 }
7155 Service::LambdaFunctions => self.lambda_state.table.collapse(),
7156 Service::SqsQueues => self.sqs_state.queues.collapse(),
7157 Service::CloudFormationStacks => {
7158 if self.cfn_state.current_stack.is_some() {
7159 match self.cfn_state.detail_tab {
7160 crate::ui::cfn::DetailTab::Resources => {
7161 self.cfn_state.resources.collapse();
7162 }
7163 crate::ui::cfn::DetailTab::Parameters => {
7164 self.cfn_state.parameters.collapse();
7165 }
7166 crate::ui::cfn::DetailTab::Outputs => {
7167 self.cfn_state.outputs.collapse();
7168 }
7169 _ => {}
7170 }
7171 } else {
7172 self.cfn_state.table.collapse();
7173 }
7174 }
7175 Service::IamUsers => {
7176 if self.iam_state.current_user.is_some() {
7177 match self.iam_state.user_tab {
7178 crate::ui::iam::UserTab::Permissions => {
7179 self.iam_state.policies.collapse();
7180 }
7181 crate::ui::iam::UserTab::Groups => {
7182 self.iam_state.user_group_memberships.collapse();
7183 }
7184 crate::ui::iam::UserTab::Tags => {
7185 self.iam_state.user_tags.collapse();
7186 }
7187 _ => {}
7188 }
7189 } else {
7190 self.iam_state.users.collapse();
7191 }
7192 }
7193 Service::IamRoles => {
7194 if self.iam_state.current_role.is_some() {
7195 match self.iam_state.role_tab {
7196 crate::ui::iam::RoleTab::Permissions => {
7197 self.iam_state.policies.collapse();
7198 }
7199 crate::ui::iam::RoleTab::Tags => {
7200 self.iam_state.tags.collapse();
7201 }
7202 _ => {}
7203 }
7204 } else {
7205 self.iam_state.roles.collapse();
7206 }
7207 }
7208 Service::IamUserGroups => self.iam_state.groups.collapse(),
7209 _ => {}
7210 }
7211 }
7212
7213 fn expand_row(&mut self) {
7214 match self.current_service {
7215 Service::S3Buckets => {
7216 if self.s3_state.current_bucket.is_none() {
7217 let filtered_buckets: Vec<_> = self
7219 .s3_state
7220 .buckets
7221 .items
7222 .iter()
7223 .filter(|b| {
7224 if self.s3_state.buckets.filter.is_empty() {
7225 true
7226 } else {
7227 b.name
7228 .to_lowercase()
7229 .contains(&self.s3_state.buckets.filter.to_lowercase())
7230 }
7231 })
7232 .collect();
7233
7234 fn check_nested_expand(
7236 objects: &[S3Object],
7237 row_idx: &mut usize,
7238 target_row: usize,
7239 expanded_prefixes: &mut std::collections::HashSet<String>,
7240 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7241 ) -> Option<(bool, usize)> {
7242 for obj in objects {
7243 if *row_idx == target_row {
7244 if obj.is_prefix {
7245 if expanded_prefixes.contains(&obj.key) {
7247 expanded_prefixes.remove(&obj.key);
7248 return Some((true, *row_idx));
7249 } else {
7250 expanded_prefixes.insert(obj.key.clone());
7251 return Some((true, *row_idx + 1)); }
7253 }
7254 return Some((false, *row_idx));
7255 }
7256 *row_idx += 1;
7257
7258 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7260 if let Some(nested) = prefix_preview.get(&obj.key) {
7261 if let Some(result) = check_nested_expand(
7262 nested,
7263 row_idx,
7264 target_row,
7265 expanded_prefixes,
7266 prefix_preview,
7267 ) {
7268 return Some(result);
7269 }
7270 }
7271 }
7272 }
7273 None
7274 }
7275
7276 let mut row_idx = 0;
7277 for bucket in filtered_buckets {
7278 if row_idx == self.s3_state.selected_row {
7279 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7281 self.s3_state.expanded_prefixes.remove(&bucket.name);
7282 } else {
7283 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
7284 self.s3_state.selected_row = row_idx + 1; self.s3_state.buckets.loading = true;
7286 }
7287 return;
7288 }
7289 row_idx += 1;
7290
7291 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7292 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
7293 continue;
7294 }
7295 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7296 if let Some((loading, new_row)) = check_nested_expand(
7297 preview,
7298 &mut row_idx,
7299 self.s3_state.selected_row,
7300 &mut self.s3_state.expanded_prefixes,
7301 &self.s3_state.prefix_preview,
7302 ) {
7303 self.s3_state.selected_row = new_row;
7304 if loading {
7305 self.s3_state.buckets.loading = true;
7306 }
7307 return;
7308 }
7309 }
7310 }
7311 }
7312 } else {
7313 fn check_object_expand(
7315 objects: &[S3Object],
7316 row_idx: &mut usize,
7317 target_row: usize,
7318 expanded_prefixes: &mut std::collections::HashSet<String>,
7319 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7320 ) -> Option<(bool, usize)> {
7321 for obj in objects {
7322 if *row_idx == target_row {
7323 if obj.is_prefix {
7324 if expanded_prefixes.contains(&obj.key) {
7325 expanded_prefixes.remove(&obj.key);
7326 return Some((true, *row_idx));
7327 } else {
7328 expanded_prefixes.insert(obj.key.clone());
7329 return Some((true, *row_idx + 1));
7330 }
7331 }
7332 return Some((false, *row_idx));
7333 }
7334 *row_idx += 1;
7335
7336 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7337 if let Some(nested) = prefix_preview.get(&obj.key) {
7338 if let Some(result) = check_object_expand(
7339 nested,
7340 row_idx,
7341 target_row,
7342 expanded_prefixes,
7343 prefix_preview,
7344 ) {
7345 return Some(result);
7346 }
7347 }
7348 }
7349 }
7350 None
7351 }
7352
7353 let mut row_idx = 0;
7354 if let Some((loading, new_row)) = check_object_expand(
7355 &self.s3_state.objects,
7356 &mut row_idx,
7357 self.s3_state.selected_object,
7358 &mut self.s3_state.expanded_prefixes,
7359 &self.s3_state.prefix_preview,
7360 ) {
7361 self.s3_state.selected_object = new_row;
7362 if loading {
7363 self.s3_state.buckets.loading = true;
7364 }
7365 }
7366 }
7367 }
7368 _ => {
7369 self.next_pane();
7371 }
7372 }
7373 }
7374
7375 fn select_item(&mut self) {
7376 if self.mode == Mode::RegionPicker {
7377 let filtered = self.get_filtered_regions();
7378 if let Some(region) = filtered.get(self.region_picker_selected) {
7379 if !self.tabs.is_empty() {
7381 let mut session = Session::new(
7382 self.profile.clone(),
7383 self.region.clone(),
7384 self.config.account_id.clone(),
7385 self.config.role_arn.clone(),
7386 );
7387
7388 for tab in &self.tabs {
7389 session.tabs.push(SessionTab {
7390 service: format!("{:?}", tab.service),
7391 title: tab.title.clone(),
7392 breadcrumb: tab.breadcrumb.clone(),
7393 filter: None,
7394 selected_item: None,
7395 });
7396 }
7397
7398 let _ = session.save();
7399 }
7400
7401 self.region = region.code.to_string();
7402 self.config.region = region.code.to_string();
7403
7404 self.tabs.clear();
7406 self.current_tab = 0;
7407 self.service_selected = false;
7408
7409 self.mode = Mode::Normal;
7410 }
7411 } else if self.mode == Mode::ProfilePicker {
7412 let filtered = self.get_filtered_profiles();
7413 if let Some(profile) = filtered.get(self.profile_picker_selected) {
7414 let profile_name = profile.name.clone();
7415 let profile_region = profile.region.clone();
7416
7417 self.profile = profile_name.clone();
7418 std::env::set_var("AWS_PROFILE", &profile_name);
7419
7420 if let Some(region) = profile_region {
7422 self.region = region;
7423 }
7424
7425 self.mode = Mode::Normal;
7426 }
7428 } else if self.mode == Mode::ServicePicker {
7429 let filtered = self.filtered_services();
7430 if let Some(&service) = filtered.get(self.service_picker.selected) {
7431 let new_service = match service {
7432 "CloudWatch > Log Groups" => Service::CloudWatchLogGroups,
7433 "CloudWatch > Logs Insights" => Service::CloudWatchInsights,
7434 "CloudWatch > Alarms" => Service::CloudWatchAlarms,
7435 "CloudFormation > Stacks" => Service::CloudFormationStacks,
7436 "EC2 > Instances" => Service::Ec2Instances,
7437 "ECR > Repositories" => Service::EcrRepositories,
7438 "IAM > Users" => Service::IamUsers,
7439 "IAM > Roles" => Service::IamRoles,
7440 "IAM > User Groups" => Service::IamUserGroups,
7441 "Lambda > Functions" => Service::LambdaFunctions,
7442 "Lambda > Applications" => Service::LambdaApplications,
7443 "S3 > Buckets" => Service::S3Buckets,
7444 "SQS > Queues" => Service::SqsQueues,
7445 _ => return,
7446 };
7447
7448 self.tabs.push(Tab {
7450 service: new_service,
7451 title: service.to_string(),
7452 breadcrumb: service.to_string(),
7453 });
7454 self.current_tab = self.tabs.len() - 1;
7455 self.current_service = new_service;
7456 self.view_mode = ViewMode::List;
7457 self.service_selected = true;
7458 self.mode = Mode::Normal;
7459 }
7460 } else if self.mode == Mode::TabPicker {
7461 let filtered = self.get_filtered_tabs();
7462 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
7463 self.current_tab = idx;
7464 self.current_service = self.tabs[idx].service;
7465 self.mode = Mode::Normal;
7466 self.tab_filter.clear();
7467 }
7468 } else if self.mode == Mode::SessionPicker {
7469 let filtered = self.get_filtered_sessions();
7470 if let Some(&session) = filtered.get(self.session_picker_selected) {
7471 let session = session.clone();
7472
7473 self.current_session = Some(session.clone());
7475 self.profile = session.profile.clone();
7476 self.region = session.region.clone();
7477 self.config.region = session.region.clone();
7478 self.config.account_id = session.account_id.clone();
7479 self.config.role_arn = session.role_arn.clone();
7480
7481 self.tabs = session
7483 .tabs
7484 .iter()
7485 .map(|st| Tab {
7486 service: match st.service.as_str() {
7487 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
7488 "CloudWatchInsights" => Service::CloudWatchInsights,
7489 "CloudWatchAlarms" => Service::CloudWatchAlarms,
7490 "S3Buckets" => Service::S3Buckets,
7491 "SqsQueues" => Service::SqsQueues,
7492 _ => Service::CloudWatchLogGroups,
7493 },
7494 title: st.title.clone(),
7495 breadcrumb: st.breadcrumb.clone(),
7496 })
7497 .collect();
7498
7499 if !self.tabs.is_empty() {
7500 self.current_tab = 0;
7501 self.current_service = self.tabs[0].service;
7502 self.service_selected = true;
7503 }
7504
7505 self.mode = Mode::Normal;
7506 }
7507 } else if self.mode == Mode::InsightsInput {
7508 use crate::app::InsightsFocus;
7510 match self.insights_state.insights.insights_focus {
7511 InsightsFocus::Query => {
7512 self.insights_state.insights.query_text.push('\n');
7514 self.insights_state.insights.query_cursor_line += 1;
7515 self.insights_state.insights.query_cursor_col = 0;
7516 }
7517 InsightsFocus::LogGroupSearch => {
7518 self.insights_state.insights.show_dropdown =
7520 !self.insights_state.insights.show_dropdown;
7521 }
7522 _ => {}
7523 }
7524 } else if self.mode == Mode::Normal {
7525 if !self.service_selected {
7527 let filtered = self.filtered_services();
7528 if let Some(&service) = filtered.get(self.service_picker.selected) {
7529 match service {
7530 "CloudWatch > Log Groups" => {
7531 self.current_service = Service::CloudWatchLogGroups;
7532 self.view_mode = ViewMode::List;
7533 self.service_selected = true;
7534 }
7535 "CloudWatch > Logs Insights" => {
7536 self.current_service = Service::CloudWatchInsights;
7537 self.view_mode = ViewMode::InsightsResults;
7538 self.service_selected = true;
7539 }
7540 "CloudWatch > Alarms" => {
7541 self.current_service = Service::CloudWatchAlarms;
7542 self.view_mode = ViewMode::List;
7543 self.service_selected = true;
7544 }
7545 "S3 > Buckets" => {
7546 self.current_service = Service::S3Buckets;
7547 self.view_mode = ViewMode::List;
7548 self.service_selected = true;
7549 }
7550 "EC2 > Instances" => {
7551 self.current_service = Service::Ec2Instances;
7552 self.view_mode = ViewMode::List;
7553 self.service_selected = true;
7554 }
7555 "ECR > Repositories" => {
7556 self.current_service = Service::EcrRepositories;
7557 self.view_mode = ViewMode::List;
7558 self.service_selected = true;
7559 }
7560 "Lambda > Functions" => {
7561 self.current_service = Service::LambdaFunctions;
7562 self.view_mode = ViewMode::List;
7563 self.service_selected = true;
7564 }
7565 "Lambda > Applications" => {
7566 self.current_service = Service::LambdaApplications;
7567 self.view_mode = ViewMode::List;
7568 self.service_selected = true;
7569 }
7570 _ => {}
7571 }
7572 }
7573 return;
7574 }
7575
7576 if self.view_mode == ViewMode::InsightsResults {
7578 if self.insights_state.insights.expanded_result
7580 == Some(self.insights_state.insights.results_selected)
7581 {
7582 self.insights_state.insights.expanded_result = None;
7583 } else {
7584 self.insights_state.insights.expanded_result =
7585 Some(self.insights_state.insights.results_selected);
7586 }
7587 } else if self.current_service == Service::S3Buckets {
7588 if self.s3_state.current_bucket.is_none() {
7589 let filtered_buckets: Vec<_> = self
7591 .s3_state
7592 .buckets
7593 .items
7594 .iter()
7595 .filter(|b| {
7596 if self.s3_state.buckets.filter.is_empty() {
7597 true
7598 } else {
7599 b.name
7600 .to_lowercase()
7601 .contains(&self.s3_state.buckets.filter.to_lowercase())
7602 }
7603 })
7604 .collect();
7605
7606 let mut row_idx = 0;
7608 for bucket in filtered_buckets {
7609 if row_idx == self.s3_state.selected_row {
7610 self.s3_state.current_bucket = Some(bucket.name.clone());
7612 self.s3_state.prefix_stack.clear();
7613 self.s3_state.buckets.loading = true;
7614 return;
7615 }
7616 row_idx += 1;
7617
7618 if self.s3_state.bucket_errors.contains_key(&bucket.name)
7620 && self.s3_state.expanded_prefixes.contains(&bucket.name)
7621 {
7622 continue;
7623 }
7624
7625 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7626 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7627 for obj in preview {
7628 if row_idx == self.s3_state.selected_row {
7629 if obj.is_prefix {
7631 self.s3_state.current_bucket =
7632 Some(bucket.name.clone());
7633 self.s3_state.prefix_stack = vec![obj.key.clone()];
7634 self.s3_state.buckets.loading = true;
7635 }
7636 return;
7637 }
7638 row_idx += 1;
7639
7640 if obj.is_prefix
7642 && self.s3_state.expanded_prefixes.contains(&obj.key)
7643 {
7644 if let Some(nested) =
7645 self.s3_state.prefix_preview.get(&obj.key)
7646 {
7647 for nested_obj in nested {
7648 if row_idx == self.s3_state.selected_row {
7649 if nested_obj.is_prefix {
7651 self.s3_state.current_bucket =
7652 Some(bucket.name.clone());
7653 self.s3_state.prefix_stack = vec![
7655 obj.key.clone(),
7656 nested_obj.key.clone(),
7657 ];
7658 self.s3_state.buckets.loading = true;
7659 }
7660 return;
7661 }
7662 row_idx += 1;
7663 }
7664 } else {
7665 row_idx += 1;
7666 }
7667 }
7668 }
7669 } else {
7670 row_idx += 1;
7671 }
7672 }
7673 }
7674 } else {
7675 let mut visual_idx = 0;
7677 let mut found_obj: Option<S3Object> = None;
7678
7679 fn check_nested_select(
7681 obj: &S3Object,
7682 visual_idx: &mut usize,
7683 target_idx: usize,
7684 expanded_prefixes: &std::collections::HashSet<String>,
7685 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7686 found_obj: &mut Option<S3Object>,
7687 ) {
7688 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7689 if let Some(preview) = prefix_preview.get(&obj.key) {
7690 for nested_obj in preview {
7691 if *visual_idx == target_idx {
7692 *found_obj = Some(nested_obj.clone());
7693 return;
7694 }
7695 *visual_idx += 1;
7696
7697 check_nested_select(
7699 nested_obj,
7700 visual_idx,
7701 target_idx,
7702 expanded_prefixes,
7703 prefix_preview,
7704 found_obj,
7705 );
7706 if found_obj.is_some() {
7707 return;
7708 }
7709 }
7710 } else {
7711 *visual_idx += 1;
7713 }
7714 }
7715 }
7716
7717 for obj in &self.s3_state.objects {
7718 if visual_idx == self.s3_state.selected_object {
7719 found_obj = Some(obj.clone());
7720 break;
7721 }
7722 visual_idx += 1;
7723
7724 check_nested_select(
7726 obj,
7727 &mut visual_idx,
7728 self.s3_state.selected_object,
7729 &self.s3_state.expanded_prefixes,
7730 &self.s3_state.prefix_preview,
7731 &mut found_obj,
7732 );
7733 if found_obj.is_some() {
7734 break;
7735 }
7736 }
7737
7738 if let Some(obj) = found_obj {
7739 if obj.is_prefix {
7740 self.s3_state.prefix_stack.push(obj.key.clone());
7742 self.s3_state.buckets.loading = true;
7743 }
7744 }
7745 }
7746 } else if self.current_service == Service::CloudFormationStacks {
7747 if self.cfn_state.current_stack.is_none() {
7748 let filtered_stacks = filtered_cloudformation_stacks(self);
7750 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
7751 let stack_name = stack.name.clone();
7752 let mut tags = stack.tags.clone();
7753 tags.sort_by(|a, b| a.0.cmp(&b.0));
7754
7755 self.cfn_state.current_stack = Some(stack_name);
7756 self.cfn_state.tags.items = tags;
7757 self.cfn_state.tags.reset();
7758 self.cfn_state.table.loading = true;
7759 self.update_current_tab_breadcrumb();
7760 }
7761 }
7762 } else if self.current_service == Service::EcrRepositories {
7763 if self.ecr_state.current_repository.is_none() {
7764 let filtered_repos = filtered_ecr_repositories(self);
7766 if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
7767 let repo_name = repo.name.clone();
7768 let repo_uri = repo.uri.clone();
7769 self.ecr_state.current_repository = Some(repo_name);
7770 self.ecr_state.current_repository_uri = Some(repo_uri);
7771 self.ecr_state.images.reset();
7772 self.ecr_state.repositories.loading = true;
7773 }
7774 }
7775 } else if self.current_service == Service::Ec2Instances {
7776 if self.ec2_state.current_instance.is_none() {
7777 let filtered_instances = filtered_ec2_instances(self);
7778 if let Some(instance) = self.ec2_state.table.get_selected(&filtered_instances) {
7779 self.ec2_state.current_instance = Some(instance.instance_id.clone());
7780 self.view_mode = ViewMode::Detail;
7781 self.update_current_tab_breadcrumb();
7782 }
7783 }
7784 } else if self.current_service == Service::SqsQueues {
7785 if self.sqs_state.current_queue.is_none() {
7786 let filtered_queues = filtered_queues(
7787 &self.sqs_state.queues.items,
7788 &self.sqs_state.queues.filter,
7789 );
7790 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
7791 self.sqs_state.current_queue = Some(queue.url.clone());
7792
7793 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
7794 self.sqs_state.metrics_loading = true;
7795 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
7796 self.sqs_state.triggers.loading = true;
7797 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
7798 self.sqs_state.pipes.loading = true;
7799 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
7800 self.sqs_state.tags.loading = true;
7801 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
7802 self.sqs_state.subscriptions.loading = true;
7803 }
7804 }
7805 }
7806 } else if self.current_service == Service::IamUsers {
7807 if self.iam_state.current_user.is_some() {
7808 if self.iam_state.user_tab == UserTab::Permissions {
7810 let filtered = filtered_iam_policies(self);
7811 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
7812 self.iam_state.current_policy = Some(policy.policy_name.clone());
7813 self.iam_state.policy_scroll = 0;
7814 self.view_mode = ViewMode::PolicyView;
7815 self.iam_state.policies.loading = true;
7816 self.update_current_tab_breadcrumb();
7817 }
7818 }
7819 } else if self.iam_state.current_user.is_none() {
7820 let filtered_users = filtered_iam_users(self);
7821 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
7822 self.iam_state.current_user = Some(user.user_name.clone());
7823 self.iam_state.user_tab = UserTab::Permissions;
7824 self.iam_state.policies.reset();
7825 self.update_current_tab_breadcrumb();
7826 }
7827 }
7828 } else if self.current_service == Service::IamRoles {
7829 if self.iam_state.current_role.is_some() {
7830 if self.iam_state.role_tab == RoleTab::Permissions {
7832 let filtered = filtered_iam_policies(self);
7833 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
7834 self.iam_state.current_policy = Some(policy.policy_name.clone());
7835 self.iam_state.policy_scroll = 0;
7836 self.view_mode = ViewMode::PolicyView;
7837 self.iam_state.policies.loading = true;
7838 self.update_current_tab_breadcrumb();
7839 }
7840 }
7841 } else if self.iam_state.current_role.is_none() {
7842 let filtered_roles = filtered_iam_roles(self);
7843 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
7844 self.iam_state.current_role = Some(role.role_name.clone());
7845 self.iam_state.role_tab = RoleTab::Permissions;
7846 self.iam_state.policies.reset();
7847 self.update_current_tab_breadcrumb();
7848 }
7849 }
7850 } else if self.current_service == Service::IamUserGroups {
7851 if self.iam_state.current_group.is_none() {
7852 let filtered_groups: Vec<_> = self
7853 .iam_state
7854 .groups
7855 .items
7856 .iter()
7857 .filter(|g| {
7858 if self.iam_state.groups.filter.is_empty() {
7859 true
7860 } else {
7861 g.group_name
7862 .to_lowercase()
7863 .contains(&self.iam_state.groups.filter.to_lowercase())
7864 }
7865 })
7866 .collect();
7867 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
7868 self.iam_state.current_group = Some(group.group_name.clone());
7869 self.update_current_tab_breadcrumb();
7870 }
7871 }
7872 } else if self.current_service == Service::LambdaFunctions {
7873 if self.lambda_state.current_function.is_some()
7874 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
7875 {
7876 if self.mode == Mode::Normal {
7879 let page_size = self.lambda_state.version_table.page_size.value();
7880 let filtered: Vec<_> = self
7881 .lambda_state
7882 .version_table
7883 .items
7884 .iter()
7885 .filter(|v| {
7886 self.lambda_state.version_table.filter.is_empty()
7887 || v.version.to_lowercase().contains(
7888 &self.lambda_state.version_table.filter.to_lowercase(),
7889 )
7890 || v.aliases.to_lowercase().contains(
7891 &self.lambda_state.version_table.filter.to_lowercase(),
7892 )
7893 })
7894 .collect();
7895 let current_page = self.lambda_state.version_table.selected / page_size;
7896 let start_idx = current_page * page_size;
7897 let end_idx = (start_idx + page_size).min(filtered.len());
7898 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
7899 let page_index = self.lambda_state.version_table.selected % page_size;
7900 if let Some(version) = paginated.get(page_index) {
7901 self.lambda_state.current_version = Some(version.version.clone());
7902 self.lambda_state.detail_tab = LambdaDetailTab::Code;
7903 }
7904 } else {
7905 if self.lambda_state.version_table.expanded_item
7907 == Some(self.lambda_state.version_table.selected)
7908 {
7909 self.lambda_state.version_table.collapse();
7910 } else {
7911 self.lambda_state.version_table.expanded_item =
7912 Some(self.lambda_state.version_table.selected);
7913 }
7914 }
7915 } else if self.lambda_state.current_function.is_some()
7916 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
7917 {
7918 let filtered: Vec<_> = self
7920 .lambda_state
7921 .alias_table
7922 .items
7923 .iter()
7924 .filter(|a| {
7925 self.lambda_state.alias_table.filter.is_empty()
7926 || a.name
7927 .to_lowercase()
7928 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
7929 || a.versions
7930 .to_lowercase()
7931 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
7932 })
7933 .collect();
7934 if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
7935 self.lambda_state.current_alias = Some(alias.name.clone());
7936 }
7937 } else if self.lambda_state.current_function.is_none() {
7938 let filtered_functions = filtered_lambda_functions(self);
7939 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
7940 self.lambda_state.current_function = Some(func.name.clone());
7941 self.lambda_state.detail_tab = LambdaDetailTab::Code;
7942 self.update_current_tab_breadcrumb();
7943 }
7944 }
7945 } else if self.current_service == Service::LambdaApplications {
7946 let filtered = filtered_lambda_applications(self);
7947 if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
7948 let app_name = app.name.clone();
7949 self.lambda_application_state.current_application = Some(app_name.clone());
7950 self.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Overview;
7951
7952 use crate::lambda::Resource;
7954 self.lambda_application_state.resources.items = vec![
7955 Resource {
7956 logical_id: "ApiGatewayRestApi".to_string(),
7957 physical_id: "abc123xyz".to_string(),
7958 resource_type: "AWS::ApiGateway::RestApi".to_string(),
7959 last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
7960 },
7961 Resource {
7962 logical_id: "LambdaFunction".to_string(),
7963 physical_id: format!("{}-function", app_name),
7964 resource_type: "AWS::Lambda::Function".to_string(),
7965 last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
7966 },
7967 Resource {
7968 logical_id: "DynamoDBTable".to_string(),
7969 physical_id: format!("{}-table", app_name),
7970 resource_type: "AWS::DynamoDB::Table".to_string(),
7971 last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
7972 },
7973 ];
7974
7975 use crate::lambda::Deployment;
7977 self.lambda_application_state.deployments.items = vec![
7978 Deployment {
7979 deployment_id: "d-ABC123XYZ".to_string(),
7980 resource_type: "AWS::Serverless::Application".to_string(),
7981 last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
7982 status: "Succeeded".to_string(),
7983 },
7984 Deployment {
7985 deployment_id: "d-DEF456UVW".to_string(),
7986 resource_type: "AWS::Serverless::Application".to_string(),
7987 last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
7988 status: "Succeeded".to_string(),
7989 },
7990 ];
7991
7992 self.update_current_tab_breadcrumb();
7993 }
7994 } else if self.current_service == Service::CloudWatchLogGroups {
7995 if self.view_mode == ViewMode::List {
7996 let filtered_groups = filtered_log_groups(self);
7998 if let Some(selected_group) =
7999 filtered_groups.get(self.log_groups_state.log_groups.selected)
8000 {
8001 if let Some(actual_idx) = self
8002 .log_groups_state
8003 .log_groups
8004 .items
8005 .iter()
8006 .position(|g| g.name == selected_group.name)
8007 {
8008 self.log_groups_state.log_groups.selected = actual_idx;
8009 }
8010 }
8011 self.view_mode = ViewMode::Detail;
8012 self.log_groups_state.log_streams.clear();
8013 self.log_groups_state.selected_stream = 0;
8014 self.log_groups_state.loading = true;
8015 self.update_current_tab_breadcrumb();
8016 } else if self.view_mode == ViewMode::Detail {
8017 let filtered_streams = filtered_log_streams(self);
8019 if let Some(selected_stream) =
8020 filtered_streams.get(self.log_groups_state.selected_stream)
8021 {
8022 if let Some(actual_idx) = self
8023 .log_groups_state
8024 .log_streams
8025 .iter()
8026 .position(|s| s.name == selected_stream.name)
8027 {
8028 self.log_groups_state.selected_stream = actual_idx;
8029 }
8030 }
8031 self.view_mode = ViewMode::Events;
8032 self.update_current_tab_breadcrumb();
8033 self.log_groups_state.log_events.clear();
8034 self.log_groups_state.event_scroll_offset = 0;
8035 self.log_groups_state.next_backward_token = None;
8036 self.log_groups_state.loading = true;
8037 } else if self.view_mode == ViewMode::Events {
8038 if self.log_groups_state.expanded_event
8040 == Some(self.log_groups_state.event_scroll_offset)
8041 {
8042 self.log_groups_state.expanded_event = None;
8043 } else {
8044 self.log_groups_state.expanded_event =
8045 Some(self.log_groups_state.event_scroll_offset);
8046 }
8047 }
8048 } else if self.current_service == Service::CloudWatchAlarms {
8049 self.alarms_state.table.toggle_expand();
8051 } else if self.current_service == Service::CloudWatchInsights {
8052 if !self.insights_state.insights.selected_log_groups.is_empty() {
8054 self.log_groups_state.loading = true;
8055 self.insights_state.insights.query_completed = true;
8056 }
8057 }
8058 }
8059 }
8060
8061 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
8062 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
8063 Ok(())
8064 }
8065
8066 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
8067 let alarms = self.alarms_client.list_alarms().await?;
8068 self.alarms_state.table.items = alarms
8069 .into_iter()
8070 .map(
8071 |(
8072 name,
8073 state,
8074 state_updated,
8075 description,
8076 metric_name,
8077 namespace,
8078 statistic,
8079 period,
8080 comparison,
8081 threshold,
8082 actions_enabled,
8083 state_reason,
8084 resource,
8085 dimensions,
8086 expression,
8087 alarm_type,
8088 cross_account,
8089 )| Alarm {
8090 name,
8091 state,
8092 state_updated_timestamp: state_updated,
8093 description,
8094 metric_name,
8095 namespace,
8096 statistic,
8097 period,
8098 comparison_operator: comparison,
8099 threshold,
8100 actions_enabled,
8101 state_reason,
8102 resource,
8103 dimensions,
8104 expression,
8105 alarm_type,
8106 cross_account,
8107 },
8108 )
8109 .collect();
8110 Ok(())
8111 }
8112
8113 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
8114 if let Some(bucket_name) = &self.s3_state.current_bucket {
8115 let bucket_region = if let Some(bucket) = self
8117 .s3_state
8118 .buckets
8119 .items
8120 .iter_mut()
8121 .find(|b| &b.name == bucket_name)
8122 {
8123 if bucket.region.is_empty() {
8124 let region = self.s3_client.get_bucket_location(bucket_name).await?;
8126 bucket.region = region.clone();
8127 region
8128 } else {
8129 bucket.region.clone()
8130 }
8131 } else {
8132 self.config.region.clone()
8133 };
8134
8135 let prefix = self
8136 .s3_state
8137 .prefix_stack
8138 .last()
8139 .cloned()
8140 .unwrap_or_default();
8141 let objects = self
8142 .s3_client
8143 .list_objects(bucket_name, &bucket_region, &prefix)
8144 .await?;
8145 self.s3_state.objects = objects
8146 .into_iter()
8147 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8148 key,
8149 size,
8150 last_modified: modified,
8151 is_prefix,
8152 storage_class,
8153 })
8154 .collect();
8155 self.s3_state.selected_object = 0;
8156 }
8157 Ok(())
8158 }
8159
8160 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
8161 let bucket_region = self
8162 .s3_state
8163 .buckets
8164 .items
8165 .iter()
8166 .find(|b| b.name == bucket_name)
8167 .and_then(|b| {
8168 if b.region.is_empty() {
8169 None
8170 } else {
8171 Some(b.region.as_str())
8172 }
8173 })
8174 .unwrap_or(self.config.region.as_str());
8175 let objects = self
8176 .s3_client
8177 .list_objects(&bucket_name, bucket_region, "")
8178 .await?;
8179 let preview: Vec<S3Object> = objects
8180 .into_iter()
8181 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8182 key,
8183 size,
8184 last_modified: modified,
8185 is_prefix,
8186 storage_class,
8187 })
8188 .collect();
8189 self.s3_state.bucket_preview.insert(bucket_name, preview);
8190 Ok(())
8191 }
8192
8193 pub async fn load_prefix_preview(
8194 &mut self,
8195 bucket_name: String,
8196 prefix: String,
8197 ) -> anyhow::Result<()> {
8198 let bucket_region = self
8199 .s3_state
8200 .buckets
8201 .items
8202 .iter()
8203 .find(|b| b.name == bucket_name)
8204 .and_then(|b| {
8205 if b.region.is_empty() {
8206 None
8207 } else {
8208 Some(b.region.as_str())
8209 }
8210 })
8211 .unwrap_or(self.config.region.as_str());
8212 let objects = self
8213 .s3_client
8214 .list_objects(&bucket_name, bucket_region, &prefix)
8215 .await?;
8216 let preview: Vec<S3Object> = objects
8217 .into_iter()
8218 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8219 key,
8220 size,
8221 last_modified: modified,
8222 is_prefix,
8223 storage_class,
8224 })
8225 .collect();
8226 self.s3_state.prefix_preview.insert(prefix, preview);
8227 Ok(())
8228 }
8229
8230 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
8231 let repos = match self.ecr_state.tab {
8232 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
8233 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
8234 };
8235
8236 self.ecr_state.repositories.items = repos
8237 .into_iter()
8238 .map(|r| EcrRepository {
8239 name: r.name,
8240 uri: r.uri,
8241 created_at: r.created_at,
8242 tag_immutability: r.tag_immutability,
8243 encryption_type: r.encryption_type,
8244 })
8245 .collect();
8246
8247 self.ecr_state
8248 .repositories
8249 .items
8250 .sort_by(|a, b| a.name.cmp(&b.name));
8251 Ok(())
8252 }
8253
8254 pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
8255 let instances = self.ec2_client.list_instances().await?;
8256
8257 self.ec2_state.table.items = instances
8258 .into_iter()
8259 .map(|i| Ec2Instance {
8260 instance_id: i.instance_id,
8261 name: i.name,
8262 state: i.state,
8263 instance_type: i.instance_type,
8264 availability_zone: i.availability_zone,
8265 public_ipv4_dns: i.public_ipv4_dns,
8266 public_ipv4_address: i.public_ipv4_address,
8267 elastic_ip: i.elastic_ip,
8268 ipv6_ips: i.ipv6_ips,
8269 monitoring: i.monitoring,
8270 security_groups: i.security_groups,
8271 key_name: i.key_name,
8272 launch_time: i.launch_time,
8273 platform_details: i.platform_details,
8274 status_checks: i.status_checks,
8275 alarm_status: i.alarm_status,
8276 private_dns_name: String::new(),
8277 private_ip_address: String::new(),
8278 security_group_ids: String::new(),
8279 owner_id: String::new(),
8280 volume_id: String::new(),
8281 root_device_name: String::new(),
8282 root_device_type: String::new(),
8283 ebs_optimized: String::new(),
8284 image_id: String::new(),
8285 kernel_id: String::new(),
8286 ramdisk_id: String::new(),
8287 ami_launch_index: String::new(),
8288 reservation_id: String::new(),
8289 vpc_id: String::new(),
8290 subnet_ids: String::new(),
8291 instance_lifecycle: String::new(),
8292 architecture: String::new(),
8293 virtualization_type: String::new(),
8294 platform: String::new(),
8295 iam_instance_profile_arn: String::new(),
8296 tenancy: String::new(),
8297 affinity: String::new(),
8298 host_id: String::new(),
8299 placement_group: String::new(),
8300 partition_number: String::new(),
8301 capacity_reservation_id: String::new(),
8302 state_transition_reason_code: String::new(),
8303 state_transition_reason_message: String::new(),
8304 stop_hibernation_behavior: String::new(),
8305 outpost_arn: String::new(),
8306 product_codes: String::new(),
8307 availability_zone_id: String::new(),
8308 imdsv2: String::new(),
8309 usage_operation: String::new(),
8310 managed: String::new(),
8311 operator: String::new(),
8312 })
8313 .collect();
8314
8315 self.ec2_state
8317 .table
8318 .items
8319 .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
8320 Ok(())
8321 }
8322
8323 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
8324 if let Some(repo_name) = &self.ecr_state.current_repository {
8325 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
8326 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
8327
8328 self.ecr_state.images.items = images
8329 .into_iter()
8330 .map(|i| EcrImage {
8331 tag: i.tag,
8332 artifact_type: i.artifact_type,
8333 pushed_at: i.pushed_at,
8334 size_bytes: i.size_bytes,
8335 uri: i.uri,
8336 digest: i.digest,
8337 last_pull_time: i.last_pull_time,
8338 })
8339 .collect();
8340
8341 self.ecr_state
8342 .images
8343 .items
8344 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
8345 }
8346 }
8347 Ok(())
8348 }
8349
8350 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
8351 let stacks = self
8352 .cloudformation_client
8353 .list_stacks(self.cfn_state.view_nested)
8354 .await?;
8355
8356 let mut stacks: Vec<CfnStack> = stacks
8357 .into_iter()
8358 .map(|s| CfnStack {
8359 name: s.name,
8360 stack_id: s.stack_id,
8361 status: s.status,
8362 created_time: s.created_time,
8363 updated_time: s.updated_time,
8364 deleted_time: s.deleted_time,
8365 drift_status: s.drift_status,
8366 last_drift_check_time: s.last_drift_check_time,
8367 status_reason: s.status_reason,
8368 description: s.description,
8369 detailed_status: String::new(),
8370 root_stack: String::new(),
8371 parent_stack: String::new(),
8372 termination_protection: false,
8373 iam_role: String::new(),
8374 tags: Vec::new(),
8375 stack_policy: String::new(),
8376 rollback_monitoring_time: String::new(),
8377 rollback_alarms: Vec::new(),
8378 notification_arns: Vec::new(),
8379 })
8380 .collect();
8381
8382 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
8384
8385 self.cfn_state.table.items = stacks;
8386
8387 Ok(())
8388 }
8389
8390 pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
8391 let template = self.cloudformation_client.get_template(stack_name).await?;
8392 self.cfn_state.template_body = template;
8393 self.cfn_state.template_scroll = 0;
8394 Ok(())
8395 }
8396
8397 pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
8398 let mut parameters = self
8399 .cloudformation_client
8400 .get_stack_parameters(stack_name)
8401 .await?;
8402 parameters.sort_by(|a, b| a.key.cmp(&b.key));
8403 self.cfn_state.parameters.items = parameters;
8404 self.cfn_state.parameters.reset();
8405 Ok(())
8406 }
8407
8408 pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
8409 let outputs = self
8410 .cloudformation_client
8411 .get_stack_outputs(stack_name)
8412 .await?;
8413 self.cfn_state.outputs.items = outputs;
8414 self.cfn_state.outputs.reset();
8415 Ok(())
8416 }
8417
8418 pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
8419 let resources = self
8420 .cloudformation_client
8421 .get_stack_resources(stack_name)
8422 .await?;
8423 self.cfn_state.resources.items = resources;
8424 self.cfn_state.resources.reset();
8425 Ok(())
8426 }
8427
8428 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
8429 let attached_policies = self
8431 .iam_client
8432 .list_attached_role_policies(role_name)
8433 .await
8434 .map_err(|e| anyhow::anyhow!(e))?;
8435
8436 let mut policies: Vec<IamPolicy> = attached_policies
8437 .into_iter()
8438 .map(|p| IamPolicy {
8439 policy_name: p.policy_name().unwrap_or("").to_string(),
8440 policy_type: "Managed".to_string(),
8441 attached_via: "Direct".to_string(),
8442 attached_entities: "-".to_string(),
8443 description: "-".to_string(),
8444 creation_time: "-".to_string(),
8445 edited_time: "-".to_string(),
8446 policy_arn: p.policy_arn().map(|s| s.to_string()),
8447 })
8448 .collect();
8449
8450 let inline_policy_names = self
8452 .iam_client
8453 .list_role_policies(role_name)
8454 .await
8455 .map_err(|e| anyhow::anyhow!(e))?;
8456
8457 for policy_name in inline_policy_names {
8458 policies.push(IamPolicy {
8459 policy_name,
8460 policy_type: "Inline".to_string(),
8461 attached_via: "Direct".to_string(),
8462 attached_entities: "-".to_string(),
8463 description: "-".to_string(),
8464 creation_time: "-".to_string(),
8465 edited_time: "-".to_string(),
8466 policy_arn: None,
8467 });
8468 }
8469
8470 self.iam_state.policies.items = policies;
8471
8472 Ok(())
8473 }
8474
8475 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
8476 let attached_policies = self
8477 .iam_client
8478 .list_attached_group_policies(group_name)
8479 .await
8480 .map_err(|e| anyhow::anyhow!(e))?;
8481
8482 let mut policies: Vec<IamPolicy> = attached_policies
8483 .into_iter()
8484 .map(|p| IamPolicy {
8485 policy_name: p.policy_name().unwrap_or("").to_string(),
8486 policy_type: "AWS managed".to_string(),
8487 attached_via: "Direct".to_string(),
8488 attached_entities: "-".to_string(),
8489 description: "-".to_string(),
8490 creation_time: "-".to_string(),
8491 edited_time: "-".to_string(),
8492 policy_arn: p.policy_arn().map(|s| s.to_string()),
8493 })
8494 .collect();
8495
8496 let inline_policy_names = self
8497 .iam_client
8498 .list_group_policies(group_name)
8499 .await
8500 .map_err(|e| anyhow::anyhow!(e))?;
8501
8502 for policy_name in inline_policy_names {
8503 policies.push(IamPolicy {
8504 policy_name,
8505 policy_type: "Inline".to_string(),
8506 attached_via: "Direct".to_string(),
8507 attached_entities: "-".to_string(),
8508 description: "-".to_string(),
8509 creation_time: "-".to_string(),
8510 edited_time: "-".to_string(),
8511 policy_arn: None,
8512 });
8513 }
8514
8515 self.iam_state.policies.items = policies;
8516
8517 Ok(())
8518 }
8519
8520 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
8521 let users = self
8522 .iam_client
8523 .get_group_users(group_name)
8524 .await
8525 .map_err(|e| anyhow::anyhow!(e))?;
8526
8527 let group_users: Vec<IamGroupUser> = users
8528 .into_iter()
8529 .map(|u| {
8530 let creation_time = {
8531 let dt = u.create_date();
8532 let timestamp = dt.secs();
8533 let datetime =
8534 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
8535 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
8536 };
8537
8538 IamGroupUser {
8539 user_name: u.user_name().to_string(),
8540 groups: String::new(),
8541 last_activity: String::new(),
8542 creation_time,
8543 }
8544 })
8545 .collect();
8546
8547 self.iam_state.group_users.items = group_users;
8548
8549 Ok(())
8550 }
8551
8552 pub async fn load_policy_document(
8553 &mut self,
8554 role_name: &str,
8555 policy_name: &str,
8556 ) -> anyhow::Result<()> {
8557 let policy = self
8559 .iam_state
8560 .policies
8561 .items
8562 .iter()
8563 .find(|p| p.policy_name == policy_name)
8564 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
8565
8566 let document = if let Some(policy_arn) = &policy.policy_arn {
8567 self.iam_client
8569 .get_policy_version(policy_arn)
8570 .await
8571 .map_err(|e| anyhow::anyhow!(e))?
8572 } else {
8573 self.iam_client
8575 .get_role_policy(role_name, policy_name)
8576 .await
8577 .map_err(|e| anyhow::anyhow!(e))?
8578 };
8579
8580 self.iam_state.policy_document = document;
8581
8582 Ok(())
8583 }
8584
8585 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
8586 let document = self
8587 .iam_client
8588 .get_role(role_name)
8589 .await
8590 .map_err(|e| anyhow::anyhow!(e))?;
8591
8592 self.iam_state.trust_policy_document = document;
8593
8594 Ok(())
8595 }
8596
8597 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
8598 self.iam_state.last_accessed_services.items = vec![];
8600 self.iam_state.last_accessed_services.selected = 0;
8601
8602 Ok(())
8603 }
8604
8605 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
8606 let tags = self
8607 .iam_client
8608 .list_role_tags(role_name)
8609 .await
8610 .map_err(|e| anyhow::anyhow!(e))?;
8611 self.iam_state.tags.items = tags
8612 .into_iter()
8613 .map(|(k, v)| IamRoleTag { key: k, value: v })
8614 .collect();
8615 self.iam_state.tags.reset();
8616 Ok(())
8617 }
8618
8619 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
8620 let tags = self
8621 .iam_client
8622 .list_user_tags(user_name)
8623 .await
8624 .map_err(|e| anyhow::anyhow!(e))?;
8625 self.iam_state.user_tags.items = tags
8626 .into_iter()
8627 .map(|(k, v)| IamUserTag { key: k, value: v })
8628 .collect();
8629 self.iam_state.user_tags.reset();
8630 Ok(())
8631 }
8632
8633 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
8634 if let Some(group) = self
8635 .log_groups_state
8636 .log_groups
8637 .items
8638 .get(self.log_groups_state.log_groups.selected)
8639 {
8640 self.log_groups_state.log_streams =
8641 self.cloudwatch_client.list_log_streams(&group.name).await?;
8642 self.log_groups_state.selected_stream = 0;
8643 }
8644 Ok(())
8645 }
8646
8647 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
8648 if let Some(group) = self
8649 .log_groups_state
8650 .log_groups
8651 .items
8652 .get(self.log_groups_state.log_groups.selected)
8653 {
8654 if let Some(stream) = self
8655 .log_groups_state
8656 .log_streams
8657 .get(self.log_groups_state.selected_stream)
8658 {
8659 let (start_time, end_time) =
8661 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
8662 let now = chrono::Utc::now().timestamp_millis();
8663 let duration_ms = match self.log_groups_state.relative_unit {
8664 TimeUnit::Minutes => amount * 60 * 1000,
8665 TimeUnit::Hours => amount * 60 * 60 * 1000,
8666 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
8667 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
8668 };
8669 (Some(now - duration_ms), Some(now))
8670 } else {
8671 (None, None)
8672 };
8673
8674 let (mut events, has_more, token) = self
8675 .cloudwatch_client
8676 .get_log_events(
8677 &group.name,
8678 &stream.name,
8679 self.log_groups_state.next_backward_token.clone(),
8680 start_time,
8681 end_time,
8682 )
8683 .await?;
8684
8685 if self.log_groups_state.next_backward_token.is_some() {
8686 events.append(&mut self.log_groups_state.log_events);
8688 self.log_groups_state.event_scroll_offset = 0;
8689 } else {
8690 self.log_groups_state.event_scroll_offset = 0;
8692 }
8693
8694 self.log_groups_state.log_events = events;
8695 self.log_groups_state.has_older_events =
8696 has_more && self.log_groups_state.log_events.len() >= 25;
8697 self.log_groups_state.next_backward_token = token;
8698 self.log_groups_state.selected_event = 0;
8699 }
8700 }
8701 Ok(())
8702 }
8703
8704 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
8705 if self.insights_state.insights.selected_log_groups.is_empty() {
8706 return Err(anyhow::anyhow!(
8707 "No log groups selected. Please select at least one log group."
8708 ));
8709 }
8710
8711 let now = chrono::Utc::now().timestamp_millis();
8712 let amount = self
8713 .insights_state
8714 .insights
8715 .insights_relative_amount
8716 .parse::<i64>()
8717 .unwrap_or(1);
8718 let duration_ms = match self.insights_state.insights.insights_relative_unit {
8719 TimeUnit::Minutes => amount * 60 * 1000,
8720 TimeUnit::Hours => amount * 60 * 60 * 1000,
8721 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
8722 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
8723 };
8724 let start_time = now - duration_ms;
8725
8726 let query_id = self
8727 .cloudwatch_client
8728 .start_query(
8729 self.insights_state.insights.selected_log_groups.clone(),
8730 self.insights_state.insights.query_text.trim().to_string(),
8731 start_time,
8732 now,
8733 )
8734 .await?;
8735
8736 for _ in 0..60 {
8738 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
8739 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
8740
8741 if status == "Complete" {
8742 self.insights_state.insights.query_results = results;
8743 self.insights_state.insights.query_completed = true;
8744 self.insights_state.insights.results_selected = 0;
8745 self.insights_state.insights.expanded_result = None;
8746 self.view_mode = ViewMode::InsightsResults;
8747 return Ok(());
8748 } else if status == "Failed" || status == "Cancelled" {
8749 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
8750 }
8751 }
8752
8753 Err(anyhow::anyhow!("Query timeout"))
8754 }
8755}
8756
8757impl CloudWatchInsightsState {
8758 fn new() -> Self {
8759 Self {
8760 insights: InsightsState::default(),
8761 loading: false,
8762 }
8763 }
8764}
8765
8766impl CloudWatchAlarmsState {
8767 fn new() -> Self {
8768 Self {
8769 table: TableState::new(),
8770 alarm_tab: AlarmTab::AllAlarms,
8771 view_as: AlarmViewMode::Table,
8772 wrap_lines: false,
8773 sort_column: "Last state update".to_string(),
8774 sort_direction: SortDirection::Asc,
8775 input_focus: InputFocus::Filter,
8776 }
8777 }
8778}
8779
8780impl ServicePickerState {
8781 fn new() -> Self {
8782 Self {
8783 filter: String::new(),
8784 selected: 0,
8785 services: vec![
8786 "CloudWatch > Log Groups",
8787 "CloudWatch > Logs Insights",
8788 "CloudWatch > Alarms",
8789 "CloudFormation > Stacks",
8790 "EC2 > Instances",
8791 "ECR > Repositories",
8792 "IAM > Users",
8793 "IAM > Roles",
8794 "IAM > User Groups",
8795 "Lambda > Functions",
8796 "Lambda > Applications",
8797 "S3 > Buckets",
8798 "SQS > Queues",
8799 ],
8800 }
8801 }
8802}
8803
8804#[cfg(test)]
8805mod test_helpers {
8806 use super::*;
8807
8808 pub fn test_app() -> App {
8810 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
8811 }
8812
8813 pub fn test_app_no_region() -> App {
8814 App::new_without_client("test".to_string(), None)
8815 }
8816}
8817
8818#[cfg(test)]
8819mod tests {
8820 use super::*;
8821 use crate::keymap::Action;
8822 use test_helpers::*;
8823
8824 #[test]
8825 fn test_next_tab_cycles_forward() {
8826 let mut app = test_app();
8827 app.tabs = vec![
8828 Tab {
8829 service: Service::CloudWatchLogGroups,
8830 title: "CloudWatch > Log Groups".to_string(),
8831 breadcrumb: "CloudWatch > Log Groups".to_string(),
8832 },
8833 Tab {
8834 service: Service::CloudWatchInsights,
8835 title: "CloudWatch > Logs Insights".to_string(),
8836 breadcrumb: "CloudWatch > Logs Insights".to_string(),
8837 },
8838 Tab {
8839 service: Service::CloudWatchAlarms,
8840 title: "CloudWatch > Alarms".to_string(),
8841 breadcrumb: "CloudWatch > Alarms".to_string(),
8842 },
8843 ];
8844 app.current_tab = 0;
8845
8846 app.handle_action(Action::NextTab);
8847 assert_eq!(app.current_tab, 1);
8848 assert_eq!(app.current_service, Service::CloudWatchInsights);
8849
8850 app.handle_action(Action::NextTab);
8851 assert_eq!(app.current_tab, 2);
8852 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8853
8854 app.handle_action(Action::NextTab);
8856 assert_eq!(app.current_tab, 0);
8857 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
8858 }
8859
8860 #[test]
8861 fn test_prev_tab_cycles_backward() {
8862 let mut app = test_app();
8863 app.tabs = vec![
8864 Tab {
8865 service: Service::CloudWatchLogGroups,
8866 title: "CloudWatch > Log Groups".to_string(),
8867 breadcrumb: "CloudWatch > Log Groups".to_string(),
8868 },
8869 Tab {
8870 service: Service::CloudWatchInsights,
8871 title: "CloudWatch > Logs Insights".to_string(),
8872 breadcrumb: "CloudWatch > Logs Insights".to_string(),
8873 },
8874 Tab {
8875 service: Service::CloudWatchAlarms,
8876 title: "CloudWatch > Alarms".to_string(),
8877 breadcrumb: "CloudWatch > Alarms".to_string(),
8878 },
8879 ];
8880 app.current_tab = 2;
8881
8882 app.handle_action(Action::PrevTab);
8883 assert_eq!(app.current_tab, 1);
8884 assert_eq!(app.current_service, Service::CloudWatchInsights);
8885
8886 app.handle_action(Action::PrevTab);
8887 assert_eq!(app.current_tab, 0);
8888 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
8889
8890 app.handle_action(Action::PrevTab);
8892 assert_eq!(app.current_tab, 2);
8893 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8894 }
8895
8896 #[test]
8897 fn test_close_tab_removes_current() {
8898 let mut app = test_app();
8899 app.tabs = vec![
8900 Tab {
8901 service: Service::CloudWatchLogGroups,
8902 title: "CloudWatch > Log Groups".to_string(),
8903 breadcrumb: "CloudWatch > Log Groups".to_string(),
8904 },
8905 Tab {
8906 service: Service::CloudWatchInsights,
8907 title: "CloudWatch > Logs Insights".to_string(),
8908 breadcrumb: "CloudWatch > Logs Insights".to_string(),
8909 },
8910 Tab {
8911 service: Service::CloudWatchAlarms,
8912 title: "CloudWatch > Alarms".to_string(),
8913 breadcrumb: "CloudWatch > Alarms".to_string(),
8914 },
8915 ];
8916 app.current_tab = 1;
8917 app.service_selected = true;
8918
8919 app.handle_action(Action::CloseTab);
8920 assert_eq!(app.tabs.len(), 2);
8921 assert_eq!(app.current_tab, 1);
8922 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8923 }
8924
8925 #[test]
8926 fn test_close_last_tab_exits_service() {
8927 let mut app = test_app();
8928 app.tabs = vec![Tab {
8929 service: Service::CloudWatchLogGroups,
8930 title: "CloudWatch > Log Groups".to_string(),
8931 breadcrumb: "CloudWatch > Log Groups".to_string(),
8932 }];
8933 app.current_tab = 0;
8934 app.service_selected = true;
8935
8936 app.handle_action(Action::CloseTab);
8937 assert_eq!(app.tabs.len(), 0);
8938 assert!(!app.service_selected);
8939 assert_eq!(app.current_tab, 0);
8940 }
8941
8942 #[test]
8943 fn test_close_service_removes_current_tab() {
8944 let mut app = test_app();
8945 app.tabs = vec![
8946 Tab {
8947 service: Service::CloudWatchLogGroups,
8948 title: "CloudWatch > Log Groups".to_string(),
8949 breadcrumb: "CloudWatch > Log Groups".to_string(),
8950 },
8951 Tab {
8952 service: Service::CloudWatchInsights,
8953 title: "CloudWatch > Logs Insights".to_string(),
8954 breadcrumb: "CloudWatch > Logs Insights".to_string(),
8955 },
8956 Tab {
8957 service: Service::CloudWatchAlarms,
8958 title: "CloudWatch > Alarms".to_string(),
8959 breadcrumb: "CloudWatch > Alarms".to_string(),
8960 },
8961 ];
8962 app.current_tab = 1;
8963 app.service_selected = true;
8964
8965 app.handle_action(Action::CloseService);
8966
8967 assert_eq!(app.tabs.len(), 2);
8969 assert_eq!(app.current_tab, 1);
8971 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8972 assert!(app.service_selected);
8974 assert_eq!(app.mode, Mode::Normal);
8975 }
8976
8977 #[test]
8978 fn test_close_service_last_tab_shows_picker() {
8979 let mut app = test_app();
8980 app.tabs = vec![Tab {
8981 service: Service::CloudWatchLogGroups,
8982 title: "CloudWatch > Log Groups".to_string(),
8983 breadcrumb: "CloudWatch > Log Groups".to_string(),
8984 }];
8985 app.current_tab = 0;
8986 app.service_selected = true;
8987
8988 app.handle_action(Action::CloseService);
8989
8990 assert_eq!(app.tabs.len(), 0);
8992 assert!(!app.service_selected);
8994 assert_eq!(app.mode, Mode::ServicePicker);
8995 }
8996
8997 #[test]
8998 fn test_open_tab_picker_with_tabs() {
8999 let mut app = test_app();
9000 app.tabs = vec![
9001 Tab {
9002 service: Service::CloudWatchLogGroups,
9003 title: "CloudWatch > Log Groups".to_string(),
9004 breadcrumb: "CloudWatch > Log Groups".to_string(),
9005 },
9006 Tab {
9007 service: Service::CloudWatchInsights,
9008 title: "CloudWatch > Logs Insights".to_string(),
9009 breadcrumb: "CloudWatch > Logs Insights".to_string(),
9010 },
9011 ];
9012 app.current_tab = 1;
9013
9014 app.handle_action(Action::OpenTabPicker);
9015 assert_eq!(app.mode, Mode::TabPicker);
9016 assert_eq!(app.tab_picker_selected, 1);
9017 }
9018
9019 #[test]
9020 fn test_open_tab_picker_without_tabs() {
9021 let mut app = test_app();
9022 app.tabs = vec![];
9023
9024 app.handle_action(Action::OpenTabPicker);
9025 assert_eq!(app.mode, Mode::Normal);
9026 }
9027
9028 #[test]
9029 fn test_pending_key_state() {
9030 let mut app = test_app();
9031 assert_eq!(app.pending_key, None);
9032
9033 app.pending_key = Some('g');
9034 assert_eq!(app.pending_key, Some('g'));
9035 }
9036
9037 #[test]
9038 fn test_tab_breadcrumb_updates() {
9039 let mut app = test_app();
9040 app.tabs = vec![Tab {
9041 service: Service::CloudWatchLogGroups,
9042 title: "CloudWatch > Log Groups".to_string(),
9043 breadcrumb: "CloudWatch > Log groups".to_string(),
9044 }];
9045 app.current_tab = 0;
9046 app.service_selected = true;
9047 app.current_service = Service::CloudWatchLogGroups;
9048
9049 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
9051
9052 app.log_groups_state
9054 .log_groups
9055 .items
9056 .push(rusticity_core::LogGroup {
9057 name: "/aws/lambda/test".to_string(),
9058 creation_time: None,
9059 stored_bytes: Some(1024),
9060 retention_days: None,
9061 log_class: None,
9062 arn: None,
9063 });
9064 app.log_groups_state.log_groups.reset();
9065 app.view_mode = ViewMode::Detail;
9066 app.update_current_tab_breadcrumb();
9067
9068 assert_eq!(
9070 app.tabs[0].breadcrumb,
9071 "CloudWatch > Log groups > /aws/lambda/test"
9072 );
9073 }
9074
9075 #[test]
9076 fn test_s3_bucket_column_selector_navigation() {
9077 let mut app = test_app();
9078 app.current_service = Service::S3Buckets;
9079 app.mode = Mode::ColumnSelector;
9080 app.column_selector_index = 0;
9081
9082 app.handle_action(Action::NextItem);
9084 assert_eq!(app.column_selector_index, 1);
9085
9086 app.handle_action(Action::NextItem);
9087 assert_eq!(app.column_selector_index, 2);
9088
9089 app.handle_action(Action::NextItem);
9090 assert_eq!(app.column_selector_index, 3);
9091
9092 for _ in 0..10 {
9094 app.handle_action(Action::NextItem);
9095 }
9096 assert_eq!(app.column_selector_index, 9);
9097
9098 app.handle_action(Action::PrevItem);
9100 assert_eq!(app.column_selector_index, 8);
9101
9102 app.handle_action(Action::PrevItem);
9103 assert_eq!(app.column_selector_index, 7);
9104
9105 for _ in 0..10 {
9107 app.handle_action(Action::PrevItem);
9108 }
9109 assert_eq!(app.column_selector_index, 0);
9110 }
9111
9112 #[test]
9113 fn test_cloudwatch_alarms_state_initialized() {
9114 let app = test_app();
9115
9116 assert_eq!(app.alarms_state.table.items.len(), 0);
9118 assert_eq!(app.alarms_state.table.selected, 0);
9119 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
9120 assert!(!app.alarms_state.table.loading);
9121 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
9122 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
9123 }
9124
9125 #[test]
9126 fn test_cloudwatch_alarms_service_selection() {
9127 let mut app = test_app();
9128
9129 app.current_service = Service::CloudWatchAlarms;
9131 app.service_selected = true;
9132
9133 assert_eq!(app.current_service, Service::CloudWatchAlarms);
9134 assert!(app.service_selected);
9135 }
9136
9137 #[test]
9138 fn test_cloudwatch_alarms_column_preferences() {
9139 let app = test_app();
9140
9141 assert!(!app.cw_alarm_column_ids.is_empty());
9143 assert!(!app.cw_alarm_visible_column_ids.is_empty());
9144
9145 assert!(app
9147 .cw_alarm_visible_column_ids
9148 .contains(&AlarmColumn::Name.id()));
9149 assert!(app
9150 .cw_alarm_visible_column_ids
9151 .contains(&AlarmColumn::State.id()));
9152 }
9153
9154 #[test]
9155 fn test_s3_bucket_navigation_without_expansion() {
9156 let mut app = test_app();
9157 app.current_service = Service::S3Buckets;
9158 app.service_selected = true;
9159 app.mode = Mode::Normal;
9160
9161 app.s3_state.buckets.items = vec![
9163 S3Bucket {
9164 name: "bucket1".to_string(),
9165 region: "us-east-1".to_string(),
9166 creation_date: "2024-01-01T00:00:00Z".to_string(),
9167 },
9168 S3Bucket {
9169 name: "bucket2".to_string(),
9170 region: "us-east-1".to_string(),
9171 creation_date: "2024-01-02T00:00:00Z".to_string(),
9172 },
9173 S3Bucket {
9174 name: "bucket3".to_string(),
9175 region: "us-east-1".to_string(),
9176 creation_date: "2024-01-03T00:00:00Z".to_string(),
9177 },
9178 ];
9179 app.s3_state.selected_row = 0;
9180
9181 app.handle_action(Action::NextItem);
9183 assert_eq!(app.s3_state.selected_row, 1);
9184
9185 app.handle_action(Action::NextItem);
9186 assert_eq!(app.s3_state.selected_row, 2);
9187
9188 app.handle_action(Action::NextItem);
9190 assert_eq!(app.s3_state.selected_row, 2);
9191
9192 app.handle_action(Action::PrevItem);
9194 assert_eq!(app.s3_state.selected_row, 1);
9195
9196 app.handle_action(Action::PrevItem);
9197 assert_eq!(app.s3_state.selected_row, 0);
9198
9199 app.handle_action(Action::PrevItem);
9201 assert_eq!(app.s3_state.selected_row, 0);
9202 }
9203
9204 #[test]
9205 fn test_s3_bucket_navigation_with_expansion() {
9206 let mut app = test_app();
9207 app.current_service = Service::S3Buckets;
9208 app.service_selected = true;
9209 app.mode = Mode::Normal;
9210
9211 app.s3_state.buckets.items = vec![
9213 S3Bucket {
9214 name: "bucket1".to_string(),
9215 region: "us-east-1".to_string(),
9216 creation_date: "2024-01-01T00:00:00Z".to_string(),
9217 },
9218 S3Bucket {
9219 name: "bucket2".to_string(),
9220 region: "us-east-1".to_string(),
9221 creation_date: "2024-01-02T00:00:00Z".to_string(),
9222 },
9223 ];
9224
9225 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9227 app.s3_state.bucket_preview.insert(
9228 "bucket1".to_string(),
9229 vec![
9230 S3Object {
9231 key: "file1.txt".to_string(),
9232 size: 100,
9233 last_modified: "2024-01-01T00:00:00Z".to_string(),
9234 is_prefix: false,
9235 storage_class: "STANDARD".to_string(),
9236 },
9237 S3Object {
9238 key: "folder/".to_string(),
9239 size: 0,
9240 last_modified: "2024-01-01T00:00:00Z".to_string(),
9241 is_prefix: true,
9242 storage_class: String::new(),
9243 },
9244 ],
9245 );
9246
9247 app.s3_state.selected_row = 0;
9248
9249 app.handle_action(Action::NextItem);
9252 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
9255 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
9258 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
9262 assert_eq!(app.s3_state.selected_row, 3);
9263 }
9264
9265 #[test]
9266 fn test_s3_bucket_navigation_with_nested_expansion() {
9267 let mut app = test_app();
9268 app.current_service = Service::S3Buckets;
9269 app.service_selected = true;
9270 app.mode = Mode::Normal;
9271
9272 app.s3_state.buckets.items = vec![S3Bucket {
9274 name: "bucket1".to_string(),
9275 region: "us-east-1".to_string(),
9276 creation_date: "2024-01-01T00:00:00Z".to_string(),
9277 }];
9278
9279 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9281 app.s3_state.bucket_preview.insert(
9282 "bucket1".to_string(),
9283 vec![S3Object {
9284 key: "folder/".to_string(),
9285 size: 0,
9286 last_modified: "2024-01-01T00:00:00Z".to_string(),
9287 is_prefix: true,
9288 storage_class: String::new(),
9289 }],
9290 );
9291
9292 app.s3_state.expanded_prefixes.insert("folder/".to_string());
9294 app.s3_state.prefix_preview.insert(
9295 "folder/".to_string(),
9296 vec![
9297 S3Object {
9298 key: "folder/file1.txt".to_string(),
9299 size: 100,
9300 last_modified: "2024-01-01T00:00:00Z".to_string(),
9301 is_prefix: false,
9302 storage_class: "STANDARD".to_string(),
9303 },
9304 S3Object {
9305 key: "folder/file2.txt".to_string(),
9306 size: 200,
9307 last_modified: "2024-01-01T00:00:00Z".to_string(),
9308 is_prefix: false,
9309 storage_class: "STANDARD".to_string(),
9310 },
9311 ],
9312 );
9313
9314 app.s3_state.selected_row = 0;
9315
9316 app.handle_action(Action::NextItem);
9318 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
9321 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
9324 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
9328 assert_eq!(app.s3_state.selected_row, 3);
9329 }
9330
9331 #[test]
9332 fn test_calculate_total_bucket_rows() {
9333 let mut app = test_app();
9334
9335 assert_eq!(app.calculate_total_bucket_rows(), 0);
9337
9338 app.s3_state.buckets.items = vec![
9340 S3Bucket {
9341 name: "bucket1".to_string(),
9342 region: "us-east-1".to_string(),
9343 creation_date: "2024-01-01T00:00:00Z".to_string(),
9344 },
9345 S3Bucket {
9346 name: "bucket2".to_string(),
9347 region: "us-east-1".to_string(),
9348 creation_date: "2024-01-02T00:00:00Z".to_string(),
9349 },
9350 ];
9351 assert_eq!(app.calculate_total_bucket_rows(), 2);
9352
9353 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9355 app.s3_state.bucket_preview.insert(
9356 "bucket1".to_string(),
9357 vec![
9358 S3Object {
9359 key: "file1.txt".to_string(),
9360 size: 100,
9361 last_modified: "2024-01-01T00:00:00Z".to_string(),
9362 is_prefix: false,
9363 storage_class: "STANDARD".to_string(),
9364 },
9365 S3Object {
9366 key: "file2.txt".to_string(),
9367 size: 200,
9368 last_modified: "2024-01-01T00:00:00Z".to_string(),
9369 is_prefix: false,
9370 storage_class: "STANDARD".to_string(),
9371 },
9372 S3Object {
9373 key: "folder/".to_string(),
9374 size: 0,
9375 last_modified: "2024-01-01T00:00:00Z".to_string(),
9376 is_prefix: true,
9377 storage_class: String::new(),
9378 },
9379 ],
9380 );
9381 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
9385 app.s3_state.prefix_preview.insert(
9386 "folder/".to_string(),
9387 vec![
9388 S3Object {
9389 key: "folder/nested1.txt".to_string(),
9390 size: 50,
9391 last_modified: "2024-01-01T00:00:00Z".to_string(),
9392 is_prefix: false,
9393 storage_class: "STANDARD".to_string(),
9394 },
9395 S3Object {
9396 key: "folder/nested2.txt".to_string(),
9397 size: 75,
9398 last_modified: "2024-01-01T00:00:00Z".to_string(),
9399 is_prefix: false,
9400 storage_class: "STANDARD".to_string(),
9401 },
9402 ],
9403 );
9404 assert_eq!(app.calculate_total_bucket_rows(), 7); }
9406
9407 #[test]
9408 fn test_calculate_total_object_rows() {
9409 let mut app = test_app();
9410 app.s3_state.current_bucket = Some("test-bucket".to_string());
9411
9412 assert_eq!(app.calculate_total_object_rows(), 0);
9414
9415 app.s3_state.objects = vec![
9417 S3Object {
9418 key: "file1.txt".to_string(),
9419 size: 100,
9420 last_modified: "2024-01-01T00:00:00Z".to_string(),
9421 is_prefix: false,
9422 storage_class: "STANDARD".to_string(),
9423 },
9424 S3Object {
9425 key: "folder/".to_string(),
9426 size: 0,
9427 last_modified: "2024-01-01T00:00:00Z".to_string(),
9428 is_prefix: true,
9429 storage_class: String::new(),
9430 },
9431 ];
9432 assert_eq!(app.calculate_total_object_rows(), 2);
9433
9434 app.s3_state.expanded_prefixes.insert("folder/".to_string());
9436 app.s3_state.prefix_preview.insert(
9437 "folder/".to_string(),
9438 vec![
9439 S3Object {
9440 key: "folder/file2.txt".to_string(),
9441 size: 200,
9442 last_modified: "2024-01-01T00:00:00Z".to_string(),
9443 is_prefix: false,
9444 storage_class: "STANDARD".to_string(),
9445 },
9446 S3Object {
9447 key: "folder/subfolder/".to_string(),
9448 size: 0,
9449 last_modified: "2024-01-01T00:00:00Z".to_string(),
9450 is_prefix: true,
9451 storage_class: String::new(),
9452 },
9453 ],
9454 );
9455 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
9459 .expanded_prefixes
9460 .insert("folder/subfolder/".to_string());
9461 app.s3_state.prefix_preview.insert(
9462 "folder/subfolder/".to_string(),
9463 vec![S3Object {
9464 key: "folder/subfolder/deep.txt".to_string(),
9465 size: 50,
9466 last_modified: "2024-01-01T00:00:00Z".to_string(),
9467 is_prefix: false,
9468 storage_class: "STANDARD".to_string(),
9469 }],
9470 );
9471 assert_eq!(app.calculate_total_object_rows(), 5); }
9473
9474 #[test]
9475 fn test_s3_object_navigation_with_deep_nesting() {
9476 let mut app = test_app();
9477 app.current_service = Service::S3Buckets;
9478 app.service_selected = true;
9479 app.mode = Mode::Normal;
9480 app.s3_state.current_bucket = Some("test-bucket".to_string());
9481
9482 app.s3_state.objects = vec![S3Object {
9484 key: "folder1/".to_string(),
9485 size: 0,
9486 last_modified: "2024-01-01T00:00:00Z".to_string(),
9487 is_prefix: true,
9488 storage_class: String::new(),
9489 }];
9490
9491 app.s3_state
9493 .expanded_prefixes
9494 .insert("folder1/".to_string());
9495 app.s3_state.prefix_preview.insert(
9496 "folder1/".to_string(),
9497 vec![S3Object {
9498 key: "folder1/folder2/".to_string(),
9499 size: 0,
9500 last_modified: "2024-01-01T00:00:00Z".to_string(),
9501 is_prefix: true,
9502 storage_class: String::new(),
9503 }],
9504 );
9505
9506 app.s3_state
9508 .expanded_prefixes
9509 .insert("folder1/folder2/".to_string());
9510 app.s3_state.prefix_preview.insert(
9511 "folder1/folder2/".to_string(),
9512 vec![S3Object {
9513 key: "folder1/folder2/file.txt".to_string(),
9514 size: 100,
9515 last_modified: "2024-01-01T00:00:00Z".to_string(),
9516 is_prefix: false,
9517 storage_class: "STANDARD".to_string(),
9518 }],
9519 );
9520
9521 app.s3_state.selected_object = 0;
9522
9523 app.handle_action(Action::NextItem);
9525 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
9528 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
9532 assert_eq!(app.s3_state.selected_object, 2);
9533 }
9534
9535 #[test]
9536 fn test_s3_expand_nested_folder_in_objects_view() {
9537 let mut app = test_app();
9538 app.current_service = Service::S3Buckets;
9539 app.service_selected = true;
9540 app.mode = Mode::Normal;
9541 app.s3_state.current_bucket = Some("test-bucket".to_string());
9542
9543 app.s3_state.objects = vec![S3Object {
9545 key: "parent/".to_string(),
9546 size: 0,
9547 last_modified: "2024-01-01T00:00:00Z".to_string(),
9548 is_prefix: true,
9549 storage_class: String::new(),
9550 }];
9551
9552 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9554 app.s3_state.prefix_preview.insert(
9555 "parent/".to_string(),
9556 vec![S3Object {
9557 key: "parent/child/".to_string(),
9558 size: 0,
9559 last_modified: "2024-01-01T00:00:00Z".to_string(),
9560 is_prefix: true,
9561 storage_class: String::new(),
9562 }],
9563 );
9564
9565 app.s3_state.selected_object = 1;
9567
9568 app.handle_action(Action::NextPane);
9570
9571 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
9573 assert!(app.s3_state.buckets.loading); }
9575
9576 #[test]
9577 fn test_s3_drill_into_nested_folder() {
9578 let mut app = test_app();
9579 app.current_service = Service::S3Buckets;
9580 app.service_selected = true;
9581 app.mode = Mode::Normal;
9582 app.s3_state.current_bucket = Some("test-bucket".to_string());
9583
9584 app.s3_state.objects = vec![S3Object {
9586 key: "parent/".to_string(),
9587 size: 0,
9588 last_modified: "2024-01-01T00:00:00Z".to_string(),
9589 is_prefix: true,
9590 storage_class: String::new(),
9591 }];
9592
9593 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9595 app.s3_state.prefix_preview.insert(
9596 "parent/".to_string(),
9597 vec![S3Object {
9598 key: "parent/child/".to_string(),
9599 size: 0,
9600 last_modified: "2024-01-01T00:00:00Z".to_string(),
9601 is_prefix: true,
9602 storage_class: String::new(),
9603 }],
9604 );
9605
9606 app.s3_state.selected_object = 1;
9608
9609 app.handle_action(Action::Select);
9611
9612 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
9614 assert!(app.s3_state.buckets.loading); }
9616
9617 #[test]
9618 fn test_s3_esc_pops_navigation_stack() {
9619 let mut app = test_app();
9620 app.current_service = Service::S3Buckets;
9621 app.s3_state.current_bucket = Some("test-bucket".to_string());
9622 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
9623
9624 app.handle_action(Action::GoBack);
9626 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
9627 assert!(app.s3_state.buckets.loading);
9628
9629 app.s3_state.buckets.loading = false;
9631 app.handle_action(Action::GoBack);
9632 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
9633 assert!(app.s3_state.buckets.loading);
9634
9635 app.s3_state.buckets.loading = false;
9637 app.handle_action(Action::GoBack);
9638 assert_eq!(app.s3_state.current_bucket, None);
9639 }
9640
9641 #[test]
9642 fn test_s3_esc_from_bucket_root_exits() {
9643 let mut app = test_app();
9644 app.current_service = Service::S3Buckets;
9645 app.s3_state.current_bucket = Some("test-bucket".to_string());
9646 app.s3_state.prefix_stack = vec![];
9647
9648 app.handle_action(Action::GoBack);
9650 assert_eq!(app.s3_state.current_bucket, None);
9651 assert_eq!(app.s3_state.objects.len(), 0);
9652 }
9653
9654 #[test]
9655 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
9656 let mut app = test_app();
9657 app.current_service = Service::S3Buckets;
9658 app.service_selected = true;
9659 app.mode = Mode::Normal;
9660
9661 app.s3_state.buckets.items = vec![S3Bucket {
9663 name: "test-bucket".to_string(),
9664 region: "us-east-1".to_string(),
9665 creation_date: "2024-01-01".to_string(),
9666 }];
9667
9668 app.s3_state
9670 .expanded_prefixes
9671 .insert("test-bucket".to_string());
9672 app.s3_state.bucket_preview.insert(
9673 "test-bucket".to_string(),
9674 vec![S3Object {
9675 key: "parent/".to_string(),
9676 size: 0,
9677 last_modified: "2024-01-01".to_string(),
9678 is_prefix: true,
9679 storage_class: String::new(),
9680 }],
9681 );
9682
9683 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9685 app.s3_state.prefix_preview.insert(
9686 "parent/".to_string(),
9687 vec![S3Object {
9688 key: "parent/child/".to_string(),
9689 size: 0,
9690 last_modified: "2024-01-01".to_string(),
9691 is_prefix: true,
9692 storage_class: String::new(),
9693 }],
9694 );
9695
9696 app.s3_state.selected_row = 2;
9698
9699 app.handle_action(Action::Select);
9701
9702 assert_eq!(
9704 app.s3_state.prefix_stack,
9705 vec!["parent/".to_string(), "parent/child/".to_string()]
9706 );
9707 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
9708 assert!(app.s3_state.buckets.loading);
9709
9710 app.s3_state.buckets.loading = false;
9712 app.handle_action(Action::GoBack);
9713 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
9714 assert!(app.s3_state.buckets.loading);
9715
9716 app.s3_state.buckets.loading = false;
9718 app.handle_action(Action::GoBack);
9719 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
9720 assert!(app.s3_state.buckets.loading);
9721
9722 app.s3_state.buckets.loading = false;
9724 app.handle_action(Action::GoBack);
9725 assert_eq!(app.s3_state.current_bucket, None);
9726 }
9727
9728 #[test]
9729 fn test_region_picker_fuzzy_filter() {
9730 let mut app = test_app();
9731 app.region_latencies.insert("us-east-1".to_string(), 10);
9732 app.region_filter = "vir".to_string();
9733 let filtered = app.get_filtered_regions();
9734 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
9735 }
9736
9737 #[test]
9738 fn test_profile_picker_loads_profiles() {
9739 let profiles = App::load_aws_profiles();
9740 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
9742 }
9743
9744 #[test]
9745 fn test_profile_with_region_uses_it() {
9746 let mut app = test_app_no_region();
9747 app.available_profiles = vec![AwsProfile {
9748 name: "test-profile".to_string(),
9749 region: Some("eu-west-1".to_string()),
9750 account: Some("123456789".to_string()),
9751 role_arn: None,
9752 source_profile: None,
9753 }];
9754 app.profile_picker_selected = 0;
9755 app.mode = Mode::ProfilePicker;
9756
9757 let filtered = app.get_filtered_profiles();
9759 if let Some(profile) = filtered.first() {
9760 let profile_name = profile.name.clone();
9761 let profile_region = profile.region.clone();
9762
9763 app.profile = profile_name;
9764 if let Some(region) = profile_region {
9765 app.region = region;
9766 }
9767 }
9768
9769 assert_eq!(app.profile, "test-profile");
9770 assert_eq!(app.region, "eu-west-1");
9771 }
9772
9773 #[test]
9774 fn test_profile_without_region_keeps_unknown() {
9775 let mut app = test_app_no_region();
9776 let initial_region = app.region.clone();
9777
9778 app.available_profiles = vec![AwsProfile {
9779 name: "test-profile".to_string(),
9780 region: None,
9781 account: None,
9782 role_arn: None,
9783 source_profile: None,
9784 }];
9785 app.profile_picker_selected = 0;
9786 app.mode = Mode::ProfilePicker;
9787
9788 let filtered = app.get_filtered_profiles();
9789 if let Some(profile) = filtered.first() {
9790 let profile_name = profile.name.clone();
9791 let profile_region = profile.region.clone();
9792
9793 app.profile = profile_name;
9794 if let Some(region) = profile_region {
9795 app.region = region;
9796 }
9797 }
9798
9799 assert_eq!(app.profile, "test-profile");
9800 assert_eq!(app.region, initial_region); }
9802
9803 #[test]
9804 fn test_region_selection_closes_all_tabs() {
9805 let mut app = test_app();
9806
9807 app.tabs.push(Tab {
9809 service: Service::CloudWatchLogGroups,
9810 title: "CloudWatch".to_string(),
9811 breadcrumb: "CloudWatch".to_string(),
9812 });
9813 app.tabs.push(Tab {
9814 service: Service::S3Buckets,
9815 title: "S3".to_string(),
9816 breadcrumb: "S3".to_string(),
9817 });
9818 app.service_selected = true;
9819 app.current_tab = 1;
9820
9821 app.region_latencies.insert("eu-west-1".to_string(), 50);
9823
9824 app.mode = Mode::RegionPicker;
9826 app.region_picker_selected = 0;
9827
9828 let filtered = app.get_filtered_regions();
9829 if let Some(region) = filtered.first() {
9830 app.region = region.code.to_string();
9831 app.tabs.clear();
9832 app.current_tab = 0;
9833 app.service_selected = false;
9834 app.mode = Mode::Normal;
9835 }
9836
9837 assert_eq!(app.tabs.len(), 0);
9838 assert_eq!(app.current_tab, 0);
9839 assert!(!app.service_selected);
9840 assert_eq!(app.region, "eu-west-1");
9841 }
9842
9843 #[test]
9844 fn test_region_picker_can_be_closed_without_selection() {
9845 let mut app = test_app();
9846 let initial_region = app.region.clone();
9847
9848 app.mode = Mode::RegionPicker;
9849
9850 app.mode = Mode::Normal;
9852
9853 assert_eq!(app.region, initial_region);
9855 }
9856
9857 #[test]
9858 fn test_session_filter_works() {
9859 let mut app = test_app();
9860
9861 app.sessions = vec![
9862 Session {
9863 id: "1".to_string(),
9864 timestamp: "2024-01-01".to_string(),
9865 profile: "prod-profile".to_string(),
9866 region: "us-east-1".to_string(),
9867 account_id: "123456789".to_string(),
9868 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
9869 tabs: vec![],
9870 },
9871 Session {
9872 id: "2".to_string(),
9873 timestamp: "2024-01-02".to_string(),
9874 profile: "dev-profile".to_string(),
9875 region: "eu-west-1".to_string(),
9876 account_id: "987654321".to_string(),
9877 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
9878 tabs: vec![],
9879 },
9880 ];
9881
9882 app.session_filter = "prod".to_string();
9884 let filtered = app.get_filtered_sessions();
9885 assert_eq!(filtered.len(), 1);
9886 assert_eq!(filtered[0].profile, "prod-profile");
9887
9888 app.session_filter = "eu".to_string();
9890 let filtered = app.get_filtered_sessions();
9891 assert_eq!(filtered.len(), 1);
9892 assert_eq!(filtered[0].region, "eu-west-1");
9893
9894 app.session_filter.clear();
9896 let filtered = app.get_filtered_sessions();
9897 assert_eq!(filtered.len(), 2);
9898 }
9899
9900 #[test]
9901 fn test_profile_picker_shows_account() {
9902 let mut app = test_app_no_region();
9903 app.available_profiles = vec![AwsProfile {
9904 name: "test-profile".to_string(),
9905 region: Some("us-east-1".to_string()),
9906 account: Some("123456789".to_string()),
9907 role_arn: None,
9908 source_profile: None,
9909 }];
9910
9911 let filtered = app.get_filtered_profiles();
9912 assert_eq!(filtered.len(), 1);
9913 assert_eq!(filtered[0].account, Some("123456789".to_string()));
9914 }
9915
9916 #[test]
9917 fn test_profile_without_account() {
9918 let mut app = test_app_no_region();
9919 app.available_profiles = vec![AwsProfile {
9920 name: "test-profile".to_string(),
9921 region: Some("us-east-1".to_string()),
9922 account: None,
9923 role_arn: None,
9924 source_profile: None,
9925 }];
9926
9927 let filtered = app.get_filtered_profiles();
9928 assert_eq!(filtered.len(), 1);
9929 assert_eq!(filtered[0].account, None);
9930 }
9931
9932 #[test]
9933 fn test_profile_with_all_fields() {
9934 let mut app = test_app_no_region();
9935 app.available_profiles = vec![AwsProfile {
9936 name: "prod-profile".to_string(),
9937 region: Some("us-west-2".to_string()),
9938 account: Some("123456789".to_string()),
9939 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
9940 source_profile: Some("base-profile".to_string()),
9941 }];
9942
9943 let filtered = app.get_filtered_profiles();
9944 assert_eq!(filtered.len(), 1);
9945 assert_eq!(filtered[0].name, "prod-profile");
9946 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
9947 assert_eq!(filtered[0].account, Some("123456789".to_string()));
9948 assert_eq!(
9949 filtered[0].role_arn,
9950 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
9951 );
9952 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
9953 }
9954
9955 #[test]
9956 fn test_profile_filter_by_source_profile() {
9957 let mut app = test_app_no_region();
9958 app.available_profiles = vec![
9959 AwsProfile {
9960 name: "profile1".to_string(),
9961 region: None,
9962 account: None,
9963 role_arn: None,
9964 source_profile: Some("base".to_string()),
9965 },
9966 AwsProfile {
9967 name: "profile2".to_string(),
9968 region: None,
9969 account: None,
9970 role_arn: None,
9971 source_profile: Some("other".to_string()),
9972 },
9973 ];
9974
9975 app.profile_filter = "base".to_string();
9976 let filtered = app.get_filtered_profiles();
9977 assert_eq!(filtered.len(), 1);
9978 assert_eq!(filtered[0].name, "profile1");
9979 }
9980
9981 #[test]
9982 fn test_profile_filter_by_role() {
9983 let mut app = test_app_no_region();
9984 app.available_profiles = vec![
9985 AwsProfile {
9986 name: "admin-profile".to_string(),
9987 region: None,
9988 account: None,
9989 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
9990 source_profile: None,
9991 },
9992 AwsProfile {
9993 name: "dev-profile".to_string(),
9994 region: None,
9995 account: None,
9996 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
9997 source_profile: None,
9998 },
9999 ];
10000
10001 app.profile_filter = "Admin".to_string();
10002 let filtered = app.get_filtered_profiles();
10003 assert_eq!(filtered.len(), 1);
10004 assert_eq!(filtered[0].name, "admin-profile");
10005 }
10006
10007 #[test]
10008 fn test_profiles_sorted_by_name() {
10009 let mut app = test_app_no_region();
10010 app.available_profiles = vec![
10011 AwsProfile {
10012 name: "zebra-profile".to_string(),
10013 region: None,
10014 account: None,
10015 role_arn: None,
10016 source_profile: None,
10017 },
10018 AwsProfile {
10019 name: "alpha-profile".to_string(),
10020 region: None,
10021 account: None,
10022 role_arn: None,
10023 source_profile: None,
10024 },
10025 AwsProfile {
10026 name: "beta-profile".to_string(),
10027 region: None,
10028 account: None,
10029 role_arn: None,
10030 source_profile: None,
10031 },
10032 ];
10033
10034 let filtered = app.get_filtered_profiles();
10035 assert_eq!(filtered.len(), 3);
10036 assert_eq!(filtered[0].name, "alpha-profile");
10037 assert_eq!(filtered[1].name, "beta-profile");
10038 assert_eq!(filtered[2].name, "zebra-profile");
10039 }
10040
10041 #[test]
10042 fn test_profile_with_role_arn() {
10043 let mut app = test_app_no_region();
10044 app.available_profiles = vec![AwsProfile {
10045 name: "role-profile".to_string(),
10046 region: Some("us-east-1".to_string()),
10047 account: Some("123456789".to_string()),
10048 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
10049 source_profile: None,
10050 }];
10051
10052 let filtered = app.get_filtered_profiles();
10053 assert_eq!(filtered.len(), 1);
10054 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
10055 }
10056
10057 #[test]
10058 fn test_profile_with_user_arn() {
10059 let mut app = test_app_no_region();
10060 app.available_profiles = vec![AwsProfile {
10061 name: "user-profile".to_string(),
10062 region: Some("us-east-1".to_string()),
10063 account: Some("123456789".to_string()),
10064 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
10065 source_profile: None,
10066 }];
10067
10068 let filtered = app.get_filtered_profiles();
10069 assert_eq!(filtered.len(), 1);
10070 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
10071 }
10072
10073 #[test]
10074 fn test_filtered_profiles_also_sorted() {
10075 let mut app = test_app_no_region();
10076 app.available_profiles = vec![
10077 AwsProfile {
10078 name: "prod-zebra".to_string(),
10079 region: Some("us-east-1".to_string()),
10080 account: None,
10081 role_arn: None,
10082 source_profile: None,
10083 },
10084 AwsProfile {
10085 name: "prod-alpha".to_string(),
10086 region: Some("us-east-1".to_string()),
10087 account: None,
10088 role_arn: None,
10089 source_profile: None,
10090 },
10091 AwsProfile {
10092 name: "dev-profile".to_string(),
10093 region: Some("us-west-2".to_string()),
10094 account: None,
10095 role_arn: None,
10096 source_profile: None,
10097 },
10098 ];
10099
10100 app.profile_filter = "prod".to_string();
10101 let filtered = app.get_filtered_profiles();
10102 assert_eq!(filtered.len(), 2);
10103 assert_eq!(filtered[0].name, "prod-alpha");
10104 assert_eq!(filtered[1].name, "prod-zebra");
10105 }
10106
10107 #[test]
10108 fn test_profile_picker_has_all_columns() {
10109 let mut app = test_app_no_region();
10110 app.available_profiles = vec![AwsProfile {
10111 name: "test".to_string(),
10112 region: Some("us-east-1".to_string()),
10113 account: Some("123456789".to_string()),
10114 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
10115 source_profile: Some("base".to_string()),
10116 }];
10117
10118 let filtered = app.get_filtered_profiles();
10119 assert_eq!(filtered.len(), 1);
10120 assert!(filtered[0].name == "test");
10121 assert!(filtered[0].region.is_some());
10122 assert!(filtered[0].account.is_some());
10123 assert!(filtered[0].role_arn.is_some());
10124 assert!(filtered[0].source_profile.is_some());
10125 }
10126
10127 #[test]
10128 fn test_session_picker_shows_tab_count() {
10129 let mut app = test_app_no_region();
10130 app.sessions = vec![Session {
10131 id: "1".to_string(),
10132 timestamp: "2024-01-01".to_string(),
10133 profile: "test".to_string(),
10134 region: "us-east-1".to_string(),
10135 account_id: "123".to_string(),
10136 role_arn: String::new(),
10137 tabs: vec![
10138 SessionTab {
10139 service: "CloudWatch".to_string(),
10140 title: "Logs".to_string(),
10141 breadcrumb: String::new(),
10142 filter: None,
10143 selected_item: None,
10144 },
10145 SessionTab {
10146 service: "S3".to_string(),
10147 title: "Buckets".to_string(),
10148 breadcrumb: String::new(),
10149 filter: None,
10150 selected_item: None,
10151 },
10152 ],
10153 }];
10154
10155 let filtered = app.get_filtered_sessions();
10156 assert_eq!(filtered.len(), 1);
10157 assert_eq!(filtered[0].tabs.len(), 2);
10158 }
10159
10160 #[test]
10161 fn test_start_background_data_fetch_loads_profiles() {
10162 let mut app = test_app_no_region();
10163 assert!(app.available_profiles.is_empty());
10164
10165 app.available_profiles = App::load_aws_profiles();
10167
10168 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10170 }
10171
10172 #[test]
10173 fn test_refresh_in_profile_picker() {
10174 let mut app = test_app_no_region();
10175 app.mode = Mode::ProfilePicker;
10176 app.available_profiles = vec![AwsProfile {
10177 name: "test".to_string(),
10178 region: None,
10179 account: None,
10180 role_arn: None,
10181 source_profile: None,
10182 }];
10183
10184 app.handle_action(Action::Refresh);
10185
10186 assert!(app.log_groups_state.loading);
10188 assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
10189 }
10190
10191 #[test]
10192 fn test_refresh_sets_loading_for_profile_picker() {
10193 let mut app = test_app_no_region();
10194 app.mode = Mode::ProfilePicker;
10195
10196 assert!(!app.log_groups_state.loading);
10197
10198 app.handle_action(Action::Refresh);
10199
10200 assert!(app.log_groups_state.loading);
10201 }
10202
10203 #[test]
10204 fn test_profiles_loaded_on_demand() {
10205 let mut app = test_app_no_region();
10206
10207 assert!(app.available_profiles.is_empty());
10209
10210 app.available_profiles = App::load_aws_profiles();
10212
10213 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10215 }
10216
10217 #[test]
10218 fn test_profile_accounts_not_fetched_automatically() {
10219 let mut app = test_app_no_region();
10220 app.available_profiles = App::load_aws_profiles();
10221
10222 for profile in &app.available_profiles {
10224 assert!(profile.account.is_none() || profile.account.is_some());
10227 }
10228 }
10229
10230 #[test]
10231 fn test_ctrl_r_triggers_account_fetch() {
10232 let mut app = test_app_no_region();
10233 app.mode = Mode::ProfilePicker;
10234 app.available_profiles = vec![AwsProfile {
10235 name: "test".to_string(),
10236 region: Some("us-east-1".to_string()),
10237 account: None,
10238 role_arn: None,
10239 source_profile: None,
10240 }];
10241
10242 assert!(app.available_profiles[0].account.is_none());
10244
10245 app.handle_action(Action::Refresh);
10247
10248 assert!(app.log_groups_state.loading);
10250 }
10251
10252 #[test]
10253 fn test_refresh_in_region_picker() {
10254 let mut app = test_app_no_region();
10255 app.mode = Mode::RegionPicker;
10256
10257 let initial_latencies = app.region_latencies.len();
10258 app.handle_action(Action::Refresh);
10259
10260 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
10262 }
10263
10264 #[test]
10265 fn test_refresh_in_session_picker() {
10266 let mut app = test_app_no_region();
10267 app.mode = Mode::SessionPicker;
10268 app.sessions = vec![];
10269
10270 app.handle_action(Action::Refresh);
10271
10272 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
10274 }
10275
10276 #[test]
10277 fn test_session_picker_selection() {
10278 let mut app = test_app();
10279
10280 app.sessions = vec![Session {
10281 id: "1".to_string(),
10282 timestamp: "2024-01-01".to_string(),
10283 profile: "prod-profile".to_string(),
10284 region: "us-west-2".to_string(),
10285 account_id: "123456789".to_string(),
10286 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
10287 tabs: vec![SessionTab {
10288 service: "CloudWatchLogGroups".to_string(),
10289 title: "Log Groups".to_string(),
10290 breadcrumb: "CloudWatch > Log Groups".to_string(),
10291 filter: Some("test".to_string()),
10292 selected_item: None,
10293 }],
10294 }];
10295
10296 app.mode = Mode::SessionPicker;
10297 app.session_picker_selected = 0;
10298
10299 app.handle_action(Action::Select);
10301
10302 assert_eq!(app.mode, Mode::Normal);
10303 assert_eq!(app.profile, "prod-profile");
10304 assert_eq!(app.region, "us-west-2");
10305 assert_eq!(app.config.account_id, "123456789");
10306 assert_eq!(app.tabs.len(), 1);
10307 assert_eq!(app.tabs[0].title, "Log Groups");
10308 }
10309
10310 #[test]
10311 fn test_save_session_creates_session() {
10312 let mut app =
10313 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10314 app.config.account_id = "123456789".to_string();
10315 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10316
10317 app.tabs.push(Tab {
10318 service: Service::CloudWatchLogGroups,
10319 title: "Log Groups".to_string(),
10320 breadcrumb: "CloudWatch > Log Groups".to_string(),
10321 });
10322
10323 app.save_current_session();
10324
10325 assert!(app.current_session.is_some());
10326 let session = app.current_session.clone().unwrap();
10327 assert_eq!(session.profile, "test-profile");
10328 assert_eq!(session.region, "us-east-1");
10329 assert_eq!(session.account_id, "123456789");
10330 assert_eq!(session.tabs.len(), 1);
10331
10332 let _ = session.delete();
10334 }
10335
10336 #[test]
10337 fn test_save_session_updates_existing() {
10338 let mut app =
10339 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10340 app.config.account_id = "123456789".to_string();
10341 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10342
10343 app.current_session = Some(Session {
10344 id: "existing".to_string(),
10345 timestamp: "2024-01-01".to_string(),
10346 profile: "test-profile".to_string(),
10347 region: "us-east-1".to_string(),
10348 account_id: "123456789".to_string(),
10349 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10350 tabs: vec![],
10351 });
10352
10353 app.tabs.push(Tab {
10354 service: Service::CloudWatchLogGroups,
10355 title: "Log Groups".to_string(),
10356 breadcrumb: "CloudWatch > Log Groups".to_string(),
10357 });
10358
10359 app.save_current_session();
10360
10361 let session = app.current_session.clone().unwrap();
10362 assert_eq!(session.id, "existing");
10363 assert_eq!(session.tabs.len(), 1);
10364
10365 let _ = session.delete();
10367 }
10368
10369 #[test]
10370 fn test_save_session_skips_empty_tabs() {
10371 let mut app =
10372 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10373 app.config.account_id = "123456789".to_string();
10374
10375 app.save_current_session();
10376
10377 assert!(app.current_session.is_none());
10378 }
10379
10380 #[test]
10381 fn test_save_session_deletes_when_tabs_closed() {
10382 let mut app =
10383 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10384 app.config.account_id = "123456789".to_string();
10385 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10386
10387 app.current_session = Some(Session {
10389 id: "test_delete".to_string(),
10390 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
10391 profile: "test-profile".to_string(),
10392 region: "us-east-1".to_string(),
10393 account_id: "123456789".to_string(),
10394 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10395 tabs: vec![],
10396 });
10397
10398 app.save_current_session();
10400
10401 assert!(app.current_session.is_none());
10402 }
10403
10404 #[test]
10405 fn test_closing_all_tabs_deletes_session() {
10406 let mut app =
10407 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10408 app.config.account_id = "123456789".to_string();
10409 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10410
10411 app.tabs.push(Tab {
10413 service: Service::CloudWatchLogGroups,
10414 title: "Log Groups".to_string(),
10415 breadcrumb: "CloudWatch > Log Groups".to_string(),
10416 });
10417
10418 app.save_current_session();
10420 assert!(app.current_session.is_some());
10421 let session_id = app.current_session.as_ref().unwrap().id.clone();
10422
10423 app.tabs.clear();
10425
10426 app.save_current_session();
10428 assert!(app.current_session.is_none());
10429
10430 let _ = Session::load(&session_id).map(|s| s.delete());
10432 }
10433
10434 #[test]
10435 fn test_credential_error_opens_profile_picker() {
10436 let mut app = App::new_without_client("default".to_string(), None);
10438 let error_str = "Unable to load credentials from any source";
10439
10440 if error_str.contains("credentials") {
10441 app.available_profiles = App::load_aws_profiles();
10442 app.mode = Mode::ProfilePicker;
10443 }
10444
10445 assert_eq!(app.mode, Mode::ProfilePicker);
10446 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10448 }
10449
10450 #[test]
10451 fn test_non_credential_error_shows_error_modal() {
10452 let mut app = App::new_without_client("default".to_string(), None);
10453 let error_str = "Network timeout";
10454
10455 if !error_str.contains("credentials") {
10456 app.error_message = Some(error_str.to_string());
10457 app.mode = Mode::ErrorModal;
10458 }
10459
10460 assert_eq!(app.mode, Mode::ErrorModal);
10461 assert!(app.error_message.is_some());
10462 }
10463
10464 #[tokio::test]
10465 async fn test_profile_selection_loads_credentials() {
10466 std::env::set_var("AWS_PROFILE", "default");
10468
10469 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
10471
10472 if let Ok(app) = result {
10473 assert!(!app.config.account_id.is_empty());
10475 assert!(!app.config.role_arn.is_empty());
10476 assert_eq!(app.profile, "default");
10477 assert_eq!(app.config.region, "us-east-1");
10478 }
10479 }
10481
10482 #[test]
10483 fn test_new_app_shows_service_picker_with_no_tabs() {
10484 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
10485
10486 assert!(!app.service_selected);
10488 assert_eq!(app.mode, Mode::ServicePicker);
10490 assert!(app.tabs.is_empty());
10492 }
10493
10494 #[tokio::test]
10495 async fn test_aws_profile_env_var_read_before_config_load() {
10496 std::env::set_var("AWS_PROFILE", "test-profile");
10498
10499 let profile_name = None
10501 .or_else(|| std::env::var("AWS_PROFILE").ok())
10502 .unwrap_or_else(|| "default".to_string());
10503
10504 assert_eq!(profile_name, "test-profile");
10506
10507 std::env::set_var("AWS_PROFILE", &profile_name);
10509
10510 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
10512
10513 std::env::remove_var("AWS_PROFILE");
10514 }
10515
10516 #[test]
10517 fn test_next_preferences_cloudformation() {
10518 let mut app = test_app();
10519 app.current_service = Service::CloudFormationStacks;
10520 app.mode = Mode::ColumnSelector;
10521 app.column_selector_index = 0;
10522
10523 let page_size_idx = app.cfn_column_ids.len() + 2;
10525 app.handle_action(Action::NextPreferences);
10526 assert_eq!(app.column_selector_index, page_size_idx);
10527
10528 app.handle_action(Action::NextPreferences);
10530 assert_eq!(app.column_selector_index, 0);
10531 }
10532
10533 #[test]
10534 fn test_s3_preferences_tab_cycling() {
10535 let mut app = test_app();
10536 app.current_service = Service::S3Buckets;
10537 app.mode = Mode::ColumnSelector;
10538 app.column_selector_index = 0;
10539
10540 let page_size_idx = app.s3_bucket_column_ids.len() + 2;
10541
10542 app.handle_action(Action::NextPreferences);
10544 assert_eq!(app.column_selector_index, page_size_idx);
10545
10546 app.handle_action(Action::NextPreferences);
10548 assert_eq!(app.column_selector_index, 0);
10549
10550 app.handle_action(Action::PrevPreferences);
10552 assert_eq!(app.column_selector_index, page_size_idx);
10553
10554 app.handle_action(Action::PrevPreferences);
10556 assert_eq!(app.column_selector_index, 0);
10557 }
10558
10559 #[test]
10560 fn test_s3_filter_resets_selection() {
10561 let mut app = test_app();
10562 app.current_service = Service::S3Buckets;
10563 app.service_selected = true;
10564
10565 app.s3_state.buckets.items = vec![
10567 S3Bucket {
10568 name: "bucket-1".to_string(),
10569 region: "us-east-1".to_string(),
10570 creation_date: "2023-01-01".to_string(),
10571 },
10572 S3Bucket {
10573 name: "bucket-2".to_string(),
10574 region: "us-east-1".to_string(),
10575 creation_date: "2023-01-02".to_string(),
10576 },
10577 S3Bucket {
10578 name: "other-bucket".to_string(),
10579 region: "us-east-1".to_string(),
10580 creation_date: "2023-01-03".to_string(),
10581 },
10582 ];
10583
10584 app.s3_state.selected_row = 1;
10586 app.s3_state.bucket_scroll_offset = 1;
10587
10588 app.mode = Mode::FilterInput;
10590 app.apply_filter_operation(|f| f.push_str("bucket-"));
10591
10592 assert_eq!(app.s3_state.selected_row, 0);
10594 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
10595 assert_eq!(app.s3_state.buckets.filter, "bucket-");
10596 }
10597
10598 #[test]
10599 fn test_s3_navigation_respects_filter() {
10600 let mut app = test_app();
10601 app.current_service = Service::S3Buckets;
10602 app.service_selected = true;
10603 app.mode = Mode::Normal;
10604
10605 app.s3_state.buckets.items = vec![
10607 S3Bucket {
10608 name: "prod-bucket".to_string(),
10609 region: "us-east-1".to_string(),
10610 creation_date: "2023-01-01".to_string(),
10611 },
10612 S3Bucket {
10613 name: "dev-bucket".to_string(),
10614 region: "us-east-1".to_string(),
10615 creation_date: "2023-01-02".to_string(),
10616 },
10617 S3Bucket {
10618 name: "prod-logs".to_string(),
10619 region: "us-east-1".to_string(),
10620 creation_date: "2023-01-03".to_string(),
10621 },
10622 ];
10623
10624 app.s3_state.buckets.filter = "prod".to_string();
10626
10627 assert_eq!(app.s3_state.selected_row, 0);
10629
10630 app.handle_action(Action::NextItem);
10632 assert_eq!(app.s3_state.selected_row, 1);
10633
10634 app.handle_action(Action::NextItem);
10636 assert_eq!(app.s3_state.selected_row, 1);
10637
10638 app.handle_action(Action::PrevItem);
10640 assert_eq!(app.s3_state.selected_row, 0);
10641 }
10642
10643 #[test]
10644 fn test_next_preferences_lambda_functions() {
10645 let mut app = test_app();
10646 app.current_service = Service::LambdaFunctions;
10647 app.mode = Mode::ColumnSelector;
10648 app.column_selector_index = 0;
10649
10650 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
10651 app.handle_action(Action::NextPreferences);
10652 assert_eq!(app.column_selector_index, page_size_idx);
10653
10654 app.handle_action(Action::NextPreferences);
10655 assert_eq!(app.column_selector_index, 0);
10656 }
10657
10658 #[test]
10659 fn test_next_preferences_lambda_applications() {
10660 let mut app = test_app();
10661 app.current_service = Service::LambdaApplications;
10662 app.mode = Mode::ColumnSelector;
10663 app.column_selector_index = 0;
10664
10665 let page_size_idx = app.lambda_application_column_ids.len() + 2;
10666 app.handle_action(Action::NextPreferences);
10667 assert_eq!(app.column_selector_index, page_size_idx);
10668
10669 app.handle_action(Action::NextPreferences);
10670 assert_eq!(app.column_selector_index, 0);
10671 }
10672
10673 #[test]
10674 fn test_next_preferences_ecr_images() {
10675 let mut app = test_app();
10676 app.current_service = Service::EcrRepositories;
10677 app.ecr_state.current_repository = Some("test-repo".to_string());
10678 app.mode = Mode::ColumnSelector;
10679 app.column_selector_index = 0;
10680
10681 let page_size_idx = app.ecr_image_column_ids.len() + 2;
10682 app.handle_action(Action::NextPreferences);
10683 assert_eq!(app.column_selector_index, page_size_idx);
10684
10685 app.handle_action(Action::NextPreferences);
10686 assert_eq!(app.column_selector_index, 0);
10687 }
10688
10689 #[test]
10690 fn test_cloudformation_next_item() {
10691 let mut app = test_app();
10692 app.current_service = Service::CloudFormationStacks;
10693 app.service_selected = true;
10694 app.mode = Mode::Normal;
10695 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10696 app.cfn_state.table.items = vec![
10697 CfnStack {
10698 name: "stack1".to_string(),
10699 stack_id: "id1".to_string(),
10700 status: "CREATE_COMPLETE".to_string(),
10701 created_time: "2024-01-01".to_string(),
10702 updated_time: String::new(),
10703 deleted_time: String::new(),
10704 drift_status: String::new(),
10705 last_drift_check_time: String::new(),
10706 status_reason: String::new(),
10707 description: String::new(),
10708 detailed_status: String::new(),
10709 root_stack: String::new(),
10710 parent_stack: String::new(),
10711 termination_protection: false,
10712 iam_role: String::new(),
10713 tags: Vec::new(),
10714 stack_policy: String::new(),
10715 rollback_monitoring_time: String::new(),
10716 rollback_alarms: Vec::new(),
10717 notification_arns: Vec::new(),
10718 },
10719 CfnStack {
10720 name: "stack2".to_string(),
10721 stack_id: "id2".to_string(),
10722 status: "UPDATE_COMPLETE".to_string(),
10723 created_time: "2024-01-02".to_string(),
10724 updated_time: String::new(),
10725 deleted_time: String::new(),
10726 drift_status: String::new(),
10727 last_drift_check_time: String::new(),
10728 status_reason: String::new(),
10729 description: String::new(),
10730 detailed_status: String::new(),
10731 root_stack: String::new(),
10732 parent_stack: String::new(),
10733 termination_protection: false,
10734 iam_role: String::new(),
10735 tags: Vec::new(),
10736 stack_policy: String::new(),
10737 rollback_monitoring_time: String::new(),
10738 rollback_alarms: Vec::new(),
10739 notification_arns: Vec::new(),
10740 },
10741 ];
10742 app.cfn_state.table.reset();
10743
10744 app.handle_action(Action::NextItem);
10745 assert_eq!(app.cfn_state.table.selected, 1);
10746
10747 app.handle_action(Action::NextItem);
10748 assert_eq!(app.cfn_state.table.selected, 1); }
10750
10751 #[test]
10752 fn test_cloudformation_prev_item() {
10753 let mut app = test_app();
10754 app.current_service = Service::CloudFormationStacks;
10755 app.service_selected = true;
10756 app.mode = Mode::Normal;
10757 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10758 app.cfn_state.table.items = vec![
10759 CfnStack {
10760 name: "stack1".to_string(),
10761 stack_id: "id1".to_string(),
10762 status: "CREATE_COMPLETE".to_string(),
10763 created_time: "2024-01-01".to_string(),
10764 updated_time: String::new(),
10765 deleted_time: String::new(),
10766 drift_status: String::new(),
10767 last_drift_check_time: String::new(),
10768 status_reason: String::new(),
10769 description: String::new(),
10770 detailed_status: String::new(),
10771 root_stack: String::new(),
10772 parent_stack: String::new(),
10773 termination_protection: false,
10774 iam_role: String::new(),
10775 tags: Vec::new(),
10776 stack_policy: String::new(),
10777 rollback_monitoring_time: String::new(),
10778 rollback_alarms: Vec::new(),
10779 notification_arns: Vec::new(),
10780 },
10781 CfnStack {
10782 name: "stack2".to_string(),
10783 stack_id: "id2".to_string(),
10784 status: "UPDATE_COMPLETE".to_string(),
10785 created_time: "2024-01-02".to_string(),
10786 updated_time: String::new(),
10787 deleted_time: String::new(),
10788 drift_status: String::new(),
10789 last_drift_check_time: String::new(),
10790 status_reason: String::new(),
10791 description: String::new(),
10792 detailed_status: String::new(),
10793 root_stack: String::new(),
10794 parent_stack: String::new(),
10795 termination_protection: false,
10796 iam_role: String::new(),
10797 tags: Vec::new(),
10798 stack_policy: String::new(),
10799 rollback_monitoring_time: String::new(),
10800 rollback_alarms: Vec::new(),
10801 notification_arns: Vec::new(),
10802 },
10803 ];
10804 app.cfn_state.table.selected = 1;
10805
10806 app.handle_action(Action::PrevItem);
10807 assert_eq!(app.cfn_state.table.selected, 0);
10808
10809 app.handle_action(Action::PrevItem);
10810 assert_eq!(app.cfn_state.table.selected, 0); }
10812
10813 #[test]
10814 fn test_cloudformation_page_down() {
10815 let mut app = test_app();
10816 app.current_service = Service::CloudFormationStacks;
10817 app.service_selected = true;
10818 app.mode = Mode::Normal;
10819 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10820
10821 for i in 0..20 {
10823 app.cfn_state.table.items.push(CfnStack {
10824 name: format!("stack{}", i),
10825 stack_id: format!("id{}", i),
10826 status: "CREATE_COMPLETE".to_string(),
10827 created_time: format!("2024-01-{:02}", i + 1),
10828 updated_time: String::new(),
10829 deleted_time: String::new(),
10830 drift_status: String::new(),
10831 last_drift_check_time: String::new(),
10832 status_reason: String::new(),
10833 description: String::new(),
10834 detailed_status: String::new(),
10835 root_stack: String::new(),
10836 parent_stack: String::new(),
10837 termination_protection: false,
10838 iam_role: String::new(),
10839 tags: Vec::new(),
10840 stack_policy: String::new(),
10841 rollback_monitoring_time: String::new(),
10842 rollback_alarms: Vec::new(),
10843 notification_arns: Vec::new(),
10844 });
10845 }
10846 app.cfn_state.table.reset();
10847
10848 app.handle_action(Action::PageDown);
10849 assert_eq!(app.cfn_state.table.selected, 10);
10850
10851 app.handle_action(Action::PageDown);
10852 assert_eq!(app.cfn_state.table.selected, 19); }
10854
10855 #[test]
10856 fn test_cloudformation_page_up() {
10857 let mut app = test_app();
10858 app.current_service = Service::CloudFormationStacks;
10859 app.service_selected = true;
10860 app.mode = Mode::Normal;
10861 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10862
10863 for i in 0..20 {
10865 app.cfn_state.table.items.push(CfnStack {
10866 name: format!("stack{}", i),
10867 stack_id: format!("id{}", i),
10868 status: "CREATE_COMPLETE".to_string(),
10869 created_time: format!("2024-01-{:02}", i + 1),
10870 updated_time: String::new(),
10871 deleted_time: String::new(),
10872 drift_status: String::new(),
10873 last_drift_check_time: String::new(),
10874 status_reason: String::new(),
10875 description: String::new(),
10876 detailed_status: String::new(),
10877 root_stack: String::new(),
10878 parent_stack: String::new(),
10879 termination_protection: false,
10880 iam_role: String::new(),
10881 tags: Vec::new(),
10882 stack_policy: String::new(),
10883 rollback_monitoring_time: String::new(),
10884 rollback_alarms: Vec::new(),
10885 notification_arns: Vec::new(),
10886 });
10887 }
10888 app.cfn_state.table.selected = 15;
10889
10890 app.handle_action(Action::PageUp);
10891 assert_eq!(app.cfn_state.table.selected, 5);
10892
10893 app.handle_action(Action::PageUp);
10894 assert_eq!(app.cfn_state.table.selected, 0); }
10896
10897 #[test]
10898 fn test_cloudformation_filter_input() {
10899 let mut app = test_app();
10900 app.current_service = Service::CloudFormationStacks;
10901 app.service_selected = true;
10902 app.mode = Mode::Normal;
10903
10904 app.handle_action(Action::StartFilter);
10905 assert_eq!(app.mode, Mode::FilterInput);
10906
10907 app.cfn_state.table.filter = "test".to_string();
10909 assert_eq!(app.cfn_state.table.filter, "test");
10910 }
10911
10912 #[test]
10913 fn test_cloudformation_filter_applies() {
10914 let mut app = test_app();
10915 app.current_service = Service::CloudFormationStacks;
10916 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10917 app.cfn_state.table.items = vec![
10918 CfnStack {
10919 name: "prod-stack".to_string(),
10920 stack_id: "id1".to_string(),
10921 status: "CREATE_COMPLETE".to_string(),
10922 created_time: "2024-01-01".to_string(),
10923 updated_time: String::new(),
10924 deleted_time: String::new(),
10925 drift_status: String::new(),
10926 last_drift_check_time: String::new(),
10927 status_reason: String::new(),
10928 description: "Production stack".to_string(),
10929 detailed_status: String::new(),
10930 root_stack: String::new(),
10931 parent_stack: String::new(),
10932 termination_protection: false,
10933 iam_role: String::new(),
10934 tags: Vec::new(),
10935 stack_policy: String::new(),
10936 rollback_monitoring_time: String::new(),
10937 rollback_alarms: Vec::new(),
10938 notification_arns: Vec::new(),
10939 },
10940 CfnStack {
10941 name: "dev-stack".to_string(),
10942 stack_id: "id2".to_string(),
10943 status: "UPDATE_COMPLETE".to_string(),
10944 created_time: "2024-01-02".to_string(),
10945 updated_time: String::new(),
10946 deleted_time: String::new(),
10947 drift_status: String::new(),
10948 last_drift_check_time: String::new(),
10949 status_reason: String::new(),
10950 description: "Development stack".to_string(),
10951 detailed_status: String::new(),
10952 root_stack: String::new(),
10953 parent_stack: String::new(),
10954 termination_protection: false,
10955 iam_role: String::new(),
10956 tags: Vec::new(),
10957 stack_policy: String::new(),
10958 rollback_monitoring_time: String::new(),
10959 rollback_alarms: Vec::new(),
10960 notification_arns: Vec::new(),
10961 },
10962 ];
10963 app.cfn_state.table.filter = "prod".to_string();
10964
10965 let filtered = filtered_cloudformation_stacks(&app);
10966 assert_eq!(filtered.len(), 1);
10967 assert_eq!(filtered[0].name, "prod-stack");
10968 }
10969
10970 #[test]
10971 fn test_cloudformation_right_arrow_expands() {
10972 let mut app = test_app();
10973 app.current_service = Service::CloudFormationStacks;
10974 app.service_selected = true;
10975 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10976 app.cfn_state.table.items = vec![CfnStack {
10977 name: "test-stack".to_string(),
10978 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
10979 .to_string(),
10980 status: "CREATE_COMPLETE".to_string(),
10981 created_time: "2024-01-01".to_string(),
10982 updated_time: String::new(),
10983 deleted_time: String::new(),
10984 drift_status: String::new(),
10985 last_drift_check_time: String::new(),
10986 status_reason: String::new(),
10987 description: "Test stack".to_string(),
10988 detailed_status: String::new(),
10989 root_stack: String::new(),
10990 parent_stack: String::new(),
10991 termination_protection: false,
10992 iam_role: String::new(),
10993 tags: Vec::new(),
10994 stack_policy: String::new(),
10995 rollback_monitoring_time: String::new(),
10996 rollback_alarms: Vec::new(),
10997 notification_arns: Vec::new(),
10998 }];
10999 app.cfn_state.table.reset();
11000
11001 assert_eq!(app.cfn_state.table.expanded_item, None);
11002
11003 app.handle_action(Action::NextPane);
11004 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
11005 }
11006
11007 #[test]
11008 fn test_cloudformation_left_arrow_collapses() {
11009 let mut app = test_app();
11010 app.current_service = Service::CloudFormationStacks;
11011 app.service_selected = true;
11012 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11013 app.cfn_state.table.items = vec![CfnStack {
11014 name: "test-stack".to_string(),
11015 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11016 .to_string(),
11017 status: "CREATE_COMPLETE".to_string(),
11018 created_time: "2024-01-01".to_string(),
11019 updated_time: String::new(),
11020 deleted_time: String::new(),
11021 drift_status: String::new(),
11022 last_drift_check_time: String::new(),
11023 status_reason: String::new(),
11024 description: "Test stack".to_string(),
11025 detailed_status: String::new(),
11026 root_stack: String::new(),
11027 parent_stack: String::new(),
11028 termination_protection: false,
11029 iam_role: String::new(),
11030 tags: Vec::new(),
11031 stack_policy: String::new(),
11032 rollback_monitoring_time: String::new(),
11033 rollback_alarms: Vec::new(),
11034 notification_arns: Vec::new(),
11035 }];
11036 app.cfn_state.table.reset();
11037 app.cfn_state.table.expanded_item = Some(0);
11038
11039 app.handle_action(Action::PrevPane);
11040 assert_eq!(app.cfn_state.table.expanded_item, None);
11041 }
11042
11043 #[test]
11044 fn test_cloudformation_enter_drills_into_stack() {
11045 let mut app = test_app();
11046 app.current_service = Service::CloudFormationStacks;
11047 app.service_selected = true;
11048 app.mode = Mode::Normal;
11049 app.tabs = vec![Tab {
11050 service: Service::CloudFormationStacks,
11051 title: "CloudFormation > Stacks".to_string(),
11052 breadcrumb: "CloudFormation > Stacks".to_string(),
11053 }];
11054 app.current_tab = 0;
11055 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11056 app.cfn_state.table.items = vec![CfnStack {
11057 name: "test-stack".to_string(),
11058 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11059 .to_string(),
11060 status: "CREATE_COMPLETE".to_string(),
11061 created_time: "2024-01-01".to_string(),
11062 updated_time: String::new(),
11063 deleted_time: String::new(),
11064 drift_status: String::new(),
11065 last_drift_check_time: String::new(),
11066 status_reason: String::new(),
11067 description: "Test stack".to_string(),
11068 detailed_status: String::new(),
11069 root_stack: String::new(),
11070 parent_stack: String::new(),
11071 termination_protection: false,
11072 iam_role: String::new(),
11073 tags: Vec::new(),
11074 stack_policy: String::new(),
11075 rollback_monitoring_time: String::new(),
11076 rollback_alarms: Vec::new(),
11077 notification_arns: Vec::new(),
11078 }];
11079 app.cfn_state.table.reset();
11080
11081 let filtered = filtered_cloudformation_stacks(&app);
11083 assert_eq!(filtered.len(), 1);
11084 assert_eq!(filtered[0].name, "test-stack");
11085
11086 assert_eq!(app.cfn_state.current_stack, None);
11087
11088 app.handle_action(Action::Select);
11090 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
11091 }
11092
11093 #[test]
11094 fn test_cloudformation_copy_to_clipboard() {
11095 let mut app = test_app();
11096 app.current_service = Service::CloudFormationStacks;
11097 app.service_selected = true;
11098 app.mode = Mode::Normal;
11099 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11100 app.cfn_state.table.items = vec![
11101 CfnStack {
11102 name: "stack1".to_string(),
11103 stack_id: "id1".to_string(),
11104 status: "CREATE_COMPLETE".to_string(),
11105 created_time: "2024-01-01".to_string(),
11106 updated_time: String::new(),
11107 deleted_time: String::new(),
11108 drift_status: String::new(),
11109 last_drift_check_time: String::new(),
11110 status_reason: String::new(),
11111 description: String::new(),
11112 detailed_status: String::new(),
11113 root_stack: String::new(),
11114 parent_stack: String::new(),
11115 termination_protection: false,
11116 iam_role: String::new(),
11117 tags: Vec::new(),
11118 stack_policy: String::new(),
11119 rollback_monitoring_time: String::new(),
11120 rollback_alarms: Vec::new(),
11121 notification_arns: Vec::new(),
11122 },
11123 CfnStack {
11124 name: "stack2".to_string(),
11125 stack_id: "id2".to_string(),
11126 status: "UPDATE_COMPLETE".to_string(),
11127 created_time: "2024-01-02".to_string(),
11128 updated_time: String::new(),
11129 deleted_time: String::new(),
11130 drift_status: String::new(),
11131 last_drift_check_time: String::new(),
11132 status_reason: String::new(),
11133 description: String::new(),
11134 detailed_status: String::new(),
11135 root_stack: String::new(),
11136 parent_stack: String::new(),
11137 termination_protection: false,
11138 iam_role: String::new(),
11139 tags: Vec::new(),
11140 stack_policy: String::new(),
11141 rollback_monitoring_time: String::new(),
11142 rollback_alarms: Vec::new(),
11143 notification_arns: Vec::new(),
11144 },
11145 ];
11146
11147 assert!(!app.snapshot_requested);
11148 app.handle_action(Action::CopyToClipboard);
11149
11150 assert!(app.snapshot_requested);
11152 }
11153
11154 #[test]
11155 fn test_cloudformation_expansion_shows_all_visible_columns() {
11156 let mut app = test_app();
11157 app.current_service = Service::CloudFormationStacks;
11158 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11159 app.cfn_state.table.items = vec![CfnStack {
11160 name: "test-stack".to_string(),
11161 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11162 .to_string(),
11163 status: "CREATE_COMPLETE".to_string(),
11164 created_time: "2024-01-01".to_string(),
11165 updated_time: "2024-01-02".to_string(),
11166 deleted_time: String::new(),
11167 drift_status: "IN_SYNC".to_string(),
11168 last_drift_check_time: "2024-01-03".to_string(),
11169 status_reason: String::new(),
11170 description: "Test description".to_string(),
11171 detailed_status: String::new(),
11172 root_stack: String::new(),
11173 parent_stack: String::new(),
11174 termination_protection: false,
11175 iam_role: String::new(),
11176 tags: Vec::new(),
11177 stack_policy: String::new(),
11178 rollback_monitoring_time: String::new(),
11179 rollback_alarms: Vec::new(),
11180 notification_arns: Vec::new(),
11181 }];
11182
11183 app.cfn_visible_column_ids = [
11185 CfnColumn::Name,
11186 CfnColumn::Status,
11187 CfnColumn::CreatedTime,
11188 CfnColumn::Description,
11189 ]
11190 .iter()
11191 .map(|c| c.id())
11192 .collect();
11193
11194 app.cfn_state.table.expanded_item = Some(0);
11195
11196 assert_eq!(app.cfn_visible_column_ids.len(), 4);
11199 assert!(app.cfn_state.table.has_expanded_item());
11200 }
11201
11202 #[test]
11203 fn test_cloudformation_empty_list_shows_page_1() {
11204 let mut app = test_app();
11205 app.current_service = Service::CloudFormationStacks;
11206 app.cfn_state.table.items = vec![];
11207
11208 let filtered = filtered_cloudformation_stacks(&app);
11209 assert_eq!(filtered.len(), 0);
11210
11211 let page_size = app.cfn_state.table.page_size.value();
11213 let total_pages = filtered.len().div_ceil(page_size);
11214 assert_eq!(total_pages, 0);
11215
11216 }
11219}
11220
11221impl App {
11222 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
11223 let mut all = AwsRegion::all();
11224
11225 for region in &mut all {
11227 region.latency_ms = self.region_latencies.get(region.code).copied();
11228 }
11229
11230 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
11232 all
11233 } else {
11234 let filter_lower = self.region_filter.to_lowercase();
11235 all.into_iter()
11236 .filter(|r| {
11237 r.name.to_lowercase().contains(&filter_lower)
11238 || r.code.to_lowercase().contains(&filter_lower)
11239 || r.group.to_lowercase().contains(&filter_lower)
11240 })
11241 .collect()
11242 };
11243
11244 let mut sorted = filtered;
11246 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
11247 sorted
11248 }
11249
11250 pub fn measure_region_latencies(&mut self) {
11251 use std::time::Instant;
11252 self.region_latencies.clear();
11253
11254 let regions = AwsRegion::all();
11255 let start_all = Instant::now();
11256 tracing::info!("Starting latency measurement for {} regions", regions.len());
11257
11258 let handles: Vec<_> = regions
11259 .iter()
11260 .map(|region| {
11261 let code = region.code.to_string();
11262 std::thread::spawn(move || {
11263 let endpoint = format!("https://sts.{}.amazonaws.com", code);
11265 let start = Instant::now();
11266
11267 match ureq::get(&endpoint)
11268 .timeout(std::time::Duration::from_secs(2))
11269 .call()
11270 {
11271 Ok(_) => {
11272 let latency = start.elapsed().as_millis() as u64;
11273 Some((code, latency))
11274 }
11275 Err(e) => {
11276 tracing::debug!("Failed to measure {}: {}", code, e);
11277 Some((code, 9999))
11278 }
11279 }
11280 })
11281 })
11282 .collect();
11283
11284 for handle in handles {
11285 if let Ok(Some((code, latency))) = handle.join() {
11286 self.region_latencies.insert(code, latency);
11287 }
11288 }
11289
11290 tracing::info!(
11291 "Measured {} regions in {:?}",
11292 self.region_latencies.len(),
11293 start_all.elapsed()
11294 );
11295 }
11296
11297 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
11298 filter_profiles(&self.available_profiles, &self.profile_filter)
11299 }
11300
11301 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
11302 if self.session_filter.is_empty() {
11303 return self.sessions.iter().collect();
11304 }
11305 let filter_lower = self.session_filter.to_lowercase();
11306 self.sessions
11307 .iter()
11308 .filter(|s| {
11309 s.profile.to_lowercase().contains(&filter_lower)
11310 || s.region.to_lowercase().contains(&filter_lower)
11311 || s.account_id.to_lowercase().contains(&filter_lower)
11312 || s.role_arn.to_lowercase().contains(&filter_lower)
11313 })
11314 .collect()
11315 }
11316
11317 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
11318 if self.tab_filter.is_empty() {
11319 return self.tabs.iter().enumerate().collect();
11320 }
11321 let filter_lower = self.tab_filter.to_lowercase();
11322 self.tabs
11323 .iter()
11324 .enumerate()
11325 .filter(|(_, tab)| {
11326 tab.title.to_lowercase().contains(&filter_lower)
11327 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
11328 })
11329 .collect()
11330 }
11331
11332 pub fn load_aws_profiles() -> Vec<AwsProfile> {
11333 AwsProfile::load_all()
11334 }
11335
11336 pub async fn fetch_profile_accounts(&mut self) {
11337 for profile in &mut self.available_profiles {
11338 if profile.account.is_none() {
11339 let region = profile
11340 .region
11341 .clone()
11342 .unwrap_or_else(|| "us-east-1".to_string());
11343 if let Ok(account) =
11344 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
11345 {
11346 profile.account = Some(account);
11347 }
11348 }
11349 }
11350 }
11351
11352 fn save_current_session(&mut self) {
11353 if self.tabs.is_empty() {
11355 if let Some(ref session) = self.current_session {
11356 let _ = session.delete();
11357 self.current_session = None;
11358 }
11359 return;
11360 }
11361
11362 let session = if let Some(ref mut current) = self.current_session {
11363 current.tabs = self
11365 .tabs
11366 .iter()
11367 .map(|t| SessionTab {
11368 service: format!("{:?}", t.service),
11369 title: t.title.clone(),
11370 breadcrumb: t.breadcrumb.clone(),
11371 filter: match t.service {
11372 Service::CloudWatchLogGroups => {
11373 Some(self.log_groups_state.log_groups.filter.clone())
11374 }
11375 _ => None,
11376 },
11377 selected_item: None,
11378 })
11379 .collect();
11380 current.clone()
11381 } else {
11382 let mut session = Session::new(
11384 self.profile.clone(),
11385 self.region.clone(),
11386 self.config.account_id.clone(),
11387 self.config.role_arn.clone(),
11388 );
11389 session.tabs = self
11390 .tabs
11391 .iter()
11392 .map(|t| SessionTab {
11393 service: format!("{:?}", t.service),
11394 title: t.title.clone(),
11395 breadcrumb: t.breadcrumb.clone(),
11396 filter: match t.service {
11397 Service::CloudWatchLogGroups => {
11398 Some(self.log_groups_state.log_groups.filter.clone())
11399 }
11400 _ => None,
11401 },
11402 selected_item: None,
11403 })
11404 .collect();
11405 self.current_session = Some(session.clone());
11406 session
11407 };
11408
11409 let _ = session.save();
11410 }
11411}
11412
11413#[cfg(test)]
11414mod iam_policy_view_tests {
11415 use super::*;
11416 use test_helpers::*;
11417
11418 #[test]
11419 fn test_enter_opens_policy_view() {
11420 let mut app = test_app();
11421 app.current_service = Service::IamRoles;
11422 app.service_selected = true;
11423 app.mode = Mode::Normal;
11424 app.view_mode = ViewMode::Detail;
11425 app.iam_state.current_role = Some("TestRole".to_string());
11426 app.iam_state.policies.items = vec![IamPolicy {
11427 policy_name: "TestPolicy".to_string(),
11428 policy_type: "Inline".to_string(),
11429 attached_via: "Direct".to_string(),
11430 attached_entities: "1".to_string(),
11431 description: "Test".to_string(),
11432 creation_time: "2023-01-01".to_string(),
11433 edited_time: "2023-01-01".to_string(),
11434 policy_arn: None,
11435 }];
11436 app.iam_state.policies.reset();
11437
11438 app.handle_action(Action::Select);
11439
11440 assert_eq!(app.view_mode, ViewMode::PolicyView);
11441 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
11442 assert_eq!(app.iam_state.policy_scroll, 0);
11443 assert!(app.iam_state.policies.loading);
11444 }
11445
11446 #[test]
11447 fn test_escape_closes_policy_view() {
11448 let mut app = test_app();
11449 app.current_service = Service::IamRoles;
11450 app.service_selected = true;
11451 app.mode = Mode::Normal;
11452 app.view_mode = ViewMode::PolicyView;
11453 app.iam_state.current_role = Some("TestRole".to_string());
11454 app.iam_state.current_policy = Some("TestPolicy".to_string());
11455 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
11456 app.iam_state.policy_scroll = 5;
11457
11458 app.handle_action(Action::PrevPane);
11459
11460 assert_eq!(app.view_mode, ViewMode::Detail);
11461 assert_eq!(app.iam_state.current_policy, None);
11462 assert_eq!(app.iam_state.policy_document, "");
11463 assert_eq!(app.iam_state.policy_scroll, 0);
11464 }
11465
11466 #[test]
11467 fn test_ctrl_d_scrolls_down_in_policy_view() {
11468 let mut app = test_app();
11469 app.current_service = Service::IamRoles;
11470 app.service_selected = true;
11471 app.mode = Mode::Normal;
11472 app.view_mode = ViewMode::PolicyView;
11473 app.iam_state.current_role = Some("TestRole".to_string());
11474 app.iam_state.current_policy = Some("TestPolicy".to_string());
11475 app.iam_state.policy_document = (0..100)
11476 .map(|i| format!("line {}", i))
11477 .collect::<Vec<_>>()
11478 .join("\n");
11479 app.iam_state.policy_scroll = 0;
11480
11481 app.handle_action(Action::ScrollDown);
11482
11483 assert_eq!(app.iam_state.policy_scroll, 10);
11484
11485 app.handle_action(Action::ScrollDown);
11486
11487 assert_eq!(app.iam_state.policy_scroll, 20);
11488 }
11489
11490 #[test]
11491 fn test_ctrl_u_scrolls_up_in_policy_view() {
11492 let mut app = test_app();
11493 app.current_service = Service::IamRoles;
11494 app.service_selected = true;
11495 app.mode = Mode::Normal;
11496 app.view_mode = ViewMode::PolicyView;
11497 app.iam_state.current_role = Some("TestRole".to_string());
11498 app.iam_state.current_policy = Some("TestPolicy".to_string());
11499 app.iam_state.policy_document = (0..100)
11500 .map(|i| format!("line {}", i))
11501 .collect::<Vec<_>>()
11502 .join("\n");
11503 app.iam_state.policy_scroll = 30;
11504
11505 app.handle_action(Action::ScrollUp);
11506
11507 assert_eq!(app.iam_state.policy_scroll, 20);
11508
11509 app.handle_action(Action::ScrollUp);
11510
11511 assert_eq!(app.iam_state.policy_scroll, 10);
11512 }
11513
11514 #[test]
11515 fn test_scroll_does_not_go_negative() {
11516 let mut app = test_app();
11517 app.current_service = Service::IamRoles;
11518 app.service_selected = true;
11519 app.mode = Mode::Normal;
11520 app.view_mode = ViewMode::PolicyView;
11521 app.iam_state.current_role = Some("TestRole".to_string());
11522 app.iam_state.current_policy = Some("TestPolicy".to_string());
11523 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11524 app.iam_state.policy_scroll = 0;
11525
11526 app.handle_action(Action::ScrollUp);
11527
11528 assert_eq!(app.iam_state.policy_scroll, 0);
11529 }
11530
11531 #[test]
11532 fn test_scroll_does_not_exceed_max() {
11533 let mut app = test_app();
11534 app.current_service = Service::IamRoles;
11535 app.service_selected = true;
11536 app.mode = Mode::Normal;
11537 app.view_mode = ViewMode::PolicyView;
11538 app.iam_state.current_role = Some("TestRole".to_string());
11539 app.iam_state.current_policy = Some("TestPolicy".to_string());
11540 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11541 app.iam_state.policy_scroll = 0;
11542
11543 app.handle_action(Action::ScrollDown);
11544
11545 assert_eq!(app.iam_state.policy_scroll, 2); }
11547
11548 #[test]
11549 fn test_policy_view_console_url() {
11550 let mut app = test_app();
11551 app.current_service = Service::IamRoles;
11552 app.service_selected = true;
11553 app.view_mode = ViewMode::PolicyView;
11554 app.iam_state.current_role = Some("TestRole".to_string());
11555 app.iam_state.current_policy = Some("TestPolicy".to_string());
11556
11557 let url = app.get_console_url();
11558
11559 assert!(url.contains("us-east-1.console.aws.amazon.com"));
11560 assert!(url.contains("/roles/details/TestRole"));
11561 assert!(url.contains("/editPolicy/TestPolicy"));
11562 assert!(url.contains("step=addPermissions"));
11563 }
11564
11565 #[test]
11566 fn test_esc_from_policy_view_goes_to_role_detail() {
11567 let mut app = test_app();
11568 app.current_service = Service::IamRoles;
11569 app.service_selected = true;
11570 app.mode = Mode::Normal;
11571 app.view_mode = ViewMode::PolicyView;
11572 app.iam_state.current_role = Some("TestRole".to_string());
11573 app.iam_state.current_policy = Some("TestPolicy".to_string());
11574 app.iam_state.policy_document = "test".to_string();
11575 app.iam_state.policy_scroll = 5;
11576
11577 app.handle_action(Action::GoBack);
11578
11579 assert_eq!(app.view_mode, ViewMode::Detail);
11580 assert_eq!(app.iam_state.current_policy, None);
11581 assert_eq!(app.iam_state.policy_document, "");
11582 assert_eq!(app.iam_state.policy_scroll, 0);
11583 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
11584 }
11585
11586 #[test]
11587 fn test_esc_from_role_detail_goes_to_role_list() {
11588 let mut app = test_app();
11589 app.current_service = Service::IamRoles;
11590 app.service_selected = true;
11591 app.mode = Mode::Normal;
11592 app.view_mode = ViewMode::Detail;
11593 app.iam_state.current_role = Some("TestRole".to_string());
11594
11595 app.handle_action(Action::GoBack);
11596
11597 assert_eq!(app.iam_state.current_role, None);
11598 }
11599
11600 #[test]
11601 fn test_right_arrow_expands_policy_row() {
11602 let mut app = test_app();
11603 app.current_service = Service::IamRoles;
11604 app.service_selected = true;
11605 app.mode = Mode::Normal;
11606 app.view_mode = ViewMode::Detail;
11607 app.iam_state.current_role = Some("TestRole".to_string());
11608 app.iam_state.policies.items = vec![IamPolicy {
11609 policy_name: "TestPolicy".to_string(),
11610 policy_type: "Inline".to_string(),
11611 attached_via: "Direct".to_string(),
11612 attached_entities: "1".to_string(),
11613 description: "Test".to_string(),
11614 creation_time: "2023-01-01".to_string(),
11615 edited_time: "2023-01-01".to_string(),
11616 policy_arn: None,
11617 }];
11618 app.iam_state.policies.reset();
11619
11620 app.handle_action(Action::NextPane);
11621
11622 assert_eq!(app.view_mode, ViewMode::Detail);
11624 assert_eq!(app.iam_state.current_policy, None);
11625 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
11626 }
11627}
11628
11629#[cfg(test)]
11630mod tab_filter_tests {
11631 use super::*;
11632 use test_helpers::*;
11633
11634 #[test]
11635 fn test_space_t_opens_tab_picker() {
11636 let mut app = test_app();
11637 app.tabs = vec![
11638 Tab {
11639 service: Service::CloudWatchLogGroups,
11640 title: "Tab 1".to_string(),
11641 breadcrumb: "CloudWatch > Log groups".to_string(),
11642 },
11643 Tab {
11644 service: Service::S3Buckets,
11645 title: "Tab 2".to_string(),
11646 breadcrumb: "S3 > Buckets".to_string(),
11647 },
11648 ];
11649 app.current_tab = 0;
11650
11651 app.handle_action(Action::OpenTabPicker);
11652
11653 assert_eq!(app.mode, Mode::TabPicker);
11654 assert_eq!(app.tab_picker_selected, 0);
11655 }
11656
11657 #[test]
11658 fn test_tab_filter_works() {
11659 let mut app = test_app();
11660 app.tabs = vec![
11661 Tab {
11662 service: Service::CloudWatchLogGroups,
11663 title: "CloudWatch Logs".to_string(),
11664 breadcrumb: "CloudWatch > Log groups".to_string(),
11665 },
11666 Tab {
11667 service: Service::S3Buckets,
11668 title: "S3 Buckets".to_string(),
11669 breadcrumb: "S3 > Buckets".to_string(),
11670 },
11671 Tab {
11672 service: Service::CloudWatchAlarms,
11673 title: "CloudWatch Alarms".to_string(),
11674 breadcrumb: "CloudWatch > Alarms".to_string(),
11675 },
11676 ];
11677 app.mode = Mode::TabPicker;
11678
11679 app.handle_action(Action::FilterInput('s'));
11681 app.handle_action(Action::FilterInput('3'));
11682
11683 let filtered = app.get_filtered_tabs();
11684 assert_eq!(filtered.len(), 1);
11685 assert_eq!(filtered[0].1.title, "S3 Buckets");
11686 }
11687
11688 #[test]
11689 fn test_tab_filter_by_breadcrumb() {
11690 let mut app = test_app();
11691 app.tabs = vec![
11692 Tab {
11693 service: Service::CloudWatchLogGroups,
11694 title: "Tab 1".to_string(),
11695 breadcrumb: "CloudWatch > Log groups".to_string(),
11696 },
11697 Tab {
11698 service: Service::S3Buckets,
11699 title: "Tab 2".to_string(),
11700 breadcrumb: "S3 > Buckets".to_string(),
11701 },
11702 ];
11703 app.mode = Mode::TabPicker;
11704
11705 app.handle_action(Action::FilterInput('c'));
11707 app.handle_action(Action::FilterInput('l'));
11708 app.handle_action(Action::FilterInput('o'));
11709 app.handle_action(Action::FilterInput('u'));
11710 app.handle_action(Action::FilterInput('d'));
11711
11712 let filtered = app.get_filtered_tabs();
11713 assert_eq!(filtered.len(), 1);
11714 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
11715 }
11716
11717 #[test]
11718 fn test_tab_filter_backspace() {
11719 let mut app = test_app();
11720 app.tabs = vec![
11721 Tab {
11722 service: Service::CloudWatchLogGroups,
11723 title: "CloudWatch Logs".to_string(),
11724 breadcrumb: "CloudWatch > Log groups".to_string(),
11725 },
11726 Tab {
11727 service: Service::S3Buckets,
11728 title: "S3 Buckets".to_string(),
11729 breadcrumb: "S3 > Buckets".to_string(),
11730 },
11731 ];
11732 app.mode = Mode::TabPicker;
11733
11734 app.handle_action(Action::FilterInput('s'));
11735 app.handle_action(Action::FilterInput('3'));
11736 assert_eq!(app.tab_filter, "s3");
11737
11738 app.handle_action(Action::FilterBackspace);
11739 assert_eq!(app.tab_filter, "s");
11740
11741 let filtered = app.get_filtered_tabs();
11742 assert_eq!(filtered.len(), 2); }
11744
11745 #[test]
11746 fn test_tab_selection_with_filter() {
11747 let mut app = test_app();
11748 app.tabs = vec![
11749 Tab {
11750 service: Service::CloudWatchLogGroups,
11751 title: "CloudWatch Logs".to_string(),
11752 breadcrumb: "CloudWatch > Log groups".to_string(),
11753 },
11754 Tab {
11755 service: Service::S3Buckets,
11756 title: "S3 Buckets".to_string(),
11757 breadcrumb: "S3 > Buckets".to_string(),
11758 },
11759 ];
11760 app.mode = Mode::TabPicker;
11761 app.current_tab = 0;
11762
11763 app.handle_action(Action::FilterInput('s'));
11765 app.handle_action(Action::FilterInput('3'));
11766
11767 app.handle_action(Action::Select);
11769
11770 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
11772 assert_eq!(app.tab_filter, ""); }
11774}
11775
11776#[cfg(test)]
11777mod region_latency_tests {
11778 use super::*;
11779 use test_helpers::*;
11780
11781 #[test]
11782 fn test_regions_sorted_by_latency() {
11783 let mut app = test_app();
11784
11785 app.region_latencies.insert("us-west-2".to_string(), 50);
11787 app.region_latencies.insert("us-east-1".to_string(), 10);
11788 app.region_latencies.insert("eu-west-1".to_string(), 100);
11789
11790 let filtered = app.get_filtered_regions();
11791
11792 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
11794
11795 assert!(with_latency.len() >= 3);
11796 assert_eq!(with_latency[0].code, "us-east-1");
11797 assert_eq!(with_latency[0].latency_ms, Some(10));
11798 assert_eq!(with_latency[1].code, "us-west-2");
11799 assert_eq!(with_latency[1].latency_ms, Some(50));
11800 assert_eq!(with_latency[2].code, "eu-west-1");
11801 assert_eq!(with_latency[2].latency_ms, Some(100));
11802 }
11803
11804 #[test]
11805 fn test_regions_with_latency_before_without() {
11806 let mut app = test_app();
11807
11808 app.region_latencies.insert("eu-west-1".to_string(), 100);
11810
11811 let filtered = app.get_filtered_regions();
11812
11813 assert_eq!(filtered[0].code, "eu-west-1");
11815 assert_eq!(filtered[0].latency_ms, Some(100));
11816
11817 for region in &filtered[1..] {
11819 assert!(region.latency_ms.is_none());
11820 }
11821 }
11822
11823 #[test]
11824 fn test_region_filter_with_latency() {
11825 let mut app = test_app();
11826
11827 app.region_latencies.insert("us-east-1".to_string(), 10);
11828 app.region_latencies.insert("us-west-2".to_string(), 50);
11829 app.region_filter = "us".to_string();
11830
11831 let filtered = app.get_filtered_regions();
11832
11833 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
11835 assert_eq!(filtered[0].code, "us-east-1");
11836 assert_eq!(filtered[1].code, "us-west-2");
11837 }
11838
11839 #[test]
11840 fn test_latency_persists_across_filters() {
11841 let mut app = test_app();
11842
11843 app.region_latencies.insert("us-east-1".to_string(), 10);
11844
11845 app.region_filter = "eu".to_string();
11847 let filtered = app.get_filtered_regions();
11848 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
11849
11850 app.region_filter.clear();
11852 let all = app.get_filtered_regions();
11853
11854 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
11856 assert_eq!(us_east.latency_ms, Some(10));
11857 }
11858
11859 #[test]
11860 fn test_measure_region_latencies_clears_previous() {
11861 let mut app = test_app();
11862
11863 app.region_latencies.insert("us-east-1".to_string(), 100);
11865 app.region_latencies.insert("eu-west-1".to_string(), 200);
11866
11867 app.measure_region_latencies();
11869
11870 assert!(
11872 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
11873 );
11874 }
11875
11876 #[test]
11877 fn test_regions_with_latency_sorted_first() {
11878 let mut app = test_app();
11879
11880 app.region_latencies.insert("us-east-1".to_string(), 50);
11882 app.region_latencies.insert("eu-west-1".to_string(), 500);
11883
11884 let filtered = app.get_filtered_regions();
11885
11886 assert!(filtered.len() > 2);
11888
11889 assert_eq!(filtered[0].code, "us-east-1");
11891 assert_eq!(filtered[0].latency_ms, Some(50));
11892 assert_eq!(filtered[1].code, "eu-west-1");
11893 assert_eq!(filtered[1].latency_ms, Some(500));
11894
11895 for region in &filtered[2..] {
11897 assert!(region.latency_ms.is_none());
11898 }
11899 }
11900
11901 #[test]
11902 fn test_regions_without_latency_sorted_as_1000ms() {
11903 let mut app = test_app();
11904
11905 app.region_latencies
11907 .insert("ap-southeast-2".to_string(), 1500);
11908 app.region_latencies.insert("us-east-1".to_string(), 50);
11910
11911 let filtered = app.get_filtered_regions();
11912
11913 assert_eq!(filtered[0].code, "us-east-1");
11915 assert_eq!(filtered[0].latency_ms, Some(50));
11916
11917 let slow_region_idx = filtered
11919 .iter()
11920 .position(|r| r.code == "ap-southeast-2")
11921 .unwrap();
11922 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
11926 assert!(region.latency_ms.is_none());
11927 }
11928 }
11929
11930 #[test]
11931 fn test_region_picker_opens_with_latencies() {
11932 let mut app = test_app();
11933
11934 app.region_filter.clear();
11936 app.region_picker_selected = 0;
11937 app.measure_region_latencies();
11938
11939 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
11942 }
11943
11944 #[test]
11945 fn test_ecr_tab_next() {
11946 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
11947 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
11948 }
11949
11950 #[test]
11951 fn test_ecr_tab_switching() {
11952 let mut app = test_app();
11953 app.current_service = Service::EcrRepositories;
11954 app.service_selected = true;
11955 app.ecr_state.tab = EcrTab::Private;
11956
11957 app.handle_action(Action::NextDetailTab);
11958 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11959 assert_eq!(app.ecr_state.repositories.selected, 0);
11960
11961 app.handle_action(Action::NextDetailTab);
11962 assert_eq!(app.ecr_state.tab, EcrTab::Private);
11963 }
11964
11965 #[test]
11966 fn test_ecr_navigation() {
11967 let mut app = test_app();
11968 app.current_service = Service::EcrRepositories;
11969 app.service_selected = true;
11970 app.mode = Mode::Normal;
11971 app.ecr_state.repositories.items = vec![
11972 EcrRepository {
11973 name: "repo1".to_string(),
11974 uri: "uri1".to_string(),
11975 created_at: "2023-01-01".to_string(),
11976 tag_immutability: "MUTABLE".to_string(),
11977 encryption_type: "AES256".to_string(),
11978 },
11979 EcrRepository {
11980 name: "repo2".to_string(),
11981 uri: "uri2".to_string(),
11982 created_at: "2023-01-02".to_string(),
11983 tag_immutability: "IMMUTABLE".to_string(),
11984 encryption_type: "KMS".to_string(),
11985 },
11986 ];
11987
11988 app.handle_action(Action::NextItem);
11989 assert_eq!(app.ecr_state.repositories.selected, 1);
11990
11991 app.handle_action(Action::PrevItem);
11992 assert_eq!(app.ecr_state.repositories.selected, 0);
11993 }
11994
11995 #[test]
11996 fn test_ecr_filter() {
11997 let mut app = test_app();
11998 app.current_service = Service::EcrRepositories;
11999 app.service_selected = true;
12000 app.ecr_state.repositories.items = vec![
12001 EcrRepository {
12002 name: "my-app".to_string(),
12003 uri: "uri1".to_string(),
12004 created_at: "2023-01-01".to_string(),
12005 tag_immutability: "MUTABLE".to_string(),
12006 encryption_type: "AES256".to_string(),
12007 },
12008 EcrRepository {
12009 name: "other-service".to_string(),
12010 uri: "uri2".to_string(),
12011 created_at: "2023-01-02".to_string(),
12012 tag_immutability: "IMMUTABLE".to_string(),
12013 encryption_type: "KMS".to_string(),
12014 },
12015 ];
12016
12017 app.ecr_state.repositories.filter = "app".to_string();
12018 let filtered = filtered_ecr_repositories(&app);
12019 assert_eq!(filtered.len(), 1);
12020 assert_eq!(filtered[0].name, "my-app");
12021 }
12022
12023 #[test]
12024 fn test_ecr_filter_input() {
12025 let mut app = test_app();
12026 app.current_service = Service::EcrRepositories;
12027 app.service_selected = true;
12028 app.mode = Mode::FilterInput;
12029
12030 app.handle_action(Action::FilterInput('t'));
12031 app.handle_action(Action::FilterInput('e'));
12032 app.handle_action(Action::FilterInput('s'));
12033 app.handle_action(Action::FilterInput('t'));
12034 assert_eq!(app.ecr_state.repositories.filter, "test");
12035
12036 app.handle_action(Action::FilterBackspace);
12037 assert_eq!(app.ecr_state.repositories.filter, "tes");
12038 }
12039
12040 #[test]
12041 fn test_ecr_filter_resets_selection() {
12042 let mut app = test_app();
12043 app.current_service = Service::EcrRepositories;
12044 app.service_selected = true;
12045 app.mode = Mode::FilterInput;
12046 app.ecr_state.repositories.items = vec![
12047 EcrRepository {
12048 name: "repo1".to_string(),
12049 uri: "uri1".to_string(),
12050 created_at: "2023-01-01".to_string(),
12051 tag_immutability: "MUTABLE".to_string(),
12052 encryption_type: "AES256".to_string(),
12053 },
12054 EcrRepository {
12055 name: "repo2".to_string(),
12056 uri: "uri2".to_string(),
12057 created_at: "2023-01-02".to_string(),
12058 tag_immutability: "IMMUTABLE".to_string(),
12059 encryption_type: "KMS".to_string(),
12060 },
12061 EcrRepository {
12062 name: "repo3".to_string(),
12063 uri: "uri3".to_string(),
12064 created_at: "2023-01-03".to_string(),
12065 tag_immutability: "MUTABLE".to_string(),
12066 encryption_type: "AES256".to_string(),
12067 },
12068 ];
12069
12070 app.ecr_state.repositories.selected = 2;
12072 assert_eq!(app.ecr_state.repositories.selected, 2);
12073
12074 app.handle_action(Action::FilterInput('t'));
12076 assert_eq!(app.ecr_state.repositories.filter, "t");
12077 assert_eq!(app.ecr_state.repositories.selected, 0);
12078
12079 app.ecr_state.repositories.selected = 1;
12081
12082 app.handle_action(Action::FilterBackspace);
12084 assert_eq!(app.ecr_state.repositories.filter, "");
12085 assert_eq!(app.ecr_state.repositories.selected, 0);
12086 }
12087
12088 #[test]
12089 fn test_ecr_images_filter_resets_selection() {
12090 let mut app = test_app();
12091 app.current_service = Service::EcrRepositories;
12092 app.service_selected = true;
12093 app.mode = Mode::FilterInput;
12094 app.ecr_state.current_repository = Some("test-repo".to_string());
12095 app.ecr_state.images.items = vec![
12096 EcrImage {
12097 tag: "v1.0.0".to_string(),
12098 artifact_type: "container".to_string(),
12099 digest: "sha256:abc123".to_string(),
12100 pushed_at: "2023-01-01".to_string(),
12101 size_bytes: 1000,
12102 uri: "uri1".to_string(),
12103 last_pull_time: "".to_string(),
12104 },
12105 EcrImage {
12106 tag: "v2.0.0".to_string(),
12107 artifact_type: "container".to_string(),
12108 digest: "sha256:def456".to_string(),
12109 pushed_at: "2023-01-02".to_string(),
12110 size_bytes: 2000,
12111 uri: "uri2".to_string(),
12112 last_pull_time: "".to_string(),
12113 },
12114 ];
12115
12116 app.ecr_state.images.selected = 1;
12118 assert_eq!(app.ecr_state.images.selected, 1);
12119
12120 app.handle_action(Action::FilterInput('v'));
12122 assert_eq!(app.ecr_state.images.filter, "v");
12123 assert_eq!(app.ecr_state.images.selected, 0);
12124 }
12125
12126 #[test]
12127 fn test_iam_users_filter_input() {
12128 let mut app = test_app();
12129 app.current_service = Service::IamUsers;
12130 app.service_selected = true;
12131 app.mode = Mode::FilterInput;
12132
12133 app.handle_action(Action::FilterInput('a'));
12134 app.handle_action(Action::FilterInput('d'));
12135 app.handle_action(Action::FilterInput('m'));
12136 app.handle_action(Action::FilterInput('i'));
12137 app.handle_action(Action::FilterInput('n'));
12138 assert_eq!(app.iam_state.users.filter, "admin");
12139
12140 app.handle_action(Action::FilterBackspace);
12141 assert_eq!(app.iam_state.users.filter, "admi");
12142 }
12143
12144 #[test]
12145 fn test_iam_policies_filter_input() {
12146 let mut app = test_app();
12147 app.current_service = Service::IamUsers;
12148 app.service_selected = true;
12149 app.iam_state.current_user = Some("testuser".to_string());
12150 app.mode = Mode::FilterInput;
12151
12152 app.handle_action(Action::FilterInput('r'));
12153 app.handle_action(Action::FilterInput('e'));
12154 app.handle_action(Action::FilterInput('a'));
12155 app.handle_action(Action::FilterInput('d'));
12156 assert_eq!(app.iam_state.policies.filter, "read");
12157
12158 app.handle_action(Action::FilterBackspace);
12159 assert_eq!(app.iam_state.policies.filter, "rea");
12160 }
12161
12162 #[test]
12163 fn test_iam_start_filter() {
12164 let mut app = test_app();
12165 app.current_service = Service::IamUsers;
12166 app.service_selected = true;
12167 app.mode = Mode::Normal;
12168
12169 app.handle_action(Action::StartFilter);
12170 assert_eq!(app.mode, Mode::FilterInput);
12171 }
12172
12173 #[test]
12174 fn test_iam_roles_filter_input() {
12175 let mut app = test_app();
12176 app.current_service = Service::IamRoles;
12177 app.service_selected = true;
12178 app.mode = Mode::FilterInput;
12179
12180 app.handle_action(Action::FilterInput('a'));
12181 app.handle_action(Action::FilterInput('d'));
12182 app.handle_action(Action::FilterInput('m'));
12183 app.handle_action(Action::FilterInput('i'));
12184 app.handle_action(Action::FilterInput('n'));
12185 assert_eq!(app.iam_state.roles.filter, "admin");
12186
12187 app.handle_action(Action::FilterBackspace);
12188 assert_eq!(app.iam_state.roles.filter, "admi");
12189 }
12190
12191 #[test]
12192 fn test_iam_roles_start_filter() {
12193 let mut app = test_app();
12194 app.current_service = Service::IamRoles;
12195 app.service_selected = true;
12196 app.mode = Mode::Normal;
12197
12198 app.handle_action(Action::StartFilter);
12199 assert_eq!(app.mode, Mode::FilterInput);
12200 }
12201
12202 #[test]
12203 fn test_iam_roles_navigation() {
12204 let mut app = test_app();
12205 app.current_service = Service::IamRoles;
12206 app.service_selected = true;
12207 app.mode = Mode::Normal;
12208 app.iam_state.roles.items = (0..10)
12209 .map(|i| IamRole {
12210 role_name: format!("role{}", i),
12211 path: "/".to_string(),
12212 trusted_entities: String::new(),
12213 last_activity: String::new(),
12214 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12215 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12216 description: String::new(),
12217 max_session_duration: Some(3600),
12218 })
12219 .collect();
12220
12221 assert_eq!(app.iam_state.roles.selected, 0);
12222
12223 app.handle_action(Action::NextItem);
12224 assert_eq!(app.iam_state.roles.selected, 1);
12225
12226 app.handle_action(Action::NextItem);
12227 assert_eq!(app.iam_state.roles.selected, 2);
12228
12229 app.handle_action(Action::PrevItem);
12230 assert_eq!(app.iam_state.roles.selected, 1);
12231 }
12232
12233 #[test]
12234 fn test_iam_roles_page_hotkey() {
12235 let mut app = test_app();
12236 app.current_service = Service::IamRoles;
12237 app.service_selected = true;
12238 app.mode = Mode::Normal;
12239 app.iam_state.roles.page_size = PageSize::Ten;
12240 app.iam_state.roles.items = (0..100)
12241 .map(|i| IamRole {
12242 role_name: format!("role{}", i),
12243 path: "/".to_string(),
12244 trusted_entities: String::new(),
12245 last_activity: String::new(),
12246 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12247 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12248 description: String::new(),
12249 max_session_duration: Some(3600),
12250 })
12251 .collect();
12252
12253 app.handle_action(Action::FilterInput('2'));
12254 app.handle_action(Action::OpenColumnSelector);
12255 assert_eq!(app.iam_state.roles.selected, 10); }
12257
12258 #[test]
12259 fn test_iam_users_page_hotkey() {
12260 let mut app = test_app();
12261 app.current_service = Service::IamUsers;
12262 app.service_selected = true;
12263 app.mode = Mode::Normal;
12264 app.iam_state.users.page_size = PageSize::Ten;
12265 app.iam_state.users.items = (0..100)
12266 .map(|i| IamUser {
12267 user_name: format!("user{}", i),
12268 path: "/".to_string(),
12269 groups: String::new(),
12270 last_activity: String::new(),
12271 mfa: String::new(),
12272 password_age: String::new(),
12273 console_last_sign_in: String::new(),
12274 access_key_id: String::new(),
12275 active_key_age: String::new(),
12276 access_key_last_used: String::new(),
12277 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
12278 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12279 console_access: String::new(),
12280 signing_certs: String::new(),
12281 })
12282 .collect();
12283
12284 app.handle_action(Action::FilterInput('3'));
12285 app.handle_action(Action::OpenColumnSelector);
12286 assert_eq!(app.iam_state.users.selected, 20); }
12288
12289 #[test]
12290 fn test_ecr_scroll_navigation() {
12291 let mut app = test_app();
12292 app.current_service = Service::EcrRepositories;
12293 app.service_selected = true;
12294 app.ecr_state.repositories.items = (0..20)
12295 .map(|i| EcrRepository {
12296 name: format!("repo{}", i),
12297 uri: format!("uri{}", i),
12298 created_at: "2023-01-01".to_string(),
12299 tag_immutability: "MUTABLE".to_string(),
12300 encryption_type: "AES256".to_string(),
12301 })
12302 .collect();
12303
12304 app.handle_action(Action::ScrollDown);
12305 assert_eq!(app.ecr_state.repositories.selected, 10);
12306
12307 app.handle_action(Action::ScrollUp);
12308 assert_eq!(app.ecr_state.repositories.selected, 0);
12309 }
12310
12311 #[test]
12312 fn test_ecr_tab_switching_triggers_reload() {
12313 let mut app = test_app();
12314 app.current_service = Service::EcrRepositories;
12315 app.service_selected = true;
12316 app.ecr_state.tab = EcrTab::Private;
12317 app.ecr_state.repositories.loading = false;
12318 app.ecr_state.repositories.items = vec![EcrRepository {
12319 name: "private-repo".to_string(),
12320 uri: "uri".to_string(),
12321 created_at: "2023-01-01".to_string(),
12322 tag_immutability: "MUTABLE".to_string(),
12323 encryption_type: "AES256".to_string(),
12324 }];
12325
12326 app.handle_action(Action::NextDetailTab);
12327 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12328 assert!(app.ecr_state.repositories.loading);
12329 assert_eq!(app.ecr_state.repositories.selected, 0);
12330 }
12331
12332 #[test]
12333 fn test_ecr_tab_cycles_between_private_and_public() {
12334 let mut app = test_app();
12335 app.current_service = Service::EcrRepositories;
12336 app.service_selected = true;
12337 app.ecr_state.tab = EcrTab::Private;
12338
12339 app.handle_action(Action::NextDetailTab);
12340 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12341
12342 app.handle_action(Action::NextDetailTab);
12343 assert_eq!(app.ecr_state.tab, EcrTab::Private);
12344 }
12345
12346 #[test]
12347 fn test_page_size_values() {
12348 assert_eq!(PageSize::Ten.value(), 10);
12349 assert_eq!(PageSize::TwentyFive.value(), 25);
12350 assert_eq!(PageSize::Fifty.value(), 50);
12351 assert_eq!(PageSize::OneHundred.value(), 100);
12352 }
12353
12354 #[test]
12355 fn test_page_size_next() {
12356 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
12357 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
12358 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
12359 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
12360 }
12361
12362 #[test]
12363 fn test_ecr_enter_drills_into_repository() {
12364 let mut app = test_app();
12365 app.current_service = Service::EcrRepositories;
12366 app.service_selected = true;
12367 app.mode = Mode::Normal;
12368 app.ecr_state.repositories.items = vec![EcrRepository {
12369 name: "my-repo".to_string(),
12370 uri: "uri".to_string(),
12371 created_at: "2023-01-01".to_string(),
12372 tag_immutability: "MUTABLE".to_string(),
12373 encryption_type: "AES256".to_string(),
12374 }];
12375
12376 app.handle_action(Action::Select);
12377 assert_eq!(
12378 app.ecr_state.current_repository,
12379 Some("my-repo".to_string())
12380 );
12381 assert!(app.ecr_state.repositories.loading);
12382 }
12383
12384 #[test]
12385 fn test_ecr_repository_expansion() {
12386 let mut app = test_app();
12387 app.current_service = Service::EcrRepositories;
12388 app.service_selected = true;
12389 app.ecr_state.repositories.items = vec![EcrRepository {
12390 name: "my-repo".to_string(),
12391 uri: "uri".to_string(),
12392 created_at: "2023-01-01".to_string(),
12393 tag_immutability: "MUTABLE".to_string(),
12394 encryption_type: "AES256".to_string(),
12395 }];
12396 app.ecr_state.repositories.selected = 0;
12397
12398 assert_eq!(app.ecr_state.repositories.expanded_item, None);
12399
12400 app.handle_action(Action::NextPane);
12401 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
12402
12403 app.handle_action(Action::PrevPane);
12404 assert_eq!(app.ecr_state.repositories.expanded_item, None);
12405 }
12406
12407 #[test]
12408 fn test_ecr_ctrl_d_scrolls_down() {
12409 let mut app = test_app();
12410 app.current_service = Service::EcrRepositories;
12411 app.service_selected = true;
12412 app.mode = Mode::Normal;
12413 app.ecr_state.repositories.items = (0..30)
12414 .map(|i| EcrRepository {
12415 name: format!("repo{}", i),
12416 uri: format!("uri{}", i),
12417 created_at: "2023-01-01".to_string(),
12418 tag_immutability: "MUTABLE".to_string(),
12419 encryption_type: "AES256".to_string(),
12420 })
12421 .collect();
12422 app.ecr_state.repositories.selected = 0;
12423
12424 app.handle_action(Action::PageDown);
12425 assert_eq!(app.ecr_state.repositories.selected, 10);
12426 }
12427
12428 #[test]
12429 fn test_ecr_ctrl_u_scrolls_up() {
12430 let mut app = test_app();
12431 app.current_service = Service::EcrRepositories;
12432 app.service_selected = true;
12433 app.mode = Mode::Normal;
12434 app.ecr_state.repositories.items = (0..30)
12435 .map(|i| EcrRepository {
12436 name: format!("repo{}", i),
12437 uri: format!("uri{}", i),
12438 created_at: "2023-01-01".to_string(),
12439 tag_immutability: "MUTABLE".to_string(),
12440 encryption_type: "AES256".to_string(),
12441 })
12442 .collect();
12443 app.ecr_state.repositories.selected = 15;
12444
12445 app.handle_action(Action::PageUp);
12446 assert_eq!(app.ecr_state.repositories.selected, 5);
12447 }
12448
12449 #[test]
12450 fn test_ecr_images_ctrl_d_scrolls_down() {
12451 let mut app = test_app();
12452 app.current_service = Service::EcrRepositories;
12453 app.service_selected = true;
12454 app.mode = Mode::Normal;
12455 app.ecr_state.current_repository = Some("repo".to_string());
12456 app.ecr_state.images.items = (0..30)
12457 .map(|i| EcrImage {
12458 tag: format!("tag{}", i),
12459 artifact_type: "container".to_string(),
12460 pushed_at: "2023-01-01T12:00:00Z".to_string(),
12461 size_bytes: 104857600,
12462 uri: format!("uri{}", i),
12463 digest: format!("sha256:{}", i),
12464 last_pull_time: String::new(),
12465 })
12466 .collect();
12467 app.ecr_state.images.selected = 0;
12468
12469 app.handle_action(Action::PageDown);
12470 assert_eq!(app.ecr_state.images.selected, 10);
12471 }
12472
12473 #[test]
12474 fn test_ecr_esc_goes_back_from_images_to_repos() {
12475 let mut app = test_app();
12476 app.current_service = Service::EcrRepositories;
12477 app.service_selected = true;
12478 app.mode = Mode::Normal;
12479 app.ecr_state.current_repository = Some("my-repo".to_string());
12480 app.ecr_state.images.items = vec![EcrImage {
12481 tag: "latest".to_string(),
12482 artifact_type: "container".to_string(),
12483 pushed_at: "2023-01-01T12:00:00Z".to_string(),
12484 size_bytes: 104857600,
12485 uri: "uri".to_string(),
12486 digest: "sha256:abc".to_string(),
12487 last_pull_time: String::new(),
12488 }];
12489
12490 app.handle_action(Action::GoBack);
12491 assert_eq!(app.ecr_state.current_repository, None);
12492 assert!(app.ecr_state.images.items.is_empty());
12493 }
12494
12495 #[test]
12496 fn test_ecr_esc_collapses_expanded_image_first() {
12497 let mut app = test_app();
12498 app.current_service = Service::EcrRepositories;
12499 app.service_selected = true;
12500 app.mode = Mode::Normal;
12501 app.ecr_state.current_repository = Some("my-repo".to_string());
12502 app.ecr_state.images.expanded_item = Some(0);
12503
12504 app.handle_action(Action::GoBack);
12505 assert_eq!(app.ecr_state.images.expanded_item, None);
12506 assert_eq!(
12507 app.ecr_state.current_repository,
12508 Some("my-repo".to_string())
12509 );
12510 }
12511
12512 #[test]
12513 fn test_pagination_with_lowercase_p() {
12514 let mut app = test_app();
12515 app.current_service = Service::EcrRepositories;
12516 app.service_selected = true;
12517 app.mode = Mode::Normal;
12518 app.ecr_state.repositories.items = (0..100)
12519 .map(|i| EcrRepository {
12520 name: format!("repo{}", i),
12521 uri: format!("uri{}", i),
12522 created_at: "2023-01-01".to_string(),
12523 tag_immutability: "MUTABLE".to_string(),
12524 encryption_type: "AES256".to_string(),
12525 })
12526 .collect();
12527
12528 app.handle_action(Action::FilterInput('2'));
12530 assert_eq!(app.page_input, "2");
12531
12532 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
12536
12537 #[test]
12538 fn test_lowercase_p_without_number_opens_preferences() {
12539 let mut app = test_app();
12540 app.current_service = Service::EcrRepositories;
12541 app.service_selected = true;
12542 app.mode = Mode::Normal;
12543
12544 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
12546 }
12547
12548 #[test]
12549 fn test_ctrl_o_generates_correct_console_url() {
12550 let mut app = test_app();
12551 app.current_service = Service::EcrRepositories;
12552 app.service_selected = true;
12553 app.mode = Mode::Normal;
12554 app.config.account_id = "123456789012".to_string();
12555
12556 let url = app.get_console_url();
12558 assert!(url.contains("ecr/private-registry/repositories"));
12559 assert!(url.contains("region=us-east-1"));
12560
12561 app.ecr_state.current_repository = Some("my-repo".to_string());
12563 let url = app.get_console_url();
12564 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
12565 assert!(url.contains("region=us-east-1"));
12566 }
12567
12568 #[test]
12569 fn test_page_input_display_and_reset() {
12570 let mut app = test_app();
12571 app.current_service = Service::EcrRepositories;
12572 app.service_selected = true;
12573 app.mode = Mode::Normal;
12574 app.ecr_state.repositories.items = (0..100)
12575 .map(|i| EcrRepository {
12576 name: format!("repo{}", i),
12577 uri: format!("uri{}", i),
12578 created_at: "2023-01-01".to_string(),
12579 tag_immutability: "MUTABLE".to_string(),
12580 encryption_type: "AES256".to_string(),
12581 })
12582 .collect();
12583
12584 app.handle_action(Action::FilterInput('2'));
12586 assert_eq!(app.page_input, "2");
12587
12588 app.handle_action(Action::OpenColumnSelector);
12590 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
12593
12594 #[test]
12595 fn test_page_navigation_updates_scroll_offset_for_cfn() {
12596 let mut app = test_app();
12597 app.current_service = Service::CloudFormationStacks;
12598 app.service_selected = true;
12599 app.mode = Mode::Normal;
12600 app.cfn_state.table.items = (0..100)
12601 .map(|i| CfnStack {
12602 name: format!("stack-{}", i),
12603 stack_id: format!(
12604 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
12605 i
12606 ),
12607 status: "CREATE_COMPLETE".to_string(),
12608 created_time: "2023-01-01T00:00:00Z".to_string(),
12609 updated_time: "2023-01-01T00:00:00Z".to_string(),
12610 deleted_time: String::new(),
12611 drift_status: "IN_SYNC".to_string(),
12612 last_drift_check_time: String::new(),
12613 status_reason: String::new(),
12614 description: String::new(),
12615 detailed_status: String::new(),
12616 root_stack: String::new(),
12617 parent_stack: String::new(),
12618 termination_protection: false,
12619 iam_role: String::new(),
12620 tags: vec![],
12621 stack_policy: String::new(),
12622 rollback_monitoring_time: String::new(),
12623 rollback_alarms: vec![],
12624 notification_arns: vec![],
12625 })
12626 .collect();
12627
12628 app.handle_action(Action::FilterInput('2'));
12630 assert_eq!(app.page_input, "2");
12631
12632 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
12637 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
12639 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
12640
12641 let current_page = app.cfn_state.table.scroll_offset / page_size;
12643 assert_eq!(
12644 current_page, 1,
12645 "2p should go to page 2 (0-indexed as 1), not page 3"
12646 ); }
12648
12649 #[test]
12650 fn test_3p_goes_to_page_3_not_page_5() {
12651 let mut app = test_app();
12652 app.current_service = Service::CloudFormationStacks;
12653 app.service_selected = true;
12654 app.mode = Mode::Normal;
12655 app.cfn_state.table.items = (0..200)
12656 .map(|i| CfnStack {
12657 name: format!("stack-{}", i),
12658 stack_id: format!(
12659 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
12660 i
12661 ),
12662 status: "CREATE_COMPLETE".to_string(),
12663 created_time: "2023-01-01T00:00:00Z".to_string(),
12664 updated_time: "2023-01-01T00:00:00Z".to_string(),
12665 deleted_time: String::new(),
12666 drift_status: "IN_SYNC".to_string(),
12667 last_drift_check_time: String::new(),
12668 status_reason: String::new(),
12669 description: String::new(),
12670 detailed_status: String::new(),
12671 root_stack: String::new(),
12672 parent_stack: String::new(),
12673 termination_protection: false,
12674 iam_role: String::new(),
12675 tags: vec![],
12676 stack_policy: String::new(),
12677 rollback_monitoring_time: String::new(),
12678 rollback_alarms: vec![],
12679 notification_arns: vec![],
12680 })
12681 .collect();
12682
12683 app.handle_action(Action::FilterInput('3'));
12685 app.handle_action(Action::OpenColumnSelector);
12686
12687 let page_size = app.cfn_state.table.page_size.value();
12688 let current_page = app.cfn_state.table.scroll_offset / page_size;
12689 assert_eq!(
12690 current_page, 2,
12691 "3p should go to page 3 (0-indexed as 2), not page 5"
12692 );
12693 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
12694 }
12695
12696 #[test]
12697 fn test_log_streams_page_navigation_uses_correct_page_size() {
12698 let mut app = test_app();
12699 app.current_service = Service::CloudWatchLogGroups;
12700 app.view_mode = ViewMode::Detail;
12701 app.service_selected = true;
12702 app.mode = Mode::Normal;
12703 app.log_groups_state.log_streams = (0..100)
12704 .map(|i| LogStream {
12705 name: format!("stream-{}", i),
12706 creation_time: None,
12707 last_event_time: None,
12708 })
12709 .collect();
12710
12711 app.handle_action(Action::FilterInput('2'));
12713 app.handle_action(Action::OpenColumnSelector);
12714
12715 assert_eq!(app.log_groups_state.stream_current_page, 1);
12717 assert_eq!(app.log_groups_state.selected_stream, 0);
12718
12719 assert_eq!(
12721 app.log_groups_state.stream_current_page, 1,
12722 "2p should go to page 2 (0-indexed as 1), not page 3"
12723 );
12724 }
12725
12726 #[test]
12727 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
12728 let mut app = test_app();
12729 app.current_service = Service::EcrRepositories;
12730 app.service_selected = true;
12731 app.mode = Mode::Normal;
12732 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
12734 .map(|i| EcrRepository {
12735 name: format!("repo{}", i),
12736 uri: format!("uri{}", i),
12737 created_at: "2023-01-01".to_string(),
12738 tag_immutability: "MUTABLE".to_string(),
12739 encryption_type: "AES256".to_string(),
12740 })
12741 .collect();
12742
12743 app.handle_action(Action::FilterInput('3'));
12745 app.handle_action(Action::OpenColumnSelector);
12746
12747 assert_eq!(app.ecr_state.repositories.selected, 50);
12749
12750 let page_size = app.ecr_state.repositories.page_size.value();
12751 let current_page = app.ecr_state.repositories.selected / page_size;
12752 assert_eq!(
12753 current_page, 2,
12754 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
12755 );
12756 }
12757
12758 #[test]
12759 fn test_page_navigation_updates_scroll_offset_for_alarms() {
12760 let mut app = test_app();
12761 app.current_service = Service::CloudWatchAlarms;
12762 app.service_selected = true;
12763 app.mode = Mode::Normal;
12764 app.alarms_state.table.items = (0..100)
12765 .map(|i| Alarm {
12766 name: format!("alarm-{}", i),
12767 state: "OK".to_string(),
12768 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
12769 description: String::new(),
12770 metric_name: "CPUUtilization".to_string(),
12771 namespace: "AWS/EC2".to_string(),
12772 statistic: "Average".to_string(),
12773 period: 300,
12774 comparison_operator: "GreaterThanThreshold".to_string(),
12775 threshold: 80.0,
12776 actions_enabled: true,
12777 state_reason: String::new(),
12778 resource: String::new(),
12779 dimensions: String::new(),
12780 expression: String::new(),
12781 alarm_type: "MetricAlarm".to_string(),
12782 cross_account: String::new(),
12783 })
12784 .collect();
12785
12786 app.handle_action(Action::FilterInput('2'));
12788 app.handle_action(Action::OpenColumnSelector);
12789
12790 let page_size = app.alarms_state.table.page_size.value();
12792 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
12794 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
12795 }
12796
12797 #[test]
12798 fn test_ecr_pagination_with_65_repos() {
12799 let mut app = test_app();
12800 app.current_service = Service::EcrRepositories;
12801 app.service_selected = true;
12802 app.mode = Mode::Normal;
12803 app.ecr_state.repositories.items = (0..65)
12804 .map(|i| EcrRepository {
12805 name: format!("repo{:02}", i),
12806 uri: format!("uri{}", i),
12807 created_at: "2023-01-01".to_string(),
12808 tag_immutability: "MUTABLE".to_string(),
12809 encryption_type: "AES256".to_string(),
12810 })
12811 .collect();
12812
12813 assert_eq!(app.ecr_state.repositories.selected, 0);
12815 let page_size = 50;
12816 let current_page = app.ecr_state.repositories.selected / page_size;
12817 assert_eq!(current_page, 0);
12818
12819 app.handle_action(Action::FilterInput('2'));
12821 app.handle_action(Action::OpenColumnSelector);
12822 assert_eq!(app.ecr_state.repositories.selected, 50);
12823
12824 let current_page = app.ecr_state.repositories.selected / page_size;
12826 assert_eq!(current_page, 1);
12827 }
12828
12829 #[test]
12830 fn test_ecr_repos_input_focus_tab_cycling() {
12831 let mut app = test_app();
12832 app.current_service = Service::EcrRepositories;
12833 app.service_selected = true;
12834 app.mode = Mode::FilterInput;
12835 app.ecr_state.input_focus = InputFocus::Filter;
12836
12837 app.handle_action(Action::NextFilterFocus);
12839 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
12840
12841 app.handle_action(Action::NextFilterFocus);
12843 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
12844
12845 app.handle_action(Action::PrevFilterFocus);
12847 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
12848
12849 app.handle_action(Action::PrevFilterFocus);
12851 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
12852 }
12853
12854 #[test]
12855 fn test_ecr_images_column_toggle_not_off_by_one() {
12856 use crate::ecr::image::Column as ImageColumn;
12857 let mut app = test_app();
12858 app.current_service = Service::EcrRepositories;
12859 app.service_selected = true;
12860 app.mode = Mode::ColumnSelector;
12861 app.ecr_state.current_repository = Some("test-repo".to_string());
12862
12863 app.ecr_image_visible_column_ids = ImageColumn::ids();
12865 let initial_count = app.ecr_image_visible_column_ids.len();
12866
12867 app.column_selector_index = 0;
12869 app.handle_action(Action::ToggleColumn);
12870
12871 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
12873 assert!(!app
12874 .ecr_image_visible_column_ids
12875 .contains(&ImageColumn::Tag.id()));
12876
12877 app.handle_action(Action::ToggleColumn);
12879 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
12880 assert!(app
12881 .ecr_image_visible_column_ids
12882 .contains(&ImageColumn::Tag.id()));
12883 }
12884
12885 #[test]
12886 fn test_ecr_repos_column_toggle_works() {
12887 let mut app = test_app();
12888 app.current_service = Service::EcrRepositories;
12889 app.service_selected = true;
12890 app.mode = Mode::ColumnSelector;
12891 app.ecr_state.current_repository = None;
12892
12893 app.ecr_repo_visible_column_ids = EcrColumn::ids();
12895 let initial_count = app.ecr_repo_visible_column_ids.len();
12896
12897 app.column_selector_index = 1;
12899 app.handle_action(Action::ToggleColumn);
12900
12901 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
12903 assert!(!app
12904 .ecr_repo_visible_column_ids
12905 .contains(&EcrColumn::Name.id()));
12906
12907 app.handle_action(Action::ToggleColumn);
12909 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
12910 assert!(app
12911 .ecr_repo_visible_column_ids
12912 .contains(&EcrColumn::Name.id()));
12913 }
12914
12915 #[test]
12916 fn test_ecr_repos_pagination_left_right_navigation() {
12917 use crate::ecr::repo::Repository as EcrRepository;
12918 let mut app = test_app();
12919 app.current_service = Service::EcrRepositories;
12920 app.service_selected = true;
12921 app.mode = Mode::FilterInput;
12922 app.ecr_state.input_focus = InputFocus::Pagination;
12923
12924 app.ecr_state.repositories.items = (0..150)
12926 .map(|i| EcrRepository {
12927 name: format!("repo{:03}", i),
12928 uri: format!("uri{}", i),
12929 created_at: "2023-01-01".to_string(),
12930 tag_immutability: "MUTABLE".to_string(),
12931 encryption_type: "AES256".to_string(),
12932 })
12933 .collect();
12934
12935 app.ecr_state.repositories.selected = 0;
12937 eprintln!(
12938 "Initial: selected={}, focus={:?}, mode={:?}",
12939 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
12940 );
12941
12942 app.handle_action(Action::PageDown);
12944 eprintln!(
12945 "After PageDown: selected={}",
12946 app.ecr_state.repositories.selected
12947 );
12948 assert_eq!(app.ecr_state.repositories.selected, 50);
12949
12950 app.handle_action(Action::PageDown);
12952 eprintln!(
12953 "After 2nd PageDown: selected={}",
12954 app.ecr_state.repositories.selected
12955 );
12956 assert_eq!(app.ecr_state.repositories.selected, 100);
12957
12958 app.handle_action(Action::PageDown);
12960 eprintln!(
12961 "After 3rd PageDown: selected={}",
12962 app.ecr_state.repositories.selected
12963 );
12964 assert_eq!(app.ecr_state.repositories.selected, 100);
12965
12966 app.handle_action(Action::PageUp);
12968 eprintln!(
12969 "After PageUp: selected={}",
12970 app.ecr_state.repositories.selected
12971 );
12972 assert_eq!(app.ecr_state.repositories.selected, 50);
12973
12974 app.handle_action(Action::PageUp);
12976 eprintln!(
12977 "After 2nd PageUp: selected={}",
12978 app.ecr_state.repositories.selected
12979 );
12980 assert_eq!(app.ecr_state.repositories.selected, 0);
12981
12982 app.handle_action(Action::PageUp);
12984 eprintln!(
12985 "After 3rd PageUp: selected={}",
12986 app.ecr_state.repositories.selected
12987 );
12988 assert_eq!(app.ecr_state.repositories.selected, 0);
12989 }
12990
12991 #[test]
12992 fn test_ecr_repos_filter_input_when_input_focused() {
12993 use crate::ecr::repo::Repository as EcrRepository;
12994 let mut app = test_app();
12995 app.current_service = Service::EcrRepositories;
12996 app.service_selected = true;
12997 app.mode = Mode::FilterInput;
12998 app.ecr_state.input_focus = InputFocus::Filter;
12999
13000 app.ecr_state.repositories.items = vec![
13002 EcrRepository {
13003 name: "test-repo".to_string(),
13004 uri: "uri1".to_string(),
13005 created_at: "2023-01-01".to_string(),
13006 tag_immutability: "MUTABLE".to_string(),
13007 encryption_type: "AES256".to_string(),
13008 },
13009 EcrRepository {
13010 name: "prod-repo".to_string(),
13011 uri: "uri2".to_string(),
13012 created_at: "2023-01-01".to_string(),
13013 tag_immutability: "MUTABLE".to_string(),
13014 encryption_type: "AES256".to_string(),
13015 },
13016 ];
13017
13018 assert_eq!(app.ecr_state.repositories.filter, "");
13020 app.handle_action(Action::FilterInput('t'));
13021 assert_eq!(app.ecr_state.repositories.filter, "t");
13022 app.handle_action(Action::FilterInput('e'));
13023 assert_eq!(app.ecr_state.repositories.filter, "te");
13024 app.handle_action(Action::FilterInput('s'));
13025 assert_eq!(app.ecr_state.repositories.filter, "tes");
13026 app.handle_action(Action::FilterInput('t'));
13027 assert_eq!(app.ecr_state.repositories.filter, "test");
13028 }
13029
13030 #[test]
13031 fn test_ecr_repos_digit_input_when_pagination_focused() {
13032 use crate::ecr::repo::Repository as EcrRepository;
13033 let mut app = test_app();
13034 app.current_service = Service::EcrRepositories;
13035 app.service_selected = true;
13036 app.mode = Mode::FilterInput;
13037 app.ecr_state.input_focus = InputFocus::Pagination;
13038
13039 app.ecr_state.repositories.items = vec![EcrRepository {
13041 name: "test-repo".to_string(),
13042 uri: "uri1".to_string(),
13043 created_at: "2023-01-01".to_string(),
13044 tag_immutability: "MUTABLE".to_string(),
13045 encryption_type: "AES256".to_string(),
13046 }];
13047
13048 assert_eq!(app.ecr_state.repositories.filter, "");
13050 assert_eq!(app.page_input, "");
13051 app.handle_action(Action::FilterInput('2'));
13052 assert_eq!(app.ecr_state.repositories.filter, "");
13053 assert_eq!(app.page_input, "2");
13054
13055 app.handle_action(Action::FilterInput('a'));
13057 assert_eq!(app.ecr_state.repositories.filter, "");
13058 assert_eq!(app.page_input, "2");
13059 }
13060
13061 #[test]
13062 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
13063 use crate::ecr::repo::Repository as EcrRepository;
13064 let mut app = test_app();
13065 app.current_service = Service::EcrRepositories;
13066 app.service_selected = true;
13067 app.mode = Mode::FilterInput;
13068 app.ecr_state.input_focus = InputFocus::Filter;
13069
13070 app.ecr_state.repositories.items = (0..150)
13072 .map(|i| EcrRepository {
13073 name: format!("repo{:03}", i),
13074 uri: format!("uri{}", i),
13075 created_at: "2023-01-01".to_string(),
13076 tag_immutability: "MUTABLE".to_string(),
13077 encryption_type: "AES256".to_string(),
13078 })
13079 .collect();
13080
13081 app.ecr_state.repositories.selected = 0;
13083
13084 app.handle_action(Action::PageDown);
13086 assert_eq!(
13087 app.ecr_state.repositories.selected, 10,
13088 "Should scroll down by 10"
13089 );
13090
13091 app.handle_action(Action::PageUp);
13092 assert_eq!(
13093 app.ecr_state.repositories.selected, 0,
13094 "Should scroll back up"
13095 );
13096 }
13097
13098 #[test]
13099 fn test_ecr_repos_pagination_control_actually_works() {
13100 use crate::ecr::repo::Repository as EcrRepository;
13101
13102 let mut app = test_app();
13104 app.current_service = Service::EcrRepositories;
13105 app.service_selected = true;
13106 app.mode = Mode::FilterInput;
13107 app.ecr_state.current_repository = None;
13108 app.ecr_state.input_focus = InputFocus::Pagination;
13109
13110 app.ecr_state.repositories.items = (0..100)
13112 .map(|i| EcrRepository {
13113 name: format!("repo{:03}", i),
13114 uri: format!("uri{}", i),
13115 created_at: "2023-01-01".to_string(),
13116 tag_immutability: "MUTABLE".to_string(),
13117 encryption_type: "AES256".to_string(),
13118 })
13119 .collect();
13120
13121 app.ecr_state.repositories.selected = 0;
13122
13123 assert_eq!(app.mode, Mode::FilterInput);
13125 assert_eq!(app.current_service, Service::EcrRepositories);
13126 assert_eq!(app.ecr_state.current_repository, None);
13127 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13128
13129 app.handle_action(Action::PageDown);
13131 assert_eq!(
13132 app.ecr_state.repositories.selected, 50,
13133 "PageDown should move to page 2"
13134 );
13135
13136 app.handle_action(Action::PageUp);
13137 assert_eq!(
13138 app.ecr_state.repositories.selected, 0,
13139 "PageUp should move back to page 1"
13140 );
13141 }
13142
13143 #[test]
13144 fn test_ecr_repos_start_filter_resets_focus_to_input() {
13145 let mut app = test_app();
13146 app.current_service = Service::EcrRepositories;
13147 app.service_selected = true;
13148 app.mode = Mode::Normal;
13149 app.ecr_state.current_repository = None;
13150
13151 app.ecr_state.input_focus = InputFocus::Pagination;
13153
13154 app.handle_action(Action::StartFilter);
13156
13157 assert_eq!(app.mode, Mode::FilterInput);
13159 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13160 }
13161
13162 #[test]
13163 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
13164 use crate::ecr::repo::Repository as EcrRepository;
13165
13166 let mut app = test_app();
13167 app.current_service = Service::EcrRepositories;
13168 app.service_selected = true;
13169 app.mode = Mode::Normal;
13170 app.ecr_state.current_repository = None;
13171
13172 app.ecr_state.repositories.items = (0..100)
13174 .map(|i| EcrRepository {
13175 name: format!("repo{:03}", i),
13176 uri: format!("uri{}", i),
13177 created_at: "2023-01-01".to_string(),
13178 tag_immutability: "MUTABLE".to_string(),
13179 encryption_type: "AES256".to_string(),
13180 })
13181 .collect();
13182
13183 app.ecr_state.repositories.selected = 0;
13184
13185 app.handle_action(Action::StartFilter);
13187 assert_eq!(app.mode, Mode::FilterInput);
13188 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13189
13190 app.handle_action(Action::NextFilterFocus);
13192 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13193
13194 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
13196 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
13197 app.handle_action(Action::PageDown);
13198 eprintln!(
13199 "After PageDown: selected={}",
13200 app.ecr_state.repositories.selected
13201 );
13202
13203 assert_eq!(
13205 app.ecr_state.repositories.selected, 50,
13206 "Right arrow should move to page 2"
13207 );
13208
13209 app.handle_action(Action::PageUp);
13211 assert_eq!(
13212 app.ecr_state.repositories.selected, 0,
13213 "Left arrow should move back to page 1"
13214 );
13215 }
13216
13217 #[test]
13218 fn test_service_picker_i_key_activates_filter() {
13219 let mut app = test_app();
13220
13221 assert_eq!(app.mode, Mode::ServicePicker);
13223 assert!(app.service_picker.filter.is_empty());
13224
13225 app.handle_action(Action::FilterInput('i'));
13227
13228 assert_eq!(app.mode, Mode::ServicePicker);
13230 assert_eq!(app.service_picker.filter, "i");
13231 }
13232
13233 #[test]
13234 fn test_service_picker_typing_filters_services() {
13235 let mut app = test_app();
13236
13237 assert_eq!(app.mode, Mode::ServicePicker);
13239
13240 app.handle_action(Action::FilterInput('s'));
13242 app.handle_action(Action::FilterInput('3'));
13243
13244 assert_eq!(app.service_picker.filter, "s3");
13245 assert_eq!(app.mode, Mode::ServicePicker);
13246 }
13247
13248 #[test]
13249 fn test_service_picker_resets_on_open() {
13250 let mut app = test_app();
13251
13252 app.service_selected = true;
13254 app.mode = Mode::Normal;
13255
13256 app.service_picker.filter = "previous".to_string();
13258 app.service_picker.selected = 5;
13259
13260 app.handle_action(Action::OpenSpaceMenu);
13262
13263 assert_eq!(app.mode, Mode::SpaceMenu);
13265 assert!(app.service_picker.filter.is_empty());
13266 assert_eq!(app.service_picker.selected, 0);
13267 }
13268
13269 #[test]
13270 fn test_no_pii_in_test_data() {
13271 let test_repo = EcrRepository {
13273 name: "test-repo".to_string(),
13274 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
13275 created_at: "2024-01-01".to_string(),
13276 tag_immutability: "MUTABLE".to_string(),
13277 encryption_type: "AES256".to_string(),
13278 };
13279
13280 assert!(test_repo.uri.starts_with("123456789012"));
13282 assert!(!test_repo.uri.contains("123456789013")); }
13284
13285 #[test]
13286 fn test_lambda_versions_tab_triggers_loading() {
13287 let mut app = test_app();
13288 app.current_service = Service::LambdaFunctions;
13289 app.service_selected = true;
13290
13291 app.lambda_state.current_function = Some("test-function".to_string());
13293 app.lambda_state.detail_tab = LambdaDetailTab::Code;
13294
13295 assert!(app.lambda_state.version_table.items.is_empty());
13297
13298 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13300
13301 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
13304 assert!(app.lambda_state.current_function.is_some());
13305 }
13306
13307 #[test]
13308 fn test_lambda_versions_navigation() {
13309 let mut app = test_app();
13310 app.current_service = Service::LambdaFunctions;
13311 app.service_selected = true;
13312 app.lambda_state.current_function = Some("test-function".to_string());
13313 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13314
13315 app.lambda_state.version_table.items = vec![
13317 LambdaVersion {
13318 version: "3".to_string(),
13319 aliases: "prod".to_string(),
13320 description: "".to_string(),
13321 last_modified: "".to_string(),
13322 architecture: "X86_64".to_string(),
13323 },
13324 LambdaVersion {
13325 version: "2".to_string(),
13326 aliases: "".to_string(),
13327 description: "".to_string(),
13328 last_modified: "".to_string(),
13329 architecture: "X86_64".to_string(),
13330 },
13331 LambdaVersion {
13332 version: "1".to_string(),
13333 aliases: "".to_string(),
13334 description: "".to_string(),
13335 last_modified: "".to_string(),
13336 architecture: "X86_64".to_string(),
13337 },
13338 ];
13339
13340 assert_eq!(app.lambda_state.version_table.items.len(), 3);
13342 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
13343 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
13344
13345 app.lambda_state.version_table.selected = 1;
13347 assert_eq!(app.lambda_state.version_table.selected, 1);
13348 }
13349
13350 #[test]
13351 fn test_lambda_versions_with_aliases() {
13352 let version = LambdaVersion {
13353 version: "35".to_string(),
13354 aliases: "prod, staging".to_string(),
13355 description: "Production version".to_string(),
13356 last_modified: "2024-01-01".to_string(),
13357 architecture: "X86_64".to_string(),
13358 };
13359
13360 assert_eq!(version.aliases, "prod, staging");
13361 assert!(!version.aliases.is_empty());
13362 }
13363
13364 #[test]
13365 fn test_lambda_versions_expansion() {
13366 let mut app = test_app();
13367 app.current_service = Service::LambdaFunctions;
13368 app.service_selected = true;
13369 app.lambda_state.current_function = Some("test-function".to_string());
13370 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13371
13372 app.lambda_state.version_table.items = vec![
13374 LambdaVersion {
13375 version: "2".to_string(),
13376 aliases: "prod".to_string(),
13377 description: "Production".to_string(),
13378 last_modified: "2024-01-01".to_string(),
13379 architecture: "X86_64".to_string(),
13380 },
13381 LambdaVersion {
13382 version: "1".to_string(),
13383 aliases: "".to_string(),
13384 description: "".to_string(),
13385 last_modified: "2024-01-01".to_string(),
13386 architecture: "Arm64".to_string(),
13387 },
13388 ];
13389
13390 app.lambda_state.version_table.selected = 0;
13391
13392 app.lambda_state.version_table.expanded_item = Some(0);
13394 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
13395
13396 app.lambda_state.version_table.selected = 1;
13398 app.lambda_state.version_table.expanded_item = Some(1);
13399 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
13400 }
13401
13402 #[test]
13403 fn test_lambda_versions_page_navigation() {
13404 let mut app = test_app();
13405 app.current_service = Service::LambdaFunctions;
13406 app.service_selected = true;
13407 app.lambda_state.current_function = Some("test-function".to_string());
13408 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13409
13410 app.lambda_state.version_table.items = (1..=30)
13412 .map(|i| LambdaVersion {
13413 version: i.to_string(),
13414 aliases: "".to_string(),
13415 description: "".to_string(),
13416 last_modified: "".to_string(),
13417 architecture: "X86_64".to_string(),
13418 })
13419 .collect();
13420
13421 app.lambda_state.version_table.page_size = PageSize::Ten;
13422 app.lambda_state.version_table.selected = 0;
13423
13424 app.page_input = "2".to_string();
13426 app.handle_action(Action::OpenColumnSelector);
13427
13428 assert_eq!(app.lambda_state.version_table.selected, 10);
13430 }
13431
13432 #[test]
13433 fn test_lambda_versions_pagination_arrow_keys() {
13434 let mut app = test_app();
13435 app.current_service = Service::LambdaFunctions;
13436 app.service_selected = true;
13437 app.lambda_state.current_function = Some("test-function".to_string());
13438 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13439 app.mode = Mode::FilterInput;
13440 app.lambda_state.version_input_focus = InputFocus::Pagination;
13441
13442 app.lambda_state.version_table.items = (1..=30)
13444 .map(|i| LambdaVersion {
13445 version: i.to_string(),
13446 aliases: "".to_string(),
13447 description: "".to_string(),
13448 last_modified: "".to_string(),
13449 architecture: "X86_64".to_string(),
13450 })
13451 .collect();
13452
13453 app.lambda_state.version_table.page_size = PageSize::Ten;
13454 app.lambda_state.version_table.selected = 0;
13455
13456 app.handle_action(Action::PageDown);
13458 assert_eq!(app.lambda_state.version_table.selected, 10);
13459
13460 app.handle_action(Action::PageUp);
13462 assert_eq!(app.lambda_state.version_table.selected, 0);
13463 }
13464
13465 #[test]
13466 fn test_lambda_versions_page_input_in_filter_mode() {
13467 let mut app = test_app();
13468 app.current_service = Service::LambdaFunctions;
13469 app.service_selected = true;
13470 app.lambda_state.current_function = Some("test-function".to_string());
13471 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13472 app.mode = Mode::FilterInput;
13473 app.lambda_state.version_input_focus = InputFocus::Pagination;
13474
13475 app.lambda_state.version_table.items = (1..=30)
13477 .map(|i| LambdaVersion {
13478 version: i.to_string(),
13479 aliases: "".to_string(),
13480 description: "".to_string(),
13481 last_modified: "".to_string(),
13482 architecture: "X86_64".to_string(),
13483 })
13484 .collect();
13485
13486 app.lambda_state.version_table.page_size = PageSize::Ten;
13487 app.lambda_state.version_table.selected = 0;
13488
13489 app.handle_action(Action::FilterInput('2'));
13491 assert_eq!(app.page_input, "2");
13492 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
13496 assert_eq!(app.lambda_state.version_table.selected, 10);
13497 assert_eq!(app.page_input, ""); }
13499
13500 #[test]
13501 fn test_lambda_versions_filter_input() {
13502 let mut app = test_app();
13503 app.current_service = Service::LambdaFunctions;
13504 app.service_selected = true;
13505 app.lambda_state.current_function = Some("test-function".to_string());
13506 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13507 app.mode = Mode::FilterInput;
13508 app.lambda_state.version_input_focus = InputFocus::Filter;
13509
13510 app.lambda_state.version_table.items = vec![
13512 LambdaVersion {
13513 version: "1".to_string(),
13514 aliases: "prod".to_string(),
13515 description: "Production".to_string(),
13516 last_modified: "".to_string(),
13517 architecture: "X86_64".to_string(),
13518 },
13519 LambdaVersion {
13520 version: "2".to_string(),
13521 aliases: "staging".to_string(),
13522 description: "Staging".to_string(),
13523 last_modified: "".to_string(),
13524 architecture: "X86_64".to_string(),
13525 },
13526 ];
13527
13528 app.handle_action(Action::FilterInput('p'));
13530 app.handle_action(Action::FilterInput('r'));
13531 app.handle_action(Action::FilterInput('o'));
13532 app.handle_action(Action::FilterInput('d'));
13533 assert_eq!(app.lambda_state.version_table.filter, "prod");
13534
13535 app.handle_action(Action::FilterBackspace);
13537 assert_eq!(app.lambda_state.version_table.filter, "pro");
13538 }
13539
13540 #[test]
13541 fn test_lambda_aliases_table_expansion() {
13542 use crate::lambda::Alias;
13543
13544 let mut app = test_app();
13545 app.current_service = Service::LambdaFunctions;
13546 app.service_selected = true;
13547 app.lambda_state.current_function = Some("test-function".to_string());
13548 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
13549 app.mode = Mode::Normal;
13550
13551 app.lambda_state.alias_table.items = vec![
13552 Alias {
13553 name: "prod".to_string(),
13554 versions: "1".to_string(),
13555 description: "Production alias".to_string(),
13556 },
13557 Alias {
13558 name: "staging".to_string(),
13559 versions: "2".to_string(),
13560 description: "Staging alias".to_string(),
13561 },
13562 ];
13563
13564 app.lambda_state.alias_table.selected = 0;
13565
13566 app.handle_action(Action::Select);
13568 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
13569 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
13570
13571 app.handle_action(Action::GoBack);
13573 assert_eq!(app.lambda_state.current_alias, None);
13574 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
13575
13576 app.lambda_state.alias_table.selected = 1;
13578 app.handle_action(Action::Select);
13579 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
13580 }
13581
13582 #[test]
13583 fn test_lambda_versions_arrow_key_expansion() {
13584 let mut app = test_app();
13585 app.current_service = Service::LambdaFunctions;
13586 app.service_selected = true;
13587 app.lambda_state.current_function = Some("test-function".to_string());
13588 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13589 app.mode = Mode::Normal;
13590
13591 app.lambda_state.version_table.items = vec![LambdaVersion {
13592 version: "1".to_string(),
13593 aliases: "prod".to_string(),
13594 description: "Production".to_string(),
13595 last_modified: "2024-01-01".to_string(),
13596 architecture: "X86_64".to_string(),
13597 }];
13598
13599 app.lambda_state.version_table.selected = 0;
13600
13601 app.handle_action(Action::NextPane);
13603 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
13604
13605 app.handle_action(Action::PrevPane);
13607 assert_eq!(app.lambda_state.version_table.expanded_item, None);
13608 }
13609
13610 #[test]
13611 fn test_lambda_version_detail_view() {
13612 use crate::lambda::Function;
13613
13614 let mut app = test_app();
13615 app.current_service = Service::LambdaFunctions;
13616 app.service_selected = true;
13617 app.lambda_state.current_function = Some("test-function".to_string());
13618 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13619 app.mode = Mode::Normal;
13620
13621 app.lambda_state.table.items = vec![Function {
13622 name: "test-function".to_string(),
13623 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
13624 application: None,
13625 description: "Test".to_string(),
13626 package_type: "Zip".to_string(),
13627 runtime: "python3.12".to_string(),
13628 architecture: "X86_64".to_string(),
13629 code_size: 1024,
13630 code_sha256: "hash".to_string(),
13631 memory_mb: 128,
13632 timeout_seconds: 30,
13633 last_modified: "2024-01-01".to_string(),
13634 layers: vec![],
13635 }];
13636
13637 app.lambda_state.version_table.items = vec![LambdaVersion {
13638 version: "1".to_string(),
13639 aliases: "prod".to_string(),
13640 description: "Production".to_string(),
13641 last_modified: "2024-01-01".to_string(),
13642 architecture: "X86_64".to_string(),
13643 }];
13644
13645 app.lambda_state.version_table.selected = 0;
13646
13647 app.handle_action(Action::Select);
13649 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
13650 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
13651
13652 app.handle_action(Action::GoBack);
13654 assert_eq!(app.lambda_state.current_version, None);
13655 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
13656 }
13657
13658 #[test]
13659 fn test_lambda_version_detail_tabs() {
13660 use crate::lambda::Function;
13661
13662 let mut app = test_app();
13663 app.current_service = Service::LambdaFunctions;
13664 app.service_selected = true;
13665 app.lambda_state.current_function = Some("test-function".to_string());
13666 app.lambda_state.current_version = Some("1".to_string());
13667 app.lambda_state.detail_tab = LambdaDetailTab::Code;
13668 app.mode = Mode::Normal;
13669
13670 app.lambda_state.table.items = vec![Function {
13671 name: "test-function".to_string(),
13672 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
13673 application: None,
13674 description: "Test".to_string(),
13675 package_type: "Zip".to_string(),
13676 runtime: "python3.12".to_string(),
13677 architecture: "X86_64".to_string(),
13678 code_size: 1024,
13679 code_sha256: "hash".to_string(),
13680 memory_mb: 128,
13681 timeout_seconds: 30,
13682 last_modified: "2024-01-01".to_string(),
13683 layers: vec![],
13684 }];
13685
13686 app.handle_action(Action::NextDetailTab);
13688 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
13689
13690 app.handle_action(Action::NextDetailTab);
13691 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
13692
13693 app.handle_action(Action::NextDetailTab);
13694 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
13695
13696 app.handle_action(Action::PrevDetailTab);
13698 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
13699
13700 app.handle_action(Action::PrevDetailTab);
13701 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
13702 }
13703
13704 #[test]
13705 fn test_lambda_aliases_arrow_key_expansion() {
13706 use crate::lambda::Alias;
13707
13708 let mut app = test_app();
13709 app.current_service = Service::LambdaFunctions;
13710 app.service_selected = true;
13711 app.lambda_state.current_function = Some("test-function".to_string());
13712 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
13713 app.mode = Mode::Normal;
13714
13715 app.lambda_state.alias_table.items = vec![Alias {
13716 name: "prod".to_string(),
13717 versions: "1".to_string(),
13718 description: "Production alias".to_string(),
13719 }];
13720
13721 app.lambda_state.alias_table.selected = 0;
13722
13723 app.handle_action(Action::NextPane);
13725 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
13726
13727 app.handle_action(Action::PrevPane);
13729 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
13730 }
13731
13732 #[test]
13733 fn test_lambda_functions_arrow_key_expansion() {
13734 use crate::lambda::Function;
13735
13736 let mut app = test_app();
13737 app.current_service = Service::LambdaFunctions;
13738 app.service_selected = true;
13739 app.mode = Mode::Normal;
13740
13741 app.lambda_state.table.items = vec![Function {
13742 name: "test-function".to_string(),
13743 arn: "arn".to_string(),
13744 application: None,
13745 description: "Test".to_string(),
13746 package_type: "Zip".to_string(),
13747 runtime: "python3.12".to_string(),
13748 architecture: "X86_64".to_string(),
13749 code_size: 1024,
13750 code_sha256: "hash".to_string(),
13751 memory_mb: 128,
13752 timeout_seconds: 30,
13753 last_modified: "2024-01-01".to_string(),
13754 layers: vec![],
13755 }];
13756
13757 app.lambda_state.table.selected = 0;
13758
13759 app.handle_action(Action::NextPane);
13761 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
13762
13763 app.handle_action(Action::PrevPane);
13765 assert_eq!(app.lambda_state.table.expanded_item, None);
13766 }
13767
13768 #[test]
13769 fn test_lambda_version_detail_with_application() {
13770 use crate::lambda::Function;
13771
13772 let mut app = test_app();
13773 app.current_service = Service::LambdaFunctions;
13774 app.service_selected = true;
13775 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
13776 app.lambda_state.current_version = Some("1".to_string());
13777 app.lambda_state.detail_tab = LambdaDetailTab::Code;
13778 app.mode = Mode::Normal;
13779
13780 app.lambda_state.table.items = vec![Function {
13781 name: "storefront-studio-beta-api".to_string(),
13782 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
13783 .to_string(),
13784 application: Some("storefront-studio-beta".to_string()),
13785 description: "API function".to_string(),
13786 package_type: "Zip".to_string(),
13787 runtime: "python3.12".to_string(),
13788 architecture: "X86_64".to_string(),
13789 code_size: 1024,
13790 code_sha256: "hash".to_string(),
13791 memory_mb: 128,
13792 timeout_seconds: 30,
13793 last_modified: "2024-01-01".to_string(),
13794 layers: vec![],
13795 }];
13796
13797 assert_eq!(
13799 app.lambda_state.table.items[0].application,
13800 Some("storefront-studio-beta".to_string())
13801 );
13802 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
13803 }
13804
13805 #[test]
13806 fn test_lambda_layer_navigation() {
13807 use crate::lambda::{Function, Layer};
13808
13809 let mut app = test_app();
13810 app.current_service = Service::LambdaFunctions;
13811 app.service_selected = true;
13812 app.lambda_state.current_function = Some("test-function".to_string());
13813 app.lambda_state.detail_tab = LambdaDetailTab::Code;
13814 app.mode = Mode::Normal;
13815
13816 app.lambda_state.table.items = vec![Function {
13817 name: "test-function".to_string(),
13818 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
13819 application: None,
13820 description: "Test".to_string(),
13821 package_type: "Zip".to_string(),
13822 runtime: "python3.12".to_string(),
13823 architecture: "X86_64".to_string(),
13824 code_size: 1024,
13825 code_sha256: "hash".to_string(),
13826 memory_mb: 128,
13827 timeout_seconds: 30,
13828 last_modified: "2024-01-01".to_string(),
13829 layers: vec![
13830 Layer {
13831 merge_order: "1".to_string(),
13832 name: "layer1".to_string(),
13833 layer_version: "1".to_string(),
13834 compatible_runtimes: "python3.9".to_string(),
13835 compatible_architectures: "x86_64".to_string(),
13836 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
13837 },
13838 Layer {
13839 merge_order: "2".to_string(),
13840 name: "layer2".to_string(),
13841 layer_version: "2".to_string(),
13842 compatible_runtimes: "python3.9".to_string(),
13843 compatible_architectures: "x86_64".to_string(),
13844 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
13845 },
13846 Layer {
13847 merge_order: "3".to_string(),
13848 name: "layer3".to_string(),
13849 layer_version: "3".to_string(),
13850 compatible_runtimes: "python3.9".to_string(),
13851 compatible_architectures: "x86_64".to_string(),
13852 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
13853 },
13854 ],
13855 }];
13856
13857 assert_eq!(app.lambda_state.layer_selected, 0);
13858
13859 app.handle_action(Action::NextItem);
13860 assert_eq!(app.lambda_state.layer_selected, 1);
13861
13862 app.handle_action(Action::NextItem);
13863 assert_eq!(app.lambda_state.layer_selected, 2);
13864
13865 app.handle_action(Action::NextItem);
13866 assert_eq!(app.lambda_state.layer_selected, 2);
13867
13868 app.handle_action(Action::PrevItem);
13869 assert_eq!(app.lambda_state.layer_selected, 1);
13870
13871 app.handle_action(Action::PrevItem);
13872 assert_eq!(app.lambda_state.layer_selected, 0);
13873
13874 app.handle_action(Action::PrevItem);
13875 assert_eq!(app.lambda_state.layer_selected, 0);
13876 }
13877
13878 #[test]
13879 fn test_lambda_layer_expansion() {
13880 use crate::lambda::{Function, Layer};
13881
13882 let mut app = test_app();
13883 app.current_service = Service::LambdaFunctions;
13884 app.service_selected = true;
13885 app.lambda_state.current_function = Some("test-function".to_string());
13886 app.lambda_state.detail_tab = LambdaDetailTab::Code;
13887 app.mode = Mode::Normal;
13888
13889 app.lambda_state.table.items = vec![Function {
13890 name: "test-function".to_string(),
13891 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
13892 application: None,
13893 description: "Test".to_string(),
13894 package_type: "Zip".to_string(),
13895 runtime: "python3.12".to_string(),
13896 architecture: "X86_64".to_string(),
13897 code_size: 1024,
13898 code_sha256: "hash".to_string(),
13899 memory_mb: 128,
13900 timeout_seconds: 30,
13901 last_modified: "2024-01-01".to_string(),
13902 layers: vec![Layer {
13903 merge_order: "1".to_string(),
13904 name: "test-layer".to_string(),
13905 layer_version: "1".to_string(),
13906 compatible_runtimes: "python3.9".to_string(),
13907 compatible_architectures: "x86_64".to_string(),
13908 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
13909 }],
13910 }];
13911
13912 assert_eq!(app.lambda_state.layer_expanded, None);
13913
13914 app.handle_action(Action::NextPane);
13915 assert_eq!(app.lambda_state.layer_expanded, Some(0));
13916
13917 app.handle_action(Action::PrevPane);
13918 assert_eq!(app.lambda_state.layer_expanded, None);
13919
13920 app.handle_action(Action::NextPane);
13921 assert_eq!(app.lambda_state.layer_expanded, Some(0));
13922
13923 app.handle_action(Action::NextPane);
13924 assert_eq!(app.lambda_state.layer_expanded, None);
13925 }
13926
13927 #[test]
13928 fn test_lambda_layer_selection_and_expansion_workflow() {
13929 use crate::lambda::{Function, Layer};
13930
13931 let mut app = test_app();
13932 app.current_service = Service::LambdaFunctions;
13933 app.service_selected = true;
13934 app.lambda_state.current_function = Some("test-function".to_string());
13935 app.lambda_state.detail_tab = LambdaDetailTab::Code;
13936 app.mode = Mode::Normal;
13937
13938 app.lambda_state.table.items = vec![Function {
13939 name: "test-function".to_string(),
13940 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
13941 application: None,
13942 description: "Test".to_string(),
13943 package_type: "Zip".to_string(),
13944 runtime: "python3.12".to_string(),
13945 architecture: "X86_64".to_string(),
13946 code_size: 1024,
13947 code_sha256: "hash".to_string(),
13948 memory_mb: 128,
13949 timeout_seconds: 30,
13950 last_modified: "2024-01-01".to_string(),
13951 layers: vec![
13952 Layer {
13953 merge_order: "1".to_string(),
13954 name: "layer1".to_string(),
13955 layer_version: "1".to_string(),
13956 compatible_runtimes: "python3.9".to_string(),
13957 compatible_architectures: "x86_64".to_string(),
13958 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
13959 },
13960 Layer {
13961 merge_order: "2".to_string(),
13962 name: "layer2".to_string(),
13963 layer_version: "2".to_string(),
13964 compatible_runtimes: "python3.9".to_string(),
13965 compatible_architectures: "x86_64".to_string(),
13966 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
13967 },
13968 ],
13969 }];
13970
13971 assert_eq!(app.lambda_state.layer_selected, 0);
13973 assert_eq!(app.lambda_state.layer_expanded, None);
13974
13975 app.handle_action(Action::NextPane);
13977 assert_eq!(app.lambda_state.layer_selected, 0);
13978 assert_eq!(app.lambda_state.layer_expanded, Some(0));
13979
13980 app.handle_action(Action::NextItem);
13982 assert_eq!(app.lambda_state.layer_selected, 1);
13983 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
13987 assert_eq!(app.lambda_state.layer_selected, 1);
13988 assert_eq!(app.lambda_state.layer_expanded, Some(1));
13989
13990 app.handle_action(Action::PrevPane);
13992 assert_eq!(app.lambda_state.layer_selected, 1);
13993 assert_eq!(app.lambda_state.layer_expanded, None);
13994
13995 app.handle_action(Action::PrevItem);
13997 assert_eq!(app.lambda_state.layer_selected, 0);
13998 assert_eq!(app.lambda_state.layer_expanded, None);
13999 }
14000
14001 #[test]
14002 fn test_backtab_cycles_detail_tabs_backward() {
14003 let mut app = test_app();
14004 app.mode = Mode::Normal;
14005
14006 app.current_service = Service::LambdaFunctions;
14008 app.service_selected = true;
14009 app.lambda_state.current_function = Some("test-function".to_string());
14010 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14011
14012 app.handle_action(Action::PrevDetailTab);
14013 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14014
14015 app.handle_action(Action::PrevDetailTab);
14016 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14017
14018 app.current_service = Service::IamRoles;
14020 app.iam_state.current_role = Some("test-role".to_string());
14021 app.iam_state.role_tab = RoleTab::Permissions;
14022
14023 app.handle_action(Action::PrevDetailTab);
14024 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
14025
14026 app.current_service = Service::IamUsers;
14028 app.iam_state.current_user = Some("test-user".to_string());
14029 app.iam_state.user_tab = UserTab::Permissions;
14030
14031 app.handle_action(Action::PrevDetailTab);
14032 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
14033
14034 app.current_service = Service::IamUserGroups;
14036 app.iam_state.current_group = Some("test-group".to_string());
14037 app.iam_state.group_tab = GroupTab::Permissions;
14038
14039 app.handle_action(Action::PrevDetailTab);
14040 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
14041
14042 app.current_service = Service::S3Buckets;
14044 app.s3_state.current_bucket = Some("test-bucket".to_string());
14045 app.s3_state.object_tab = S3ObjectTab::Properties;
14046
14047 app.handle_action(Action::PrevDetailTab);
14048 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
14049
14050 app.current_service = Service::EcrRepositories;
14052 app.ecr_state.current_repository = None;
14053 app.ecr_state.tab = EcrTab::Private;
14054
14055 app.handle_action(Action::PrevDetailTab);
14056 assert_eq!(app.ecr_state.tab, EcrTab::Public);
14057
14058 app.current_service = Service::CloudFormationStacks;
14060 app.cfn_state.current_stack = Some("test-stack".to_string());
14061 app.cfn_state.detail_tab = CfnDetailTab::Resources;
14062 }
14063
14064 #[test]
14065 fn test_cloudformation_status_filter_active() {
14066 let filter = CfnStatusFilter::Active;
14067 assert!(filter.matches("CREATE_IN_PROGRESS"));
14068 assert!(filter.matches("UPDATE_IN_PROGRESS"));
14069 assert!(!filter.matches("CREATE_COMPLETE"));
14070 assert!(!filter.matches("DELETE_COMPLETE"));
14071 assert!(!filter.matches("CREATE_FAILED"));
14072 }
14073
14074 #[test]
14075 fn test_cloudformation_status_filter_complete() {
14076 let filter = CfnStatusFilter::Complete;
14077 assert!(filter.matches("CREATE_COMPLETE"));
14078 assert!(filter.matches("UPDATE_COMPLETE"));
14079 assert!(!filter.matches("DELETE_COMPLETE"));
14080 assert!(!filter.matches("CREATE_IN_PROGRESS"));
14081 }
14082
14083 #[test]
14084 fn test_cloudformation_status_filter_failed() {
14085 let filter = CfnStatusFilter::Failed;
14086 assert!(filter.matches("CREATE_FAILED"));
14087 assert!(filter.matches("UPDATE_FAILED"));
14088 assert!(!filter.matches("CREATE_COMPLETE"));
14089 }
14090
14091 #[test]
14092 fn test_cloudformation_status_filter_deleted() {
14093 let filter = CfnStatusFilter::Deleted;
14094 assert!(filter.matches("DELETE_COMPLETE"));
14095 assert!(filter.matches("DELETE_IN_PROGRESS"));
14096 assert!(!filter.matches("CREATE_COMPLETE"));
14097 }
14098
14099 #[test]
14100 fn test_cloudformation_status_filter_in_progress() {
14101 let filter = CfnStatusFilter::InProgress;
14102 assert!(filter.matches("CREATE_IN_PROGRESS"));
14103 assert!(filter.matches("UPDATE_IN_PROGRESS"));
14104 assert!(filter.matches("DELETE_IN_PROGRESS"));
14105 assert!(!filter.matches("CREATE_COMPLETE"));
14106 }
14107
14108 #[test]
14109 fn test_cloudformation_status_filter_cycle() {
14110 let filter = CfnStatusFilter::All;
14111 assert_eq!(filter.next(), CfnStatusFilter::Active);
14112 assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
14113 assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
14114 assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
14115 assert_eq!(
14116 filter.next().next().next().next().next(),
14117 CfnStatusFilter::InProgress
14118 );
14119 assert_eq!(
14120 filter.next().next().next().next().next().next(),
14121 CfnStatusFilter::All
14122 );
14123 }
14124
14125 #[test]
14126 fn test_cloudformation_default_columns() {
14127 let app = test_app();
14128 assert_eq!(app.cfn_visible_column_ids.len(), 4);
14129 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
14130 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
14131 assert!(app
14132 .cfn_visible_column_ids
14133 .contains(&CfnColumn::CreatedTime.id()));
14134 assert!(app
14135 .cfn_visible_column_ids
14136 .contains(&CfnColumn::Description.id()));
14137 }
14138
14139 #[test]
14140 fn test_cloudformation_all_columns() {
14141 let app = test_app();
14142 assert_eq!(app.cfn_column_ids.len(), 10);
14143 }
14144
14145 #[test]
14146 fn test_cloudformation_filter_by_name() {
14147 let mut app = test_app();
14148 app.cfn_state.status_filter = CfnStatusFilter::Complete;
14149 app.cfn_state.table.items = vec![
14150 CfnStack {
14151 name: "my-stack".to_string(),
14152 stack_id: "id1".to_string(),
14153 status: "CREATE_COMPLETE".to_string(),
14154 created_time: "2024-01-01".to_string(),
14155 updated_time: String::new(),
14156 deleted_time: String::new(),
14157 drift_status: String::new(),
14158 last_drift_check_time: String::new(),
14159 status_reason: String::new(),
14160 description: String::new(),
14161 detailed_status: String::new(),
14162 root_stack: String::new(),
14163 parent_stack: String::new(),
14164 termination_protection: false,
14165 iam_role: String::new(),
14166 tags: Vec::new(),
14167 stack_policy: String::new(),
14168 rollback_monitoring_time: String::new(),
14169 rollback_alarms: Vec::new(),
14170 notification_arns: Vec::new(),
14171 },
14172 CfnStack {
14173 name: "other-stack".to_string(),
14174 stack_id: "id2".to_string(),
14175 status: "CREATE_COMPLETE".to_string(),
14176 created_time: "2024-01-02".to_string(),
14177 updated_time: String::new(),
14178 deleted_time: String::new(),
14179 drift_status: String::new(),
14180 last_drift_check_time: String::new(),
14181 status_reason: String::new(),
14182 description: String::new(),
14183 detailed_status: String::new(),
14184 root_stack: String::new(),
14185 parent_stack: String::new(),
14186 termination_protection: false,
14187 iam_role: String::new(),
14188 tags: Vec::new(),
14189 stack_policy: String::new(),
14190 rollback_monitoring_time: String::new(),
14191 rollback_alarms: Vec::new(),
14192 notification_arns: Vec::new(),
14193 },
14194 ];
14195
14196 app.cfn_state.table.filter = "my".to_string();
14197 let filtered = filtered_cloudformation_stacks(&app);
14198 assert_eq!(filtered.len(), 1);
14199 assert_eq!(filtered[0].name, "my-stack");
14200 }
14201
14202 #[test]
14203 fn test_cloudformation_filter_by_description() {
14204 let mut app = test_app();
14205 app.cfn_state.status_filter = CfnStatusFilter::Complete;
14206 app.cfn_state.table.items = vec![CfnStack {
14207 name: "stack1".to_string(),
14208 stack_id: "id1".to_string(),
14209 status: "CREATE_COMPLETE".to_string(),
14210 created_time: "2024-01-01".to_string(),
14211 updated_time: String::new(),
14212 deleted_time: String::new(),
14213 drift_status: String::new(),
14214 last_drift_check_time: String::new(),
14215 status_reason: String::new(),
14216 description: "production stack".to_string(),
14217 detailed_status: String::new(),
14218 root_stack: String::new(),
14219 parent_stack: String::new(),
14220 termination_protection: false,
14221 iam_role: String::new(),
14222 tags: Vec::new(),
14223 stack_policy: String::new(),
14224 rollback_monitoring_time: String::new(),
14225 rollback_alarms: Vec::new(),
14226 notification_arns: Vec::new(),
14227 }];
14228
14229 app.cfn_state.table.filter = "production".to_string();
14230 let filtered = filtered_cloudformation_stacks(&app);
14231 assert_eq!(filtered.len(), 1);
14232 }
14233
14234 #[test]
14235 fn test_cloudformation_status_filter_applied() {
14236 let mut app = test_app();
14237 app.cfn_state.table.items = vec![
14238 CfnStack {
14239 name: "complete-stack".to_string(),
14240 stack_id: "id1".to_string(),
14241 status: "CREATE_COMPLETE".to_string(),
14242 created_time: "2024-01-01".to_string(),
14243 updated_time: String::new(),
14244 deleted_time: String::new(),
14245 drift_status: String::new(),
14246 last_drift_check_time: String::new(),
14247 status_reason: String::new(),
14248 description: String::new(),
14249 detailed_status: String::new(),
14250 root_stack: String::new(),
14251 parent_stack: String::new(),
14252 termination_protection: false,
14253 iam_role: String::new(),
14254 tags: Vec::new(),
14255 stack_policy: String::new(),
14256 rollback_monitoring_time: String::new(),
14257 rollback_alarms: Vec::new(),
14258 notification_arns: Vec::new(),
14259 },
14260 CfnStack {
14261 name: "failed-stack".to_string(),
14262 stack_id: "id2".to_string(),
14263 status: "CREATE_FAILED".to_string(),
14264 created_time: "2024-01-02".to_string(),
14265 updated_time: String::new(),
14266 deleted_time: String::new(),
14267 drift_status: String::new(),
14268 last_drift_check_time: String::new(),
14269 status_reason: String::new(),
14270 description: String::new(),
14271 detailed_status: String::new(),
14272 root_stack: String::new(),
14273 parent_stack: String::new(),
14274 termination_protection: false,
14275 iam_role: String::new(),
14276 tags: Vec::new(),
14277 stack_policy: String::new(),
14278 rollback_monitoring_time: String::new(),
14279 rollback_alarms: Vec::new(),
14280 notification_arns: Vec::new(),
14281 },
14282 ];
14283
14284 app.cfn_state.status_filter = CfnStatusFilter::Complete;
14285 let filtered = filtered_cloudformation_stacks(&app);
14286 assert_eq!(filtered.len(), 1);
14287 assert_eq!(filtered[0].name, "complete-stack");
14288
14289 app.cfn_state.status_filter = CfnStatusFilter::Failed;
14290 let filtered = filtered_cloudformation_stacks(&app);
14291 assert_eq!(filtered.len(), 1);
14292 assert_eq!(filtered[0].name, "failed-stack");
14293 }
14294
14295 #[test]
14296 fn test_cloudformation_default_page_size() {
14297 let app = test_app();
14298 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
14299 }
14300
14301 #[test]
14302 fn test_cloudformation_default_status_filter() {
14303 let app = test_app();
14304 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
14305 }
14306
14307 #[test]
14308 fn test_cloudformation_view_nested_default_false() {
14309 let app = test_app();
14310 assert!(!app.cfn_state.view_nested);
14311 }
14312
14313 #[test]
14314 fn test_cloudformation_pagination_hotkeys() {
14315 let mut app = test_app();
14316 app.current_service = Service::CloudFormationStacks;
14317 app.service_selected = true;
14318 app.cfn_state.status_filter = CfnStatusFilter::All;
14319
14320 for i in 0..150 {
14322 app.cfn_state.table.items.push(CfnStack {
14323 name: format!("stack-{}", i),
14324 stack_id: format!("id-{}", i),
14325 status: "CREATE_COMPLETE".to_string(),
14326 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14327 updated_time: String::new(),
14328 deleted_time: String::new(),
14329 drift_status: String::new(),
14330 last_drift_check_time: String::new(),
14331 status_reason: String::new(),
14332 description: String::new(),
14333 detailed_status: String::new(),
14334 root_stack: String::new(),
14335 parent_stack: String::new(),
14336 termination_protection: false,
14337 iam_role: String::new(),
14338 tags: vec![],
14339 stack_policy: String::new(),
14340 rollback_monitoring_time: String::new(),
14341 rollback_alarms: vec![],
14342 notification_arns: vec![],
14343 });
14344 }
14345
14346 app.go_to_page(2);
14348 assert_eq!(app.cfn_state.table.selected, 50);
14349
14350 app.go_to_page(3);
14352 assert_eq!(app.cfn_state.table.selected, 100);
14353
14354 app.go_to_page(1);
14356 assert_eq!(app.cfn_state.table.selected, 0);
14357 }
14358
14359 #[test]
14360 fn test_cloudformation_tab_cycling_in_filter_mode() {
14361 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
14362 let mut app = test_app();
14363 app.current_service = Service::CloudFormationStacks;
14364 app.service_selected = true;
14365 app.mode = Mode::FilterInput;
14366 app.cfn_state.input_focus = InputFocus::Filter;
14367
14368 app.handle_action(Action::NextFilterFocus);
14370 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
14371
14372 app.handle_action(Action::NextFilterFocus);
14374 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
14375
14376 app.handle_action(Action::NextFilterFocus);
14378 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
14379
14380 app.handle_action(Action::NextFilterFocus);
14382 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
14383 }
14384
14385 #[test]
14386 fn test_cloudformation_timestamp_format_includes_utc() {
14387 let stack = CfnStack {
14388 name: "test-stack".to_string(),
14389 stack_id: "id-123".to_string(),
14390 status: "CREATE_COMPLETE".to_string(),
14391 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
14392 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
14393 deleted_time: String::new(),
14394 drift_status: String::new(),
14395 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
14396 status_reason: String::new(),
14397 description: String::new(),
14398 detailed_status: String::new(),
14399 root_stack: String::new(),
14400 parent_stack: String::new(),
14401 termination_protection: false,
14402 iam_role: String::new(),
14403 tags: vec![],
14404 stack_policy: String::new(),
14405 rollback_monitoring_time: String::new(),
14406 rollback_alarms: vec![],
14407 notification_arns: vec![],
14408 };
14409
14410 assert!(stack.created_time.contains("(UTC)"));
14411 assert!(stack.updated_time.contains("(UTC)"));
14412 assert!(stack.last_drift_check_time.contains("(UTC)"));
14413 assert_eq!(stack.created_time.len(), 25);
14414 }
14415
14416 #[test]
14417 fn test_cloudformation_enter_drills_into_stack_view() {
14418 let mut app = test_app();
14419 app.current_service = Service::CloudFormationStacks;
14420 app.service_selected = true;
14421 app.mode = Mode::Normal;
14422 app.cfn_state.status_filter = CfnStatusFilter::All;
14423 app.tabs = vec![Tab {
14424 service: Service::CloudFormationStacks,
14425 title: "CloudFormation > Stacks".to_string(),
14426 breadcrumb: "CloudFormation > Stacks".to_string(),
14427 }];
14428 app.current_tab = 0;
14429
14430 app.cfn_state.table.items.push(CfnStack {
14431 name: "test-stack".to_string(),
14432 stack_id: "id-123".to_string(),
14433 status: "CREATE_COMPLETE".to_string(),
14434 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14435 updated_time: String::new(),
14436 deleted_time: String::new(),
14437 drift_status: String::new(),
14438 last_drift_check_time: String::new(),
14439 status_reason: String::new(),
14440 description: String::new(),
14441 detailed_status: String::new(),
14442 root_stack: String::new(),
14443 parent_stack: String::new(),
14444 termination_protection: false,
14445 iam_role: String::new(),
14446 tags: vec![],
14447 stack_policy: String::new(),
14448 rollback_monitoring_time: String::new(),
14449 rollback_alarms: vec![],
14450 notification_arns: vec![],
14451 });
14452
14453 app.cfn_state.table.reset();
14454 assert_eq!(app.cfn_state.current_stack, None);
14455
14456 app.handle_action(Action::Select);
14458 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
14459 }
14460
14461 #[test]
14462 fn test_cloudformation_arrow_keys_expand_collapse() {
14463 let mut app = test_app();
14464 app.current_service = Service::CloudFormationStacks;
14465 app.service_selected = true;
14466 app.mode = Mode::Normal;
14467 app.cfn_state.status_filter = CfnStatusFilter::All;
14468
14469 app.cfn_state.table.items.push(CfnStack {
14470 name: "test-stack".to_string(),
14471 stack_id: "id-123".to_string(),
14472 status: "CREATE_COMPLETE".to_string(),
14473 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14474 updated_time: String::new(),
14475 deleted_time: String::new(),
14476 drift_status: String::new(),
14477 last_drift_check_time: String::new(),
14478 status_reason: String::new(),
14479 description: String::new(),
14480 detailed_status: String::new(),
14481 root_stack: String::new(),
14482 parent_stack: String::new(),
14483 termination_protection: false,
14484 iam_role: String::new(),
14485 tags: vec![],
14486 stack_policy: String::new(),
14487 rollback_monitoring_time: String::new(),
14488 rollback_alarms: vec![],
14489 notification_arns: vec![],
14490 });
14491
14492 app.cfn_state.table.reset();
14493 assert_eq!(app.cfn_state.table.expanded_item, None);
14494
14495 app.handle_action(Action::NextPane);
14497 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
14498
14499 app.handle_action(Action::PrevPane);
14501 assert_eq!(app.cfn_state.table.expanded_item, None);
14502
14503 assert_eq!(app.cfn_state.current_stack, None);
14505 }
14506
14507 #[test]
14508 fn test_cloudformation_tab_cycling() {
14509 use crate::ui::cfn::DetailTab;
14510 let mut app = test_app();
14511 app.current_service = Service::CloudFormationStacks;
14512 app.service_selected = true;
14513 app.mode = Mode::Normal;
14514 app.cfn_state.status_filter = CfnStatusFilter::All;
14515 app.cfn_state.current_stack = Some("test-stack".to_string());
14516
14517 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
14518 }
14519
14520 #[test]
14521 fn test_cloudformation_console_url() {
14522 use crate::ui::cfn::DetailTab;
14523 let mut app = test_app();
14524 app.current_service = Service::CloudFormationStacks;
14525 app.service_selected = true;
14526 app.cfn_state.status_filter = CfnStatusFilter::All;
14527
14528 app.cfn_state.table.items.push(CfnStack {
14529 name: "test-stack".to_string(),
14530 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
14531 .to_string(),
14532 status: "CREATE_COMPLETE".to_string(),
14533 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14534 updated_time: String::new(),
14535 deleted_time: String::new(),
14536 drift_status: String::new(),
14537 last_drift_check_time: String::new(),
14538 status_reason: String::new(),
14539 description: String::new(),
14540 detailed_status: String::new(),
14541 root_stack: String::new(),
14542 parent_stack: String::new(),
14543 termination_protection: false,
14544 iam_role: String::new(),
14545 tags: vec![],
14546 stack_policy: String::new(),
14547 rollback_monitoring_time: String::new(),
14548 rollback_alarms: vec![],
14549 notification_arns: vec![],
14550 });
14551
14552 app.cfn_state.current_stack = Some("test-stack".to_string());
14553
14554 app.cfn_state.detail_tab = DetailTab::StackInfo;
14556 let url = app.get_console_url();
14557 assert!(url.contains("stackinfo"));
14558 assert!(url.contains("arn%3Aaws%3Acloudformation"));
14559
14560 app.cfn_state.detail_tab = DetailTab::Events;
14562 let url = app.get_console_url();
14563 assert!(url.contains("events"));
14564 assert!(url.contains("arn%3Aaws%3Acloudformation"));
14565 }
14566
14567 #[test]
14568 fn test_iam_role_select() {
14569 let mut app = test_app();
14570 app.current_service = Service::IamRoles;
14571 app.service_selected = true;
14572 app.mode = Mode::Normal;
14573
14574 app.iam_state.roles.items = vec![
14575 IamRole {
14576 role_name: "role1".to_string(),
14577 path: "/".to_string(),
14578 trusted_entities: "AWS Service: ec2".to_string(),
14579 last_activity: "-".to_string(),
14580 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
14581 creation_time: "2025-01-01".to_string(),
14582 description: "Test role 1".to_string(),
14583 max_session_duration: Some(3600),
14584 },
14585 IamRole {
14586 role_name: "role2".to_string(),
14587 path: "/".to_string(),
14588 trusted_entities: "AWS Service: lambda".to_string(),
14589 last_activity: "-".to_string(),
14590 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
14591 creation_time: "2025-01-02".to_string(),
14592 description: "Test role 2".to_string(),
14593 max_session_duration: Some(7200),
14594 },
14595 ];
14596
14597 app.iam_state.roles.selected = 0;
14599 app.handle_action(Action::Select);
14600
14601 assert_eq!(
14602 app.iam_state.current_role,
14603 Some("role1".to_string()),
14604 "Should open role detail view"
14605 );
14606 assert_eq!(
14607 app.iam_state.role_tab,
14608 RoleTab::Permissions,
14609 "Should default to Permissions tab"
14610 );
14611 }
14612
14613 #[test]
14614 fn test_iam_role_back_navigation() {
14615 let mut app = test_app();
14616 app.current_service = Service::IamRoles;
14617 app.service_selected = true;
14618 app.iam_state.current_role = Some("test-role".to_string());
14619
14620 app.handle_action(Action::GoBack);
14621
14622 assert_eq!(
14623 app.iam_state.current_role, None,
14624 "Should return to roles list"
14625 );
14626 }
14627
14628 #[test]
14629 fn test_iam_role_tab_navigation() {
14630 let mut app = test_app();
14631 app.current_service = Service::IamRoles;
14632 app.service_selected = true;
14633 app.iam_state.current_role = Some("test-role".to_string());
14634 app.iam_state.role_tab = RoleTab::Permissions;
14635
14636 app.handle_action(Action::NextDetailTab);
14637
14638 assert_eq!(
14639 app.iam_state.role_tab,
14640 RoleTab::TrustRelationships,
14641 "Should move to next tab"
14642 );
14643 }
14644
14645 #[test]
14646 fn test_iam_role_tab_cycle_order() {
14647 let mut app = test_app();
14648 app.current_service = Service::IamRoles;
14649 app.service_selected = true;
14650 app.iam_state.current_role = Some("test-role".to_string());
14651 app.iam_state.role_tab = RoleTab::Permissions;
14652
14653 app.handle_action(Action::NextDetailTab);
14654 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
14655
14656 app.handle_action(Action::NextDetailTab);
14657 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
14658
14659 app.handle_action(Action::NextDetailTab);
14660 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
14661
14662 app.handle_action(Action::NextDetailTab);
14663 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
14664
14665 app.handle_action(Action::NextDetailTab);
14666 assert_eq!(
14667 app.iam_state.role_tab,
14668 RoleTab::Permissions,
14669 "Should cycle back to first tab"
14670 );
14671 }
14672
14673 #[test]
14674 fn test_iam_role_pagination() {
14675 let mut app = test_app();
14676 app.current_service = Service::IamRoles;
14677 app.service_selected = true;
14678 app.iam_state.roles.page_size = PageSize::Ten;
14679
14680 app.iam_state.roles.items = (0..25)
14681 .map(|i| IamRole {
14682 role_name: format!("role{}", i),
14683 path: "/".to_string(),
14684 trusted_entities: "AWS Service: ec2".to_string(),
14685 last_activity: "-".to_string(),
14686 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
14687 creation_time: "2025-01-01".to_string(),
14688 description: format!("Test role {}", i),
14689 max_session_duration: Some(3600),
14690 })
14691 .collect();
14692
14693 app.go_to_page(2);
14695
14696 assert_eq!(
14697 app.iam_state.roles.selected, 10,
14698 "Should select first item of page 2"
14699 );
14700 assert_eq!(
14701 app.iam_state.roles.scroll_offset, 10,
14702 "Should update scroll offset"
14703 );
14704 }
14705
14706 #[test]
14707 fn test_tags_table_populated_on_role_detail() {
14708 let mut app = test_app();
14709 app.current_service = Service::IamRoles;
14710 app.service_selected = true;
14711 app.mode = Mode::Normal;
14712 app.iam_state.roles.items = vec![IamRole {
14713 role_name: "TestRole".to_string(),
14714 path: "/".to_string(),
14715 trusted_entities: String::new(),
14716 last_activity: String::new(),
14717 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
14718 creation_time: "2025-01-01".to_string(),
14719 description: String::new(),
14720 max_session_duration: Some(3600),
14721 }];
14722
14723 app.iam_state.tags.items = vec![
14725 IamRoleTag {
14726 key: "Environment".to_string(),
14727 value: "Production".to_string(),
14728 },
14729 IamRoleTag {
14730 key: "Team".to_string(),
14731 value: "Platform".to_string(),
14732 },
14733 ];
14734
14735 assert_eq!(app.iam_state.tags.items.len(), 2);
14736 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
14737 assert_eq!(app.iam_state.tags.items[0].value, "Production");
14738 assert_eq!(app.iam_state.tags.selected, 0);
14739 }
14740
14741 #[test]
14742 fn test_tags_table_navigation() {
14743 let mut app = test_app();
14744 app.current_service = Service::IamRoles;
14745 app.service_selected = true;
14746 app.mode = Mode::Normal;
14747 app.iam_state.current_role = Some("TestRole".to_string());
14748 app.iam_state.role_tab = RoleTab::Tags;
14749 app.iam_state.tags.items = vec![
14750 IamRoleTag {
14751 key: "Tag1".to_string(),
14752 value: "Value1".to_string(),
14753 },
14754 IamRoleTag {
14755 key: "Tag2".to_string(),
14756 value: "Value2".to_string(),
14757 },
14758 ];
14759
14760 app.handle_action(Action::NextItem);
14761 assert_eq!(app.iam_state.tags.selected, 1);
14762
14763 app.handle_action(Action::PrevItem);
14764 assert_eq!(app.iam_state.tags.selected, 0);
14765 }
14766
14767 #[test]
14768 fn test_last_accessed_table_navigation() {
14769 let mut app = test_app();
14770 app.current_service = Service::IamRoles;
14771 app.service_selected = true;
14772 app.mode = Mode::Normal;
14773 app.iam_state.current_role = Some("TestRole".to_string());
14774 app.iam_state.role_tab = RoleTab::LastAccessed;
14775 app.iam_state.last_accessed_services.items = vec![
14776 LastAccessedService {
14777 service: "S3".to_string(),
14778 policies_granting: "Policy1".to_string(),
14779 last_accessed: "2025-01-01".to_string(),
14780 },
14781 LastAccessedService {
14782 service: "EC2".to_string(),
14783 policies_granting: "Policy2".to_string(),
14784 last_accessed: "2025-01-02".to_string(),
14785 },
14786 ];
14787
14788 app.handle_action(Action::NextItem);
14789 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
14790
14791 app.handle_action(Action::PrevItem);
14792 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
14793 }
14794
14795 #[test]
14796 fn test_cfn_input_focus_next() {
14797 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
14798 let mut app = test_app();
14799 app.current_service = Service::CloudFormationStacks;
14800 app.mode = Mode::FilterInput;
14801 app.cfn_state.input_focus = InputFocus::Filter;
14802
14803 app.handle_action(Action::NextFilterFocus);
14804 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
14805
14806 app.handle_action(Action::NextFilterFocus);
14807 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
14808
14809 app.handle_action(Action::NextFilterFocus);
14810 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
14811
14812 app.handle_action(Action::NextFilterFocus);
14813 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
14814 }
14815
14816 #[test]
14817 fn test_cfn_input_focus_prev() {
14818 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
14819 let mut app = test_app();
14820 app.current_service = Service::CloudFormationStacks;
14821 app.mode = Mode::FilterInput;
14822 app.cfn_state.input_focus = InputFocus::Filter;
14823
14824 app.handle_action(Action::PrevFilterFocus);
14825 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
14826
14827 app.handle_action(Action::PrevFilterFocus);
14828 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
14829
14830 app.handle_action(Action::PrevFilterFocus);
14831 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
14832
14833 app.handle_action(Action::PrevFilterFocus);
14834 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
14835 }
14836
14837 #[test]
14838 fn test_cw_logs_input_focus_prev() {
14839 let mut app = test_app();
14840 app.current_service = Service::CloudWatchLogGroups;
14841 app.mode = Mode::FilterInput;
14842 app.view_mode = ViewMode::Detail;
14843 app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
14844 app.log_groups_state.input_focus = InputFocus::Filter;
14845
14846 app.handle_action(Action::PrevFilterFocus);
14847 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
14848
14849 app.handle_action(Action::PrevFilterFocus);
14850 assert_eq!(
14851 app.log_groups_state.input_focus,
14852 InputFocus::Checkbox("ShowExpired")
14853 );
14854
14855 app.handle_action(Action::PrevFilterFocus);
14856 assert_eq!(
14857 app.log_groups_state.input_focus,
14858 InputFocus::Checkbox("ExactMatch")
14859 );
14860
14861 app.handle_action(Action::PrevFilterFocus);
14862 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
14863 }
14864
14865 #[test]
14866 fn test_cw_events_input_focus_prev() {
14867 use crate::ui::cw::logs::EventFilterFocus;
14868 let mut app = test_app();
14869 app.mode = Mode::EventFilterInput;
14870 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
14871
14872 app.handle_action(Action::PrevFilterFocus);
14873 assert_eq!(
14874 app.log_groups_state.event_input_focus,
14875 EventFilterFocus::DateRange
14876 );
14877
14878 app.handle_action(Action::PrevFilterFocus);
14879 assert_eq!(
14880 app.log_groups_state.event_input_focus,
14881 EventFilterFocus::Filter
14882 );
14883 }
14884
14885 #[test]
14886 fn test_cfn_input_focus_cycle_complete() {
14887 let mut app = test_app();
14888 app.current_service = Service::CloudFormationStacks;
14889 app.mode = Mode::FilterInput;
14890 app.cfn_state.input_focus = InputFocus::Filter;
14891
14892 for _ in 0..4 {
14894 app.handle_action(Action::NextFilterFocus);
14895 }
14896 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
14897
14898 for _ in 0..4 {
14900 app.handle_action(Action::PrevFilterFocus);
14901 }
14902 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
14903 }
14904
14905 #[test]
14906 fn test_cfn_filter_status_arrow_keys() {
14907 use crate::ui::cfn::STATUS_FILTER;
14908 let mut app = test_app();
14909 app.current_service = Service::CloudFormationStacks;
14910 app.mode = Mode::FilterInput;
14911 app.cfn_state.input_focus = STATUS_FILTER;
14912 app.cfn_state.status_filter = CfnStatusFilter::All;
14913
14914 app.handle_action(Action::NextItem);
14915 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
14916
14917 app.handle_action(Action::PrevItem);
14918 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
14919 }
14920
14921 #[test]
14922 fn test_cfn_filter_shift_tab_cycles_backward() {
14923 use crate::ui::cfn::STATUS_FILTER;
14924 let mut app = test_app();
14925 app.current_service = Service::CloudFormationStacks;
14926 app.mode = Mode::FilterInput;
14927 app.cfn_state.input_focus = STATUS_FILTER;
14928
14929 app.handle_action(Action::PrevFilterFocus);
14931 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
14932
14933 app.handle_action(Action::PrevFilterFocus);
14935 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
14936 }
14937
14938 #[test]
14939 fn test_cfn_pagination_arrow_keys() {
14940 let mut app = test_app();
14941 app.current_service = Service::CloudFormationStacks;
14942 app.mode = Mode::FilterInput;
14943 app.cfn_state.input_focus = InputFocus::Pagination;
14944 app.cfn_state.table.scroll_offset = 0;
14945 app.cfn_state.table.page_size = PageSize::Ten;
14946
14947 app.cfn_state.table.items = (0..30)
14949 .map(|i| CfnStack {
14950 name: format!("stack-{}", i),
14951 stack_id: format!("id-{}", i),
14952 status: "CREATE_COMPLETE".to_string(),
14953 created_time: "2024-01-01".to_string(),
14954 updated_time: String::new(),
14955 deleted_time: String::new(),
14956 drift_status: String::new(),
14957 last_drift_check_time: String::new(),
14958 status_reason: String::new(),
14959 description: String::new(),
14960 detailed_status: String::new(),
14961 root_stack: String::new(),
14962 parent_stack: String::new(),
14963 termination_protection: false,
14964 iam_role: String::new(),
14965 tags: Vec::new(),
14966 stack_policy: String::new(),
14967 rollback_monitoring_time: String::new(),
14968 rollback_alarms: Vec::new(),
14969 notification_arns: Vec::new(),
14970 })
14971 .collect();
14972
14973 app.handle_action(Action::PageDown);
14975 assert_eq!(app.cfn_state.table.scroll_offset, 10);
14976 let page_size = app.cfn_state.table.page_size.value();
14978 let current_page = app.cfn_state.table.scroll_offset / page_size;
14979 assert_eq!(current_page, 1);
14980
14981 app.handle_action(Action::PageUp);
14983 assert_eq!(app.cfn_state.table.scroll_offset, 0);
14984 let current_page = app.cfn_state.table.scroll_offset / page_size;
14985 assert_eq!(current_page, 0);
14986 }
14987
14988 #[test]
14989 fn test_cfn_page_navigation_updates_selection() {
14990 let mut app = test_app();
14991 app.current_service = Service::CloudFormationStacks;
14992 app.mode = Mode::Normal;
14993
14994 app.cfn_state.table.items = (0..30)
14996 .map(|i| CfnStack {
14997 name: format!("stack-{}", i),
14998 stack_id: format!("id-{}", i),
14999 status: "CREATE_COMPLETE".to_string(),
15000 created_time: "2024-01-01".to_string(),
15001 updated_time: String::new(),
15002 deleted_time: String::new(),
15003 drift_status: String::new(),
15004 last_drift_check_time: String::new(),
15005 status_reason: String::new(),
15006 description: String::new(),
15007 detailed_status: String::new(),
15008 root_stack: String::new(),
15009 parent_stack: String::new(),
15010 termination_protection: false,
15011 iam_role: String::new(),
15012 tags: Vec::new(),
15013 stack_policy: String::new(),
15014 rollback_monitoring_time: String::new(),
15015 rollback_alarms: Vec::new(),
15016 notification_arns: Vec::new(),
15017 })
15018 .collect();
15019
15020 app.cfn_state.table.reset();
15021 app.cfn_state.table.scroll_offset = 0;
15022
15023 app.handle_action(Action::PageDown);
15025 assert_eq!(app.cfn_state.table.selected, 10);
15026
15027 app.handle_action(Action::PageDown);
15029 assert_eq!(app.cfn_state.table.selected, 20);
15030
15031 app.handle_action(Action::PageUp);
15033 assert_eq!(app.cfn_state.table.selected, 10);
15034 }
15035
15036 #[test]
15037 fn test_cfn_filter_input_only_when_focused() {
15038 use crate::ui::cfn::STATUS_FILTER;
15039 let mut app = test_app();
15040 app.current_service = Service::CloudFormationStacks;
15041 app.mode = Mode::FilterInput;
15042 app.cfn_state.input_focus = STATUS_FILTER;
15043 app.cfn_state.table.filter = String::new();
15044
15045 app.handle_action(Action::FilterInput('t'));
15047 app.handle_action(Action::FilterInput('e'));
15048 app.handle_action(Action::FilterInput('s'));
15049 app.handle_action(Action::FilterInput('t'));
15050 assert_eq!(app.cfn_state.table.filter, "");
15051
15052 app.cfn_state.input_focus = InputFocus::Filter;
15054 app.handle_action(Action::FilterInput('t'));
15055 app.handle_action(Action::FilterInput('e'));
15056 app.handle_action(Action::FilterInput('s'));
15057 app.handle_action(Action::FilterInput('t'));
15058 assert_eq!(app.cfn_state.table.filter, "test");
15059 }
15060
15061 #[test]
15062 fn test_cfn_input_focus_resets_on_start() {
15063 let mut app = test_app();
15064 app.current_service = Service::CloudFormationStacks;
15065 app.service_selected = true;
15066 app.mode = Mode::Normal;
15067 app.cfn_state.input_focus = InputFocus::Pagination;
15068
15069 app.handle_action(Action::StartFilter);
15071 assert_eq!(app.mode, Mode::FilterInput);
15072 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15073 }
15074
15075 #[test]
15076 fn test_iam_roles_input_focus_cycles_forward() {
15077 let mut app = test_app();
15078 app.current_service = Service::IamRoles;
15079 app.mode = Mode::FilterInput;
15080 app.iam_state.role_input_focus = InputFocus::Filter;
15081
15082 app.handle_action(Action::NextFilterFocus);
15083 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
15084
15085 app.handle_action(Action::NextFilterFocus);
15086 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
15087 }
15088
15089 #[test]
15090 fn test_iam_roles_input_focus_cycles_backward() {
15091 let mut app = test_app();
15092 app.current_service = Service::IamRoles;
15093 app.mode = Mode::FilterInput;
15094 app.iam_state.role_input_focus = InputFocus::Filter;
15095
15096 app.handle_action(Action::PrevFilterFocus);
15097 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
15098
15099 app.handle_action(Action::PrevFilterFocus);
15100 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
15101 }
15102
15103 #[test]
15104 fn test_iam_roles_filter_input_only_when_focused() {
15105 let mut app = test_app();
15106 app.current_service = Service::IamRoles;
15107 app.mode = Mode::FilterInput;
15108 app.iam_state.role_input_focus = InputFocus::Pagination;
15109 app.iam_state.roles.filter = String::new();
15110
15111 app.handle_action(Action::FilterInput('t'));
15113 app.handle_action(Action::FilterInput('e'));
15114 app.handle_action(Action::FilterInput('s'));
15115 app.handle_action(Action::FilterInput('t'));
15116 assert_eq!(app.iam_state.roles.filter, "");
15117
15118 app.iam_state.role_input_focus = InputFocus::Filter;
15120 app.handle_action(Action::FilterInput('t'));
15121 app.handle_action(Action::FilterInput('e'));
15122 app.handle_action(Action::FilterInput('s'));
15123 app.handle_action(Action::FilterInput('t'));
15124 assert_eq!(app.iam_state.roles.filter, "test");
15125 }
15126
15127 #[test]
15128 fn test_iam_roles_page_down_updates_scroll_offset() {
15129 let mut app = test_app();
15130 app.current_service = Service::IamRoles;
15131 app.mode = Mode::Normal;
15132 app.iam_state.roles.items = (0..50)
15133 .map(|i| IamRole {
15134 role_name: format!("role-{}", i),
15135 path: "/".to_string(),
15136 trusted_entities: "AWS Service".to_string(),
15137 last_activity: "N/A".to_string(),
15138 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
15139 creation_time: "2024-01-01".to_string(),
15140 description: String::new(),
15141 max_session_duration: Some(3600),
15142 })
15143 .collect();
15144
15145 app.iam_state.roles.selected = 0;
15146 app.iam_state.roles.scroll_offset = 0;
15147
15148 app.handle_action(Action::PageDown);
15150 assert_eq!(app.iam_state.roles.selected, 10);
15151 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
15153
15154 app.handle_action(Action::PageDown);
15156 assert_eq!(app.iam_state.roles.selected, 20);
15157 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
15158 }
15159
15160 #[test]
15161 fn test_application_selection_and_deployments_tab() {
15162 use crate::lambda::Application as LambdaApplication;
15163 use LambdaApplicationDetailTab;
15164
15165 let mut app = test_app();
15166 app.current_service = Service::LambdaApplications;
15167 app.service_selected = true;
15168 app.mode = Mode::Normal;
15169
15170 app.lambda_application_state.table.items = vec![LambdaApplication {
15171 name: "test-app".to_string(),
15172 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15173 description: "Test application".to_string(),
15174 status: "CREATE_COMPLETE".to_string(),
15175 last_modified: "2024-01-01".to_string(),
15176 }];
15177
15178 app.handle_action(Action::Select);
15180 assert_eq!(
15181 app.lambda_application_state.current_application,
15182 Some("test-app".to_string())
15183 );
15184 assert_eq!(
15185 app.lambda_application_state.detail_tab,
15186 LambdaApplicationDetailTab::Overview
15187 );
15188
15189 app.handle_action(Action::NextDetailTab);
15191 assert_eq!(
15192 app.lambda_application_state.detail_tab,
15193 LambdaApplicationDetailTab::Deployments
15194 );
15195
15196 app.handle_action(Action::GoBack);
15198 assert_eq!(app.lambda_application_state.current_application, None);
15199 }
15200
15201 #[test]
15202 fn test_application_resources_filter_and_pagination() {
15203 use crate::lambda::Application as LambdaApplication;
15204 use LambdaApplicationDetailTab;
15205
15206 let mut app = test_app();
15207 app.current_service = Service::LambdaApplications;
15208 app.service_selected = true;
15209 app.mode = Mode::Normal;
15210
15211 app.lambda_application_state.table.items = vec![LambdaApplication {
15212 name: "test-app".to_string(),
15213 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15214 description: "Test application".to_string(),
15215 status: "CREATE_COMPLETE".to_string(),
15216 last_modified: "2024-01-01".to_string(),
15217 }];
15218
15219 app.handle_action(Action::Select);
15221 assert_eq!(
15222 app.lambda_application_state.detail_tab,
15223 LambdaApplicationDetailTab::Overview
15224 );
15225
15226 assert!(!app.lambda_application_state.resources.items.is_empty());
15228
15229 app.mode = Mode::FilterInput;
15231 assert_eq!(
15232 app.lambda_application_state.resource_input_focus,
15233 InputFocus::Filter
15234 );
15235
15236 app.handle_action(Action::NextFilterFocus);
15237 assert_eq!(
15238 app.lambda_application_state.resource_input_focus,
15239 InputFocus::Pagination
15240 );
15241
15242 app.handle_action(Action::PrevFilterFocus);
15243 assert_eq!(
15244 app.lambda_application_state.resource_input_focus,
15245 InputFocus::Filter
15246 );
15247 }
15248
15249 #[test]
15250 fn test_application_deployments_filter_and_pagination() {
15251 use crate::lambda::Application as LambdaApplication;
15252 use LambdaApplicationDetailTab;
15253
15254 let mut app = test_app();
15255 app.current_service = Service::LambdaApplications;
15256 app.service_selected = true;
15257 app.mode = Mode::Normal;
15258
15259 app.lambda_application_state.table.items = vec![LambdaApplication {
15260 name: "test-app".to_string(),
15261 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15262 description: "Test application".to_string(),
15263 status: "CREATE_COMPLETE".to_string(),
15264 last_modified: "2024-01-01".to_string(),
15265 }];
15266
15267 app.handle_action(Action::Select);
15269 app.handle_action(Action::NextDetailTab);
15270 assert_eq!(
15271 app.lambda_application_state.detail_tab,
15272 LambdaApplicationDetailTab::Deployments
15273 );
15274
15275 assert!(!app.lambda_application_state.deployments.items.is_empty());
15277
15278 app.mode = Mode::FilterInput;
15280 assert_eq!(
15281 app.lambda_application_state.deployment_input_focus,
15282 InputFocus::Filter
15283 );
15284
15285 app.handle_action(Action::NextFilterFocus);
15286 assert_eq!(
15287 app.lambda_application_state.deployment_input_focus,
15288 InputFocus::Pagination
15289 );
15290
15291 app.handle_action(Action::PrevFilterFocus);
15292 assert_eq!(
15293 app.lambda_application_state.deployment_input_focus,
15294 InputFocus::Filter
15295 );
15296 }
15297
15298 #[test]
15299 fn test_application_resource_expansion() {
15300 use crate::lambda::Application as LambdaApplication;
15301 use LambdaApplicationDetailTab;
15302
15303 let mut app = test_app();
15304 app.current_service = Service::LambdaApplications;
15305 app.service_selected = true;
15306 app.mode = Mode::Normal;
15307
15308 app.lambda_application_state.table.items = vec![LambdaApplication {
15309 name: "test-app".to_string(),
15310 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15311 description: "Test application".to_string(),
15312 status: "CREATE_COMPLETE".to_string(),
15313 last_modified: "2024-01-01".to_string(),
15314 }];
15315
15316 app.handle_action(Action::Select);
15318 assert_eq!(
15319 app.lambda_application_state.detail_tab,
15320 LambdaApplicationDetailTab::Overview
15321 );
15322
15323 app.handle_action(Action::NextPane);
15325 assert_eq!(
15326 app.lambda_application_state.resources.expanded_item,
15327 Some(0)
15328 );
15329
15330 app.handle_action(Action::PrevPane);
15332 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
15333 }
15334
15335 #[test]
15336 fn test_application_deployment_expansion() {
15337 use crate::lambda::Application as LambdaApplication;
15338 use LambdaApplicationDetailTab;
15339
15340 let mut app = test_app();
15341 app.current_service = Service::LambdaApplications;
15342 app.service_selected = true;
15343 app.mode = Mode::Normal;
15344
15345 app.lambda_application_state.table.items = vec![LambdaApplication {
15346 name: "test-app".to_string(),
15347 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15348 description: "Test application".to_string(),
15349 status: "CREATE_COMPLETE".to_string(),
15350 last_modified: "2024-01-01".to_string(),
15351 }];
15352
15353 app.handle_action(Action::Select);
15355 app.handle_action(Action::NextDetailTab);
15356 assert_eq!(
15357 app.lambda_application_state.detail_tab,
15358 LambdaApplicationDetailTab::Deployments
15359 );
15360
15361 app.handle_action(Action::NextPane);
15363 assert_eq!(
15364 app.lambda_application_state.deployments.expanded_item,
15365 Some(0)
15366 );
15367
15368 app.handle_action(Action::PrevPane);
15370 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
15371 }
15372
15373 #[test]
15374 fn test_s3_nested_prefix_expansion() {
15375 use crate::s3::Bucket;
15376 use S3Object;
15377
15378 let mut app = test_app();
15379 app.current_service = Service::S3Buckets;
15380 app.service_selected = true;
15381 app.mode = Mode::Normal;
15382
15383 app.s3_state.buckets.items = vec![Bucket {
15385 name: "test-bucket".to_string(),
15386 region: "us-east-1".to_string(),
15387 creation_date: "2024-01-01".to_string(),
15388 }];
15389
15390 app.s3_state.bucket_preview.insert(
15392 "test-bucket".to_string(),
15393 vec![S3Object {
15394 key: "level1/".to_string(),
15395 size: 0,
15396 last_modified: "".to_string(),
15397 is_prefix: true,
15398 storage_class: "".to_string(),
15399 }],
15400 );
15401
15402 app.s3_state.prefix_preview.insert(
15404 "level1/".to_string(),
15405 vec![S3Object {
15406 key: "level1/level2/".to_string(),
15407 size: 0,
15408 last_modified: "".to_string(),
15409 is_prefix: true,
15410 storage_class: "".to_string(),
15411 }],
15412 );
15413
15414 app.s3_state.selected_row = 0;
15416 app.handle_action(Action::NextPane);
15417 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
15418
15419 app.s3_state.selected_row = 1;
15421 app.handle_action(Action::NextPane);
15422 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
15423
15424 app.s3_state.selected_row = 2;
15426 app.handle_action(Action::NextPane);
15427 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
15428
15429 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
15431 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
15432 }
15433
15434 #[test]
15435 fn test_s3_nested_prefix_collapse() {
15436 use crate::s3::Bucket;
15437 use S3Object;
15438
15439 let mut app = test_app();
15440 app.current_service = Service::S3Buckets;
15441 app.service_selected = true;
15442 app.mode = Mode::Normal;
15443
15444 app.s3_state.buckets.items = vec![Bucket {
15445 name: "test-bucket".to_string(),
15446 region: "us-east-1".to_string(),
15447 creation_date: "2024-01-01".to_string(),
15448 }];
15449
15450 app.s3_state.bucket_preview.insert(
15451 "test-bucket".to_string(),
15452 vec![S3Object {
15453 key: "level1/".to_string(),
15454 size: 0,
15455 last_modified: "".to_string(),
15456 is_prefix: true,
15457 storage_class: "".to_string(),
15458 }],
15459 );
15460
15461 app.s3_state.prefix_preview.insert(
15462 "level1/".to_string(),
15463 vec![S3Object {
15464 key: "level1/level2/".to_string(),
15465 size: 0,
15466 last_modified: "".to_string(),
15467 is_prefix: true,
15468 storage_class: "".to_string(),
15469 }],
15470 );
15471
15472 app.s3_state
15474 .expanded_prefixes
15475 .insert("test-bucket".to_string());
15476 app.s3_state.expanded_prefixes.insert("level1/".to_string());
15477 app.s3_state
15478 .expanded_prefixes
15479 .insert("level1/level2/".to_string());
15480
15481 app.s3_state.selected_row = 2;
15483 app.handle_action(Action::PrevPane);
15484 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
15485 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
15489 app.handle_action(Action::PrevPane);
15490 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
15491 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
15495 app.handle_action(Action::PrevPane);
15496 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
15497 }
15498}
15499
15500#[cfg(test)]
15501mod sqs_tests {
15502 use super::*;
15503 use test_helpers::*;
15504
15505 #[test]
15506 fn test_sqs_filter_input() {
15507 let mut app = test_app();
15508 app.current_service = Service::SqsQueues;
15509 app.service_selected = true;
15510 app.mode = Mode::FilterInput;
15511
15512 app.handle_action(Action::FilterInput('t'));
15513 app.handle_action(Action::FilterInput('e'));
15514 app.handle_action(Action::FilterInput('s'));
15515 app.handle_action(Action::FilterInput('t'));
15516 assert_eq!(app.sqs_state.queues.filter, "test");
15517
15518 app.handle_action(Action::FilterBackspace);
15519 assert_eq!(app.sqs_state.queues.filter, "tes");
15520 }
15521
15522 #[test]
15523 fn test_sqs_start_filter() {
15524 let mut app = test_app();
15525 app.current_service = Service::SqsQueues;
15526 app.service_selected = true;
15527 app.mode = Mode::Normal;
15528
15529 app.handle_action(Action::StartFilter);
15530 assert_eq!(app.mode, Mode::FilterInput);
15531 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
15532 }
15533
15534 #[test]
15535 fn test_sqs_filter_focus_cycling() {
15536 let mut app = test_app();
15537 app.current_service = Service::SqsQueues;
15538 app.service_selected = true;
15539 app.mode = Mode::FilterInput;
15540 app.sqs_state.input_focus = InputFocus::Filter;
15541
15542 app.handle_action(Action::NextFilterFocus);
15543 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
15544
15545 app.handle_action(Action::NextFilterFocus);
15546 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
15547
15548 app.handle_action(Action::PrevFilterFocus);
15549 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
15550 }
15551
15552 #[test]
15553 fn test_sqs_navigation() {
15554 let mut app = test_app();
15555 app.current_service = Service::SqsQueues;
15556 app.service_selected = true;
15557 app.mode = Mode::Normal;
15558 app.sqs_state.queues.items = (0..10)
15559 .map(|i| SqsQueue {
15560 name: format!("queue{}", i),
15561 url: String::new(),
15562 queue_type: "Standard".to_string(),
15563 created_timestamp: String::new(),
15564 messages_available: "0".to_string(),
15565 messages_in_flight: "0".to_string(),
15566 encryption: "Disabled".to_string(),
15567 content_based_deduplication: "Disabled".to_string(),
15568 last_modified_timestamp: String::new(),
15569 visibility_timeout: String::new(),
15570 message_retention_period: String::new(),
15571 maximum_message_size: String::new(),
15572 delivery_delay: String::new(),
15573 receive_message_wait_time: String::new(),
15574 high_throughput_fifo: "N/A".to_string(),
15575 deduplication_scope: "N/A".to_string(),
15576 fifo_throughput_limit: "N/A".to_string(),
15577 dead_letter_queue: "-".to_string(),
15578 messages_delayed: "0".to_string(),
15579 redrive_allow_policy: "-".to_string(),
15580 redrive_policy: "".to_string(),
15581 redrive_task_id: "-".to_string(),
15582 redrive_task_start_time: "-".to_string(),
15583 redrive_task_status: "-".to_string(),
15584 redrive_task_percent: "-".to_string(),
15585 redrive_task_destination: "-".to_string(),
15586 })
15587 .collect();
15588
15589 app.handle_action(Action::NextItem);
15590 assert_eq!(app.sqs_state.queues.selected, 1);
15591
15592 app.handle_action(Action::PrevItem);
15593 assert_eq!(app.sqs_state.queues.selected, 0);
15594 }
15595
15596 #[test]
15597 fn test_sqs_page_navigation() {
15598 let mut app = test_app();
15599 app.current_service = Service::SqsQueues;
15600 app.service_selected = true;
15601 app.mode = Mode::Normal;
15602 app.sqs_state.queues.items = (0..100)
15603 .map(|i| SqsQueue {
15604 name: format!("queue{}", i),
15605 url: String::new(),
15606 queue_type: "Standard".to_string(),
15607 created_timestamp: String::new(),
15608 messages_available: "0".to_string(),
15609 messages_in_flight: "0".to_string(),
15610 encryption: "Disabled".to_string(),
15611 content_based_deduplication: "Disabled".to_string(),
15612 last_modified_timestamp: String::new(),
15613 visibility_timeout: String::new(),
15614 message_retention_period: String::new(),
15615 maximum_message_size: String::new(),
15616 delivery_delay: String::new(),
15617 receive_message_wait_time: String::new(),
15618 high_throughput_fifo: "N/A".to_string(),
15619 deduplication_scope: "N/A".to_string(),
15620 fifo_throughput_limit: "N/A".to_string(),
15621 dead_letter_queue: "-".to_string(),
15622 messages_delayed: "0".to_string(),
15623 redrive_allow_policy: "-".to_string(),
15624 redrive_policy: "".to_string(),
15625 redrive_task_id: "-".to_string(),
15626 redrive_task_start_time: "-".to_string(),
15627 redrive_task_status: "-".to_string(),
15628 redrive_task_percent: "-".to_string(),
15629 redrive_task_destination: "-".to_string(),
15630 })
15631 .collect();
15632
15633 app.handle_action(Action::PageDown);
15634 assert_eq!(app.sqs_state.queues.selected, 10);
15635
15636 app.handle_action(Action::PageUp);
15637 assert_eq!(app.sqs_state.queues.selected, 0);
15638 }
15639
15640 #[test]
15641 fn test_sqs_queue_expansion() {
15642 let mut app = test_app();
15643 app.current_service = Service::SqsQueues;
15644 app.service_selected = true;
15645 app.sqs_state.queues.items = vec![SqsQueue {
15646 name: "my-queue".to_string(),
15647 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
15648 queue_type: "Standard".to_string(),
15649 created_timestamp: "2023-01-01".to_string(),
15650 messages_available: "5".to_string(),
15651 messages_in_flight: "2".to_string(),
15652 encryption: "Enabled".to_string(),
15653 content_based_deduplication: "Disabled".to_string(),
15654 last_modified_timestamp: "2023-01-02".to_string(),
15655 visibility_timeout: "30".to_string(),
15656 message_retention_period: "345600".to_string(),
15657 maximum_message_size: "262144".to_string(),
15658 delivery_delay: "0".to_string(),
15659 receive_message_wait_time: "0".to_string(),
15660 high_throughput_fifo: "N/A".to_string(),
15661 deduplication_scope: "N/A".to_string(),
15662 fifo_throughput_limit: "N/A".to_string(),
15663 dead_letter_queue: "-".to_string(),
15664 messages_delayed: "0".to_string(),
15665 redrive_allow_policy: "-".to_string(),
15666 redrive_policy: "".to_string(),
15667 redrive_task_id: "-".to_string(),
15668 redrive_task_start_time: "-".to_string(),
15669 redrive_task_status: "-".to_string(),
15670 redrive_task_percent: "-".to_string(),
15671 redrive_task_destination: "-".to_string(),
15672 }];
15673 app.sqs_state.queues.selected = 0;
15674
15675 assert_eq!(app.sqs_state.queues.expanded_item, None);
15676
15677 app.handle_action(Action::NextPane);
15679 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
15680
15681 app.handle_action(Action::NextPane);
15683 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
15684
15685 app.handle_action(Action::PrevPane);
15687 assert_eq!(app.sqs_state.queues.expanded_item, None);
15688
15689 app.handle_action(Action::PrevPane);
15691 assert_eq!(app.sqs_state.queues.expanded_item, None);
15692 }
15693
15694 #[test]
15695 fn test_sqs_column_toggle() {
15696 use crate::sqs::queue::Column as SqsColumn;
15697 let mut app = test_app();
15698 app.current_service = Service::SqsQueues;
15699 app.service_selected = true;
15700 app.mode = Mode::ColumnSelector;
15701
15702 app.sqs_visible_column_ids = SqsColumn::ids();
15704 let initial_count = app.sqs_visible_column_ids.len();
15705
15706 app.column_selector_index = 0;
15708 app.handle_action(Action::ToggleColumn);
15709
15710 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
15712 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
15713
15714 app.handle_action(Action::ToggleColumn);
15716 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
15717 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
15718 }
15719
15720 #[test]
15721 fn test_sqs_column_selector_navigation() {
15722 let mut app = test_app();
15723 app.current_service = Service::SqsQueues;
15724 app.service_selected = true;
15725 app.mode = Mode::ColumnSelector;
15726 app.column_selector_index = 0;
15727
15728 let max_index = app.sqs_column_ids.len() - 1;
15730
15731 for _ in 0..max_index {
15733 app.handle_action(Action::NextItem);
15734 }
15735 assert_eq!(app.column_selector_index, max_index);
15736
15737 for _ in 0..max_index {
15739 app.handle_action(Action::PrevItem);
15740 }
15741 assert_eq!(app.column_selector_index, 0);
15742 }
15743
15744 #[test]
15745 fn test_sqs_queue_selection() {
15746 let mut app = test_app();
15747 app.current_service = Service::SqsQueues;
15748 app.service_selected = true;
15749 app.mode = Mode::Normal;
15750 app.sqs_state.queues.items = vec![SqsQueue {
15751 name: "my-queue".to_string(),
15752 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
15753 queue_type: "Standard".to_string(),
15754 created_timestamp: "2023-01-01".to_string(),
15755 messages_available: "5".to_string(),
15756 messages_in_flight: "2".to_string(),
15757 encryption: "Enabled".to_string(),
15758 content_based_deduplication: "Disabled".to_string(),
15759 last_modified_timestamp: "2023-01-02".to_string(),
15760 visibility_timeout: "30".to_string(),
15761 message_retention_period: "345600".to_string(),
15762 maximum_message_size: "262144".to_string(),
15763 delivery_delay: "0".to_string(),
15764 receive_message_wait_time: "0".to_string(),
15765 high_throughput_fifo: "N/A".to_string(),
15766 deduplication_scope: "N/A".to_string(),
15767 fifo_throughput_limit: "N/A".to_string(),
15768 dead_letter_queue: "-".to_string(),
15769 messages_delayed: "0".to_string(),
15770 redrive_allow_policy: "-".to_string(),
15771 redrive_policy: "".to_string(),
15772 redrive_task_id: "-".to_string(),
15773 redrive_task_start_time: "-".to_string(),
15774 redrive_task_status: "-".to_string(),
15775 redrive_task_percent: "-".to_string(),
15776 redrive_task_destination: "-".to_string(),
15777 }];
15778 app.sqs_state.queues.selected = 0;
15779
15780 assert_eq!(app.sqs_state.current_queue, None);
15781
15782 app.handle_action(Action::Select);
15784 assert_eq!(
15785 app.sqs_state.current_queue,
15786 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
15787 );
15788
15789 app.handle_action(Action::GoBack);
15791 assert_eq!(app.sqs_state.current_queue, None);
15792 }
15793
15794 #[test]
15795 fn test_sqs_lambda_triggers_expand_collapse() {
15796 let mut app = test_app();
15797 app.current_service = Service::SqsQueues;
15798 app.service_selected = true;
15799 app.sqs_state.current_queue =
15800 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
15801 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
15802 app.sqs_state.triggers.items = vec![LambdaTrigger {
15803 uuid: "test-uuid".to_string(),
15804 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
15805 status: "Enabled".to_string(),
15806 last_modified: "2024-01-01T00:00:00Z".to_string(),
15807 }];
15808 app.sqs_state.triggers.selected = 0;
15809
15810 assert_eq!(app.sqs_state.triggers.expanded_item, None);
15811
15812 app.handle_action(Action::NextPane);
15814 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
15815
15816 app.handle_action(Action::PrevPane);
15818 assert_eq!(app.sqs_state.triggers.expanded_item, None);
15819 }
15820
15821 #[test]
15822 fn test_sqs_lambda_triggers_expand_toggle() {
15823 let mut app = test_app();
15824 app.current_service = Service::SqsQueues;
15825 app.service_selected = true;
15826 app.sqs_state.current_queue =
15827 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
15828 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
15829 app.sqs_state.triggers.items = vec![LambdaTrigger {
15830 uuid: "test-uuid".to_string(),
15831 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
15832 status: "Enabled".to_string(),
15833 last_modified: "2024-01-01T00:00:00Z".to_string(),
15834 }];
15835 app.sqs_state.triggers.selected = 0;
15836
15837 app.handle_action(Action::NextPane);
15839 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
15840
15841 app.handle_action(Action::NextPane);
15843 assert_eq!(app.sqs_state.triggers.expanded_item, None);
15844
15845 app.handle_action(Action::NextPane);
15847 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
15848 }
15849
15850 #[test]
15851 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
15852 use crate::ui::sqs::filtered_lambda_triggers;
15853
15854 let mut app = test_app();
15855 app.current_service = Service::SqsQueues;
15856 app.service_selected = true;
15857 app.sqs_state.current_queue =
15858 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
15859 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
15860 app.sqs_state.triggers.items = vec![
15861 LambdaTrigger {
15862 uuid: "uuid-3".to_string(),
15863 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
15864 status: "Enabled".to_string(),
15865 last_modified: "2024-03-01T00:00:00Z".to_string(),
15866 },
15867 LambdaTrigger {
15868 uuid: "uuid-1".to_string(),
15869 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
15870 status: "Enabled".to_string(),
15871 last_modified: "2024-01-01T00:00:00Z".to_string(),
15872 },
15873 LambdaTrigger {
15874 uuid: "uuid-2".to_string(),
15875 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
15876 status: "Enabled".to_string(),
15877 last_modified: "2024-02-01T00:00:00Z".to_string(),
15878 },
15879 ];
15880
15881 let sorted = filtered_lambda_triggers(&app);
15882
15883 assert_eq!(sorted.len(), 3);
15885 assert_eq!(sorted[0].uuid, "uuid-1");
15886 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
15887 assert_eq!(sorted[1].uuid, "uuid-2");
15888 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
15889 assert_eq!(sorted[2].uuid, "uuid-3");
15890 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
15891 }
15892
15893 #[test]
15894 fn test_sqs_lambda_triggers_filter_input() {
15895 let mut app = test_app();
15896 app.current_service = Service::SqsQueues;
15897 app.service_selected = true;
15898 app.mode = Mode::FilterInput;
15899 app.sqs_state.current_queue =
15900 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
15901 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
15902 app.sqs_state.input_focus = InputFocus::Filter;
15903
15904 assert_eq!(app.sqs_state.triggers.filter, "");
15905
15906 app.handle_action(Action::FilterInput('t'));
15908 assert_eq!(app.sqs_state.triggers.filter, "t");
15909
15910 app.handle_action(Action::FilterInput('e'));
15911 assert_eq!(app.sqs_state.triggers.filter, "te");
15912
15913 app.handle_action(Action::FilterInput('s'));
15914 assert_eq!(app.sqs_state.triggers.filter, "tes");
15915
15916 app.handle_action(Action::FilterInput('t'));
15917 assert_eq!(app.sqs_state.triggers.filter, "test");
15918
15919 app.handle_action(Action::FilterBackspace);
15921 assert_eq!(app.sqs_state.triggers.filter, "tes");
15922 }
15923
15924 #[test]
15925 fn test_sqs_lambda_triggers_filter_applied() {
15926 use crate::ui::sqs::filtered_lambda_triggers;
15927
15928 let mut app = test_app();
15929 app.current_service = Service::SqsQueues;
15930 app.service_selected = true;
15931 app.sqs_state.current_queue =
15932 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
15933 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
15934 app.sqs_state.triggers.items = vec![
15935 LambdaTrigger {
15936 uuid: "uuid-1".to_string(),
15937 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
15938 status: "Enabled".to_string(),
15939 last_modified: "2024-01-01T00:00:00Z".to_string(),
15940 },
15941 LambdaTrigger {
15942 uuid: "uuid-2".to_string(),
15943 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
15944 status: "Enabled".to_string(),
15945 last_modified: "2024-02-01T00:00:00Z".to_string(),
15946 },
15947 LambdaTrigger {
15948 uuid: "uuid-3".to_string(),
15949 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
15950 status: "Enabled".to_string(),
15951 last_modified: "2024-03-01T00:00:00Z".to_string(),
15952 },
15953 ];
15954
15955 let filtered = filtered_lambda_triggers(&app);
15957 assert_eq!(filtered.len(), 3);
15958
15959 app.sqs_state.triggers.filter = "alpha".to_string();
15961 let filtered = filtered_lambda_triggers(&app);
15962 assert_eq!(filtered.len(), 1);
15963 assert_eq!(
15964 filtered[0].arn,
15965 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
15966 );
15967
15968 app.sqs_state.triggers.filter = "test".to_string();
15970 let filtered = filtered_lambda_triggers(&app);
15971 assert_eq!(filtered.len(), 2);
15972 assert_eq!(
15973 filtered[0].arn,
15974 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
15975 );
15976 assert_eq!(
15977 filtered[1].arn,
15978 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
15979 );
15980
15981 app.sqs_state.triggers.filter = "uuid-3".to_string();
15983 let filtered = filtered_lambda_triggers(&app);
15984 assert_eq!(filtered.len(), 1);
15985 assert_eq!(filtered[0].uuid, "uuid-3");
15986 }
15987
15988 #[test]
15989 fn test_sqs_triggers_navigation() {
15990 let mut app = test_app();
15991 app.service_selected = true;
15992 app.mode = Mode::Normal;
15993 app.current_service = Service::SqsQueues;
15994 app.sqs_state.current_queue = Some("test-queue".to_string());
15995 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
15996 app.sqs_state.triggers.items = vec![
15997 LambdaTrigger {
15998 uuid: "1".to_string(),
15999 arn: "arn1".to_string(),
16000 status: "Enabled".to_string(),
16001 last_modified: "2024-01-01".to_string(),
16002 },
16003 LambdaTrigger {
16004 uuid: "2".to_string(),
16005 arn: "arn2".to_string(),
16006 status: "Enabled".to_string(),
16007 last_modified: "2024-01-02".to_string(),
16008 },
16009 ];
16010
16011 assert_eq!(app.sqs_state.triggers.selected, 0);
16012 app.next_item();
16013 assert_eq!(app.sqs_state.triggers.selected, 1);
16014 app.prev_item();
16015 assert_eq!(app.sqs_state.triggers.selected, 0);
16016 }
16017
16018 #[test]
16019 fn test_sqs_pipes_navigation() {
16020 let mut app = test_app();
16021 app.service_selected = true;
16022 app.mode = Mode::Normal;
16023 app.current_service = Service::SqsQueues;
16024 app.sqs_state.current_queue = Some("test-queue".to_string());
16025 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
16026 app.sqs_state.pipes.items = vec![
16027 EventBridgePipe {
16028 name: "pipe1".to_string(),
16029 status: "RUNNING".to_string(),
16030 target: "target1".to_string(),
16031 last_modified: "2024-01-01".to_string(),
16032 },
16033 EventBridgePipe {
16034 name: "pipe2".to_string(),
16035 status: "RUNNING".to_string(),
16036 target: "target2".to_string(),
16037 last_modified: "2024-01-02".to_string(),
16038 },
16039 ];
16040
16041 assert_eq!(app.sqs_state.pipes.selected, 0);
16042 app.next_item();
16043 assert_eq!(app.sqs_state.pipes.selected, 1);
16044 app.prev_item();
16045 assert_eq!(app.sqs_state.pipes.selected, 0);
16046 }
16047
16048 #[test]
16049 fn test_sqs_tags_navigation() {
16050 let mut app = test_app();
16051 app.service_selected = true;
16052 app.mode = Mode::Normal;
16053 app.current_service = Service::SqsQueues;
16054 app.sqs_state.current_queue = Some("test-queue".to_string());
16055 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
16056 app.sqs_state.tags.items = vec![
16057 SqsQueueTag {
16058 key: "Env".to_string(),
16059 value: "prod".to_string(),
16060 },
16061 SqsQueueTag {
16062 key: "Team".to_string(),
16063 value: "backend".to_string(),
16064 },
16065 ];
16066
16067 assert_eq!(app.sqs_state.tags.selected, 0);
16068 app.next_item();
16069 assert_eq!(app.sqs_state.tags.selected, 1);
16070 app.prev_item();
16071 assert_eq!(app.sqs_state.tags.selected, 0);
16072 }
16073
16074 #[test]
16075 fn test_sqs_queues_navigation() {
16076 let mut app = test_app();
16077 app.service_selected = true;
16078 app.mode = Mode::Normal;
16079 app.current_service = Service::SqsQueues;
16080 app.sqs_state.queues.items = vec![
16081 SqsQueue {
16082 name: "queue1".to_string(),
16083 url: "url1".to_string(),
16084 queue_type: "Standard".to_string(),
16085 created_timestamp: "".to_string(),
16086 messages_available: "0".to_string(),
16087 messages_in_flight: "0".to_string(),
16088 encryption: "Disabled".to_string(),
16089 content_based_deduplication: "Disabled".to_string(),
16090 last_modified_timestamp: "".to_string(),
16091 visibility_timeout: "".to_string(),
16092 message_retention_period: "".to_string(),
16093 maximum_message_size: "".to_string(),
16094 delivery_delay: "".to_string(),
16095 receive_message_wait_time: "".to_string(),
16096 high_throughput_fifo: "-".to_string(),
16097 deduplication_scope: "-".to_string(),
16098 fifo_throughput_limit: "-".to_string(),
16099 dead_letter_queue: "-".to_string(),
16100 messages_delayed: "0".to_string(),
16101 redrive_allow_policy: "-".to_string(),
16102 redrive_policy: "".to_string(),
16103 redrive_task_id: "-".to_string(),
16104 redrive_task_start_time: "-".to_string(),
16105 redrive_task_status: "-".to_string(),
16106 redrive_task_percent: "-".to_string(),
16107 redrive_task_destination: "-".to_string(),
16108 },
16109 SqsQueue {
16110 name: "queue2".to_string(),
16111 url: "url2".to_string(),
16112 queue_type: "Standard".to_string(),
16113 created_timestamp: "".to_string(),
16114 messages_available: "0".to_string(),
16115 messages_in_flight: "0".to_string(),
16116 encryption: "Disabled".to_string(),
16117 content_based_deduplication: "Disabled".to_string(),
16118 last_modified_timestamp: "".to_string(),
16119 visibility_timeout: "".to_string(),
16120 message_retention_period: "".to_string(),
16121 maximum_message_size: "".to_string(),
16122 delivery_delay: "".to_string(),
16123 receive_message_wait_time: "".to_string(),
16124 high_throughput_fifo: "-".to_string(),
16125 deduplication_scope: "-".to_string(),
16126 fifo_throughput_limit: "-".to_string(),
16127 dead_letter_queue: "-".to_string(),
16128 messages_delayed: "0".to_string(),
16129 redrive_allow_policy: "-".to_string(),
16130 redrive_policy: "".to_string(),
16131 redrive_task_id: "-".to_string(),
16132 redrive_task_start_time: "-".to_string(),
16133 redrive_task_status: "-".to_string(),
16134 redrive_task_percent: "-".to_string(),
16135 redrive_task_destination: "-".to_string(),
16136 },
16137 ];
16138
16139 assert_eq!(app.sqs_state.queues.selected, 0);
16140 app.next_item();
16141 assert_eq!(app.sqs_state.queues.selected, 1);
16142 app.prev_item();
16143 assert_eq!(app.sqs_state.queues.selected, 0);
16144 }
16145
16146 #[test]
16147 fn test_sqs_subscriptions_navigation() {
16148 let mut app = test_app();
16149 app.service_selected = true;
16150 app.mode = Mode::Normal;
16151 app.current_service = Service::SqsQueues;
16152 app.sqs_state.current_queue = Some("test-queue".to_string());
16153 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16154 app.sqs_state.subscriptions.items = vec![
16155 SnsSubscription {
16156 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
16157 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
16158 },
16159 SnsSubscription {
16160 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
16161 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
16162 },
16163 ];
16164
16165 assert_eq!(app.sqs_state.subscriptions.selected, 0);
16166 app.next_item();
16167 assert_eq!(app.sqs_state.subscriptions.selected, 1);
16168 app.prev_item();
16169 assert_eq!(app.sqs_state.subscriptions.selected, 0);
16170 }
16171
16172 #[test]
16173 fn test_sqs_subscription_region_dropdown_navigation() {
16174 let mut app = test_app();
16175 app.service_selected = true;
16176 app.mode = Mode::FilterInput;
16177 app.current_service = Service::SqsQueues;
16178 app.sqs_state.current_queue = Some("test-queue".to_string());
16179 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16180 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
16181
16182 assert_eq!(app.sqs_state.subscription_region_selected, 0);
16183 app.next_item();
16184 assert_eq!(app.sqs_state.subscription_region_selected, 1);
16185 app.next_item();
16186 assert_eq!(app.sqs_state.subscription_region_selected, 2);
16187 app.prev_item();
16188 assert_eq!(app.sqs_state.subscription_region_selected, 1);
16189 app.prev_item();
16190 assert_eq!(app.sqs_state.subscription_region_selected, 0);
16191 }
16192
16193 #[test]
16194 fn test_sqs_subscription_region_selection() {
16195 let mut app = test_app();
16196 app.service_selected = true;
16197 app.mode = Mode::FilterInput;
16198 app.current_service = Service::SqsQueues;
16199 app.sqs_state.current_queue = Some("test-queue".to_string());
16200 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16201 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
16202 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
16205 app.handle_action(Action::ApplyFilter);
16206 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
16207 assert_eq!(app.mode, Mode::Normal);
16208 }
16209
16210 #[test]
16211 fn test_sqs_subscription_region_change_resets_selection() {
16212 let mut app = test_app();
16213 app.service_selected = true;
16214 app.mode = Mode::FilterInput;
16215 app.current_service = Service::SqsQueues;
16216 app.sqs_state.current_queue = Some("test-queue".to_string());
16217 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16218 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
16219 app.sqs_state.subscription_region_selected = 0;
16220 app.sqs_state.subscriptions.selected = 5;
16221
16222 app.handle_action(Action::NextItem);
16223
16224 assert_eq!(app.sqs_state.subscription_region_selected, 1);
16225 assert_eq!(app.sqs_state.subscriptions.selected, 0);
16226 }
16227
16228 #[test]
16229 fn test_s3_object_filter_resets_selection() {
16230 let mut app = test_app();
16231 app.service_selected = true;
16232 app.current_service = Service::S3Buckets;
16233 app.s3_state.current_bucket = Some("test-bucket".to_string());
16234 app.s3_state.selected_row = 5;
16235 app.mode = Mode::FilterInput;
16236
16237 app.handle_action(Action::CloseMenu);
16238
16239 assert_eq!(app.s3_state.selected_row, 0);
16240 assert_eq!(app.mode, Mode::Normal);
16241 }
16242
16243 #[test]
16244 fn test_s3_bucket_filter_resets_selection() {
16245 let mut app = test_app();
16246 app.service_selected = true;
16247 app.current_service = Service::S3Buckets;
16248 app.s3_state.selected_row = 10;
16249 app.mode = Mode::FilterInput;
16250
16251 app.handle_action(Action::CloseMenu);
16252
16253 assert_eq!(app.s3_state.selected_row, 0);
16254 assert_eq!(app.mode, Mode::Normal);
16255 }
16256
16257 #[test]
16258 fn test_s3_selection_stays_in_bounds() {
16259 let mut app = test_app();
16260 app.service_selected = true;
16261 app.current_service = Service::S3Buckets;
16262 app.s3_state.selected_row = 0;
16263 app.s3_state.selected_object = 0;
16264
16265 app.prev_item();
16267
16268 assert_eq!(app.s3_state.selected_row, 0);
16270 assert_eq!(app.s3_state.selected_object, 0);
16271 }
16272
16273 #[test]
16274 fn test_cfn_filter_resets_selection() {
16275 let mut app = test_app();
16276 app.service_selected = true;
16277 app.current_service = Service::CloudFormationStacks;
16278 app.cfn_state.table.selected = 10;
16279 app.mode = Mode::FilterInput;
16280
16281 app.handle_action(Action::CloseMenu);
16282
16283 assert_eq!(app.cfn_state.table.selected, 0);
16284 assert_eq!(app.mode, Mode::Normal);
16285 }
16286
16287 #[test]
16288 fn test_lambda_filter_resets_selection() {
16289 let mut app = test_app();
16290 app.service_selected = true;
16291 app.current_service = Service::LambdaFunctions;
16292 app.lambda_state.table.selected = 8;
16293 app.mode = Mode::FilterInput;
16294
16295 app.handle_action(Action::CloseMenu);
16296
16297 assert_eq!(app.lambda_state.table.selected, 0);
16298 assert_eq!(app.mode, Mode::Normal);
16299 }
16300
16301 #[test]
16302 fn test_sqs_filter_resets_selection() {
16303 let mut app = test_app();
16304 app.service_selected = true;
16305 app.current_service = Service::SqsQueues;
16306 app.sqs_state.queues.selected = 7;
16307 app.mode = Mode::FilterInput;
16308
16309 app.handle_action(Action::CloseMenu);
16310
16311 assert_eq!(app.sqs_state.queues.selected, 0);
16312 assert_eq!(app.mode, Mode::Normal);
16313 }
16314
16315 #[test]
16316 fn test_sqs_queues_list_shows_preferences() {
16317 let mut app = test_app();
16318 app.service_selected = true;
16319 app.current_service = Service::SqsQueues;
16320 app.mode = Mode::Normal;
16321
16322 app.handle_action(Action::OpenColumnSelector);
16323
16324 assert_eq!(app.mode, Mode::ColumnSelector);
16325 }
16326
16327 #[test]
16328 fn test_sqs_queue_policies_tab_no_preferences() {
16329 let mut app = test_app();
16330 app.service_selected = true;
16331 app.current_service = Service::SqsQueues;
16332 app.sqs_state.current_queue = Some("test-queue".to_string());
16333 app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
16334 app.mode = Mode::Normal;
16335
16336 app.handle_action(Action::OpenColumnSelector);
16337
16338 assert_eq!(app.mode, Mode::Normal);
16339 }
16340
16341 #[test]
16342 fn test_sqs_sns_subscriptions_tab_shows_preferences() {
16343 let mut app = test_app();
16344 app.service_selected = true;
16345 app.current_service = Service::SqsQueues;
16346 app.sqs_state.current_queue = Some("test-queue".to_string());
16347 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16348 app.mode = Mode::Normal;
16349
16350 app.handle_action(Action::OpenColumnSelector);
16351
16352 assert_eq!(app.mode, Mode::ColumnSelector);
16353 }
16354
16355 #[test]
16356 fn test_sqs_monitoring_tab_no_preferences() {
16357 let mut app = test_app();
16358 app.service_selected = true;
16359 app.current_service = Service::SqsQueues;
16360 app.sqs_state.current_queue = Some("test-queue".to_string());
16361 app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
16362 app.mode = Mode::Normal;
16363
16364 app.handle_action(Action::OpenColumnSelector);
16365
16366 assert_eq!(app.mode, Mode::Normal);
16367 }
16368
16369 #[test]
16370 fn test_cfn_status_filter_change_resets_selection() {
16371 use crate::ui::cfn::STATUS_FILTER;
16372 let mut app = test_app();
16373 app.service_selected = true;
16374 app.current_service = Service::CloudFormationStacks;
16375 app.mode = Mode::FilterInput;
16376 app.cfn_state.input_focus = STATUS_FILTER;
16377 app.cfn_state.status_filter = CfnStatusFilter::All;
16378 app.cfn_state.table.items = vec![
16379 CfnStack {
16380 name: "stack1".to_string(),
16381 stack_id: "id1".to_string(),
16382 status: "CREATE_COMPLETE".to_string(),
16383 created_time: "2024-01-01".to_string(),
16384 updated_time: String::new(),
16385 deleted_time: String::new(),
16386 drift_status: String::new(),
16387 last_drift_check_time: String::new(),
16388 status_reason: String::new(),
16389 description: String::new(),
16390 detailed_status: String::new(),
16391 root_stack: String::new(),
16392 parent_stack: String::new(),
16393 termination_protection: false,
16394 iam_role: String::new(),
16395 tags: Vec::new(),
16396 stack_policy: String::new(),
16397 rollback_monitoring_time: String::new(),
16398 rollback_alarms: Vec::new(),
16399 notification_arns: Vec::new(),
16400 },
16401 CfnStack {
16402 name: "stack2".to_string(),
16403 stack_id: "id2".to_string(),
16404 status: "UPDATE_IN_PROGRESS".to_string(),
16405 created_time: "2024-01-02".to_string(),
16406 updated_time: String::new(),
16407 deleted_time: String::new(),
16408 drift_status: String::new(),
16409 last_drift_check_time: String::new(),
16410 status_reason: String::new(),
16411 description: String::new(),
16412 detailed_status: String::new(),
16413 root_stack: String::new(),
16414 parent_stack: String::new(),
16415 termination_protection: false,
16416 iam_role: String::new(),
16417 tags: Vec::new(),
16418 stack_policy: String::new(),
16419 rollback_monitoring_time: String::new(),
16420 rollback_alarms: Vec::new(),
16421 notification_arns: Vec::new(),
16422 },
16423 ];
16424 app.cfn_state.table.selected = 1;
16425
16426 app.handle_action(Action::NextItem);
16427
16428 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
16429 assert_eq!(app.cfn_state.table.selected, 0);
16430 }
16431
16432 #[test]
16433 fn test_cfn_view_nested_toggle_resets_selection() {
16434 use crate::ui::cfn::VIEW_NESTED;
16435 let mut app = test_app();
16436 app.service_selected = true;
16437 app.current_service = Service::CloudFormationStacks;
16438 app.mode = Mode::FilterInput;
16439 app.cfn_state.input_focus = VIEW_NESTED;
16440 app.cfn_state.view_nested = false;
16441 app.cfn_state.table.items = vec![CfnStack {
16442 name: "stack1".to_string(),
16443 stack_id: "id1".to_string(),
16444 status: "CREATE_COMPLETE".to_string(),
16445 created_time: "2024-01-01".to_string(),
16446 updated_time: String::new(),
16447 deleted_time: String::new(),
16448 drift_status: String::new(),
16449 last_drift_check_time: String::new(),
16450 status_reason: String::new(),
16451 description: String::new(),
16452 detailed_status: String::new(),
16453 root_stack: String::new(),
16454 parent_stack: String::new(),
16455 termination_protection: false,
16456 iam_role: String::new(),
16457 tags: Vec::new(),
16458 stack_policy: String::new(),
16459 rollback_monitoring_time: String::new(),
16460 rollback_alarms: Vec::new(),
16461 notification_arns: Vec::new(),
16462 }];
16463 app.cfn_state.table.selected = 5;
16464
16465 app.handle_action(Action::ToggleFilterCheckbox);
16466
16467 assert!(app.cfn_state.view_nested);
16468 assert_eq!(app.cfn_state.table.selected, 0);
16469 }
16470
16471 #[test]
16472 fn test_cfn_template_scroll_up() {
16473 let mut app = test_app();
16474 app.service_selected = true;
16475 app.current_service = Service::CloudFormationStacks;
16476 app.cfn_state.current_stack = Some("test-stack".to_string());
16477 app.cfn_state.detail_tab = CfnDetailTab::Template;
16478 app.cfn_state.template_scroll = 20;
16479
16480 app.page_up();
16481
16482 assert_eq!(app.cfn_state.template_scroll, 10);
16483 }
16484
16485 #[test]
16486 fn test_cfn_template_scroll_down() {
16487 let mut app = test_app();
16488 app.service_selected = true;
16489 app.current_service = Service::CloudFormationStacks;
16490 app.cfn_state.current_stack = Some("test-stack".to_string());
16491 app.cfn_state.detail_tab = CfnDetailTab::Template;
16492 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
16493 app.cfn_state.template_scroll = 0;
16494
16495 app.page_down();
16496
16497 assert_eq!(app.cfn_state.template_scroll, 10);
16498 }
16499
16500 #[test]
16501 fn test_cfn_template_scroll_down_respects_max() {
16502 let mut app = test_app();
16503 app.service_selected = true;
16504 app.current_service = Service::CloudFormationStacks;
16505 app.cfn_state.current_stack = Some("test-stack".to_string());
16506 app.cfn_state.detail_tab = CfnDetailTab::Template;
16507 app.cfn_state.template_body = "line1\nline2\nline3".to_string();
16508 app.cfn_state.template_scroll = 0;
16509
16510 app.page_down();
16511
16512 assert_eq!(app.cfn_state.template_scroll, 2);
16514 }
16515
16516 #[test]
16517 fn test_cfn_template_arrow_up() {
16518 let mut app = test_app();
16519 app.service_selected = true;
16520 app.current_service = Service::CloudFormationStacks;
16521 app.mode = Mode::Normal;
16522 app.cfn_state.current_stack = Some("test-stack".to_string());
16523 app.cfn_state.detail_tab = CfnDetailTab::Template;
16524 app.cfn_state.template_scroll = 5;
16525
16526 app.prev_item();
16527
16528 assert_eq!(app.cfn_state.template_scroll, 4);
16529 }
16530
16531 #[test]
16532 fn test_cfn_template_arrow_down() {
16533 let mut app = test_app();
16534 app.service_selected = true;
16535 app.current_service = Service::CloudFormationStacks;
16536 app.mode = Mode::Normal;
16537 app.cfn_state.current_stack = Some("test-stack".to_string());
16538 app.cfn_state.detail_tab = CfnDetailTab::Template;
16539 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
16540 app.cfn_state.template_scroll = 2;
16541
16542 app.next_item();
16543
16544 assert_eq!(app.cfn_state.template_scroll, 3);
16545 }
16546
16547 #[test]
16548 fn test_cfn_template_arrow_down_respects_max() {
16549 let mut app = test_app();
16550 app.service_selected = true;
16551 app.current_service = Service::CloudFormationStacks;
16552 app.mode = Mode::Normal;
16553 app.cfn_state.current_stack = Some("test-stack".to_string());
16554 app.cfn_state.detail_tab = CfnDetailTab::Template;
16555 app.cfn_state.template_body = "line1\nline2".to_string();
16556 app.cfn_state.template_scroll = 1;
16557
16558 app.next_item();
16559
16560 assert_eq!(app.cfn_state.template_scroll, 1);
16562 }
16563}
16564
16565#[cfg(test)]
16566mod lambda_version_tab_tests {
16567 use super::*;
16568 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
16569 use test_helpers::*;
16570
16571 #[test]
16572 fn test_lambda_version_tab_cycling_next() {
16573 let mut app = test_app();
16574 app.current_service = Service::LambdaFunctions;
16575 app.lambda_state.current_function = Some("test-function".to_string());
16576 app.lambda_state.current_version = Some("1".to_string());
16577 app.lambda_state.detail_tab = LambdaDetailTab::Code;
16578
16579 app.handle_action(Action::NextDetailTab);
16581 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
16582 assert!(app.lambda_state.metrics_loading);
16583
16584 app.lambda_state.metrics_loading = false;
16586 app.handle_action(Action::NextDetailTab);
16587 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
16588
16589 app.handle_action(Action::NextDetailTab);
16591 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
16592 }
16593
16594 #[test]
16595 fn test_lambda_version_tab_cycling_prev() {
16596 let mut app = test_app();
16597 app.current_service = Service::LambdaFunctions;
16598 app.lambda_state.current_function = Some("test-function".to_string());
16599 app.lambda_state.current_version = Some("1".to_string());
16600 app.lambda_state.detail_tab = LambdaDetailTab::Code;
16601
16602 app.handle_action(Action::PrevDetailTab);
16604 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
16605
16606 app.handle_action(Action::PrevDetailTab);
16608 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
16609 assert!(app.lambda_state.metrics_loading);
16610
16611 app.lambda_state.metrics_loading = false;
16613 app.handle_action(Action::PrevDetailTab);
16614 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
16615 }
16616
16617 #[test]
16618 fn test_lambda_version_monitor_clears_metrics() {
16619 let mut app = test_app();
16620 app.current_service = Service::LambdaFunctions;
16621 app.lambda_state.current_function = Some("test-function".to_string());
16622 app.lambda_state.current_version = Some("1".to_string());
16623 app.lambda_state.detail_tab = LambdaDetailTab::Code;
16624
16625 app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
16627 app.lambda_state.monitoring_scroll = 5;
16628
16629 app.handle_action(Action::NextDetailTab);
16631
16632 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
16633 assert!(app.lambda_state.metrics_loading);
16634 assert_eq!(app.lambda_state.monitoring_scroll, 0);
16635 assert!(app.lambda_state.metric_data_invocations.is_empty());
16636 }
16637
16638 #[test]
16639 fn test_cfn_parameters_expand_collapse() {
16640 let mut app = test_app();
16641 app.current_service = Service::CloudFormationStacks;
16642 app.service_selected = true;
16643 app.cfn_state.current_stack = Some("test-stack".to_string());
16644 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
16645 app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
16646 key: "Param1".to_string(),
16647 value: "Value1".to_string(),
16648 resolved_value: "Resolved1".to_string(),
16649 }];
16650 app.cfn_state.parameters.reset();
16651
16652 assert_eq!(app.cfn_state.parameters.expanded_item, None);
16653
16654 app.handle_action(Action::NextPane);
16656 assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
16657
16658 app.handle_action(Action::PrevPane);
16660 assert_eq!(app.cfn_state.parameters.expanded_item, None);
16661 }
16662
16663 #[test]
16664 fn test_cfn_parameters_filter_resets_selection() {
16665 let mut app = test_app();
16666 app.current_service = Service::CloudFormationStacks;
16667 app.service_selected = true;
16668 app.cfn_state.current_stack = Some("test-stack".to_string());
16669 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
16670 app.cfn_state.parameters.items = vec![
16671 rusticity_core::cfn::StackParameter {
16672 key: "DatabaseName".to_string(),
16673 value: "mydb".to_string(),
16674 resolved_value: "mydb".to_string(),
16675 },
16676 rusticity_core::cfn::StackParameter {
16677 key: "InstanceType".to_string(),
16678 value: "t2.micro".to_string(),
16679 resolved_value: "t2.micro".to_string(),
16680 },
16681 rusticity_core::cfn::StackParameter {
16682 key: "Environment".to_string(),
16683 value: "production".to_string(),
16684 resolved_value: "production".to_string(),
16685 },
16686 ];
16687 app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
16689 app.cfn_state.parameters_input_focus = InputFocus::Filter;
16690
16691 app.handle_action(Action::FilterInput('D'));
16693 assert_eq!(app.cfn_state.parameters.selected, 0);
16694 assert_eq!(app.cfn_state.parameters.filter, "D");
16695
16696 app.cfn_state.parameters.selected = 1;
16698
16699 app.handle_action(Action::FilterInput('a'));
16701 assert_eq!(app.cfn_state.parameters.selected, 0);
16702 assert_eq!(app.cfn_state.parameters.filter, "Da");
16703
16704 app.cfn_state.parameters.selected = 1;
16706
16707 app.handle_action(Action::FilterBackspace);
16709 assert_eq!(app.cfn_state.parameters.selected, 0);
16710 assert_eq!(app.cfn_state.parameters.filter, "D");
16711 }
16712
16713 #[test]
16714 fn test_cfn_template_tab_no_preferences() {
16715 let mut app = test_app();
16716 app.current_service = Service::CloudFormationStacks;
16717 app.service_selected = true;
16718 app.cfn_state.current_stack = Some("test-stack".to_string());
16719 app.cfn_state.detail_tab = CfnDetailTab::Template;
16720 app.mode = Mode::Normal;
16721
16722 app.handle_action(Action::OpenColumnSelector);
16724 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::GitSync;
16728 app.handle_action(Action::OpenColumnSelector);
16729 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::Parameters;
16733 app.handle_action(Action::OpenColumnSelector);
16734 assert_eq!(app.mode, Mode::ColumnSelector); app.mode = Mode::Normal;
16738 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
16739 app.handle_action(Action::OpenColumnSelector);
16740 assert_eq!(app.mode, Mode::ColumnSelector); }
16742
16743 #[test]
16744 fn test_iam_user_groups_tab_shows_preferences() {
16745 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
16746 app.current_service = Service::IamUsers;
16747 app.service_selected = true;
16748 app.mode = Mode::Normal;
16749 app.iam_state.current_user = Some("test-user".to_string());
16750 app.iam_state.user_tab = UserTab::Groups;
16751
16752 app.handle_action(Action::OpenColumnSelector);
16754 assert_eq!(app.mode, Mode::ColumnSelector);
16755 }
16756
16757 #[test]
16758 fn test_iam_user_tags_tab_shows_preferences() {
16759 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
16760 app.current_service = Service::IamUsers;
16761 app.service_selected = true;
16762 app.mode = Mode::Normal;
16763 app.iam_state.current_user = Some("test-user".to_string());
16764 app.iam_state.user_tab = UserTab::Tags;
16765
16766 app.handle_action(Action::OpenColumnSelector);
16768 assert_eq!(app.mode, Mode::ColumnSelector);
16769 }
16770
16771 #[test]
16772 fn test_iam_user_last_accessed_tab_shows_preferences() {
16773 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
16774 app.current_service = Service::IamUsers;
16775 app.service_selected = true;
16776 app.mode = Mode::Normal;
16777 app.iam_state.current_user = Some("test-user".to_string());
16778 app.iam_state.user_tab = UserTab::LastAccessed;
16779
16780 app.handle_action(Action::OpenColumnSelector);
16782 assert_eq!(app.mode, Mode::ColumnSelector);
16783 }
16784
16785 #[test]
16786 fn test_iam_user_security_credentials_tab_no_preferences() {
16787 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
16788 app.current_service = Service::IamUsers;
16789 app.service_selected = true;
16790 app.mode = Mode::Normal;
16791 app.iam_state.current_user = Some("test-user".to_string());
16792 app.iam_state.user_tab = UserTab::SecurityCredentials;
16793
16794 app.handle_action(Action::OpenColumnSelector);
16796 assert_eq!(app.mode, Mode::Normal);
16797 }
16798
16799 #[test]
16800 fn test_iam_user_tabs_without_column_preferences() {
16801 let mut app = test_app();
16802 app.current_service = Service::IamUsers;
16803 app.service_selected = true;
16804 app.iam_state.current_user = Some("test-user".to_string());
16805 app.mode = Mode::Normal;
16806
16807 app.iam_state.user_tab = UserTab::Groups;
16809 app.handle_action(Action::OpenColumnSelector);
16810 assert_eq!(app.mode, Mode::ColumnSelector);
16811 app.mode = Mode::Normal;
16812
16813 app.iam_state.user_tab = UserTab::Tags;
16815 app.handle_action(Action::OpenColumnSelector);
16816 assert_eq!(app.mode, Mode::ColumnSelector);
16817 app.mode = Mode::Normal;
16818
16819 app.iam_state.user_tab = UserTab::SecurityCredentials;
16821 app.handle_action(Action::OpenColumnSelector);
16822 assert_eq!(app.mode, Mode::Normal);
16823
16824 app.iam_state.user_tab = UserTab::LastAccessed;
16826 app.handle_action(Action::OpenColumnSelector);
16827 assert_eq!(app.mode, Mode::ColumnSelector);
16828 app.mode = Mode::Normal;
16829
16830 app.iam_state.user_tab = UserTab::Permissions;
16832 app.handle_action(Action::OpenColumnSelector);
16833 assert_eq!(app.mode, Mode::ColumnSelector);
16834
16835 app.mode = Mode::Normal;
16837 app.iam_state.current_user = None;
16838 app.handle_action(Action::OpenColumnSelector);
16839 assert_eq!(app.mode, Mode::ColumnSelector);
16840 }
16841
16842 #[test]
16843 fn test_iam_role_policies_dropdown_cycling() {
16844 let mut app = test_app();
16845 app.current_service = Service::IamRoles;
16846 app.service_selected = true;
16847 app.iam_state.current_role = Some("test-role".to_string());
16848 app.iam_state.role_tab = RoleTab::Permissions;
16849 app.mode = Mode::FilterInput;
16850 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
16851 app.iam_state.policy_type_filter = "All types".to_string();
16852
16853 app.next_item();
16855 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
16856 app.next_item();
16857 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
16858 app.next_item();
16859 assert_eq!(app.iam_state.policy_type_filter, "All types");
16860
16861 app.prev_item();
16863 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
16864 app.prev_item();
16865 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
16866 app.prev_item();
16867 assert_eq!(app.iam_state.policy_type_filter, "All types");
16868 }
16869
16870 #[test]
16871 fn test_iam_user_policies_dropdown_cycling() {
16872 let mut app = test_app();
16873 app.current_service = Service::IamUsers;
16874 app.service_selected = true;
16875 app.iam_state.current_user = Some("test-user".to_string());
16876 app.iam_state.user_tab = UserTab::Permissions;
16877 app.mode = Mode::FilterInput;
16878 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
16879 app.iam_state.policy_type_filter = "All types".to_string();
16880
16881 app.next_item();
16883 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
16884 app.next_item();
16885 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
16886 app.next_item();
16887 assert_eq!(app.iam_state.policy_type_filter, "All types");
16888
16889 app.prev_item();
16891 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
16892 app.prev_item();
16893 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
16894 app.prev_item();
16895 assert_eq!(app.iam_state.policy_type_filter, "All types");
16896 }
16897
16898 #[test]
16899 fn test_iam_role_tabs_without_column_preferences() {
16900 let mut app = test_app();
16901 app.current_service = Service::IamRoles;
16902 app.service_selected = true;
16903 app.iam_state.current_role = Some("test-role".to_string());
16904 app.mode = Mode::Normal;
16905
16906 app.iam_state.role_tab = RoleTab::TrustRelationships;
16908 app.handle_action(Action::OpenColumnSelector);
16909 assert_eq!(app.mode, Mode::Normal);
16910
16911 app.iam_state.role_tab = RoleTab::RevokeSessions;
16913 app.handle_action(Action::OpenColumnSelector);
16914 assert_eq!(app.mode, Mode::Normal);
16915
16916 app.iam_state.role_tab = RoleTab::LastAccessed;
16918 app.handle_action(Action::OpenColumnSelector);
16919 assert_eq!(app.mode, Mode::ColumnSelector);
16920
16921 app.mode = Mode::Normal;
16923 app.iam_state.role_tab = RoleTab::Permissions;
16924 app.handle_action(Action::OpenColumnSelector);
16925 assert_eq!(app.mode, Mode::ColumnSelector);
16926
16927 app.mode = Mode::Normal;
16929 app.iam_state.role_tab = RoleTab::Tags;
16930 app.handle_action(Action::OpenColumnSelector);
16931 assert_eq!(app.mode, Mode::ColumnSelector);
16932
16933 app.mode = Mode::Normal;
16935 app.iam_state.current_role = None;
16936 app.handle_action(Action::OpenColumnSelector);
16937 assert_eq!(app.mode, Mode::ColumnSelector);
16938 }
16939
16940 #[test]
16941 fn test_iam_role_tags_tab_cycling() {
16942 let mut app = test_app();
16943 app.current_service = Service::IamRoles;
16944 app.service_selected = true;
16945 app.iam_state.current_role = Some("test-role".to_string());
16946 app.iam_state.role_tab = RoleTab::Tags;
16947 app.mode = Mode::ColumnSelector;
16948 app.column_selector_index = 0;
16949
16950 app.handle_action(Action::NextPreferences);
16952 assert_eq!(app.column_selector_index, 4);
16953
16954 app.handle_action(Action::NextPreferences);
16956 assert_eq!(app.column_selector_index, 0);
16957
16958 app.handle_action(Action::PrevPreferences);
16960 assert_eq!(app.column_selector_index, 4);
16961
16962 app.handle_action(Action::PrevPreferences);
16964 assert_eq!(app.column_selector_index, 0);
16965 }
16966
16967 #[test]
16968 fn test_cfn_outputs_expand_collapse() {
16969 let mut app = test_app();
16970 app.current_service = Service::CloudFormationStacks;
16971 app.service_selected = true;
16972 app.cfn_state.current_stack = Some("test-stack".to_string());
16973 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
16974 app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
16975 key: "Output1".to_string(),
16976 value: "Value1".to_string(),
16977 description: "Description1".to_string(),
16978 export_name: "Export1".to_string(),
16979 }];
16980 app.cfn_state.outputs.reset();
16981
16982 assert_eq!(app.cfn_state.outputs.expanded_item, None);
16983
16984 app.handle_action(Action::NextPane);
16986 assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
16987
16988 app.handle_action(Action::PrevPane);
16990 assert_eq!(app.cfn_state.outputs.expanded_item, None);
16991 }
16992
16993 #[test]
16994 fn test_cfn_outputs_filter_resets_selection() {
16995 let mut app = test_app();
16996 app.current_service = Service::CloudFormationStacks;
16997 app.service_selected = true;
16998 app.cfn_state.current_stack = Some("test-stack".to_string());
16999 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17000 app.cfn_state.outputs.items = vec![
17001 rusticity_core::cfn::StackOutput {
17002 key: "ApiUrl".to_string(),
17003 value: "https://api.example.com".to_string(),
17004 description: "API endpoint".to_string(),
17005 export_name: "MyApiUrl".to_string(),
17006 },
17007 rusticity_core::cfn::StackOutput {
17008 key: "BucketName".to_string(),
17009 value: "my-bucket".to_string(),
17010 description: "S3 bucket".to_string(),
17011 export_name: "MyBucket".to_string(),
17012 },
17013 ];
17014 app.cfn_state.outputs.reset();
17015 app.cfn_state.outputs.selected = 1;
17016
17017 app.handle_action(Action::StartFilter);
17019 assert_eq!(app.mode, Mode::FilterInput);
17020
17021 app.handle_action(Action::FilterInput('A'));
17023 assert_eq!(app.cfn_state.outputs.selected, 0);
17024 assert_eq!(app.cfn_state.outputs.filter, "A");
17025
17026 app.cfn_state.outputs.selected = 1;
17028 app.handle_action(Action::FilterInput('p'));
17029 assert_eq!(app.cfn_state.outputs.selected, 0);
17030
17031 app.cfn_state.outputs.selected = 1;
17033 app.handle_action(Action::FilterBackspace);
17034 assert_eq!(app.cfn_state.outputs.selected, 0);
17035 }
17036
17037 #[test]
17038 fn test_ec2_service_in_picker() {
17039 let app = test_app();
17040 assert!(app.service_picker.services.contains(&"EC2 > Instances"));
17041 }
17042
17043 #[test]
17044 fn test_ec2_state_filter_cycles() {
17045 let mut app = test_app();
17046 app.current_service = Service::Ec2Instances;
17047 app.service_selected = true;
17048 app.mode = Mode::FilterInput;
17049 app.ec2_state.input_focus = EC2_STATE_FILTER;
17050
17051 let initial = app.ec2_state.state_filter;
17052 assert_eq!(initial, Ec2StateFilter::AllStates);
17053
17054 app.handle_action(Action::ToggleFilterCheckbox);
17056 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17057
17058 app.handle_action(Action::ToggleFilterCheckbox);
17059 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
17060
17061 app.handle_action(Action::ToggleFilterCheckbox);
17062 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
17063
17064 app.handle_action(Action::ToggleFilterCheckbox);
17065 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
17066
17067 app.handle_action(Action::ToggleFilterCheckbox);
17068 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
17069
17070 app.handle_action(Action::ToggleFilterCheckbox);
17071 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
17072
17073 app.handle_action(Action::ToggleFilterCheckbox);
17074 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
17075 }
17076
17077 #[test]
17078 fn test_ec2_filter_resets_table() {
17079 let mut app = test_app();
17080 app.current_service = Service::Ec2Instances;
17081 app.service_selected = true;
17082 app.mode = Mode::FilterInput;
17083 app.ec2_state.input_focus = EC2_STATE_FILTER;
17084 app.ec2_state.table.selected = 5;
17085
17086 app.handle_action(Action::ToggleFilterCheckbox);
17087 assert_eq!(app.ec2_state.table.selected, 0);
17088 }
17089
17090 #[test]
17091 fn test_ec2_columns_visible() {
17092 let app = test_app();
17093 assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
17096
17097 #[test]
17098 fn test_ec2_breadcrumbs() {
17099 let mut app = test_app();
17100 app.current_service = Service::Ec2Instances;
17101 app.service_selected = true;
17102 let breadcrumb = app.breadcrumbs();
17103 assert_eq!(breadcrumb, "EC2 > Instances");
17104 }
17105
17106 #[test]
17107 fn test_ec2_console_url() {
17108 let mut app = test_app();
17109 app.current_service = Service::Ec2Instances;
17110 app.service_selected = true;
17111 let url = app.get_console_url();
17112 assert!(url.contains("ec2"));
17113 assert!(url.contains("Instances"));
17114 }
17115
17116 #[test]
17117 fn test_ec2_filter_handling() {
17118 let mut app = test_app();
17119 app.current_service = Service::Ec2Instances;
17120 app.service_selected = true;
17121 app.mode = Mode::FilterInput;
17122
17123 app.handle_action(Action::FilterInput('t'));
17124 app.handle_action(Action::FilterInput('e'));
17125 app.handle_action(Action::FilterInput('s'));
17126 app.handle_action(Action::FilterInput('t'));
17127
17128 assert_eq!(app.ec2_state.table.filter, "test");
17129
17130 app.handle_action(Action::FilterBackspace);
17131 assert_eq!(app.ec2_state.table.filter, "tes");
17132 }
17133
17134 #[test]
17135 fn test_column_selector_page_down_ec2() {
17136 let mut app = test_app();
17137 app.current_service = Service::Ec2Instances;
17138 app.service_selected = true;
17139 app.mode = Mode::ColumnSelector;
17140 app.column_selector_index = 0;
17141
17142 app.handle_action(Action::PageDown);
17143 assert_eq!(app.column_selector_index, 10);
17144
17145 app.handle_action(Action::PageDown);
17146 assert_eq!(app.column_selector_index, 20);
17147 }
17148
17149 #[test]
17150 fn test_column_selector_page_up_ec2() {
17151 let mut app = test_app();
17152 app.current_service = Service::Ec2Instances;
17153 app.service_selected = true;
17154 app.mode = Mode::ColumnSelector;
17155 app.column_selector_index = 30;
17156
17157 app.handle_action(Action::PageUp);
17158 assert_eq!(app.column_selector_index, 20);
17159
17160 app.handle_action(Action::PageUp);
17161 assert_eq!(app.column_selector_index, 10);
17162 }
17163
17164 #[test]
17165 fn test_ec2_state_filter_dropdown_focus() {
17166 let mut app = test_app();
17167 app.current_service = Service::Ec2Instances;
17168 app.service_selected = true;
17169 app.mode = Mode::FilterInput;
17170
17171 app.handle_action(Action::NextFilterFocus);
17173 assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
17174
17175 app.handle_action(Action::ToggleFilterCheckbox);
17178 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17179 }
17180
17181 #[test]
17182 fn test_column_selector_ctrl_d_scrolling() {
17183 let mut app = test_app();
17184 app.current_service = Service::LambdaFunctions;
17185 app.mode = Mode::ColumnSelector;
17186 app.column_selector_index = 0;
17187
17188 app.handle_action(Action::PageDown);
17189 assert_eq!(app.column_selector_index, 10);
17190
17191 let max = app.get_column_selector_max();
17193 app.handle_action(Action::PageDown);
17194 assert_eq!(app.column_selector_index, max);
17195 }
17196
17197 #[test]
17198 fn test_column_selector_ctrl_u_scrolling() {
17199 let mut app = test_app();
17200 app.current_service = Service::CloudFormationStacks;
17201 app.mode = Mode::ColumnSelector;
17202 app.column_selector_index = 25;
17203
17204 app.handle_action(Action::PageUp);
17205 assert_eq!(app.column_selector_index, 15);
17206
17207 app.handle_action(Action::PageUp);
17208 assert_eq!(app.column_selector_index, 5);
17209 }
17210
17211 #[test]
17212 fn test_prev_preferences_lambda() {
17213 let mut app = test_app();
17214 app.current_service = Service::LambdaFunctions;
17215 app.mode = Mode::ColumnSelector;
17216 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
17217 app.column_selector_index = page_size_idx;
17218
17219 app.handle_action(Action::PrevPreferences);
17220 assert_eq!(app.column_selector_index, 0);
17221
17222 app.handle_action(Action::PrevPreferences);
17223 assert_eq!(app.column_selector_index, page_size_idx);
17224 }
17225
17226 #[test]
17227 fn test_prev_preferences_cloudformation() {
17228 let mut app = test_app();
17229 app.current_service = Service::CloudFormationStacks;
17230 app.mode = Mode::ColumnSelector;
17231 let page_size_idx = app.cfn_column_ids.len() + 2;
17232 app.column_selector_index = page_size_idx;
17233
17234 app.handle_action(Action::PrevPreferences);
17235 assert_eq!(app.column_selector_index, 0);
17236
17237 app.handle_action(Action::PrevPreferences);
17238 assert_eq!(app.column_selector_index, page_size_idx);
17239 }
17240
17241 #[test]
17242 fn test_prev_preferences_alarms() {
17243 let mut app = test_app();
17244 app.current_service = Service::CloudWatchAlarms;
17245 app.mode = Mode::ColumnSelector;
17246 app.column_selector_index = 28; app.handle_action(Action::PrevPreferences);
17249 assert_eq!(app.column_selector_index, 22); app.handle_action(Action::PrevPreferences);
17252 assert_eq!(app.column_selector_index, 18); app.handle_action(Action::PrevPreferences);
17255 assert_eq!(app.column_selector_index, 0); app.handle_action(Action::PrevPreferences);
17258 assert_eq!(app.column_selector_index, 28); }
17260
17261 #[test]
17262 fn test_ec2_page_size_in_preferences() {
17263 let mut app = test_app();
17264 app.current_service = Service::Ec2Instances;
17265 app.mode = Mode::ColumnSelector;
17266 app.ec2_state.table.page_size = PageSize::Fifty;
17267
17268 let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
17271 app.handle_action(Action::ToggleColumn);
17272
17273 assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
17274 }
17275
17276 #[test]
17277 fn test_ec2_next_preferences_with_page_size() {
17278 let mut app = test_app();
17279 app.current_service = Service::Ec2Instances;
17280 app.mode = Mode::ColumnSelector;
17281 app.column_selector_index = 0;
17282
17283 let page_size_idx = app.ec2_column_ids.len() + 2;
17284 app.handle_action(Action::NextPreferences);
17285 assert_eq!(app.column_selector_index, page_size_idx);
17286
17287 app.handle_action(Action::NextPreferences);
17288 assert_eq!(app.column_selector_index, 0);
17289 }
17290
17291 #[test]
17292 fn test_ec2_dropdown_next_item() {
17293 let mut app = test_app();
17294 app.current_service = Service::Ec2Instances;
17295 app.mode = Mode::FilterInput;
17296 app.ec2_state.input_focus = EC2_STATE_FILTER;
17297 app.ec2_state.state_filter = Ec2StateFilter::AllStates;
17298
17299 app.handle_action(Action::NextItem);
17300 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17301
17302 app.handle_action(Action::NextItem);
17303 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
17304 }
17305
17306 #[test]
17307 fn test_ec2_dropdown_prev_item() {
17308 let mut app = test_app();
17309 app.current_service = Service::Ec2Instances;
17310 app.mode = Mode::FilterInput;
17311 app.ec2_state.input_focus = EC2_STATE_FILTER;
17312 app.ec2_state.state_filter = Ec2StateFilter::Stopped;
17313
17314 app.handle_action(Action::PrevItem);
17315 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17316
17317 app.handle_action(Action::PrevItem);
17318 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
17319 }
17320
17321 #[test]
17322 fn test_ec2_dropdown_cycles_with_arrows() {
17323 let mut app = test_app();
17324 app.current_service = Service::Ec2Instances;
17325 app.mode = Mode::FilterInput;
17326 app.ec2_state.input_focus = EC2_STATE_FILTER;
17327 app.ec2_state.state_filter = Ec2StateFilter::Stopping;
17328
17329 app.handle_action(Action::NextItem);
17331 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
17332
17333 app.handle_action(Action::PrevItem);
17335 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
17336 }
17337
17338 #[test]
17339 fn test_collapse_row_ec2_instances() {
17340 let mut app = test_app();
17341 app.current_service = Service::Ec2Instances;
17342 app.ec2_state.table.expanded_item = Some(0);
17343
17344 app.handle_action(Action::CollapseRow);
17345 assert_eq!(app.ec2_state.table.expanded_item, None);
17346 }
17347
17348 #[test]
17349 fn test_collapse_row_ec2_tags() {
17350 let mut app = test_app();
17351 app.current_service = Service::Ec2Instances;
17352 app.ec2_state.current_instance = Some("i-123".to_string());
17353 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
17354 app.ec2_state.tags.expanded_item = Some(1);
17355
17356 app.handle_action(Action::CollapseRow);
17357 assert_eq!(app.ec2_state.tags.expanded_item, None);
17358 }
17359
17360 #[test]
17361 fn test_collapse_row_cloudwatch_log_groups() {
17362 let mut app = test_app();
17363 app.current_service = Service::CloudWatchLogGroups;
17364 app.log_groups_state.log_groups.expanded_item = Some(2);
17365
17366 app.handle_action(Action::CollapseRow);
17367 assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
17368 }
17369
17370 #[test]
17371 fn test_collapse_row_cloudwatch_alarms() {
17372 let mut app = test_app();
17373 app.current_service = Service::CloudWatchAlarms;
17374 app.alarms_state.table.expanded_item = Some(0);
17375
17376 app.handle_action(Action::CollapseRow);
17377 assert_eq!(app.alarms_state.table.expanded_item, None);
17378 }
17379
17380 #[test]
17381 fn test_collapse_row_lambda_functions() {
17382 let mut app = test_app();
17383 app.current_service = Service::LambdaFunctions;
17384 app.lambda_state.table.expanded_item = Some(1);
17385
17386 app.handle_action(Action::CollapseRow);
17387 assert_eq!(app.lambda_state.table.expanded_item, None);
17388 }
17389
17390 #[test]
17391 fn test_collapse_row_cfn_stacks() {
17392 let mut app = test_app();
17393 app.current_service = Service::CloudFormationStacks;
17394 app.cfn_state.table.expanded_item = Some(0);
17395
17396 app.handle_action(Action::CollapseRow);
17397 assert_eq!(app.cfn_state.table.expanded_item, None);
17398 }
17399
17400 #[test]
17401 fn test_collapse_row_cfn_resources() {
17402 let mut app = test_app();
17403 app.current_service = Service::CloudFormationStacks;
17404 app.cfn_state.current_stack = Some("test-stack".to_string());
17405 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
17406 app.cfn_state.resources.expanded_item = Some(2);
17407
17408 app.handle_action(Action::CollapseRow);
17409 assert_eq!(app.cfn_state.resources.expanded_item, None);
17410 }
17411
17412 #[test]
17413 fn test_collapse_row_iam_users() {
17414 let mut app = test_app();
17415 app.current_service = Service::IamUsers;
17416 app.iam_state.users.expanded_item = Some(1);
17417
17418 app.handle_action(Action::CollapseRow);
17419 assert_eq!(app.iam_state.users.expanded_item, None);
17420 }
17421
17422 #[test]
17423 fn test_collapse_row_does_nothing_when_not_expanded() {
17424 let mut app = test_app();
17425 app.current_service = Service::Ec2Instances;
17426 app.ec2_state.table.expanded_item = None;
17427
17428 app.handle_action(Action::CollapseRow);
17429 assert_eq!(app.ec2_state.table.expanded_item, None);
17430 }
17431
17432 #[test]
17433 fn test_s3_collapse_expanded_folder_moves_to_parent() {
17434 let mut app = test_app();
17435 app.current_service = Service::S3Buckets;
17436 app.service_selected = true;
17437 app.mode = Mode::Normal;
17438
17439 app.s3_state.buckets.items = vec![S3Bucket {
17441 name: "bucket1".to_string(),
17442 region: "us-east-1".to_string(),
17443 creation_date: "2024-01-01T00:00:00Z".to_string(),
17444 }];
17445
17446 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17448 app.s3_state.bucket_preview.insert(
17449 "bucket1".to_string(),
17450 vec![S3Object {
17451 key: "folder1/".to_string(),
17452 size: 0,
17453 last_modified: "2024-01-01T00:00:00Z".to_string(),
17454 is_prefix: true,
17455 storage_class: String::new(),
17456 }],
17457 );
17458
17459 app.s3_state
17461 .expanded_prefixes
17462 .insert("folder1/".to_string());
17463 app.s3_state.prefix_preview.insert(
17464 "folder1/".to_string(),
17465 vec![S3Object {
17466 key: "folder1/file.txt".to_string(),
17467 size: 0,
17468 last_modified: "2024-01-01T00:00:00Z".to_string(),
17469 is_prefix: false,
17470 storage_class: String::new(),
17471 }],
17472 );
17473
17474 app.s3_state.selected_row = 1;
17476
17477 app.handle_action(Action::PrevPane);
17479
17480 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
17482 assert_eq!(app.s3_state.selected_row, 0);
17484 }
17485
17486 #[test]
17487 fn test_s3_collapse_hierarchy_level_by_level() {
17488 let mut app = test_app();
17489 app.current_service = Service::S3Buckets;
17490 app.service_selected = true;
17491 app.mode = Mode::Normal;
17492
17493 app.s3_state.buckets.items = vec![S3Bucket {
17495 name: "bucket1".to_string(),
17496 region: "us-east-1".to_string(),
17497 creation_date: "2024-01-01T00:00:00Z".to_string(),
17498 }];
17499
17500 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17502 app.s3_state.bucket_preview.insert(
17503 "bucket1".to_string(),
17504 vec![S3Object {
17505 key: "level1/".to_string(),
17506 size: 0,
17507 last_modified: "2024-01-01T00:00:00Z".to_string(),
17508 is_prefix: true,
17509 storage_class: String::new(),
17510 }],
17511 );
17512
17513 app.s3_state.expanded_prefixes.insert("level1/".to_string());
17515 app.s3_state.prefix_preview.insert(
17516 "level1/".to_string(),
17517 vec![S3Object {
17518 key: "level1/level2/".to_string(),
17519 size: 0,
17520 last_modified: "2024-01-01T00:00:00Z".to_string(),
17521 is_prefix: true,
17522 storage_class: String::new(),
17523 }],
17524 );
17525
17526 app.s3_state
17528 .expanded_prefixes
17529 .insert("level1/level2/".to_string());
17530 app.s3_state.prefix_preview.insert(
17531 "level1/level2/".to_string(),
17532 vec![S3Object {
17533 key: "level1/level2/file.txt".to_string(),
17534 size: 100,
17535 last_modified: "2024-01-01T00:00:00Z".to_string(),
17536 is_prefix: false,
17537 storage_class: String::new(),
17538 }],
17539 );
17540
17541 app.s3_state.selected_row = 3;
17543
17544 app.handle_action(Action::PrevPane);
17546 assert_eq!(app.s3_state.selected_row, 2);
17547
17548 app.handle_action(Action::PrevPane);
17550 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
17551 assert_eq!(app.s3_state.selected_row, 1);
17552
17553 app.handle_action(Action::PrevPane);
17555 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
17556 assert_eq!(app.s3_state.selected_row, 0);
17557
17558 app.handle_action(Action::PrevPane);
17560 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
17561 assert_eq!(app.s3_state.selected_row, 0);
17562 }
17563
17564 #[test]
17565 fn test_ec2_instance_detail_tabs_no_preferences() {
17566 let mut app = test_app();
17567 app.current_service = Service::Ec2Instances;
17568 app.service_selected = true;
17569 app.ec2_state.table.expanded_item = Some(0);
17570 app.mode = Mode::Normal;
17571
17572 app.ec2_state.detail_tab = Ec2DetailTab::Details;
17574 app.handle_action(Action::OpenColumnSelector);
17575 assert_eq!(app.mode, Mode::Normal);
17576
17577 app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
17579 app.handle_action(Action::OpenColumnSelector);
17580 assert_eq!(app.mode, Mode::Normal);
17581
17582 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
17584 app.handle_action(Action::OpenColumnSelector);
17585 assert_eq!(app.mode, Mode::Normal);
17586
17587 app.ec2_state.detail_tab = Ec2DetailTab::Security;
17589 app.handle_action(Action::OpenColumnSelector);
17590 assert_eq!(app.mode, Mode::Normal);
17591
17592 app.ec2_state.detail_tab = Ec2DetailTab::Networking;
17594 app.handle_action(Action::OpenColumnSelector);
17595 assert_eq!(app.mode, Mode::Normal);
17596
17597 app.ec2_state.detail_tab = Ec2DetailTab::Storage;
17599 app.handle_action(Action::OpenColumnSelector);
17600 assert_eq!(app.mode, Mode::Normal);
17601
17602 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
17604 app.handle_action(Action::OpenColumnSelector);
17605 assert_eq!(app.mode, Mode::ColumnSelector);
17606 }
17607
17608 #[test]
17609 fn test_log_streams_filter_only_updates_when_focused() {
17610 let mut app = test_app();
17611 app.current_service = Service::CloudWatchLogGroups;
17612 app.service_selected = true;
17613 app.view_mode = ViewMode::Detail;
17614 app.mode = Mode::FilterInput;
17615 app.log_groups_state.stream_filter = "test".to_string();
17616
17617 app.log_groups_state.input_focus = InputFocus::Filter;
17619 app.handle_action(Action::FilterInput('x'));
17620 assert_eq!(app.log_groups_state.stream_filter, "testx");
17621
17622 app.log_groups_state.input_focus = InputFocus::Pagination;
17624 app.handle_action(Action::FilterInput('y'));
17625 assert_eq!(app.log_groups_state.stream_filter, "testx"); }
17627
17628 #[test]
17629 fn test_log_streams_backspace_only_updates_when_focused() {
17630 let mut app = test_app();
17631 app.current_service = Service::CloudWatchLogGroups;
17632 app.service_selected = true;
17633 app.view_mode = ViewMode::Detail;
17634 app.mode = Mode::FilterInput;
17635 app.log_groups_state.stream_filter = "test".to_string();
17636
17637 app.log_groups_state.input_focus = InputFocus::Filter;
17639 app.handle_action(Action::FilterBackspace);
17640 assert_eq!(app.log_groups_state.stream_filter, "tes");
17641
17642 app.log_groups_state.input_focus = InputFocus::Pagination;
17644 app.handle_action(Action::FilterBackspace);
17645 assert_eq!(app.log_groups_state.stream_filter, "tes"); }
17647
17648 #[test]
17649 fn test_log_groups_filter_only_updates_when_focused() {
17650 let mut app = test_app();
17651 app.current_service = Service::CloudWatchLogGroups;
17652 app.service_selected = true;
17653 app.view_mode = ViewMode::List;
17654 app.mode = Mode::FilterInput;
17655 app.log_groups_state.log_groups.filter = "test".to_string();
17656
17657 app.log_groups_state.input_focus = InputFocus::Filter;
17659 app.handle_action(Action::FilterInput('x'));
17660 assert_eq!(app.log_groups_state.log_groups.filter, "testx");
17661
17662 app.log_groups_state.input_focus = InputFocus::Pagination;
17664 app.handle_action(Action::FilterInput('y'));
17665 assert_eq!(app.log_groups_state.log_groups.filter, "testx"); }
17667
17668 #[test]
17669 fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
17670 use S3Bucket;
17671 use S3Object;
17672
17673 let mut app = test_app();
17674 app.current_service = Service::S3Buckets;
17675 app.service_selected = true;
17676
17677 app.s3_state.buckets.items = vec![S3Bucket {
17679 name: "test-bucket".to_string(),
17680 region: "us-east-1".to_string(),
17681 creation_date: String::new(),
17682 }];
17683
17684 app.s3_state
17686 .expanded_prefixes
17687 .insert("test-bucket".to_string());
17688 app.s3_state.bucket_preview.insert(
17689 "test-bucket".to_string(),
17690 vec![S3Object {
17691 key: "folder1/".to_string(),
17692 is_prefix: true,
17693 size: 0,
17694 last_modified: String::new(),
17695 storage_class: String::new(),
17696 }],
17697 );
17698
17699 app.s3_state
17701 .expanded_prefixes
17702 .insert("folder1/".to_string());
17703 app.s3_state.prefix_preview.insert(
17704 "folder1/".to_string(),
17705 vec![S3Object {
17706 key: "folder1/folder2/".to_string(),
17707 is_prefix: true,
17708 size: 0,
17709 last_modified: String::new(),
17710 storage_class: String::new(),
17711 }],
17712 );
17713
17714 app.s3_state.selected_row = 2;
17716
17717 app.handle_action(Action::CollapseRow);
17719
17720 assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
17722 assert_eq!(app.s3_state.selected_row, 1);
17724 }
17725
17726 #[test]
17727 fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
17728 use S3Bucket;
17729 use S3Object;
17730
17731 let mut app = test_app();
17732 app.current_service = Service::S3Buckets;
17733 app.service_selected = true;
17734
17735 app.s3_state.buckets.items = vec![S3Bucket {
17737 name: "test-bucket".to_string(),
17738 region: "us-east-1".to_string(),
17739 creation_date: String::new(),
17740 }];
17741
17742 app.s3_state
17744 .expanded_prefixes
17745 .insert("test-bucket".to_string());
17746 app.s3_state.bucket_preview.insert(
17747 "test-bucket".to_string(),
17748 vec![S3Object {
17749 key: "folder1/".to_string(),
17750 is_prefix: true,
17751 size: 0,
17752 last_modified: String::new(),
17753 storage_class: String::new(),
17754 }],
17755 );
17756
17757 app.s3_state
17759 .expanded_prefixes
17760 .insert("folder1/".to_string());
17761 app.s3_state.prefix_preview.insert(
17762 "folder1/".to_string(),
17763 vec![S3Object {
17764 key: "folder1/file.txt".to_string(),
17765 is_prefix: false,
17766 size: 100,
17767 last_modified: String::new(),
17768 storage_class: String::new(),
17769 }],
17770 );
17771
17772 app.s3_state.selected_row = 1;
17774
17775 app.handle_action(Action::CollapseRow);
17777
17778 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
17780 assert_eq!(app.s3_state.selected_row, 0);
17782 }
17783
17784 #[test]
17785 fn test_log_streams_pagination_limits_table_content() {
17786 let mut app = test_app();
17787 app.current_service = Service::CloudWatchLogGroups;
17788 app.service_selected = true;
17789 app.view_mode = ViewMode::Detail;
17790
17791 app.log_groups_state.log_streams = (0..50)
17793 .map(|i| rusticity_core::LogStream {
17794 name: format!("stream-{}", i),
17795 creation_time: None,
17796 last_event_time: None,
17797 })
17798 .collect();
17799
17800 app.log_groups_state.stream_page_size = 10;
17802 app.log_groups_state.stream_current_page = 0;
17803
17804 assert_eq!(app.log_groups_state.stream_page_size, 10);
17807 assert_eq!(app.log_groups_state.stream_current_page, 0);
17808
17809 app.log_groups_state.stream_current_page = 1;
17811 assert_eq!(app.log_groups_state.stream_current_page, 1);
17812 }
17813
17814 #[test]
17815 fn test_log_streams_page_size_change_resets_page() {
17816 let mut app = test_app();
17817 app.current_service = Service::CloudWatchLogGroups;
17818 app.service_selected = true;
17819 app.view_mode = ViewMode::Detail;
17820 app.mode = Mode::ColumnSelector;
17821
17822 app.log_groups_state.stream_page_size = 10;
17823 app.log_groups_state.stream_current_page = 3;
17824
17825 app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; app.handle_action(Action::ToggleColumn);
17828
17829 assert_eq!(app.log_groups_state.stream_page_size, 25);
17830 assert_eq!(app.log_groups_state.stream_current_page, 0);
17831 }
17832
17833 #[test]
17834 fn test_s3_objects_expanded_rows_stay_visible() {
17835 use S3Object;
17836
17837 let mut app = test_app();
17838 app.current_service = Service::S3Buckets;
17839 app.service_selected = true;
17840 app.mode = Mode::Normal;
17841 app.s3_state.current_bucket = Some("test-bucket".to_string());
17842
17843 app.s3_state.objects = vec![S3Object {
17845 key: "folder1/".to_string(),
17846 is_prefix: true,
17847 size: 0,
17848 last_modified: String::new(),
17849 storage_class: String::new(),
17850 }];
17851
17852 app.s3_state
17854 .expanded_prefixes
17855 .insert("folder1/".to_string());
17856 app.s3_state.prefix_preview.insert(
17857 "folder1/".to_string(),
17858 (0..20)
17859 .map(|i| S3Object {
17860 key: format!("folder1/file{}.txt", i),
17861 is_prefix: false,
17862 size: 100,
17863 last_modified: String::new(),
17864 storage_class: String::new(),
17865 })
17866 .collect(),
17867 );
17868
17869 app.s3_state.object_visible_rows.set(10);
17871 app.s3_state.object_scroll_offset = 0;
17872 app.s3_state.selected_object = 0; for i in 1..=20 {
17876 app.handle_action(Action::NextItem);
17877 assert_eq!(app.s3_state.selected_object, i);
17878
17879 let visible_start = app.s3_state.object_scroll_offset;
17881 let visible_end = visible_start + app.s3_state.object_visible_rows.get();
17882 assert!(
17883 app.s3_state.selected_object >= visible_start
17884 && app.s3_state.selected_object < visible_end,
17885 "Selection {} should be visible in range [{}, {})",
17886 app.s3_state.selected_object,
17887 visible_start,
17888 visible_end
17889 );
17890 }
17891 }
17892
17893 #[test]
17894 fn test_s3_bucket_error_rows_counted_in_total() {
17895 use S3Bucket;
17896
17897 let mut app = test_app();
17898 app.current_service = Service::S3Buckets;
17899 app.service_selected = true;
17900
17901 app.s3_state.buckets.items = vec![
17903 S3Bucket {
17904 name: "bucket1".to_string(),
17905 region: "us-east-1".to_string(),
17906 creation_date: String::new(),
17907 },
17908 S3Bucket {
17909 name: "bucket2".to_string(),
17910 region: "us-east-1".to_string(),
17911 creation_date: String::new(),
17912 },
17913 ];
17914
17915 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17917 let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG, s3_extended_request_id: CGSwddO9ummjFYFHKyqNEU= }".to_string();
17918 app.s3_state
17919 .bucket_errors
17920 .insert("bucket1".to_string(), long_error.clone());
17921
17922 let total = app.calculate_total_bucket_rows();
17924
17925 let error_rows = long_error.len().div_ceil(120);
17927 assert_eq!(total, 2 + error_rows);
17928 }
17929
17930 #[test]
17931 fn test_s3_bucket_with_error_can_be_collapsed() {
17932 use S3Bucket;
17933
17934 let mut app = test_app();
17935 app.current_service = Service::S3Buckets;
17936 app.service_selected = true;
17937 app.mode = Mode::Normal;
17938
17939 app.s3_state.buckets.items = vec![S3Bucket {
17941 name: "bucket1".to_string(),
17942 region: "us-east-1".to_string(),
17943 creation_date: String::new(),
17944 }];
17945
17946 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17948 let error = "service error: PermanentRedirect".to_string();
17949 app.s3_state
17950 .bucket_errors
17951 .insert("bucket1".to_string(), error);
17952
17953 app.s3_state.selected_row = 0;
17955
17956 app.handle_action(Action::CollapseRow);
17958
17959 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
17961 assert_eq!(app.s3_state.selected_row, 0);
17963 }
17964
17965 #[test]
17966 fn test_s3_bucket_collapse_on_bucket_row() {
17967 use S3Bucket;
17968
17969 let mut app = test_app();
17970 app.current_service = Service::S3Buckets;
17971 app.service_selected = true;
17972 app.mode = Mode::Normal;
17973
17974 app.s3_state.buckets.items = vec![S3Bucket {
17976 name: "bucket1".to_string(),
17977 region: "us-east-1".to_string(),
17978 creation_date: String::new(),
17979 }];
17980
17981 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17983 let error = "service error: PermanentRedirect".to_string();
17984 app.s3_state
17985 .bucket_errors
17986 .insert("bucket1".to_string(), error);
17987
17988 app.s3_state.selected_row = 0;
17990
17991 app.handle_action(Action::CollapseRow);
17993
17994 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
17996 assert_eq!(app.s3_state.selected_row, 0);
17998 }
17999
18000 #[test]
18001 fn test_s3_bucket_collapse_adjusts_scroll_offset() {
18002 use S3Bucket;
18003
18004 let mut app = test_app();
18005 app.current_service = Service::S3Buckets;
18006 app.service_selected = true;
18007 app.mode = Mode::Normal;
18008
18009 app.s3_state.buckets.items = (0..20)
18011 .map(|i| S3Bucket {
18012 name: format!("bucket{}", i),
18013 region: "us-east-1".to_string(),
18014 creation_date: String::new(),
18015 })
18016 .collect();
18017
18018 app.s3_state
18020 .expanded_prefixes
18021 .insert("bucket10".to_string());
18022 let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG }".to_string();
18023 app.s3_state
18024 .bucket_errors
18025 .insert("bucket10".to_string(), long_error.clone());
18026
18027 app.s3_state.bucket_visible_rows.set(10);
18029 app.s3_state.bucket_scroll_offset = 10; app.s3_state.selected_row = 10;
18033
18034 app.handle_action(Action::CollapseRow);
18036
18037 assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
18039 assert_eq!(app.s3_state.selected_row, 10);
18041 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
18043 assert!(
18044 app.s3_state.selected_row
18045 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
18046 );
18047 }
18048
18049 #[test]
18050 fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
18051 use S3Bucket;
18052
18053 let mut app = test_app();
18054 app.current_service = Service::S3Buckets;
18055 app.service_selected = true;
18056 app.mode = Mode::Normal;
18057
18058 app.s3_state.buckets.items = vec![
18060 S3Bucket {
18061 name: "bucket1".to_string(),
18062 region: "us-east-1".to_string(),
18063 creation_date: String::new(),
18064 },
18065 S3Bucket {
18066 name: "bucket2".to_string(),
18067 region: "us-east-1".to_string(),
18068 creation_date: String::new(),
18069 },
18070 S3Bucket {
18071 name: "bucket3".to_string(),
18072 region: "us-east-1".to_string(),
18073 creation_date: String::new(),
18074 },
18075 ];
18076
18077 app.s3_state.expanded_prefixes.insert("bucket2".to_string());
18079 app.s3_state.bucket_preview.insert(
18080 "bucket2".to_string(),
18081 vec![
18082 S3Object {
18083 key: "folder1/".to_string(),
18084 is_prefix: true,
18085 size: 0,
18086 last_modified: String::new(),
18087 storage_class: String::new(),
18088 },
18089 S3Object {
18090 key: "file1.txt".to_string(),
18091 is_prefix: false,
18092 size: 100,
18093 last_modified: String::new(),
18094 storage_class: String::new(),
18095 },
18096 ],
18097 );
18098
18099 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
18101 let error = "service error: PermanentRedirect".to_string();
18102 app.s3_state
18103 .bucket_errors
18104 .insert("bucket3".to_string(), error);
18105
18106 app.s3_state.bucket_visible_rows.set(10);
18108 app.s3_state.bucket_scroll_offset = 0;
18109
18110 app.s3_state.selected_row = 3;
18112
18113 app.handle_action(Action::CollapseRow);
18115
18116 assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
18118 assert_eq!(app.s3_state.selected_row, 1);
18120 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
18122 assert!(
18123 app.s3_state.selected_row
18124 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
18125 );
18126 }
18127
18128 #[test]
18129 fn test_s3_collapse_bucket_with_error() {
18130 use S3Bucket;
18131
18132 let mut app = test_app();
18133 app.current_service = Service::S3Buckets;
18134 app.service_selected = true;
18135 app.mode = Mode::Normal;
18136
18137 app.s3_state.buckets.items = vec![
18138 S3Bucket {
18139 name: "bucket1".to_string(),
18140 region: "us-east-1".to_string(),
18141 creation_date: String::new(),
18142 },
18143 S3Bucket {
18144 name: "bucket2".to_string(),
18145 region: "us-east-1".to_string(),
18146 creation_date: String::new(),
18147 },
18148 ];
18149
18150 let error = "service error: unhandled error (PermanentRedirect)".to_string();
18151 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18152 app.s3_state
18153 .bucket_errors
18154 .insert("bucket1".to_string(), error);
18155
18156 app.s3_state.bucket_visible_rows.set(10);
18157 app.s3_state.bucket_scroll_offset = 0;
18158
18159 app.s3_state.selected_row = 0;
18161
18162 app.handle_action(Action::CollapseRow);
18164
18165 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18167 assert_eq!(app.s3_state.selected_row, 0);
18168 }
18169
18170 #[test]
18171 fn test_s3_collapse_row_with_multiple_error_buckets() {
18172 use S3Bucket;
18173
18174 let mut app = test_app();
18175 app.current_service = Service::S3Buckets;
18176 app.service_selected = true;
18177 app.mode = Mode::Normal;
18178
18179 app.s3_state.buckets.items = vec![
18180 S3Bucket {
18181 name: "bucket1".to_string(),
18182 region: "us-east-1".to_string(),
18183 creation_date: String::new(),
18184 },
18185 S3Bucket {
18186 name: "bucket2".to_string(),
18187 region: "us-east-1".to_string(),
18188 creation_date: String::new(),
18189 },
18190 S3Bucket {
18191 name: "bucket3".to_string(),
18192 region: "us-east-1".to_string(),
18193 creation_date: String::new(),
18194 },
18195 ];
18196
18197 let error = "service error: unhandled error (PermanentRedirect)".to_string();
18198
18199 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18201 app.s3_state
18202 .bucket_errors
18203 .insert("bucket1".to_string(), error.clone());
18204
18205 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
18207 app.s3_state
18208 .bucket_errors
18209 .insert("bucket3".to_string(), error.clone());
18210
18211 app.s3_state.bucket_visible_rows.set(30);
18212 app.s3_state.bucket_scroll_offset = 0;
18213
18214 app.s3_state.selected_row = 2;
18219
18220 app.handle_action(Action::CollapseRow);
18221
18222 assert!(
18224 !app.s3_state.expanded_prefixes.contains("bucket3"),
18225 "bucket3 should be collapsed"
18226 );
18227 assert!(
18228 app.s3_state.expanded_prefixes.contains("bucket1"),
18229 "bucket1 should still be expanded"
18230 );
18231 assert_eq!(app.s3_state.selected_row, 2);
18232 }
18233
18234 #[test]
18235 fn test_s3_collapse_row_nested_only_collapses_one_level() {
18236 use S3Bucket;
18237
18238 let mut app = test_app();
18239 app.current_service = Service::S3Buckets;
18240 app.service_selected = true;
18241 app.mode = Mode::Normal;
18242
18243 app.s3_state.buckets.items = vec![S3Bucket {
18244 name: "bucket1".to_string(),
18245 region: "us-east-1".to_string(),
18246 creation_date: String::new(),
18247 }];
18248
18249 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18251 app.s3_state.bucket_preview.insert(
18252 "bucket1".to_string(),
18253 vec![S3Object {
18254 key: "level1/".to_string(),
18255 size: 0,
18256 last_modified: "2024-01-01T00:00:00Z".to_string(),
18257 is_prefix: true,
18258 storage_class: String::new(),
18259 }],
18260 );
18261
18262 app.s3_state.expanded_prefixes.insert("level1/".to_string());
18264 app.s3_state.prefix_preview.insert(
18265 "level1/".to_string(),
18266 vec![S3Object {
18267 key: "level1/level2/".to_string(),
18268 size: 0,
18269 last_modified: "2024-01-01T00:00:00Z".to_string(),
18270 is_prefix: true,
18271 storage_class: String::new(),
18272 }],
18273 );
18274
18275 app.s3_state
18277 .expanded_prefixes
18278 .insert("level1/level2/".to_string());
18279 app.s3_state.prefix_preview.insert(
18280 "level1/level2/".to_string(),
18281 vec![S3Object {
18282 key: "level1/level2/file.txt".to_string(),
18283 size: 100,
18284 last_modified: "2024-01-01T00:00:00Z".to_string(),
18285 is_prefix: false,
18286 storage_class: String::new(),
18287 }],
18288 );
18289
18290 app.s3_state.bucket_visible_rows.set(10);
18291
18292 app.s3_state.selected_row = 2;
18294
18295 app.handle_action(Action::CollapseRow);
18297
18298 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
18300 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
18302 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
18304 assert_eq!(app.s3_state.selected_row, 1);
18306 }
18307
18308 #[test]
18309 fn test_s3_collapse_row_deeply_nested_file() {
18310 use S3Bucket;
18311
18312 let mut app = test_app();
18313 app.current_service = Service::S3Buckets;
18314 app.service_selected = true;
18315 app.mode = Mode::Normal;
18316
18317 app.s3_state.buckets.items = vec![S3Bucket {
18318 name: "bucket1".to_string(),
18319 region: "us-east-1".to_string(),
18320 creation_date: String::new(),
18321 }];
18322
18323 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18325 app.s3_state.bucket_preview.insert(
18326 "bucket1".to_string(),
18327 vec![S3Object {
18328 key: "level1/".to_string(),
18329 size: 0,
18330 last_modified: "2024-01-01T00:00:00Z".to_string(),
18331 is_prefix: true,
18332 storage_class: String::new(),
18333 }],
18334 );
18335
18336 app.s3_state.expanded_prefixes.insert("level1/".to_string());
18338 app.s3_state.prefix_preview.insert(
18339 "level1/".to_string(),
18340 vec![S3Object {
18341 key: "level1/level2/".to_string(),
18342 size: 0,
18343 last_modified: "2024-01-01T00:00:00Z".to_string(),
18344 is_prefix: true,
18345 storage_class: String::new(),
18346 }],
18347 );
18348
18349 app.s3_state
18351 .expanded_prefixes
18352 .insert("level1/level2/".to_string());
18353 app.s3_state.prefix_preview.insert(
18354 "level1/level2/".to_string(),
18355 vec![S3Object {
18356 key: "level1/level2/file.txt".to_string(),
18357 size: 100,
18358 last_modified: "2024-01-01T00:00:00Z".to_string(),
18359 is_prefix: false,
18360 storage_class: String::new(),
18361 }],
18362 );
18363
18364 app.s3_state.bucket_visible_rows.set(10);
18365
18366 app.s3_state.selected_row = 3;
18368
18369 app.handle_action(Action::CollapseRow);
18371
18372 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
18374 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
18375 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
18376 assert_eq!(app.s3_state.selected_row, 2);
18378 }
18379
18380 #[test]
18381 fn test_s3_bucket_pagination_adjusts_scroll() {
18382 use S3Bucket;
18383
18384 let mut app = test_app();
18385 app.current_service = Service::S3Buckets;
18386 app.service_selected = true;
18387 app.mode = Mode::Normal;
18388
18389 app.s3_state.buckets.items = (0..150)
18391 .map(|i| S3Bucket {
18392 name: format!("bucket{:03}", i),
18393 region: "us-east-1".to_string(),
18394 creation_date: String::new(),
18395 })
18396 .collect();
18397
18398 app.s3_state.bucket_visible_rows.set(20);
18399 app.s3_state.selected_row = 0;
18400 app.s3_state.bucket_scroll_offset = 0;
18401
18402 app.go_to_page(2);
18404
18405 assert_eq!(app.s3_state.selected_row, 50);
18406 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
18408
18409 app.go_to_page(3);
18411
18412 assert_eq!(app.s3_state.selected_row, 100);
18413 assert_eq!(app.s3_state.bucket_scroll_offset, 100);
18414
18415 app.go_to_page(1);
18417
18418 assert_eq!(app.s3_state.selected_row, 0);
18419 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
18420 }
18421
18422 #[test]
18423 fn test_s3_bucket_pagination_uses_page_size() {
18424 use S3Bucket;
18425
18426 let mut app = test_app();
18427 app.current_service = Service::S3Buckets;
18428 app.service_selected = true;
18429 app.mode = Mode::Normal;
18430
18431 app.s3_state.buckets.items = (0..100)
18433 .map(|i| S3Bucket {
18434 name: format!("bucket{:03}", i),
18435 region: "us-east-1".to_string(),
18436 creation_date: String::new(),
18437 })
18438 .collect();
18439
18440 app.s3_state.bucket_visible_rows.set(20);
18441 app.s3_state.selected_row = 0;
18442
18443 assert_eq!(app.s3_state.buckets.page_size.value(), 50);
18445
18446 app.go_to_page(2);
18448 assert_eq!(app.s3_state.selected_row, 50);
18449 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
18450
18451 app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
18453 assert_eq!(app.s3_state.buckets.page_size.value(), 25);
18454
18455 app.go_to_page(2);
18457 assert_eq!(app.s3_state.selected_row, 25);
18458 assert_eq!(app.s3_state.bucket_scroll_offset, 25);
18459 }
18460
18461 #[test]
18462 fn test_s3_bucket_page_size_limits_visible_rows() {
18463 use S3Bucket;
18464
18465 let mut app = test_app();
18466 app.current_service = Service::S3Buckets;
18467 app.service_selected = true;
18468 app.mode = Mode::Normal;
18469
18470 app.s3_state.buckets.items = (0..100)
18472 .map(|i| S3Bucket {
18473 name: format!("bucket{:03}", i),
18474 region: "us-east-1".to_string(),
18475 creation_date: String::new(),
18476 })
18477 .collect();
18478
18479 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
18481 assert_eq!(app.s3_state.buckets.page_size.value(), 10);
18482
18483 let total_rows = app.calculate_total_bucket_rows();
18485 assert!(total_rows >= 10, "Should have at least 10 rows");
18489 }
18490
18491 #[test]
18492 fn test_s3_bucket_tab_cycling_in_filter() {
18493 use crate::common::InputFocus;
18494
18495 let mut app = test_app();
18496 app.current_service = Service::S3Buckets;
18497 app.mode = Mode::FilterInput;
18498
18499 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
18501
18502 app.handle_action(Action::NextFilterFocus);
18504 assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
18505
18506 app.handle_action(Action::NextFilterFocus);
18508 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
18509 }
18510
18511 #[test]
18512 fn test_s3_bucket_pagination_navigation_with_arrows() {
18513 use S3Bucket;
18514
18515 let mut app = test_app();
18516 app.current_service = Service::S3Buckets;
18517 app.mode = Mode::FilterInput;
18518
18519 app.s3_state.buckets.items = (0..100)
18521 .map(|i| S3Bucket {
18522 name: format!("bucket{:03}", i),
18523 region: "us-east-1".to_string(),
18524 creation_date: String::new(),
18525 })
18526 .collect();
18527
18528 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
18529 app.s3_state.selected_row = 0;
18530
18531 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
18533
18534 app.handle_action(Action::NextItem);
18536 assert_eq!(app.s3_state.selected_row, 10);
18537
18538 app.handle_action(Action::NextItem);
18540 assert_eq!(app.s3_state.selected_row, 20);
18541
18542 app.handle_action(Action::PrevItem);
18544 assert_eq!(app.s3_state.selected_row, 10);
18545 }
18546
18547 #[test]
18548 fn test_s3_bucket_go_to_page_shows_correct_buckets() {
18549 use S3Bucket;
18550
18551 let mut app = test_app();
18552 app.current_service = Service::S3Buckets;
18553 app.service_selected = true;
18554 app.mode = Mode::Normal;
18555
18556 app.s3_state.buckets.items = (0..100)
18558 .map(|i| S3Bucket {
18559 name: format!("bucket{:03}", i),
18560 region: "us-east-1".to_string(),
18561 creation_date: String::new(),
18562 })
18563 .collect();
18564
18565 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
18566
18567 app.go_to_page(2);
18569 assert_eq!(app.s3_state.selected_row, 10);
18570
18571 app.go_to_page(5);
18573 assert_eq!(app.s3_state.selected_row, 40);
18574 }
18575
18576 #[test]
18577 fn test_s3_bucket_left_right_arrows_change_pages() {
18578 use S3Bucket;
18579
18580 let mut app = test_app();
18581 app.current_service = Service::S3Buckets;
18582 app.mode = Mode::FilterInput;
18583
18584 app.s3_state.buckets.items = (0..100)
18586 .map(|i| S3Bucket {
18587 name: format!("bucket{:03}", i),
18588 region: "us-east-1".to_string(),
18589 creation_date: String::new(),
18590 })
18591 .collect();
18592
18593 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
18594 app.s3_state.selected_row = 0;
18595 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
18596
18597 app.handle_action(Action::PageDown);
18599 assert_eq!(app.s3_state.selected_row, 10);
18600
18601 app.handle_action(Action::PageDown);
18603 assert_eq!(app.s3_state.selected_row, 20);
18604
18605 app.handle_action(Action::PageUp);
18607 assert_eq!(app.s3_state.selected_row, 10);
18608 }
18609}