1use crate::apig::api::Column as ApigColumn;
2use crate::apig::resource::Column as ResourceColumn;
3use crate::apig::resource::Resource as ApigResource;
4use crate::apig::route::Column as RouteColumn;
5use crate::apig::route::Route;
6pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
7use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
8use crate::cloudtrail::{CloudTrailEvent, CloudTrailEventColumn, EventResourceColumn};
9use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
10pub use crate::cw::insights::InsightsFocus;
11use crate::cw::insights::InsightsState;
12pub use crate::cw::{Alarm, AlarmColumn};
13pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
14use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
15use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
16use crate::iam::{
17 self, GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag,
18 UserColumn, UserTag as IamUserTag,
19};
20#[cfg(test)]
21use crate::iam::{IamRole, IamUser, LastAccessedService};
22use crate::keymap::{Action, Mode};
23pub use crate::lambda::{
24 Alias as LambdaAlias, Application as LambdaApplication,
25 ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
26 Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
27 ResourceColumn as LambdaResourceColumn, Version as LambdaVersion,
28};
29pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
30use crate::session::{Session, SessionTab};
31pub use crate::sqs::queue::Column as SqsColumn;
32pub use crate::sqs::trigger::Column as SqsTriggerColumn;
33use crate::sqs::{console_url_queue_detail, console_url_queues};
34#[cfg(test)]
35use crate::sqs::{
36 EventBridgePipe, LambdaTrigger, Queue as SqsQueue, QueueTag as SqsQueueTag, SnsSubscription,
37};
38use crate::table::TableState;
39pub use crate::ui::apig::{
40 filtered_apis, State as ApigState, FILTER_CONTROLS as APIG_FILTER_CONTROLS,
41};
42use crate::ui::cfn::State as CfnStateConstants;
43pub use crate::ui::cfn::{
44 filtered_cloudformation_stacks, filtered_outputs, filtered_parameters, filtered_resources,
45 output_column_ids, parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
46 State as CfnState, StatusFilter as CfnStatusFilter,
47};
48pub use crate::ui::cw::alarms::{
49 AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
50};
51pub use crate::ui::cw::logs::{
52 filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
53 DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
54};
55use crate::ui::ec2;
56use crate::ui::ec2::filtered_ec2_instances;
57pub use crate::ui::ec2::{
58 DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
59 STATE_FILTER as EC2_STATE_FILTER,
60};
61pub use crate::ui::ecr::{
62 filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
63 FILTER_CONTROLS as ECR_FILTER_CONTROLS,
64};
65use crate::ui::iam::{
66 filtered_iam_policies, filtered_iam_roles, filtered_iam_users, filtered_last_accessed,
67 filtered_tags as filtered_iam_tags, filtered_user_tags, GroupTab, RoleTab, State as IamState,
68 UserTab,
69};
70pub use crate::ui::lambda::{
71 filtered_lambda_applications, filtered_lambda_functions,
72 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
73 DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
74};
75use crate::ui::monitoring::MonitoringState;
76pub use crate::ui::s3::{
77 calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
78 ObjectTab as S3ObjectTab, State as S3State,
79};
80pub use crate::ui::sqs::{
81 extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
82 filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
83 State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
84 SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
85};
86use crate::ui::tree::TreeItem;
87pub use crate::ui::{
88 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
89 StreamColumn, StreamSort, TimeUnit,
90};
91#[cfg(test)]
92use rusticity_core::LogStream;
93use rusticity_core::{
94 AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
95 CloudWatchClient, Ec2Client, EcrClient, IamClient, LambdaClient, S3Client, SqsClient,
96};
97use std::collections::HashMap;
98
99#[derive(Clone)]
100pub struct Tab {
101 pub service: Service,
102 pub title: String,
103 pub breadcrumb: String,
104}
105
106pub struct App {
107 pub running: bool,
108 pub mode: Mode,
109 pub config: AwsConfig,
110 pub cloudwatch_client: CloudWatchClient,
111 pub cloudtrail_client: CloudTrailClient,
112 pub s3_client: S3Client,
113 pub sqs_client: SqsClient,
114 pub alarms_client: AlarmsClient,
115 pub ec2_client: Ec2Client,
116 pub ecr_client: EcrClient,
117 pub apig_client: ApiGatewayClient,
118 pub iam_client: IamClient,
119 pub lambda_client: LambdaClient,
120 pub cloudformation_client: CloudFormationClient,
121 pub current_service: Service,
122 pub tabs: Vec<Tab>,
123 pub current_tab: usize,
124 pub tab_picker_selected: usize,
125 pub tab_filter: String,
126 pub pending_key: Option<char>,
127 pub log_groups_state: CloudWatchLogGroupsState,
128 pub insights_state: CloudWatchInsightsState,
129 pub alarms_state: CloudWatchAlarmsState,
130 pub cloudtrail_state: CloudTrailState,
131 pub s3_state: S3State,
132 pub sqs_state: SqsState,
133 pub ec2_state: Ec2State,
134 pub ecr_state: EcrState,
135 pub apig_state: ApigState,
136 pub lambda_state: LambdaState,
137 pub lambda_application_state: LambdaApplicationState,
138 pub cfn_state: CfnState,
139 pub iam_state: IamState,
140 pub service_picker: ServicePickerState,
141 pub service_selected: bool,
142 pub profile: String,
143 pub region: String,
144 pub region_selector_index: usize,
145 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
146 pub cw_log_group_column_ids: Vec<ColumnId>,
147 pub column_selector_index: usize,
148 pub preference_section: Preferences,
149 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
150 pub cw_log_stream_column_ids: Vec<ColumnId>,
151 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
152 pub cw_log_event_column_ids: Vec<ColumnId>,
153 pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
154 pub cw_log_tag_column_ids: Vec<ColumnId>,
155 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
156 pub cw_alarm_column_ids: Vec<ColumnId>,
157 pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
158 pub cloudtrail_event_column_ids: Vec<ColumnId>,
159 pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
160 pub cloudtrail_resource_column_ids: Vec<ColumnId>,
161 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
162 pub s3_bucket_column_ids: Vec<ColumnId>,
163 pub sqs_visible_column_ids: Vec<ColumnId>,
164 pub sqs_column_ids: Vec<ColumnId>,
165 pub ec2_visible_column_ids: Vec<ColumnId>,
166 pub ec2_column_ids: Vec<ColumnId>,
167 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
168 pub ecr_repo_column_ids: Vec<ColumnId>,
169 pub ecr_image_visible_column_ids: Vec<ColumnId>,
170 pub ecr_image_column_ids: Vec<ColumnId>,
171 pub apig_api_visible_column_ids: Vec<ColumnId>,
172 pub apig_api_column_ids: Vec<ColumnId>,
173 pub apig_route_visible_column_ids: Vec<ColumnId>,
174 pub apig_route_column_ids: Vec<ColumnId>,
175 pub apig_resource_visible_column_ids: Vec<ColumnId>,
176 pub apig_resource_column_ids: Vec<ColumnId>,
177 pub lambda_application_visible_column_ids: Vec<ColumnId>,
178 pub lambda_application_column_ids: Vec<ColumnId>,
179 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
180 pub lambda_deployment_column_ids: Vec<ColumnId>,
181 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
182 pub lambda_resource_column_ids: Vec<ColumnId>,
183 pub cfn_visible_column_ids: Vec<ColumnId>,
184 pub cfn_column_ids: Vec<ColumnId>,
185 pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
186 pub cfn_parameter_column_ids: Vec<ColumnId>,
187 pub cfn_output_visible_column_ids: Vec<ColumnId>,
188 pub cfn_output_column_ids: Vec<ColumnId>,
189 pub cfn_resource_visible_column_ids: Vec<ColumnId>,
190 pub cfn_resource_column_ids: Vec<ColumnId>,
191 pub iam_user_visible_column_ids: Vec<ColumnId>,
192 pub iam_user_column_ids: Vec<ColumnId>,
193 pub iam_role_visible_column_ids: Vec<ColumnId>,
194 pub iam_role_column_ids: Vec<ColumnId>,
195 pub iam_group_visible_column_ids: Vec<String>,
196 pub iam_group_column_ids: Vec<String>,
197 pub iam_policy_visible_column_ids: Vec<String>,
198 pub iam_policy_column_ids: Vec<String>,
199 pub view_mode: ViewMode,
200 pub error_message: Option<String>,
201 pub error_scroll: usize,
202 pub page_input: String,
203 pub calendar_date: Option<time::Date>,
204 pub calendar_selecting: CalendarField,
205 pub cursor_pos: usize,
206 pub current_session: Option<Session>,
207 pub sessions: Vec<Session>,
208 pub session_picker_selected: usize,
209 pub session_filter: String,
210 pub session_filter_active: bool,
211 pub region_filter: String,
212 pub region_picker_selected: usize,
213 pub region_filter_active: bool,
214 pub region_latencies: std::collections::HashMap<String, u64>,
215 pub profile_filter: String,
216 pub profile_picker_selected: usize,
217 pub profile_filter_active: bool,
218 pub available_profiles: Vec<AwsProfile>,
219 pub snapshot_requested: bool,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq)]
223pub enum CalendarField {
224 StartDate,
225 EndDate,
226}
227
228pub struct CloudWatchInsightsState {
229 pub insights: InsightsState,
230 pub loading: bool,
231}
232
233pub struct CloudWatchAlarmsState {
234 pub table: TableState<Alarm>,
235 pub alarm_tab: AlarmTab,
236 pub view_as: AlarmViewMode,
237 pub wrap_lines: bool,
238 pub sort_column: String,
239 pub sort_direction: SortDirection,
240 pub input_focus: InputFocus,
241}
242
243#[derive(Debug, Clone)]
244pub struct CloudTrailState {
245 pub table: TableState<CloudTrailEvent>,
246 pub input_focus: InputFocus,
247 pub current_event: Option<CloudTrailEvent>,
248 pub event_json_scroll: usize,
249 pub detail_focus: CloudTrailDetailFocus,
250 pub resources_expanded_index: Option<usize>,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq)]
254pub enum CloudTrailDetailFocus {
255 Resources,
256 EventRecord,
257}
258
259impl CyclicEnum for CloudTrailDetailFocus {
260 const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
261}
262
263impl PageSize {
264 pub fn value(&self) -> usize {
265 match self {
266 PageSize::Ten => 10,
267 PageSize::TwentyFive => 25,
268 PageSize::Fifty => 50,
269 PageSize::OneHundred => 100,
270 }
271 }
272
273 pub fn next(&self) -> Self {
274 match self {
275 PageSize::Ten => PageSize::TwentyFive,
276 PageSize::TwentyFive => PageSize::Fifty,
277 PageSize::Fifty => PageSize::OneHundred,
278 PageSize::OneHundred => PageSize::Ten,
279 }
280 }
281}
282
283pub struct ServicePickerState {
284 pub filter: String,
285 pub filter_active: bool,
286 pub selected: usize,
287 pub services: Vec<&'static str>,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq)]
291pub enum ViewMode {
292 List,
293 Detail,
294 Events,
295 InsightsResults,
296 PolicyView,
297}
298
299#[derive(Debug, Clone, Copy, PartialEq)]
300pub enum Service {
301 ApiGatewayApis,
302 CloudWatchLogGroups,
303 CloudWatchInsights,
304 CloudWatchAlarms,
305 CloudTrailEvents,
306 S3Buckets,
307 SqsQueues,
308 Ec2Instances,
309 EcrRepositories,
310 LambdaFunctions,
311 LambdaApplications,
312 CloudFormationStacks,
313 IamUsers,
314 IamRoles,
315 IamUserGroups,
316}
317
318impl Service {
319 pub fn name(&self) -> &str {
320 match self {
321 Service::ApiGatewayApis => "API Gateway › APIs",
322 Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
323 Service::CloudWatchInsights => "CloudWatch › Logs Insights",
324 Service::CloudWatchAlarms => "CloudWatch › Alarms",
325 Service::CloudTrailEvents => "CloudTrail › Event History",
326 Service::S3Buckets => "S3 › Buckets",
327 Service::SqsQueues => "SQS › Queues",
328 Service::Ec2Instances => "EC2 › Instances",
329 Service::EcrRepositories => "ECR › Repositories",
330 Service::LambdaFunctions => "Lambda › Functions",
331 Service::LambdaApplications => "Lambda › Applications",
332 Service::CloudFormationStacks => "CloudFormation › Stacks",
333 Service::IamUsers => "IAM › Users",
334 Service::IamRoles => "IAM › Roles",
335 Service::IamUserGroups => "IAM › User Groups",
336 }
337 }
338}
339
340fn copy_to_clipboard(text: &str) {
341 use std::io::Write;
342 use std::process::{Command, Stdio};
343 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
344 if let Some(mut stdin) = child.stdin.take() {
345 let _ = stdin.write_all(text.as_bytes());
346 }
347 let _ = child.wait();
348 }
349}
350
351fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
352 if max > 0 {
353 *selected = (*selected + page_size).min(max - 1);
354 }
355}
356
357fn toggle_iam_preference(
358 idx: usize,
359 column_ids: &[String],
360 visible_column_ids: &mut Vec<String>,
361 page_size: &mut PageSize,
362) {
363 if idx > 0 && idx <= column_ids.len() {
364 if let Some(col) = column_ids.get(idx - 1) {
365 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
366 visible_column_ids.remove(pos);
367 } else {
368 visible_column_ids.push(col.clone());
369 }
370 }
371 } else if idx == column_ids.len() + 3 {
372 *page_size = PageSize::Ten;
373 } else if idx == column_ids.len() + 4 {
374 *page_size = PageSize::TwentyFive;
375 } else if idx == column_ids.len() + 5 {
376 *page_size = PageSize::Fifty;
377 }
378}
379
380fn toggle_iam_preference_static(
381 idx: usize,
382 column_ids: &[ColumnId],
383 visible_column_ids: &mut Vec<ColumnId>,
384 page_size: &mut PageSize,
385) {
386 if idx > 0 && idx <= column_ids.len() {
387 if let Some(col) = column_ids.get(idx - 1) {
388 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
389 visible_column_ids.remove(pos);
390 } else {
391 visible_column_ids.push(*col);
392 }
393 }
394 } else if idx == column_ids.len() + 3 {
395 *page_size = PageSize::Ten;
396 } else if idx == column_ids.len() + 4 {
397 *page_size = PageSize::TwentyFive;
398 } else if idx == column_ids.len() + 5 {
399 *page_size = PageSize::Fifty;
400 }
401}
402
403fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
404 if idx == base_idx {
405 *page_size = PageSize::Ten;
406 } else if idx == base_idx + 1 {
407 *page_size = PageSize::TwentyFive;
408 } else if idx == base_idx + 2 {
409 *page_size = PageSize::Fifty;
410 }
411}
412
413fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
415 let page_size_idx = num_columns + 2;
416 if *current_idx < page_size_idx {
417 *current_idx = page_size_idx;
418 } else {
419 *current_idx = 0;
420 }
421}
422
423fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
425 let page_size_idx = num_columns + 2;
426 if *current_idx >= page_size_idx {
427 *current_idx = 0;
428 } else {
429 *current_idx = page_size_idx;
430 }
431}
432
433impl App {
434 pub fn get_input_focus(&self) -> InputFocus {
435 InputFocus::Filter
436 }
437
438 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
439 if self.current_service == Service::ApiGatewayApis {
440 if self.apig_state.current_api.is_some()
441 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
442 {
443 Some(&mut self.apig_state.route_filter)
444 } else {
445 Some(&mut self.apig_state.apis.filter)
446 }
447 } else if self.current_service == Service::CloudWatchAlarms {
448 Some(&mut self.alarms_state.table.filter)
449 } else if self.current_service == Service::CloudTrailEvents {
450 Some(&mut self.cloudtrail_state.table.filter)
451 } else if self.current_service == Service::Ec2Instances {
452 if self.ec2_state.current_instance.is_some()
453 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
454 {
455 Some(&mut self.ec2_state.tags.filter)
456 } else {
457 Some(&mut self.ec2_state.table.filter)
458 }
459 } else if self.current_service == Service::S3Buckets {
460 if self.s3_state.current_bucket.is_some() {
461 Some(&mut self.s3_state.object_filter)
462 } else {
463 Some(&mut self.s3_state.buckets.filter)
464 }
465 } else if self.current_service == Service::EcrRepositories {
466 if self.ecr_state.current_repository.is_some() {
467 Some(&mut self.ecr_state.images.filter)
468 } else {
469 Some(&mut self.ecr_state.repositories.filter)
470 }
471 } else if self.current_service == Service::SqsQueues {
472 if self.sqs_state.current_queue.is_some()
473 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
474 {
475 Some(&mut self.sqs_state.triggers.filter)
476 } else if self.sqs_state.current_queue.is_some()
477 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
478 {
479 Some(&mut self.sqs_state.pipes.filter)
480 } else if self.sqs_state.current_queue.is_some()
481 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
482 {
483 Some(&mut self.sqs_state.tags.filter)
484 } else if self.sqs_state.current_queue.is_some()
485 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
486 {
487 Some(&mut self.sqs_state.subscriptions.filter)
488 } else {
489 Some(&mut self.sqs_state.queues.filter)
490 }
491 } else if self.current_service == Service::LambdaFunctions {
492 if self.lambda_state.current_version.is_some()
493 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
494 {
495 Some(&mut self.lambda_state.alias_table.filter)
496 } else if self.lambda_state.current_function.is_some()
497 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
498 {
499 Some(&mut self.lambda_state.version_table.filter)
500 } else if self.lambda_state.current_function.is_some()
501 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
502 {
503 Some(&mut self.lambda_state.alias_table.filter)
504 } else {
505 Some(&mut self.lambda_state.table.filter)
506 }
507 } else if self.current_service == Service::LambdaApplications {
508 if self.lambda_application_state.current_application.is_some() {
509 if self.lambda_application_state.detail_tab
510 == LambdaApplicationDetailTab::Deployments
511 {
512 Some(&mut self.lambda_application_state.deployments.filter)
513 } else {
514 Some(&mut self.lambda_application_state.resources.filter)
515 }
516 } else {
517 Some(&mut self.lambda_application_state.table.filter)
518 }
519 } else if self.current_service == Service::CloudFormationStacks {
520 if self.cfn_state.current_stack.is_some()
521 && self.cfn_state.detail_tab == CfnDetailTab::Resources
522 {
523 Some(&mut self.cfn_state.resources.filter)
524 } else {
525 Some(&mut self.cfn_state.table.filter)
526 }
527 } else if self.current_service == Service::IamUsers {
528 if self.iam_state.current_user.is_some() {
529 if self.iam_state.user_tab == UserTab::Tags {
530 Some(&mut self.iam_state.user_tags.filter)
531 } else {
532 Some(&mut self.iam_state.policies.filter)
533 }
534 } else {
535 Some(&mut self.iam_state.users.filter)
536 }
537 } else if self.current_service == Service::IamRoles {
538 if self.iam_state.current_role.is_some() {
539 if self.iam_state.role_tab == RoleTab::Tags {
540 Some(&mut self.iam_state.tags.filter)
541 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
542 Some(&mut self.iam_state.last_accessed_filter)
543 } else {
544 Some(&mut self.iam_state.policies.filter)
545 }
546 } else {
547 Some(&mut self.iam_state.roles.filter)
548 }
549 } else if self.current_service == Service::IamUserGroups {
550 if self.iam_state.current_group.is_some() {
551 if self.iam_state.group_tab == GroupTab::Permissions {
552 Some(&mut self.iam_state.policies.filter)
553 } else if self.iam_state.group_tab == GroupTab::Users {
554 Some(&mut self.iam_state.group_users.filter)
555 } else {
556 None
557 }
558 } else {
559 Some(&mut self.iam_state.groups.filter)
560 }
561 } else if self.view_mode == ViewMode::List {
562 Some(&mut self.log_groups_state.log_groups.filter)
563 } else if self.view_mode == ViewMode::Detail
564 && self.log_groups_state.detail_tab == DetailTab::LogStreams
565 {
566 Some(&mut self.log_groups_state.stream_filter)
567 } else {
568 None
569 }
570 }
571
572 fn apply_filter_operation<F>(&mut self, op: F)
573 where
574 F: FnOnce(&mut String),
575 {
576 if let Some(filter) = self.get_active_filter_mut() {
577 op(filter);
578 if self.current_service == Service::CloudWatchAlarms {
580 self.alarms_state.table.reset();
581 } else if self.current_service == Service::CloudTrailEvents {
582 self.cloudtrail_state.table.reset();
583 } else if self.current_service == Service::Ec2Instances {
584 self.ec2_state.table.reset();
585 } else if self.current_service == Service::S3Buckets {
586 if self.s3_state.current_bucket.is_some() {
587 self.s3_state.selected_object = 0;
588 } else {
589 self.s3_state.buckets.reset();
590 self.s3_state.selected_row = 0;
591 self.s3_state.bucket_scroll_offset = 0;
592 }
593 } else if self.current_service == Service::EcrRepositories {
594 if self.ecr_state.current_repository.is_some() {
595 self.ecr_state.images.reset();
596 } else {
597 self.ecr_state.repositories.reset();
598 }
599 } else if self.current_service == Service::SqsQueues {
600 self.sqs_state.queues.reset();
601 } else if self.current_service == Service::LambdaFunctions {
602 if self.lambda_state.current_version.is_some()
603 || self.lambda_state.current_function.is_some()
604 {
605 self.lambda_state.version_table.reset();
606 self.lambda_state.alias_table.reset();
607 } else {
608 self.lambda_state.table.reset();
609 }
610 } else if self.current_service == Service::LambdaApplications {
611 if self.lambda_application_state.current_application.is_some() {
612 self.lambda_application_state.deployments.reset();
613 self.lambda_application_state.resources.reset();
614 } else {
615 self.lambda_application_state.table.reset();
616 }
617 } else if self.current_service == Service::CloudFormationStacks {
618 self.cfn_state.table.reset();
619 } else if self.current_service == Service::IamUsers {
620 if self.iam_state.current_user.is_some() {
621 self.iam_state.user_tags.reset();
622 self.iam_state.policies.reset();
623 } else {
624 self.iam_state.users.reset();
625 }
626 } else if self.current_service == Service::IamRoles {
627 if self.iam_state.current_role.is_some() {
628 self.iam_state.tags.reset();
629 self.iam_state.policies.reset();
630 } else {
631 self.iam_state.roles.reset();
632 }
633 } else if self.current_service == Service::IamUserGroups {
634 if self.iam_state.current_group.is_some() {
635 self.iam_state.policies.reset();
636 self.iam_state.group_users.reset();
637 } else {
638 self.iam_state.groups.reset();
639 }
640 } else if self.current_service == Service::CloudWatchLogGroups {
641 if self.view_mode == ViewMode::List {
642 self.log_groups_state.log_groups.reset();
643 } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
644 self.log_groups_state.selected_stream = 0;
645 }
646 } else if self.current_service == Service::ApiGatewayApis {
647 self.apig_state.apis.reset();
648 }
649 }
650 }
651
652 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
653 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
654 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
655
656 std::env::set_var("AWS_PROFILE", &profile_name);
657
658 let config = AwsConfig::new(region).await?;
659 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
660 let cloudtrail_client = CloudTrailClient::new(config.clone());
661 let s3_client = S3Client::new(config.clone());
662 let sqs_client = SqsClient::new(config.clone());
663 let alarms_client = AlarmsClient::new(config.clone());
664 let ec2_client = Ec2Client::new(config.clone());
665 let ecr_client = EcrClient::new(config.clone());
666 let apig_client = ApiGatewayClient::new(config.clone());
667 let iam_client = IamClient::new(config.clone());
668 let lambda_client = LambdaClient::new(config.clone());
669 let cloudformation_client = CloudFormationClient::new(config.clone());
670 let region_name = config.region.clone();
671
672 Ok(Self {
673 running: true,
674 mode: Mode::ServicePicker,
675 config,
676 cloudwatch_client,
677 cloudtrail_client,
678 s3_client,
679 sqs_client,
680 alarms_client,
681 ec2_client,
682 ecr_client,
683 apig_client,
684 iam_client,
685 lambda_client,
686 cloudformation_client,
687 current_service: Service::CloudWatchLogGroups,
688 tabs: Vec::new(),
689 current_tab: 0,
690 tab_picker_selected: 0,
691 tab_filter: String::new(),
692 pending_key: None,
693 log_groups_state: CloudWatchLogGroupsState::new(),
694 insights_state: CloudWatchInsightsState::new(),
695 alarms_state: CloudWatchAlarmsState::new(),
696 cloudtrail_state: CloudTrailState {
697 table: TableState::new(),
698 input_focus: InputFocus::Filter,
699 current_event: None,
700 event_json_scroll: 0,
701 detail_focus: CloudTrailDetailFocus::Resources,
702 resources_expanded_index: None,
703 },
704 s3_state: S3State::new(),
705 sqs_state: SqsState::new(),
706 ec2_state: Ec2State::default(),
707 ecr_state: EcrState::new(),
708 apig_state: ApigState::new(),
709 lambda_state: LambdaState::new(),
710 lambda_application_state: LambdaApplicationState::new(),
711 cfn_state: CfnState::new(),
712 iam_state: IamState::new(),
713 service_picker: ServicePickerState::new(),
714 service_selected: false,
715 profile: profile_name,
716 region: region_name,
717 region_selector_index: 0,
718 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
719 cw_log_group_column_ids: LogGroupColumn::ids(),
720 column_selector_index: 0,
721 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
722 cw_log_stream_column_ids: StreamColumn::ids(),
723 cw_log_event_visible_column_ids: EventColumn::default_visible(),
724 cw_log_event_column_ids: EventColumn::ids(),
725 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
726 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
727 cw_alarm_visible_column_ids: [
728 AlarmColumn::Name,
729 AlarmColumn::State,
730 AlarmColumn::LastStateUpdate,
731 AlarmColumn::Conditions,
732 AlarmColumn::Actions,
733 ]
734 .iter()
735 .map(|c| c.id())
736 .collect(),
737 cw_alarm_column_ids: AlarmColumn::ids(),
738 cloudtrail_event_visible_column_ids: [
739 CloudTrailEventColumn::EventName,
740 CloudTrailEventColumn::EventTime,
741 CloudTrailEventColumn::Username,
742 CloudTrailEventColumn::EventSource,
743 CloudTrailEventColumn::ResourceType,
744 CloudTrailEventColumn::ResourceName,
745 ]
746 .iter()
747 .map(|c| c.id())
748 .collect(),
749 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
750 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
751 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
752 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
753 s3_bucket_column_ids: S3BucketColumn::ids(),
754 sqs_visible_column_ids: [
755 SqsColumn::Name,
756 SqsColumn::Type,
757 SqsColumn::Created,
758 SqsColumn::MessagesAvailable,
759 SqsColumn::MessagesInFlight,
760 SqsColumn::Encryption,
761 SqsColumn::ContentBasedDeduplication,
762 ]
763 .iter()
764 .map(|c| c.id())
765 .collect(),
766 sqs_column_ids: SqsColumn::ids(),
767 ec2_visible_column_ids: [
768 Ec2Column::Name,
769 Ec2Column::InstanceId,
770 Ec2Column::InstanceState,
771 Ec2Column::InstanceType,
772 Ec2Column::StatusCheck,
773 Ec2Column::AlarmStatus,
774 Ec2Column::AvailabilityZone,
775 Ec2Column::PublicIpv4Dns,
776 Ec2Column::PublicIpv4Address,
777 Ec2Column::ElasticIp,
778 Ec2Column::Ipv6Ips,
779 Ec2Column::Monitoring,
780 Ec2Column::SecurityGroupName,
781 Ec2Column::KeyName,
782 Ec2Column::LaunchTime,
783 Ec2Column::PlatformDetails,
784 ]
785 .iter()
786 .map(|c| c.id())
787 .collect(),
788 ec2_column_ids: Ec2Column::ids(),
789 ecr_repo_visible_column_ids: EcrColumn::ids(),
790 ecr_repo_column_ids: EcrColumn::ids(),
791 ecr_image_visible_column_ids: EcrImageColumn::ids(),
792 ecr_image_column_ids: EcrImageColumn::ids(),
793 apig_api_visible_column_ids: ApigColumn::ids(),
794 apig_api_column_ids: ApigColumn::ids(),
795 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
796 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
797 apig_resource_visible_column_ids: ResourceColumn::all()
798 .iter()
799 .map(|c| c.id())
800 .collect(),
801 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
802 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
803 lambda_application_column_ids: LambdaApplicationColumn::ids(),
804 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
805 lambda_deployment_column_ids: DeploymentColumn::ids(),
806 lambda_resource_visible_column_ids: ResourceColumn::ids(),
807 lambda_resource_column_ids: ResourceColumn::ids(),
808 cfn_visible_column_ids: [
809 CfnColumn::Name,
810 CfnColumn::Status,
811 CfnColumn::CreatedTime,
812 CfnColumn::Description,
813 ]
814 .iter()
815 .map(|c| c.id())
816 .collect(),
817 cfn_column_ids: CfnColumn::ids(),
818 cfn_parameter_visible_column_ids: parameter_column_ids(),
819 cfn_parameter_column_ids: parameter_column_ids(),
820 cfn_output_visible_column_ids: output_column_ids(),
821 cfn_output_column_ids: output_column_ids(),
822 cfn_resource_visible_column_ids: resource_column_ids(),
823 cfn_resource_column_ids: resource_column_ids(),
824 iam_user_visible_column_ids: UserColumn::visible(),
825 iam_user_column_ids: UserColumn::ids(),
826 iam_role_visible_column_ids: RoleColumn::visible(),
827 iam_role_column_ids: RoleColumn::ids(),
828 iam_group_visible_column_ids: vec![
829 "Group name".to_string(),
830 "Users".to_string(),
831 "Permissions".to_string(),
832 "Creation time".to_string(),
833 ],
834 iam_group_column_ids: vec![
835 "Group name".to_string(),
836 "Path".to_string(),
837 "Users".to_string(),
838 "Permissions".to_string(),
839 "Creation time".to_string(),
840 ],
841 iam_policy_visible_column_ids: vec![
842 "Policy name".to_string(),
843 "Type".to_string(),
844 "Attached via".to_string(),
845 ],
846 iam_policy_column_ids: vec![
847 "Policy name".to_string(),
848 "Type".to_string(),
849 "Attached via".to_string(),
850 "Attached entities".to_string(),
851 "Description".to_string(),
852 "Creation time".to_string(),
853 "Edited time".to_string(),
854 ],
855 preference_section: Preferences::Columns,
856 view_mode: ViewMode::List,
857 error_message: None,
858 error_scroll: 0,
859 page_input: String::new(),
860 calendar_date: None,
861 calendar_selecting: CalendarField::StartDate,
862 cursor_pos: 0,
863 current_session: None,
864 sessions: Vec::new(),
865 session_picker_selected: 0,
866 session_filter: String::new(),
867 session_filter_active: false,
868 region_filter: String::new(),
869 region_filter_active: false,
870 region_picker_selected: 0,
871 region_latencies: std::collections::HashMap::new(),
872 profile_filter: String::new(),
873 profile_filter_active: false,
874 profile_picker_selected: 0,
875 available_profiles: Vec::new(),
876 snapshot_requested: false,
877 })
878 }
879
880 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
881 let config = AwsConfig::dummy(region.clone());
882 Self {
883 running: true,
884 mode: Mode::ServicePicker,
885 config: config.clone(),
886 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
887 cloudtrail_client: CloudTrailClient::new(config.clone()),
888 s3_client: S3Client::new(config.clone()),
889 sqs_client: SqsClient::new(config.clone()),
890 alarms_client: AlarmsClient::new(config.clone()),
891 ec2_client: Ec2Client::new(config.clone()),
892 ecr_client: EcrClient::new(config.clone()),
893 apig_client: ApiGatewayClient::new(config.clone()),
894 iam_client: IamClient::new(config.clone()),
895 lambda_client: LambdaClient::new(config.clone()),
896 cloudformation_client: CloudFormationClient::new(config.clone()),
897 current_service: Service::CloudWatchLogGroups,
898 tabs: Vec::new(),
899 current_tab: 0,
900 tab_picker_selected: 0,
901 tab_filter: String::new(),
902 pending_key: None,
903 log_groups_state: CloudWatchLogGroupsState::new(),
904 insights_state: CloudWatchInsightsState::new(),
905 alarms_state: CloudWatchAlarmsState::new(),
906 s3_state: S3State::new(),
907 cloudtrail_state: CloudTrailState {
908 table: TableState::new(),
909 input_focus: InputFocus::Filter,
910 current_event: None,
911 event_json_scroll: 0,
912 detail_focus: CloudTrailDetailFocus::Resources,
913 resources_expanded_index: None,
914 },
915 sqs_state: SqsState::new(),
916 ec2_state: Ec2State::default(),
917 ecr_state: EcrState::new(),
918 apig_state: ApigState::new(),
919 lambda_state: LambdaState::new(),
920 lambda_application_state: LambdaApplicationState::new(),
921 cfn_state: CfnState::new(),
922 iam_state: IamState::new(),
923 service_picker: ServicePickerState::new(),
924 service_selected: false,
925 profile,
926 region: region.unwrap_or_default(),
927 region_selector_index: 0,
928 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
929 cw_log_group_column_ids: LogGroupColumn::ids(),
930 column_selector_index: 0,
931 preference_section: Preferences::Columns,
932 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
933 cw_log_stream_column_ids: StreamColumn::ids(),
934 cw_log_event_visible_column_ids: EventColumn::default_visible(),
935 cw_log_event_column_ids: EventColumn::ids(),
936 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
937 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
938 cw_alarm_visible_column_ids: [
939 AlarmColumn::Name,
940 AlarmColumn::State,
941 AlarmColumn::LastStateUpdate,
942 AlarmColumn::Conditions,
943 AlarmColumn::Actions,
944 ]
945 .iter()
946 .map(|c| c.id())
947 .collect(),
948 cw_alarm_column_ids: AlarmColumn::ids(),
949 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
950 cloudtrail_event_visible_column_ids: [
951 CloudTrailEventColumn::EventName,
952 CloudTrailEventColumn::EventTime,
953 CloudTrailEventColumn::Username,
954 CloudTrailEventColumn::EventSource,
955 CloudTrailEventColumn::ResourceType,
956 CloudTrailEventColumn::ResourceName,
957 ]
958 .iter()
959 .map(|c| c.id())
960 .collect(),
961 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
962 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
963 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
964 s3_bucket_column_ids: S3BucketColumn::ids(),
965 sqs_visible_column_ids: [
966 SqsColumn::Name,
967 SqsColumn::Type,
968 SqsColumn::Created,
969 SqsColumn::MessagesAvailable,
970 SqsColumn::MessagesInFlight,
971 SqsColumn::Encryption,
972 SqsColumn::ContentBasedDeduplication,
973 ]
974 .iter()
975 .map(|c| c.id())
976 .collect(),
977 sqs_column_ids: SqsColumn::ids(),
978 ec2_visible_column_ids: [
979 Ec2Column::Name,
980 Ec2Column::InstanceId,
981 Ec2Column::InstanceState,
982 Ec2Column::InstanceType,
983 Ec2Column::StatusCheck,
984 Ec2Column::AlarmStatus,
985 Ec2Column::AvailabilityZone,
986 Ec2Column::PublicIpv4Dns,
987 Ec2Column::PublicIpv4Address,
988 Ec2Column::ElasticIp,
989 Ec2Column::Ipv6Ips,
990 Ec2Column::Monitoring,
991 Ec2Column::SecurityGroupName,
992 Ec2Column::KeyName,
993 Ec2Column::LaunchTime,
994 Ec2Column::PlatformDetails,
995 ]
996 .iter()
997 .map(|c| c.id())
998 .collect(),
999 ec2_column_ids: Ec2Column::ids(),
1000 ecr_repo_visible_column_ids: EcrColumn::ids(),
1001 ecr_repo_column_ids: EcrColumn::ids(),
1002 ecr_image_visible_column_ids: EcrImageColumn::ids(),
1003 ecr_image_column_ids: EcrImageColumn::ids(),
1004 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
1005 lambda_application_column_ids: LambdaApplicationColumn::ids(),
1006 apig_api_visible_column_ids: ApigColumn::ids(),
1007 apig_api_column_ids: ApigColumn::ids(),
1008 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
1009 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
1010 apig_resource_visible_column_ids: ResourceColumn::all()
1011 .iter()
1012 .map(|c| c.id())
1013 .collect(),
1014 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
1015 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
1016 lambda_deployment_column_ids: DeploymentColumn::ids(),
1017 lambda_resource_visible_column_ids: ResourceColumn::ids(),
1018 lambda_resource_column_ids: ResourceColumn::ids(),
1019 cfn_visible_column_ids: [
1020 CfnColumn::Name,
1021 CfnColumn::Status,
1022 CfnColumn::CreatedTime,
1023 CfnColumn::Description,
1024 ]
1025 .iter()
1026 .map(|c| c.id())
1027 .collect(),
1028 cfn_column_ids: CfnColumn::ids(),
1029 iam_user_visible_column_ids: UserColumn::visible(),
1030 cfn_parameter_visible_column_ids: parameter_column_ids(),
1031 cfn_parameter_column_ids: parameter_column_ids(),
1032 cfn_output_visible_column_ids: output_column_ids(),
1033 cfn_output_column_ids: output_column_ids(),
1034 cfn_resource_visible_column_ids: resource_column_ids(),
1035 cfn_resource_column_ids: resource_column_ids(),
1036 iam_user_column_ids: UserColumn::ids(),
1037 iam_role_visible_column_ids: RoleColumn::visible(),
1038 iam_role_column_ids: RoleColumn::ids(),
1039 iam_group_visible_column_ids: vec![
1040 "Group name".to_string(),
1041 "Users".to_string(),
1042 "Permissions".to_string(),
1043 "Creation time".to_string(),
1044 ],
1045 iam_group_column_ids: vec![
1046 "Group name".to_string(),
1047 "Path".to_string(),
1048 "Users".to_string(),
1049 "Permissions".to_string(),
1050 "Creation time".to_string(),
1051 ],
1052 iam_policy_visible_column_ids: vec![
1053 "Policy name".to_string(),
1054 "Type".to_string(),
1055 "Attached via".to_string(),
1056 ],
1057 iam_policy_column_ids: vec![
1058 "Policy name".to_string(),
1059 "Type".to_string(),
1060 "Attached via".to_string(),
1061 "Attached entities".to_string(),
1062 "Description".to_string(),
1063 "Creation time".to_string(),
1064 "Edited time".to_string(),
1065 ],
1066 view_mode: ViewMode::List,
1067 error_message: None,
1068 error_scroll: 0,
1069 page_input: String::new(),
1070 calendar_date: None,
1071 calendar_selecting: CalendarField::StartDate,
1072 cursor_pos: 0,
1073 current_session: None,
1074 sessions: Vec::new(),
1075 session_picker_selected: 0,
1076 session_filter: String::new(),
1077 session_filter_active: false,
1078 region_filter: String::new(),
1079 region_filter_active: false,
1080 region_picker_selected: 0,
1081 region_latencies: std::collections::HashMap::new(),
1082 profile_filter: String::new(),
1083 profile_filter_active: false,
1084 profile_picker_selected: 0,
1085 available_profiles: Vec::new(),
1086 snapshot_requested: false,
1087 }
1088 }
1089
1090 pub fn handle_action(&mut self, action: Action) {
1091 match action {
1092 Action::Noop => {}
1093 Action::EnterFilterMode => match self.mode {
1094 Mode::ServicePicker => self.service_picker.filter_active = true,
1095 Mode::RegionPicker => self.region_filter_active = true,
1096 Mode::SessionPicker => self.session_filter_active = true,
1097 Mode::ProfilePicker => self.profile_filter_active = true,
1098 _ => {}
1099 },
1100 Action::ExitFilterMode => match self.mode {
1101 Mode::ServicePicker => {
1102 if self.service_picker.filter_active {
1103 self.service_picker.filter_active = false;
1104 } else {
1105 self.mode = Mode::Normal;
1106 }
1107 }
1108 Mode::RegionPicker => {
1109 if self.region_filter_active {
1110 self.region_filter_active = false;
1111 } else {
1112 self.mode = Mode::Normal;
1113 }
1114 }
1115 Mode::SessionPicker => {
1116 if self.session_filter_active {
1117 self.session_filter_active = false;
1118 } else {
1119 self.mode = Mode::Normal;
1120 }
1121 }
1122 Mode::ProfilePicker => {
1123 if self.profile_filter_active {
1124 self.profile_filter_active = false;
1125 } else {
1126 self.mode = Mode::Normal;
1127 }
1128 }
1129 _ => {}
1130 },
1131 Action::Quit => {
1132 self.save_current_session();
1133 self.running = false;
1134 }
1135 Action::CloseService => {
1136 if !self.tabs.is_empty() {
1137 self.tabs.remove(self.current_tab);
1139
1140 if self.tabs.is_empty() {
1141 self.service_selected = false;
1143 self.current_tab = 0;
1144 self.mode = Mode::ServicePicker;
1145 } else {
1146 if self.current_tab >= self.tabs.len() {
1148 self.current_tab = self.tabs.len() - 1;
1149 }
1150 self.current_service = self.tabs[self.current_tab].service;
1151 self.service_selected = true;
1152 self.mode = Mode::Normal;
1153 }
1154 } else {
1155 self.service_selected = false;
1157 self.mode = Mode::Normal;
1158 }
1159 self.service_picker.filter.clear();
1160 self.service_picker.selected = 0;
1161 }
1162 Action::NextItem => {
1163 let should_navigate = match self.mode {
1165 Mode::ServicePicker => !self.service_picker.filter_active,
1166 Mode::RegionPicker => !self.region_filter_active,
1167 Mode::SessionPicker => !self.session_filter_active,
1168 Mode::ProfilePicker => !self.profile_filter_active,
1169 _ => true,
1170 };
1171 if should_navigate {
1172 self.next_item();
1173 }
1174 }
1175 Action::PrevItem => {
1176 let should_navigate = match self.mode {
1178 Mode::ServicePicker => !self.service_picker.filter_active,
1179 Mode::RegionPicker => !self.region_filter_active,
1180 Mode::SessionPicker => !self.session_filter_active,
1181 Mode::ProfilePicker => !self.profile_filter_active,
1182 _ => true,
1183 };
1184 if should_navigate {
1185 self.prev_item();
1186 }
1187 }
1188 Action::PageUp => self.page_up(),
1189 Action::PageDown => self.page_down(),
1190 Action::NextPane => self.next_pane(),
1191 Action::PrevPane => self.prev_pane(),
1192 Action::CollapseRow => self.collapse_row(),
1193 Action::ExpandRow => self.expand_row(),
1194 Action::Select => self.select_item(),
1195 Action::OpenSpaceMenu => {
1196 self.mode = Mode::SpaceMenu;
1197 self.service_picker.filter.clear();
1198 self.service_picker.filter_active = false;
1199 self.service_picker.selected = 0;
1200 }
1201 Action::CloseMenu => {
1202 self.mode = Mode::Normal;
1203 self.service_picker.filter.clear();
1204 match self.current_service {
1206 Service::S3Buckets => {
1207 self.s3_state.selected_row = 0;
1208 self.s3_state.selected_object = 0;
1209 }
1210 Service::CloudFormationStacks => {
1211 if self.cfn_state.current_stack.is_some()
1212 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1213 {
1214 self.cfn_state.parameters.reset();
1215 } else if self.cfn_state.current_stack.is_some()
1216 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1217 {
1218 self.cfn_state.outputs.reset();
1219 } else {
1220 self.cfn_state.table.reset();
1221 }
1222 }
1223 Service::LambdaFunctions => {
1224 self.lambda_state.table.reset();
1225 }
1226 Service::SqsQueues => {
1227 self.sqs_state.queues.reset();
1228 }
1229 Service::IamRoles => {
1230 self.iam_state.roles.reset();
1231 }
1232 Service::IamUsers => {
1233 self.iam_state.users.reset();
1234 }
1235 Service::IamUserGroups => {
1236 self.iam_state.groups.reset();
1237 }
1238 Service::CloudWatchAlarms => {
1239 self.alarms_state.table.reset();
1240 }
1241 Service::Ec2Instances => {
1242 self.ec2_state.table.reset();
1243 }
1244 Service::EcrRepositories => {
1245 self.ecr_state.repositories.reset();
1246 }
1247 Service::ApiGatewayApis => {
1248 self.apig_state.apis.reset();
1249 }
1250 Service::LambdaApplications => {
1251 self.lambda_application_state.table.reset();
1252 }
1253 _ => {}
1254 }
1255 }
1256 Action::NextTab => {
1257 if !self.tabs.is_empty() {
1258 self.current_tab = (self.current_tab + 1) % self.tabs.len();
1259 self.current_service = self.tabs[self.current_tab].service;
1260 }
1261 }
1262 Action::PrevTab => {
1263 if !self.tabs.is_empty() {
1264 self.current_tab = if self.current_tab == 0 {
1265 self.tabs.len() - 1
1266 } else {
1267 self.current_tab - 1
1268 };
1269 self.current_service = self.tabs[self.current_tab].service;
1270 }
1271 }
1272 Action::CloseTab => {
1273 if !self.tabs.is_empty() {
1274 self.tabs.remove(self.current_tab);
1275 if self.tabs.is_empty() {
1276 self.service_selected = false;
1278 self.current_tab = 0;
1279 self.service_picker.filter.clear();
1280 self.service_picker.selected = 0;
1281 self.mode = Mode::ServicePicker;
1282 } else {
1283 if self.current_tab >= self.tabs.len() {
1286 self.current_tab = self.tabs.len() - 1;
1287 }
1288 self.current_service = self.tabs[self.current_tab].service;
1289 self.service_selected = true;
1290 self.mode = Mode::Normal;
1291 }
1292 }
1293 }
1294 Action::OpenTabPicker => {
1295 if !self.tabs.is_empty() {
1296 self.tab_picker_selected = self.current_tab;
1297 self.mode = Mode::TabPicker;
1298 } else {
1299 self.mode = Mode::Normal;
1300 }
1301 }
1302 Action::OpenSessionPicker => {
1303 self.save_current_session();
1304 self.sessions = Session::list_all().unwrap_or_default();
1305 self.session_picker_selected = 0;
1306 self.mode = Mode::SessionPicker;
1307 }
1308 Action::LoadSession => {
1309 let filtered_sessions = self.get_filtered_sessions();
1310 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1311 let session = session.clone();
1312 self.profile = session.profile.clone();
1314 self.region = session.region.clone();
1315 self.config.account_id = session.account_id.clone();
1316 self.config.role_arn = session.role_arn.clone();
1317
1318 self.tabs.clear();
1320 for session_tab in &session.tabs {
1321 let service = match session_tab.service.as_str() {
1323 "ApiGatewayApis" => Service::ApiGatewayApis,
1324 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1325 "CloudWatchInsights" => Service::CloudWatchInsights,
1326 "CloudWatchAlarms" => Service::CloudWatchAlarms,
1327 "CloudTrailEvents" => Service::CloudTrailEvents,
1328 "S3Buckets" => Service::S3Buckets,
1329 "SqsQueues" => Service::SqsQueues,
1330 "Ec2Instances" => Service::Ec2Instances,
1331 "EcrRepositories" => Service::EcrRepositories,
1332 "LambdaFunctions" => Service::LambdaFunctions,
1333 "LambdaApplications" => Service::LambdaApplications,
1334 "CloudFormationStacks" => Service::CloudFormationStacks,
1335 "IamUsers" => Service::IamUsers,
1336 "IamRoles" => Service::IamRoles,
1337 "IamUserGroups" => Service::IamUserGroups,
1338 _ => continue,
1339 };
1340
1341 self.tabs.push(Tab {
1342 service,
1343 title: session_tab.title.clone(),
1344 breadcrumb: session_tab.breadcrumb.clone(),
1345 });
1346
1347 if let Some(filter) = &session_tab.filter {
1349 if service == Service::CloudWatchLogGroups {
1350 self.log_groups_state.log_groups.filter = filter.clone();
1351 }
1352 }
1353 }
1354
1355 if !self.tabs.is_empty() {
1356 self.current_tab = 0;
1357 self.current_service = self.tabs[0].service;
1358 self.service_selected = true;
1359 self.current_session = Some(session.clone());
1360 }
1361 }
1362 self.mode = Mode::Normal;
1363 }
1364 Action::SaveSession => {
1365 }
1367 Action::OpenServicePicker => {
1368 if self.mode == Mode::ServicePicker {
1369 self.tabs.push(Tab {
1370 service: Service::S3Buckets,
1371 title: "S3 › Buckets".to_string(),
1372 breadcrumb: "S3 › Buckets".to_string(),
1373 });
1374 self.current_tab = self.tabs.len() - 1;
1375 self.current_service = Service::S3Buckets;
1376 self.view_mode = ViewMode::List;
1377 self.service_selected = true;
1378 self.mode = Mode::Normal;
1379 } else {
1380 self.mode = Mode::ServicePicker;
1381 self.service_picker.filter.clear();
1382 self.service_picker.selected = 0;
1383 }
1384 }
1385 Action::OpenCloudWatch => {
1386 self.current_service = Service::CloudWatchLogGroups;
1387 self.view_mode = ViewMode::List;
1388 self.service_selected = true;
1389 self.mode = Mode::Normal;
1390 }
1391 Action::OpenCloudWatchSplit => {
1392 self.current_service = Service::CloudWatchInsights;
1393 self.view_mode = ViewMode::InsightsResults;
1394 self.service_selected = true;
1395 self.mode = Mode::Normal;
1396 }
1397 Action::OpenCloudWatchAlarms => {
1398 self.current_service = Service::CloudWatchAlarms;
1399 self.view_mode = ViewMode::List;
1400 self.service_selected = true;
1401 self.mode = Mode::Normal;
1402 }
1403 Action::FilterInput(c) => {
1404 if self.mode == Mode::TabPicker {
1405 self.tab_filter.push(c);
1406 self.tab_picker_selected = 0;
1407 } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1408 self.service_picker.filter.push(c);
1409 self.service_picker.selected = 0;
1410 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1411 self.region_filter.push(c);
1412 self.region_picker_selected = 0;
1413 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1414 self.profile_filter.push(c);
1415 self.profile_picker_selected = 0;
1416 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1417 self.session_filter.push(c);
1418 self.session_picker_selected = 0;
1419 } else if self.mode == Mode::InsightsInput {
1420 match self.insights_state.insights.insights_focus {
1421 InsightsFocus::Query => {
1422 self.insights_state.insights.query_text.push(c);
1423 }
1424 InsightsFocus::LogGroupSearch => {
1425 self.insights_state.insights.log_group_search.push(c);
1426 if !self.insights_state.insights.log_group_search.is_empty() {
1428 self.insights_state.insights.log_group_matches = self
1429 .log_groups_state
1430 .log_groups
1431 .items
1432 .iter()
1433 .filter(|g| {
1434 g.name.to_lowercase().contains(
1435 &self
1436 .insights_state
1437 .insights
1438 .log_group_search
1439 .to_lowercase(),
1440 )
1441 })
1442 .take(50)
1443 .map(|g| g.name.clone())
1444 .collect();
1445 self.insights_state.insights.show_dropdown = true;
1446 } else {
1447 self.insights_state.insights.log_group_matches.clear();
1448 self.insights_state.insights.show_dropdown = false;
1449 }
1450 }
1451 _ => {}
1452 }
1453 } else if self.mode == Mode::FilterInput {
1454 let is_pagination_focused = if self.current_service
1456 == Service::LambdaApplications
1457 {
1458 if self.lambda_application_state.current_application.is_some() {
1459 if self.lambda_application_state.detail_tab
1460 == LambdaApplicationDetailTab::Deployments
1461 {
1462 self.lambda_application_state.deployment_input_focus
1463 == InputFocus::Pagination
1464 } else {
1465 self.lambda_application_state.resource_input_focus
1466 == InputFocus::Pagination
1467 }
1468 } else {
1469 self.lambda_application_state.input_focus == InputFocus::Pagination
1470 }
1471 } else if self.current_service == Service::CloudFormationStacks {
1472 self.cfn_state.input_focus == InputFocus::Pagination
1473 } else if self.current_service == Service::IamRoles
1474 && self.iam_state.current_role.is_none()
1475 {
1476 self.iam_state.role_input_focus == InputFocus::Pagination
1477 } else if self.view_mode == ViewMode::PolicyView {
1478 self.iam_state.policy_input_focus == InputFocus::Pagination
1479 } else if self.current_service == Service::CloudWatchAlarms {
1480 self.alarms_state.input_focus == InputFocus::Pagination
1481 } else if self.current_service == Service::CloudTrailEvents {
1482 self.cloudtrail_state.input_focus == InputFocus::Pagination
1483 } else if self.current_service == Service::Ec2Instances {
1484 self.ec2_state.input_focus == InputFocus::Pagination
1485 } else if self.current_service == Service::CloudWatchLogGroups {
1486 self.log_groups_state.input_focus == InputFocus::Pagination
1487 } else if self.current_service == Service::ApiGatewayApis {
1488 self.apig_state.input_focus == InputFocus::Pagination
1489 } else if self.current_service == Service::EcrRepositories
1490 && self.ecr_state.current_repository.is_none()
1491 {
1492 self.ecr_state.input_focus == InputFocus::Pagination
1493 } else if self.current_service == Service::LambdaFunctions {
1494 if self.lambda_state.current_function.is_some()
1495 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1496 {
1497 self.lambda_state.version_input_focus == InputFocus::Pagination
1498 } else if self.lambda_state.current_function.is_none() {
1499 self.lambda_state.input_focus == InputFocus::Pagination
1500 } else {
1501 false
1502 }
1503 } else if self.current_service == Service::SqsQueues {
1504 if self.sqs_state.current_queue.is_some()
1505 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1506 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1507 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1508 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1509 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1510 {
1511 self.sqs_state.input_focus == InputFocus::Pagination
1512 } else {
1513 false
1514 }
1515 } else {
1516 false
1517 };
1518
1519 if is_pagination_focused && c.is_ascii_digit() {
1520 self.page_input.push(c);
1521 } else if self.current_service == Service::LambdaApplications {
1522 let is_input_focused =
1523 if self.lambda_application_state.current_application.is_some() {
1524 if self.lambda_application_state.detail_tab
1525 == LambdaApplicationDetailTab::Deployments
1526 {
1527 self.lambda_application_state.deployment_input_focus
1528 == InputFocus::Filter
1529 } else {
1530 self.lambda_application_state.resource_input_focus
1531 == InputFocus::Filter
1532 }
1533 } else {
1534 self.lambda_application_state.input_focus == InputFocus::Filter
1535 };
1536 if is_input_focused {
1537 self.apply_filter_operation(|f| f.push(c));
1538 }
1539 } else if self.current_service == Service::CloudFormationStacks {
1540 if self.cfn_state.current_stack.is_some()
1541 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1542 {
1543 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1544 self.cfn_state.parameters.filter.push(c);
1545 self.cfn_state.parameters.selected = 0;
1546 }
1547 } else if self.cfn_state.current_stack.is_some()
1548 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1549 {
1550 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1551 self.cfn_state.outputs.filter.push(c);
1552 self.cfn_state.outputs.selected = 0;
1553 }
1554 } else if self.cfn_state.current_stack.is_some()
1555 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1556 {
1557 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1558 self.cfn_state.resources.filter.push(c);
1559 self.cfn_state.resources.selected = 0;
1560 }
1561 } else if self.cfn_state.input_focus == InputFocus::Filter {
1562 self.apply_filter_operation(|f| f.push(c));
1563 }
1564 } else if self.current_service == Service::EcrRepositories
1565 && self.ecr_state.current_repository.is_none()
1566 {
1567 if self.ecr_state.input_focus == InputFocus::Filter {
1568 self.apply_filter_operation(|f| f.push(c));
1569 }
1570 } else if self.current_service == Service::IamRoles
1571 && self.iam_state.current_role.is_none()
1572 {
1573 if self.iam_state.role_input_focus == InputFocus::Filter {
1574 self.apply_filter_operation(|f| f.push(c));
1575 }
1576 } else if self.view_mode == ViewMode::PolicyView {
1577 if self.iam_state.policy_input_focus == InputFocus::Filter {
1578 self.apply_filter_operation(|f| f.push(c));
1579 }
1580 } else if self.current_service == Service::ApiGatewayApis
1581 && self.apig_state.current_api.is_some()
1582 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1583 {
1584 if self.apig_state.input_focus == InputFocus::Filter {
1585 self.apply_filter_operation(|f| f.push(c));
1586 }
1587 } else if self.current_service == Service::LambdaFunctions
1588 && self.lambda_state.current_version.is_some()
1589 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1590 {
1591 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1592 self.apply_filter_operation(|f| f.push(c));
1593 }
1594 } else if self.current_service == Service::LambdaFunctions
1595 && self.lambda_state.current_function.is_some()
1596 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1597 {
1598 if self.lambda_state.version_input_focus == InputFocus::Filter {
1599 self.apply_filter_operation(|f| f.push(c));
1600 }
1601 } else if self.current_service == Service::LambdaFunctions
1602 && self.lambda_state.current_function.is_some()
1603 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1604 {
1605 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1606 self.apply_filter_operation(|f| f.push(c));
1607 }
1608 } else if self.current_service == Service::SqsQueues
1609 && self.sqs_state.current_queue.is_some()
1610 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1611 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1612 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1613 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1614 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1615 {
1616 if self.sqs_state.input_focus == InputFocus::Filter {
1617 self.apply_filter_operation(|f| f.push(c));
1618 }
1619 } else if self.current_service == Service::Ec2Instances
1620 && self.ec2_state.current_instance.is_some()
1621 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1622 {
1623 if self.ec2_state.input_focus == InputFocus::Filter {
1624 self.ec2_state.tags.filter.push(c);
1625 self.ec2_state.tags.selected = 0;
1626 }
1627 } else if self.current_service == Service::CloudWatchLogGroups {
1628 if self.log_groups_state.input_focus == InputFocus::Filter {
1629 self.apply_filter_operation(|f| f.push(c));
1630 }
1631 } else {
1632 self.apply_filter_operation(|f| f.push(c));
1633 }
1634 } else if self.mode == Mode::EventFilterInput {
1635 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1636 self.log_groups_state.event_filter.push(c);
1637 } else if c.is_ascii_digit() {
1638 self.log_groups_state.relative_amount.push(c);
1639 }
1640 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1641 self.page_input.push(c);
1642 }
1643 }
1644 Action::FilterBackspace => {
1645 if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1646 self.service_picker.filter.pop();
1647 self.service_picker.selected = 0;
1648 } else if self.mode == Mode::TabPicker {
1649 self.tab_filter.pop();
1650 self.tab_picker_selected = 0;
1651 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1652 self.region_filter.pop();
1653 self.region_picker_selected = 0;
1654 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1655 self.profile_filter.pop();
1656 self.profile_picker_selected = 0;
1657 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1658 self.session_filter.pop();
1659 self.session_picker_selected = 0;
1660 } else if self.mode == Mode::InsightsInput {
1661 match self.insights_state.insights.insights_focus {
1662 InsightsFocus::Query => {
1663 self.insights_state.insights.query_text.pop();
1664 }
1665 InsightsFocus::LogGroupSearch => {
1666 self.insights_state.insights.log_group_search.pop();
1667 if !self.insights_state.insights.log_group_search.is_empty() {
1669 self.insights_state.insights.log_group_matches = self
1670 .log_groups_state
1671 .log_groups
1672 .items
1673 .iter()
1674 .filter(|g| {
1675 g.name.to_lowercase().contains(
1676 &self
1677 .insights_state
1678 .insights
1679 .log_group_search
1680 .to_lowercase(),
1681 )
1682 })
1683 .take(50)
1684 .map(|g| g.name.clone())
1685 .collect();
1686 self.insights_state.insights.show_dropdown = true;
1687 } else {
1688 self.insights_state.insights.log_group_matches.clear();
1689 self.insights_state.insights.show_dropdown = false;
1690 }
1691 }
1692 _ => {}
1693 }
1694 } else if self.mode == Mode::FilterInput {
1695 if self.current_service == Service::CloudFormationStacks {
1697 if self.cfn_state.current_stack.is_some()
1698 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1699 {
1700 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1701 self.cfn_state.parameters.filter.pop();
1702 self.cfn_state.parameters.selected = 0;
1703 }
1704 } else if self.cfn_state.current_stack.is_some()
1705 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1706 {
1707 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1708 self.cfn_state.outputs.filter.pop();
1709 self.cfn_state.outputs.selected = 0;
1710 }
1711 } else if self.cfn_state.current_stack.is_some()
1712 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1713 {
1714 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1715 self.cfn_state.resources.filter.pop();
1716 self.cfn_state.resources.selected = 0;
1717 }
1718 } else if self.cfn_state.input_focus == InputFocus::Filter {
1719 self.apply_filter_operation(|f| {
1720 f.pop();
1721 });
1722 }
1723 } else if self.current_service == Service::Ec2Instances
1724 && self.ec2_state.current_instance.is_some()
1725 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1726 {
1727 if self.ec2_state.input_focus == InputFocus::Filter {
1728 self.ec2_state.tags.filter.pop();
1729 self.ec2_state.tags.selected = 0;
1730 }
1731 } else if self.current_service == Service::CloudWatchLogGroups {
1732 if self.log_groups_state.input_focus == InputFocus::Filter {
1733 self.apply_filter_operation(|f| {
1734 f.pop();
1735 });
1736 }
1737 } else {
1738 self.apply_filter_operation(|f| {
1739 f.pop();
1740 });
1741 }
1742 } else if self.mode == Mode::EventFilterInput {
1743 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1744 self.log_groups_state.event_filter.pop();
1745 } else {
1746 self.log_groups_state.relative_amount.pop();
1747 }
1748 }
1749 }
1750 Action::DeleteWord => {
1751 let text = if self.mode == Mode::ServicePicker {
1752 &mut self.service_picker.filter
1753 } else if self.mode == Mode::InsightsInput {
1754 use crate::app::InsightsFocus;
1755 match self.insights_state.insights.insights_focus {
1756 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1757 InsightsFocus::LogGroupSearch => {
1758 &mut self.insights_state.insights.log_group_search
1759 }
1760 _ => return,
1761 }
1762 } else if self.mode == Mode::FilterInput {
1763 if let Some(filter) = self.get_active_filter_mut() {
1764 filter
1765 } else {
1766 return;
1767 }
1768 } else if self.mode == Mode::EventFilterInput {
1769 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1770 &mut self.log_groups_state.event_filter
1771 } else {
1772 &mut self.log_groups_state.relative_amount
1773 }
1774 } else {
1775 return;
1776 };
1777
1778 if text.is_empty() {
1779 return;
1780 }
1781
1782 let mut chars: Vec<char> = text.chars().collect();
1783 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1784 chars.pop();
1785 }
1786 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1787 chars.pop();
1788 }
1789 *text = chars.into_iter().collect();
1790 }
1791 Action::WordLeft => {
1792 }
1794 Action::WordRight => {
1795 }
1797 Action::OpenColumnSelector => {
1798 if self.current_service == Service::CloudFormationStacks
1800 && self.cfn_state.current_stack.is_some()
1801 && (self.cfn_state.detail_tab == CfnDetailTab::Template
1802 || self.cfn_state.detail_tab == CfnDetailTab::GitSync)
1803 {
1804 return;
1805 }
1806
1807 if self.current_service == Service::IamUsers
1809 && self.iam_state.current_user.is_some()
1810 && self.iam_state.user_tab == UserTab::SecurityCredentials
1811 {
1812 return;
1813 }
1814
1815 if self.current_service == Service::IamRoles
1817 && self.iam_state.current_role.is_some()
1818 && (self.iam_state.role_tab == RoleTab::TrustRelationships
1819 || self.iam_state.role_tab == RoleTab::RevokeSessions)
1820 {
1821 return;
1822 }
1823
1824 if self.current_service == Service::SqsQueues
1826 && self.sqs_state.current_queue.is_some()
1827 && matches!(
1828 self.sqs_state.detail_tab,
1829 SqsQueueDetailTab::QueuePolicies
1830 | SqsQueueDetailTab::Monitoring
1831 | SqsQueueDetailTab::DeadLetterQueue
1832 | SqsQueueDetailTab::Encryption
1833 | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1834 )
1835 {
1836 return;
1837 }
1838
1839 if self.current_service == Service::Ec2Instances
1841 && self.ec2_state.table.expanded_item.is_some()
1842 && self.ec2_state.detail_tab != Ec2DetailTab::Tags
1843 {
1844 return;
1845 }
1846
1847 if self.current_service == Service::CloudTrailEvents
1849 && self.cloudtrail_state.current_event.is_some()
1850 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1851 {
1852 return;
1853 }
1854
1855 if !self.page_input.is_empty() {
1857 if let Ok(page) = self.page_input.parse::<usize>() {
1858 self.go_to_page(page);
1859 }
1860 self.page_input.clear();
1861 } else {
1862 self.mode = Mode::ColumnSelector;
1863 self.column_selector_index = 0;
1864 }
1865 }
1866 Action::ToggleColumn => {
1867 if self.current_service == Service::S3Buckets
1868 && self.s3_state.current_bucket.is_none()
1869 {
1870 let idx = self.column_selector_index;
1871 if idx > 0 && idx <= self.s3_bucket_column_ids.len() {
1872 if let Some(col) = self.s3_bucket_column_ids.get(idx - 1) {
1873 if let Some(pos) = self
1874 .s3_bucket_visible_column_ids
1875 .iter()
1876 .position(|c| c == col)
1877 {
1878 self.s3_bucket_visible_column_ids.remove(pos);
1879 } else {
1880 self.s3_bucket_visible_column_ids.push(*col);
1881 }
1882 }
1883 } else if idx == self.s3_bucket_column_ids.len() + 3 {
1884 self.s3_state.buckets.page_size = PageSize::Ten;
1885 } else if idx == self.s3_bucket_column_ids.len() + 4 {
1886 self.s3_state.buckets.page_size = PageSize::TwentyFive;
1887 } else if idx == self.s3_bucket_column_ids.len() + 5 {
1888 self.s3_state.buckets.page_size = PageSize::Fifty;
1889 } else if idx == self.s3_bucket_column_ids.len() + 6 {
1890 self.s3_state.buckets.page_size = PageSize::OneHundred;
1891 }
1892 } else if self.current_service == Service::CloudWatchAlarms {
1893 let idx = self.column_selector_index;
1897 if (1..=16).contains(&idx) {
1898 if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1900 if let Some(pos) = self
1901 .cw_alarm_visible_column_ids
1902 .iter()
1903 .position(|c| c == col)
1904 {
1905 self.cw_alarm_visible_column_ids.remove(pos);
1906 } else {
1907 self.cw_alarm_visible_column_ids.push(*col);
1908 }
1909 }
1910 } else if idx == 19 {
1911 self.alarms_state.view_as = AlarmViewMode::Table;
1912 } else if idx == 20 {
1913 self.alarms_state.view_as = AlarmViewMode::Cards;
1914 } else if idx == 23 {
1915 self.alarms_state.table.page_size = PageSize::Ten;
1916 } else if idx == 24 {
1917 self.alarms_state.table.page_size = PageSize::TwentyFive;
1918 } else if idx == 25 {
1919 self.alarms_state.table.page_size = PageSize::Fifty;
1920 } else if idx == 26 {
1921 self.alarms_state.table.page_size = PageSize::OneHundred;
1922 } else if idx == 29 {
1923 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1924 }
1925 } else if self.current_service == Service::CloudTrailEvents {
1926 if self.cloudtrail_state.current_event.is_some()
1927 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
1928 {
1929 let idx = self.column_selector_index;
1931 if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
1932 if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
1933 if let Some(pos) = self
1934 .cloudtrail_resource_visible_column_ids
1935 .iter()
1936 .position(|c| c == col)
1937 {
1938 if self.cloudtrail_resource_visible_column_ids.len() > 1 {
1940 self.cloudtrail_resource_visible_column_ids.remove(pos);
1941 }
1942 } else {
1943 self.cloudtrail_resource_visible_column_ids.push(*col);
1944 }
1945 }
1946 }
1947 } else {
1948 let idx = self.column_selector_index;
1950 if (1..=14).contains(&idx) {
1951 if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
1952 if let Some(pos) = self
1953 .cloudtrail_event_visible_column_ids
1954 .iter()
1955 .position(|c| c == col)
1956 {
1957 self.cloudtrail_event_visible_column_ids.remove(pos);
1958 } else {
1959 self.cloudtrail_event_visible_column_ids.push(*col);
1960 }
1961 }
1962 } else if idx == 17 {
1963 self.cloudtrail_state.table.page_size = PageSize::Ten;
1964 self.cloudtrail_state.table.snap_to_page();
1965 } else if idx == 18 {
1966 self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
1967 self.cloudtrail_state.table.snap_to_page();
1968 } else if idx == 19 {
1969 self.cloudtrail_state.table.page_size = PageSize::Fifty;
1970 self.cloudtrail_state.table.snap_to_page();
1971 } else if idx == 20 {
1972 self.cloudtrail_state.table.page_size = PageSize::OneHundred;
1973 self.cloudtrail_state.table.snap_to_page();
1974 }
1975 }
1976 } else if self.current_service == Service::ApiGatewayApis {
1977 if let Some(api) = &self.apig_state.current_api {
1978 use crate::ui::apig::ApiDetailTab;
1979 if self.apig_state.detail_tab == ApiDetailTab::Routes {
1980 if api.protocol_type.to_uppercase() == "REST" {
1982 let idx = self.column_selector_index;
1984 if idx > 1 && idx <= self.apig_resource_column_ids.len() {
1985 if let Some(col) = self.apig_resource_column_ids.get(idx - 1) {
1986 if let Some(pos) = self
1987 .apig_resource_visible_column_ids
1988 .iter()
1989 .position(|c| c == col)
1990 {
1991 self.apig_resource_visible_column_ids.remove(pos);
1992 } else {
1993 self.apig_resource_visible_column_ids.push(*col);
1994 }
1995 }
1996 }
1997 } else {
1998 let idx = self.column_selector_index;
2000 if idx > 1 && idx <= self.apig_route_column_ids.len() {
2001 if let Some(col) = self.apig_route_column_ids.get(idx - 1) {
2002 if let Some(pos) = self
2003 .apig_route_visible_column_ids
2004 .iter()
2005 .position(|c| c == col)
2006 {
2007 self.apig_route_visible_column_ids.remove(pos);
2008 } else {
2009 self.apig_route_visible_column_ids.push(*col);
2010 }
2011 }
2012 }
2013 }
2014 }
2016 } else {
2017 let idx = self.column_selector_index;
2019 if idx > 0 && idx <= self.apig_api_column_ids.len() {
2020 if let Some(col) = self.apig_api_column_ids.get(idx - 1) {
2021 if let Some(pos) = self
2022 .apig_api_visible_column_ids
2023 .iter()
2024 .position(|c| c == col)
2025 {
2026 self.apig_api_visible_column_ids.remove(pos);
2027 } else {
2028 self.apig_api_visible_column_ids.push(*col);
2029 }
2030 }
2031 } else if idx == self.apig_api_column_ids.len() + 3 {
2032 self.apig_state.apis.page_size = PageSize::Ten;
2033 } else if idx == self.apig_api_column_ids.len() + 4 {
2034 self.apig_state.apis.page_size = PageSize::TwentyFive;
2035 } else if idx == self.apig_api_column_ids.len() + 5 {
2036 self.apig_state.apis.page_size = PageSize::Fifty;
2037 } else if idx == self.apig_api_column_ids.len() + 6 {
2038 self.apig_state.apis.page_size = PageSize::OneHundred;
2039 }
2040 }
2041 } else if self.current_service == Service::EcrRepositories {
2042 if self.ecr_state.current_repository.is_some() {
2043 let idx = self.column_selector_index;
2045 if let Some(col) = self.ecr_image_column_ids.get(idx) {
2046 if let Some(pos) = self
2047 .ecr_image_visible_column_ids
2048 .iter()
2049 .position(|c| c == col)
2050 {
2051 self.ecr_image_visible_column_ids.remove(pos);
2052 } else {
2053 self.ecr_image_visible_column_ids.push(*col);
2054 }
2055 }
2056 } else {
2057 let idx = self.column_selector_index;
2059 if idx > 0 && idx <= self.ecr_repo_column_ids.len() {
2060 if let Some(col) = self.ecr_repo_column_ids.get(idx - 1) {
2061 if let Some(pos) = self
2062 .ecr_repo_visible_column_ids
2063 .iter()
2064 .position(|c| c == col)
2065 {
2066 self.ecr_repo_visible_column_ids.remove(pos);
2067 } else {
2068 self.ecr_repo_visible_column_ids.push(*col);
2069 }
2070 }
2071 } else if idx == self.ecr_repo_column_ids.len() + 3 {
2072 self.ecr_state.repositories.page_size = PageSize::Ten;
2073 } else if idx == self.ecr_repo_column_ids.len() + 4 {
2074 self.ecr_state.repositories.page_size = PageSize::TwentyFive;
2075 } else if idx == self.ecr_repo_column_ids.len() + 5 {
2076 self.ecr_state.repositories.page_size = PageSize::Fifty;
2077 } else if idx == self.ecr_repo_column_ids.len() + 6 {
2078 self.ecr_state.repositories.page_size = PageSize::OneHundred;
2079 }
2080 }
2081 } else if self.current_service == Service::Ec2Instances {
2082 if self.ec2_state.current_instance.is_some()
2083 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
2084 {
2085 let idx = self.column_selector_index;
2086 if idx > 0 && idx <= self.ec2_state.tag_column_ids.len() {
2087 if let Some(col) = self.ec2_state.tag_column_ids.get(idx - 1) {
2088 if let Some(pos) = self
2089 .ec2_state
2090 .tag_visible_column_ids
2091 .iter()
2092 .position(|c| c == col)
2093 {
2094 self.ec2_state.tag_visible_column_ids.remove(pos);
2095 } else {
2096 self.ec2_state.tag_visible_column_ids.push(col.clone());
2097 }
2098 }
2099 } else if idx == self.ec2_state.tag_column_ids.len() + 3 {
2100 self.ec2_state.tags.page_size = PageSize::Ten;
2101 } else if idx == self.ec2_state.tag_column_ids.len() + 4 {
2102 self.ec2_state.tags.page_size = PageSize::TwentyFive;
2103 } else if idx == self.ec2_state.tag_column_ids.len() + 5 {
2104 self.ec2_state.tags.page_size = PageSize::Fifty;
2105 } else if idx == self.ec2_state.tag_column_ids.len() + 6 {
2106 self.ec2_state.tags.page_size = PageSize::OneHundred;
2107 }
2108 } else {
2109 let idx = self.column_selector_index;
2110 if idx > 0 && idx <= self.ec2_column_ids.len() {
2111 if let Some(col) = self.ec2_column_ids.get(idx - 1) {
2112 if let Some(pos) =
2113 self.ec2_visible_column_ids.iter().position(|c| c == col)
2114 {
2115 self.ec2_visible_column_ids.remove(pos);
2116 } else {
2117 self.ec2_visible_column_ids.push(*col);
2118 }
2119 }
2120 } else if idx == self.ec2_column_ids.len() + 3 {
2121 self.ec2_state.table.page_size = PageSize::Ten;
2122 } else if idx == self.ec2_column_ids.len() + 4 {
2123 self.ec2_state.table.page_size = PageSize::TwentyFive;
2124 } else if idx == self.ec2_column_ids.len() + 5 {
2125 self.ec2_state.table.page_size = PageSize::Fifty;
2126 } else if idx == self.ec2_column_ids.len() + 6 {
2127 self.ec2_state.table.page_size = PageSize::OneHundred;
2128 }
2129 }
2130 } else if self.current_service == Service::SqsQueues {
2131 if self.sqs_state.current_queue.is_some()
2132 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2133 {
2134 let idx = self.column_selector_index;
2136 if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
2137 if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
2138 if let Some(pos) = self
2139 .sqs_state
2140 .trigger_visible_column_ids
2141 .iter()
2142 .position(|c| c == col)
2143 {
2144 self.sqs_state.trigger_visible_column_ids.remove(pos);
2145 } else {
2146 self.sqs_state.trigger_visible_column_ids.push(col.clone());
2147 }
2148 }
2149 } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
2150 self.sqs_state.triggers.page_size = PageSize::Ten;
2151 } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
2152 self.sqs_state.triggers.page_size = PageSize::TwentyFive;
2153 } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
2154 self.sqs_state.triggers.page_size = PageSize::Fifty;
2155 } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
2156 self.sqs_state.triggers.page_size = PageSize::OneHundred;
2157 }
2158 } else if self.sqs_state.current_queue.is_some()
2159 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2160 {
2161 let idx = self.column_selector_index;
2163 if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
2164 if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
2165 if let Some(pos) = self
2166 .sqs_state
2167 .pipe_visible_column_ids
2168 .iter()
2169 .position(|c| c == col)
2170 {
2171 self.sqs_state.pipe_visible_column_ids.remove(pos);
2172 } else {
2173 self.sqs_state.pipe_visible_column_ids.push(col.clone());
2174 }
2175 }
2176 } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
2177 self.sqs_state.pipes.page_size = PageSize::Ten;
2178 } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
2179 self.sqs_state.pipes.page_size = PageSize::TwentyFive;
2180 } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
2181 self.sqs_state.pipes.page_size = PageSize::Fifty;
2182 } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
2183 self.sqs_state.pipes.page_size = PageSize::OneHundred;
2184 }
2185 } else if self.sqs_state.current_queue.is_some()
2186 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2187 {
2188 let idx = self.column_selector_index;
2190 if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
2191 if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
2192 if let Some(pos) = self
2193 .sqs_state
2194 .tag_visible_column_ids
2195 .iter()
2196 .position(|c| c == col)
2197 {
2198 self.sqs_state.tag_visible_column_ids.remove(pos);
2199 } else {
2200 self.sqs_state.tag_visible_column_ids.push(col.clone());
2201 }
2202 }
2203 } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
2204 self.sqs_state.tags.page_size = PageSize::Ten;
2205 } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
2206 self.sqs_state.tags.page_size = PageSize::TwentyFive;
2207 } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
2208 self.sqs_state.tags.page_size = PageSize::Fifty;
2209 } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
2210 self.sqs_state.tags.page_size = PageSize::OneHundred;
2211 }
2212 } else if self.sqs_state.current_queue.is_some()
2213 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2214 {
2215 let idx = self.column_selector_index;
2217 if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
2218 if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
2219 if let Some(pos) = self
2220 .sqs_state
2221 .subscription_visible_column_ids
2222 .iter()
2223 .position(|c| c == col)
2224 {
2225 self.sqs_state.subscription_visible_column_ids.remove(pos);
2226 } else {
2227 self.sqs_state
2228 .subscription_visible_column_ids
2229 .push(col.clone());
2230 }
2231 }
2232 } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
2233 self.sqs_state.subscriptions.page_size = PageSize::Ten;
2234 } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
2235 self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
2236 } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
2237 self.sqs_state.subscriptions.page_size = PageSize::Fifty;
2238 } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
2239 self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
2240 }
2241 } else {
2242 let idx = self.column_selector_index;
2244 if let Some(col) = self.sqs_column_ids.get(idx) {
2245 if let Some(pos) =
2246 self.sqs_visible_column_ids.iter().position(|c| c == col)
2247 {
2248 self.sqs_visible_column_ids.remove(pos);
2249 } else {
2250 self.sqs_visible_column_ids.push(*col);
2251 }
2252 } else if idx == self.sqs_column_ids.len() + 2 {
2253 self.sqs_state.queues.page_size = PageSize::Ten;
2254 } else if idx == self.sqs_column_ids.len() + 3 {
2255 self.sqs_state.queues.page_size = PageSize::TwentyFive;
2256 } else if idx == self.sqs_column_ids.len() + 4 {
2257 self.sqs_state.queues.page_size = PageSize::Fifty;
2258 } else if idx == self.sqs_column_ids.len() + 5 {
2259 self.sqs_state.queues.page_size = PageSize::OneHundred;
2260 }
2261 }
2262 } else if self.current_service == Service::LambdaFunctions {
2263 let idx = self.column_selector_index;
2264 if self.lambda_state.current_function.is_some()
2266 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2267 {
2268 if idx > 0 && idx <= self.lambda_state.version_column_ids.len() {
2270 if let Some(col) = self.lambda_state.version_column_ids.get(idx - 1) {
2271 if let Some(pos) = self
2272 .lambda_state
2273 .version_visible_column_ids
2274 .iter()
2275 .position(|c| *c == *col)
2276 {
2277 self.lambda_state.version_visible_column_ids.remove(pos);
2278 } else {
2279 self.lambda_state
2280 .version_visible_column_ids
2281 .push(col.clone());
2282 }
2283 }
2284 } else if idx == self.lambda_state.version_column_ids.len() + 3 {
2285 self.lambda_state.version_table.page_size = PageSize::Ten;
2286 } else if idx == self.lambda_state.version_column_ids.len() + 4 {
2287 self.lambda_state.version_table.page_size = PageSize::TwentyFive;
2288 } else if idx == self.lambda_state.version_column_ids.len() + 5 {
2289 self.lambda_state.version_table.page_size = PageSize::Fifty;
2290 } else if idx == self.lambda_state.version_column_ids.len() + 6 {
2291 self.lambda_state.version_table.page_size = PageSize::OneHundred;
2292 }
2293 } else if (self.lambda_state.current_function.is_some()
2294 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases)
2295 || (self.lambda_state.current_version.is_some()
2296 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration)
2297 {
2298 if idx > 0 && idx <= self.lambda_state.alias_column_ids.len() {
2300 if let Some(col) = self.lambda_state.alias_column_ids.get(idx - 1) {
2301 if let Some(pos) = self
2302 .lambda_state
2303 .alias_visible_column_ids
2304 .iter()
2305 .position(|c| *c == *col)
2306 {
2307 self.lambda_state.alias_visible_column_ids.remove(pos);
2308 } else {
2309 self.lambda_state.alias_visible_column_ids.push(col.clone());
2310 }
2311 }
2312 } else if idx == self.lambda_state.alias_column_ids.len() + 3 {
2313 self.lambda_state.alias_table.page_size = PageSize::Ten;
2314 } else if idx == self.lambda_state.alias_column_ids.len() + 4 {
2315 self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
2316 } else if idx == self.lambda_state.alias_column_ids.len() + 5 {
2317 self.lambda_state.alias_table.page_size = PageSize::Fifty;
2318 } else if idx == self.lambda_state.alias_column_ids.len() + 6 {
2319 self.lambda_state.alias_table.page_size = PageSize::OneHundred;
2320 }
2321 } else {
2322 if idx > 0 && idx <= self.lambda_state.function_column_ids.len() {
2324 if let Some(col) = self.lambda_state.function_column_ids.get(idx - 1) {
2325 if let Some(pos) = self
2326 .lambda_state
2327 .function_visible_column_ids
2328 .iter()
2329 .position(|c| *c == *col)
2330 {
2331 self.lambda_state.function_visible_column_ids.remove(pos);
2332 } else {
2333 self.lambda_state.function_visible_column_ids.push(*col);
2334 }
2335 }
2336 } else if idx == self.lambda_state.function_column_ids.len() + 3 {
2337 self.lambda_state.table.page_size = PageSize::Ten;
2338 } else if idx == self.lambda_state.function_column_ids.len() + 4 {
2339 self.lambda_state.table.page_size = PageSize::TwentyFive;
2340 } else if idx == self.lambda_state.function_column_ids.len() + 5 {
2341 self.lambda_state.table.page_size = PageSize::Fifty;
2342 } else if idx == self.lambda_state.function_column_ids.len() + 6 {
2343 self.lambda_state.table.page_size = PageSize::OneHundred;
2344 }
2345 }
2346 } else if self.current_service == Service::LambdaApplications {
2347 if self.lambda_application_state.current_application.is_some() {
2348 if self.lambda_application_state.detail_tab
2350 == LambdaApplicationDetailTab::Overview
2351 {
2352 let idx = self.column_selector_index;
2354 if idx > 0 && idx <= self.lambda_resource_column_ids.len() {
2355 if let Some(col) = self.lambda_resource_column_ids.get(idx - 1) {
2356 if let Some(pos) = self
2357 .lambda_resource_visible_column_ids
2358 .iter()
2359 .position(|c| c == col)
2360 {
2361 self.lambda_resource_visible_column_ids.remove(pos);
2362 } else {
2363 self.lambda_resource_visible_column_ids.push(*col);
2364 }
2365 }
2366 } else if idx == self.lambda_resource_column_ids.len() + 3 {
2367 self.lambda_application_state.resources.page_size = PageSize::Ten;
2368 } else if idx == self.lambda_resource_column_ids.len() + 4 {
2369 self.lambda_application_state.resources.page_size =
2370 PageSize::TwentyFive;
2371 } else if idx == self.lambda_resource_column_ids.len() + 5 {
2372 self.lambda_application_state.resources.page_size = PageSize::Fifty;
2373 }
2374 } else {
2375 let idx = self.column_selector_index;
2377 if idx > 0 && idx <= self.lambda_deployment_column_ids.len() {
2378 if let Some(col) = self.lambda_deployment_column_ids.get(idx - 1) {
2379 if let Some(pos) = self
2380 .lambda_deployment_visible_column_ids
2381 .iter()
2382 .position(|c| c == col)
2383 {
2384 self.lambda_deployment_visible_column_ids.remove(pos);
2385 } else {
2386 self.lambda_deployment_visible_column_ids.push(*col);
2387 }
2388 }
2389 } else if idx == self.lambda_deployment_column_ids.len() + 3 {
2390 self.lambda_application_state.deployments.page_size = PageSize::Ten;
2391 } else if idx == self.lambda_deployment_column_ids.len() + 4 {
2392 self.lambda_application_state.deployments.page_size =
2393 PageSize::TwentyFive;
2394 } else if idx == self.lambda_deployment_column_ids.len() + 5 {
2395 self.lambda_application_state.deployments.page_size =
2396 PageSize::Fifty;
2397 }
2398 }
2399 } else {
2400 let idx = self.column_selector_index;
2402 if idx > 0 && idx <= self.lambda_application_column_ids.len() {
2403 if let Some(col) = self.lambda_application_column_ids.get(idx - 1) {
2404 if let Some(pos) = self
2405 .lambda_application_visible_column_ids
2406 .iter()
2407 .position(|c| *c == *col)
2408 {
2409 self.lambda_application_visible_column_ids.remove(pos);
2410 } else {
2411 self.lambda_application_visible_column_ids.push(*col);
2412 }
2413 }
2414 } else if idx == self.lambda_application_column_ids.len() + 3 {
2415 self.lambda_application_state.table.page_size = PageSize::Ten;
2416 } else if idx == self.lambda_application_column_ids.len() + 4 {
2417 self.lambda_application_state.table.page_size = PageSize::TwentyFive;
2418 } else if idx == self.lambda_application_column_ids.len() + 5 {
2419 self.lambda_application_state.table.page_size = PageSize::Fifty;
2420 }
2421 }
2422 } else if self.view_mode == ViewMode::Events {
2423 if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
2424 {
2425 if let Some(pos) = self
2426 .cw_log_event_visible_column_ids
2427 .iter()
2428 .position(|c| c == col)
2429 {
2430 self.cw_log_event_visible_column_ids.remove(pos);
2431 } else {
2432 self.cw_log_event_visible_column_ids.push(*col);
2433 }
2434 }
2435 } else if self.view_mode == ViewMode::Detail {
2436 let idx = self.column_selector_index;
2437 if self.log_groups_state.detail_tab == DetailTab::Tags {
2438 if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
2440 if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
2441 if let Some(pos) = self
2442 .cw_log_tag_visible_column_ids
2443 .iter()
2444 .position(|c| c == col)
2445 {
2446 self.cw_log_tag_visible_column_ids.remove(pos);
2447 } else {
2448 self.cw_log_tag_visible_column_ids.push(*col);
2449 }
2450 }
2451 } else if idx == self.cw_log_tag_column_ids.len() + 3 {
2452 self.log_groups_state.tags.page_size = PageSize::Ten;
2453 } else if idx == self.cw_log_tag_column_ids.len() + 4 {
2454 self.log_groups_state.tags.page_size = PageSize::TwentyFive;
2455 } else if idx == self.cw_log_tag_column_ids.len() + 5 {
2456 self.log_groups_state.tags.page_size = PageSize::Fifty;
2457 } else if idx == self.cw_log_tag_column_ids.len() + 6 {
2458 self.log_groups_state.tags.page_size = PageSize::OneHundred;
2459 }
2460 } else {
2461 if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
2463 if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
2464 if let Some(pos) = self
2465 .cw_log_stream_visible_column_ids
2466 .iter()
2467 .position(|c| c == col)
2468 {
2469 self.cw_log_stream_visible_column_ids.remove(pos);
2470 } else {
2471 self.cw_log_stream_visible_column_ids.push(*col);
2472 }
2473 }
2474 } else if idx == self.cw_log_stream_column_ids.len() + 3 {
2475 self.log_groups_state.stream_page_size = 10;
2476 self.log_groups_state.stream_current_page = 0;
2477 } else if idx == self.cw_log_stream_column_ids.len() + 4 {
2478 self.log_groups_state.stream_page_size = 25;
2479 self.log_groups_state.stream_current_page = 0;
2480 } else if idx == self.cw_log_stream_column_ids.len() + 5 {
2481 self.log_groups_state.stream_page_size = 50;
2482 self.log_groups_state.stream_current_page = 0;
2483 } else if idx == self.cw_log_stream_column_ids.len() + 6 {
2484 self.log_groups_state.stream_page_size = 100;
2485 self.log_groups_state.stream_current_page = 0;
2486 }
2487 }
2488 } else if self.current_service == Service::CloudFormationStacks {
2489 let idx = self.column_selector_index;
2490 if self.cfn_state.current_stack.is_some()
2492 && self.cfn_state.detail_tab == CfnDetailTab::StackInfo
2493 {
2494 if idx == 4 {
2496 self.cfn_state.tags.page_size = PageSize::Ten;
2497 } else if idx == 5 {
2498 self.cfn_state.tags.page_size = PageSize::TwentyFive;
2499 } else if idx == 6 {
2500 self.cfn_state.tags.page_size = PageSize::Fifty;
2501 } else if idx == 7 {
2502 self.cfn_state.tags.page_size = PageSize::OneHundred;
2503 }
2504 } else if self.cfn_state.current_stack.is_some()
2505 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2506 {
2507 if idx > 0 && idx <= self.cfn_parameter_column_ids.len() {
2508 if let Some(col) = self.cfn_parameter_column_ids.get(idx - 1) {
2509 if let Some(pos) = self
2510 .cfn_parameter_visible_column_ids
2511 .iter()
2512 .position(|c| c == col)
2513 {
2514 self.cfn_parameter_visible_column_ids.remove(pos);
2515 } else {
2516 self.cfn_parameter_visible_column_ids.push(col);
2517 }
2518 }
2519 } else if idx == self.cfn_parameter_column_ids.len() + 3 {
2520 self.cfn_state.parameters.page_size = PageSize::Ten;
2521 } else if idx == self.cfn_parameter_column_ids.len() + 4 {
2522 self.cfn_state.parameters.page_size = PageSize::TwentyFive;
2523 } else if idx == self.cfn_parameter_column_ids.len() + 5 {
2524 self.cfn_state.parameters.page_size = PageSize::Fifty;
2525 } else if idx == self.cfn_parameter_column_ids.len() + 6 {
2526 self.cfn_state.parameters.page_size = PageSize::OneHundred;
2527 }
2528 } else if self.cfn_state.current_stack.is_some()
2529 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2530 {
2531 if idx > 0 && idx <= self.cfn_output_column_ids.len() {
2532 if let Some(col) = self.cfn_output_column_ids.get(idx - 1) {
2533 if let Some(pos) = self
2534 .cfn_output_visible_column_ids
2535 .iter()
2536 .position(|c| c == col)
2537 {
2538 self.cfn_output_visible_column_ids.remove(pos);
2539 } else {
2540 self.cfn_output_visible_column_ids.push(col);
2541 }
2542 }
2543 } else if idx == self.cfn_output_column_ids.len() + 3 {
2544 self.cfn_state.outputs.page_size = PageSize::Ten;
2545 } else if idx == self.cfn_output_column_ids.len() + 4 {
2546 self.cfn_state.outputs.page_size = PageSize::TwentyFive;
2547 } else if idx == self.cfn_output_column_ids.len() + 5 {
2548 self.cfn_state.outputs.page_size = PageSize::Fifty;
2549 } else if idx == self.cfn_output_column_ids.len() + 6 {
2550 self.cfn_state.outputs.page_size = PageSize::OneHundred;
2551 }
2552 } else if self.cfn_state.current_stack.is_some()
2553 && self.cfn_state.detail_tab == CfnDetailTab::Resources
2554 {
2555 if idx > 0 && idx <= self.cfn_resource_column_ids.len() {
2556 if let Some(col) = self.cfn_resource_column_ids.get(idx - 1) {
2557 if let Some(pos) = self
2558 .cfn_resource_visible_column_ids
2559 .iter()
2560 .position(|c| c == col)
2561 {
2562 self.cfn_resource_visible_column_ids.remove(pos);
2563 } else {
2564 self.cfn_resource_visible_column_ids.push(col);
2565 }
2566 }
2567 } else if idx == self.cfn_resource_column_ids.len() + 3 {
2568 self.cfn_state.resources.page_size = PageSize::Ten;
2569 } else if idx == self.cfn_resource_column_ids.len() + 4 {
2570 self.cfn_state.resources.page_size = PageSize::TwentyFive;
2571 } else if idx == self.cfn_resource_column_ids.len() + 5 {
2572 self.cfn_state.resources.page_size = PageSize::Fifty;
2573 } else if idx == self.cfn_resource_column_ids.len() + 6 {
2574 self.cfn_state.resources.page_size = PageSize::OneHundred;
2575 }
2576 } else if self.cfn_state.current_stack.is_none() {
2577 if idx > 0 && idx <= self.cfn_column_ids.len() {
2579 if let Some(col) = self.cfn_column_ids.get(idx - 1) {
2580 if let Some(pos) =
2581 self.cfn_visible_column_ids.iter().position(|c| c == col)
2582 {
2583 self.cfn_visible_column_ids.remove(pos);
2584 } else {
2585 self.cfn_visible_column_ids.push(*col);
2586 }
2587 }
2588 } else if idx == self.cfn_column_ids.len() + 3 {
2589 self.cfn_state.table.page_size = PageSize::Ten;
2590 } else if idx == self.cfn_column_ids.len() + 4 {
2591 self.cfn_state.table.page_size = PageSize::TwentyFive;
2592 } else if idx == self.cfn_column_ids.len() + 5 {
2593 self.cfn_state.table.page_size = PageSize::Fifty;
2594 } else if idx == self.cfn_column_ids.len() + 6 {
2595 self.cfn_state.table.page_size = PageSize::OneHundred;
2596 }
2597 }
2598 } else if self.current_service == Service::IamUsers {
2600 let idx = self.column_selector_index;
2601 if self.iam_state.current_user.is_some() {
2602 match self.iam_state.user_tab {
2603 UserTab::Permissions => {
2604 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
2606 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
2607 if let Some(pos) = self
2608 .iam_policy_visible_column_ids
2609 .iter()
2610 .position(|c| c == col)
2611 {
2612 self.iam_policy_visible_column_ids.remove(pos);
2613 } else {
2614 self.iam_policy_visible_column_ids.push(col.clone());
2615 }
2616 }
2617 } else if idx == self.iam_policy_column_ids.len() + 3 {
2618 self.iam_state.policies.page_size = PageSize::Ten;
2619 } else if idx == self.iam_policy_column_ids.len() + 4 {
2620 self.iam_state.policies.page_size = PageSize::TwentyFive;
2621 } else if idx == self.iam_policy_column_ids.len() + 5 {
2622 self.iam_state.policies.page_size = PageSize::Fifty;
2623 }
2624 }
2625 UserTab::Groups => {
2626 toggle_iam_page_size_only(
2627 idx,
2628 5,
2629 &mut self.iam_state.user_group_memberships.page_size,
2630 );
2631 }
2632 UserTab::Tags => {
2633 toggle_iam_page_size_only(
2634 idx,
2635 5,
2636 &mut self.iam_state.user_tags.page_size,
2637 );
2638 }
2639 UserTab::LastAccessed => {
2640 toggle_iam_page_size_only(
2641 idx,
2642 6,
2643 &mut self.iam_state.last_accessed_services.page_size,
2644 );
2645 }
2646 _ => {}
2647 }
2648 } else {
2649 toggle_iam_preference_static(
2651 idx,
2652 &self.iam_user_column_ids,
2653 &mut self.iam_user_visible_column_ids,
2654 &mut self.iam_state.users.page_size,
2655 );
2656 }
2657 } else if self.current_service == Service::IamRoles {
2658 let idx = self.column_selector_index;
2659 if self.iam_state.current_role.is_some() {
2660 match self.iam_state.role_tab {
2661 RoleTab::Permissions => {
2662 toggle_iam_preference(
2664 idx,
2665 &self.iam_policy_column_ids,
2666 &mut self.iam_policy_visible_column_ids,
2667 &mut self.iam_state.policies.page_size,
2668 );
2669 }
2670 RoleTab::LastAccessed => {
2671 toggle_iam_page_size_only(
2673 idx,
2674 6,
2675 &mut self.iam_state.last_accessed_services.page_size,
2676 );
2677 }
2678 _ => {}
2679 }
2680 } else {
2681 toggle_iam_preference_static(
2683 idx,
2684 &self.iam_role_column_ids,
2685 &mut self.iam_role_visible_column_ids,
2686 &mut self.iam_state.roles.page_size,
2687 );
2688 }
2689 } else if self.current_service == Service::IamUserGroups {
2690 toggle_iam_preference(
2691 self.column_selector_index,
2692 &self.iam_group_column_ids,
2693 &mut self.iam_group_visible_column_ids,
2694 &mut self.iam_state.groups.page_size,
2695 );
2696 } else {
2697 let idx = self.column_selector_index;
2698 if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
2699 if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
2700 if let Some(pos) = self
2701 .cw_log_group_visible_column_ids
2702 .iter()
2703 .position(|c| c == col)
2704 {
2705 self.cw_log_group_visible_column_ids.remove(pos);
2706 } else {
2707 self.cw_log_group_visible_column_ids.push(*col);
2708 }
2709 }
2710 } else if idx == self.cw_log_group_column_ids.len() + 3 {
2711 self.log_groups_state.log_groups.page_size = PageSize::Ten;
2712 } else if idx == self.cw_log_group_column_ids.len() + 4 {
2713 self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
2714 } else if idx == self.cw_log_group_column_ids.len() + 5 {
2715 self.log_groups_state.log_groups.page_size = PageSize::Fifty;
2716 } else if idx == self.cw_log_group_column_ids.len() + 6 {
2717 self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2718 }
2719 }
2720 }
2721 Action::NextPreferences => {
2722 if self.current_service == Service::ApiGatewayApis {
2723 cycle_preference_next(
2724 &mut self.column_selector_index,
2725 self.apig_api_column_ids.len(),
2726 );
2727 } else if self.current_service == Service::CloudWatchAlarms {
2728 if self.column_selector_index < 18 {
2730 self.column_selector_index = 18; } else if self.column_selector_index < 22 {
2732 self.column_selector_index = 22; } else if self.column_selector_index < 28 {
2734 self.column_selector_index = 28; } else {
2736 self.column_selector_index = 0; }
2738 } else if self.current_service == Service::EcrRepositories
2739 && self.ecr_state.current_repository.is_some()
2740 {
2741 let page_size_idx = self.ecr_image_column_ids.len() + 2;
2743 if self.column_selector_index < page_size_idx {
2744 self.column_selector_index = page_size_idx;
2745 } else {
2746 self.column_selector_index = 0;
2747 }
2748 } else if self.current_service == Service::LambdaFunctions {
2749 let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2751 if self.column_selector_index < page_size_idx {
2752 self.column_selector_index = page_size_idx;
2753 } else {
2754 self.column_selector_index = 0;
2755 }
2756 } else if self.current_service == Service::LambdaApplications {
2757 let page_size_idx = self.lambda_application_column_ids.len() + 2;
2759 if self.column_selector_index < page_size_idx {
2760 self.column_selector_index = page_size_idx;
2761 } else {
2762 self.column_selector_index = 0;
2763 }
2764 } else if self.current_service == Service::CloudFormationStacks {
2765 let page_size_idx = self.cfn_column_ids.len() + 2;
2767 if self.column_selector_index < page_size_idx {
2768 self.column_selector_index = page_size_idx;
2769 } else {
2770 self.column_selector_index = 0;
2771 }
2772 } else if self.current_service == Service::CloudTrailEvents {
2773 if self.cloudtrail_state.current_event.is_some()
2774 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2775 {
2776 self.column_selector_index = 0;
2778 } else {
2779 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2780 if self.column_selector_index < page_size_idx {
2781 self.column_selector_index = page_size_idx;
2782 } else {
2783 self.column_selector_index = 0;
2784 }
2785 }
2786 } else if self.current_service == Service::Ec2Instances {
2787 let page_size_idx = self.ec2_column_ids.len() + 2;
2788 if self.column_selector_index < page_size_idx {
2789 self.column_selector_index = page_size_idx;
2790 } else {
2791 self.column_selector_index = 0;
2792 }
2793 } else if self.current_service == Service::IamUsers {
2794 if self.iam_state.current_user.is_some() {
2795 match self.iam_state.user_tab {
2796 UserTab::Permissions => {
2797 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2799 if self.column_selector_index < page_size_idx {
2800 self.column_selector_index = page_size_idx;
2801 } else {
2802 self.column_selector_index = 0;
2803 }
2804 }
2805 UserTab::Groups | UserTab::Tags => {
2806 if self.column_selector_index < 4 {
2808 self.column_selector_index = 4;
2809 } else {
2810 self.column_selector_index = 0;
2811 }
2812 }
2813 UserTab::LastAccessed => {
2814 if self.column_selector_index < 5 {
2816 self.column_selector_index = 5;
2817 } else {
2818 self.column_selector_index = 0;
2819 }
2820 }
2821 _ => {}
2822 }
2823 } else {
2824 let page_size_idx = self.iam_user_column_ids.len() + 2;
2826 if self.column_selector_index < page_size_idx {
2827 self.column_selector_index = page_size_idx;
2828 } else {
2829 self.column_selector_index = 0;
2830 }
2831 }
2832 } else if self.current_service == Service::IamRoles {
2833 if self.iam_state.current_role.is_some() {
2834 match self.iam_state.role_tab {
2835 RoleTab::Permissions => {
2836 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2838 if self.column_selector_index < page_size_idx {
2839 self.column_selector_index = page_size_idx;
2840 } else {
2841 self.column_selector_index = 0;
2842 }
2843 }
2844 RoleTab::Tags => {
2845 if self.column_selector_index < 4 {
2847 self.column_selector_index = 4;
2848 } else {
2849 self.column_selector_index = 0;
2850 }
2851 }
2852 RoleTab::LastAccessed => {
2853 if self.column_selector_index < 5 {
2855 self.column_selector_index = 5;
2856 } else {
2857 self.column_selector_index = 0;
2858 }
2859 }
2860 _ => {}
2861 }
2862 } else {
2863 let page_size_idx = self.iam_role_column_ids.len() + 2;
2865 if self.column_selector_index < page_size_idx {
2866 self.column_selector_index = page_size_idx;
2867 } else {
2868 self.column_selector_index = 0;
2869 }
2870 }
2871 } else if self.current_service == Service::IamUserGroups {
2872 let page_size_idx = self.iam_group_column_ids.len() + 2;
2874 if self.column_selector_index < page_size_idx {
2875 self.column_selector_index = page_size_idx;
2876 } else {
2877 self.column_selector_index = 0;
2878 }
2879 } else if self.current_service == Service::SqsQueues
2880 && self.sqs_state.current_queue.is_some()
2881 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2882 {
2883 let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2885 if self.column_selector_index < page_size_idx {
2886 self.column_selector_index = page_size_idx;
2887 } else {
2888 self.column_selector_index = 0;
2889 }
2890 } else if self.current_service == Service::SqsQueues
2891 && self.sqs_state.current_queue.is_some()
2892 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2893 {
2894 let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2896 if self.column_selector_index < page_size_idx {
2897 self.column_selector_index = page_size_idx;
2898 } else {
2899 self.column_selector_index = 0;
2900 }
2901 } else if self.current_service == Service::SqsQueues
2902 && self.sqs_state.current_queue.is_some()
2903 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2904 {
2905 let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2907 if self.column_selector_index < page_size_idx {
2908 self.column_selector_index = page_size_idx;
2909 } else {
2910 self.column_selector_index = 0;
2911 }
2912 } else if self.current_service == Service::SqsQueues
2913 && self.sqs_state.current_queue.is_some()
2914 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2915 {
2916 let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2918 if self.column_selector_index < page_size_idx {
2919 self.column_selector_index = page_size_idx;
2920 } else {
2921 self.column_selector_index = 0;
2922 }
2923 } else if self.current_service == Service::S3Buckets
2924 && self.s3_state.current_bucket.is_none()
2925 {
2926 let page_size_idx = self.s3_bucket_column_ids.len() + 2;
2927 if self.column_selector_index < page_size_idx {
2928 self.column_selector_index = page_size_idx;
2929 } else {
2930 self.column_selector_index = 0;
2931 }
2932 } else if self.current_service == Service::CloudWatchLogGroups {
2933 if self.view_mode == ViewMode::Events {
2934 } else if self.view_mode == ViewMode::Detail {
2936 if self.log_groups_state.detail_tab == DetailTab::Tags {
2937 cycle_preference_next(
2939 &mut self.column_selector_index,
2940 self.cw_log_tag_column_ids.len(),
2941 );
2942 } else {
2943 cycle_preference_next(
2945 &mut self.column_selector_index,
2946 self.cw_log_stream_column_ids.len(),
2947 );
2948 }
2949 } else {
2950 cycle_preference_next(
2952 &mut self.column_selector_index,
2953 self.cw_log_group_column_ids.len(),
2954 );
2955 }
2956 }
2957 }
2958 Action::PrevPreferences => {
2959 if self.current_service == Service::ApiGatewayApis {
2960 cycle_preference_prev(
2961 &mut self.column_selector_index,
2962 self.apig_api_column_ids.len(),
2963 );
2964 } else if self.current_service == Service::CloudWatchAlarms {
2965 if self.column_selector_index >= 28 {
2967 self.column_selector_index = 22;
2968 } else if self.column_selector_index >= 22 {
2969 self.column_selector_index = 18;
2970 } else if self.column_selector_index >= 18 {
2971 self.column_selector_index = 0;
2972 } else {
2973 self.column_selector_index = 28;
2974 }
2975 } else if self.current_service == Service::EcrRepositories
2976 && self.ecr_state.current_repository.is_some()
2977 {
2978 let page_size_idx = self.ecr_image_column_ids.len() + 2;
2979 if self.column_selector_index >= page_size_idx {
2980 self.column_selector_index = 0;
2981 } else {
2982 self.column_selector_index = page_size_idx;
2983 }
2984 } else if self.current_service == Service::LambdaFunctions {
2985 let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2986 if self.column_selector_index >= page_size_idx {
2987 self.column_selector_index = 0;
2988 } else {
2989 self.column_selector_index = page_size_idx;
2990 }
2991 } else if self.current_service == Service::LambdaApplications {
2992 let page_size_idx = self.lambda_application_column_ids.len() + 2;
2993 if self.column_selector_index >= page_size_idx {
2994 self.column_selector_index = 0;
2995 } else {
2996 self.column_selector_index = page_size_idx;
2997 }
2998 } else if self.current_service == Service::CloudFormationStacks {
2999 let page_size_idx = self.cfn_column_ids.len() + 2;
3000 if self.column_selector_index >= page_size_idx {
3001 self.column_selector_index = 0;
3002 } else {
3003 self.column_selector_index = page_size_idx;
3004 }
3005 } else if self.current_service == Service::CloudTrailEvents {
3006 if self.cloudtrail_state.current_event.is_some()
3007 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
3008 {
3009 self.column_selector_index = 0;
3011 } else {
3012 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
3013 if self.column_selector_index >= page_size_idx {
3014 self.column_selector_index = 0;
3015 } else {
3016 self.column_selector_index = page_size_idx;
3017 }
3018 }
3019 } else if self.current_service == Service::Ec2Instances {
3020 let page_size_idx = self.ec2_column_ids.len() + 2;
3021 if self.column_selector_index >= page_size_idx {
3022 self.column_selector_index = 0;
3023 } else {
3024 self.column_selector_index = page_size_idx;
3025 }
3026 } else if self.current_service == Service::IamUsers {
3027 if self.iam_state.current_user.is_some() {
3028 match self.iam_state.user_tab {
3029 UserTab::Permissions => {
3030 let page_size_idx = self.iam_policy_column_ids.len() + 2;
3031 if self.column_selector_index >= page_size_idx {
3032 self.column_selector_index = 0;
3033 } else {
3034 self.column_selector_index = page_size_idx;
3035 }
3036 }
3037 UserTab::Groups | UserTab::Tags => {
3038 if self.column_selector_index >= 4 {
3039 self.column_selector_index = 0;
3040 } else {
3041 self.column_selector_index = 4;
3042 }
3043 }
3044 UserTab::LastAccessed => {
3045 if self.column_selector_index >= 5 {
3046 self.column_selector_index = 0;
3047 } else {
3048 self.column_selector_index = 5;
3049 }
3050 }
3051 _ => {}
3052 }
3053 } else {
3054 let page_size_idx = self.iam_user_column_ids.len() + 2;
3055 if self.column_selector_index >= page_size_idx {
3056 self.column_selector_index = 0;
3057 } else {
3058 self.column_selector_index = page_size_idx;
3059 }
3060 }
3061 } else if self.current_service == Service::IamRoles {
3062 if self.iam_state.current_role.is_some() {
3063 match self.iam_state.role_tab {
3064 RoleTab::Permissions => {
3065 let page_size_idx = self.iam_policy_column_ids.len() + 2;
3066 if self.column_selector_index >= page_size_idx {
3067 self.column_selector_index = 0;
3068 } else {
3069 self.column_selector_index = page_size_idx;
3070 }
3071 }
3072 RoleTab::Tags => {
3073 if self.column_selector_index >= 4 {
3074 self.column_selector_index = 0;
3075 } else {
3076 self.column_selector_index = 4;
3077 }
3078 }
3079 RoleTab::LastAccessed => {
3080 if self.column_selector_index >= 5 {
3081 self.column_selector_index = 0;
3082 } else {
3083 self.column_selector_index = 5;
3084 }
3085 }
3086 _ => {}
3087 }
3088 } else {
3089 let page_size_idx = self.iam_role_column_ids.len() + 2;
3090 if self.column_selector_index >= page_size_idx {
3091 self.column_selector_index = 0;
3092 } else {
3093 self.column_selector_index = page_size_idx;
3094 }
3095 }
3096 } else if self.current_service == Service::IamUserGroups {
3097 let page_size_idx = self.iam_group_column_ids.len() + 2;
3098 if self.column_selector_index >= page_size_idx {
3099 self.column_selector_index = 0;
3100 } else {
3101 self.column_selector_index = page_size_idx;
3102 }
3103 } else if self.current_service == Service::SqsQueues
3104 && self.sqs_state.current_queue.is_some()
3105 {
3106 let page_size_idx = match self.sqs_state.detail_tab {
3107 SqsQueueDetailTab::LambdaTriggers => {
3108 self.sqs_state.trigger_column_ids.len() + 2
3109 }
3110 SqsQueueDetailTab::EventBridgePipes => {
3111 self.sqs_state.pipe_column_ids.len() + 2
3112 }
3113 SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
3114 SqsQueueDetailTab::SnsSubscriptions => {
3115 self.sqs_state.subscription_column_ids.len() + 2
3116 }
3117 _ => 0,
3118 };
3119 if page_size_idx > 0 {
3120 if self.column_selector_index >= page_size_idx {
3121 self.column_selector_index = 0;
3122 } else {
3123 self.column_selector_index = page_size_idx;
3124 }
3125 }
3126 } else if self.current_service == Service::S3Buckets
3127 && self.s3_state.current_bucket.is_none()
3128 {
3129 let page_size_idx = self.s3_bucket_column_ids.len() + 2;
3130 if self.column_selector_index >= page_size_idx {
3131 self.column_selector_index = 0;
3132 } else {
3133 self.column_selector_index = page_size_idx;
3134 }
3135 } else if self.current_service == Service::CloudWatchLogGroups {
3136 if self.view_mode == ViewMode::Events {
3137 } else if self.view_mode == ViewMode::Detail {
3139 if self.log_groups_state.detail_tab == DetailTab::Tags {
3140 cycle_preference_prev(
3142 &mut self.column_selector_index,
3143 self.cw_log_tag_column_ids.len(),
3144 );
3145 } else {
3146 cycle_preference_prev(
3148 &mut self.column_selector_index,
3149 self.cw_log_stream_column_ids.len(),
3150 );
3151 }
3152 } else {
3153 cycle_preference_prev(
3155 &mut self.column_selector_index,
3156 self.cw_log_group_column_ids.len(),
3157 );
3158 }
3159 }
3160 }
3161 Action::CloseColumnSelector => {
3162 self.mode = Mode::Normal;
3163 self.preference_section = Preferences::Columns;
3164 }
3165 Action::NextDetailTab => {
3166 if self.current_service == Service::CloudTrailEvents
3167 && self.cloudtrail_state.current_event.is_some()
3168 {
3169 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
3170 } else if self.current_service == Service::ApiGatewayApis
3171 && self.apig_state.current_api.is_some()
3172 {
3173 self.apig_state.detail_tab = self.apig_state.detail_tab.next();
3174 } else if self.current_service == Service::SqsQueues
3175 && self.sqs_state.current_queue.is_some()
3176 {
3177 self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
3178 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
3179 self.sqs_state.set_metrics_loading(true);
3180 self.sqs_state.set_monitoring_scroll(0);
3181 self.sqs_state.clear_metrics();
3182 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
3183 self.sqs_state.triggers.loading = true;
3184 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
3185 self.sqs_state.pipes.loading = true;
3186 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
3187 self.sqs_state.tags.loading = true;
3188 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
3189 self.sqs_state.subscriptions.loading = true;
3190 }
3191 } else if self.current_service == Service::Ec2Instances
3192 && self.ec2_state.current_instance.is_some()
3193 {
3194 self.ec2_state.detail_tab = self.ec2_state.detail_tab.next();
3195 if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
3196 self.ec2_state.tags.loading = true;
3197 } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
3198 self.ec2_state.set_metrics_loading(true);
3199 self.ec2_state.set_monitoring_scroll(0);
3200 self.ec2_state.clear_metrics();
3201 }
3202 } else if self.current_service == Service::LambdaApplications
3203 && self.lambda_application_state.current_application.is_some()
3204 {
3205 self.lambda_application_state.detail_tab =
3206 self.lambda_application_state.detail_tab.next();
3207 } else if self.current_service == Service::IamRoles
3208 && self.iam_state.current_role.is_some()
3209 {
3210 self.iam_state.role_tab = self.iam_state.role_tab.next();
3211 if self.iam_state.role_tab == RoleTab::Tags {
3212 self.iam_state.tags.loading = true;
3213 }
3214 } else if self.current_service == Service::IamUsers
3215 && self.iam_state.current_user.is_some()
3216 {
3217 self.iam_state.user_tab = self.iam_state.user_tab.next();
3218 if self.iam_state.user_tab == UserTab::Tags {
3219 self.iam_state.user_tags.loading = true;
3220 }
3221 } else if self.current_service == Service::IamUserGroups
3222 && self.iam_state.current_group.is_some()
3223 {
3224 self.iam_state.group_tab = self.iam_state.group_tab.next();
3225 } else if self.view_mode == ViewMode::Detail {
3226 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
3227 if self.log_groups_state.detail_tab == DetailTab::Tags {
3228 self.log_groups_state.tags.loading = true;
3229 }
3230 } else if self.current_service == Service::S3Buckets {
3231 if self.s3_state.current_bucket.is_some() {
3232 self.s3_state.object_tab = self.s3_state.object_tab.next();
3233 } else {
3234 self.s3_state.bucket_type = match self.s3_state.bucket_type {
3235 S3BucketType::GeneralPurpose => S3BucketType::Directory,
3236 S3BucketType::Directory => S3BucketType::GeneralPurpose,
3237 };
3238 self.s3_state.buckets.reset();
3239 }
3240 } else if self.current_service == Service::CloudWatchAlarms {
3241 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
3242 AlarmTab::AllAlarms => AlarmTab::InAlarm,
3243 AlarmTab::InAlarm => AlarmTab::AllAlarms,
3244 };
3245 self.alarms_state.table.reset();
3246 } else if self.current_service == Service::EcrRepositories
3247 && self.ecr_state.current_repository.is_none()
3248 {
3249 self.ecr_state.tab = self.ecr_state.tab.next();
3250 self.ecr_state.repositories.reset();
3251 self.ecr_state.repositories.loading = true;
3252 } else if self.current_service == Service::LambdaFunctions
3253 && self.lambda_state.current_function.is_some()
3254 {
3255 if self.lambda_state.current_version.is_some() {
3256 self.lambda_state.version_detail_tab =
3258 self.lambda_state.version_detail_tab.next();
3259 self.lambda_state.detail_tab =
3260 self.lambda_state.version_detail_tab.to_detail_tab();
3261 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3262 self.lambda_state.set_metrics_loading(true);
3263 self.lambda_state.set_monitoring_scroll(0);
3264 self.lambda_state.clear_metrics();
3265 }
3266 } else {
3267 self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
3268 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3269 self.lambda_state.set_metrics_loading(true);
3270 self.lambda_state.set_monitoring_scroll(0);
3271 self.lambda_state.clear_metrics();
3272 }
3273 }
3274 } else if self.current_service == Service::CloudFormationStacks
3275 && self.cfn_state.current_stack.is_some()
3276 {
3277 self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
3278 }
3279 }
3280 Action::PrevDetailTab => {
3281 if self.current_service == Service::CloudTrailEvents
3282 && self.cloudtrail_state.current_event.is_some()
3283 {
3284 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
3285 } else if self.current_service == Service::ApiGatewayApis
3286 && self.apig_state.current_api.is_some()
3287 {
3288 self.apig_state.detail_tab = self.apig_state.detail_tab.prev();
3289 } else if self.current_service == Service::SqsQueues
3290 && self.sqs_state.current_queue.is_some()
3291 {
3292 self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
3293 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
3294 self.sqs_state.set_metrics_loading(true);
3295 self.sqs_state.set_monitoring_scroll(0);
3296 self.sqs_state.clear_metrics();
3297 }
3298 } else if self.current_service == Service::Ec2Instances
3299 && self.ec2_state.current_instance.is_some()
3300 {
3301 self.ec2_state.detail_tab = self.ec2_state.detail_tab.prev();
3302 if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
3303 self.ec2_state.tags.loading = true;
3304 } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
3305 self.ec2_state.set_metrics_loading(true);
3306 self.ec2_state.set_monitoring_scroll(0);
3307 self.ec2_state.clear_metrics();
3308 }
3309 } else if self.current_service == Service::LambdaApplications
3310 && self.lambda_application_state.current_application.is_some()
3311 {
3312 self.lambda_application_state.detail_tab =
3313 self.lambda_application_state.detail_tab.prev();
3314 } else if self.current_service == Service::IamRoles
3315 && self.iam_state.current_role.is_some()
3316 {
3317 self.iam_state.role_tab = self.iam_state.role_tab.prev();
3318 } else if self.current_service == Service::IamUsers
3319 && self.iam_state.current_user.is_some()
3320 {
3321 self.iam_state.user_tab = self.iam_state.user_tab.prev();
3322 } else if self.current_service == Service::IamUserGroups
3323 && self.iam_state.current_group.is_some()
3324 {
3325 self.iam_state.group_tab = self.iam_state.group_tab.prev();
3326 } else if self.view_mode == ViewMode::Detail {
3327 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
3328 } else if self.current_service == Service::S3Buckets {
3329 if self.s3_state.current_bucket.is_some() {
3330 self.s3_state.object_tab = self.s3_state.object_tab.prev();
3331 }
3332 } else if self.current_service == Service::CloudWatchAlarms {
3333 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
3334 AlarmTab::AllAlarms => AlarmTab::InAlarm,
3335 AlarmTab::InAlarm => AlarmTab::AllAlarms,
3336 };
3337 } else if self.current_service == Service::EcrRepositories
3338 && self.ecr_state.current_repository.is_none()
3339 {
3340 self.ecr_state.tab = self.ecr_state.tab.prev();
3341 self.ecr_state.repositories.reset();
3342 self.ecr_state.repositories.loading = true;
3343 } else if self.current_service == Service::LambdaFunctions
3344 && self.lambda_state.current_function.is_some()
3345 {
3346 if self.lambda_state.current_version.is_some() {
3347 self.lambda_state.version_detail_tab =
3349 self.lambda_state.version_detail_tab.prev();
3350 self.lambda_state.detail_tab =
3351 self.lambda_state.version_detail_tab.to_detail_tab();
3352 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3353 self.lambda_state.set_metrics_loading(true);
3354 self.lambda_state.set_monitoring_scroll(0);
3355 self.lambda_state.clear_metrics();
3356 }
3357 } else {
3358 self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
3359 if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3360 self.lambda_state.set_metrics_loading(true);
3361 self.lambda_state.set_monitoring_scroll(0);
3362 self.lambda_state.clear_metrics();
3363 }
3364 }
3365 } else if self.current_service == Service::CloudFormationStacks
3366 && self.cfn_state.current_stack.is_some()
3367 {
3368 self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
3369 }
3370 }
3371 Action::StartFilter => {
3372 if !self.service_selected && self.tabs.is_empty() {
3374 return;
3375 }
3376
3377 if self.current_service == Service::CloudWatchInsights {
3378 self.mode = Mode::InsightsInput;
3379 } else if self.current_service == Service::CloudWatchAlarms {
3380 self.mode = Mode::FilterInput;
3381 } else if self.current_service == Service::CloudTrailEvents {
3382 self.mode = Mode::FilterInput;
3383 self.cloudtrail_state.input_focus = InputFocus::Filter;
3384 } else if self.current_service == Service::S3Buckets {
3385 self.mode = Mode::FilterInput;
3386 self.log_groups_state.filter_mode = true;
3387 } else if self.current_service == Service::ApiGatewayApis
3388 || self.current_service == Service::EcrRepositories
3389 || self.current_service == Service::IamUsers
3390 || self.current_service == Service::IamUserGroups
3391 {
3392 self.mode = Mode::FilterInput;
3393 if self.current_service == Service::ApiGatewayApis {
3394 self.apig_state.input_focus = InputFocus::Filter;
3395 } else if self.current_service == Service::EcrRepositories
3396 && self.ecr_state.current_repository.is_none()
3397 {
3398 self.ecr_state.input_focus = InputFocus::Filter;
3399 }
3400 } else if self.current_service == Service::LambdaFunctions {
3401 self.mode = Mode::FilterInput;
3402 if self.lambda_state.current_version.is_some()
3403 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3404 {
3405 self.lambda_state.alias_input_focus = InputFocus::Filter;
3406 } else if self.lambda_state.current_function.is_some()
3407 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3408 {
3409 self.lambda_state.version_input_focus = InputFocus::Filter;
3410 } else if self.lambda_state.current_function.is_none() {
3411 self.lambda_state.input_focus = InputFocus::Filter;
3412 }
3413 } else if self.current_service == Service::LambdaApplications {
3414 self.mode = Mode::FilterInput;
3415 if self.lambda_application_state.current_application.is_some() {
3416 if self.lambda_application_state.detail_tab
3418 == LambdaApplicationDetailTab::Overview
3419 {
3420 self.lambda_application_state.resource_input_focus = InputFocus::Filter;
3421 } else {
3422 self.lambda_application_state.deployment_input_focus =
3423 InputFocus::Filter;
3424 }
3425 } else {
3426 self.lambda_application_state.input_focus = InputFocus::Filter;
3427 }
3428 } else if self.current_service == Service::IamRoles {
3429 self.mode = Mode::FilterInput;
3430 } else if self.current_service == Service::CloudFormationStacks {
3431 self.mode = Mode::FilterInput;
3432 if self.cfn_state.current_stack.is_some()
3433 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3434 {
3435 self.cfn_state.parameters_input_focus = InputFocus::Filter;
3436 } else if self.cfn_state.current_stack.is_some()
3437 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3438 {
3439 self.cfn_state.outputs_input_focus = InputFocus::Filter;
3440 } else {
3441 self.cfn_state.input_focus = InputFocus::Filter;
3442 }
3443 } else if self.current_service == Service::SqsQueues {
3444 self.mode = Mode::FilterInput;
3445 self.sqs_state.input_focus = InputFocus::Filter;
3446 } else if self.view_mode == ViewMode::List
3447 || (self.view_mode == ViewMode::Detail
3448 && (self.log_groups_state.detail_tab == DetailTab::LogStreams
3449 || self.log_groups_state.detail_tab == DetailTab::Tags))
3450 {
3451 self.mode = Mode::FilterInput;
3452 self.log_groups_state.filter_mode = true;
3453 self.log_groups_state.input_focus = InputFocus::Filter;
3454 } else if self.view_mode == ViewMode::Events {
3455 self.mode = Mode::EventFilterInput;
3456 }
3457 }
3458 Action::StartEventFilter => {
3459 if self.current_service == Service::CloudWatchInsights {
3460 self.mode = Mode::InsightsInput;
3461 } else if self.view_mode == ViewMode::List {
3462 self.mode = Mode::FilterInput;
3463 self.log_groups_state.filter_mode = true;
3464 self.log_groups_state.input_focus = InputFocus::Filter;
3465 } else if self.view_mode == ViewMode::Events {
3466 self.mode = Mode::EventFilterInput;
3467 } else if self.view_mode == ViewMode::Detail
3468 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3469 {
3470 self.mode = Mode::FilterInput;
3471 self.log_groups_state.filter_mode = true;
3472 self.log_groups_state.input_focus = InputFocus::Filter;
3473 }
3474 }
3475 Action::NextFilterFocus => {
3476 if self.current_service == Service::CloudTrailEvents
3477 && self.cloudtrail_state.current_event.is_some()
3478 {
3479 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
3480 } else if self.mode == Mode::FilterInput
3481 && self.current_service == Service::S3Buckets
3482 {
3483 const S3_FILTER_CONTROLS: [InputFocus; 2] =
3484 [InputFocus::Filter, InputFocus::Pagination];
3485 self.s3_state.input_focus = self.s3_state.input_focus.next(&S3_FILTER_CONTROLS);
3486 } else if self.mode == Mode::FilterInput
3487 && self.current_service == Service::Ec2Instances
3488 {
3489 self.ec2_state.input_focus =
3490 self.ec2_state.input_focus.next(&ec2::FILTER_CONTROLS);
3491 } else if self.mode == Mode::FilterInput
3492 && self.current_service == Service::LambdaApplications
3493 {
3494 use crate::ui::lambda::FILTER_CONTROLS;
3495 if self.lambda_application_state.current_application.is_some() {
3496 if self.lambda_application_state.detail_tab
3497 == LambdaApplicationDetailTab::Deployments
3498 {
3499 self.lambda_application_state.deployment_input_focus = self
3500 .lambda_application_state
3501 .deployment_input_focus
3502 .next(&FILTER_CONTROLS);
3503 } else {
3504 self.lambda_application_state.resource_input_focus = self
3505 .lambda_application_state
3506 .resource_input_focus
3507 .next(&FILTER_CONTROLS);
3508 }
3509 } else {
3510 self.lambda_application_state.input_focus = self
3511 .lambda_application_state
3512 .input_focus
3513 .next(&FILTER_CONTROLS);
3514 }
3515 } else if self.mode == Mode::FilterInput
3516 && self.current_service == Service::IamRoles
3517 && self.iam_state.current_role.is_some()
3518 {
3519 use crate::ui::iam::POLICY_FILTER_CONTROLS;
3520 self.iam_state.policy_input_focus = self
3521 .iam_state
3522 .policy_input_focus
3523 .next(&POLICY_FILTER_CONTROLS);
3524 } else if self.mode == Mode::FilterInput
3525 && self.current_service == Service::IamRoles
3526 && self.iam_state.current_role.is_none()
3527 {
3528 use crate::ui::iam::ROLE_FILTER_CONTROLS;
3529 self.iam_state.role_input_focus =
3530 self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
3531 } else if self.mode == Mode::FilterInput
3532 && self.current_service == Service::IamUsers
3533 && self.iam_state.current_user.is_some()
3534 {
3535 use crate::ui::iam::{
3536 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3537 USER_SIMPLE_FILTER_CONTROLS,
3538 };
3539 if self.iam_state.user_tab == UserTab::Permissions {
3540 self.iam_state.policy_input_focus = self
3541 .iam_state
3542 .policy_input_focus
3543 .next(&POLICY_FILTER_CONTROLS);
3544 } else if self.iam_state.user_tab == UserTab::LastAccessed {
3545 self.iam_state.last_accessed_input_focus = self
3546 .iam_state
3547 .last_accessed_input_focus
3548 .next(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3549 } else {
3550 self.iam_state.user_input_focus = self
3551 .iam_state
3552 .user_input_focus
3553 .next(&USER_SIMPLE_FILTER_CONTROLS);
3554 }
3555 } else if self.mode == Mode::FilterInput
3556 && self.current_service == Service::IamUserGroups
3557 {
3558 use crate::ui::iam::GROUP_FILTER_CONTROLS;
3559 self.iam_state.group_input_focus = self
3560 .iam_state
3561 .group_input_focus
3562 .next(&GROUP_FILTER_CONTROLS);
3563 } else if self.mode == Mode::InsightsInput {
3564 use crate::app::InsightsFocus;
3565 self.insights_state.insights.insights_focus =
3566 match self.insights_state.insights.insights_focus {
3567 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
3568 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
3569 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
3570 InsightsFocus::Query => InsightsFocus::QueryLanguage,
3571 };
3572 } else if self.mode == Mode::FilterInput
3573 && self.current_service == Service::CloudFormationStacks
3574 {
3575 if self.cfn_state.current_stack.is_some()
3576 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3577 {
3578 self.cfn_state.parameters_input_focus = self
3579 .cfn_state
3580 .parameters_input_focus
3581 .next(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3582 } else if self.cfn_state.current_stack.is_some()
3583 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3584 {
3585 self.cfn_state.outputs_input_focus = self
3586 .cfn_state
3587 .outputs_input_focus
3588 .next(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3589 } else if self.cfn_state.current_stack.is_some()
3590 && self.cfn_state.detail_tab == CfnDetailTab::Resources
3591 {
3592 self.cfn_state.resources_input_focus = self
3593 .cfn_state
3594 .resources_input_focus
3595 .next(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3596 } else {
3597 self.cfn_state.input_focus = self
3598 .cfn_state
3599 .input_focus
3600 .next(&CfnStateConstants::FILTER_CONTROLS);
3601 }
3602 } else if self.mode == Mode::FilterInput
3603 && self.current_service == Service::SqsQueues
3604 {
3605 if self.sqs_state.current_queue.is_some()
3606 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3607 {
3608 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3609 self.sqs_state.input_focus = self
3610 .sqs_state
3611 .input_focus
3612 .next(SUBSCRIPTION_FILTER_CONTROLS);
3613 } else {
3614 use crate::ui::sqs::FILTER_CONTROLS;
3615 self.sqs_state.input_focus =
3616 self.sqs_state.input_focus.next(FILTER_CONTROLS);
3617 }
3618 } else if self.mode == Mode::FilterInput
3619 && self.current_service == Service::CloudWatchLogGroups
3620 {
3621 use crate::ui::cw::logs::FILTER_CONTROLS;
3622 self.log_groups_state.input_focus =
3623 self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
3624 } else if self.mode == Mode::EventFilterInput {
3625 self.log_groups_state.event_input_focus =
3626 self.log_groups_state.event_input_focus.next();
3627 } else if self.mode == Mode::FilterInput
3628 && self.current_service == Service::CloudWatchAlarms
3629 {
3630 use crate::ui::cw::alarms::FILTER_CONTROLS;
3631 self.alarms_state.input_focus =
3632 self.alarms_state.input_focus.next(&FILTER_CONTROLS);
3633 } else if self.mode == Mode::FilterInput
3634 && self.current_service == Service::CloudTrailEvents
3635 {
3636 const FILTER_CONTROLS: [InputFocus; 2] =
3637 [InputFocus::Filter, InputFocus::Pagination];
3638 self.cloudtrail_state.input_focus =
3639 self.cloudtrail_state.input_focus.next(&FILTER_CONTROLS);
3640 } else if self.mode == Mode::FilterInput
3641 && self.current_service == Service::ApiGatewayApis
3642 {
3643 use crate::ui::apig::FILTER_CONTROLS;
3644 self.apig_state.input_focus =
3645 self.apig_state.input_focus.next(&FILTER_CONTROLS);
3646 } else if self.mode == Mode::FilterInput
3647 && self.current_service == Service::EcrRepositories
3648 && self.ecr_state.current_repository.is_none()
3649 {
3650 use crate::ui::ecr::FILTER_CONTROLS;
3651 self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
3652 } else if self.mode == Mode::FilterInput
3653 && self.current_service == Service::LambdaFunctions
3654 {
3655 use crate::ui::lambda::FILTER_CONTROLS;
3656 if self.lambda_state.current_version.is_some()
3657 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3658 {
3659 self.lambda_state.alias_input_focus =
3660 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
3661 } else if self.lambda_state.current_function.is_some()
3662 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3663 {
3664 self.lambda_state.version_input_focus =
3665 self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
3666 } else if self.lambda_state.current_function.is_some()
3667 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3668 {
3669 self.lambda_state.alias_input_focus =
3670 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
3671 } else if self.lambda_state.current_function.is_none() {
3672 self.lambda_state.input_focus =
3673 self.lambda_state.input_focus.next(&FILTER_CONTROLS);
3674 }
3675 }
3676 }
3677 Action::PrevFilterFocus => {
3678 if self.current_service == Service::CloudTrailEvents
3679 && self.cloudtrail_state.current_event.is_some()
3680 {
3681 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
3682 } else if self.mode == Mode::FilterInput
3683 && self.current_service == Service::ApiGatewayApis
3684 {
3685 use crate::ui::apig::FILTER_CONTROLS;
3686 self.apig_state.input_focus =
3687 self.apig_state.input_focus.prev(&FILTER_CONTROLS);
3688 } else if self.mode == Mode::FilterInput
3689 && self.current_service == Service::Ec2Instances
3690 {
3691 self.ec2_state.input_focus =
3692 self.ec2_state.input_focus.prev(&ec2::FILTER_CONTROLS);
3693 } else if self.mode == Mode::FilterInput
3694 && self.current_service == Service::LambdaApplications
3695 {
3696 use crate::ui::lambda::FILTER_CONTROLS;
3697 if self.lambda_application_state.current_application.is_some() {
3698 if self.lambda_application_state.detail_tab
3699 == LambdaApplicationDetailTab::Deployments
3700 {
3701 self.lambda_application_state.deployment_input_focus = self
3702 .lambda_application_state
3703 .deployment_input_focus
3704 .prev(&FILTER_CONTROLS);
3705 } else {
3706 self.lambda_application_state.resource_input_focus = self
3707 .lambda_application_state
3708 .resource_input_focus
3709 .prev(&FILTER_CONTROLS);
3710 }
3711 } else {
3712 self.lambda_application_state.input_focus = self
3713 .lambda_application_state
3714 .input_focus
3715 .prev(&FILTER_CONTROLS);
3716 }
3717 } else if self.mode == Mode::FilterInput
3718 && self.current_service == Service::CloudFormationStacks
3719 {
3720 if self.cfn_state.current_stack.is_some()
3721 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3722 {
3723 self.cfn_state.parameters_input_focus = self
3724 .cfn_state
3725 .parameters_input_focus
3726 .prev(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3727 } else if self.cfn_state.current_stack.is_some()
3728 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3729 {
3730 self.cfn_state.outputs_input_focus = self
3731 .cfn_state
3732 .outputs_input_focus
3733 .prev(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3734 } else if self.cfn_state.current_stack.is_some()
3735 && self.cfn_state.detail_tab == CfnDetailTab::Resources
3736 {
3737 self.cfn_state.resources_input_focus = self
3738 .cfn_state
3739 .resources_input_focus
3740 .prev(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3741 } else {
3742 self.cfn_state.input_focus = self
3743 .cfn_state
3744 .input_focus
3745 .prev(&CfnStateConstants::FILTER_CONTROLS);
3746 }
3747 } else if self.mode == Mode::FilterInput
3748 && self.current_service == Service::SqsQueues
3749 {
3750 if self.sqs_state.current_queue.is_some()
3751 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3752 {
3753 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3754 self.sqs_state.input_focus = self
3755 .sqs_state
3756 .input_focus
3757 .prev(SUBSCRIPTION_FILTER_CONTROLS);
3758 } else {
3759 use crate::ui::sqs::FILTER_CONTROLS;
3760 self.sqs_state.input_focus =
3761 self.sqs_state.input_focus.prev(FILTER_CONTROLS);
3762 }
3763 } else if self.mode == Mode::FilterInput
3764 && self.current_service == Service::IamRoles
3765 && self.iam_state.current_role.is_none()
3766 {
3767 use crate::ui::iam::ROLE_FILTER_CONTROLS;
3768 self.iam_state.role_input_focus =
3769 self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
3770 } else if self.mode == Mode::FilterInput
3771 && self.current_service == Service::IamUsers
3772 && self.iam_state.current_user.is_some()
3773 {
3774 use crate::ui::iam::{
3775 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3776 USER_SIMPLE_FILTER_CONTROLS,
3777 };
3778 if self.iam_state.user_tab == UserTab::Permissions {
3779 self.iam_state.policy_input_focus = self
3780 .iam_state
3781 .policy_input_focus
3782 .prev(&POLICY_FILTER_CONTROLS);
3783 } else if self.iam_state.user_tab == UserTab::LastAccessed {
3784 self.iam_state.last_accessed_input_focus = self
3785 .iam_state
3786 .last_accessed_input_focus
3787 .prev(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3788 } else {
3789 self.iam_state.user_input_focus = self
3790 .iam_state
3791 .user_input_focus
3792 .prev(&USER_SIMPLE_FILTER_CONTROLS);
3793 }
3794 } else if self.mode == Mode::FilterInput
3795 && self.current_service == Service::IamUserGroups
3796 {
3797 use crate::ui::iam::GROUP_FILTER_CONTROLS;
3798 self.iam_state.group_input_focus = self
3799 .iam_state
3800 .group_input_focus
3801 .prev(&GROUP_FILTER_CONTROLS);
3802 } else if self.mode == Mode::FilterInput
3803 && self.current_service == Service::CloudWatchLogGroups
3804 {
3805 use crate::ui::cw::logs::FILTER_CONTROLS;
3806 self.log_groups_state.input_focus =
3807 self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
3808 } else if self.mode == Mode::EventFilterInput {
3809 self.log_groups_state.event_input_focus =
3810 self.log_groups_state.event_input_focus.prev();
3811 } else if self.mode == Mode::FilterInput
3812 && self.current_service == Service::IamRoles
3813 && self.iam_state.current_role.is_some()
3814 {
3815 use crate::ui::iam::POLICY_FILTER_CONTROLS;
3816 self.iam_state.policy_input_focus = self
3817 .iam_state
3818 .policy_input_focus
3819 .prev(&POLICY_FILTER_CONTROLS);
3820 } else if self.mode == Mode::FilterInput
3821 && self.current_service == Service::CloudWatchAlarms
3822 {
3823 use crate::ui::cw::alarms::FILTER_CONTROLS;
3824 self.alarms_state.input_focus =
3825 self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
3826 } else if self.mode == Mode::FilterInput
3827 && self.current_service == Service::CloudTrailEvents
3828 {
3829 const FILTER_CONTROLS: [InputFocus; 2] =
3830 [InputFocus::Filter, InputFocus::Pagination];
3831 self.cloudtrail_state.input_focus =
3832 self.cloudtrail_state.input_focus.prev(&FILTER_CONTROLS);
3833 } else if self.mode == Mode::FilterInput
3834 && self.current_service == Service::EcrRepositories
3835 && self.ecr_state.current_repository.is_none()
3836 {
3837 use crate::ui::ecr::FILTER_CONTROLS;
3838 self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
3839 } else if self.mode == Mode::FilterInput
3840 && self.current_service == Service::LambdaFunctions
3841 {
3842 use crate::ui::lambda::FILTER_CONTROLS;
3843 if self.lambda_state.current_version.is_some()
3844 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3845 {
3846 self.lambda_state.alias_input_focus =
3847 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
3848 } else if self.lambda_state.current_function.is_some()
3849 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3850 {
3851 self.lambda_state.version_input_focus =
3852 self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
3853 } else if self.lambda_state.current_function.is_some()
3854 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3855 {
3856 self.lambda_state.alias_input_focus =
3857 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
3858 } else if self.lambda_state.current_function.is_none() {
3859 self.lambda_state.input_focus =
3860 self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
3861 }
3862 }
3863 }
3864 Action::ToggleFilterCheckbox => {
3865 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
3866 if self.ec2_state.input_focus == EC2_STATE_FILTER {
3867 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
3868 self.ec2_state.table.reset();
3869 }
3870 } else if self.mode == Mode::InsightsInput {
3871 use crate::app::InsightsFocus;
3872 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3873 && self.insights_state.insights.show_dropdown
3874 && !self.insights_state.insights.log_group_matches.is_empty()
3875 {
3876 let selected_idx = self.insights_state.insights.dropdown_selected;
3877 if let Some(group_name) = self
3878 .insights_state
3879 .insights
3880 .log_group_matches
3881 .get(selected_idx)
3882 {
3883 let group_name = group_name.clone();
3884 if let Some(pos) = self
3885 .insights_state
3886 .insights
3887 .selected_log_groups
3888 .iter()
3889 .position(|g| g == &group_name)
3890 {
3891 self.insights_state.insights.selected_log_groups.remove(pos);
3892 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
3893 self.insights_state
3894 .insights
3895 .selected_log_groups
3896 .push(group_name);
3897 }
3898 }
3899 }
3900 } else if self.mode == Mode::FilterInput
3901 && self.current_service == Service::CloudFormationStacks
3902 {
3903 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
3904 match self.cfn_state.input_focus {
3905 STATUS_FILTER => {
3906 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3907 self.cfn_state.table.reset();
3908 }
3909 VIEW_NESTED => {
3910 self.cfn_state.view_nested = !self.cfn_state.view_nested;
3911 self.cfn_state.table.reset();
3912 }
3913 _ => {}
3914 }
3915 } else if self.mode == Mode::FilterInput
3916 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3917 {
3918 match self.log_groups_state.input_focus {
3919 InputFocus::Checkbox("ExactMatch") => {
3920 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
3921 }
3922 InputFocus::Checkbox("ShowExpired") => {
3923 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
3924 }
3925 _ => {}
3926 }
3927 } else if self.mode == Mode::EventFilterInput
3928 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
3929 {
3930 self.log_groups_state.relative_unit =
3931 self.log_groups_state.relative_unit.next();
3932 }
3933 }
3934 Action::CycleSortColumn => {
3935 if self.view_mode == ViewMode::Detail
3936 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3937 {
3938 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
3939 StreamSort::Name => StreamSort::CreationTime,
3940 StreamSort::CreationTime => StreamSort::LastEventTime,
3941 StreamSort::LastEventTime => StreamSort::Name,
3942 };
3943 }
3944 }
3945 Action::ToggleSortDirection => {
3946 if self.view_mode == ViewMode::Detail
3947 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3948 {
3949 self.log_groups_state.stream_sort_desc =
3950 !self.log_groups_state.stream_sort_desc;
3951 }
3952 }
3953 Action::ScrollUp => {
3954 if self.mode == Mode::ErrorModal {
3955 self.error_scroll = self.error_scroll.saturating_sub(1);
3956 } else if self.current_service == Service::CloudTrailEvents
3957 && self.cloudtrail_state.current_event.is_some()
3958 {
3959 self.cloudtrail_state.event_json_scroll =
3960 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
3961 } else if self.current_service == Service::LambdaFunctions
3962 && self.lambda_state.current_function.is_some()
3963 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3964 && !self.lambda_state.is_metrics_loading()
3965 {
3966 self.lambda_state.set_monitoring_scroll(
3967 self.lambda_state.monitoring_scroll().saturating_sub(1),
3968 );
3969 } else if self.current_service == Service::Ec2Instances
3970 && self.ec2_state.current_instance.is_some()
3971 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
3972 && !self.ec2_state.is_metrics_loading()
3973 {
3974 self.ec2_state.set_monitoring_scroll(
3975 self.ec2_state.monitoring_scroll().saturating_sub(1),
3976 );
3977 } else if self.current_service == Service::SqsQueues
3978 && self.sqs_state.current_queue.is_some()
3979 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3980 && !self.sqs_state.is_metrics_loading()
3981 {
3982 self.sqs_state.set_monitoring_scroll(
3983 self.sqs_state.monitoring_scroll().saturating_sub(1),
3984 );
3985 } else if self.view_mode == ViewMode::PolicyView {
3986 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
3987 } else if self.current_service == Service::IamRoles
3988 && self.iam_state.current_role.is_some()
3989 && self.iam_state.role_tab == RoleTab::TrustRelationships
3990 {
3991 self.iam_state.trust_policy_scroll =
3992 self.iam_state.trust_policy_scroll.saturating_sub(10);
3993 } else if self.view_mode == ViewMode::Events {
3994 if self.log_groups_state.event_scroll_offset == 0
3995 && self.log_groups_state.has_older_events
3996 {
3997 self.log_groups_state.loading = true;
3998 } else {
3999 self.log_groups_state.event_scroll_offset =
4000 self.log_groups_state.event_scroll_offset.saturating_sub(1);
4001 }
4002 } else if self.view_mode == ViewMode::InsightsResults {
4003 self.insights_state.insights.results_selected = self
4004 .insights_state
4005 .insights
4006 .results_selected
4007 .saturating_sub(1);
4008 } else if self.view_mode == ViewMode::Detail {
4009 self.log_groups_state.selected_stream =
4010 self.log_groups_state.selected_stream.saturating_sub(1);
4011 self.log_groups_state.expanded_stream = None;
4012 } else if self.view_mode == ViewMode::List
4013 && self.current_service == Service::CloudWatchLogGroups
4014 {
4015 self.log_groups_state.log_groups.selected =
4016 self.log_groups_state.log_groups.selected.saturating_sub(1);
4017 self.log_groups_state.log_groups.snap_to_page();
4018 } else if self.current_service == Service::EcrRepositories {
4019 if self.ecr_state.current_repository.is_some() {
4020 self.ecr_state.images.page_up();
4021 } else {
4022 self.ecr_state.repositories.page_up();
4023 }
4024 }
4025 }
4026 Action::ScrollDown => {
4027 if self.mode == Mode::ErrorModal {
4028 if let Some(error_msg) = &self.error_message {
4029 let lines = error_msg.lines().count();
4030 let max_scroll = lines.saturating_sub(1);
4031 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
4032 }
4033 } else if self.current_service == Service::CloudTrailEvents
4034 && self.cloudtrail_state.current_event.is_some()
4035 {
4036 if let Some(event) = &self.cloudtrail_state.current_event {
4037 let lines = event.cloud_trail_event_json.lines().count();
4038 let max_scroll = lines.saturating_sub(1);
4039 self.cloudtrail_state.event_json_scroll =
4040 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
4041 }
4042 } else if self.current_service == Service::SqsQueues
4043 && self.sqs_state.current_queue.is_some()
4044 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4045 {
4046 self.sqs_state
4047 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
4048 } else if self.view_mode == ViewMode::PolicyView {
4049 let lines = self.iam_state.policy_document.lines().count();
4050 let max_scroll = lines.saturating_sub(1);
4051 self.iam_state.policy_scroll =
4052 (self.iam_state.policy_scroll + 10).min(max_scroll);
4053 } else if self.current_service == Service::IamRoles
4054 && self.iam_state.current_role.is_some()
4055 && self.iam_state.role_tab == RoleTab::TrustRelationships
4056 {
4057 let lines = self.iam_state.trust_policy_document.lines().count();
4058 let max_scroll = lines.saturating_sub(1);
4059 self.iam_state.trust_policy_scroll =
4060 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
4061 } else if self.view_mode == ViewMode::Events {
4062 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
4063 if self.log_groups_state.event_scroll_offset >= max_scroll {
4064 } else {
4066 self.log_groups_state.event_scroll_offset =
4067 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
4068 }
4069 } else if self.view_mode == ViewMode::InsightsResults {
4070 let max = self
4071 .insights_state
4072 .insights
4073 .query_results
4074 .len()
4075 .saturating_sub(1);
4076 self.insights_state.insights.results_selected =
4077 (self.insights_state.insights.results_selected + 1).min(max);
4078 } else if self.view_mode == ViewMode::Detail {
4079 let filtered_streams = filtered_log_streams(self);
4080 let max = filtered_streams.len().saturating_sub(1);
4081 self.log_groups_state.selected_stream =
4082 (self.log_groups_state.selected_stream + 1).min(max);
4083 } else if self.view_mode == ViewMode::List
4084 && self.current_service == Service::CloudWatchLogGroups
4085 {
4086 let filtered_groups = filtered_log_groups(self);
4087 self.log_groups_state
4088 .log_groups
4089 .next_item(filtered_groups.len());
4090 } else if self.current_service == Service::EcrRepositories {
4091 if self.ecr_state.current_repository.is_some() {
4092 let filtered_images = filtered_ecr_images(self);
4093 self.ecr_state.images.page_down(filtered_images.len());
4094 } else {
4095 let filtered_repos = filtered_ecr_repositories(self);
4096 self.ecr_state.repositories.page_down(filtered_repos.len());
4097 }
4098 }
4099 }
4100
4101 Action::Refresh => {
4102 if self.mode == Mode::ProfilePicker {
4103 self.log_groups_state.loading = true;
4104 self.log_groups_state.loading_message = "Refreshing...".to_string();
4105 } else if self.mode == Mode::RegionPicker {
4106 self.measure_region_latencies();
4107 } else if self.mode == Mode::SessionPicker {
4108 self.sessions = Session::list_all().unwrap_or_default();
4109 } else if self.current_service == Service::CloudWatchInsights
4110 && !self.insights_state.insights.selected_log_groups.is_empty()
4111 {
4112 self.log_groups_state.loading = true;
4113 self.insights_state.insights.query_completed = true;
4114 } else if self.current_service == Service::LambdaFunctions {
4115 self.lambda_state.table.loading = true;
4116 } else if self.current_service == Service::LambdaApplications {
4117 self.lambda_application_state.table.loading = true;
4118 } else if matches!(
4119 self.view_mode,
4120 ViewMode::Events | ViewMode::Detail | ViewMode::List
4121 ) {
4122 self.log_groups_state.loading = true;
4123 }
4124 }
4125 Action::Yank => {
4126 if self.mode == Mode::ErrorModal {
4127 if let Some(error) = &self.error_message {
4129 copy_to_clipboard(error);
4130 }
4131 } else if self.view_mode == ViewMode::Events {
4132 if let Some(event) = self
4133 .log_groups_state
4134 .log_events
4135 .get(self.log_groups_state.event_scroll_offset)
4136 {
4137 copy_to_clipboard(&event.message);
4138 }
4139 } else if self.current_service == Service::EcrRepositories {
4140 if self.ecr_state.current_repository.is_some() {
4141 let filtered_images = filtered_ecr_images(self);
4142 if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
4143 copy_to_clipboard(&image.uri);
4144 }
4145 } else {
4146 let filtered_repos = filtered_ecr_repositories(self);
4147 if let Some(repo) =
4148 self.ecr_state.repositories.get_selected(&filtered_repos)
4149 {
4150 copy_to_clipboard(&repo.uri);
4151 }
4152 }
4153 } else if self.current_service == Service::LambdaFunctions {
4154 let filtered_functions = filtered_lambda_functions(self);
4155 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
4156 copy_to_clipboard(&func.arn);
4157 }
4158 } else if self.current_service == Service::CloudFormationStacks {
4159 if let Some(stack_name) = &self.cfn_state.current_stack {
4160 if let Some(stack) = self
4162 .cfn_state
4163 .table
4164 .items
4165 .iter()
4166 .find(|s| &s.name == stack_name)
4167 {
4168 copy_to_clipboard(&stack.stack_id);
4169 }
4170 } else {
4171 let filtered_stacks = filtered_cloudformation_stacks(self);
4173 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
4174 copy_to_clipboard(&stack.stack_id);
4175 }
4176 }
4177 } else if self.current_service == Service::IamUsers {
4178 if self.iam_state.current_user.is_some() {
4179 if let Some(user_name) = &self.iam_state.current_user {
4180 if let Some(user) = self
4181 .iam_state
4182 .users
4183 .items
4184 .iter()
4185 .find(|u| u.user_name == *user_name)
4186 {
4187 copy_to_clipboard(&user.arn);
4188 }
4189 }
4190 } else {
4191 let filtered_users = filtered_iam_users(self);
4192 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
4193 copy_to_clipboard(&user.arn);
4194 }
4195 }
4196 } else if self.current_service == Service::IamRoles {
4197 if self.iam_state.current_role.is_some() {
4198 if let Some(role_name) = &self.iam_state.current_role {
4199 if let Some(role) = self
4200 .iam_state
4201 .roles
4202 .items
4203 .iter()
4204 .find(|r| r.role_name == *role_name)
4205 {
4206 copy_to_clipboard(&role.arn);
4207 }
4208 }
4209 } else {
4210 let filtered_roles = filtered_iam_roles(self);
4211 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
4212 copy_to_clipboard(&role.arn);
4213 }
4214 }
4215 } else if self.current_service == Service::IamUserGroups {
4216 if self.iam_state.current_group.is_some() {
4217 if let Some(group_name) = &self.iam_state.current_group {
4218 let arn = iam::format_arn(&self.config.account_id, "group", group_name);
4219 copy_to_clipboard(&arn);
4220 }
4221 } else {
4222 let filtered_groups: Vec<_> = self
4223 .iam_state
4224 .groups
4225 .items
4226 .iter()
4227 .filter(|g| {
4228 if self.iam_state.groups.filter.is_empty() {
4229 true
4230 } else {
4231 g.group_name
4232 .to_lowercase()
4233 .contains(&self.iam_state.groups.filter.to_lowercase())
4234 }
4235 })
4236 .collect();
4237 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
4238 let arn = iam::format_arn(
4239 &self.config.account_id,
4240 "group",
4241 &group.group_name,
4242 );
4243 copy_to_clipboard(&arn);
4244 }
4245 }
4246 } else if self.current_service == Service::SqsQueues {
4247 if self.sqs_state.current_queue.is_some() {
4248 if let Some(queue) = self
4250 .sqs_state
4251 .queues
4252 .items
4253 .iter()
4254 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
4255 {
4256 let arn = format!(
4257 "arn:aws:sqs:{}:{}:{}",
4258 extract_region(&queue.url),
4259 extract_account_id(&queue.url),
4260 queue.name
4261 );
4262 copy_to_clipboard(&arn);
4263 }
4264 } else {
4265 let filtered_queues = filtered_queues(
4267 &self.sqs_state.queues.items,
4268 &self.sqs_state.queues.filter,
4269 );
4270 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
4271 let arn = format!(
4272 "arn:aws:sqs:{}:{}:{}",
4273 extract_region(&queue.url),
4274 extract_account_id(&queue.url),
4275 queue.name
4276 );
4277 copy_to_clipboard(&arn);
4278 }
4279 }
4280 } else if self.current_service == Service::ApiGatewayApis {
4281 if let Some(_api) = &self.apig_state.current_api {
4282 use crate::ui::apig::ApiDetailTab;
4283 if self.apig_state.detail_tab == ApiDetailTab::Routes {
4284 let (filtered_routes, _) = crate::ui::apig::filter_tree_items(
4286 &self.apig_state.routes.items,
4287 &self.apig_state.route_children,
4288 &self.apig_state.route_filter,
4289 );
4290 let filtered_refs: Vec<&Route> = filtered_routes.iter().collect();
4291 if let Some(route) = self.apig_state.routes.get_selected(&filtered_refs)
4292 {
4293 if !route.arn.is_empty() {
4294 copy_to_clipboard(&route.arn);
4295 }
4296 }
4297 }
4298 }
4299 }
4300 }
4301 Action::CopyToClipboard => {
4302 self.snapshot_requested = true;
4304 }
4305 Action::RetryLoad => {
4306 self.error_message = None;
4307 self.mode = Mode::Normal;
4308 self.log_groups_state.loading = true;
4309 }
4310 Action::ApplyFilter => {
4311 if self.mode == Mode::FilterInput
4312 && self.current_service == Service::SqsQueues
4313 && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
4314 {
4315 let regions = AwsRegion::all();
4316 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
4317 self.sqs_state.subscription_region_filter = region.code.to_string();
4318 }
4319 self.mode = Mode::Normal;
4320 } else if self.mode == Mode::InsightsInput {
4321 use crate::app::InsightsFocus;
4322 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4323 && self.insights_state.insights.show_dropdown
4324 {
4325 self.insights_state.insights.show_dropdown = false;
4327 self.mode = Mode::Normal;
4328 if !self.insights_state.insights.selected_log_groups.is_empty() {
4329 self.log_groups_state.loading = true;
4330 self.insights_state.insights.query_completed = true;
4331 }
4332 }
4333 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
4334 if let Ok(page) = self.page_input.parse::<usize>() {
4335 self.go_to_page(page);
4336 }
4337 self.page_input.clear();
4338 } else {
4339 self.mode = Mode::Normal;
4340 self.log_groups_state.filter_mode = false;
4341 }
4342 }
4343 Action::ToggleExactMatch => {
4344 if self.view_mode == ViewMode::Detail
4345 && self.log_groups_state.detail_tab == DetailTab::LogStreams
4346 {
4347 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
4348 }
4349 }
4350 Action::ToggleShowExpired => {
4351 if self.view_mode == ViewMode::Detail
4352 && self.log_groups_state.detail_tab == DetailTab::LogStreams
4353 {
4354 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
4355 }
4356 }
4357 Action::GoBack => {
4358 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
4360 self.mode = Mode::Normal;
4361 self.service_picker.filter.clear();
4362 }
4363 else if self.current_service == Service::ApiGatewayApis
4365 && self.apig_state.current_api.is_some()
4366 {
4367 self.apig_state.current_api = None;
4368 self.apig_state.routes.items.clear();
4369 self.apig_state.detail_tab = crate::ui::apig::ApiDetailTab::Routes;
4370 self.update_current_tab_breadcrumb();
4371 }
4372 else if self.current_service == Service::S3Buckets
4374 && self.s3_state.current_bucket.is_some()
4375 {
4376 if !self.s3_state.prefix_stack.is_empty() {
4377 self.s3_state.prefix_stack.pop();
4378 self.s3_state.buckets.loading = true;
4379 } else {
4380 self.s3_state.current_bucket = None;
4381 self.s3_state.objects.clear();
4382 }
4383 }
4384 else if self.current_service == Service::EcrRepositories
4386 && self.ecr_state.current_repository.is_some()
4387 {
4388 if self.ecr_state.images.has_expanded_item() {
4389 self.ecr_state.images.collapse();
4390 } else {
4391 self.ecr_state.current_repository = None;
4392 self.ecr_state.current_repository_uri = None;
4393 self.ecr_state.images.items.clear();
4394 self.ecr_state.images.reset();
4395 }
4396 }
4397 else if self.current_service == Service::Ec2Instances
4399 && self.ec2_state.current_instance.is_some()
4400 {
4401 self.ec2_state.current_instance = None;
4402 self.view_mode = ViewMode::List;
4403 self.update_current_tab_breadcrumb();
4404 }
4405 else if self.current_service == Service::SqsQueues
4407 && self.sqs_state.current_queue.is_some()
4408 {
4409 self.sqs_state.current_queue = None;
4410 }
4411 else if self.current_service == Service::IamUsers
4413 && self.iam_state.current_user.is_some()
4414 {
4415 self.iam_state.current_user = None;
4416 self.iam_state.policies.items.clear();
4417 self.iam_state.policies.reset();
4418 self.update_current_tab_breadcrumb();
4419 }
4420 else if self.current_service == Service::IamUserGroups
4422 && self.iam_state.current_group.is_some()
4423 {
4424 self.iam_state.current_group = None;
4425 self.update_current_tab_breadcrumb();
4426 }
4427 else if self.current_service == Service::IamRoles {
4429 if self.view_mode == ViewMode::PolicyView {
4430 self.view_mode = ViewMode::Detail;
4432 self.iam_state.current_policy = None;
4433 self.iam_state.policy_document.clear();
4434 self.iam_state.policy_scroll = 0;
4435 self.update_current_tab_breadcrumb();
4436 } else if self.iam_state.current_role.is_some() {
4437 self.iam_state.current_role = None;
4438 self.iam_state.policies.items.clear();
4439 self.iam_state.policies.reset();
4440 self.update_current_tab_breadcrumb();
4441 }
4442 }
4443 else if self.current_service == Service::LambdaFunctions
4445 && self.lambda_state.current_version.is_some()
4446 {
4447 self.lambda_state.current_version = None;
4448 self.lambda_state.detail_tab = LambdaDetailTab::Versions;
4449 }
4450 else if self.current_service == Service::LambdaFunctions
4452 && self.lambda_state.current_alias.is_some()
4453 {
4454 self.lambda_state.current_alias = None;
4455 self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
4456 }
4457 else if self.current_service == Service::LambdaFunctions
4459 && self.lambda_state.current_function.is_some()
4460 {
4461 self.lambda_state.current_function = None;
4462 self.update_current_tab_breadcrumb();
4463 }
4464 else if self.current_service == Service::LambdaApplications
4466 && self.lambda_application_state.current_application.is_some()
4467 {
4468 self.lambda_application_state.current_application = None;
4469 self.update_current_tab_breadcrumb();
4470 }
4471 else if self.current_service == Service::CloudFormationStacks
4473 && self.cfn_state.current_stack.is_some()
4474 {
4475 self.cfn_state.current_stack = None;
4476 self.update_current_tab_breadcrumb();
4477 }
4478 else if self.current_service == Service::CloudTrailEvents
4480 && self.cloudtrail_state.current_event.is_some()
4481 {
4482 self.cloudtrail_state.current_event = None;
4483 self.update_current_tab_breadcrumb();
4484 }
4485 else if self.view_mode == ViewMode::InsightsResults {
4487 if self.insights_state.insights.expanded_result.is_some() {
4488 self.insights_state.insights.expanded_result = None;
4489 }
4490 }
4491 else if self.current_service == Service::CloudWatchAlarms {
4493 if self.alarms_state.table.has_expanded_item() {
4494 self.alarms_state.table.collapse();
4495 }
4496 }
4497 else if self.current_service == Service::Ec2Instances {
4499 if self.ec2_state.current_instance.is_some()
4500 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4501 {
4502 self.ec2_state.tags.collapse();
4503 } else {
4504 self.ec2_state.table.collapse();
4505 }
4506 }
4507 else if self.view_mode == ViewMode::Events {
4509 if self.log_groups_state.expanded_event.is_some() {
4510 self.log_groups_state.expanded_event = None;
4511 } else {
4512 self.view_mode = ViewMode::Detail;
4513 self.log_groups_state.event_filter.clear();
4514 }
4515 }
4516 else if self.view_mode == ViewMode::Detail {
4518 self.view_mode = ViewMode::List;
4519 self.log_groups_state.stream_filter.clear();
4520 self.log_groups_state.exact_match = false;
4521 self.log_groups_state.show_expired = false;
4522 }
4523 }
4524 Action::OpenInConsole | Action::OpenInBrowser => {
4525 let url = self.get_console_url();
4526 let _ = webbrowser::open(&url);
4527 }
4528 Action::ShowHelp => {
4529 self.mode = Mode::HelpModal;
4530 }
4531 Action::OpenRegionPicker => {
4532 self.region_filter.clear();
4533 self.region_picker_selected = 0;
4534 self.mode = Mode::RegionPicker;
4535 }
4536 Action::OpenProfilePicker => {
4537 self.profile_filter.clear();
4538 self.profile_picker_selected = 0;
4539 self.available_profiles = Self::load_aws_profiles();
4540 self.mode = Mode::ProfilePicker;
4541 }
4542 Action::OpenCalendar => {
4543 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
4544 self.calendar_selecting = CalendarField::StartDate;
4545 self.mode = Mode::CalendarPicker;
4546 }
4547 Action::CloseCalendar => {
4548 self.mode = Mode::Normal;
4549 self.calendar_date = None;
4550 }
4551 Action::CalendarPrevDay => {
4552 if let Some(date) = self.calendar_date {
4553 self.calendar_date = date.checked_sub(time::Duration::days(1));
4554 }
4555 }
4556 Action::CalendarNextDay => {
4557 if let Some(date) = self.calendar_date {
4558 self.calendar_date = date.checked_add(time::Duration::days(1));
4559 }
4560 }
4561 Action::CalendarPrevWeek => {
4562 if let Some(date) = self.calendar_date {
4563 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
4564 }
4565 }
4566 Action::CalendarNextWeek => {
4567 if let Some(date) = self.calendar_date {
4568 self.calendar_date = date.checked_add(time::Duration::weeks(1));
4569 }
4570 }
4571 Action::CalendarPrevMonth => {
4572 if let Some(date) = self.calendar_date {
4573 self.calendar_date = Some(if date.month() == time::Month::January {
4574 date.replace_month(time::Month::December)
4575 .unwrap()
4576 .replace_year(date.year() - 1)
4577 .unwrap()
4578 } else {
4579 date.replace_month(date.month().previous()).unwrap()
4580 });
4581 }
4582 }
4583 Action::CalendarNextMonth => {
4584 if let Some(date) = self.calendar_date {
4585 self.calendar_date = Some(if date.month() == time::Month::December {
4586 date.replace_month(time::Month::January)
4587 .unwrap()
4588 .replace_year(date.year() + 1)
4589 .unwrap()
4590 } else {
4591 date.replace_month(date.month().next()).unwrap()
4592 });
4593 }
4594 }
4595 Action::CalendarSelect => {
4596 if let Some(date) = self.calendar_date {
4597 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
4598 .unix_timestamp()
4599 * 1000;
4600 match self.calendar_selecting {
4601 CalendarField::StartDate => {
4602 self.log_groups_state.start_time = Some(timestamp);
4603 self.calendar_selecting = CalendarField::EndDate;
4604 }
4605 CalendarField::EndDate => {
4606 self.log_groups_state.end_time = Some(timestamp);
4607 self.mode = Mode::Normal;
4608 self.calendar_date = None;
4609 }
4610 }
4611 }
4612 }
4613 }
4614 }
4615
4616 pub fn filtered_services(&self) -> Vec<&'static str> {
4617 let mut services = if self.service_picker.filter.is_empty() {
4618 self.service_picker.services.clone()
4619 } else {
4620 self.service_picker
4621 .services
4622 .iter()
4623 .filter(|s| {
4624 s.to_lowercase()
4625 .contains(&self.service_picker.filter.to_lowercase())
4626 })
4627 .copied()
4628 .collect()
4629 };
4630 services.sort();
4631 services
4632 }
4633
4634 pub fn breadcrumbs(&self) -> String {
4635 if !self.service_selected {
4636 return String::new();
4637 }
4638
4639 let mut parts = vec![];
4640
4641 match self.current_service {
4642 Service::CloudWatchLogGroups => {
4643 parts.push("CloudWatch".to_string());
4644 parts.push("Log groups".to_string());
4645
4646 if self.view_mode != ViewMode::List {
4647 if let Some(group) = selected_log_group(self) {
4648 parts.push(group.name.clone());
4649 }
4650 }
4651
4652 if self.view_mode == ViewMode::Events {
4653 if let Some(stream) = self
4654 .log_groups_state
4655 .log_streams
4656 .get(self.log_groups_state.selected_stream)
4657 {
4658 parts.push(stream.name.clone());
4659 }
4660 }
4661 }
4662 Service::CloudWatchInsights => {
4663 parts.push("CloudWatch".to_string());
4664 parts.push("Insights".to_string());
4665 }
4666 Service::CloudWatchAlarms => {
4667 parts.push("CloudWatch".to_string());
4668 parts.push("Alarms".to_string());
4669 }
4670 Service::CloudTrailEvents => {
4671 parts.push("CloudTrail".to_string());
4672 parts.push("Event History".to_string());
4673 }
4674 Service::S3Buckets => {
4675 parts.push("S3".to_string());
4676 if let Some(bucket) = &self.s3_state.current_bucket {
4677 parts.push(bucket.clone());
4678 if let Some(prefix) = self.s3_state.prefix_stack.last() {
4679 parts.push(prefix.trim_end_matches('/').to_string());
4680 }
4681 } else {
4682 parts.push("Buckets".to_string());
4683 }
4684 }
4685 Service::SqsQueues => {
4686 parts.push("SQS".to_string());
4687 parts.push("Queues".to_string());
4688 }
4689 Service::EcrRepositories => {
4690 parts.push("ECR".to_string());
4691 if let Some(repo) = &self.ecr_state.current_repository {
4692 parts.push(repo.clone());
4693 } else {
4694 parts.push("Repositories".to_string());
4695 }
4696 }
4697 Service::LambdaFunctions => {
4698 parts.push("Lambda".to_string());
4699 if let Some(func) = &self.lambda_state.current_function {
4700 parts.push(func.clone());
4701 } else {
4702 parts.push("Functions".to_string());
4703 }
4704 }
4705 Service::LambdaApplications => {
4706 parts.push("Lambda".to_string());
4707 parts.push("Applications".to_string());
4708 }
4709 Service::CloudFormationStacks => {
4710 parts.push("CloudFormation".to_string());
4711 if let Some(stack_name) = &self.cfn_state.current_stack {
4712 parts.push(stack_name.clone());
4713 } else {
4714 parts.push("Stacks".to_string());
4715 }
4716 }
4717 Service::IamUsers => {
4718 parts.push("IAM".to_string());
4719 parts.push("Users".to_string());
4720 }
4721 Service::IamRoles => {
4722 parts.push("IAM".to_string());
4723 parts.push("Roles".to_string());
4724 if let Some(role_name) = &self.iam_state.current_role {
4725 parts.push(role_name.clone());
4726 if let Some(policy_name) = &self.iam_state.current_policy {
4727 parts.push(policy_name.clone());
4728 }
4729 }
4730 }
4731 Service::IamUserGroups => {
4732 parts.push("IAM".to_string());
4733 parts.push("User Groups".to_string());
4734 if let Some(group_name) = &self.iam_state.current_group {
4735 parts.push(group_name.clone());
4736 }
4737 }
4738 Service::Ec2Instances => {
4739 parts.push("EC2".to_string());
4740 parts.push("Instances".to_string());
4741 }
4742 Service::ApiGatewayApis => {
4743 parts.push("API Gateway".to_string());
4744 if let Some(api) = &self.apig_state.current_api {
4745 parts.push(api.name.clone());
4746 } else {
4747 parts.push("APIs".to_string());
4748 }
4749 }
4750 }
4751
4752 parts.join(" > ")
4753 }
4754
4755 pub fn update_current_tab_breadcrumb(&mut self) {
4756 if !self.tabs.is_empty() {
4757 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
4758 }
4759 }
4760
4761 pub fn get_console_url(&self) -> String {
4762 use crate::{cfn, cw, ecr, iam, lambda, s3};
4763
4764 match self.current_service {
4765 Service::CloudWatchLogGroups => {
4766 if self.view_mode == ViewMode::Events {
4767 if let Some(group) = selected_log_group(self) {
4768 if let Some(stream) = self
4769 .log_groups_state
4770 .log_streams
4771 .get(self.log_groups_state.selected_stream)
4772 {
4773 return cw::logs::console_url_stream(
4774 &self.config.region,
4775 &group.name,
4776 &stream.name,
4777 );
4778 }
4779 }
4780 } else if self.view_mode == ViewMode::Detail {
4781 if let Some(group) = selected_log_group(self) {
4782 return cw::logs::console_url_detail(&self.config.region, &group.name);
4783 }
4784 }
4785 cw::logs::console_url_list(&self.config.region)
4786 }
4787 Service::CloudWatchInsights => cw::insights::console_url(
4788 &self.config.region,
4789 &self.config.account_id,
4790 &self.insights_state.insights.query_text,
4791 &self.insights_state.insights.selected_log_groups,
4792 ),
4793 Service::CloudWatchAlarms => {
4794 let view_type = match self.alarms_state.view_as {
4795 AlarmViewMode::Table | AlarmViewMode::Detail => "table",
4796 AlarmViewMode::Cards => "card",
4797 };
4798 cw::alarms::console_url(
4799 &self.config.region,
4800 view_type,
4801 self.alarms_state.table.page_size.value(),
4802 &self.alarms_state.sort_column,
4803 self.alarms_state.sort_direction.as_str(),
4804 )
4805 }
4806 Service::CloudTrailEvents => {
4807 if let Some(event) = &self.cloudtrail_state.current_event {
4808 format!(
4809 "https://{}.console.aws.amazon.com/cloudtrailv2/home?region={}#/events/{}",
4810 self.config.region, self.config.region, event.event_id
4811 )
4812 } else {
4813 format!(
4814 "https://{}.console.aws.amazon.com/cloudtrail/home?region={}#/events",
4815 self.config.region, self.config.region
4816 )
4817 }
4818 }
4819 Service::S3Buckets => {
4820 if let Some(bucket_name) = &self.s3_state.current_bucket {
4821 let prefix = self.s3_state.prefix_stack.join("");
4822 s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
4823 } else {
4824 s3::console_url_buckets(&self.config.region)
4825 }
4826 }
4827 Service::SqsQueues => {
4828 if let Some(queue_url) = &self.sqs_state.current_queue {
4829 console_url_queue_detail(&self.config.region, queue_url)
4830 } else {
4831 console_url_queues(&self.config.region)
4832 }
4833 }
4834 Service::EcrRepositories => {
4835 if let Some(repo_name) = &self.ecr_state.current_repository {
4836 ecr::console_url_private_repository(
4837 &self.config.region,
4838 &self.config.account_id,
4839 repo_name,
4840 )
4841 } else {
4842 ecr::console_url_repositories(&self.config.region)
4843 }
4844 }
4845 Service::LambdaFunctions => {
4846 if let Some(func_name) = &self.lambda_state.current_function {
4847 if let Some(version) = &self.lambda_state.current_version {
4848 lambda::console_url_function_version(
4849 &self.config.region,
4850 func_name,
4851 version,
4852 &self.lambda_state.detail_tab,
4853 )
4854 } else {
4855 lambda::console_url_function_detail(&self.config.region, func_name)
4856 }
4857 } else {
4858 lambda::console_url_functions(&self.config.region)
4859 }
4860 }
4861 Service::LambdaApplications => {
4862 if let Some(app_name) = &self.lambda_application_state.current_application {
4863 lambda::console_url_application_detail(
4864 &self.config.region,
4865 app_name,
4866 &self.lambda_application_state.detail_tab,
4867 )
4868 } else {
4869 lambda::console_url_applications(&self.config.region)
4870 }
4871 }
4872 Service::CloudFormationStacks => {
4873 if let Some(stack_name) = &self.cfn_state.current_stack {
4874 if let Some(stack) = self
4875 .cfn_state
4876 .table
4877 .items
4878 .iter()
4879 .find(|s| &s.name == stack_name)
4880 {
4881 return cfn::console_url_stack_detail_with_tab(
4882 &self.config.region,
4883 &stack.stack_id,
4884 &self.cfn_state.detail_tab,
4885 );
4886 }
4887 }
4888 cfn::console_url_stacks(&self.config.region)
4889 }
4890 Service::IamUsers => {
4891 if let Some(user_name) = &self.iam_state.current_user {
4892 let section = match self.iam_state.user_tab {
4893 UserTab::Permissions => "permissions",
4894 UserTab::Groups => "groups",
4895 UserTab::Tags => "tags",
4896 UserTab::SecurityCredentials => "security_credentials",
4897 UserTab::LastAccessed => "access_advisor",
4898 };
4899 iam::console_url_user_detail(&self.config.region, user_name, section)
4900 } else {
4901 iam::console_url_users(&self.config.region)
4902 }
4903 }
4904 Service::IamRoles => {
4905 if let Some(policy_name) = &self.iam_state.current_policy {
4906 if let Some(role_name) = &self.iam_state.current_role {
4907 return iam::console_url_role_policy(
4908 &self.config.region,
4909 role_name,
4910 policy_name,
4911 );
4912 }
4913 }
4914 if let Some(role_name) = &self.iam_state.current_role {
4915 let section = match self.iam_state.role_tab {
4916 RoleTab::Permissions => "permissions",
4917 RoleTab::TrustRelationships => "trust_relationships",
4918 RoleTab::Tags => "tags",
4919 RoleTab::LastAccessed => "access_advisor",
4920 RoleTab::RevokeSessions => "revoke_sessions",
4921 };
4922 iam::console_url_role_detail(&self.config.region, role_name, section)
4923 } else {
4924 iam::console_url_roles(&self.config.region)
4925 }
4926 }
4927 Service::IamUserGroups => iam::console_url_groups(&self.config.region),
4928 Service::Ec2Instances => {
4929 if let Some(instance_id) = &self.ec2_state.current_instance {
4930 format!(
4931 "https://{}.console.aws.amazon.com/ec2/home?region={}#InstanceDetails:instanceId={}",
4932 self.config.region, self.config.region, instance_id
4933 )
4934 } else {
4935 format!(
4936 "https://{}.console.aws.amazon.com/ec2/home?region={}#Instances:",
4937 self.config.region, self.config.region
4938 )
4939 }
4940 }
4941 Service::ApiGatewayApis => {
4942 use crate::apig;
4943 if let Some(api) = &self.apig_state.current_api {
4944 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4945 let protocol = api.protocol_type.to_uppercase();
4946 if protocol == "REST" {
4947 let (filtered_items, _) = crate::ui::apig::filter_tree_items(
4949 &self.apig_state.resources.items,
4950 &self.apig_state.resource_children,
4951 &self.apig_state.route_filter,
4952 );
4953
4954 let resource_id =
4955 if self.apig_state.resources.selected < filtered_items.len() {
4956 let mut current_row = 0;
4958 self.find_resource_at_row(
4959 &filtered_items,
4960 self.apig_state.resources.selected,
4961 &mut current_row,
4962 )
4963 } else {
4964 None
4965 };
4966 apig::console_url_resources(
4967 &self.config.region,
4968 &api.id,
4969 resource_id.as_deref(),
4970 )
4971 } else {
4972 let (filtered_items, filtered_children) =
4974 crate::ui::apig::filter_tree_items(
4975 &self.apig_state.routes.items,
4976 &self.apig_state.route_children,
4977 &self.apig_state.route_filter,
4978 );
4979
4980 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4981 &filtered_items,
4982 &self.apig_state.expanded_routes,
4983 &filtered_children,
4984 );
4985
4986 let route_id = if self.apig_state.routes.selected < total_rows {
4987 let mut current_row = 0;
4989 self.find_route_id_at_row_with_children(
4990 &filtered_items,
4991 &filtered_children,
4992 self.apig_state.routes.selected,
4993 &mut current_row,
4994 )
4995 } else {
4996 None
4997 };
4998 apig::console_url_routes(
4999 &self.config.region,
5000 &api.id,
5001 route_id.as_deref(),
5002 )
5003 }
5004 } else {
5005 apig::console_url_api(&self.config.region, &api.id)
5006 }
5007 } else {
5008 apig::console_url_apis(&self.config.region)
5009 }
5010 }
5011 }
5012 }
5013
5014 pub fn calculate_total_bucket_rows(&self) -> usize {
5015 calculate_total_bucket_rows(self)
5016 }
5017
5018 fn calculate_total_object_rows(&self) -> usize {
5019 calculate_total_object_rows(self)
5020 }
5021
5022 fn get_column_selector_max(&self) -> usize {
5023 if self.current_service == Service::ApiGatewayApis {
5024 self.apig_api_column_ids.len() + 6
5025 } else if self.current_service == Service::S3Buckets
5026 && self.s3_state.current_bucket.is_none()
5027 {
5028 self.s3_bucket_column_ids.len() + 6
5029 } else if self.view_mode == ViewMode::Events {
5030 self.cw_log_event_column_ids.len() - 1
5031 } else if self.view_mode == ViewMode::Detail {
5032 self.cw_log_stream_column_ids.len() + 6
5033 } else if self.current_service == Service::CloudWatchAlarms {
5034 29
5035 } else if self.current_service == Service::CloudTrailEvents {
5036 if self.cloudtrail_state.current_event.is_some()
5037 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
5038 {
5039 self.cloudtrail_resource_column_ids.len()
5040 } else {
5041 self.cloudtrail_event_column_ids.len() + 6
5042 }
5043 } else if self.current_service == Service::Ec2Instances {
5044 if self.ec2_state.current_instance.is_some()
5045 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5046 {
5047 self.ec2_state.tag_column_ids.len() + 6
5048 } else {
5049 self.ec2_column_ids.len() + 6
5050 }
5051 } else if self.current_service == Service::EcrRepositories {
5052 if self.ecr_state.current_repository.is_some() {
5053 self.ecr_image_column_ids.len() + 6
5054 } else {
5055 self.ecr_repo_column_ids.len() + 6
5056 }
5057 } else if self.current_service == Service::SqsQueues {
5058 self.sqs_column_ids.len() - 1
5059 } else if self.current_service == Service::LambdaFunctions {
5060 self.lambda_state.function_column_ids.len() + 6
5061 } else if self.current_service == Service::LambdaApplications {
5062 self.lambda_application_column_ids.len() + 5
5063 } else if self.current_service == Service::CloudFormationStacks {
5064 self.cfn_column_ids.len() + 6
5065 } else if self.current_service == Service::IamUsers {
5066 if self.iam_state.current_user.is_some() {
5067 self.iam_policy_column_ids.len() + 5
5068 } else {
5069 self.iam_user_column_ids.len() + 5
5070 }
5071 } else if self.current_service == Service::IamRoles {
5072 if self.iam_state.current_role.is_some() {
5073 self.iam_policy_column_ids.len() + 5
5074 } else {
5075 self.iam_role_column_ids.len() + 5
5076 }
5077 } else {
5078 self.cw_log_group_column_ids.len() + 6
5079 }
5080 }
5081
5082 fn get_column_count(&self) -> usize {
5083 if self.current_service == Service::ApiGatewayApis {
5084 self.apig_api_column_ids.len()
5085 } else if self.current_service == Service::S3Buckets
5086 && self.s3_state.current_bucket.is_none()
5087 {
5088 self.s3_bucket_column_ids.len()
5089 } else if self.view_mode == ViewMode::Events {
5090 self.cw_log_event_column_ids.len()
5091 } else if self.view_mode == ViewMode::Detail {
5092 self.cw_log_stream_column_ids.len()
5093 } else if self.current_service == Service::CloudWatchAlarms {
5094 14
5095 } else if self.current_service == Service::CloudTrailEvents {
5096 if self.cloudtrail_state.current_event.is_some()
5097 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
5098 {
5099 self.cloudtrail_resource_column_ids.len()
5100 } else {
5101 self.cloudtrail_event_column_ids.len()
5102 }
5103 } else if self.current_service == Service::Ec2Instances {
5104 if self.ec2_state.current_instance.is_some()
5105 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5106 {
5107 self.ec2_state.tag_column_ids.len()
5108 } else {
5109 self.ec2_column_ids.len()
5110 }
5111 } else if self.current_service == Service::EcrRepositories {
5112 if self.ecr_state.current_repository.is_some() {
5113 self.ecr_image_column_ids.len()
5114 } else {
5115 self.ecr_repo_column_ids.len()
5116 }
5117 } else if self.current_service == Service::SqsQueues {
5118 self.sqs_column_ids.len()
5119 } else if self.current_service == Service::LambdaFunctions {
5120 self.lambda_state.function_column_ids.len()
5121 } else if self.current_service == Service::LambdaApplications {
5122 self.lambda_application_column_ids.len()
5123 } else if self.current_service == Service::CloudFormationStacks {
5124 self.cfn_column_ids.len()
5125 } else if self.current_service == Service::IamUsers {
5126 if self.iam_state.current_user.is_some() {
5127 self.iam_policy_column_ids.len()
5128 } else {
5129 self.iam_user_column_ids.len()
5130 }
5131 } else if self.current_service == Service::IamRoles {
5132 if self.iam_state.current_role.is_some() {
5133 self.iam_policy_column_ids.len()
5134 } else {
5135 self.iam_role_column_ids.len()
5136 }
5137 } else {
5138 self.cw_log_group_column_ids.len()
5139 }
5140 }
5141
5142 fn is_blank_row_index(&self, idx: usize) -> bool {
5143 let column_count = self.get_column_count();
5144 idx == column_count + 1
5146 }
5147
5148 fn next_item(&mut self) {
5149 match self.mode {
5150 Mode::FilterInput => {
5151 if self.current_service == Service::S3Buckets
5152 && self.s3_state.input_focus == InputFocus::Pagination
5153 {
5154 let page_size = self.s3_state.buckets.page_size.value();
5156 let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
5157 let max_offset = total_rows.saturating_sub(page_size);
5158 self.s3_state.selected_row =
5159 (self.s3_state.selected_row + page_size).min(max_offset);
5160 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5161 } else if self.current_service == Service::CloudTrailEvents
5162 && self.cloudtrail_state.input_focus == InputFocus::Pagination
5163 {
5164 let page_size = self.cloudtrail_state.table.page_size.value();
5166 let total_items = self.cloudtrail_state.table.items.len();
5167 let current_page = self.cloudtrail_state.table.selected / page_size;
5168 let total_pages = total_items.div_ceil(page_size);
5169 if current_page + 1 < total_pages {
5170 self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
5171 }
5172 } else if self.current_service == Service::CloudFormationStacks {
5173 use crate::ui::cfn::STATUS_FILTER;
5174 if self.cfn_state.input_focus == STATUS_FILTER {
5175 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
5176 self.cfn_state.table.reset();
5177 }
5178 } else if self.current_service == Service::IamUsers
5179 && self.iam_state.current_user.is_some()
5180 {
5181 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
5182 if self.iam_state.user_tab == UserTab::Permissions
5183 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
5184 {
5185 self.cycle_policy_type_next();
5186 } else if self.iam_state.user_tab == UserTab::LastAccessed
5187 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
5188 {
5189 self.iam_state.last_accessed_history_filter =
5190 self.iam_state.last_accessed_history_filter.next();
5191 self.iam_state.last_accessed_services.reset();
5192 }
5193 } else if self.current_service == Service::IamRoles
5194 && self.iam_state.current_role.is_some()
5195 && self.iam_state.role_tab == RoleTab::Permissions
5196 {
5197 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
5198 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
5199 self.cycle_policy_type_next();
5200 }
5201 } else if self.current_service == Service::Ec2Instances {
5202 if self.ec2_state.input_focus == EC2_STATE_FILTER {
5203 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
5204 self.ec2_state.table.reset();
5205 }
5206 } else if self.current_service == Service::SqsQueues {
5207 use crate::ui::sqs::SUBSCRIPTION_REGION;
5208 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
5209 let regions = AwsRegion::all();
5210 self.sqs_state.subscription_region_selected =
5211 (self.sqs_state.subscription_region_selected + 1)
5212 .min(regions.len() - 1);
5213 self.sqs_state.subscriptions.reset();
5214 }
5215 }
5216 }
5217 Mode::RegionPicker => {
5218 let filtered = self.get_filtered_regions();
5219 if !filtered.is_empty() {
5220 self.region_picker_selected =
5221 (self.region_picker_selected + 1).min(filtered.len() - 1);
5222 }
5223 }
5224 Mode::ProfilePicker => {
5225 let filtered = self.get_filtered_profiles();
5226 if !filtered.is_empty() {
5227 self.profile_picker_selected =
5228 (self.profile_picker_selected + 1).min(filtered.len() - 1);
5229 }
5230 }
5231 Mode::SessionPicker => {
5232 let filtered = self.get_filtered_sessions();
5233 if !filtered.is_empty() {
5234 self.session_picker_selected =
5235 (self.session_picker_selected + 1).min(filtered.len() - 1);
5236 }
5237 }
5238 Mode::InsightsInput => {
5239 use crate::app::InsightsFocus;
5240 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
5241 && self.insights_state.insights.show_dropdown
5242 && !self.insights_state.insights.log_group_matches.is_empty()
5243 {
5244 let max = self.insights_state.insights.log_group_matches.len() - 1;
5245 self.insights_state.insights.dropdown_selected =
5246 (self.insights_state.insights.dropdown_selected + 1).min(max);
5247 }
5248 }
5249 Mode::ColumnSelector => {
5250 let max = self.get_column_selector_max();
5251 let mut next_idx = (self.column_selector_index + 1).min(max);
5252 if self.is_blank_row_index(next_idx) {
5254 next_idx = (next_idx + 1).min(max);
5255 }
5256 self.column_selector_index = next_idx;
5257 }
5258 Mode::ServicePicker => {
5259 let filtered = self.filtered_services();
5260 if !filtered.is_empty() {
5261 self.service_picker.selected =
5262 (self.service_picker.selected + 1).min(filtered.len() - 1);
5263 }
5264 }
5265 Mode::TabPicker => {
5266 let filtered = self.get_filtered_tabs();
5267 if !filtered.is_empty() {
5268 self.tab_picker_selected =
5269 (self.tab_picker_selected + 1).min(filtered.len() - 1);
5270 }
5271 }
5272 Mode::Normal => {
5273 if !self.service_selected {
5274 let filtered = self.filtered_services();
5275 if !filtered.is_empty() {
5276 self.service_picker.selected =
5277 (self.service_picker.selected + 1).min(filtered.len() - 1);
5278 }
5279 } else if self.current_service == Service::S3Buckets {
5280 if self.s3_state.current_bucket.is_some() {
5281 if self.s3_state.object_tab == S3ObjectTab::Properties {
5282 self.s3_state.properties_scroll =
5284 self.s3_state.properties_scroll.saturating_add(1);
5285 } else {
5286 let total_rows = self.calculate_total_object_rows();
5288 let max = total_rows.saturating_sub(1);
5289 self.s3_state.selected_object =
5290 (self.s3_state.selected_object + 1).min(max);
5291
5292 let visible_rows = self.s3_state.object_visible_rows.get();
5294 if self.s3_state.selected_object
5295 >= self.s3_state.object_scroll_offset + visible_rows
5296 {
5297 self.s3_state.object_scroll_offset =
5298 self.s3_state.selected_object - visible_rows + 1;
5299 }
5300 }
5301 } else {
5302 let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
5304 if total_rows > 0 {
5305 self.s3_state.selected_row =
5306 (self.s3_state.selected_row + 1).min(total_rows - 1);
5307
5308 let visible_rows = self.s3_state.bucket_visible_rows.get();
5310 if self.s3_state.selected_row
5311 >= self.s3_state.bucket_scroll_offset + visible_rows
5312 {
5313 self.s3_state.bucket_scroll_offset =
5314 self.s3_state.selected_row - visible_rows + 1;
5315 }
5316 }
5317 }
5318 } else if self.view_mode == ViewMode::InsightsResults {
5319 let max = self
5320 .insights_state
5321 .insights
5322 .query_results
5323 .len()
5324 .saturating_sub(1);
5325 if self.insights_state.insights.results_selected < max {
5326 self.insights_state.insights.results_selected += 1;
5327 }
5328 } else if self.current_service == Service::ApiGatewayApis {
5329 if let Some(api) = &self.apig_state.current_api {
5330 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
5331 let protocol = api.protocol_type.to_uppercase();
5332 if protocol == "REST" {
5333 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
5335 &self.apig_state.resources.items,
5336 &self.apig_state.expanded_resources,
5337 &self.apig_state.resource_children,
5338 );
5339 if total_rows > 0 {
5340 self.apig_state.resources.selected =
5341 (self.apig_state.resources.selected + 1)
5342 .min(total_rows - 1);
5343 }
5344 } else {
5345 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
5347 &self.apig_state.routes.items,
5348 &self.apig_state.expanded_routes,
5349 &self.apig_state.route_children,
5350 );
5351 if total_rows > 0 {
5352 self.apig_state.routes.selected =
5353 (self.apig_state.routes.selected + 1).min(total_rows - 1);
5354 }
5355 }
5356 }
5357 } else {
5358 let filtered = crate::ui::apig::filtered_apis(self);
5359 if !filtered.is_empty() {
5360 self.apig_state.apis.next_item(filtered.len());
5361 }
5362 }
5363 } else if self.view_mode == ViewMode::PolicyView {
5364 let lines = self.iam_state.policy_document.lines().count();
5365 let max_scroll = lines.saturating_sub(1);
5366 self.iam_state.policy_scroll =
5367 (self.iam_state.policy_scroll + 1).min(max_scroll);
5368 } else if self.current_service == Service::CloudFormationStacks
5369 && self.cfn_state.current_stack.is_some()
5370 && self.cfn_state.detail_tab == CfnDetailTab::Template
5371 {
5372 let lines = self.cfn_state.template_body.lines().count();
5373 let max_scroll = lines.saturating_sub(1);
5374 self.cfn_state.template_scroll =
5375 (self.cfn_state.template_scroll + 1).min(max_scroll);
5376 } else if self.current_service == Service::SqsQueues
5377 && self.sqs_state.current_queue.is_some()
5378 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
5379 {
5380 let lines = self.sqs_state.policy_document.lines().count();
5381 let max_scroll = lines.saturating_sub(1);
5382 self.sqs_state.policy_scroll =
5383 (self.sqs_state.policy_scroll + 1).min(max_scroll);
5384 } else if self.current_service == Service::LambdaFunctions
5385 && self.lambda_state.current_function.is_some()
5386 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5387 && !self.lambda_state.is_metrics_loading()
5388 {
5389 self.lambda_state
5390 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
5391 } else if self.current_service == Service::Ec2Instances
5392 && self.ec2_state.current_instance.is_some()
5393 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5394 && !self.ec2_state.is_metrics_loading()
5395 {
5396 self.ec2_state
5397 .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
5398 } else if self.current_service == Service::SqsQueues
5399 && self.sqs_state.current_queue.is_some()
5400 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
5401 && !self.sqs_state.is_metrics_loading()
5402 {
5403 self.sqs_state
5404 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
5405 } else if self.view_mode == ViewMode::Events {
5406 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
5407 if self.log_groups_state.event_scroll_offset >= max_scroll {
5408 } else {
5410 self.log_groups_state.event_scroll_offset =
5411 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
5412 }
5413 } else if self.current_service == Service::CloudWatchLogGroups {
5414 if self.view_mode == ViewMode::List {
5415 let filtered_groups = filtered_log_groups(self);
5416 self.log_groups_state
5417 .log_groups
5418 .next_item(filtered_groups.len());
5419 } else if self.view_mode == ViewMode::Detail {
5420 let filtered_streams = filtered_log_streams(self);
5421 if !filtered_streams.is_empty() {
5422 let max = filtered_streams.len() - 1;
5423 if self.log_groups_state.selected_stream >= max {
5424 } else {
5426 self.log_groups_state.selected_stream =
5427 (self.log_groups_state.selected_stream + 1).min(max);
5428 self.log_groups_state.expanded_stream = None;
5429 }
5430 }
5431 }
5432 } else if self.current_service == Service::CloudWatchAlarms {
5433 let filtered_alarms = match self.alarms_state.alarm_tab {
5434 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5435 AlarmTab::InAlarm => self
5436 .alarms_state
5437 .table
5438 .items
5439 .iter()
5440 .filter(|a| a.state.to_uppercase() == "ALARM")
5441 .count(),
5442 };
5443 if filtered_alarms > 0 {
5444 self.alarms_state.table.next_item(filtered_alarms);
5445 }
5446 } else if self.current_service == Service::CloudTrailEvents {
5447 if self.cloudtrail_state.current_event.is_some()
5448 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
5449 {
5450 if let Some(event) = &self.cloudtrail_state.current_event {
5452 let line_count = event.cloud_trail_event_json.lines().count();
5453 let max_scroll = line_count.saturating_sub(1);
5454 self.cloudtrail_state.event_json_scroll =
5455 (self.cloudtrail_state.event_json_scroll + 1).min(max_scroll);
5456 }
5457 } else {
5458 let page_size = self.cloudtrail_state.table.page_size.value();
5460 let current_page = self.cloudtrail_state.table.selected / page_size;
5461 let page_start = current_page * page_size;
5462 let page_end = page_start + page_size;
5463 let filtered_count = self.cloudtrail_state.table.items.len();
5464
5465 if self.cloudtrail_state.table.selected < filtered_count.saturating_sub(1) {
5467 self.cloudtrail_state.table.selected =
5468 (self.cloudtrail_state.table.selected + 1)
5469 .min(page_end - 1)
5470 .min(filtered_count - 1);
5471 }
5472 }
5473 } else if self.current_service == Service::Ec2Instances {
5474 if self.ec2_state.current_instance.is_some()
5475 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5476 {
5477 let filtered = crate::ui::ec2::filtered_tags(self);
5478 if !filtered.is_empty() {
5479 self.ec2_state.tags.next_item(filtered.len());
5480 }
5481 } else {
5482 let filtered: Vec<_> = self
5483 .ec2_state
5484 .table
5485 .items
5486 .iter()
5487 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
5488 .filter(|i| {
5489 if self.ec2_state.table.filter.is_empty() {
5490 return true;
5491 }
5492 i.name.contains(&self.ec2_state.table.filter)
5493 || i.instance_id.contains(&self.ec2_state.table.filter)
5494 || i.state.contains(&self.ec2_state.table.filter)
5495 || i.instance_type.contains(&self.ec2_state.table.filter)
5496 || i.availability_zone.contains(&self.ec2_state.table.filter)
5497 || i.security_groups.contains(&self.ec2_state.table.filter)
5498 || i.key_name.contains(&self.ec2_state.table.filter)
5499 })
5500 .collect();
5501 if !filtered.is_empty() {
5502 self.ec2_state.table.next_item(filtered.len());
5503 }
5504 }
5505 } else if self.current_service == Service::EcrRepositories {
5506 if self.ecr_state.current_repository.is_some() {
5507 let filtered_images = filtered_ecr_images(self);
5508 if !filtered_images.is_empty() {
5509 self.ecr_state.images.next_item(filtered_images.len());
5510 }
5511 } else {
5512 let filtered_repos = filtered_ecr_repositories(self);
5513 if !filtered_repos.is_empty() {
5514 self.ecr_state.repositories.selected =
5515 (self.ecr_state.repositories.selected + 1)
5516 .min(filtered_repos.len() - 1);
5517 self.ecr_state.repositories.snap_to_page();
5518 }
5519 }
5520 } else if self.current_service == Service::SqsQueues {
5521 if self.sqs_state.current_queue.is_some()
5522 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5523 {
5524 let filtered = filtered_lambda_triggers(self);
5525 if !filtered.is_empty() {
5526 self.sqs_state.triggers.next_item(filtered.len());
5527 }
5528 } else if self.sqs_state.current_queue.is_some()
5529 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5530 {
5531 let filtered = filtered_eventbridge_pipes(self);
5532 if !filtered.is_empty() {
5533 self.sqs_state.pipes.next_item(filtered.len());
5534 }
5535 } else if self.sqs_state.current_queue.is_some()
5536 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5537 {
5538 let filtered = filtered_tags(self);
5539 if !filtered.is_empty() {
5540 self.sqs_state.tags.next_item(filtered.len());
5541 }
5542 } else if self.sqs_state.current_queue.is_some()
5543 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5544 {
5545 let filtered = filtered_subscriptions(self);
5546 if !filtered.is_empty() {
5547 self.sqs_state.subscriptions.next_item(filtered.len());
5548 }
5549 } else {
5550 let filtered_queues = filtered_queues(
5551 &self.sqs_state.queues.items,
5552 &self.sqs_state.queues.filter,
5553 );
5554 if !filtered_queues.is_empty() {
5555 self.sqs_state.queues.next_item(filtered_queues.len());
5556 }
5557 }
5558 } else if self.current_service == Service::LambdaFunctions {
5559 if self.lambda_state.current_function.is_some()
5560 && self.lambda_state.detail_tab == LambdaDetailTab::Code
5561 {
5562 if let Some(func_name) = &self.lambda_state.current_function {
5564 if let Some(func) = self
5565 .lambda_state
5566 .table
5567 .items
5568 .iter()
5569 .find(|f| f.name == *func_name)
5570 {
5571 let max = func.layers.len().saturating_sub(1);
5572 if !func.layers.is_empty() {
5573 self.lambda_state.layer_selected =
5574 (self.lambda_state.layer_selected + 1).min(max);
5575 }
5576 }
5577 }
5578 } else if self.lambda_state.current_function.is_some()
5579 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5580 {
5581 let filtered: Vec<_> = self
5583 .lambda_state
5584 .version_table
5585 .items
5586 .iter()
5587 .filter(|v| {
5588 self.lambda_state.version_table.filter.is_empty()
5589 || v.version.to_lowercase().contains(
5590 &self.lambda_state.version_table.filter.to_lowercase(),
5591 )
5592 || v.aliases.to_lowercase().contains(
5593 &self.lambda_state.version_table.filter.to_lowercase(),
5594 )
5595 || v.description.to_lowercase().contains(
5596 &self.lambda_state.version_table.filter.to_lowercase(),
5597 )
5598 })
5599 .collect();
5600 if !filtered.is_empty() {
5601 self.lambda_state.version_table.selected =
5602 (self.lambda_state.version_table.selected + 1)
5603 .min(filtered.len() - 1);
5604 self.lambda_state.version_table.snap_to_page();
5605 }
5606 } else if self.lambda_state.current_function.is_some()
5607 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5608 || (self.lambda_state.current_version.is_some()
5609 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5610 {
5611 let version_filter = self.lambda_state.current_version.clone();
5613 let filtered: Vec<_> = self
5614 .lambda_state
5615 .alias_table
5616 .items
5617 .iter()
5618 .filter(|a| {
5619 (version_filter.is_none()
5620 || a.versions.contains(version_filter.as_ref().unwrap()))
5621 && (self.lambda_state.alias_table.filter.is_empty()
5622 || a.name.to_lowercase().contains(
5623 &self.lambda_state.alias_table.filter.to_lowercase(),
5624 )
5625 || a.versions.to_lowercase().contains(
5626 &self.lambda_state.alias_table.filter.to_lowercase(),
5627 )
5628 || a.description.to_lowercase().contains(
5629 &self.lambda_state.alias_table.filter.to_lowercase(),
5630 ))
5631 })
5632 .collect();
5633 if !filtered.is_empty() {
5634 self.lambda_state.alias_table.selected =
5635 (self.lambda_state.alias_table.selected + 1)
5636 .min(filtered.len() - 1);
5637 self.lambda_state.alias_table.snap_to_page();
5638 }
5639 } else if self.lambda_state.current_function.is_none() {
5640 let filtered = filtered_lambda_functions(self);
5641 if !filtered.is_empty() {
5642 self.lambda_state.table.next_item(filtered.len());
5643 self.lambda_state.table.snap_to_page();
5644 }
5645 }
5646 } else if self.current_service == Service::LambdaApplications {
5647 if self.lambda_application_state.current_application.is_some() {
5648 if self.lambda_application_state.detail_tab
5649 == LambdaApplicationDetailTab::Overview
5650 {
5651 let len = self.lambda_application_state.resources.items.len();
5652 if len > 0 {
5653 self.lambda_application_state.resources.next_item(len);
5654 }
5655 } else {
5656 let len = self.lambda_application_state.deployments.items.len();
5657 if len > 0 {
5658 self.lambda_application_state.deployments.next_item(len);
5659 }
5660 }
5661 } else {
5662 let filtered = filtered_lambda_applications(self);
5663 if !filtered.is_empty() {
5664 self.lambda_application_state.table.selected =
5665 (self.lambda_application_state.table.selected + 1)
5666 .min(filtered.len() - 1);
5667 self.lambda_application_state.table.snap_to_page();
5668 }
5669 }
5670 } else if self.current_service == Service::CloudFormationStacks {
5671 if self.cfn_state.current_stack.is_some()
5672 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5673 {
5674 let filtered = filtered_parameters(self);
5675 self.cfn_state.parameters.next_item(filtered.len());
5676 } else if self.cfn_state.current_stack.is_some()
5677 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5678 {
5679 let filtered = filtered_outputs(self);
5680 self.cfn_state.outputs.next_item(filtered.len());
5681 } else if self.cfn_state.current_stack.is_some()
5682 && self.cfn_state.detail_tab == CfnDetailTab::Resources
5683 {
5684 let filtered = filtered_resources(self);
5685 self.cfn_state.resources.next_item(filtered.len());
5686 } else {
5687 let filtered = filtered_cloudformation_stacks(self);
5688 self.cfn_state.table.next_item(filtered.len());
5689 }
5690 } else if self.current_service == Service::IamUsers {
5691 if self.iam_state.current_user.is_some() {
5692 if self.iam_state.user_tab == UserTab::Tags {
5693 let filtered = filtered_user_tags(self);
5694 if !filtered.is_empty() {
5695 self.iam_state.user_tags.next_item(filtered.len());
5696 }
5697 } else {
5698 let filtered = filtered_iam_policies(self);
5699 if !filtered.is_empty() {
5700 self.iam_state.policies.next_item(filtered.len());
5701 }
5702 }
5703 } else {
5704 let filtered = filtered_iam_users(self);
5705 if !filtered.is_empty() {
5706 self.iam_state.users.next_item(filtered.len());
5707 }
5708 }
5709 } else if self.current_service == Service::IamRoles {
5710 if self.iam_state.current_role.is_some() {
5711 if self.iam_state.role_tab == RoleTab::TrustRelationships {
5712 let lines = self.iam_state.trust_policy_document.lines().count();
5713 let max_scroll = lines.saturating_sub(1);
5714 self.iam_state.trust_policy_scroll =
5715 (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
5716 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
5717 self.iam_state.revoke_sessions_scroll =
5718 (self.iam_state.revoke_sessions_scroll + 1).min(19);
5719 } else if self.iam_state.role_tab == RoleTab::Tags {
5720 let filtered = filtered_iam_tags(self);
5721 if !filtered.is_empty() {
5722 self.iam_state.tags.next_item(filtered.len());
5723 }
5724 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5725 let filtered = filtered_last_accessed(self);
5726 if !filtered.is_empty() {
5727 self.iam_state
5728 .last_accessed_services
5729 .next_item(filtered.len());
5730 }
5731 } else {
5732 let filtered = filtered_iam_policies(self);
5733 if !filtered.is_empty() {
5734 self.iam_state.policies.next_item(filtered.len());
5735 }
5736 }
5737 } else {
5738 let filtered = filtered_iam_roles(self);
5739 if !filtered.is_empty() {
5740 self.iam_state.roles.next_item(filtered.len());
5741 }
5742 }
5743 } else if self.current_service == Service::IamUserGroups {
5744 if self.iam_state.current_group.is_some() {
5745 if self.iam_state.group_tab == GroupTab::Users {
5746 let filtered: Vec<_> = self
5747 .iam_state
5748 .group_users
5749 .items
5750 .iter()
5751 .filter(|u| {
5752 if self.iam_state.group_users.filter.is_empty() {
5753 true
5754 } else {
5755 u.user_name.to_lowercase().contains(
5756 &self.iam_state.group_users.filter.to_lowercase(),
5757 )
5758 }
5759 })
5760 .collect();
5761 if !filtered.is_empty() {
5762 self.iam_state.group_users.next_item(filtered.len());
5763 }
5764 } else if self.iam_state.group_tab == GroupTab::Permissions {
5765 let filtered = filtered_iam_policies(self);
5766 if !filtered.is_empty() {
5767 self.iam_state.policies.next_item(filtered.len());
5768 }
5769 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5770 let filtered = filtered_last_accessed(self);
5771 if !filtered.is_empty() {
5772 self.iam_state
5773 .last_accessed_services
5774 .next_item(filtered.len());
5775 }
5776 }
5777 } else {
5778 let filtered: Vec<_> = self
5779 .iam_state
5780 .groups
5781 .items
5782 .iter()
5783 .filter(|g| {
5784 if self.iam_state.groups.filter.is_empty() {
5785 true
5786 } else {
5787 g.group_name
5788 .to_lowercase()
5789 .contains(&self.iam_state.groups.filter.to_lowercase())
5790 }
5791 })
5792 .collect();
5793 if !filtered.is_empty() {
5794 self.iam_state.groups.next_item(filtered.len());
5795 }
5796 }
5797 }
5798 }
5799 _ => {}
5800 }
5801 }
5802
5803 fn prev_item(&mut self) {
5804 match self.mode {
5805 Mode::FilterInput => {
5806 if self.current_service == Service::S3Buckets
5807 && self.s3_state.input_focus == InputFocus::Pagination
5808 {
5809 let page_size = self.s3_state.buckets.page_size.value();
5811 self.s3_state.selected_row =
5812 self.s3_state.selected_row.saturating_sub(page_size);
5813 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5814 } else if self.current_service == Service::CloudTrailEvents
5815 && self.cloudtrail_state.input_focus == InputFocus::Pagination
5816 {
5817 let page_size = self.cloudtrail_state.table.page_size.value();
5819 let current_page = self.cloudtrail_state.table.selected / page_size;
5820 if current_page > 0 {
5821 self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
5822 }
5823 } else if self.current_service == Service::CloudFormationStacks {
5824 use crate::ui::cfn::STATUS_FILTER;
5825 if self.cfn_state.input_focus == STATUS_FILTER {
5826 self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
5827 self.cfn_state.table.reset();
5828 }
5829 } else if self.current_service == Service::IamUsers
5830 && self.iam_state.current_user.is_some()
5831 {
5832 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
5833 if self.iam_state.user_tab == UserTab::Permissions
5834 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
5835 {
5836 self.cycle_policy_type_prev();
5837 } else if self.iam_state.user_tab == UserTab::LastAccessed
5838 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
5839 {
5840 self.iam_state.last_accessed_history_filter =
5841 self.iam_state.last_accessed_history_filter.prev();
5842 self.iam_state.last_accessed_services.reset();
5843 }
5844 } else if self.current_service == Service::IamRoles
5845 && self.iam_state.current_role.is_some()
5846 && self.iam_state.role_tab == RoleTab::Permissions
5847 {
5848 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
5849 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
5850 self.cycle_policy_type_prev();
5851 }
5852 } else if self.current_service == Service::Ec2Instances {
5853 if self.ec2_state.input_focus == EC2_STATE_FILTER {
5854 self.ec2_state.state_filter = self.ec2_state.state_filter.prev();
5855 self.ec2_state.table.reset();
5856 }
5857 } else if self.current_service == Service::SqsQueues {
5858 use crate::ui::sqs::SUBSCRIPTION_REGION;
5859 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
5860 self.sqs_state.subscription_region_selected = self
5861 .sqs_state
5862 .subscription_region_selected
5863 .saturating_sub(1);
5864 self.sqs_state.subscriptions.reset();
5865 }
5866 }
5867 }
5868 Mode::RegionPicker => {
5869 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
5870 }
5871 Mode::ProfilePicker => {
5872 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
5873 }
5874 Mode::SessionPicker => {
5875 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
5876 }
5877 Mode::InsightsInput => {
5878 use crate::app::InsightsFocus;
5879 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
5880 && self.insights_state.insights.show_dropdown
5881 && !self.insights_state.insights.log_group_matches.is_empty()
5882 {
5883 self.insights_state.insights.dropdown_selected = self
5884 .insights_state
5885 .insights
5886 .dropdown_selected
5887 .saturating_sub(1);
5888 }
5889 }
5890 Mode::ColumnSelector => {
5891 let mut prev_idx = self.column_selector_index.saturating_sub(1);
5892 if self.is_blank_row_index(prev_idx) {
5894 prev_idx = prev_idx.saturating_sub(1);
5895 }
5896 self.column_selector_index = prev_idx;
5897 }
5898 Mode::ServicePicker => {
5899 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5900 }
5901 Mode::TabPicker => {
5902 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
5903 }
5904 Mode::Normal => {
5905 if !self.service_selected {
5906 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5907 } else if self.current_service == Service::S3Buckets {
5908 if self.s3_state.current_bucket.is_some() {
5909 if self.s3_state.object_tab == S3ObjectTab::Properties {
5910 self.s3_state.properties_scroll =
5911 self.s3_state.properties_scroll.saturating_sub(1);
5912 } else {
5913 self.s3_state.selected_object =
5914 self.s3_state.selected_object.saturating_sub(1);
5915
5916 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5918 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5919 }
5920 }
5921 } else {
5922 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
5923
5924 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5926 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5927 }
5928 }
5929 } else if self.current_service == Service::ApiGatewayApis {
5930 if let Some(api) = &self.apig_state.current_api {
5931 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
5932 let protocol = api.protocol_type.to_uppercase();
5933 if protocol == "REST" {
5934 if self.apig_state.resources.selected > 0 {
5936 self.apig_state.resources.selected -= 1;
5937 }
5938 } else {
5939 if self.apig_state.routes.selected > 0 {
5941 self.apig_state.routes.selected -= 1;
5942 }
5943 }
5944 }
5945 } else {
5946 self.apig_state.apis.prev_item();
5947 }
5948 } else if self.view_mode == ViewMode::InsightsResults {
5949 if self.insights_state.insights.results_selected > 0 {
5950 self.insights_state.insights.results_selected -= 1;
5951 }
5952 } else if self.view_mode == ViewMode::PolicyView {
5953 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
5954 } else if self.current_service == Service::CloudFormationStacks
5955 && self.cfn_state.current_stack.is_some()
5956 && self.cfn_state.detail_tab == CfnDetailTab::Template
5957 {
5958 self.cfn_state.template_scroll =
5959 self.cfn_state.template_scroll.saturating_sub(1);
5960 } else if self.current_service == Service::SqsQueues
5961 && self.sqs_state.current_queue.is_some()
5962 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
5963 {
5964 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(1);
5965 } else if self.current_service == Service::LambdaFunctions
5966 && self.lambda_state.current_function.is_some()
5967 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5968 && !self.lambda_state.is_metrics_loading()
5969 {
5970 self.lambda_state.set_monitoring_scroll(
5971 self.lambda_state.monitoring_scroll().saturating_sub(1),
5972 );
5973 } else if self.current_service == Service::Ec2Instances
5974 && self.ec2_state.current_instance.is_some()
5975 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5976 && !self.ec2_state.is_metrics_loading()
5977 {
5978 self.ec2_state.set_monitoring_scroll(
5979 self.ec2_state.monitoring_scroll().saturating_sub(1),
5980 );
5981 } else if self.current_service == Service::SqsQueues
5982 && self.sqs_state.current_queue.is_some()
5983 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
5984 && !self.sqs_state.is_metrics_loading()
5985 {
5986 self.sqs_state.set_monitoring_scroll(
5987 self.sqs_state.monitoring_scroll().saturating_sub(1),
5988 );
5989 } else if self.view_mode == ViewMode::Events {
5990 if self.log_groups_state.event_scroll_offset == 0 {
5991 if self.log_groups_state.has_older_events {
5992 self.log_groups_state.loading = true;
5993 }
5994 } else {
5996 self.log_groups_state.event_scroll_offset =
5997 self.log_groups_state.event_scroll_offset.saturating_sub(1);
5998 }
5999 } else if self.current_service == Service::CloudWatchLogGroups {
6000 if self.view_mode == ViewMode::List {
6001 self.log_groups_state.log_groups.prev_item();
6002 } else if self.view_mode == ViewMode::Detail
6003 && self.log_groups_state.selected_stream > 0
6004 {
6005 self.log_groups_state.selected_stream =
6006 self.log_groups_state.selected_stream.saturating_sub(1);
6007 self.log_groups_state.expanded_stream = None;
6008 }
6009 } else if self.current_service == Service::CloudWatchAlarms {
6010 self.alarms_state.table.prev_item();
6011 } else if self.current_service == Service::CloudTrailEvents {
6012 if self.cloudtrail_state.current_event.is_some()
6013 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
6014 {
6015 self.cloudtrail_state.event_json_scroll =
6017 self.cloudtrail_state.event_json_scroll.saturating_sub(1);
6018 } else {
6019 let page_size = self.cloudtrail_state.table.page_size.value();
6021 let current_page = self.cloudtrail_state.table.selected / page_size;
6022 let page_start = current_page * page_size;
6023
6024 if self.cloudtrail_state.table.selected > page_start {
6026 self.cloudtrail_state.table.selected -= 1;
6027 }
6028 }
6029 } else if self.current_service == Service::Ec2Instances {
6030 if self.ec2_state.current_instance.is_some()
6031 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
6032 {
6033 self.ec2_state.tags.prev_item();
6034 } else {
6035 self.ec2_state.table.prev_item();
6036 }
6037 } else if self.current_service == Service::EcrRepositories {
6038 if self.ecr_state.current_repository.is_some() {
6039 self.ecr_state.images.prev_item();
6040 } else {
6041 self.ecr_state.repositories.prev_item();
6042 }
6043 } else if self.current_service == Service::SqsQueues {
6044 if self.sqs_state.current_queue.is_some()
6045 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6046 {
6047 self.sqs_state.triggers.prev_item();
6048 } else if self.sqs_state.current_queue.is_some()
6049 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6050 {
6051 self.sqs_state.pipes.prev_item();
6052 } else if self.sqs_state.current_queue.is_some()
6053 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6054 {
6055 self.sqs_state.tags.prev_item();
6056 } else if self.sqs_state.current_queue.is_some()
6057 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6058 {
6059 self.sqs_state.subscriptions.prev_item();
6060 } else {
6061 self.sqs_state.queues.prev_item();
6062 }
6063 } else if self.current_service == Service::LambdaFunctions {
6064 if self.lambda_state.current_function.is_some()
6065 && self.lambda_state.detail_tab == LambdaDetailTab::Code
6066 {
6067 self.lambda_state.layer_selected =
6069 self.lambda_state.layer_selected.saturating_sub(1);
6070 } else if self.lambda_state.current_function.is_some()
6071 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6072 {
6073 self.lambda_state.version_table.prev_item();
6074 } else if self.lambda_state.current_function.is_some()
6075 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6076 || (self.lambda_state.current_version.is_some()
6077 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6078 {
6079 self.lambda_state.alias_table.prev_item();
6080 } else if self.lambda_state.current_function.is_none() {
6081 self.lambda_state.table.prev_item();
6082 }
6083 } else if self.current_service == Service::LambdaApplications {
6084 if self.lambda_application_state.current_application.is_some()
6085 && self.lambda_application_state.detail_tab
6086 == LambdaApplicationDetailTab::Overview
6087 {
6088 self.lambda_application_state.resources.selected = self
6089 .lambda_application_state
6090 .resources
6091 .selected
6092 .saturating_sub(1);
6093 } else if self.lambda_application_state.current_application.is_some()
6094 && self.lambda_application_state.detail_tab
6095 == LambdaApplicationDetailTab::Deployments
6096 {
6097 self.lambda_application_state.deployments.selected = self
6098 .lambda_application_state
6099 .deployments
6100 .selected
6101 .saturating_sub(1);
6102 } else {
6103 self.lambda_application_state.table.selected = self
6104 .lambda_application_state
6105 .table
6106 .selected
6107 .saturating_sub(1);
6108 self.lambda_application_state.table.snap_to_page();
6109 }
6110 } else if self.current_service == Service::CloudFormationStacks {
6111 if self.cfn_state.current_stack.is_some()
6112 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6113 {
6114 self.cfn_state.parameters.prev_item();
6115 } else if self.cfn_state.current_stack.is_some()
6116 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6117 {
6118 self.cfn_state.outputs.prev_item();
6119 } else if self.cfn_state.current_stack.is_some()
6120 && self.cfn_state.detail_tab == CfnDetailTab::Resources
6121 {
6122 self.cfn_state.resources.prev_item();
6123 } else {
6124 self.cfn_state.table.prev_item();
6125 }
6126 } else if self.current_service == Service::IamUsers {
6127 self.iam_state.users.prev_item();
6128 } else if self.current_service == Service::IamRoles {
6129 if self.iam_state.current_role.is_some() {
6130 if self.iam_state.role_tab == RoleTab::TrustRelationships {
6131 self.iam_state.trust_policy_scroll =
6132 self.iam_state.trust_policy_scroll.saturating_sub(1);
6133 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
6134 self.iam_state.revoke_sessions_scroll =
6135 self.iam_state.revoke_sessions_scroll.saturating_sub(1);
6136 } else if self.iam_state.role_tab == RoleTab::Tags {
6137 self.iam_state.tags.prev_item();
6138 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
6139 self.iam_state.last_accessed_services.prev_item();
6140 } else {
6141 self.iam_state.policies.prev_item();
6142 }
6143 } else {
6144 self.iam_state.roles.prev_item();
6145 }
6146 } else if self.current_service == Service::IamUserGroups {
6147 if self.iam_state.current_group.is_some() {
6148 if self.iam_state.group_tab == GroupTab::Users {
6149 self.iam_state.group_users.prev_item();
6150 } else if self.iam_state.group_tab == GroupTab::Permissions {
6151 self.iam_state.policies.prev_item();
6152 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
6153 self.iam_state.last_accessed_services.prev_item();
6154 }
6155 } else {
6156 self.iam_state.groups.prev_item();
6157 }
6158 }
6159 }
6160 _ => {}
6161 }
6162 }
6163
6164 fn page_down(&mut self) {
6165 if self.current_service == Service::CloudTrailEvents
6166 && self.cloudtrail_state.current_event.is_some()
6167 {
6168 if let Some(event) = &self.cloudtrail_state.current_event {
6169 let lines = event.cloud_trail_event_json.lines().count();
6170 let max_scroll = lines.saturating_sub(1);
6171 self.cloudtrail_state.event_json_scroll =
6172 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
6173 }
6174 } else if self.mode == Mode::ColumnSelector {
6175 let max = self.get_column_selector_max();
6176 let mut next_idx = (self.column_selector_index + 10).min(max);
6177 if self.is_blank_row_index(next_idx) {
6179 next_idx = (next_idx + 1).min(max);
6180 }
6181 self.column_selector_index = next_idx;
6182 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
6183 if self.s3_state.input_focus == InputFocus::Pagination {
6184 let page_size = self.s3_state.buckets.page_size.value();
6186 let total_rows = self.calculate_total_bucket_rows();
6187 let max_offset = total_rows.saturating_sub(page_size);
6188 self.s3_state.selected_row =
6189 (self.s3_state.selected_row + page_size).min(max_offset);
6190 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6191 }
6192 } else if self.mode == Mode::FilterInput
6193 && self.current_service == Service::CloudFormationStacks
6194 {
6195 if self.cfn_state.current_stack.is_some()
6196 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6197 {
6198 let page_size = self.cfn_state.parameters.page_size.value();
6199 let filtered_count = filtered_parameters(self).len();
6200 self.cfn_state.parameters_input_focus.handle_page_down(
6201 &mut self.cfn_state.parameters.selected,
6202 &mut self.cfn_state.parameters.scroll_offset,
6203 page_size,
6204 filtered_count,
6205 );
6206 } else if self.cfn_state.current_stack.is_some()
6207 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6208 {
6209 let page_size = self.cfn_state.outputs.page_size.value();
6210 let filtered_count = filtered_outputs(self).len();
6211 self.cfn_state.outputs_input_focus.handle_page_down(
6212 &mut self.cfn_state.outputs.selected,
6213 &mut self.cfn_state.outputs.scroll_offset,
6214 page_size,
6215 filtered_count,
6216 );
6217 } else {
6218 use crate::ui::cfn::filtered_cloudformation_stacks;
6219 let page_size = self.cfn_state.table.page_size.value();
6220 let filtered_count = filtered_cloudformation_stacks(self).len();
6221 self.cfn_state.input_focus.handle_page_down(
6222 &mut self.cfn_state.table.selected,
6223 &mut self.cfn_state.table.scroll_offset,
6224 page_size,
6225 filtered_count,
6226 );
6227 }
6228 } else if self.mode == Mode::FilterInput
6229 && self.current_service == Service::IamRoles
6230 && self.iam_state.current_role.is_none()
6231 {
6232 let page_size = self.iam_state.roles.page_size.value();
6233 let filtered_count = filtered_iam_roles(self).len();
6234 self.iam_state.role_input_focus.handle_page_down(
6235 &mut self.iam_state.roles.selected,
6236 &mut self.iam_state.roles.scroll_offset,
6237 page_size,
6238 filtered_count,
6239 );
6240 } else if self.mode == Mode::FilterInput
6241 && self.current_service == Service::CloudWatchAlarms
6242 {
6243 let page_size = self.alarms_state.table.page_size.value();
6244 let filtered_count = self.alarms_state.table.items.len();
6245 self.alarms_state.input_focus.handle_page_down(
6246 &mut self.alarms_state.table.selected,
6247 &mut self.alarms_state.table.scroll_offset,
6248 page_size,
6249 filtered_count,
6250 );
6251 } else if self.mode == Mode::FilterInput
6252 && self.current_service == Service::CloudTrailEvents
6253 {
6254 let page_size = self.cloudtrail_state.table.page_size.value();
6255 let filtered_count = self.cloudtrail_state.table.items.len();
6256 self.cloudtrail_state.input_focus.handle_page_down(
6257 &mut self.cloudtrail_state.table.selected,
6258 &mut self.cloudtrail_state.table.scroll_offset,
6259 page_size,
6260 filtered_count,
6261 );
6262 } else if self.mode == Mode::FilterInput
6263 && self.current_service == Service::CloudWatchLogGroups
6264 {
6265 if self.view_mode == ViewMode::List {
6266 let filtered = filtered_log_groups(self);
6268 let page_size = self.log_groups_state.log_groups.page_size.value();
6269 let filtered_count = filtered.len();
6270 self.log_groups_state.input_focus.handle_page_down(
6271 &mut self.log_groups_state.log_groups.selected,
6272 &mut self.log_groups_state.log_groups.scroll_offset,
6273 page_size,
6274 filtered_count,
6275 );
6276 } else {
6277 let filtered = filtered_log_streams(self);
6279 let page_size = self.log_groups_state.stream_page_size;
6280 let filtered_count = filtered.len();
6281 self.log_groups_state.input_focus.handle_page_down(
6282 &mut self.log_groups_state.selected_stream,
6283 &mut self.log_groups_state.stream_current_page,
6284 page_size,
6285 filtered_count,
6286 );
6287 self.log_groups_state.expanded_stream = None;
6288 }
6289 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
6290 {
6291 if self.lambda_state.current_function.is_some()
6292 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6293 && self.lambda_state.version_input_focus == InputFocus::Pagination
6294 {
6295 let page_size = self.lambda_state.version_table.page_size.value();
6296 let filtered_count: usize = self
6297 .lambda_state
6298 .version_table
6299 .items
6300 .iter()
6301 .filter(|v| {
6302 self.lambda_state.version_table.filter.is_empty()
6303 || v.version
6304 .to_lowercase()
6305 .contains(&self.lambda_state.version_table.filter.to_lowercase())
6306 || v.aliases
6307 .to_lowercase()
6308 .contains(&self.lambda_state.version_table.filter.to_lowercase())
6309 || v.description
6310 .to_lowercase()
6311 .contains(&self.lambda_state.version_table.filter.to_lowercase())
6312 })
6313 .count();
6314 let target = self.lambda_state.version_table.selected + page_size;
6315 self.lambda_state.version_table.selected =
6316 target.min(filtered_count.saturating_sub(1));
6317 } else if self.lambda_state.current_function.is_some()
6318 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6319 || (self.lambda_state.current_version.is_some()
6320 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6321 && self.lambda_state.alias_input_focus == InputFocus::Pagination
6322 {
6323 let page_size = self.lambda_state.alias_table.page_size.value();
6324 let version_filter = self.lambda_state.current_version.clone();
6325 let filtered_count = self
6326 .lambda_state
6327 .alias_table
6328 .items
6329 .iter()
6330 .filter(|a| {
6331 (version_filter.is_none()
6332 || a.versions.contains(version_filter.as_ref().unwrap()))
6333 && (self.lambda_state.alias_table.filter.is_empty()
6334 || a.name
6335 .to_lowercase()
6336 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6337 || a.versions
6338 .to_lowercase()
6339 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6340 || a.description
6341 .to_lowercase()
6342 .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
6343 })
6344 .count();
6345 let target = self.lambda_state.alias_table.selected + page_size;
6346 self.lambda_state.alias_table.selected =
6347 target.min(filtered_count.saturating_sub(1));
6348 } else if self.lambda_state.current_function.is_none() {
6349 let page_size = self.lambda_state.table.page_size.value();
6350 let filtered_count = filtered_lambda_functions(self).len();
6351 self.lambda_state.input_focus.handle_page_down(
6352 &mut self.lambda_state.table.selected,
6353 &mut self.lambda_state.table.scroll_offset,
6354 page_size,
6355 filtered_count,
6356 );
6357 }
6358 } else if self.mode == Mode::FilterInput
6359 && self.current_service == Service::LambdaApplications
6360 {
6361 if self.lambda_application_state.current_application.is_some() {
6362 if self.lambda_application_state.detail_tab
6363 == LambdaApplicationDetailTab::Deployments
6364 {
6365 let page_size = self.lambda_application_state.deployments.page_size.value();
6366 let filtered_count = self.lambda_application_state.deployments.items.len();
6367 self.lambda_application_state
6368 .deployment_input_focus
6369 .handle_page_down(
6370 &mut self.lambda_application_state.deployments.selected,
6371 &mut self.lambda_application_state.deployments.scroll_offset,
6372 page_size,
6373 filtered_count,
6374 );
6375 } else {
6376 let page_size = self.lambda_application_state.resources.page_size.value();
6377 let filtered_count = self.lambda_application_state.resources.items.len();
6378 self.lambda_application_state
6379 .resource_input_focus
6380 .handle_page_down(
6381 &mut self.lambda_application_state.resources.selected,
6382 &mut self.lambda_application_state.resources.scroll_offset,
6383 page_size,
6384 filtered_count,
6385 );
6386 }
6387 } else {
6388 let page_size = self.lambda_application_state.table.page_size.value();
6389 let filtered_count = filtered_lambda_applications(self).len();
6390 self.lambda_application_state.input_focus.handle_page_down(
6391 &mut self.lambda_application_state.table.selected,
6392 &mut self.lambda_application_state.table.scroll_offset,
6393 page_size,
6394 filtered_count,
6395 );
6396 }
6397 } else if self.mode == Mode::FilterInput
6398 && self.current_service == Service::EcrRepositories
6399 && self.ecr_state.current_repository.is_none()
6400 && self.ecr_state.input_focus == InputFocus::Filter
6401 {
6402 let filtered = filtered_ecr_repositories(self);
6404 self.ecr_state.repositories.page_down(filtered.len());
6405 } else if self.mode == Mode::FilterInput
6406 && self.current_service == Service::EcrRepositories
6407 && self.ecr_state.current_repository.is_none()
6408 {
6409 let page_size = self.ecr_state.repositories.page_size.value();
6410 let filtered_count = filtered_ecr_repositories(self).len();
6411 self.ecr_state.input_focus.handle_page_down(
6412 &mut self.ecr_state.repositories.selected,
6413 &mut self.ecr_state.repositories.scroll_offset,
6414 page_size,
6415 filtered_count,
6416 );
6417 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
6418 let page_size = self.iam_state.policies.page_size.value();
6419 let filtered_count = filtered_iam_policies(self).len();
6420 self.iam_state.policy_input_focus.handle_page_down(
6421 &mut self.iam_state.policies.selected,
6422 &mut self.iam_state.policies.scroll_offset,
6423 page_size,
6424 filtered_count,
6425 );
6426 } else if self.view_mode == ViewMode::PolicyView {
6427 let lines = self.iam_state.policy_document.lines().count();
6428 let max_scroll = lines.saturating_sub(1);
6429 self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
6430 } else if self.current_service == Service::CloudFormationStacks
6431 && self.cfn_state.current_stack.is_some()
6432 && self.cfn_state.detail_tab == CfnDetailTab::Template
6433 {
6434 let lines = self.cfn_state.template_body.lines().count();
6435 let max_scroll = lines.saturating_sub(1);
6436 self.cfn_state.template_scroll = (self.cfn_state.template_scroll + 10).min(max_scroll);
6437 } else if self.current_service == Service::LambdaFunctions
6438 && self.lambda_state.current_function.is_some()
6439 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
6440 && !self.lambda_state.is_metrics_loading()
6441 {
6442 self.lambda_state
6443 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
6444 } else if self.current_service == Service::Ec2Instances
6445 && self.ec2_state.current_instance.is_some()
6446 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
6447 && !self.ec2_state.is_metrics_loading()
6448 {
6449 self.ec2_state
6450 .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
6451 } else if self.current_service == Service::SqsQueues
6452 && self.sqs_state.current_queue.is_some()
6453 {
6454 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
6455 self.sqs_state
6456 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
6457 } else {
6458 let lines = self.sqs_state.policy_document.lines().count();
6459 let max_scroll = lines.saturating_sub(1);
6460 self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
6461 }
6462 } else if self.current_service == Service::IamRoles
6463 && self.iam_state.current_role.is_some()
6464 && self.iam_state.role_tab == RoleTab::TrustRelationships
6465 {
6466 let lines = self.iam_state.trust_policy_document.lines().count();
6467 let max_scroll = lines.saturating_sub(1);
6468 self.iam_state.trust_policy_scroll =
6469 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
6470 } else if self.current_service == Service::IamRoles
6471 && self.iam_state.current_role.is_some()
6472 && self.iam_state.role_tab == RoleTab::RevokeSessions
6473 {
6474 self.iam_state.revoke_sessions_scroll =
6475 (self.iam_state.revoke_sessions_scroll + 10).min(19);
6476 } else if self.mode == Mode::Normal {
6477 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
6478 {
6479 let total_rows = self.calculate_total_bucket_rows();
6480 self.s3_state.selected_row = self
6481 .s3_state
6482 .selected_row
6483 .saturating_add(10)
6484 .min(total_rows.saturating_sub(1));
6485
6486 let visible_rows = self.s3_state.bucket_visible_rows.get();
6488 if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
6489 self.s3_state.bucket_scroll_offset =
6490 self.s3_state.selected_row - visible_rows + 1;
6491 }
6492 } else if self.current_service == Service::S3Buckets
6493 && self.s3_state.current_bucket.is_some()
6494 {
6495 let total_rows = self.calculate_total_object_rows();
6496 self.s3_state.selected_object = self
6497 .s3_state
6498 .selected_object
6499 .saturating_add(10)
6500 .min(total_rows.saturating_sub(1));
6501
6502 let visible_rows = self.s3_state.object_visible_rows.get();
6504 if self.s3_state.selected_object
6505 >= self.s3_state.object_scroll_offset + visible_rows
6506 {
6507 self.s3_state.object_scroll_offset =
6508 self.s3_state.selected_object - visible_rows + 1;
6509 }
6510 } else if self.current_service == Service::CloudWatchLogGroups
6511 && self.view_mode == ViewMode::List
6512 {
6513 let filtered = filtered_log_groups(self);
6514 self.log_groups_state.log_groups.page_down(filtered.len());
6515 } else if self.current_service == Service::CloudWatchLogGroups
6516 && self.view_mode == ViewMode::Detail
6517 {
6518 let len = filtered_log_streams(self).len();
6519 nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
6520 } else if self.view_mode == ViewMode::Events {
6521 let max = self.log_groups_state.log_events.len();
6522 nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
6523 } else if self.view_mode == ViewMode::InsightsResults {
6524 let max = self.insights_state.insights.query_results.len();
6525 nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
6526 } else if self.current_service == Service::CloudWatchAlarms {
6527 let filtered = match self.alarms_state.alarm_tab {
6528 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
6529 AlarmTab::InAlarm => self
6530 .alarms_state
6531 .table
6532 .items
6533 .iter()
6534 .filter(|a| a.state.to_uppercase() == "ALARM")
6535 .count(),
6536 };
6537 if filtered > 0 {
6538 self.alarms_state.table.page_down(filtered);
6539 }
6540 } else if self.current_service == Service::CloudTrailEvents {
6541 let filtered_count = self.cloudtrail_state.table.items.len();
6542 if filtered_count > 0 {
6543 self.cloudtrail_state.table.page_down(filtered_count);
6544 }
6545 } else if self.current_service == Service::Ec2Instances {
6546 let filtered: Vec<_> = self
6547 .ec2_state
6548 .table
6549 .items
6550 .iter()
6551 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
6552 .filter(|i| {
6553 if self.ec2_state.table.filter.is_empty() {
6554 return true;
6555 }
6556 i.name.contains(&self.ec2_state.table.filter)
6557 || i.instance_id.contains(&self.ec2_state.table.filter)
6558 || i.state.contains(&self.ec2_state.table.filter)
6559 || i.instance_type.contains(&self.ec2_state.table.filter)
6560 || i.availability_zone.contains(&self.ec2_state.table.filter)
6561 || i.security_groups.contains(&self.ec2_state.table.filter)
6562 || i.key_name.contains(&self.ec2_state.table.filter)
6563 })
6564 .collect();
6565 if !filtered.is_empty() {
6566 self.ec2_state.table.page_down(filtered.len());
6567 }
6568 } else if self.current_service == Service::EcrRepositories {
6569 if self.ecr_state.current_repository.is_some() {
6570 let filtered = filtered_ecr_images(self);
6571 self.ecr_state.images.page_down(filtered.len());
6572 } else {
6573 let filtered = filtered_ecr_repositories(self);
6574 self.ecr_state.repositories.page_down(filtered.len());
6575 }
6576 } else if self.current_service == Service::SqsQueues {
6577 let filtered =
6578 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter);
6579 self.sqs_state.queues.page_down(filtered.len());
6580 } else if self.current_service == Service::LambdaFunctions {
6581 let len = filtered_lambda_functions(self).len();
6582 self.lambda_state.table.page_down(len);
6583 } else if self.current_service == Service::LambdaApplications {
6584 let len = filtered_lambda_applications(self).len();
6585 self.lambda_application_state.table.page_down(len);
6586 } else if self.current_service == Service::CloudFormationStacks {
6587 if self.cfn_state.current_stack.is_some()
6588 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6589 {
6590 let filtered = filtered_parameters(self);
6591 self.cfn_state.parameters.page_down(filtered.len());
6592 } else if self.cfn_state.current_stack.is_some()
6593 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6594 {
6595 let filtered = filtered_outputs(self);
6596 self.cfn_state.outputs.page_down(filtered.len());
6597 } else {
6598 let filtered = filtered_cloudformation_stacks(self);
6599 self.cfn_state.table.page_down(filtered.len());
6600 }
6601 } else if self.current_service == Service::IamUsers {
6602 let len = filtered_iam_users(self).len();
6603 nav_page_down(&mut self.iam_state.users.selected, len, 10);
6604 } else if self.current_service == Service::IamRoles {
6605 if self.iam_state.current_role.is_some() {
6606 let filtered = filtered_iam_policies(self);
6607 if !filtered.is_empty() {
6608 self.iam_state.policies.page_down(filtered.len());
6609 }
6610 } else {
6611 let filtered = filtered_iam_roles(self);
6612 self.iam_state.roles.page_down(filtered.len());
6613 }
6614 } else if self.current_service == Service::IamUserGroups {
6615 if self.iam_state.current_group.is_some() {
6616 if self.iam_state.group_tab == GroupTab::Users {
6617 let filtered: Vec<_> = self
6618 .iam_state
6619 .group_users
6620 .items
6621 .iter()
6622 .filter(|u| {
6623 if self.iam_state.group_users.filter.is_empty() {
6624 true
6625 } else {
6626 u.user_name
6627 .to_lowercase()
6628 .contains(&self.iam_state.group_users.filter.to_lowercase())
6629 }
6630 })
6631 .collect();
6632 if !filtered.is_empty() {
6633 self.iam_state.group_users.page_down(filtered.len());
6634 }
6635 } else if self.iam_state.group_tab == GroupTab::Permissions {
6636 let filtered = filtered_iam_policies(self);
6637 if !filtered.is_empty() {
6638 self.iam_state.policies.page_down(filtered.len());
6639 }
6640 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
6641 let filtered = filtered_last_accessed(self);
6642 if !filtered.is_empty() {
6643 self.iam_state
6644 .last_accessed_services
6645 .page_down(filtered.len());
6646 }
6647 }
6648 } else {
6649 let filtered: Vec<_> = self
6650 .iam_state
6651 .groups
6652 .items
6653 .iter()
6654 .filter(|g| {
6655 if self.iam_state.groups.filter.is_empty() {
6656 true
6657 } else {
6658 g.group_name
6659 .to_lowercase()
6660 .contains(&self.iam_state.groups.filter.to_lowercase())
6661 }
6662 })
6663 .collect();
6664 if !filtered.is_empty() {
6665 self.iam_state.groups.page_down(filtered.len());
6666 }
6667 }
6668 }
6669 }
6670 }
6671
6672 fn cycle_policy_type_next(&mut self) {
6673 let types = ["All types", "AWS managed", "Customer managed"];
6674 let current_idx = types
6675 .iter()
6676 .position(|&t| t == self.iam_state.policy_type_filter)
6677 .unwrap_or(0);
6678 let next_idx = (current_idx + 1) % types.len();
6679 self.iam_state.policy_type_filter = types[next_idx].to_string();
6680 self.iam_state.policies.reset();
6681 }
6682
6683 fn cycle_policy_type_prev(&mut self) {
6684 let types = ["All types", "AWS managed", "Customer managed"];
6685 let current_idx = types
6686 .iter()
6687 .position(|&t| t == self.iam_state.policy_type_filter)
6688 .unwrap_or(0);
6689 let prev_idx = if current_idx == 0 {
6690 types.len() - 1
6691 } else {
6692 current_idx - 1
6693 };
6694 self.iam_state.policy_type_filter = types[prev_idx].to_string();
6695 self.iam_state.policies.reset();
6696 }
6697
6698 fn page_up(&mut self) {
6699 if self.current_service == Service::CloudTrailEvents
6700 && self.cloudtrail_state.current_event.is_some()
6701 {
6702 self.cloudtrail_state.event_json_scroll =
6703 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
6704 } else if self.mode == Mode::ColumnSelector {
6705 let mut prev_idx = self.column_selector_index.saturating_sub(10);
6706 if self.is_blank_row_index(prev_idx) {
6708 prev_idx = prev_idx.saturating_sub(1);
6709 }
6710 self.column_selector_index = prev_idx;
6711 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
6712 if self.s3_state.input_focus == InputFocus::Pagination {
6713 let page_size = self.s3_state.buckets.page_size.value();
6715 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(page_size);
6716 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6717 }
6718 } else if self.mode == Mode::FilterInput
6719 && self.current_service == Service::CloudFormationStacks
6720 {
6721 if self.cfn_state.current_stack.is_some()
6722 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6723 {
6724 let page_size = self.cfn_state.parameters.page_size.value();
6725 self.cfn_state.parameters_input_focus.handle_page_up(
6726 &mut self.cfn_state.parameters.selected,
6727 &mut self.cfn_state.parameters.scroll_offset,
6728 page_size,
6729 );
6730 } else if self.cfn_state.current_stack.is_some()
6731 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6732 {
6733 let page_size = self.cfn_state.outputs.page_size.value();
6734 self.cfn_state.outputs_input_focus.handle_page_up(
6735 &mut self.cfn_state.outputs.selected,
6736 &mut self.cfn_state.outputs.scroll_offset,
6737 page_size,
6738 );
6739 } else {
6740 let page_size = self.cfn_state.table.page_size.value();
6741 self.cfn_state.input_focus.handle_page_up(
6742 &mut self.cfn_state.table.selected,
6743 &mut self.cfn_state.table.scroll_offset,
6744 page_size,
6745 );
6746 }
6747 } else if self.mode == Mode::FilterInput
6748 && self.current_service == Service::IamRoles
6749 && self.iam_state.current_role.is_none()
6750 {
6751 let page_size = self.iam_state.roles.page_size.value();
6752 self.iam_state.role_input_focus.handle_page_up(
6753 &mut self.iam_state.roles.selected,
6754 &mut self.iam_state.roles.scroll_offset,
6755 page_size,
6756 );
6757 } else if self.mode == Mode::FilterInput
6758 && self.current_service == Service::CloudWatchAlarms
6759 {
6760 let page_size = self.alarms_state.table.page_size.value();
6761 self.alarms_state.input_focus.handle_page_up(
6762 &mut self.alarms_state.table.selected,
6763 &mut self.alarms_state.table.scroll_offset,
6764 page_size,
6765 );
6766 } else if self.mode == Mode::FilterInput
6767 && self.current_service == Service::CloudTrailEvents
6768 {
6769 let page_size = self.cloudtrail_state.table.page_size.value();
6770 self.cloudtrail_state.input_focus.handle_page_up(
6771 &mut self.cloudtrail_state.table.selected,
6772 &mut self.cloudtrail_state.table.scroll_offset,
6773 page_size,
6774 );
6775 } else if self.mode == Mode::FilterInput
6776 && self.current_service == Service::CloudWatchLogGroups
6777 {
6778 if self.view_mode == ViewMode::List {
6779 let page_size = self.log_groups_state.log_groups.page_size.value();
6781 self.log_groups_state.input_focus.handle_page_up(
6782 &mut self.log_groups_state.log_groups.selected,
6783 &mut self.log_groups_state.log_groups.scroll_offset,
6784 page_size,
6785 );
6786 } else {
6787 let page_size = self.log_groups_state.stream_page_size;
6789 self.log_groups_state.input_focus.handle_page_up(
6790 &mut self.log_groups_state.selected_stream,
6791 &mut self.log_groups_state.stream_current_page,
6792 page_size,
6793 );
6794 self.log_groups_state.expanded_stream = None;
6795 }
6796 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
6797 {
6798 if self.lambda_state.current_function.is_some()
6799 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6800 && self.lambda_state.version_input_focus == InputFocus::Pagination
6801 {
6802 let page_size = self.lambda_state.version_table.page_size.value();
6803 self.lambda_state.version_table.selected = self
6804 .lambda_state
6805 .version_table
6806 .selected
6807 .saturating_sub(page_size);
6808 } else if self.lambda_state.current_function.is_some()
6809 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6810 || (self.lambda_state.current_version.is_some()
6811 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6812 && self.lambda_state.alias_input_focus == InputFocus::Pagination
6813 {
6814 let page_size = self.lambda_state.alias_table.page_size.value();
6815 self.lambda_state.alias_table.selected = self
6816 .lambda_state
6817 .alias_table
6818 .selected
6819 .saturating_sub(page_size);
6820 } else if self.lambda_state.current_function.is_none() {
6821 let page_size = self.lambda_state.table.page_size.value();
6822 self.lambda_state.input_focus.handle_page_up(
6823 &mut self.lambda_state.table.selected,
6824 &mut self.lambda_state.table.scroll_offset,
6825 page_size,
6826 );
6827 }
6828 } else if self.mode == Mode::FilterInput
6829 && self.current_service == Service::LambdaApplications
6830 {
6831 if self.lambda_application_state.current_application.is_some() {
6832 if self.lambda_application_state.detail_tab
6833 == LambdaApplicationDetailTab::Deployments
6834 {
6835 let page_size = self.lambda_application_state.deployments.page_size.value();
6836 self.lambda_application_state
6837 .deployment_input_focus
6838 .handle_page_up(
6839 &mut self.lambda_application_state.deployments.selected,
6840 &mut self.lambda_application_state.deployments.scroll_offset,
6841 page_size,
6842 );
6843 } else {
6844 let page_size = self.lambda_application_state.resources.page_size.value();
6845 self.lambda_application_state
6846 .resource_input_focus
6847 .handle_page_up(
6848 &mut self.lambda_application_state.resources.selected,
6849 &mut self.lambda_application_state.resources.scroll_offset,
6850 page_size,
6851 );
6852 }
6853 } else {
6854 let page_size = self.lambda_application_state.table.page_size.value();
6855 self.lambda_application_state.input_focus.handle_page_up(
6856 &mut self.lambda_application_state.table.selected,
6857 &mut self.lambda_application_state.table.scroll_offset,
6858 page_size,
6859 );
6860 }
6861 } else if self.mode == Mode::FilterInput
6862 && self.current_service == Service::EcrRepositories
6863 && self.ecr_state.current_repository.is_none()
6864 && self.ecr_state.input_focus == InputFocus::Filter
6865 {
6866 self.ecr_state.repositories.page_up();
6868 } else if self.mode == Mode::FilterInput
6869 && self.current_service == Service::EcrRepositories
6870 && self.ecr_state.current_repository.is_none()
6871 {
6872 let page_size = self.ecr_state.repositories.page_size.value();
6873 self.ecr_state.input_focus.handle_page_up(
6874 &mut self.ecr_state.repositories.selected,
6875 &mut self.ecr_state.repositories.scroll_offset,
6876 page_size,
6877 );
6878 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
6879 let page_size = self.iam_state.policies.page_size.value();
6880 self.iam_state.policy_input_focus.handle_page_up(
6881 &mut self.iam_state.policies.selected,
6882 &mut self.iam_state.policies.scroll_offset,
6883 page_size,
6884 );
6885 } else if self.view_mode == ViewMode::PolicyView {
6886 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
6887 } else if self.current_service == Service::CloudFormationStacks
6888 && self.cfn_state.current_stack.is_some()
6889 && self.cfn_state.detail_tab == CfnDetailTab::Template
6890 {
6891 self.cfn_state.template_scroll = self.cfn_state.template_scroll.saturating_sub(10);
6892 } else if self.current_service == Service::SqsQueues
6893 && self.sqs_state.current_queue.is_some()
6894 {
6895 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
6896 self.sqs_state
6897 .set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
6898 } else {
6899 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
6900 }
6901 } else if self.current_service == Service::IamRoles
6902 && self.iam_state.current_role.is_some()
6903 && self.iam_state.role_tab == RoleTab::TrustRelationships
6904 {
6905 self.iam_state.trust_policy_scroll =
6906 self.iam_state.trust_policy_scroll.saturating_sub(10);
6907 } else if self.current_service == Service::IamRoles
6908 && self.iam_state.current_role.is_some()
6909 && self.iam_state.role_tab == RoleTab::RevokeSessions
6910 {
6911 self.iam_state.revoke_sessions_scroll =
6912 self.iam_state.revoke_sessions_scroll.saturating_sub(10);
6913 } else if self.mode == Mode::Normal {
6914 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
6915 {
6916 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
6917
6918 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6920 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6921 }
6922 } else if self.current_service == Service::S3Buckets
6923 && self.s3_state.current_bucket.is_some()
6924 {
6925 self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
6926
6927 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
6929 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
6930 }
6931 } else if self.current_service == Service::CloudWatchLogGroups
6932 && self.view_mode == ViewMode::List
6933 {
6934 self.log_groups_state.log_groups.page_up();
6935 } else if self.current_service == Service::CloudWatchLogGroups
6936 && self.view_mode == ViewMode::Detail
6937 {
6938 self.log_groups_state.selected_stream =
6939 self.log_groups_state.selected_stream.saturating_sub(10);
6940 } else if self.view_mode == ViewMode::Events {
6941 if self.log_groups_state.event_scroll_offset < 10
6942 && self.log_groups_state.has_older_events
6943 {
6944 self.log_groups_state.loading = true;
6945 }
6946 self.log_groups_state.event_scroll_offset =
6947 self.log_groups_state.event_scroll_offset.saturating_sub(10);
6948 } else if self.view_mode == ViewMode::InsightsResults {
6949 self.insights_state.insights.results_selected = self
6950 .insights_state
6951 .insights
6952 .results_selected
6953 .saturating_sub(10);
6954 } else if self.current_service == Service::CloudWatchAlarms {
6955 self.alarms_state.table.page_up();
6956 } else if self.current_service == Service::CloudTrailEvents {
6957 self.cloudtrail_state.table.page_up();
6958 } else if self.current_service == Service::Ec2Instances {
6959 self.ec2_state.table.page_up();
6960 } else if self.current_service == Service::EcrRepositories {
6961 if self.ecr_state.current_repository.is_some() {
6962 self.ecr_state.images.page_up();
6963 } else {
6964 self.ecr_state.repositories.page_up();
6965 }
6966 } else if self.current_service == Service::SqsQueues {
6967 self.sqs_state.queues.page_up();
6968 } else if self.current_service == Service::LambdaFunctions {
6969 self.lambda_state.table.page_up();
6970 } else if self.current_service == Service::LambdaApplications {
6971 self.lambda_application_state.table.page_up();
6972 } else if self.current_service == Service::CloudFormationStacks {
6973 if self.cfn_state.current_stack.is_some()
6974 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6975 {
6976 self.cfn_state.parameters.page_up();
6977 } else if self.cfn_state.current_stack.is_some()
6978 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6979 {
6980 self.cfn_state.outputs.page_up();
6981 } else {
6982 self.cfn_state.table.page_up();
6983 }
6984 } else if self.current_service == Service::IamUsers {
6985 self.iam_state.users.page_up();
6986 } else if self.current_service == Service::IamRoles {
6987 if self.iam_state.current_role.is_some() {
6988 self.iam_state.policies.page_up();
6989 } else {
6990 self.iam_state.roles.page_up();
6991 }
6992 }
6993 }
6994 }
6995
6996 fn next_pane(&mut self) {
6997 if self.current_service == Service::S3Buckets {
6998 if self.s3_state.current_bucket.is_some() {
6999 let mut visual_idx = 0;
7002 let mut found_obj: Option<S3Object> = None;
7003
7004 fn check_nested(
7006 obj: &S3Object,
7007 visual_idx: &mut usize,
7008 target_idx: usize,
7009 expanded_prefixes: &std::collections::HashSet<String>,
7010 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7011 found_obj: &mut Option<S3Object>,
7012 ) {
7013 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7014 if let Some(preview) = prefix_preview.get(&obj.key) {
7015 for nested_obj in preview {
7016 if *visual_idx == target_idx {
7017 *found_obj = Some(nested_obj.clone());
7018 return;
7019 }
7020 *visual_idx += 1;
7021
7022 check_nested(
7024 nested_obj,
7025 visual_idx,
7026 target_idx,
7027 expanded_prefixes,
7028 prefix_preview,
7029 found_obj,
7030 );
7031 if found_obj.is_some() {
7032 return;
7033 }
7034 }
7035 } else {
7036 *visual_idx += 1;
7038 }
7039 }
7040 }
7041
7042 for obj in &self.s3_state.objects {
7043 if visual_idx == self.s3_state.selected_object {
7044 found_obj = Some(obj.clone());
7045 break;
7046 }
7047 visual_idx += 1;
7048
7049 check_nested(
7051 obj,
7052 &mut visual_idx,
7053 self.s3_state.selected_object,
7054 &self.s3_state.expanded_prefixes,
7055 &self.s3_state.prefix_preview,
7056 &mut found_obj,
7057 );
7058 if found_obj.is_some() {
7059 break;
7060 }
7061 }
7062
7063 if let Some(obj) = found_obj {
7064 if obj.is_prefix {
7065 if !self.s3_state.expanded_prefixes.contains(&obj.key) {
7066 self.s3_state.expanded_prefixes.insert(obj.key.clone());
7067 if !self.s3_state.prefix_preview.contains_key(&obj.key) {
7069 self.s3_state.buckets.loading = true;
7070 }
7071 }
7072 if self.s3_state.expanded_prefixes.contains(&obj.key) {
7074 if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
7075 if !preview.is_empty() {
7076 self.s3_state.selected_object += 1;
7077 }
7078 }
7079 }
7080 }
7081 }
7082 } else {
7083 let mut row_idx = 0;
7085 let mut found = false;
7086 for bucket in &self.s3_state.buckets.items {
7087 if row_idx == self.s3_state.selected_row {
7088 if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
7090 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
7091 if !self.s3_state.bucket_preview.contains_key(&bucket.name)
7092 && !self.s3_state.bucket_errors.contains_key(&bucket.name)
7093 {
7094 self.s3_state.buckets.loading = true;
7095 }
7096 }
7097 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7099 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7100 if !preview.is_empty() {
7101 self.s3_state.selected_row = row_idx + 1;
7102 }
7103 }
7104 }
7105 break;
7106 }
7107 row_idx += 1;
7108
7109 if self.s3_state.bucket_errors.contains_key(&bucket.name)
7111 && self.s3_state.expanded_prefixes.contains(&bucket.name)
7112 {
7113 continue;
7114 }
7115
7116 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7117 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7118 #[allow(clippy::too_many_arguments)]
7120 fn check_nested_expansion(
7121 objects: &[S3Object],
7122 row_idx: &mut usize,
7123 target_row: usize,
7124 expanded_prefixes: &mut std::collections::HashSet<String>,
7125 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7126 found: &mut bool,
7127 loading: &mut bool,
7128 selected_row: &mut usize,
7129 ) {
7130 for obj in objects {
7131 if *row_idx == target_row {
7132 if obj.is_prefix {
7134 if !expanded_prefixes.contains(&obj.key) {
7135 expanded_prefixes.insert(obj.key.clone());
7136 if !prefix_preview.contains_key(&obj.key) {
7137 *loading = true;
7138 }
7139 }
7140 if expanded_prefixes.contains(&obj.key) {
7142 if let Some(preview) = prefix_preview.get(&obj.key)
7143 {
7144 if !preview.is_empty() {
7145 *selected_row = *row_idx + 1;
7146 }
7147 }
7148 }
7149 }
7150 *found = true;
7151 return;
7152 }
7153 *row_idx += 1;
7154
7155 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7157 if let Some(nested) = prefix_preview.get(&obj.key) {
7158 check_nested_expansion(
7159 nested,
7160 row_idx,
7161 target_row,
7162 expanded_prefixes,
7163 prefix_preview,
7164 found,
7165 loading,
7166 selected_row,
7167 );
7168 if *found {
7169 return;
7170 }
7171 } else {
7172 *row_idx += 1; }
7174 }
7175 }
7176 }
7177
7178 check_nested_expansion(
7179 preview,
7180 &mut row_idx,
7181 self.s3_state.selected_row,
7182 &mut self.s3_state.expanded_prefixes,
7183 &self.s3_state.prefix_preview,
7184 &mut found,
7185 &mut self.s3_state.buckets.loading,
7186 &mut self.s3_state.selected_row,
7187 );
7188 if found || row_idx > self.s3_state.selected_row {
7189 break;
7190 }
7191 } else {
7192 row_idx += 1;
7193 if row_idx > self.s3_state.selected_row {
7194 break;
7195 }
7196 }
7197 }
7198 if found {
7199 break;
7200 }
7201 }
7202 }
7203 } else if self.view_mode == ViewMode::InsightsResults {
7204 let max_cols = self
7206 .insights_state
7207 .insights
7208 .query_results
7209 .first()
7210 .map(|r| r.len())
7211 .unwrap_or(0);
7212 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
7213 self.insights_state.insights.results_horizontal_scroll += 1;
7214 }
7215 } else if self.current_service == Service::CloudWatchLogGroups
7216 && self.view_mode == ViewMode::List
7217 {
7218 if self.log_groups_state.log_groups.expanded_item
7220 != Some(self.log_groups_state.log_groups.selected)
7221 {
7222 self.log_groups_state.log_groups.expanded_item =
7223 Some(self.log_groups_state.log_groups.selected);
7224 }
7225 } else if self.current_service == Service::CloudWatchLogGroups
7226 && self.view_mode == ViewMode::Detail
7227 {
7228 if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
7230 {
7231 self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
7232 }
7233 } else if self.view_mode == ViewMode::Events {
7234 if self.log_groups_state.expanded_event
7237 != Some(self.log_groups_state.event_scroll_offset)
7238 {
7239 self.log_groups_state.expanded_event =
7240 Some(self.log_groups_state.event_scroll_offset);
7241 }
7242 } else if self.current_service == Service::CloudWatchAlarms {
7243 if !self.alarms_state.table.is_expanded() {
7245 self.alarms_state.table.toggle_expand();
7246 }
7247 } else if self.current_service == Service::Ec2Instances {
7248 if self.ec2_state.current_instance.is_some()
7249 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
7250 {
7251 self.ec2_state.tags.toggle_expand();
7252 } else if !self.ec2_state.table.is_expanded() {
7253 self.ec2_state.table.toggle_expand();
7254 }
7255 } else if self.current_service == Service::EcrRepositories {
7256 if self.ecr_state.current_repository.is_some() {
7257 self.ecr_state.images.toggle_expand();
7259 } else {
7260 self.ecr_state.repositories.toggle_expand();
7262 }
7263 } else if self.current_service == Service::SqsQueues {
7264 if self.sqs_state.current_queue.is_some()
7265 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
7266 {
7267 self.sqs_state.triggers.toggle_expand();
7268 } else if self.sqs_state.current_queue.is_some()
7269 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
7270 {
7271 self.sqs_state.pipes.toggle_expand();
7272 } else if self.sqs_state.current_queue.is_some()
7273 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
7274 {
7275 self.sqs_state.tags.toggle_expand();
7276 } else if self.sqs_state.current_queue.is_some()
7277 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
7278 {
7279 self.sqs_state.subscriptions.toggle_expand();
7280 } else {
7281 self.sqs_state.queues.expand();
7282 }
7283 } else if self.current_service == Service::LambdaFunctions {
7284 if self.lambda_state.current_function.is_some()
7285 && self.lambda_state.detail_tab == LambdaDetailTab::Code
7286 {
7287 if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
7289 self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
7290 } else {
7291 self.lambda_state.layer_expanded = None;
7292 }
7293 } else if self.lambda_state.current_function.is_some()
7294 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
7295 {
7296 self.lambda_state.version_table.toggle_expand();
7298 } else if self.lambda_state.current_function.is_some()
7299 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
7300 || (self.lambda_state.current_version.is_some()
7301 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
7302 {
7303 self.lambda_state.alias_table.toggle_expand();
7305 } else if self.lambda_state.current_function.is_none() {
7306 self.lambda_state.table.toggle_expand();
7308 }
7309 } else if self.current_service == Service::LambdaApplications {
7310 if self.lambda_application_state.current_application.is_some() {
7311 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
7313 {
7314 self.lambda_application_state.resources.toggle_expand();
7315 } else {
7316 self.lambda_application_state.deployments.toggle_expand();
7317 }
7318 } else {
7319 if self.lambda_application_state.table.expanded_item
7321 != Some(self.lambda_application_state.table.selected)
7322 {
7323 self.lambda_application_state.table.expanded_item =
7324 Some(self.lambda_application_state.table.selected);
7325 }
7326 }
7327 } else if self.current_service == Service::CloudFormationStacks
7328 && self.cfn_state.current_stack.is_none()
7329 {
7330 self.cfn_state.table.toggle_expand();
7331 } else if self.current_service == Service::CloudFormationStacks
7332 && self.cfn_state.current_stack.is_some()
7333 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
7334 {
7335 self.cfn_state.parameters.toggle_expand();
7336 } else if self.current_service == Service::CloudFormationStacks
7337 && self.cfn_state.current_stack.is_some()
7338 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
7339 {
7340 self.cfn_state.outputs.toggle_expand();
7341 } else if self.current_service == Service::CloudFormationStacks
7342 && self.cfn_state.current_stack.is_some()
7343 && self.cfn_state.detail_tab == CfnDetailTab::Resources
7344 {
7345 self.cfn_state.resources.toggle_expand();
7346 } else if self.current_service == Service::IamUsers {
7347 if self.iam_state.current_user.is_some() {
7348 if self.iam_state.user_tab == UserTab::Tags {
7349 if self.iam_state.user_tags.expanded_item
7350 != Some(self.iam_state.user_tags.selected)
7351 {
7352 self.iam_state.user_tags.expanded_item =
7353 Some(self.iam_state.user_tags.selected);
7354 }
7355 } else if self.iam_state.policies.expanded_item
7356 != Some(self.iam_state.policies.selected)
7357 {
7358 self.iam_state.policies.toggle_expand();
7359 }
7360 } else if !self.iam_state.users.is_expanded() {
7361 self.iam_state.users.toggle_expand();
7362 }
7363 } else if self.current_service == Service::IamRoles {
7364 if self.iam_state.current_role.is_some() {
7365 if self.iam_state.role_tab == RoleTab::Tags {
7367 if !self.iam_state.tags.is_expanded() {
7368 self.iam_state.tags.expand();
7369 }
7370 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
7371 if !self.iam_state.last_accessed_services.is_expanded() {
7372 self.iam_state.last_accessed_services.expand();
7373 }
7374 } else if !self.iam_state.policies.is_expanded() {
7375 self.iam_state.policies.expand();
7376 }
7377 } else if !self.iam_state.roles.is_expanded() {
7378 self.iam_state.roles.expand();
7379 }
7380 } else if self.current_service == Service::IamUserGroups {
7381 if self.iam_state.current_group.is_some() {
7382 if self.iam_state.group_tab == GroupTab::Users {
7383 if !self.iam_state.group_users.is_expanded() {
7384 self.iam_state.group_users.expand();
7385 }
7386 } else if self.iam_state.group_tab == GroupTab::Permissions {
7387 if !self.iam_state.policies.is_expanded() {
7388 self.iam_state.policies.expand();
7389 }
7390 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
7391 && !self.iam_state.last_accessed_services.is_expanded()
7392 {
7393 self.iam_state.last_accessed_services.expand();
7394 }
7395 } else if !self.iam_state.groups.is_expanded() {
7396 self.iam_state.groups.expand();
7397 }
7398 }
7399 }
7400
7401 fn go_to_page(&mut self, page: usize) {
7402 if page == 0 {
7403 return;
7404 }
7405
7406 match self.current_service {
7407 Service::CloudWatchAlarms => {
7408 let alarm_page_size = self.alarms_state.table.page_size.value();
7409 let target = (page - 1) * alarm_page_size;
7410 let filtered_count = match self.alarms_state.alarm_tab {
7411 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
7412 AlarmTab::InAlarm => self
7413 .alarms_state
7414 .table
7415 .items
7416 .iter()
7417 .filter(|a| a.state.to_uppercase() == "ALARM")
7418 .count(),
7419 };
7420 let max_offset = filtered_count.saturating_sub(alarm_page_size);
7421 self.alarms_state.table.scroll_offset = target.min(max_offset);
7422 self.alarms_state.table.selected = self
7423 .alarms_state
7424 .table
7425 .scroll_offset
7426 .min(filtered_count.saturating_sub(1));
7427 }
7428 Service::CloudTrailEvents => {
7429 let page_size = self.cloudtrail_state.table.page_size.value();
7430 let filtered_count = self.cloudtrail_state.table.items.len();
7431 let max_page = (filtered_count / page_size) + 1; if page <= max_page {
7435 let target = (page - 1) * page_size;
7436 self.cloudtrail_state.table.scroll_offset = target;
7437 self.cloudtrail_state.table.selected = target;
7438 self.cloudtrail_state.table.expanded_item = None; }
7440 }
7442 Service::CloudWatchLogGroups => match self.view_mode {
7443 ViewMode::Events => {
7444 let page_size = 20;
7445 let target = (page - 1) * page_size;
7446 let max = self.log_groups_state.log_events.len().saturating_sub(1);
7447 self.log_groups_state.event_scroll_offset = target.min(max);
7448 }
7449 ViewMode::Detail => {
7450 let page_size = self.log_groups_state.stream_page_size;
7451 self.log_groups_state.stream_current_page = (page - 1).min(
7452 self.log_groups_state
7453 .log_streams
7454 .len()
7455 .div_ceil(page_size)
7456 .saturating_sub(1),
7457 );
7458 self.log_groups_state.selected_stream = 0;
7459 }
7460 ViewMode::List => {
7461 let total = self.log_groups_state.log_groups.items.len();
7462 self.log_groups_state.log_groups.goto_page(page, total);
7463 }
7464 _ => {}
7465 },
7466 Service::EcrRepositories => {
7467 if self.ecr_state.current_repository.is_some() {
7468 let filtered_count = self
7469 .ecr_state
7470 .images
7471 .filtered(|img| {
7472 self.ecr_state.images.filter.is_empty()
7473 || img
7474 .tag
7475 .to_lowercase()
7476 .contains(&self.ecr_state.images.filter.to_lowercase())
7477 || img
7478 .digest
7479 .to_lowercase()
7480 .contains(&self.ecr_state.images.filter.to_lowercase())
7481 })
7482 .len();
7483 self.ecr_state.images.goto_page(page, filtered_count);
7484 } else {
7485 let filtered_count = self
7486 .ecr_state
7487 .repositories
7488 .filtered(|r| {
7489 self.ecr_state.repositories.filter.is_empty()
7490 || r.name
7491 .to_lowercase()
7492 .contains(&self.ecr_state.repositories.filter.to_lowercase())
7493 })
7494 .len();
7495 self.ecr_state.repositories.goto_page(page, filtered_count);
7496 }
7497 }
7498 Service::SqsQueues => {
7499 let filtered_count =
7500 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter)
7501 .len();
7502 self.sqs_state.queues.goto_page(page, filtered_count);
7503 }
7504 Service::S3Buckets => {
7505 if self.s3_state.current_bucket.is_some() {
7506 let page_size = 50; let target = (page - 1) * page_size;
7508 let total_rows = self.calculate_total_object_rows();
7509 let max = total_rows.saturating_sub(1);
7510 self.s3_state.selected_object = target.min(max);
7511 } else {
7512 let page_size = self.s3_state.buckets.page_size.value();
7513 let target = (page - 1) * page_size;
7514 let total_rows = self.calculate_total_bucket_rows();
7515 let max = total_rows.saturating_sub(1);
7516 self.s3_state.selected_row = target.min(max);
7517 self.s3_state.bucket_scroll_offset =
7519 target.min(total_rows.saturating_sub(page_size));
7520 }
7521 }
7522 Service::LambdaFunctions => {
7523 if self.lambda_state.current_function.is_some()
7524 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
7525 {
7526 let filtered_count = self
7527 .lambda_state
7528 .version_table
7529 .filtered(|v| {
7530 self.lambda_state.version_table.filter.is_empty()
7531 || v.version.to_lowercase().contains(
7532 &self.lambda_state.version_table.filter.to_lowercase(),
7533 )
7534 || v.aliases.to_lowercase().contains(
7535 &self.lambda_state.version_table.filter.to_lowercase(),
7536 )
7537 || v.description.to_lowercase().contains(
7538 &self.lambda_state.version_table.filter.to_lowercase(),
7539 )
7540 })
7541 .len();
7542 self.lambda_state
7543 .version_table
7544 .goto_page(page, filtered_count);
7545 } else {
7546 let filtered_count = filtered_lambda_functions(self).len();
7547 self.lambda_state.table.goto_page(page, filtered_count);
7548 }
7549 }
7550 Service::LambdaApplications => {
7551 let filtered_count = filtered_lambda_applications(self).len();
7552 self.lambda_application_state
7553 .table
7554 .goto_page(page, filtered_count);
7555 }
7556 Service::CloudFormationStacks => {
7557 let filtered_count = filtered_cloudformation_stacks(self).len();
7558 self.cfn_state.table.goto_page(page, filtered_count);
7559 }
7560 Service::IamUsers => {
7561 let filtered_count = filtered_iam_users(self).len();
7562 self.iam_state.users.goto_page(page, filtered_count);
7563 }
7564 Service::IamRoles => {
7565 let filtered_count = filtered_iam_roles(self).len();
7566 self.iam_state.roles.goto_page(page, filtered_count);
7567 }
7568 _ => {}
7569 }
7570 }
7571
7572 fn prev_pane(&mut self) {
7573 if self.current_service == Service::S3Buckets {
7574 if self.s3_state.current_bucket.is_some() {
7575 let mut visual_idx = 0;
7578 let mut found_obj: Option<S3Object> = None;
7579 let mut parent_idx: Option<usize> = None;
7580
7581 #[allow(clippy::too_many_arguments)]
7583 fn find_with_parent(
7584 objects: &[S3Object],
7585 visual_idx: &mut usize,
7586 target_idx: usize,
7587 expanded_prefixes: &std::collections::HashSet<String>,
7588 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7589 found_obj: &mut Option<S3Object>,
7590 parent_idx: &mut Option<usize>,
7591 current_parent: Option<usize>,
7592 ) {
7593 for obj in objects {
7594 if *visual_idx == target_idx {
7595 *found_obj = Some(obj.clone());
7596 *parent_idx = current_parent;
7597 return;
7598 }
7599 let obj_idx = *visual_idx;
7600 *visual_idx += 1;
7601
7602 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7604 if let Some(preview) = prefix_preview.get(&obj.key) {
7605 find_with_parent(
7606 preview,
7607 visual_idx,
7608 target_idx,
7609 expanded_prefixes,
7610 prefix_preview,
7611 found_obj,
7612 parent_idx,
7613 Some(obj_idx),
7614 );
7615 if found_obj.is_some() {
7616 return;
7617 }
7618 }
7619 }
7620 }
7621 }
7622
7623 find_with_parent(
7624 &self.s3_state.objects,
7625 &mut visual_idx,
7626 self.s3_state.selected_object,
7627 &self.s3_state.expanded_prefixes,
7628 &self.s3_state.prefix_preview,
7629 &mut found_obj,
7630 &mut parent_idx,
7631 None,
7632 );
7633
7634 if let Some(obj) = found_obj {
7635 if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
7636 self.s3_state.expanded_prefixes.remove(&obj.key);
7638 if let Some(parent) = parent_idx {
7639 self.s3_state.selected_object = parent;
7640 }
7641 } else if let Some(parent) = parent_idx {
7642 self.s3_state.selected_object = parent;
7644 }
7645 }
7646
7647 let visible_rows = self.s3_state.object_visible_rows.get();
7649 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
7650 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
7651 } else if self.s3_state.selected_object
7652 >= self.s3_state.object_scroll_offset + visible_rows
7653 {
7654 self.s3_state.object_scroll_offset = self
7655 .s3_state
7656 .selected_object
7657 .saturating_sub(visible_rows - 1);
7658 }
7659 } else {
7660 let mut row_idx = 0;
7662 for bucket in &self.s3_state.buckets.items {
7663 if row_idx == self.s3_state.selected_row {
7664 self.s3_state.expanded_prefixes.remove(&bucket.name);
7666 break;
7667 }
7668 row_idx += 1;
7669 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7670 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7671 #[allow(clippy::too_many_arguments)]
7673 fn check_nested_collapse(
7674 objects: &[S3Object],
7675 row_idx: &mut usize,
7676 target_row: usize,
7677 expanded_prefixes: &mut std::collections::HashSet<String>,
7678 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7679 found: &mut bool,
7680 selected_row: &mut usize,
7681 parent_row: usize,
7682 ) {
7683 for obj in objects {
7684 let current_row = *row_idx;
7685 if *row_idx == target_row {
7686 if obj.is_prefix {
7688 if expanded_prefixes.contains(&obj.key) {
7689 expanded_prefixes.remove(&obj.key);
7691 *selected_row = parent_row;
7692 } else {
7693 *selected_row = parent_row;
7695 }
7696 } else {
7697 *selected_row = parent_row;
7699 }
7700 *found = true;
7701 return;
7702 }
7703 *row_idx += 1;
7704
7705 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7707 if let Some(nested) = prefix_preview.get(&obj.key) {
7708 check_nested_collapse(
7709 nested,
7710 row_idx,
7711 target_row,
7712 expanded_prefixes,
7713 prefix_preview,
7714 found,
7715 selected_row,
7716 current_row,
7717 );
7718 if *found {
7719 return;
7720 }
7721 } else {
7722 *row_idx += 1; }
7724 }
7725 }
7726 }
7727
7728 let mut found = false;
7729 let parent_row = row_idx - 1; check_nested_collapse(
7731 preview,
7732 &mut row_idx,
7733 self.s3_state.selected_row,
7734 &mut self.s3_state.expanded_prefixes,
7735 &self.s3_state.prefix_preview,
7736 &mut found,
7737 &mut self.s3_state.selected_row,
7738 parent_row,
7739 );
7740 if found {
7741 let visible_rows = self.s3_state.bucket_visible_rows.get();
7743 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
7744 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
7745 } else if self.s3_state.selected_row
7746 >= self.s3_state.bucket_scroll_offset + visible_rows
7747 {
7748 self.s3_state.bucket_scroll_offset =
7749 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
7750 }
7751 return;
7752 }
7753 } else {
7754 row_idx += 1;
7755 }
7756 }
7757 }
7758
7759 let visible_rows = self.s3_state.bucket_visible_rows.get();
7761 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
7762 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
7763 } else if self.s3_state.selected_row
7764 >= self.s3_state.bucket_scroll_offset + visible_rows
7765 {
7766 self.s3_state.bucket_scroll_offset =
7767 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
7768 }
7769 }
7770 } else if self.view_mode == ViewMode::InsightsResults {
7771 self.insights_state.insights.results_horizontal_scroll = self
7773 .insights_state
7774 .insights
7775 .results_horizontal_scroll
7776 .saturating_sub(1);
7777 } else if self.current_service == Service::CloudWatchLogGroups
7778 && self.view_mode == ViewMode::List
7779 {
7780 if self.log_groups_state.log_groups.has_expanded_item() {
7782 self.log_groups_state.log_groups.collapse();
7783 }
7784 } else if self.current_service == Service::CloudWatchLogGroups
7785 && self.view_mode == ViewMode::Detail
7786 {
7787 if self.log_groups_state.expanded_stream.is_some() {
7789 self.log_groups_state.expanded_stream = None;
7790 }
7791 } else if self.view_mode == ViewMode::Events {
7792 if self.log_groups_state.expanded_event.is_some() {
7794 self.log_groups_state.expanded_event = None;
7795 }
7796 } else if self.current_service == Service::CloudWatchAlarms {
7797 self.alarms_state.table.collapse();
7799 } else if self.current_service == Service::Ec2Instances {
7800 self.ec2_state.table.collapse();
7801 } else if self.current_service == Service::ApiGatewayApis {
7802 self.apig_state.apis.collapse();
7803 } else if self.current_service == Service::EcrRepositories {
7804 if self.ecr_state.current_repository.is_some() {
7805 self.ecr_state.images.collapse();
7807 } else {
7808 self.ecr_state.repositories.collapse();
7810 }
7811 } else if self.current_service == Service::SqsQueues {
7812 if self.sqs_state.current_queue.is_some()
7813 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
7814 {
7815 self.sqs_state.triggers.collapse();
7816 } else if self.sqs_state.current_queue.is_some()
7817 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
7818 {
7819 self.sqs_state.pipes.collapse();
7820 } else if self.sqs_state.current_queue.is_some()
7821 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
7822 {
7823 self.sqs_state.tags.collapse();
7824 } else if self.sqs_state.current_queue.is_some()
7825 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
7826 {
7827 self.sqs_state.subscriptions.collapse();
7828 } else {
7829 self.sqs_state.queues.collapse();
7830 }
7831 } else if self.current_service == Service::LambdaFunctions {
7832 if self.lambda_state.current_function.is_some()
7833 && self.lambda_state.detail_tab == LambdaDetailTab::Code
7834 {
7835 self.lambda_state.layer_expanded = None;
7837 } else if self.lambda_state.current_function.is_some()
7838 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
7839 {
7840 self.lambda_state.version_table.collapse();
7842 } else if self.lambda_state.current_function.is_some()
7843 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
7844 || (self.lambda_state.current_version.is_some()
7845 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
7846 {
7847 self.lambda_state.alias_table.collapse();
7849 } else if self.lambda_state.current_function.is_none() {
7850 self.lambda_state.table.collapse();
7852 }
7853 } else if self.current_service == Service::LambdaApplications {
7854 if self.lambda_application_state.current_application.is_some() {
7855 if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
7857 {
7858 self.lambda_application_state.resources.collapse();
7859 } else {
7860 self.lambda_application_state.deployments.collapse();
7861 }
7862 } else {
7863 if self.lambda_application_state.table.has_expanded_item() {
7865 self.lambda_application_state.table.collapse();
7866 }
7867 }
7868 } else if self.current_service == Service::CloudFormationStacks
7869 && self.cfn_state.current_stack.is_none()
7870 {
7871 self.cfn_state.table.collapse();
7872 } else if self.current_service == Service::CloudFormationStacks
7873 && self.cfn_state.current_stack.is_some()
7874 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
7875 {
7876 self.cfn_state.parameters.collapse();
7877 } else if self.current_service == Service::CloudFormationStacks
7878 && self.cfn_state.current_stack.is_some()
7879 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
7880 {
7881 self.cfn_state.outputs.collapse();
7882 } else if self.current_service == Service::CloudFormationStacks
7883 && self.cfn_state.current_stack.is_some()
7884 && self.cfn_state.detail_tab == CfnDetailTab::Resources
7885 {
7886 self.cfn_state.resources.collapse();
7887 } else if self.current_service == Service::IamUsers {
7888 if self.iam_state.users.has_expanded_item() {
7889 self.iam_state.users.collapse();
7890 }
7891 } else if self.current_service == Service::IamRoles {
7892 if self.view_mode == ViewMode::PolicyView {
7893 self.view_mode = ViewMode::Detail;
7895 self.iam_state.current_policy = None;
7896 self.iam_state.policy_document.clear();
7897 self.iam_state.policy_scroll = 0;
7898 } else if self.iam_state.current_role.is_some() {
7899 if self.iam_state.role_tab == RoleTab::Tags
7900 && self.iam_state.tags.has_expanded_item()
7901 {
7902 self.iam_state.tags.collapse();
7903 } else if self.iam_state.role_tab == RoleTab::LastAccessed
7904 && self
7905 .iam_state
7906 .last_accessed_services
7907 .expanded_item
7908 .is_some()
7909 {
7910 self.iam_state.last_accessed_services.collapse();
7911 } else if self.iam_state.policies.has_expanded_item() {
7912 self.iam_state.policies.collapse();
7913 }
7914 } else if self.iam_state.roles.has_expanded_item() {
7915 self.iam_state.roles.collapse();
7916 }
7917 } else if self.current_service == Service::IamUserGroups {
7918 if self.iam_state.current_group.is_some() {
7919 if self.iam_state.group_tab == GroupTab::Users
7920 && self.iam_state.group_users.has_expanded_item()
7921 {
7922 self.iam_state.group_users.collapse();
7923 } else if self.iam_state.group_tab == GroupTab::Permissions
7924 && self.iam_state.policies.has_expanded_item()
7925 {
7926 self.iam_state.policies.collapse();
7927 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
7928 && self
7929 .iam_state
7930 .last_accessed_services
7931 .expanded_item
7932 .is_some()
7933 {
7934 self.iam_state.last_accessed_services.collapse();
7935 }
7936 } else if self.iam_state.groups.has_expanded_item() {
7937 self.iam_state.groups.collapse();
7938 }
7939 }
7940 }
7941
7942 fn collapse_row(&mut self) {
7943 match self.current_service {
7944 Service::S3Buckets => {
7945 if self.s3_state.current_bucket.is_none() {
7946 let filtered_buckets: Vec<_> = self
7948 .s3_state
7949 .buckets
7950 .items
7951 .iter()
7952 .filter(|b| {
7953 if self.s3_state.buckets.filter.is_empty() {
7954 true
7955 } else {
7956 b.name
7957 .to_lowercase()
7958 .contains(&self.s3_state.buckets.filter.to_lowercase())
7959 }
7960 })
7961 .collect();
7962
7963 let mut row_idx = 0;
7965
7966 for bucket in filtered_buckets {
7967 if row_idx == self.s3_state.selected_row {
7968 self.s3_state.expanded_prefixes.remove(&bucket.name);
7970 break;
7972 }
7973 row_idx += 1;
7974 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7975 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
7977 continue;
7979 }
7980 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7981 #[allow(clippy::too_many_arguments)]
7983 fn check_nested_collapse(
7984 objects: &[S3Object],
7985 row_idx: &mut usize,
7986 target_row: usize,
7987 expanded_prefixes: &mut std::collections::HashSet<String>,
7988 prefix_preview: &std::collections::HashMap<
7989 String,
7990 Vec<S3Object>,
7991 >,
7992 found: &mut bool,
7993 selected_row: &mut usize,
7994 parent_row: usize,
7995 ) {
7996 for obj in objects {
7997 let current_row = *row_idx;
7998 if *row_idx == target_row {
7999 if obj.is_prefix {
8001 if expanded_prefixes.contains(&obj.key) {
8002 expanded_prefixes.remove(&obj.key);
8004 *selected_row = parent_row;
8005 } else {
8006 *selected_row = parent_row;
8008 }
8009 } else {
8010 *selected_row = parent_row;
8012 }
8013 *found = true;
8014 return;
8015 }
8016 *row_idx += 1;
8017
8018 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8020 if let Some(nested) = prefix_preview.get(&obj.key) {
8021 check_nested_collapse(
8022 nested,
8023 row_idx,
8024 target_row,
8025 expanded_prefixes,
8026 prefix_preview,
8027 found,
8028 selected_row,
8029 current_row,
8030 );
8031 if *found {
8032 return;
8033 }
8034 } else {
8035 *row_idx += 1; }
8037 }
8038 }
8039 }
8040
8041 let mut found = false;
8042 let parent_row = row_idx - 1; check_nested_collapse(
8044 preview,
8045 &mut row_idx,
8046 self.s3_state.selected_row,
8047 &mut self.s3_state.expanded_prefixes,
8048 &self.s3_state.prefix_preview,
8049 &mut found,
8050 &mut self.s3_state.selected_row,
8051 parent_row,
8052 );
8053 if found {
8054 let visible_rows = self.s3_state.bucket_visible_rows.get();
8056 if self.s3_state.selected_row
8057 < self.s3_state.bucket_scroll_offset
8058 {
8059 self.s3_state.bucket_scroll_offset =
8060 self.s3_state.selected_row;
8061 } else if self.s3_state.selected_row
8062 >= self.s3_state.bucket_scroll_offset + visible_rows
8063 {
8064 self.s3_state.bucket_scroll_offset = self
8065 .s3_state
8066 .selected_row
8067 .saturating_sub(visible_rows - 1);
8068 }
8069 return;
8070 }
8071 } else {
8072 row_idx += 1;
8073 }
8074 }
8075 }
8076
8077 let visible_rows = self.s3_state.bucket_visible_rows.get();
8079 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
8080 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
8081 } else if self.s3_state.selected_row
8082 >= self.s3_state.bucket_scroll_offset + visible_rows
8083 {
8084 self.s3_state.bucket_scroll_offset =
8085 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
8086 }
8087 }
8088 }
8089 Service::CloudWatchLogGroups => {
8090 if self.view_mode == ViewMode::Events {
8091 if let Some(idx) = self.log_groups_state.expanded_event {
8092 self.log_groups_state.expanded_event = None;
8093 self.log_groups_state.selected_event = idx;
8094 }
8095 } else if self.view_mode == ViewMode::Detail {
8096 if let Some(idx) = self.log_groups_state.expanded_stream {
8097 self.log_groups_state.expanded_stream = None;
8098 self.log_groups_state.selected_stream = idx;
8099 }
8100 } else {
8101 self.log_groups_state.log_groups.collapse();
8102 }
8103 }
8104 Service::CloudWatchAlarms => self.alarms_state.table.collapse(),
8105 Service::Ec2Instances => {
8106 if self.ec2_state.current_instance.is_some()
8107 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
8108 {
8109 self.ec2_state.tags.collapse();
8110 } else {
8111 self.ec2_state.table.collapse();
8112 }
8113 }
8114 Service::EcrRepositories => {
8115 if self.ecr_state.current_repository.is_some() {
8116 self.ecr_state.images.collapse();
8117 } else {
8118 self.ecr_state.repositories.collapse();
8119 }
8120 }
8121 Service::LambdaFunctions => self.lambda_state.table.collapse(),
8122 Service::SqsQueues => self.sqs_state.queues.collapse(),
8123 Service::CloudFormationStacks => {
8124 if self.cfn_state.current_stack.is_some() {
8125 match self.cfn_state.detail_tab {
8126 crate::ui::cfn::DetailTab::Resources => {
8127 self.cfn_state.resources.collapse();
8128 }
8129 crate::ui::cfn::DetailTab::Parameters => {
8130 self.cfn_state.parameters.collapse();
8131 }
8132 crate::ui::cfn::DetailTab::Outputs => {
8133 self.cfn_state.outputs.collapse();
8134 }
8135 _ => {}
8136 }
8137 } else {
8138 self.cfn_state.table.collapse();
8139 }
8140 }
8141 Service::IamUsers => {
8142 if self.iam_state.current_user.is_some() {
8143 match self.iam_state.user_tab {
8144 crate::ui::iam::UserTab::Permissions => {
8145 self.iam_state.policies.collapse();
8146 }
8147 crate::ui::iam::UserTab::Groups => {
8148 self.iam_state.user_group_memberships.collapse();
8149 }
8150 crate::ui::iam::UserTab::Tags => {
8151 self.iam_state.user_tags.collapse();
8152 }
8153 _ => {}
8154 }
8155 } else {
8156 self.iam_state.users.collapse();
8157 }
8158 }
8159 Service::IamRoles => {
8160 if self.iam_state.current_role.is_some() {
8161 match self.iam_state.role_tab {
8162 crate::ui::iam::RoleTab::Permissions => {
8163 self.iam_state.policies.collapse();
8164 }
8165 crate::ui::iam::RoleTab::Tags => {
8166 self.iam_state.tags.collapse();
8167 }
8168 _ => {}
8169 }
8170 } else {
8171 self.iam_state.roles.collapse();
8172 }
8173 }
8174 Service::IamUserGroups => self.iam_state.groups.collapse(),
8175 Service::ApiGatewayApis => {
8176 if let Some(api) = &self.apig_state.current_api {
8177 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
8178 let protocol = api.protocol_type.to_uppercase();
8179 if protocol == "REST" {
8180 let (filtered_items, _filtered_children) =
8182 crate::ui::apig::filter_tree_items(
8183 &self.apig_state.resources.items,
8184 &self.apig_state.resource_children,
8185 &self.apig_state.route_filter,
8186 );
8187
8188 let selected_row = self.apig_state.resources.selected;
8189 let mut current_row = 0;
8190 if let Some(resource_id) = self.find_resource_at_row(
8191 &filtered_items,
8192 selected_row,
8193 &mut current_row,
8194 ) {
8195 if self.apig_state.expanded_resources.contains(&resource_id) {
8196 self.apig_state.expanded_resources.remove(&resource_id);
8197 } else if let Some(parent_row) =
8198 self.find_resource_parent_row(&filtered_items, &resource_id)
8199 {
8200 self.apig_state.resources.selected = parent_row;
8201 }
8202 }
8203 } else {
8204 let (filtered_items, _filtered_children) =
8206 crate::ui::apig::filter_tree_items(
8207 &self.apig_state.routes.items,
8208 &self.apig_state.route_children,
8209 &self.apig_state.route_filter,
8210 );
8211
8212 let selected_row = self.apig_state.routes.selected;
8213 let mut current_row = 0;
8214 if let Some(route_key) = self.find_route_at_row(
8215 &filtered_items,
8216 selected_row,
8217 &mut current_row,
8218 ) {
8219 if self.apig_state.expanded_routes.contains(&route_key) {
8220 self.apig_state.expanded_routes.remove(&route_key);
8221 } else if let Some(parent_row) =
8222 self.find_parent_row(&filtered_items, &route_key)
8223 {
8224 self.apig_state.routes.selected = parent_row;
8225 }
8226 }
8227 }
8228 }
8229 } else {
8230 self.apig_state.apis.collapse();
8231 }
8232 }
8233 Service::CloudTrailEvents => {
8234 if self.cloudtrail_state.current_event.is_some()
8235 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
8236 {
8237 self.cloudtrail_state.resources_expanded_index = None;
8239 } else {
8240 self.cloudtrail_state.table.collapse();
8241 }
8242 }
8243 _ => {}
8244 }
8245 }
8246
8247 fn expand_row(&mut self) {
8248 match self.current_service {
8249 Service::S3Buckets => {
8250 if self.s3_state.current_bucket.is_none() {
8251 let filtered_buckets: Vec<_> = self
8253 .s3_state
8254 .buckets
8255 .items
8256 .iter()
8257 .filter(|b| {
8258 if self.s3_state.buckets.filter.is_empty() {
8259 true
8260 } else {
8261 b.name
8262 .to_lowercase()
8263 .contains(&self.s3_state.buckets.filter.to_lowercase())
8264 }
8265 })
8266 .collect();
8267
8268 fn check_nested_expand(
8270 objects: &[S3Object],
8271 row_idx: &mut usize,
8272 target_row: usize,
8273 expanded_prefixes: &mut std::collections::HashSet<String>,
8274 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
8275 ) -> Option<(bool, usize)> {
8276 for obj in objects {
8277 if *row_idx == target_row {
8278 if obj.is_prefix {
8279 if expanded_prefixes.contains(&obj.key) {
8281 expanded_prefixes.remove(&obj.key);
8282 return Some((true, *row_idx));
8283 } else {
8284 expanded_prefixes.insert(obj.key.clone());
8285 return Some((true, *row_idx + 1)); }
8287 }
8288 return Some((false, *row_idx));
8289 }
8290 *row_idx += 1;
8291
8292 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8294 if let Some(nested) = prefix_preview.get(&obj.key) {
8295 if let Some(result) = check_nested_expand(
8296 nested,
8297 row_idx,
8298 target_row,
8299 expanded_prefixes,
8300 prefix_preview,
8301 ) {
8302 return Some(result);
8303 }
8304 }
8305 }
8306 }
8307 None
8308 }
8309
8310 let mut row_idx = 0;
8311 for bucket in filtered_buckets {
8312 if row_idx == self.s3_state.selected_row {
8313 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
8315 self.s3_state.expanded_prefixes.remove(&bucket.name);
8316 } else {
8317 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
8318 self.s3_state.selected_row = row_idx + 1; self.s3_state.buckets.loading = true;
8320 }
8321 return;
8322 }
8323 row_idx += 1;
8324
8325 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
8326 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
8327 continue;
8328 }
8329 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
8330 if let Some((loading, new_row)) = check_nested_expand(
8331 preview,
8332 &mut row_idx,
8333 self.s3_state.selected_row,
8334 &mut self.s3_state.expanded_prefixes,
8335 &self.s3_state.prefix_preview,
8336 ) {
8337 self.s3_state.selected_row = new_row;
8338 if loading {
8339 self.s3_state.buckets.loading = true;
8340 }
8341 return;
8342 }
8343 }
8344 }
8345 }
8346 } else {
8347 fn check_object_expand(
8349 objects: &[S3Object],
8350 row_idx: &mut usize,
8351 target_row: usize,
8352 expanded_prefixes: &mut std::collections::HashSet<String>,
8353 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
8354 ) -> Option<(bool, usize)> {
8355 for obj in objects {
8356 if *row_idx == target_row {
8357 if obj.is_prefix {
8358 if expanded_prefixes.contains(&obj.key) {
8359 expanded_prefixes.remove(&obj.key);
8360 return Some((true, *row_idx));
8361 } else {
8362 expanded_prefixes.insert(obj.key.clone());
8363 return Some((true, *row_idx + 1));
8364 }
8365 }
8366 return Some((false, *row_idx));
8367 }
8368 *row_idx += 1;
8369
8370 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8371 if let Some(nested) = prefix_preview.get(&obj.key) {
8372 if let Some(result) = check_object_expand(
8373 nested,
8374 row_idx,
8375 target_row,
8376 expanded_prefixes,
8377 prefix_preview,
8378 ) {
8379 return Some(result);
8380 }
8381 }
8382 }
8383 }
8384 None
8385 }
8386
8387 let mut row_idx = 0;
8388 if let Some((loading, new_row)) = check_object_expand(
8389 &self.s3_state.objects,
8390 &mut row_idx,
8391 self.s3_state.selected_object,
8392 &mut self.s3_state.expanded_prefixes,
8393 &self.s3_state.prefix_preview,
8394 ) {
8395 self.s3_state.selected_object = new_row;
8396 if loading {
8397 self.s3_state.buckets.loading = true;
8398 }
8399 }
8400 }
8401 }
8402 Service::ApiGatewayApis => {
8403 if let Some(api) = &self.apig_state.current_api {
8404 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
8405 let protocol = api.protocol_type.to_uppercase();
8406 if protocol == "REST" {
8407 let (filtered_items, filtered_children) =
8409 crate::ui::apig::filter_tree_items(
8410 &self.apig_state.resources.items,
8411 &self.apig_state.resource_children,
8412 &self.apig_state.route_filter,
8413 );
8414
8415 let selected_row = self.apig_state.resources.selected;
8416 let mut current_row = 0;
8417 if let Some(resource_id) = self.find_resource_at_row(
8418 &filtered_items,
8419 selected_row,
8420 &mut current_row,
8421 ) {
8422 if self.apig_state.expanded_resources.contains(&resource_id) {
8423 let total_rows =
8424 crate::ui::tree::TreeRenderer::count_visible_rows(
8425 &filtered_items,
8426 &self.apig_state.expanded_resources,
8427 &filtered_children,
8428 );
8429 if selected_row + 1 < total_rows {
8430 self.apig_state.resources.selected = selected_row + 1;
8431 }
8432 } else {
8433 self.apig_state.expanded_resources.insert(resource_id);
8434 }
8435 }
8436 } else {
8437 let (filtered_items, filtered_children) =
8439 crate::ui::apig::filter_tree_items(
8440 &self.apig_state.routes.items,
8441 &self.apig_state.route_children,
8442 &self.apig_state.route_filter,
8443 );
8444
8445 let selected_row = self.apig_state.routes.selected;
8446 let mut current_row = 0;
8447 if let Some(route_key) = self.find_route_at_row(
8448 &filtered_items,
8449 selected_row,
8450 &mut current_row,
8451 ) {
8452 if self.apig_state.expanded_routes.contains(&route_key) {
8453 let total_rows =
8454 crate::ui::tree::TreeRenderer::count_visible_rows(
8455 &filtered_items,
8456 &self.apig_state.expanded_routes,
8457 &filtered_children,
8458 );
8459 if selected_row + 1 < total_rows {
8460 self.apig_state.routes.selected = selected_row + 1;
8461 }
8462 } else {
8463 self.apig_state.expanded_routes.insert(route_key);
8464 }
8465 }
8466 }
8467 }
8468 } else {
8469 self.apig_state.apis.expand();
8470 }
8471 }
8472 Service::CloudTrailEvents => {
8473 if self.cloudtrail_state.current_event.is_some()
8474 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
8475 {
8476 self.cloudtrail_state.resources_expanded_index = Some(0);
8478 } else {
8479 self.cloudtrail_state.table.expand();
8480 }
8481 }
8482 _ => {
8483 self.next_pane();
8485 }
8486 }
8487 }
8488
8489 fn find_route_at_row(
8490 &self,
8491 routes: &[Route],
8492 target_row: usize,
8493 current_row: &mut usize,
8494 ) -> Option<String> {
8495 for route in routes {
8496 if *current_row == target_row {
8497 return Some(route.route_key.clone());
8498 }
8499 *current_row += 1;
8500
8501 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
8503 if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
8504 if let Some(key) = self.find_route_at_row(children, target_row, current_row) {
8505 return Some(key);
8506 }
8507 }
8508 }
8509 }
8510 None
8511 }
8512
8513 fn find_route_id_at_row_with_children(
8514 &self,
8515 routes: &[Route],
8516 children_map: &HashMap<String, Vec<Route>>,
8517 target_row: usize,
8518 current_row: &mut usize,
8519 ) -> Option<String> {
8520 for route in routes {
8521 if *current_row == target_row {
8522 if !route.target.is_empty() {
8524 return Some(route.route_id.clone());
8525 } else {
8526 return None;
8527 }
8528 }
8529 *current_row += 1;
8530
8531 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
8533 if let Some(children) = children_map.get(&route.route_key) {
8534 if let Some(id) = self.find_route_id_at_row_with_children(
8535 children,
8536 children_map,
8537 target_row,
8538 current_row,
8539 ) {
8540 return Some(id);
8541 }
8542 }
8543 }
8544 }
8545 None
8546 }
8547
8548 fn find_parent_row(&self, routes: &[Route], child_key: &str) -> Option<usize> {
8549 let mut current_row = 0;
8550 self.find_parent_row_recursive(routes, child_key, &mut current_row)
8551 }
8552
8553 fn find_parent_row_recursive(
8554 &self,
8555 routes: &[Route],
8556 child_key: &str,
8557 current_row: &mut usize,
8558 ) -> Option<usize> {
8559 for route in routes {
8560 let parent_row = *current_row;
8561 *current_row += 1;
8562
8563 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
8565 if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
8566 for child in children {
8568 if child.route_key == child_key {
8569 return Some(parent_row);
8570 }
8571 }
8572
8573 if let Some(row) =
8575 self.find_parent_row_recursive(children, child_key, current_row)
8576 {
8577 return Some(row);
8578 }
8579 }
8580 }
8581 }
8582 None
8583 }
8584
8585 fn find_resource_at_row(
8586 &self,
8587 resources: &[ApigResource],
8588 target_row: usize,
8589 current_row: &mut usize,
8590 ) -> Option<String> {
8591 for resource in resources {
8592 if *current_row == target_row {
8593 return Some(resource.id.clone());
8594 }
8595 *current_row += 1;
8596
8597 if self.apig_state.expanded_resources.contains(&resource.id) {
8598 if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
8599 if let Some(id) = self.find_resource_at_row(children, target_row, current_row) {
8600 return Some(id);
8601 }
8602 }
8603 }
8604 }
8605 None
8606 }
8607
8608 fn find_resource_parent_row(
8609 &self,
8610 resources: &[ApigResource],
8611 child_id: &str,
8612 ) -> Option<usize> {
8613 let mut current_row = 0;
8614 self.find_resource_parent_row_recursive(resources, child_id, &mut current_row)
8615 }
8616
8617 fn find_resource_parent_row_recursive(
8618 &self,
8619 resources: &[ApigResource],
8620 child_id: &str,
8621 current_row: &mut usize,
8622 ) -> Option<usize> {
8623 for resource in resources {
8624 let parent_row = *current_row;
8625 *current_row += 1;
8626
8627 if self.apig_state.expanded_resources.contains(&resource.id) {
8628 if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
8629 for child in children {
8630 if child.id == child_id {
8631 return Some(parent_row);
8632 }
8633 }
8634
8635 if let Some(row) =
8636 self.find_resource_parent_row_recursive(children, child_id, current_row)
8637 {
8638 return Some(row);
8639 }
8640 }
8641 }
8642 }
8643 None
8644 }
8645
8646 fn select_item(&mut self) {
8647 if self.mode == Mode::RegionPicker {
8648 let filtered = self.get_filtered_regions();
8649 if let Some(region) = filtered.get(self.region_picker_selected) {
8650 if !self.tabs.is_empty() {
8652 let mut session = Session::new(
8653 self.profile.clone(),
8654 self.region.clone(),
8655 self.config.account_id.clone(),
8656 self.config.role_arn.clone(),
8657 );
8658
8659 for tab in &self.tabs {
8660 session.tabs.push(SessionTab {
8661 service: format!("{:?}", tab.service),
8662 title: tab.title.clone(),
8663 breadcrumb: tab.breadcrumb.clone(),
8664 filter: None,
8665 selected_item: None,
8666 });
8667 }
8668
8669 let _ = session.save();
8670 }
8671
8672 self.region = region.code.to_string();
8673 self.config.region = region.code.to_string();
8674
8675 self.tabs.clear();
8677 self.current_tab = 0;
8678 self.service_selected = false;
8679
8680 self.mode = Mode::Normal;
8681 }
8682 } else if self.mode == Mode::ProfilePicker {
8683 let filtered = self.get_filtered_profiles();
8684 if let Some(profile) = filtered.get(self.profile_picker_selected) {
8685 let profile_name = profile.name.clone();
8686 let profile_region = profile.region.clone();
8687
8688 self.profile = profile_name.clone();
8689 std::env::set_var("AWS_PROFILE", &profile_name);
8690
8691 if let Some(region) = profile_region {
8693 self.region = region;
8694 }
8695
8696 self.mode = Mode::Normal;
8697 }
8699 } else if self.mode == Mode::ServicePicker {
8700 let filtered = self.filtered_services();
8701 if let Some(&service) = filtered.get(self.service_picker.selected) {
8702 let new_service = match service {
8703 "API Gateway › APIs" => Service::ApiGatewayApis,
8704 "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
8705 "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
8706 "CloudWatch › Alarms" => Service::CloudWatchAlarms,
8707 "CloudTrail › Event History" => Service::CloudTrailEvents,
8708 "CloudFormation › Stacks" => Service::CloudFormationStacks,
8709 "EC2 › Instances" => Service::Ec2Instances,
8710 "ECR › Repositories" => Service::EcrRepositories,
8711 "IAM › Users" => Service::IamUsers,
8712 "IAM › Roles" => Service::IamRoles,
8713 "IAM › User Groups" => Service::IamUserGroups,
8714 "Lambda › Functions" => Service::LambdaFunctions,
8715 "Lambda › Applications" => Service::LambdaApplications,
8716 "S3 › Buckets" => Service::S3Buckets,
8717 "SQS › Queues" => Service::SqsQueues,
8718 _ => return,
8719 };
8720
8721 self.tabs.push(Tab {
8723 service: new_service,
8724 title: service.to_string(),
8725 breadcrumb: service.to_string(),
8726 });
8727 self.current_tab = self.tabs.len() - 1;
8728 self.current_service = new_service;
8729 self.view_mode = ViewMode::List;
8730 self.service_selected = true;
8731 self.mode = Mode::Normal;
8732 }
8733 } else if self.mode == Mode::TabPicker {
8734 let filtered = self.get_filtered_tabs();
8735 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
8736 self.current_tab = idx;
8737 self.current_service = self.tabs[idx].service;
8738 self.mode = Mode::Normal;
8739 self.tab_filter.clear();
8740 }
8741 } else if self.mode == Mode::SessionPicker {
8742 let filtered = self.get_filtered_sessions();
8743 if let Some(&session) = filtered.get(self.session_picker_selected) {
8744 let session = session.clone();
8745
8746 self.current_session = Some(session.clone());
8748 self.profile = session.profile.clone();
8749 self.region = session.region.clone();
8750 self.config.region = session.region.clone();
8751 self.config.account_id = session.account_id.clone();
8752 self.config.role_arn = session.role_arn.clone();
8753
8754 self.tabs = session
8756 .tabs
8757 .iter()
8758 .map(|st| Tab {
8759 service: match st.service.as_str() {
8760 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
8761 "CloudWatchInsights" => Service::CloudWatchInsights,
8762 "CloudWatchAlarms" => Service::CloudWatchAlarms,
8763 "S3Buckets" => Service::S3Buckets,
8764 "CloudTrailEvents" => Service::CloudTrailEvents,
8765 "SqsQueues" => Service::SqsQueues,
8766 _ => Service::CloudWatchLogGroups,
8767 },
8768 title: st.title.clone(),
8769 breadcrumb: st.breadcrumb.clone(),
8770 })
8771 .collect();
8772
8773 if !self.tabs.is_empty() {
8774 self.current_tab = 0;
8775 self.current_service = self.tabs[0].service;
8776 self.service_selected = true;
8777 }
8778
8779 self.mode = Mode::Normal;
8780 }
8781 } else if self.mode == Mode::InsightsInput {
8782 use crate::app::InsightsFocus;
8784 match self.insights_state.insights.insights_focus {
8785 InsightsFocus::Query => {
8786 self.insights_state.insights.query_text.push('\n');
8788 self.insights_state.insights.query_cursor_line += 1;
8789 self.insights_state.insights.query_cursor_col = 0;
8790 }
8791 InsightsFocus::LogGroupSearch => {
8792 self.insights_state.insights.show_dropdown =
8794 !self.insights_state.insights.show_dropdown;
8795 }
8796 _ => {}
8797 }
8798 } else if self.mode == Mode::Normal {
8799 if !self.service_selected {
8801 let filtered = self.filtered_services();
8802 if let Some(&service) = filtered.get(self.service_picker.selected) {
8803 match service {
8804 "CloudWatch › Log Groups" => {
8805 self.current_service = Service::CloudWatchLogGroups;
8806 self.view_mode = ViewMode::List;
8807 self.service_selected = true;
8808 }
8809 "CloudWatch › Logs Insights" => {
8810 self.current_service = Service::CloudWatchInsights;
8811 self.view_mode = ViewMode::InsightsResults;
8812 self.service_selected = true;
8813 }
8814 "CloudWatch › Alarms" => {
8815 self.current_service = Service::CloudWatchAlarms;
8816 self.view_mode = ViewMode::List;
8817 self.service_selected = true;
8818 }
8819 "S3 › Buckets" => {
8820 self.current_service = Service::S3Buckets;
8821 self.view_mode = ViewMode::List;
8822 self.service_selected = true;
8823 }
8824 "EC2 › Instances" => {
8825 self.current_service = Service::Ec2Instances;
8826 self.view_mode = ViewMode::List;
8827 self.service_selected = true;
8828 }
8829 "ECR › Repositories" => {
8830 self.current_service = Service::EcrRepositories;
8831 self.view_mode = ViewMode::List;
8832 self.service_selected = true;
8833 }
8834 "Lambda › Functions" => {
8835 self.current_service = Service::LambdaFunctions;
8836 self.view_mode = ViewMode::List;
8837 self.service_selected = true;
8838 }
8839 "Lambda › Applications" => {
8840 self.current_service = Service::LambdaApplications;
8841 self.view_mode = ViewMode::List;
8842 self.service_selected = true;
8843 }
8844 _ => {}
8845 }
8846 }
8847 return;
8848 }
8849
8850 if self.view_mode == ViewMode::InsightsResults {
8852 if self.insights_state.insights.expanded_result
8854 == Some(self.insights_state.insights.results_selected)
8855 {
8856 self.insights_state.insights.expanded_result = None;
8857 } else {
8858 self.insights_state.insights.expanded_result =
8859 Some(self.insights_state.insights.results_selected);
8860 }
8861 } else if self.current_service == Service::S3Buckets {
8862 if self.s3_state.current_bucket.is_none() {
8863 let filtered_buckets: Vec<_> = self
8865 .s3_state
8866 .buckets
8867 .items
8868 .iter()
8869 .filter(|b| {
8870 if self.s3_state.buckets.filter.is_empty() {
8871 true
8872 } else {
8873 b.name
8874 .to_lowercase()
8875 .contains(&self.s3_state.buckets.filter.to_lowercase())
8876 }
8877 })
8878 .collect();
8879
8880 let mut row_idx = 0;
8882 for bucket in filtered_buckets {
8883 if row_idx == self.s3_state.selected_row {
8884 self.s3_state.current_bucket = Some(bucket.name.clone());
8886 self.s3_state.prefix_stack.clear();
8887 self.s3_state.buckets.loading = true;
8888 return;
8889 }
8890 row_idx += 1;
8891
8892 if self.s3_state.bucket_errors.contains_key(&bucket.name)
8894 && self.s3_state.expanded_prefixes.contains(&bucket.name)
8895 {
8896 continue;
8897 }
8898
8899 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
8900 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
8901 for obj in preview {
8902 if row_idx == self.s3_state.selected_row {
8903 if obj.is_prefix {
8905 self.s3_state.current_bucket =
8906 Some(bucket.name.clone());
8907 self.s3_state.prefix_stack = vec![obj.key.clone()];
8908 self.s3_state.buckets.loading = true;
8909 }
8910 return;
8911 }
8912 row_idx += 1;
8913
8914 if obj.is_prefix
8916 && self.s3_state.expanded_prefixes.contains(&obj.key)
8917 {
8918 if let Some(nested) =
8919 self.s3_state.prefix_preview.get(&obj.key)
8920 {
8921 for nested_obj in nested {
8922 if row_idx == self.s3_state.selected_row {
8923 if nested_obj.is_prefix {
8925 self.s3_state.current_bucket =
8926 Some(bucket.name.clone());
8927 self.s3_state.prefix_stack = vec![
8929 obj.key.clone(),
8930 nested_obj.key.clone(),
8931 ];
8932 self.s3_state.buckets.loading = true;
8933 }
8934 return;
8935 }
8936 row_idx += 1;
8937 }
8938 } else {
8939 row_idx += 1;
8940 }
8941 }
8942 }
8943 } else {
8944 row_idx += 1;
8945 }
8946 }
8947 }
8948 } else {
8949 let mut visual_idx = 0;
8951 let mut found_obj: Option<S3Object> = None;
8952
8953 fn check_nested_select(
8955 obj: &S3Object,
8956 visual_idx: &mut usize,
8957 target_idx: usize,
8958 expanded_prefixes: &std::collections::HashSet<String>,
8959 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
8960 found_obj: &mut Option<S3Object>,
8961 ) {
8962 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8963 if let Some(preview) = prefix_preview.get(&obj.key) {
8964 for nested_obj in preview {
8965 if *visual_idx == target_idx {
8966 *found_obj = Some(nested_obj.clone());
8967 return;
8968 }
8969 *visual_idx += 1;
8970
8971 check_nested_select(
8973 nested_obj,
8974 visual_idx,
8975 target_idx,
8976 expanded_prefixes,
8977 prefix_preview,
8978 found_obj,
8979 );
8980 if found_obj.is_some() {
8981 return;
8982 }
8983 }
8984 } else {
8985 *visual_idx += 1;
8987 }
8988 }
8989 }
8990
8991 for obj in &self.s3_state.objects {
8992 if visual_idx == self.s3_state.selected_object {
8993 found_obj = Some(obj.clone());
8994 break;
8995 }
8996 visual_idx += 1;
8997
8998 check_nested_select(
9000 obj,
9001 &mut visual_idx,
9002 self.s3_state.selected_object,
9003 &self.s3_state.expanded_prefixes,
9004 &self.s3_state.prefix_preview,
9005 &mut found_obj,
9006 );
9007 if found_obj.is_some() {
9008 break;
9009 }
9010 }
9011
9012 if let Some(obj) = found_obj {
9013 if obj.is_prefix {
9014 self.s3_state.prefix_stack.push(obj.key.clone());
9016 self.s3_state.buckets.loading = true;
9017 }
9018 }
9019 }
9020 } else if self.current_service == Service::ApiGatewayApis {
9021 if self.apig_state.current_api.is_none() {
9022 let filtered_apis = crate::ui::apig::filtered_apis(self);
9024 if let Some(api) = self.apig_state.apis.get_selected(&filtered_apis) {
9025 let protocol = api.protocol_type.to_uppercase();
9026 self.apig_state.current_api = Some((*api).clone());
9027 if protocol == "REST" {
9028 self.apig_state.resources.loading = true;
9029 } else {
9030 self.apig_state.routes.loading = true;
9031 }
9032 self.update_current_tab_breadcrumb();
9033 }
9034 }
9035 } else if self.current_service == Service::CloudFormationStacks {
9036 if self.cfn_state.current_stack.is_none() {
9037 let filtered_stacks = filtered_cloudformation_stacks(self);
9039 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
9040 let stack_name = stack.name.clone();
9041 let mut tags = stack.tags.clone();
9042 tags.sort_by(|a, b| a.0.cmp(&b.0));
9043
9044 self.cfn_state.current_stack = Some(stack_name);
9045 self.cfn_state.tags.items = tags;
9046 self.cfn_state.tags.reset();
9047 self.cfn_state.table.loading = true;
9048 self.update_current_tab_breadcrumb();
9049 }
9050 }
9051 } else if self.current_service == Service::CloudTrailEvents {
9052 if self.cloudtrail_state.current_event.is_none() {
9053 let filtered_events: Vec<_> =
9054 self.cloudtrail_state.table.items.iter().collect();
9055 if let Some(event) = self.cloudtrail_state.table.get_selected(&filtered_events)
9056 {
9057 self.cloudtrail_state.current_event = Some((*event).clone());
9058 self.cloudtrail_state.event_json_scroll = 0;
9059 self.update_current_tab_breadcrumb();
9060 }
9061 }
9062 } else if self.current_service == Service::EcrRepositories {
9063 if self.ecr_state.current_repository.is_none() {
9064 let filtered_repos = filtered_ecr_repositories(self);
9066 if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
9067 let repo_name = repo.name.clone();
9068 let repo_uri = repo.uri.clone();
9069 self.ecr_state.current_repository = Some(repo_name);
9070 self.ecr_state.current_repository_uri = Some(repo_uri);
9071 self.ecr_state.images.reset();
9072 self.ecr_state.repositories.loading = true;
9073 }
9074 }
9075 } else if self.current_service == Service::Ec2Instances {
9076 if self.ec2_state.current_instance.is_none() {
9077 let filtered_instances = filtered_ec2_instances(self);
9078 if let Some(instance) = self.ec2_state.table.get_selected(&filtered_instances) {
9079 self.ec2_state.current_instance = Some(instance.instance_id.clone());
9080 self.view_mode = ViewMode::Detail;
9081 self.update_current_tab_breadcrumb();
9082 }
9083 }
9084 } else if self.current_service == Service::SqsQueues {
9085 if self.sqs_state.current_queue.is_none() {
9086 let filtered_queues = filtered_queues(
9087 &self.sqs_state.queues.items,
9088 &self.sqs_state.queues.filter,
9089 );
9090 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
9091 self.sqs_state.current_queue = Some(queue.url.clone());
9092
9093 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
9094 self.sqs_state.metrics_loading = true;
9095 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
9096 self.sqs_state.triggers.loading = true;
9097 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
9098 self.sqs_state.pipes.loading = true;
9099 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
9100 self.sqs_state.tags.loading = true;
9101 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
9102 self.sqs_state.subscriptions.loading = true;
9103 }
9104 }
9105 }
9106 } else if self.current_service == Service::IamUsers {
9107 if self.iam_state.current_user.is_some() {
9108 if self.iam_state.user_tab == UserTab::Permissions {
9110 let filtered = filtered_iam_policies(self);
9111 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
9112 self.iam_state.current_policy = Some(policy.policy_name.clone());
9113 self.iam_state.policy_scroll = 0;
9114 self.view_mode = ViewMode::PolicyView;
9115 self.iam_state.policies.loading = true;
9116 self.update_current_tab_breadcrumb();
9117 }
9118 }
9119 } else if self.iam_state.current_user.is_none() {
9120 let filtered_users = filtered_iam_users(self);
9121 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
9122 self.iam_state.current_user = Some(user.user_name.clone());
9123 self.iam_state.user_tab = UserTab::Permissions;
9124 self.iam_state.policies.reset();
9125 self.update_current_tab_breadcrumb();
9126 }
9127 }
9128 } else if self.current_service == Service::IamRoles {
9129 if self.iam_state.current_role.is_some() {
9130 if self.iam_state.role_tab == RoleTab::Permissions {
9132 let filtered = filtered_iam_policies(self);
9133 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
9134 self.iam_state.current_policy = Some(policy.policy_name.clone());
9135 self.iam_state.policy_scroll = 0;
9136 self.view_mode = ViewMode::PolicyView;
9137 self.iam_state.policies.loading = true;
9138 self.update_current_tab_breadcrumb();
9139 }
9140 }
9141 } else if self.iam_state.current_role.is_none() {
9142 let filtered_roles = filtered_iam_roles(self);
9143 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
9144 self.iam_state.current_role = Some(role.role_name.clone());
9145 self.iam_state.role_tab = RoleTab::Permissions;
9146 self.iam_state.policies.reset();
9147 self.update_current_tab_breadcrumb();
9148 }
9149 }
9150 } else if self.current_service == Service::IamUserGroups {
9151 if self.iam_state.current_group.is_none() {
9152 let filtered_groups: Vec<_> = self
9153 .iam_state
9154 .groups
9155 .items
9156 .iter()
9157 .filter(|g| {
9158 if self.iam_state.groups.filter.is_empty() {
9159 true
9160 } else {
9161 g.group_name
9162 .to_lowercase()
9163 .contains(&self.iam_state.groups.filter.to_lowercase())
9164 }
9165 })
9166 .collect();
9167 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
9168 self.iam_state.current_group = Some(group.group_name.clone());
9169 self.update_current_tab_breadcrumb();
9170 }
9171 }
9172 } else if self.current_service == Service::LambdaFunctions {
9173 if self.lambda_state.current_function.is_some()
9174 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
9175 {
9176 if self.mode == Mode::Normal {
9179 let page_size = self.lambda_state.version_table.page_size.value();
9180 let filtered: Vec<_> = self
9181 .lambda_state
9182 .version_table
9183 .items
9184 .iter()
9185 .filter(|v| {
9186 self.lambda_state.version_table.filter.is_empty()
9187 || v.version.to_lowercase().contains(
9188 &self.lambda_state.version_table.filter.to_lowercase(),
9189 )
9190 || v.aliases.to_lowercase().contains(
9191 &self.lambda_state.version_table.filter.to_lowercase(),
9192 )
9193 })
9194 .collect();
9195 let current_page = self.lambda_state.version_table.selected / page_size;
9196 let start_idx = current_page * page_size;
9197 let end_idx = (start_idx + page_size).min(filtered.len());
9198 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
9199 let page_index = self.lambda_state.version_table.selected % page_size;
9200 if let Some(version) = paginated.get(page_index) {
9201 self.lambda_state.current_version = Some(version.version.clone());
9202 self.lambda_state.detail_tab = LambdaDetailTab::Code;
9203 }
9204 } else {
9205 if self.lambda_state.version_table.expanded_item
9207 == Some(self.lambda_state.version_table.selected)
9208 {
9209 self.lambda_state.version_table.collapse();
9210 } else {
9211 self.lambda_state.version_table.expanded_item =
9212 Some(self.lambda_state.version_table.selected);
9213 }
9214 }
9215 } else if self.lambda_state.current_function.is_some()
9216 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
9217 {
9218 let filtered: Vec<_> = self
9220 .lambda_state
9221 .alias_table
9222 .items
9223 .iter()
9224 .filter(|a| {
9225 self.lambda_state.alias_table.filter.is_empty()
9226 || a.name
9227 .to_lowercase()
9228 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
9229 || a.versions
9230 .to_lowercase()
9231 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
9232 })
9233 .collect();
9234 if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
9235 self.lambda_state.current_alias = Some(alias.name.clone());
9236 }
9237 } else if self.lambda_state.current_function.is_none() {
9238 let filtered_functions = filtered_lambda_functions(self);
9239 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
9240 self.lambda_state.current_function = Some(func.name.clone());
9241 self.lambda_state.detail_tab = LambdaDetailTab::Code;
9242 self.update_current_tab_breadcrumb();
9243 }
9244 }
9245 } else if self.current_service == Service::LambdaApplications {
9246 let filtered = filtered_lambda_applications(self);
9247 if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
9248 let app_name = app.name.clone();
9249 self.lambda_application_state.current_application = Some(app_name.clone());
9250 self.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Overview;
9251
9252 use crate::lambda::Resource;
9254 self.lambda_application_state.resources.items = vec![
9255 Resource {
9256 logical_id: "ApiGatewayRestApi".to_string(),
9257 physical_id: "abc123xyz".to_string(),
9258 resource_type: "AWS::ApiGateway::RestApi".to_string(),
9259 last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
9260 },
9261 Resource {
9262 logical_id: "LambdaFunction".to_string(),
9263 physical_id: format!("{}-function", app_name),
9264 resource_type: "AWS::Lambda::Function".to_string(),
9265 last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
9266 },
9267 Resource {
9268 logical_id: "DynamoDBTable".to_string(),
9269 physical_id: format!("{}-table", app_name),
9270 resource_type: "AWS::DynamoDB::Table".to_string(),
9271 last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
9272 },
9273 ];
9274
9275 use crate::lambda::Deployment;
9277 self.lambda_application_state.deployments.items = vec![
9278 Deployment {
9279 deployment_id: "d-ABC123XYZ".to_string(),
9280 resource_type: "AWS::Serverless::Application".to_string(),
9281 last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
9282 status: "Succeeded".to_string(),
9283 },
9284 Deployment {
9285 deployment_id: "d-DEF456UVW".to_string(),
9286 resource_type: "AWS::Serverless::Application".to_string(),
9287 last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
9288 status: "Succeeded".to_string(),
9289 },
9290 ];
9291
9292 self.update_current_tab_breadcrumb();
9293 }
9294 } else if self.current_service == Service::CloudWatchLogGroups {
9295 if self.view_mode == ViewMode::List {
9296 let filtered_groups = filtered_log_groups(self);
9298 if let Some(selected_group) =
9299 filtered_groups.get(self.log_groups_state.log_groups.selected)
9300 {
9301 if let Some(actual_idx) = self
9302 .log_groups_state
9303 .log_groups
9304 .items
9305 .iter()
9306 .position(|g| g.name == selected_group.name)
9307 {
9308 self.log_groups_state.log_groups.selected = actual_idx;
9309 }
9310 }
9311 self.view_mode = ViewMode::Detail;
9312 self.log_groups_state.log_streams.clear();
9313 self.log_groups_state.tags.items.clear();
9314 self.log_groups_state.tags.reset();
9315 self.log_groups_state.selected_stream = 0;
9316 self.log_groups_state.loading = true;
9317 self.column_selector_index = 0;
9318 self.update_current_tab_breadcrumb();
9319 } else if self.view_mode == ViewMode::Detail {
9320 let filtered_streams = filtered_log_streams(self);
9322 if let Some(selected_stream) =
9323 filtered_streams.get(self.log_groups_state.selected_stream)
9324 {
9325 if let Some(actual_idx) = self
9326 .log_groups_state
9327 .log_streams
9328 .iter()
9329 .position(|s| s.name == selected_stream.name)
9330 {
9331 self.log_groups_state.selected_stream = actual_idx;
9332 }
9333 }
9334 self.view_mode = ViewMode::Events;
9335 self.update_current_tab_breadcrumb();
9336 self.log_groups_state.log_events.clear();
9337 self.log_groups_state.event_scroll_offset = 0;
9338 self.log_groups_state.next_backward_token = None;
9339 self.log_groups_state.loading = true;
9340 } else if self.view_mode == ViewMode::Events {
9341 if self.log_groups_state.expanded_event
9343 == Some(self.log_groups_state.event_scroll_offset)
9344 {
9345 self.log_groups_state.expanded_event = None;
9346 } else {
9347 self.log_groups_state.expanded_event =
9348 Some(self.log_groups_state.event_scroll_offset);
9349 }
9350 }
9351 } else if self.current_service == Service::CloudWatchAlarms {
9352 self.alarms_state.table.toggle_expand();
9354 } else if self.current_service == Service::CloudWatchInsights {
9355 if !self.insights_state.insights.selected_log_groups.is_empty() {
9357 self.log_groups_state.loading = true;
9358 self.insights_state.insights.query_completed = true;
9359 }
9360 }
9361 }
9362 }
9363
9364 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
9365 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
9366 Ok(())
9367 }
9368
9369 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
9370 let alarms = self.alarms_client.list_alarms().await?;
9371 self.alarms_state.table.items = alarms
9372 .into_iter()
9373 .map(
9374 |(
9375 name,
9376 state,
9377 state_updated,
9378 description,
9379 metric_name,
9380 namespace,
9381 statistic,
9382 period,
9383 comparison,
9384 threshold,
9385 actions_enabled,
9386 state_reason,
9387 resource,
9388 dimensions,
9389 expression,
9390 alarm_type,
9391 cross_account,
9392 )| Alarm {
9393 name,
9394 state,
9395 state_updated_timestamp: state_updated,
9396 description,
9397 metric_name,
9398 namespace,
9399 statistic,
9400 period,
9401 comparison_operator: comparison,
9402 threshold,
9403 actions_enabled,
9404 state_reason,
9405 resource,
9406 dimensions,
9407 expression,
9408 alarm_type,
9409 cross_account,
9410 },
9411 )
9412 .collect();
9413 Ok(())
9414 }
9415
9416 pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
9417 let (events, next_token) = self.cloudtrail_client.lookup_events(None, None).await?;
9418 self.cloudtrail_state.table.items = events
9419 .into_iter()
9420 .map(
9421 |(
9422 event_name,
9423 event_time,
9424 username,
9425 event_source,
9426 resource_type,
9427 resource_name,
9428 read_only,
9429 aws_region,
9430 event_id,
9431 access_key_id,
9432 source_ip_address,
9433 error_code,
9434 request_id,
9435 event_type,
9436 cloud_trail_event_json,
9437 )| CloudTrailEvent {
9438 event_name,
9439 event_time,
9440 username,
9441 event_source,
9442 resource_type,
9443 resource_name,
9444 read_only,
9445 aws_region,
9446 event_id,
9447 access_key_id,
9448 source_ip_address,
9449 error_code,
9450 request_id,
9451 event_type,
9452 cloud_trail_event_json,
9453 },
9454 )
9455 .collect();
9456 self.cloudtrail_state.table.next_token = next_token;
9457 Ok(())
9458 }
9459
9460 pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
9461 if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
9462 let (events, next_token) = self
9464 .cloudtrail_client
9465 .lookup_events(None, Some(token))
9466 .await?;
9467 self.cloudtrail_state
9468 .table
9469 .items
9470 .extend(events.into_iter().map(
9471 |(
9472 event_name,
9473 event_time,
9474 username,
9475 event_source,
9476 resource_type,
9477 resource_name,
9478 read_only,
9479 aws_region,
9480 event_id,
9481 access_key_id,
9482 source_ip_address,
9483 error_code,
9484 request_id,
9485 event_type,
9486 cloud_trail_event_json,
9487 )| CloudTrailEvent {
9488 event_name,
9489 event_time,
9490 username,
9491 event_source,
9492 resource_type,
9493 resource_name,
9494 read_only,
9495 aws_region,
9496 event_id,
9497 access_key_id,
9498 source_ip_address,
9499 error_code,
9500 request_id,
9501 event_type,
9502 cloud_trail_event_json,
9503 },
9504 ));
9505 self.cloudtrail_state.table.next_token = next_token;
9506 }
9507 Ok(())
9508 }
9509
9510 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
9511 if let Some(bucket_name) = &self.s3_state.current_bucket {
9512 let bucket_region = if let Some(bucket) = self
9514 .s3_state
9515 .buckets
9516 .items
9517 .iter_mut()
9518 .find(|b| &b.name == bucket_name)
9519 {
9520 if bucket.region.is_empty() {
9521 let region = self.s3_client.get_bucket_location(bucket_name).await?;
9523 bucket.region = region.clone();
9524 region
9525 } else {
9526 bucket.region.clone()
9527 }
9528 } else {
9529 self.config.region.clone()
9530 };
9531
9532 let prefix = self
9533 .s3_state
9534 .prefix_stack
9535 .last()
9536 .cloned()
9537 .unwrap_or_default();
9538 let objects = self
9539 .s3_client
9540 .list_objects(bucket_name, &bucket_region, &prefix)
9541 .await?;
9542 self.s3_state.objects = objects
9543 .into_iter()
9544 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
9545 key,
9546 size,
9547 last_modified: modified,
9548 is_prefix,
9549 storage_class,
9550 })
9551 .collect();
9552 self.s3_state.selected_object = 0;
9553 }
9554 Ok(())
9555 }
9556
9557 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
9558 let bucket_region = self
9559 .s3_state
9560 .buckets
9561 .items
9562 .iter()
9563 .find(|b| b.name == bucket_name)
9564 .and_then(|b| {
9565 if b.region.is_empty() {
9566 None
9567 } else {
9568 Some(b.region.as_str())
9569 }
9570 })
9571 .unwrap_or(self.config.region.as_str());
9572 let objects = self
9573 .s3_client
9574 .list_objects(&bucket_name, bucket_region, "")
9575 .await?;
9576 let preview: Vec<S3Object> = objects
9577 .into_iter()
9578 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
9579 key,
9580 size,
9581 last_modified: modified,
9582 is_prefix,
9583 storage_class,
9584 })
9585 .collect();
9586 self.s3_state.bucket_preview.insert(bucket_name, preview);
9587 Ok(())
9588 }
9589
9590 pub async fn load_prefix_preview(
9591 &mut self,
9592 bucket_name: String,
9593 prefix: String,
9594 ) -> anyhow::Result<()> {
9595 let bucket_region = self
9596 .s3_state
9597 .buckets
9598 .items
9599 .iter()
9600 .find(|b| b.name == bucket_name)
9601 .and_then(|b| {
9602 if b.region.is_empty() {
9603 None
9604 } else {
9605 Some(b.region.as_str())
9606 }
9607 })
9608 .unwrap_or(self.config.region.as_str());
9609 let objects = self
9610 .s3_client
9611 .list_objects(&bucket_name, bucket_region, &prefix)
9612 .await?;
9613 let preview: Vec<S3Object> = objects
9614 .into_iter()
9615 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
9616 key,
9617 size,
9618 last_modified: modified,
9619 is_prefix,
9620 storage_class,
9621 })
9622 .collect();
9623 self.s3_state.prefix_preview.insert(prefix, preview);
9624 Ok(())
9625 }
9626
9627 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
9628 let repos = match self.ecr_state.tab {
9629 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
9630 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
9631 };
9632
9633 self.ecr_state.repositories.items = repos
9634 .into_iter()
9635 .map(|r| EcrRepository {
9636 name: r.name,
9637 uri: r.uri,
9638 created_at: r.created_at,
9639 tag_immutability: r.tag_immutability,
9640 encryption_type: r.encryption_type,
9641 })
9642 .collect();
9643
9644 self.ecr_state
9645 .repositories
9646 .items
9647 .sort_by(|a, b| a.name.cmp(&b.name));
9648 Ok(())
9649 }
9650
9651 pub async fn load_apis(&mut self) -> anyhow::Result<()> {
9652 let apis = self.apig_client.list_rest_apis().await?;
9653
9654 self.apig_state.apis.items = apis
9655 .into_iter()
9656 .map(|a| crate::apig::api::RestApi {
9657 id: a.id,
9658 name: a.name,
9659 description: a.description,
9660 created_date: a.created_date,
9661 api_key_source: a.api_key_source,
9662 endpoint_configuration: a.endpoint_configuration,
9663 protocol_type: a.protocol_type,
9664 disable_execute_api_endpoint: a.disable_execute_api_endpoint,
9665 status: a.status,
9666 })
9667 .collect();
9668
9669 self.apig_state
9670 .apis
9671 .items
9672 .sort_by(|a, b| a.name.cmp(&b.name));
9673 Ok(())
9674 }
9675
9676 pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
9677 let instances = self.ec2_client.list_instances().await?;
9678
9679 self.ec2_state.table.items = instances
9680 .into_iter()
9681 .map(|i| Ec2Instance {
9682 instance_id: i.instance_id,
9683 name: i.name,
9684 state: i.state,
9685 instance_type: i.instance_type,
9686 availability_zone: i.availability_zone,
9687 public_ipv4_dns: i.public_ipv4_dns,
9688 public_ipv4_address: i.public_ipv4_address,
9689 elastic_ip: i.elastic_ip,
9690 ipv6_ips: i.ipv6_ips,
9691 monitoring: i.monitoring,
9692 security_groups: i.security_groups,
9693 key_name: i.key_name,
9694 launch_time: i.launch_time,
9695 platform_details: i.platform_details,
9696 status_checks: i.status_checks,
9697 alarm_status: i.alarm_status,
9698 private_dns_name: String::new(),
9699 private_ip_address: String::new(),
9700 security_group_ids: String::new(),
9701 owner_id: String::new(),
9702 volume_id: String::new(),
9703 root_device_name: String::new(),
9704 root_device_type: String::new(),
9705 ebs_optimized: String::new(),
9706 image_id: String::new(),
9707 kernel_id: String::new(),
9708 ramdisk_id: String::new(),
9709 ami_launch_index: String::new(),
9710 reservation_id: String::new(),
9711 vpc_id: String::new(),
9712 subnet_ids: String::new(),
9713 instance_lifecycle: String::new(),
9714 architecture: String::new(),
9715 virtualization_type: String::new(),
9716 platform: String::new(),
9717 iam_instance_profile_arn: String::new(),
9718 tenancy: String::new(),
9719 affinity: String::new(),
9720 host_id: String::new(),
9721 placement_group: String::new(),
9722 partition_number: String::new(),
9723 capacity_reservation_id: String::new(),
9724 state_transition_reason_code: String::new(),
9725 state_transition_reason_message: String::new(),
9726 stop_hibernation_behavior: String::new(),
9727 outpost_arn: String::new(),
9728 product_codes: String::new(),
9729 availability_zone_id: String::new(),
9730 imdsv2: String::new(),
9731 usage_operation: String::new(),
9732 managed: String::new(),
9733 operator: String::new(),
9734 })
9735 .collect();
9736
9737 self.ec2_state
9739 .table
9740 .items
9741 .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
9742 Ok(())
9743 }
9744
9745 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
9746 if let Some(repo_name) = &self.ecr_state.current_repository {
9747 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
9748 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
9749
9750 self.ecr_state.images.items = images
9751 .into_iter()
9752 .map(|i| EcrImage {
9753 tag: i.tag,
9754 artifact_type: i.artifact_type,
9755 pushed_at: i.pushed_at,
9756 size_bytes: i.size_bytes,
9757 uri: i.uri,
9758 digest: i.digest,
9759 last_pull_time: i.last_pull_time,
9760 })
9761 .collect();
9762
9763 self.ecr_state
9764 .images
9765 .items
9766 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
9767 }
9768 }
9769 Ok(())
9770 }
9771
9772 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
9773 let stacks = self
9774 .cloudformation_client
9775 .list_stacks(self.cfn_state.view_nested)
9776 .await?;
9777
9778 let mut stacks: Vec<CfnStack> = stacks
9779 .into_iter()
9780 .map(|s| CfnStack {
9781 name: s.name,
9782 stack_id: s.stack_id,
9783 status: s.status,
9784 created_time: s.created_time,
9785 updated_time: s.updated_time,
9786 deleted_time: s.deleted_time,
9787 drift_status: s.drift_status,
9788 last_drift_check_time: s.last_drift_check_time,
9789 status_reason: s.status_reason,
9790 description: s.description,
9791 detailed_status: String::new(),
9792 root_stack: String::new(),
9793 parent_stack: String::new(),
9794 termination_protection: false,
9795 iam_role: String::new(),
9796 tags: Vec::new(),
9797 stack_policy: String::new(),
9798 rollback_monitoring_time: String::new(),
9799 rollback_alarms: Vec::new(),
9800 notification_arns: Vec::new(),
9801 })
9802 .collect();
9803
9804 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
9806
9807 self.cfn_state.table.items = stacks;
9808
9809 Ok(())
9810 }
9811
9812 pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
9813 let template = self.cloudformation_client.get_template(stack_name).await?;
9814 self.cfn_state.template_body = template;
9815 self.cfn_state.template_scroll = 0;
9816 Ok(())
9817 }
9818
9819 pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
9820 let mut parameters = self
9821 .cloudformation_client
9822 .get_stack_parameters(stack_name)
9823 .await?;
9824 parameters.sort_by(|a, b| a.key.cmp(&b.key));
9825 self.cfn_state.parameters.items = parameters;
9826 self.cfn_state.parameters.reset();
9827 Ok(())
9828 }
9829
9830 pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
9831 let outputs = self
9832 .cloudformation_client
9833 .get_stack_outputs(stack_name)
9834 .await?;
9835 self.cfn_state.outputs.items = outputs;
9836 self.cfn_state.outputs.reset();
9837 Ok(())
9838 }
9839
9840 pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
9841 let resources = self
9842 .cloudformation_client
9843 .get_stack_resources(stack_name)
9844 .await?;
9845 self.cfn_state.resources.items = resources;
9846 self.cfn_state.resources.reset();
9847 Ok(())
9848 }
9849
9850 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
9851 let attached_policies = self
9853 .iam_client
9854 .list_attached_role_policies(role_name)
9855 .await
9856 .map_err(|e| anyhow::anyhow!(e))?;
9857
9858 let mut policies: Vec<IamPolicy> = attached_policies
9859 .into_iter()
9860 .map(|p| IamPolicy {
9861 policy_name: p.policy_name().unwrap_or("").to_string(),
9862 policy_type: "Managed".to_string(),
9863 attached_via: "Direct".to_string(),
9864 attached_entities: "-".to_string(),
9865 description: "-".to_string(),
9866 creation_time: "-".to_string(),
9867 edited_time: "-".to_string(),
9868 policy_arn: p.policy_arn().map(|s| s.to_string()),
9869 })
9870 .collect();
9871
9872 let inline_policy_names = self
9874 .iam_client
9875 .list_role_policies(role_name)
9876 .await
9877 .map_err(|e| anyhow::anyhow!(e))?;
9878
9879 for policy_name in inline_policy_names {
9880 policies.push(IamPolicy {
9881 policy_name,
9882 policy_type: "Inline".to_string(),
9883 attached_via: "Direct".to_string(),
9884 attached_entities: "-".to_string(),
9885 description: "-".to_string(),
9886 creation_time: "-".to_string(),
9887 edited_time: "-".to_string(),
9888 policy_arn: None,
9889 });
9890 }
9891
9892 self.iam_state.policies.items = policies;
9893
9894 Ok(())
9895 }
9896
9897 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
9898 let attached_policies = self
9899 .iam_client
9900 .list_attached_group_policies(group_name)
9901 .await
9902 .map_err(|e| anyhow::anyhow!(e))?;
9903
9904 let mut policies: Vec<IamPolicy> = attached_policies
9905 .into_iter()
9906 .map(|p| IamPolicy {
9907 policy_name: p.policy_name().unwrap_or("").to_string(),
9908 policy_type: "AWS managed".to_string(),
9909 attached_via: "Direct".to_string(),
9910 attached_entities: "-".to_string(),
9911 description: "-".to_string(),
9912 creation_time: "-".to_string(),
9913 edited_time: "-".to_string(),
9914 policy_arn: p.policy_arn().map(|s| s.to_string()),
9915 })
9916 .collect();
9917
9918 let inline_policy_names = self
9919 .iam_client
9920 .list_group_policies(group_name)
9921 .await
9922 .map_err(|e| anyhow::anyhow!(e))?;
9923
9924 for policy_name in inline_policy_names {
9925 policies.push(IamPolicy {
9926 policy_name,
9927 policy_type: "Inline".to_string(),
9928 attached_via: "Direct".to_string(),
9929 attached_entities: "-".to_string(),
9930 description: "-".to_string(),
9931 creation_time: "-".to_string(),
9932 edited_time: "-".to_string(),
9933 policy_arn: None,
9934 });
9935 }
9936
9937 self.iam_state.policies.items = policies;
9938
9939 Ok(())
9940 }
9941
9942 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
9943 let users = self
9944 .iam_client
9945 .get_group_users(group_name)
9946 .await
9947 .map_err(|e| anyhow::anyhow!(e))?;
9948
9949 let group_users: Vec<IamGroupUser> = users
9950 .into_iter()
9951 .map(|u| {
9952 let creation_time = {
9953 let dt = u.create_date();
9954 let timestamp = dt.secs();
9955 let datetime =
9956 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
9957 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
9958 };
9959
9960 IamGroupUser {
9961 user_name: u.user_name().to_string(),
9962 groups: String::new(),
9963 last_activity: String::new(),
9964 creation_time,
9965 }
9966 })
9967 .collect();
9968
9969 self.iam_state.group_users.items = group_users;
9970
9971 Ok(())
9972 }
9973
9974 pub async fn load_policy_document(
9975 &mut self,
9976 role_name: &str,
9977 policy_name: &str,
9978 ) -> anyhow::Result<()> {
9979 let policy = self
9981 .iam_state
9982 .policies
9983 .items
9984 .iter()
9985 .find(|p| p.policy_name == policy_name)
9986 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
9987
9988 let document = if let Some(policy_arn) = &policy.policy_arn {
9989 self.iam_client
9991 .get_policy_version(policy_arn)
9992 .await
9993 .map_err(|e| anyhow::anyhow!(e))?
9994 } else {
9995 self.iam_client
9997 .get_role_policy(role_name, policy_name)
9998 .await
9999 .map_err(|e| anyhow::anyhow!(e))?
10000 };
10001
10002 self.iam_state.policy_document = document;
10003
10004 Ok(())
10005 }
10006
10007 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
10008 let document = self
10009 .iam_client
10010 .get_role(role_name)
10011 .await
10012 .map_err(|e| anyhow::anyhow!(e))?;
10013
10014 self.iam_state.trust_policy_document = document;
10015
10016 Ok(())
10017 }
10018
10019 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
10020 self.iam_state.last_accessed_services.items = vec![];
10022 self.iam_state.last_accessed_services.selected = 0;
10023
10024 Ok(())
10025 }
10026
10027 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
10028 let tags = self
10029 .iam_client
10030 .list_role_tags(role_name)
10031 .await
10032 .map_err(|e| anyhow::anyhow!(e))?;
10033 self.iam_state.tags.items = tags
10034 .into_iter()
10035 .map(|(k, v)| IamRoleTag { key: k, value: v })
10036 .collect();
10037 self.iam_state.tags.reset();
10038 Ok(())
10039 }
10040
10041 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
10042 let tags = self
10043 .iam_client
10044 .list_user_tags(user_name)
10045 .await
10046 .map_err(|e| anyhow::anyhow!(e))?;
10047 self.iam_state.user_tags.items = tags
10048 .into_iter()
10049 .map(|(k, v)| IamUserTag { key: k, value: v })
10050 .collect();
10051 self.iam_state.user_tags.reset();
10052 Ok(())
10053 }
10054
10055 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
10056 if let Some(group) = self
10057 .log_groups_state
10058 .log_groups
10059 .items
10060 .get(self.log_groups_state.log_groups.selected)
10061 {
10062 self.log_groups_state.log_streams =
10063 self.cloudwatch_client.list_log_streams(&group.name).await?;
10064 self.log_groups_state.selected_stream = 0;
10065 }
10066 Ok(())
10067 }
10068
10069 pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
10070 if let Some(group) = self
10071 .log_groups_state
10072 .log_groups
10073 .items
10074 .get(self.log_groups_state.log_groups.selected)
10075 {
10076 let arn = if let Some(arn) = &group.log_group_arn {
10078 arn.clone()
10079 } else if let Some(arn) = &group.arn {
10080 arn.clone()
10081 } else {
10082 let account_id = if self.config.account_id.is_empty() {
10084 "*"
10085 } else {
10086 &self.config.account_id
10087 };
10088 format!(
10089 "arn:aws:logs:{}:{}:log-group:{}",
10090 self.config.region, account_id, group.name
10091 )
10092 };
10093
10094 let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
10095 self.log_groups_state.tags.items = tags;
10096 self.log_groups_state.tags.selected = 0;
10097 self.log_groups_state.tags.scroll_offset = 0;
10098 }
10099 Ok(())
10100 }
10101
10102 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
10103 if let Some(group) = self
10104 .log_groups_state
10105 .log_groups
10106 .items
10107 .get(self.log_groups_state.log_groups.selected)
10108 {
10109 if let Some(stream) = self
10110 .log_groups_state
10111 .log_streams
10112 .get(self.log_groups_state.selected_stream)
10113 {
10114 let (start_time, end_time) =
10116 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
10117 let now = chrono::Utc::now().timestamp_millis();
10118 let duration_ms = match self.log_groups_state.relative_unit {
10119 TimeUnit::Minutes => amount * 60 * 1000,
10120 TimeUnit::Hours => amount * 60 * 60 * 1000,
10121 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
10122 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
10123 };
10124 (Some(now - duration_ms), Some(now))
10125 } else {
10126 (None, None)
10127 };
10128
10129 let (mut events, has_more, token) = self
10130 .cloudwatch_client
10131 .get_log_events(
10132 &group.name,
10133 &stream.name,
10134 self.log_groups_state.next_backward_token.clone(),
10135 start_time,
10136 end_time,
10137 )
10138 .await?;
10139
10140 if self.log_groups_state.next_backward_token.is_some() {
10141 events.append(&mut self.log_groups_state.log_events);
10143 self.log_groups_state.event_scroll_offset = 0;
10144 } else {
10145 self.log_groups_state.event_scroll_offset = 0;
10147 }
10148
10149 self.log_groups_state.log_events = events;
10150 self.log_groups_state.has_older_events =
10151 has_more && self.log_groups_state.log_events.len() >= 25;
10152 self.log_groups_state.next_backward_token = token;
10153 self.log_groups_state.selected_event = 0;
10154 }
10155 }
10156 Ok(())
10157 }
10158
10159 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
10160 if self.insights_state.insights.selected_log_groups.is_empty() {
10161 return Err(anyhow::anyhow!(
10162 "No log groups selected. Please select at least one log group."
10163 ));
10164 }
10165
10166 let now = chrono::Utc::now().timestamp_millis();
10167 let amount = self
10168 .insights_state
10169 .insights
10170 .insights_relative_amount
10171 .parse::<i64>()
10172 .unwrap_or(1);
10173 let duration_ms = match self.insights_state.insights.insights_relative_unit {
10174 TimeUnit::Minutes => amount * 60 * 1000,
10175 TimeUnit::Hours => amount * 60 * 60 * 1000,
10176 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
10177 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
10178 };
10179 let start_time = now - duration_ms;
10180
10181 let query_id = self
10182 .cloudwatch_client
10183 .start_query(
10184 self.insights_state.insights.selected_log_groups.clone(),
10185 self.insights_state.insights.query_text.trim().to_string(),
10186 start_time,
10187 now,
10188 )
10189 .await?;
10190
10191 for _ in 0..60 {
10193 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
10194 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
10195
10196 if status == "Complete" {
10197 self.insights_state.insights.query_results = results;
10198 self.insights_state.insights.query_completed = true;
10199 self.insights_state.insights.results_selected = 0;
10200 self.insights_state.insights.expanded_result = None;
10201 self.view_mode = ViewMode::InsightsResults;
10202 return Ok(());
10203 } else if status == "Failed" || status == "Cancelled" {
10204 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
10205 }
10206 }
10207
10208 Err(anyhow::anyhow!("Query timeout"))
10209 }
10210}
10211
10212impl CloudWatchInsightsState {
10213 fn new() -> Self {
10214 Self {
10215 insights: InsightsState::default(),
10216 loading: false,
10217 }
10218 }
10219}
10220
10221impl CloudWatchAlarmsState {
10222 fn new() -> Self {
10223 Self {
10224 table: TableState::new(),
10225 alarm_tab: AlarmTab::AllAlarms,
10226 view_as: AlarmViewMode::Table,
10227 wrap_lines: false,
10228 sort_column: "Last state update".to_string(),
10229 sort_direction: SortDirection::Asc,
10230 input_focus: InputFocus::Filter,
10231 }
10232 }
10233}
10234
10235impl ServicePickerState {
10236 fn new() -> Self {
10237 Self {
10238 filter: String::new(),
10239 filter_active: false,
10240 selected: 0,
10241 services: vec![
10242 "API Gateway › APIs",
10243 "CloudWatch › Log Groups",
10244 "CloudWatch › Logs Insights",
10245 "CloudWatch › Alarms",
10246 "CloudTrail › Event History",
10247 "CloudFormation › Stacks",
10248 "EC2 › Instances",
10249 "ECR › Repositories",
10250 "IAM › Users",
10251 "IAM › Roles",
10252 "IAM › User Groups",
10253 "Lambda › Functions",
10254 "Lambda › Applications",
10255 "S3 › Buckets",
10256 "SQS › Queues",
10257 ],
10258 }
10259 }
10260}
10261
10262#[cfg(test)]
10263mod test_helpers {
10264 use super::*;
10265
10266 pub fn test_app() -> App {
10268 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
10269 }
10270
10271 pub fn test_app_no_region() -> App {
10272 App::new_without_client("test".to_string(), None)
10273 }
10274}
10275
10276#[cfg(test)]
10277mod tests {
10278 use super::*;
10279 use crate::keymap::Action;
10280 use test_helpers::*;
10281
10282 #[test]
10283 fn test_next_tab_cycles_forward() {
10284 let mut app = test_app();
10285 app.tabs = vec![
10286 Tab {
10287 service: Service::CloudWatchLogGroups,
10288 title: "CloudWatch › Log Groups".to_string(),
10289 breadcrumb: "CloudWatch › Log Groups".to_string(),
10290 },
10291 Tab {
10292 service: Service::CloudWatchInsights,
10293 title: "CloudWatch › Logs Insights".to_string(),
10294 breadcrumb: "CloudWatch › Logs Insights".to_string(),
10295 },
10296 Tab {
10297 service: Service::CloudWatchAlarms,
10298 title: "CloudWatch › Alarms".to_string(),
10299 breadcrumb: "CloudWatch › Alarms".to_string(),
10300 },
10301 ];
10302 app.current_tab = 0;
10303
10304 app.handle_action(Action::NextTab);
10305 assert_eq!(app.current_tab, 1);
10306 assert_eq!(app.current_service, Service::CloudWatchInsights);
10307
10308 app.handle_action(Action::NextTab);
10309 assert_eq!(app.current_tab, 2);
10310 assert_eq!(app.current_service, Service::CloudWatchAlarms);
10311
10312 app.handle_action(Action::NextTab);
10314 assert_eq!(app.current_tab, 0);
10315 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
10316 }
10317
10318 #[test]
10319 fn test_prev_tab_cycles_backward() {
10320 let mut app = test_app();
10321 app.tabs = vec![
10322 Tab {
10323 service: Service::CloudWatchLogGroups,
10324 title: "CloudWatch › Log Groups".to_string(),
10325 breadcrumb: "CloudWatch › Log Groups".to_string(),
10326 },
10327 Tab {
10328 service: Service::CloudWatchInsights,
10329 title: "CloudWatch › Logs Insights".to_string(),
10330 breadcrumb: "CloudWatch › Logs Insights".to_string(),
10331 },
10332 Tab {
10333 service: Service::CloudWatchAlarms,
10334 title: "CloudWatch › Alarms".to_string(),
10335 breadcrumb: "CloudWatch › Alarms".to_string(),
10336 },
10337 ];
10338 app.current_tab = 2;
10339
10340 app.handle_action(Action::PrevTab);
10341 assert_eq!(app.current_tab, 1);
10342 assert_eq!(app.current_service, Service::CloudWatchInsights);
10343
10344 app.handle_action(Action::PrevTab);
10345 assert_eq!(app.current_tab, 0);
10346 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
10347
10348 app.handle_action(Action::PrevTab);
10350 assert_eq!(app.current_tab, 2);
10351 assert_eq!(app.current_service, Service::CloudWatchAlarms);
10352 }
10353
10354 #[test]
10355 fn test_close_tab_removes_current() {
10356 let mut app = test_app();
10357 app.tabs = vec![
10358 Tab {
10359 service: Service::CloudWatchLogGroups,
10360 title: "CloudWatch › Log Groups".to_string(),
10361 breadcrumb: "CloudWatch › Log Groups".to_string(),
10362 },
10363 Tab {
10364 service: Service::CloudWatchInsights,
10365 title: "CloudWatch › Logs Insights".to_string(),
10366 breadcrumb: "CloudWatch › Logs Insights".to_string(),
10367 },
10368 Tab {
10369 service: Service::CloudWatchAlarms,
10370 title: "CloudWatch › Alarms".to_string(),
10371 breadcrumb: "CloudWatch › Alarms".to_string(),
10372 },
10373 ];
10374 app.current_tab = 1;
10375 app.service_selected = true;
10376
10377 app.handle_action(Action::CloseTab);
10378 assert_eq!(app.tabs.len(), 2);
10379 assert_eq!(app.current_tab, 1);
10380 assert_eq!(app.current_service, Service::CloudWatchAlarms);
10381 }
10382
10383 #[test]
10384 fn test_close_last_tab_exits_service() {
10385 let mut app = test_app();
10386 app.tabs = vec![Tab {
10387 service: Service::CloudWatchLogGroups,
10388 title: "CloudWatch › Log Groups".to_string(),
10389 breadcrumb: "CloudWatch › Log Groups".to_string(),
10390 }];
10391 app.current_tab = 0;
10392 app.service_selected = true;
10393
10394 app.handle_action(Action::CloseTab);
10395 assert_eq!(app.tabs.len(), 0);
10396 assert!(!app.service_selected);
10397 assert_eq!(app.current_tab, 0);
10398 }
10399
10400 #[test]
10401 fn test_close_service_removes_current_tab() {
10402 let mut app = test_app();
10403 app.tabs = vec![
10404 Tab {
10405 service: Service::CloudWatchLogGroups,
10406 title: "CloudWatch › Log Groups".to_string(),
10407 breadcrumb: "CloudWatch › Log Groups".to_string(),
10408 },
10409 Tab {
10410 service: Service::CloudWatchInsights,
10411 title: "CloudWatch › Logs Insights".to_string(),
10412 breadcrumb: "CloudWatch › Logs Insights".to_string(),
10413 },
10414 Tab {
10415 service: Service::CloudWatchAlarms,
10416 title: "CloudWatch › Alarms".to_string(),
10417 breadcrumb: "CloudWatch › Alarms".to_string(),
10418 },
10419 ];
10420 app.current_tab = 1;
10421 app.service_selected = true;
10422
10423 app.handle_action(Action::CloseService);
10424
10425 assert_eq!(app.tabs.len(), 2);
10427 assert_eq!(app.current_tab, 1);
10429 assert_eq!(app.current_service, Service::CloudWatchAlarms);
10430 assert!(app.service_selected);
10432 assert_eq!(app.mode, Mode::Normal);
10433 }
10434
10435 #[test]
10436 fn test_close_service_last_tab_shows_picker() {
10437 let mut app = test_app();
10438 app.tabs = vec![Tab {
10439 service: Service::CloudWatchLogGroups,
10440 title: "CloudWatch › Log Groups".to_string(),
10441 breadcrumb: "CloudWatch › Log Groups".to_string(),
10442 }];
10443 app.current_tab = 0;
10444 app.service_selected = true;
10445
10446 app.handle_action(Action::CloseService);
10447
10448 assert_eq!(app.tabs.len(), 0);
10450 assert!(!app.service_selected);
10452 assert_eq!(app.mode, Mode::ServicePicker);
10453 }
10454
10455 #[test]
10456 fn test_open_tab_picker_with_tabs() {
10457 let mut app = test_app();
10458 app.tabs = vec![
10459 Tab {
10460 service: Service::CloudWatchLogGroups,
10461 title: "CloudWatch › Log Groups".to_string(),
10462 breadcrumb: "CloudWatch › Log Groups".to_string(),
10463 },
10464 Tab {
10465 service: Service::CloudWatchInsights,
10466 title: "CloudWatch › Logs Insights".to_string(),
10467 breadcrumb: "CloudWatch › Logs Insights".to_string(),
10468 },
10469 ];
10470 app.current_tab = 1;
10471
10472 app.handle_action(Action::OpenTabPicker);
10473 assert_eq!(app.mode, Mode::TabPicker);
10474 assert_eq!(app.tab_picker_selected, 1);
10475 }
10476
10477 #[test]
10478 fn test_open_tab_picker_without_tabs() {
10479 let mut app = test_app();
10480 app.tabs = vec![];
10481
10482 app.handle_action(Action::OpenTabPicker);
10483 assert_eq!(app.mode, Mode::Normal);
10484 }
10485
10486 #[test]
10487 fn test_pending_key_state() {
10488 let mut app = test_app();
10489 assert_eq!(app.pending_key, None);
10490
10491 app.pending_key = Some('g');
10492 assert_eq!(app.pending_key, Some('g'));
10493 }
10494
10495 #[test]
10496 fn test_tab_breadcrumb_updates() {
10497 let mut app = test_app();
10498 app.tabs = vec![Tab {
10499 service: Service::CloudWatchLogGroups,
10500 title: "CloudWatch › Log Groups".to_string(),
10501 breadcrumb: "CloudWatch > Log groups".to_string(),
10502 }];
10503 app.current_tab = 0;
10504 app.service_selected = true;
10505 app.current_service = Service::CloudWatchLogGroups;
10506
10507 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
10509
10510 app.log_groups_state
10512 .log_groups
10513 .items
10514 .push(rusticity_core::LogGroup {
10515 name: "/aws/lambda/test".to_string(),
10516 creation_time: None,
10517 stored_bytes: Some(1024),
10518 retention_days: None,
10519 log_class: None,
10520 arn: None,
10521 log_group_arn: None,
10522 deletion_protection_enabled: None,
10523 });
10524 app.log_groups_state.log_groups.reset();
10525 app.view_mode = ViewMode::Detail;
10526 app.update_current_tab_breadcrumb();
10527
10528 assert_eq!(
10530 app.tabs[0].breadcrumb,
10531 "CloudWatch > Log groups > /aws/lambda/test"
10532 );
10533 }
10534
10535 #[test]
10536 fn test_s3_bucket_column_selector_navigation() {
10537 let mut app = test_app();
10538 app.current_service = Service::S3Buckets;
10539 app.mode = Mode::ColumnSelector;
10540 app.column_selector_index = 0;
10541
10542 app.handle_action(Action::NextItem);
10544 assert_eq!(app.column_selector_index, 1);
10545
10546 app.handle_action(Action::NextItem);
10547 assert_eq!(app.column_selector_index, 2);
10548
10549 app.handle_action(Action::NextItem);
10550 assert_eq!(app.column_selector_index, 3);
10551
10552 for _ in 0..10 {
10554 app.handle_action(Action::NextItem);
10555 }
10556 assert_eq!(app.column_selector_index, 9);
10557
10558 app.handle_action(Action::PrevItem);
10560 assert_eq!(app.column_selector_index, 8);
10561
10562 app.handle_action(Action::PrevItem);
10563 assert_eq!(app.column_selector_index, 7);
10564
10565 for _ in 0..10 {
10567 app.handle_action(Action::PrevItem);
10568 }
10569 assert_eq!(app.column_selector_index, 0);
10570 }
10571
10572 #[test]
10573 fn test_cloudwatch_alarms_state_initialized() {
10574 let app = test_app();
10575
10576 assert_eq!(app.alarms_state.table.items.len(), 0);
10578 assert_eq!(app.alarms_state.table.selected, 0);
10579 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
10580 assert!(!app.alarms_state.table.loading);
10581 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
10582 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
10583 }
10584
10585 #[test]
10586 fn test_cloudwatch_alarms_service_selection() {
10587 let mut app = test_app();
10588
10589 app.current_service = Service::CloudWatchAlarms;
10591 app.service_selected = true;
10592
10593 assert_eq!(app.current_service, Service::CloudWatchAlarms);
10594 assert!(app.service_selected);
10595 }
10596
10597 #[test]
10598 fn test_cloudwatch_alarms_column_preferences() {
10599 let app = test_app();
10600
10601 assert!(!app.cw_alarm_column_ids.is_empty());
10603 assert!(!app.cw_alarm_visible_column_ids.is_empty());
10604
10605 assert!(app
10607 .cw_alarm_visible_column_ids
10608 .contains(&AlarmColumn::Name.id()));
10609 assert!(app
10610 .cw_alarm_visible_column_ids
10611 .contains(&AlarmColumn::State.id()));
10612 }
10613
10614 #[test]
10615 fn test_s3_bucket_navigation_without_expansion() {
10616 let mut app = test_app();
10617 app.current_service = Service::S3Buckets;
10618 app.service_selected = true;
10619 app.mode = Mode::Normal;
10620
10621 app.s3_state.buckets.items = vec![
10623 S3Bucket {
10624 name: "bucket1".to_string(),
10625 region: "us-east-1".to_string(),
10626 creation_date: "2024-01-01T00:00:00Z".to_string(),
10627 },
10628 S3Bucket {
10629 name: "bucket2".to_string(),
10630 region: "us-east-1".to_string(),
10631 creation_date: "2024-01-02T00:00:00Z".to_string(),
10632 },
10633 S3Bucket {
10634 name: "bucket3".to_string(),
10635 region: "us-east-1".to_string(),
10636 creation_date: "2024-01-03T00:00:00Z".to_string(),
10637 },
10638 ];
10639 app.s3_state.selected_row = 0;
10640
10641 app.handle_action(Action::NextItem);
10643 assert_eq!(app.s3_state.selected_row, 1);
10644
10645 app.handle_action(Action::NextItem);
10646 assert_eq!(app.s3_state.selected_row, 2);
10647
10648 app.handle_action(Action::NextItem);
10650 assert_eq!(app.s3_state.selected_row, 2);
10651
10652 app.handle_action(Action::PrevItem);
10654 assert_eq!(app.s3_state.selected_row, 1);
10655
10656 app.handle_action(Action::PrevItem);
10657 assert_eq!(app.s3_state.selected_row, 0);
10658
10659 app.handle_action(Action::PrevItem);
10661 assert_eq!(app.s3_state.selected_row, 0);
10662 }
10663
10664 #[test]
10665 fn test_s3_bucket_navigation_with_expansion() {
10666 let mut app = test_app();
10667 app.current_service = Service::S3Buckets;
10668 app.service_selected = true;
10669 app.mode = Mode::Normal;
10670
10671 app.s3_state.buckets.items = vec![
10673 S3Bucket {
10674 name: "bucket1".to_string(),
10675 region: "us-east-1".to_string(),
10676 creation_date: "2024-01-01T00:00:00Z".to_string(),
10677 },
10678 S3Bucket {
10679 name: "bucket2".to_string(),
10680 region: "us-east-1".to_string(),
10681 creation_date: "2024-01-02T00:00:00Z".to_string(),
10682 },
10683 ];
10684
10685 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
10687 app.s3_state.bucket_preview.insert(
10688 "bucket1".to_string(),
10689 vec![
10690 S3Object {
10691 key: "file1.txt".to_string(),
10692 size: 100,
10693 last_modified: "2024-01-01T00:00:00Z".to_string(),
10694 is_prefix: false,
10695 storage_class: "STANDARD".to_string(),
10696 },
10697 S3Object {
10698 key: "folder/".to_string(),
10699 size: 0,
10700 last_modified: "2024-01-01T00:00:00Z".to_string(),
10701 is_prefix: true,
10702 storage_class: String::new(),
10703 },
10704 ],
10705 );
10706
10707 app.s3_state.selected_row = 0;
10708
10709 app.handle_action(Action::NextItem);
10712 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
10715 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
10718 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
10722 assert_eq!(app.s3_state.selected_row, 3);
10723 }
10724
10725 #[test]
10726 fn test_s3_bucket_navigation_with_nested_expansion() {
10727 let mut app = test_app();
10728 app.current_service = Service::S3Buckets;
10729 app.service_selected = true;
10730 app.mode = Mode::Normal;
10731
10732 app.s3_state.buckets.items = vec![S3Bucket {
10734 name: "bucket1".to_string(),
10735 region: "us-east-1".to_string(),
10736 creation_date: "2024-01-01T00:00:00Z".to_string(),
10737 }];
10738
10739 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
10741 app.s3_state.bucket_preview.insert(
10742 "bucket1".to_string(),
10743 vec![S3Object {
10744 key: "folder/".to_string(),
10745 size: 0,
10746 last_modified: "2024-01-01T00:00:00Z".to_string(),
10747 is_prefix: true,
10748 storage_class: String::new(),
10749 }],
10750 );
10751
10752 app.s3_state.expanded_prefixes.insert("folder/".to_string());
10754 app.s3_state.prefix_preview.insert(
10755 "folder/".to_string(),
10756 vec![
10757 S3Object {
10758 key: "folder/file1.txt".to_string(),
10759 size: 100,
10760 last_modified: "2024-01-01T00:00:00Z".to_string(),
10761 is_prefix: false,
10762 storage_class: "STANDARD".to_string(),
10763 },
10764 S3Object {
10765 key: "folder/file2.txt".to_string(),
10766 size: 200,
10767 last_modified: "2024-01-01T00:00:00Z".to_string(),
10768 is_prefix: false,
10769 storage_class: "STANDARD".to_string(),
10770 },
10771 ],
10772 );
10773
10774 app.s3_state.selected_row = 0;
10775
10776 app.handle_action(Action::NextItem);
10778 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
10781 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
10784 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
10788 assert_eq!(app.s3_state.selected_row, 3);
10789 }
10790
10791 #[test]
10792 fn test_calculate_total_bucket_rows() {
10793 let mut app = test_app();
10794
10795 assert_eq!(app.calculate_total_bucket_rows(), 0);
10797
10798 app.s3_state.buckets.items = vec![
10800 S3Bucket {
10801 name: "bucket1".to_string(),
10802 region: "us-east-1".to_string(),
10803 creation_date: "2024-01-01T00:00:00Z".to_string(),
10804 },
10805 S3Bucket {
10806 name: "bucket2".to_string(),
10807 region: "us-east-1".to_string(),
10808 creation_date: "2024-01-02T00:00:00Z".to_string(),
10809 },
10810 ];
10811 assert_eq!(app.calculate_total_bucket_rows(), 2);
10812
10813 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
10815 app.s3_state.bucket_preview.insert(
10816 "bucket1".to_string(),
10817 vec![
10818 S3Object {
10819 key: "file1.txt".to_string(),
10820 size: 100,
10821 last_modified: "2024-01-01T00:00:00Z".to_string(),
10822 is_prefix: false,
10823 storage_class: "STANDARD".to_string(),
10824 },
10825 S3Object {
10826 key: "file2.txt".to_string(),
10827 size: 200,
10828 last_modified: "2024-01-01T00:00:00Z".to_string(),
10829 is_prefix: false,
10830 storage_class: "STANDARD".to_string(),
10831 },
10832 S3Object {
10833 key: "folder/".to_string(),
10834 size: 0,
10835 last_modified: "2024-01-01T00:00:00Z".to_string(),
10836 is_prefix: true,
10837 storage_class: String::new(),
10838 },
10839 ],
10840 );
10841 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
10845 app.s3_state.prefix_preview.insert(
10846 "folder/".to_string(),
10847 vec![
10848 S3Object {
10849 key: "folder/nested1.txt".to_string(),
10850 size: 50,
10851 last_modified: "2024-01-01T00:00:00Z".to_string(),
10852 is_prefix: false,
10853 storage_class: "STANDARD".to_string(),
10854 },
10855 S3Object {
10856 key: "folder/nested2.txt".to_string(),
10857 size: 75,
10858 last_modified: "2024-01-01T00:00:00Z".to_string(),
10859 is_prefix: false,
10860 storage_class: "STANDARD".to_string(),
10861 },
10862 ],
10863 );
10864 assert_eq!(app.calculate_total_bucket_rows(), 7); }
10866
10867 #[test]
10868 fn test_calculate_total_object_rows() {
10869 let mut app = test_app();
10870 app.s3_state.current_bucket = Some("test-bucket".to_string());
10871
10872 assert_eq!(app.calculate_total_object_rows(), 0);
10874
10875 app.s3_state.objects = vec![
10877 S3Object {
10878 key: "file1.txt".to_string(),
10879 size: 100,
10880 last_modified: "2024-01-01T00:00:00Z".to_string(),
10881 is_prefix: false,
10882 storage_class: "STANDARD".to_string(),
10883 },
10884 S3Object {
10885 key: "folder/".to_string(),
10886 size: 0,
10887 last_modified: "2024-01-01T00:00:00Z".to_string(),
10888 is_prefix: true,
10889 storage_class: String::new(),
10890 },
10891 ];
10892 assert_eq!(app.calculate_total_object_rows(), 2);
10893
10894 app.s3_state.expanded_prefixes.insert("folder/".to_string());
10896 app.s3_state.prefix_preview.insert(
10897 "folder/".to_string(),
10898 vec![
10899 S3Object {
10900 key: "folder/file2.txt".to_string(),
10901 size: 200,
10902 last_modified: "2024-01-01T00:00:00Z".to_string(),
10903 is_prefix: false,
10904 storage_class: "STANDARD".to_string(),
10905 },
10906 S3Object {
10907 key: "folder/subfolder/".to_string(),
10908 size: 0,
10909 last_modified: "2024-01-01T00:00:00Z".to_string(),
10910 is_prefix: true,
10911 storage_class: String::new(),
10912 },
10913 ],
10914 );
10915 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
10919 .expanded_prefixes
10920 .insert("folder/subfolder/".to_string());
10921 app.s3_state.prefix_preview.insert(
10922 "folder/subfolder/".to_string(),
10923 vec![S3Object {
10924 key: "folder/subfolder/deep.txt".to_string(),
10925 size: 50,
10926 last_modified: "2024-01-01T00:00:00Z".to_string(),
10927 is_prefix: false,
10928 storage_class: "STANDARD".to_string(),
10929 }],
10930 );
10931 assert_eq!(app.calculate_total_object_rows(), 5); }
10933
10934 #[test]
10935 fn test_s3_object_navigation_with_deep_nesting() {
10936 let mut app = test_app();
10937 app.current_service = Service::S3Buckets;
10938 app.service_selected = true;
10939 app.mode = Mode::Normal;
10940 app.s3_state.current_bucket = Some("test-bucket".to_string());
10941
10942 app.s3_state.objects = vec![S3Object {
10944 key: "folder1/".to_string(),
10945 size: 0,
10946 last_modified: "2024-01-01T00:00:00Z".to_string(),
10947 is_prefix: true,
10948 storage_class: String::new(),
10949 }];
10950
10951 app.s3_state
10953 .expanded_prefixes
10954 .insert("folder1/".to_string());
10955 app.s3_state.prefix_preview.insert(
10956 "folder1/".to_string(),
10957 vec![S3Object {
10958 key: "folder1/folder2/".to_string(),
10959 size: 0,
10960 last_modified: "2024-01-01T00:00:00Z".to_string(),
10961 is_prefix: true,
10962 storage_class: String::new(),
10963 }],
10964 );
10965
10966 app.s3_state
10968 .expanded_prefixes
10969 .insert("folder1/folder2/".to_string());
10970 app.s3_state.prefix_preview.insert(
10971 "folder1/folder2/".to_string(),
10972 vec![S3Object {
10973 key: "folder1/folder2/file.txt".to_string(),
10974 size: 100,
10975 last_modified: "2024-01-01T00:00:00Z".to_string(),
10976 is_prefix: false,
10977 storage_class: "STANDARD".to_string(),
10978 }],
10979 );
10980
10981 app.s3_state.selected_object = 0;
10982
10983 app.handle_action(Action::NextItem);
10985 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
10988 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
10992 assert_eq!(app.s3_state.selected_object, 2);
10993 }
10994
10995 #[test]
10996 fn test_s3_expand_nested_folder_in_objects_view() {
10997 let mut app = test_app();
10998 app.current_service = Service::S3Buckets;
10999 app.service_selected = true;
11000 app.mode = Mode::Normal;
11001 app.s3_state.current_bucket = Some("test-bucket".to_string());
11002
11003 app.s3_state.objects = vec![S3Object {
11005 key: "parent/".to_string(),
11006 size: 0,
11007 last_modified: "2024-01-01T00:00:00Z".to_string(),
11008 is_prefix: true,
11009 storage_class: String::new(),
11010 }];
11011
11012 app.s3_state.expanded_prefixes.insert("parent/".to_string());
11014 app.s3_state.prefix_preview.insert(
11015 "parent/".to_string(),
11016 vec![S3Object {
11017 key: "parent/child/".to_string(),
11018 size: 0,
11019 last_modified: "2024-01-01T00:00:00Z".to_string(),
11020 is_prefix: true,
11021 storage_class: String::new(),
11022 }],
11023 );
11024
11025 app.s3_state.selected_object = 1;
11027
11028 app.handle_action(Action::NextPane);
11030
11031 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
11033 assert!(app.s3_state.buckets.loading); }
11035
11036 #[test]
11037 fn test_s3_drill_into_nested_folder() {
11038 let mut app = test_app();
11039 app.current_service = Service::S3Buckets;
11040 app.service_selected = true;
11041 app.mode = Mode::Normal;
11042 app.s3_state.current_bucket = Some("test-bucket".to_string());
11043
11044 app.s3_state.objects = vec![S3Object {
11046 key: "parent/".to_string(),
11047 size: 0,
11048 last_modified: "2024-01-01T00:00:00Z".to_string(),
11049 is_prefix: true,
11050 storage_class: String::new(),
11051 }];
11052
11053 app.s3_state.expanded_prefixes.insert("parent/".to_string());
11055 app.s3_state.prefix_preview.insert(
11056 "parent/".to_string(),
11057 vec![S3Object {
11058 key: "parent/child/".to_string(),
11059 size: 0,
11060 last_modified: "2024-01-01T00:00:00Z".to_string(),
11061 is_prefix: true,
11062 storage_class: String::new(),
11063 }],
11064 );
11065
11066 app.s3_state.selected_object = 1;
11068
11069 app.handle_action(Action::Select);
11071
11072 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
11074 assert!(app.s3_state.buckets.loading); }
11076
11077 #[test]
11078 fn test_s3_esc_pops_navigation_stack() {
11079 let mut app = test_app();
11080 app.current_service = Service::S3Buckets;
11081 app.s3_state.current_bucket = Some("test-bucket".to_string());
11082 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
11083
11084 app.handle_action(Action::GoBack);
11086 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
11087 assert!(app.s3_state.buckets.loading);
11088
11089 app.s3_state.buckets.loading = false;
11091 app.handle_action(Action::GoBack);
11092 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
11093 assert!(app.s3_state.buckets.loading);
11094
11095 app.s3_state.buckets.loading = false;
11097 app.handle_action(Action::GoBack);
11098 assert_eq!(app.s3_state.current_bucket, None);
11099 }
11100
11101 #[test]
11102 fn test_s3_esc_from_bucket_root_exits() {
11103 let mut app = test_app();
11104 app.current_service = Service::S3Buckets;
11105 app.s3_state.current_bucket = Some("test-bucket".to_string());
11106 app.s3_state.prefix_stack = vec![];
11107
11108 app.handle_action(Action::GoBack);
11110 assert_eq!(app.s3_state.current_bucket, None);
11111 assert_eq!(app.s3_state.objects.len(), 0);
11112 }
11113
11114 #[test]
11115 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
11116 let mut app = test_app();
11117 app.current_service = Service::S3Buckets;
11118 app.service_selected = true;
11119 app.mode = Mode::Normal;
11120
11121 app.s3_state.buckets.items = vec![S3Bucket {
11123 name: "test-bucket".to_string(),
11124 region: "us-east-1".to_string(),
11125 creation_date: "2024-01-01".to_string(),
11126 }];
11127
11128 app.s3_state
11130 .expanded_prefixes
11131 .insert("test-bucket".to_string());
11132 app.s3_state.bucket_preview.insert(
11133 "test-bucket".to_string(),
11134 vec![S3Object {
11135 key: "parent/".to_string(),
11136 size: 0,
11137 last_modified: "2024-01-01".to_string(),
11138 is_prefix: true,
11139 storage_class: String::new(),
11140 }],
11141 );
11142
11143 app.s3_state.expanded_prefixes.insert("parent/".to_string());
11145 app.s3_state.prefix_preview.insert(
11146 "parent/".to_string(),
11147 vec![S3Object {
11148 key: "parent/child/".to_string(),
11149 size: 0,
11150 last_modified: "2024-01-01".to_string(),
11151 is_prefix: true,
11152 storage_class: String::new(),
11153 }],
11154 );
11155
11156 app.s3_state.selected_row = 2;
11158
11159 app.handle_action(Action::Select);
11161
11162 assert_eq!(
11164 app.s3_state.prefix_stack,
11165 vec!["parent/".to_string(), "parent/child/".to_string()]
11166 );
11167 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
11168 assert!(app.s3_state.buckets.loading);
11169
11170 app.s3_state.buckets.loading = false;
11172 app.handle_action(Action::GoBack);
11173 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
11174 assert!(app.s3_state.buckets.loading);
11175
11176 app.s3_state.buckets.loading = false;
11178 app.handle_action(Action::GoBack);
11179 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
11180 assert!(app.s3_state.buckets.loading);
11181
11182 app.s3_state.buckets.loading = false;
11184 app.handle_action(Action::GoBack);
11185 assert_eq!(app.s3_state.current_bucket, None);
11186 }
11187
11188 #[test]
11189 fn test_region_picker_fuzzy_filter() {
11190 let mut app = test_app();
11191 app.region_latencies.insert("us-east-1".to_string(), 10);
11192 app.region_filter = "vir".to_string();
11193 let filtered = app.get_filtered_regions();
11194 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
11195 }
11196
11197 #[test]
11198 fn test_profile_picker_loads_profiles() {
11199 let profiles = App::load_aws_profiles();
11200 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
11202 }
11203
11204 #[test]
11205 fn test_profile_with_region_uses_it() {
11206 let mut app = test_app_no_region();
11207 app.available_profiles = vec![AwsProfile {
11208 name: "test-profile".to_string(),
11209 region: Some("eu-west-1".to_string()),
11210 account: Some("123456789".to_string()),
11211 role_arn: None,
11212 source_profile: None,
11213 }];
11214 app.profile_picker_selected = 0;
11215 app.mode = Mode::ProfilePicker;
11216
11217 let filtered = app.get_filtered_profiles();
11219 if let Some(profile) = filtered.first() {
11220 let profile_name = profile.name.clone();
11221 let profile_region = profile.region.clone();
11222
11223 app.profile = profile_name;
11224 if let Some(region) = profile_region {
11225 app.region = region;
11226 }
11227 }
11228
11229 assert_eq!(app.profile, "test-profile");
11230 assert_eq!(app.region, "eu-west-1");
11231 }
11232
11233 #[test]
11234 fn test_profile_without_region_keeps_unknown() {
11235 let mut app = test_app_no_region();
11236 let initial_region = app.region.clone();
11237
11238 app.available_profiles = vec![AwsProfile {
11239 name: "test-profile".to_string(),
11240 region: None,
11241 account: None,
11242 role_arn: None,
11243 source_profile: None,
11244 }];
11245 app.profile_picker_selected = 0;
11246 app.mode = Mode::ProfilePicker;
11247
11248 let filtered = app.get_filtered_profiles();
11249 if let Some(profile) = filtered.first() {
11250 let profile_name = profile.name.clone();
11251 let profile_region = profile.region.clone();
11252
11253 app.profile = profile_name;
11254 if let Some(region) = profile_region {
11255 app.region = region;
11256 }
11257 }
11258
11259 assert_eq!(app.profile, "test-profile");
11260 assert_eq!(app.region, initial_region); }
11262
11263 #[test]
11264 fn test_region_selection_closes_all_tabs() {
11265 let mut app = test_app();
11266
11267 app.tabs.push(Tab {
11269 service: Service::CloudWatchLogGroups,
11270 title: "CloudWatch".to_string(),
11271 breadcrumb: "CloudWatch".to_string(),
11272 });
11273 app.tabs.push(Tab {
11274 service: Service::S3Buckets,
11275 title: "S3".to_string(),
11276 breadcrumb: "S3".to_string(),
11277 });
11278 app.service_selected = true;
11279 app.current_tab = 1;
11280
11281 app.region_latencies.insert("eu-west-1".to_string(), 50);
11283
11284 app.mode = Mode::RegionPicker;
11286 app.region_picker_selected = 0;
11287
11288 let filtered = app.get_filtered_regions();
11289 if let Some(region) = filtered.first() {
11290 app.region = region.code.to_string();
11291 app.tabs.clear();
11292 app.current_tab = 0;
11293 app.service_selected = false;
11294 app.mode = Mode::Normal;
11295 }
11296
11297 assert_eq!(app.tabs.len(), 0);
11298 assert_eq!(app.current_tab, 0);
11299 assert!(!app.service_selected);
11300 assert_eq!(app.region, "eu-west-1");
11301 }
11302
11303 #[test]
11304 fn test_region_picker_can_be_closed_without_selection() {
11305 let mut app = test_app();
11306 let initial_region = app.region.clone();
11307
11308 app.mode = Mode::RegionPicker;
11309
11310 app.mode = Mode::Normal;
11312
11313 assert_eq!(app.region, initial_region);
11315 }
11316
11317 #[test]
11318 fn test_session_filter_works() {
11319 let mut app = test_app();
11320
11321 app.sessions = vec![
11322 Session {
11323 id: "1".to_string(),
11324 timestamp: "2024-01-01".to_string(),
11325 profile: "prod-profile".to_string(),
11326 region: "us-east-1".to_string(),
11327 account_id: "123456789".to_string(),
11328 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
11329 tabs: vec![],
11330 },
11331 Session {
11332 id: "2".to_string(),
11333 timestamp: "2024-01-02".to_string(),
11334 profile: "dev-profile".to_string(),
11335 region: "eu-west-1".to_string(),
11336 account_id: "987654321".to_string(),
11337 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
11338 tabs: vec![],
11339 },
11340 ];
11341
11342 app.session_filter = "prod".to_string();
11344 let filtered = app.get_filtered_sessions();
11345 assert_eq!(filtered.len(), 1);
11346 assert_eq!(filtered[0].profile, "prod-profile");
11347
11348 app.session_filter = "eu".to_string();
11350 let filtered = app.get_filtered_sessions();
11351 assert_eq!(filtered.len(), 1);
11352 assert_eq!(filtered[0].region, "eu-west-1");
11353
11354 app.session_filter.clear();
11356 let filtered = app.get_filtered_sessions();
11357 assert_eq!(filtered.len(), 2);
11358 }
11359
11360 #[test]
11361 fn test_profile_picker_shows_account() {
11362 let mut app = test_app_no_region();
11363 app.available_profiles = vec![AwsProfile {
11364 name: "test-profile".to_string(),
11365 region: Some("us-east-1".to_string()),
11366 account: Some("123456789".to_string()),
11367 role_arn: None,
11368 source_profile: None,
11369 }];
11370
11371 let filtered = app.get_filtered_profiles();
11372 assert_eq!(filtered.len(), 1);
11373 assert_eq!(filtered[0].account, Some("123456789".to_string()));
11374 }
11375
11376 #[test]
11377 fn test_profile_without_account() {
11378 let mut app = test_app_no_region();
11379 app.available_profiles = vec![AwsProfile {
11380 name: "test-profile".to_string(),
11381 region: Some("us-east-1".to_string()),
11382 account: None,
11383 role_arn: None,
11384 source_profile: None,
11385 }];
11386
11387 let filtered = app.get_filtered_profiles();
11388 assert_eq!(filtered.len(), 1);
11389 assert_eq!(filtered[0].account, None);
11390 }
11391
11392 #[test]
11393 fn test_profile_with_all_fields() {
11394 let mut app = test_app_no_region();
11395 app.available_profiles = vec![AwsProfile {
11396 name: "prod-profile".to_string(),
11397 region: Some("us-west-2".to_string()),
11398 account: Some("123456789".to_string()),
11399 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
11400 source_profile: Some("base-profile".to_string()),
11401 }];
11402
11403 let filtered = app.get_filtered_profiles();
11404 assert_eq!(filtered.len(), 1);
11405 assert_eq!(filtered[0].name, "prod-profile");
11406 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
11407 assert_eq!(filtered[0].account, Some("123456789".to_string()));
11408 assert_eq!(
11409 filtered[0].role_arn,
11410 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
11411 );
11412 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
11413 }
11414
11415 #[test]
11416 fn test_profile_filter_by_source_profile() {
11417 let mut app = test_app_no_region();
11418 app.available_profiles = vec![
11419 AwsProfile {
11420 name: "profile1".to_string(),
11421 region: None,
11422 account: None,
11423 role_arn: None,
11424 source_profile: Some("base".to_string()),
11425 },
11426 AwsProfile {
11427 name: "profile2".to_string(),
11428 region: None,
11429 account: None,
11430 role_arn: None,
11431 source_profile: Some("other".to_string()),
11432 },
11433 ];
11434
11435 app.profile_filter = "base".to_string();
11436 let filtered = app.get_filtered_profiles();
11437 assert_eq!(filtered.len(), 1);
11438 assert_eq!(filtered[0].name, "profile1");
11439 }
11440
11441 #[test]
11442 fn test_profile_filter_by_role() {
11443 let mut app = test_app_no_region();
11444 app.available_profiles = vec![
11445 AwsProfile {
11446 name: "admin-profile".to_string(),
11447 region: None,
11448 account: None,
11449 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
11450 source_profile: None,
11451 },
11452 AwsProfile {
11453 name: "dev-profile".to_string(),
11454 region: None,
11455 account: None,
11456 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
11457 source_profile: None,
11458 },
11459 ];
11460
11461 app.profile_filter = "Admin".to_string();
11462 let filtered = app.get_filtered_profiles();
11463 assert_eq!(filtered.len(), 1);
11464 assert_eq!(filtered[0].name, "admin-profile");
11465 }
11466
11467 #[test]
11468 fn test_profiles_sorted_by_name() {
11469 let mut app = test_app_no_region();
11470 app.available_profiles = vec![
11471 AwsProfile {
11472 name: "zebra-profile".to_string(),
11473 region: None,
11474 account: None,
11475 role_arn: None,
11476 source_profile: None,
11477 },
11478 AwsProfile {
11479 name: "alpha-profile".to_string(),
11480 region: None,
11481 account: None,
11482 role_arn: None,
11483 source_profile: None,
11484 },
11485 AwsProfile {
11486 name: "beta-profile".to_string(),
11487 region: None,
11488 account: None,
11489 role_arn: None,
11490 source_profile: None,
11491 },
11492 ];
11493
11494 let filtered = app.get_filtered_profiles();
11495 assert_eq!(filtered.len(), 3);
11496 assert_eq!(filtered[0].name, "alpha-profile");
11497 assert_eq!(filtered[1].name, "beta-profile");
11498 assert_eq!(filtered[2].name, "zebra-profile");
11499 }
11500
11501 #[test]
11502 fn test_profile_with_role_arn() {
11503 let mut app = test_app_no_region();
11504 app.available_profiles = vec![AwsProfile {
11505 name: "role-profile".to_string(),
11506 region: Some("us-east-1".to_string()),
11507 account: Some("123456789".to_string()),
11508 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
11509 source_profile: None,
11510 }];
11511
11512 let filtered = app.get_filtered_profiles();
11513 assert_eq!(filtered.len(), 1);
11514 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
11515 }
11516
11517 #[test]
11518 fn test_profile_with_user_arn() {
11519 let mut app = test_app_no_region();
11520 app.available_profiles = vec![AwsProfile {
11521 name: "user-profile".to_string(),
11522 region: Some("us-east-1".to_string()),
11523 account: Some("123456789".to_string()),
11524 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
11525 source_profile: None,
11526 }];
11527
11528 let filtered = app.get_filtered_profiles();
11529 assert_eq!(filtered.len(), 1);
11530 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
11531 }
11532
11533 #[test]
11534 fn test_filtered_profiles_also_sorted() {
11535 let mut app = test_app_no_region();
11536 app.available_profiles = vec![
11537 AwsProfile {
11538 name: "prod-zebra".to_string(),
11539 region: Some("us-east-1".to_string()),
11540 account: None,
11541 role_arn: None,
11542 source_profile: None,
11543 },
11544 AwsProfile {
11545 name: "prod-alpha".to_string(),
11546 region: Some("us-east-1".to_string()),
11547 account: None,
11548 role_arn: None,
11549 source_profile: None,
11550 },
11551 AwsProfile {
11552 name: "dev-profile".to_string(),
11553 region: Some("us-west-2".to_string()),
11554 account: None,
11555 role_arn: None,
11556 source_profile: None,
11557 },
11558 ];
11559
11560 app.profile_filter = "prod".to_string();
11561 let filtered = app.get_filtered_profiles();
11562 assert_eq!(filtered.len(), 2);
11563 assert_eq!(filtered[0].name, "prod-alpha");
11564 assert_eq!(filtered[1].name, "prod-zebra");
11565 }
11566
11567 #[test]
11568 fn test_profile_picker_has_all_columns() {
11569 let mut app = test_app_no_region();
11570 app.available_profiles = vec![AwsProfile {
11571 name: "test".to_string(),
11572 region: Some("us-east-1".to_string()),
11573 account: Some("123456789".to_string()),
11574 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
11575 source_profile: Some("base".to_string()),
11576 }];
11577
11578 let filtered = app.get_filtered_profiles();
11579 assert_eq!(filtered.len(), 1);
11580 assert!(filtered[0].name == "test");
11581 assert!(filtered[0].region.is_some());
11582 assert!(filtered[0].account.is_some());
11583 assert!(filtered[0].role_arn.is_some());
11584 assert!(filtered[0].source_profile.is_some());
11585 }
11586
11587 #[test]
11588 fn test_session_picker_shows_tab_count() {
11589 let mut app = test_app_no_region();
11590 app.sessions = vec![Session {
11591 id: "1".to_string(),
11592 timestamp: "2024-01-01".to_string(),
11593 profile: "test".to_string(),
11594 region: "us-east-1".to_string(),
11595 account_id: "123".to_string(),
11596 role_arn: String::new(),
11597 tabs: vec![
11598 SessionTab {
11599 service: "CloudWatch".to_string(),
11600 title: "Logs".to_string(),
11601 breadcrumb: String::new(),
11602 filter: None,
11603 selected_item: None,
11604 },
11605 SessionTab {
11606 service: "S3".to_string(),
11607 title: "Buckets".to_string(),
11608 breadcrumb: String::new(),
11609 filter: None,
11610 selected_item: None,
11611 },
11612 ],
11613 }];
11614
11615 let filtered = app.get_filtered_sessions();
11616 assert_eq!(filtered.len(), 1);
11617 assert_eq!(filtered[0].tabs.len(), 2);
11618 }
11619
11620 #[test]
11621 fn test_start_background_data_fetch_loads_profiles() {
11622 let mut app = test_app_no_region();
11623 assert!(app.available_profiles.is_empty());
11624
11625 app.available_profiles = App::load_aws_profiles();
11627
11628 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
11630 }
11631
11632 #[test]
11633 fn test_refresh_in_profile_picker() {
11634 let mut app = test_app_no_region();
11635 app.mode = Mode::ProfilePicker;
11636 app.available_profiles = vec![AwsProfile {
11637 name: "test".to_string(),
11638 region: None,
11639 account: None,
11640 role_arn: None,
11641 source_profile: None,
11642 }];
11643
11644 app.handle_action(Action::Refresh);
11645
11646 assert!(app.log_groups_state.loading);
11648 assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
11649 }
11650
11651 #[test]
11652 fn test_refresh_sets_loading_for_profile_picker() {
11653 let mut app = test_app_no_region();
11654 app.mode = Mode::ProfilePicker;
11655
11656 assert!(!app.log_groups_state.loading);
11657
11658 app.handle_action(Action::Refresh);
11659
11660 assert!(app.log_groups_state.loading);
11661 }
11662
11663 #[test]
11664 fn test_profiles_loaded_on_demand() {
11665 let mut app = test_app_no_region();
11666
11667 assert!(app.available_profiles.is_empty());
11669
11670 app.available_profiles = App::load_aws_profiles();
11672
11673 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
11675 }
11676
11677 #[test]
11678 fn test_profile_accounts_not_fetched_automatically() {
11679 let mut app = test_app_no_region();
11680 app.available_profiles = App::load_aws_profiles();
11681
11682 for profile in &app.available_profiles {
11684 assert!(profile.account.is_none() || profile.account.is_some());
11687 }
11688 }
11689
11690 #[test]
11691 fn test_ctrl_r_triggers_account_fetch() {
11692 let mut app = test_app_no_region();
11693 app.mode = Mode::ProfilePicker;
11694 app.available_profiles = vec![AwsProfile {
11695 name: "test".to_string(),
11696 region: Some("us-east-1".to_string()),
11697 account: None,
11698 role_arn: None,
11699 source_profile: None,
11700 }];
11701
11702 assert!(app.available_profiles[0].account.is_none());
11704
11705 app.handle_action(Action::Refresh);
11707
11708 assert!(app.log_groups_state.loading);
11710 }
11711
11712 #[test]
11713 fn test_refresh_in_region_picker() {
11714 let mut app = test_app_no_region();
11715 app.mode = Mode::RegionPicker;
11716
11717 let initial_latencies = app.region_latencies.len();
11718 app.handle_action(Action::Refresh);
11719
11720 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
11722 }
11723
11724 #[test]
11725 fn test_refresh_in_session_picker() {
11726 let mut app = test_app_no_region();
11727 app.mode = Mode::SessionPicker;
11728 app.sessions = vec![];
11729
11730 app.handle_action(Action::Refresh);
11731
11732 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
11734 }
11735
11736 #[test]
11737 fn test_session_picker_selection() {
11738 let mut app = test_app();
11739
11740 app.sessions = vec![Session {
11741 id: "1".to_string(),
11742 timestamp: "2024-01-01".to_string(),
11743 profile: "prod-profile".to_string(),
11744 region: "us-west-2".to_string(),
11745 account_id: "123456789".to_string(),
11746 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
11747 tabs: vec![SessionTab {
11748 service: "CloudWatchLogGroups".to_string(),
11749 title: "Log Groups".to_string(),
11750 breadcrumb: "CloudWatch › Log Groups".to_string(),
11751 filter: Some("test".to_string()),
11752 selected_item: None,
11753 }],
11754 }];
11755
11756 app.mode = Mode::SessionPicker;
11757 app.session_picker_selected = 0;
11758
11759 app.handle_action(Action::Select);
11761
11762 assert_eq!(app.mode, Mode::Normal);
11763 assert_eq!(app.profile, "prod-profile");
11764 assert_eq!(app.region, "us-west-2");
11765 assert_eq!(app.config.account_id, "123456789");
11766 assert_eq!(app.tabs.len(), 1);
11767 assert_eq!(app.tabs[0].title, "Log Groups");
11768 }
11769
11770 #[test]
11771 fn test_save_session_creates_session() {
11772 let mut app =
11773 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11774 app.config.account_id = "123456789".to_string();
11775 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11776
11777 app.tabs.push(Tab {
11778 service: Service::CloudWatchLogGroups,
11779 title: "Log Groups".to_string(),
11780 breadcrumb: "CloudWatch › Log Groups".to_string(),
11781 });
11782
11783 app.save_current_session();
11784
11785 assert!(app.current_session.is_some());
11786 let session = app.current_session.clone().unwrap();
11787 assert_eq!(session.profile, "test-profile");
11788 assert_eq!(session.region, "us-east-1");
11789 assert_eq!(session.account_id, "123456789");
11790 assert_eq!(session.tabs.len(), 1);
11791
11792 let _ = session.delete();
11794 }
11795
11796 #[test]
11797 fn test_save_session_updates_existing() {
11798 let mut app =
11799 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11800 app.config.account_id = "123456789".to_string();
11801 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11802
11803 app.current_session = Some(Session {
11804 id: "existing".to_string(),
11805 timestamp: "2024-01-01".to_string(),
11806 profile: "test-profile".to_string(),
11807 region: "us-east-1".to_string(),
11808 account_id: "123456789".to_string(),
11809 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
11810 tabs: vec![],
11811 });
11812
11813 app.tabs.push(Tab {
11814 service: Service::CloudWatchLogGroups,
11815 title: "Log Groups".to_string(),
11816 breadcrumb: "CloudWatch › Log Groups".to_string(),
11817 });
11818
11819 app.save_current_session();
11820
11821 let session = app.current_session.clone().unwrap();
11822 assert_eq!(session.id, "existing");
11823 assert_eq!(session.tabs.len(), 1);
11824
11825 let _ = session.delete();
11827 }
11828
11829 #[test]
11830 fn test_save_session_skips_empty_tabs() {
11831 let mut app =
11832 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11833 app.config.account_id = "123456789".to_string();
11834
11835 app.save_current_session();
11836
11837 assert!(app.current_session.is_none());
11838 }
11839
11840 #[test]
11841 fn test_save_session_deletes_when_tabs_closed() {
11842 let mut app =
11843 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11844 app.config.account_id = "123456789".to_string();
11845 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11846
11847 app.current_session = Some(Session {
11849 id: "test_delete".to_string(),
11850 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
11851 profile: "test-profile".to_string(),
11852 region: "us-east-1".to_string(),
11853 account_id: "123456789".to_string(),
11854 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
11855 tabs: vec![],
11856 });
11857
11858 app.save_current_session();
11860
11861 assert!(app.current_session.is_none());
11862 }
11863
11864 #[test]
11865 fn test_closing_all_tabs_deletes_session() {
11866 let mut app =
11867 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11868 app.config.account_id = "123456789".to_string();
11869 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11870
11871 app.tabs.push(Tab {
11873 service: Service::CloudWatchLogGroups,
11874 title: "Log Groups".to_string(),
11875 breadcrumb: "CloudWatch › Log Groups".to_string(),
11876 });
11877
11878 app.save_current_session();
11880 assert!(app.current_session.is_some());
11881 let session_id = app.current_session.as_ref().unwrap().id.clone();
11882
11883 app.tabs.clear();
11885
11886 app.save_current_session();
11888 assert!(app.current_session.is_none());
11889
11890 let _ = Session::load(&session_id).map(|s| s.delete());
11892 }
11893
11894 #[test]
11895 fn test_credential_error_opens_profile_picker() {
11896 let mut app = App::new_without_client("default".to_string(), None);
11898 let error_str = "Unable to load credentials from any source";
11899
11900 if error_str.contains("credentials") {
11901 app.available_profiles = App::load_aws_profiles();
11902 app.mode = Mode::ProfilePicker;
11903 }
11904
11905 assert_eq!(app.mode, Mode::ProfilePicker);
11906 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
11908 }
11909
11910 #[test]
11911 fn test_non_credential_error_shows_error_modal() {
11912 let mut app = App::new_without_client("default".to_string(), None);
11913 let error_str = "Network timeout";
11914
11915 if !error_str.contains("credentials") {
11916 app.error_message = Some(error_str.to_string());
11917 app.mode = Mode::ErrorModal;
11918 }
11919
11920 assert_eq!(app.mode, Mode::ErrorModal);
11921 assert!(app.error_message.is_some());
11922 }
11923
11924 #[tokio::test]
11925 async fn test_profile_selection_loads_credentials() {
11926 std::env::set_var("AWS_PROFILE", "default");
11928
11929 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
11931
11932 if let Ok(app) = result {
11933 assert!(!app.config.account_id.is_empty());
11935 assert!(!app.config.role_arn.is_empty());
11936 assert_eq!(app.profile, "default");
11937 assert_eq!(app.config.region, "us-east-1");
11938 }
11939 }
11941
11942 #[test]
11943 fn test_new_app_shows_service_picker_with_no_tabs() {
11944 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
11945
11946 assert!(!app.service_selected);
11948 assert_eq!(app.mode, Mode::ServicePicker);
11950 assert!(app.tabs.is_empty());
11952 }
11953
11954 #[tokio::test]
11955 async fn test_aws_profile_env_var_read_before_config_load() {
11956 std::env::set_var("AWS_PROFILE", "test-profile");
11958
11959 let profile_name = None
11961 .or_else(|| std::env::var("AWS_PROFILE").ok())
11962 .unwrap_or_else(|| "default".to_string());
11963
11964 assert_eq!(profile_name, "test-profile");
11966
11967 std::env::set_var("AWS_PROFILE", &profile_name);
11969
11970 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
11972
11973 std::env::remove_var("AWS_PROFILE");
11974 }
11975
11976 #[test]
11977 fn test_next_preferences_cloudformation() {
11978 let mut app = test_app();
11979 app.current_service = Service::CloudFormationStacks;
11980 app.mode = Mode::ColumnSelector;
11981 app.column_selector_index = 0;
11982
11983 let page_size_idx = app.cfn_column_ids.len() + 2;
11985 app.handle_action(Action::NextPreferences);
11986 assert_eq!(app.column_selector_index, page_size_idx);
11987
11988 app.handle_action(Action::NextPreferences);
11990 assert_eq!(app.column_selector_index, 0);
11991 }
11992
11993 #[test]
11994 fn test_s3_preferences_tab_cycling() {
11995 let mut app = test_app();
11996 app.current_service = Service::S3Buckets;
11997 app.mode = Mode::ColumnSelector;
11998 app.column_selector_index = 0;
11999
12000 let page_size_idx = app.s3_bucket_column_ids.len() + 2;
12001
12002 app.handle_action(Action::NextPreferences);
12004 assert_eq!(app.column_selector_index, page_size_idx);
12005
12006 app.handle_action(Action::NextPreferences);
12008 assert_eq!(app.column_selector_index, 0);
12009
12010 app.handle_action(Action::PrevPreferences);
12012 assert_eq!(app.column_selector_index, page_size_idx);
12013
12014 app.handle_action(Action::PrevPreferences);
12016 assert_eq!(app.column_selector_index, 0);
12017 }
12018
12019 #[test]
12020 fn test_s3_filter_resets_selection() {
12021 let mut app = test_app();
12022 app.current_service = Service::S3Buckets;
12023 app.service_selected = true;
12024
12025 app.s3_state.buckets.items = vec![
12027 S3Bucket {
12028 name: "bucket-1".to_string(),
12029 region: "us-east-1".to_string(),
12030 creation_date: "2023-01-01".to_string(),
12031 },
12032 S3Bucket {
12033 name: "bucket-2".to_string(),
12034 region: "us-east-1".to_string(),
12035 creation_date: "2023-01-02".to_string(),
12036 },
12037 S3Bucket {
12038 name: "other-bucket".to_string(),
12039 region: "us-east-1".to_string(),
12040 creation_date: "2023-01-03".to_string(),
12041 },
12042 ];
12043
12044 app.s3_state.selected_row = 1;
12046 app.s3_state.bucket_scroll_offset = 1;
12047
12048 app.mode = Mode::FilterInput;
12050 app.apply_filter_operation(|f| f.push_str("bucket-"));
12051
12052 assert_eq!(app.s3_state.selected_row, 0);
12054 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
12055 assert_eq!(app.s3_state.buckets.filter, "bucket-");
12056 }
12057
12058 #[test]
12059 fn test_s3_navigation_respects_filter() {
12060 let mut app = test_app();
12061 app.current_service = Service::S3Buckets;
12062 app.service_selected = true;
12063 app.mode = Mode::Normal;
12064
12065 app.s3_state.buckets.items = vec![
12067 S3Bucket {
12068 name: "prod-bucket".to_string(),
12069 region: "us-east-1".to_string(),
12070 creation_date: "2023-01-01".to_string(),
12071 },
12072 S3Bucket {
12073 name: "dev-bucket".to_string(),
12074 region: "us-east-1".to_string(),
12075 creation_date: "2023-01-02".to_string(),
12076 },
12077 S3Bucket {
12078 name: "prod-logs".to_string(),
12079 region: "us-east-1".to_string(),
12080 creation_date: "2023-01-03".to_string(),
12081 },
12082 ];
12083
12084 app.s3_state.buckets.filter = "prod".to_string();
12086
12087 assert_eq!(app.s3_state.selected_row, 0);
12089
12090 app.handle_action(Action::NextItem);
12092 assert_eq!(app.s3_state.selected_row, 1);
12093
12094 app.handle_action(Action::NextItem);
12096 assert_eq!(app.s3_state.selected_row, 1);
12097
12098 app.handle_action(Action::PrevItem);
12100 assert_eq!(app.s3_state.selected_row, 0);
12101 }
12102
12103 #[test]
12104 fn test_next_preferences_lambda_functions() {
12105 let mut app = test_app();
12106 app.current_service = Service::LambdaFunctions;
12107 app.mode = Mode::ColumnSelector;
12108 app.column_selector_index = 0;
12109
12110 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
12111 app.handle_action(Action::NextPreferences);
12112 assert_eq!(app.column_selector_index, page_size_idx);
12113
12114 app.handle_action(Action::NextPreferences);
12115 assert_eq!(app.column_selector_index, 0);
12116 }
12117
12118 #[test]
12119 fn test_next_preferences_lambda_applications() {
12120 let mut app = test_app();
12121 app.current_service = Service::LambdaApplications;
12122 app.mode = Mode::ColumnSelector;
12123 app.column_selector_index = 0;
12124
12125 let page_size_idx = app.lambda_application_column_ids.len() + 2;
12126 app.handle_action(Action::NextPreferences);
12127 assert_eq!(app.column_selector_index, page_size_idx);
12128
12129 app.handle_action(Action::NextPreferences);
12130 assert_eq!(app.column_selector_index, 0);
12131 }
12132
12133 #[test]
12134 fn test_next_preferences_ecr_images() {
12135 let mut app = test_app();
12136 app.current_service = Service::EcrRepositories;
12137 app.ecr_state.current_repository = Some("test-repo".to_string());
12138 app.mode = Mode::ColumnSelector;
12139 app.column_selector_index = 0;
12140
12141 let page_size_idx = app.ecr_image_column_ids.len() + 2;
12142 app.handle_action(Action::NextPreferences);
12143 assert_eq!(app.column_selector_index, page_size_idx);
12144
12145 app.handle_action(Action::NextPreferences);
12146 assert_eq!(app.column_selector_index, 0);
12147 }
12148
12149 #[test]
12150 fn test_next_preferences_cloudwatch_log_groups() {
12151 let mut app = test_app();
12152 app.current_service = Service::CloudWatchLogGroups;
12153 app.view_mode = ViewMode::List;
12154 app.mode = Mode::ColumnSelector;
12155 app.column_selector_index = 0;
12156
12157 let page_size_idx = app.cw_log_group_column_ids.len() + 2;
12159 app.handle_action(Action::NextPreferences);
12160 assert_eq!(app.column_selector_index, page_size_idx);
12161
12162 app.handle_action(Action::NextPreferences);
12164 assert_eq!(app.column_selector_index, 0);
12165
12166 app.handle_action(Action::PrevPreferences);
12168 assert_eq!(app.column_selector_index, page_size_idx);
12169
12170 app.handle_action(Action::PrevPreferences);
12172 assert_eq!(app.column_selector_index, 0);
12173 }
12174
12175 #[test]
12176 fn test_next_preferences_cloudwatch_log_streams() {
12177 let mut app = test_app();
12178 app.current_service = Service::CloudWatchLogGroups;
12179 app.view_mode = ViewMode::Detail;
12180 app.mode = Mode::ColumnSelector;
12181 app.column_selector_index = 0;
12182
12183 let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
12185 app.handle_action(Action::NextPreferences);
12186 assert_eq!(app.column_selector_index, page_size_idx);
12187
12188 app.handle_action(Action::NextPreferences);
12190 assert_eq!(app.column_selector_index, 0);
12191 }
12192
12193 #[test]
12194 fn test_cloudformation_next_item() {
12195 let mut app = test_app();
12196 app.current_service = Service::CloudFormationStacks;
12197 app.service_selected = true;
12198 app.mode = Mode::Normal;
12199 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12200 app.cfn_state.table.items = vec![
12201 CfnStack {
12202 name: "stack1".to_string(),
12203 stack_id: "id1".to_string(),
12204 status: "CREATE_COMPLETE".to_string(),
12205 created_time: "2024-01-01".to_string(),
12206 updated_time: String::new(),
12207 deleted_time: String::new(),
12208 drift_status: String::new(),
12209 last_drift_check_time: String::new(),
12210 status_reason: String::new(),
12211 description: String::new(),
12212 detailed_status: String::new(),
12213 root_stack: String::new(),
12214 parent_stack: String::new(),
12215 termination_protection: false,
12216 iam_role: String::new(),
12217 tags: Vec::new(),
12218 stack_policy: String::new(),
12219 rollback_monitoring_time: String::new(),
12220 rollback_alarms: Vec::new(),
12221 notification_arns: Vec::new(),
12222 },
12223 CfnStack {
12224 name: "stack2".to_string(),
12225 stack_id: "id2".to_string(),
12226 status: "UPDATE_COMPLETE".to_string(),
12227 created_time: "2024-01-02".to_string(),
12228 updated_time: String::new(),
12229 deleted_time: String::new(),
12230 drift_status: String::new(),
12231 last_drift_check_time: String::new(),
12232 status_reason: String::new(),
12233 description: String::new(),
12234 detailed_status: String::new(),
12235 root_stack: String::new(),
12236 parent_stack: String::new(),
12237 termination_protection: false,
12238 iam_role: String::new(),
12239 tags: Vec::new(),
12240 stack_policy: String::new(),
12241 rollback_monitoring_time: String::new(),
12242 rollback_alarms: Vec::new(),
12243 notification_arns: Vec::new(),
12244 },
12245 ];
12246 app.cfn_state.table.reset();
12247
12248 app.handle_action(Action::NextItem);
12249 assert_eq!(app.cfn_state.table.selected, 1);
12250
12251 app.handle_action(Action::NextItem);
12252 assert_eq!(app.cfn_state.table.selected, 1); }
12254
12255 #[test]
12256 fn test_cloudformation_prev_item() {
12257 let mut app = test_app();
12258 app.current_service = Service::CloudFormationStacks;
12259 app.service_selected = true;
12260 app.mode = Mode::Normal;
12261 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12262 app.cfn_state.table.items = vec![
12263 CfnStack {
12264 name: "stack1".to_string(),
12265 stack_id: "id1".to_string(),
12266 status: "CREATE_COMPLETE".to_string(),
12267 created_time: "2024-01-01".to_string(),
12268 updated_time: String::new(),
12269 deleted_time: String::new(),
12270 drift_status: String::new(),
12271 last_drift_check_time: String::new(),
12272 status_reason: String::new(),
12273 description: String::new(),
12274 detailed_status: String::new(),
12275 root_stack: String::new(),
12276 parent_stack: String::new(),
12277 termination_protection: false,
12278 iam_role: String::new(),
12279 tags: Vec::new(),
12280 stack_policy: String::new(),
12281 rollback_monitoring_time: String::new(),
12282 rollback_alarms: Vec::new(),
12283 notification_arns: Vec::new(),
12284 },
12285 CfnStack {
12286 name: "stack2".to_string(),
12287 stack_id: "id2".to_string(),
12288 status: "UPDATE_COMPLETE".to_string(),
12289 created_time: "2024-01-02".to_string(),
12290 updated_time: String::new(),
12291 deleted_time: String::new(),
12292 drift_status: String::new(),
12293 last_drift_check_time: String::new(),
12294 status_reason: String::new(),
12295 description: String::new(),
12296 detailed_status: String::new(),
12297 root_stack: String::new(),
12298 parent_stack: String::new(),
12299 termination_protection: false,
12300 iam_role: String::new(),
12301 tags: Vec::new(),
12302 stack_policy: String::new(),
12303 rollback_monitoring_time: String::new(),
12304 rollback_alarms: Vec::new(),
12305 notification_arns: Vec::new(),
12306 },
12307 ];
12308 app.cfn_state.table.selected = 1;
12309
12310 app.handle_action(Action::PrevItem);
12311 assert_eq!(app.cfn_state.table.selected, 0);
12312
12313 app.handle_action(Action::PrevItem);
12314 assert_eq!(app.cfn_state.table.selected, 0); }
12316
12317 #[test]
12318 fn test_cloudformation_page_down() {
12319 let mut app = test_app();
12320 app.current_service = Service::CloudFormationStacks;
12321 app.service_selected = true;
12322 app.mode = Mode::Normal;
12323 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12324
12325 for i in 0..20 {
12327 app.cfn_state.table.items.push(CfnStack {
12328 name: format!("stack{}", i),
12329 stack_id: format!("id{}", i),
12330 status: "CREATE_COMPLETE".to_string(),
12331 created_time: format!("2024-01-{:02}", i + 1),
12332 updated_time: String::new(),
12333 deleted_time: String::new(),
12334 drift_status: String::new(),
12335 last_drift_check_time: String::new(),
12336 status_reason: String::new(),
12337 description: String::new(),
12338 detailed_status: String::new(),
12339 root_stack: String::new(),
12340 parent_stack: String::new(),
12341 termination_protection: false,
12342 iam_role: String::new(),
12343 tags: Vec::new(),
12344 stack_policy: String::new(),
12345 rollback_monitoring_time: String::new(),
12346 rollback_alarms: Vec::new(),
12347 notification_arns: Vec::new(),
12348 });
12349 }
12350 app.cfn_state.table.reset();
12351
12352 app.handle_action(Action::PageDown);
12353 assert_eq!(app.cfn_state.table.selected, 10);
12354
12355 app.handle_action(Action::PageDown);
12356 assert_eq!(app.cfn_state.table.selected, 19); }
12358
12359 #[test]
12360 fn test_cloudformation_page_up() {
12361 let mut app = test_app();
12362 app.current_service = Service::CloudFormationStacks;
12363 app.service_selected = true;
12364 app.mode = Mode::Normal;
12365 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12366
12367 for i in 0..20 {
12369 app.cfn_state.table.items.push(CfnStack {
12370 name: format!("stack{}", i),
12371 stack_id: format!("id{}", i),
12372 status: "CREATE_COMPLETE".to_string(),
12373 created_time: format!("2024-01-{:02}", i + 1),
12374 updated_time: String::new(),
12375 deleted_time: String::new(),
12376 drift_status: String::new(),
12377 last_drift_check_time: String::new(),
12378 status_reason: String::new(),
12379 description: String::new(),
12380 detailed_status: String::new(),
12381 root_stack: String::new(),
12382 parent_stack: String::new(),
12383 termination_protection: false,
12384 iam_role: String::new(),
12385 tags: Vec::new(),
12386 stack_policy: String::new(),
12387 rollback_monitoring_time: String::new(),
12388 rollback_alarms: Vec::new(),
12389 notification_arns: Vec::new(),
12390 });
12391 }
12392 app.cfn_state.table.selected = 15;
12393
12394 app.handle_action(Action::PageUp);
12395 assert_eq!(app.cfn_state.table.selected, 5);
12396
12397 app.handle_action(Action::PageUp);
12398 assert_eq!(app.cfn_state.table.selected, 0); }
12400
12401 #[test]
12402 fn test_cloudformation_filter_input() {
12403 let mut app = test_app();
12404 app.current_service = Service::CloudFormationStacks;
12405 app.service_selected = true;
12406 app.mode = Mode::Normal;
12407
12408 app.handle_action(Action::StartFilter);
12409 assert_eq!(app.mode, Mode::FilterInput);
12410
12411 app.cfn_state.table.filter = "test".to_string();
12413 assert_eq!(app.cfn_state.table.filter, "test");
12414 }
12415
12416 #[test]
12417 fn test_cloudformation_filter_applies() {
12418 let mut app = test_app();
12419 app.current_service = Service::CloudFormationStacks;
12420 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12421 app.cfn_state.table.items = vec![
12422 CfnStack {
12423 name: "prod-stack".to_string(),
12424 stack_id: "id1".to_string(),
12425 status: "CREATE_COMPLETE".to_string(),
12426 created_time: "2024-01-01".to_string(),
12427 updated_time: String::new(),
12428 deleted_time: String::new(),
12429 drift_status: String::new(),
12430 last_drift_check_time: String::new(),
12431 status_reason: String::new(),
12432 description: "Production stack".to_string(),
12433 detailed_status: String::new(),
12434 root_stack: String::new(),
12435 parent_stack: String::new(),
12436 termination_protection: false,
12437 iam_role: String::new(),
12438 tags: Vec::new(),
12439 stack_policy: String::new(),
12440 rollback_monitoring_time: String::new(),
12441 rollback_alarms: Vec::new(),
12442 notification_arns: Vec::new(),
12443 },
12444 CfnStack {
12445 name: "dev-stack".to_string(),
12446 stack_id: "id2".to_string(),
12447 status: "UPDATE_COMPLETE".to_string(),
12448 created_time: "2024-01-02".to_string(),
12449 updated_time: String::new(),
12450 deleted_time: String::new(),
12451 drift_status: String::new(),
12452 last_drift_check_time: String::new(),
12453 status_reason: String::new(),
12454 description: "Development stack".to_string(),
12455 detailed_status: String::new(),
12456 root_stack: String::new(),
12457 parent_stack: String::new(),
12458 termination_protection: false,
12459 iam_role: String::new(),
12460 tags: Vec::new(),
12461 stack_policy: String::new(),
12462 rollback_monitoring_time: String::new(),
12463 rollback_alarms: Vec::new(),
12464 notification_arns: Vec::new(),
12465 },
12466 ];
12467 app.cfn_state.table.filter = "prod".to_string();
12468
12469 let filtered = filtered_cloudformation_stacks(&app);
12470 assert_eq!(filtered.len(), 1);
12471 assert_eq!(filtered[0].name, "prod-stack");
12472 }
12473
12474 #[test]
12475 fn test_cloudformation_right_arrow_expands() {
12476 let mut app = test_app();
12477 app.current_service = Service::CloudFormationStacks;
12478 app.service_selected = true;
12479 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12480 app.cfn_state.table.items = vec![CfnStack {
12481 name: "test-stack".to_string(),
12482 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12483 .to_string(),
12484 status: "CREATE_COMPLETE".to_string(),
12485 created_time: "2024-01-01".to_string(),
12486 updated_time: String::new(),
12487 deleted_time: String::new(),
12488 drift_status: String::new(),
12489 last_drift_check_time: String::new(),
12490 status_reason: String::new(),
12491 description: "Test stack".to_string(),
12492 detailed_status: String::new(),
12493 root_stack: String::new(),
12494 parent_stack: String::new(),
12495 termination_protection: false,
12496 iam_role: String::new(),
12497 tags: Vec::new(),
12498 stack_policy: String::new(),
12499 rollback_monitoring_time: String::new(),
12500 rollback_alarms: Vec::new(),
12501 notification_arns: Vec::new(),
12502 }];
12503 app.cfn_state.table.reset();
12504
12505 assert_eq!(app.cfn_state.table.expanded_item, None);
12506
12507 app.handle_action(Action::NextPane);
12508 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
12509 }
12510
12511 #[test]
12512 fn test_cloudformation_left_arrow_collapses() {
12513 let mut app = test_app();
12514 app.current_service = Service::CloudFormationStacks;
12515 app.service_selected = true;
12516 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12517 app.cfn_state.table.items = vec![CfnStack {
12518 name: "test-stack".to_string(),
12519 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12520 .to_string(),
12521 status: "CREATE_COMPLETE".to_string(),
12522 created_time: "2024-01-01".to_string(),
12523 updated_time: String::new(),
12524 deleted_time: String::new(),
12525 drift_status: String::new(),
12526 last_drift_check_time: String::new(),
12527 status_reason: String::new(),
12528 description: "Test stack".to_string(),
12529 detailed_status: String::new(),
12530 root_stack: String::new(),
12531 parent_stack: String::new(),
12532 termination_protection: false,
12533 iam_role: String::new(),
12534 tags: Vec::new(),
12535 stack_policy: String::new(),
12536 rollback_monitoring_time: String::new(),
12537 rollback_alarms: Vec::new(),
12538 notification_arns: Vec::new(),
12539 }];
12540 app.cfn_state.table.reset();
12541 app.cfn_state.table.expanded_item = Some(0);
12542
12543 app.handle_action(Action::PrevPane);
12544 assert_eq!(app.cfn_state.table.expanded_item, None);
12545 }
12546
12547 #[test]
12548 fn test_cloudformation_enter_drills_into_stack() {
12549 let mut app = test_app();
12550 app.current_service = Service::CloudFormationStacks;
12551 app.service_selected = true;
12552 app.mode = Mode::Normal;
12553 app.tabs = vec![Tab {
12554 service: Service::CloudFormationStacks,
12555 title: "CloudFormation › Stacks".to_string(),
12556 breadcrumb: "CloudFormation › Stacks".to_string(),
12557 }];
12558 app.current_tab = 0;
12559 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12560 app.cfn_state.table.items = vec![CfnStack {
12561 name: "test-stack".to_string(),
12562 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12563 .to_string(),
12564 status: "CREATE_COMPLETE".to_string(),
12565 created_time: "2024-01-01".to_string(),
12566 updated_time: String::new(),
12567 deleted_time: String::new(),
12568 drift_status: String::new(),
12569 last_drift_check_time: String::new(),
12570 status_reason: String::new(),
12571 description: "Test stack".to_string(),
12572 detailed_status: String::new(),
12573 root_stack: String::new(),
12574 parent_stack: String::new(),
12575 termination_protection: false,
12576 iam_role: String::new(),
12577 tags: Vec::new(),
12578 stack_policy: String::new(),
12579 rollback_monitoring_time: String::new(),
12580 rollback_alarms: Vec::new(),
12581 notification_arns: Vec::new(),
12582 }];
12583 app.cfn_state.table.reset();
12584
12585 let filtered = filtered_cloudformation_stacks(&app);
12587 assert_eq!(filtered.len(), 1);
12588 assert_eq!(filtered[0].name, "test-stack");
12589
12590 assert_eq!(app.cfn_state.current_stack, None);
12591
12592 app.handle_action(Action::Select);
12594 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
12595 }
12596
12597 #[test]
12598 fn test_cloudformation_copy_to_clipboard() {
12599 let mut app = test_app();
12600 app.current_service = Service::CloudFormationStacks;
12601 app.service_selected = true;
12602 app.mode = Mode::Normal;
12603 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12604 app.cfn_state.table.items = vec![
12605 CfnStack {
12606 name: "stack1".to_string(),
12607 stack_id: "id1".to_string(),
12608 status: "CREATE_COMPLETE".to_string(),
12609 created_time: "2024-01-01".to_string(),
12610 updated_time: String::new(),
12611 deleted_time: String::new(),
12612 drift_status: String::new(),
12613 last_drift_check_time: String::new(),
12614 status_reason: String::new(),
12615 description: String::new(),
12616 detailed_status: String::new(),
12617 root_stack: String::new(),
12618 parent_stack: String::new(),
12619 termination_protection: false,
12620 iam_role: String::new(),
12621 tags: Vec::new(),
12622 stack_policy: String::new(),
12623 rollback_monitoring_time: String::new(),
12624 rollback_alarms: Vec::new(),
12625 notification_arns: Vec::new(),
12626 },
12627 CfnStack {
12628 name: "stack2".to_string(),
12629 stack_id: "id2".to_string(),
12630 status: "UPDATE_COMPLETE".to_string(),
12631 created_time: "2024-01-02".to_string(),
12632 updated_time: String::new(),
12633 deleted_time: String::new(),
12634 drift_status: String::new(),
12635 last_drift_check_time: String::new(),
12636 status_reason: String::new(),
12637 description: String::new(),
12638 detailed_status: String::new(),
12639 root_stack: String::new(),
12640 parent_stack: String::new(),
12641 termination_protection: false,
12642 iam_role: String::new(),
12643 tags: Vec::new(),
12644 stack_policy: String::new(),
12645 rollback_monitoring_time: String::new(),
12646 rollback_alarms: Vec::new(),
12647 notification_arns: Vec::new(),
12648 },
12649 ];
12650
12651 assert!(!app.snapshot_requested);
12652 app.handle_action(Action::CopyToClipboard);
12653
12654 assert!(app.snapshot_requested);
12656 }
12657
12658 #[test]
12659 fn test_cloudformation_expansion_shows_all_visible_columns() {
12660 let mut app = test_app();
12661 app.current_service = Service::CloudFormationStacks;
12662 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12663 app.cfn_state.table.items = vec![CfnStack {
12664 name: "test-stack".to_string(),
12665 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12666 .to_string(),
12667 status: "CREATE_COMPLETE".to_string(),
12668 created_time: "2024-01-01".to_string(),
12669 updated_time: "2024-01-02".to_string(),
12670 deleted_time: String::new(),
12671 drift_status: "IN_SYNC".to_string(),
12672 last_drift_check_time: "2024-01-03".to_string(),
12673 status_reason: String::new(),
12674 description: "Test description".to_string(),
12675 detailed_status: String::new(),
12676 root_stack: String::new(),
12677 parent_stack: String::new(),
12678 termination_protection: false,
12679 iam_role: String::new(),
12680 tags: Vec::new(),
12681 stack_policy: String::new(),
12682 rollback_monitoring_time: String::new(),
12683 rollback_alarms: Vec::new(),
12684 notification_arns: Vec::new(),
12685 }];
12686
12687 app.cfn_visible_column_ids = [
12689 CfnColumn::Name,
12690 CfnColumn::Status,
12691 CfnColumn::CreatedTime,
12692 CfnColumn::Description,
12693 ]
12694 .iter()
12695 .map(|c| c.id())
12696 .collect();
12697
12698 app.cfn_state.table.expanded_item = Some(0);
12699
12700 assert_eq!(app.cfn_visible_column_ids.len(), 4);
12703 assert!(app.cfn_state.table.has_expanded_item());
12704 }
12705
12706 #[test]
12707 fn test_cloudformation_empty_list_shows_page_1() {
12708 let mut app = test_app();
12709 app.current_service = Service::CloudFormationStacks;
12710 app.cfn_state.table.items = vec![];
12711
12712 let filtered = filtered_cloudformation_stacks(&app);
12713 assert_eq!(filtered.len(), 0);
12714
12715 let page_size = app.cfn_state.table.page_size.value();
12717 let total_pages = filtered.len().div_ceil(page_size);
12718 assert_eq!(total_pages, 0);
12719
12720 }
12723}
12724
12725impl App {
12726 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
12727 let mut all = AwsRegion::all();
12728
12729 for region in &mut all {
12731 region.latency_ms = self.region_latencies.get(region.code).copied();
12732 }
12733
12734 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
12736 all
12737 } else {
12738 let filter_lower = self.region_filter.to_lowercase();
12739 all.into_iter()
12740 .filter(|r| {
12741 r.name.to_lowercase().contains(&filter_lower)
12742 || r.code.to_lowercase().contains(&filter_lower)
12743 || r.group.to_lowercase().contains(&filter_lower)
12744 })
12745 .collect()
12746 };
12747
12748 let mut sorted = filtered;
12750 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
12751 sorted
12752 }
12753
12754 pub fn measure_region_latencies(&mut self) {
12755 use std::time::Instant;
12756 self.region_latencies.clear();
12757
12758 let regions = AwsRegion::all();
12759 let start_all = Instant::now();
12760 tracing::info!("Starting latency measurement for {} regions", regions.len());
12761
12762 let handles: Vec<_> = regions
12763 .iter()
12764 .map(|region| {
12765 let code = region.code.to_string();
12766 std::thread::spawn(move || {
12767 let endpoint = format!("https://sts.{}.amazonaws.com", code);
12769 let start = Instant::now();
12770
12771 match ureq::get(&endpoint)
12772 .timeout(std::time::Duration::from_secs(2))
12773 .call()
12774 {
12775 Ok(_) => {
12776 let latency = start.elapsed().as_millis() as u64;
12777 Some((code, latency))
12778 }
12779 Err(e) => {
12780 tracing::debug!("Failed to measure {}: {}", code, e);
12781 Some((code, 9999))
12782 }
12783 }
12784 })
12785 })
12786 .collect();
12787
12788 for handle in handles {
12789 if let Ok(Some((code, latency))) = handle.join() {
12790 self.region_latencies.insert(code, latency);
12791 }
12792 }
12793
12794 tracing::info!(
12795 "Measured {} regions in {:?}",
12796 self.region_latencies.len(),
12797 start_all.elapsed()
12798 );
12799 }
12800
12801 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
12802 filter_profiles(&self.available_profiles, &self.profile_filter)
12803 }
12804
12805 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
12806 if self.session_filter.is_empty() {
12807 return self.sessions.iter().collect();
12808 }
12809 let filter_lower = self.session_filter.to_lowercase();
12810 self.sessions
12811 .iter()
12812 .filter(|s| {
12813 s.profile.to_lowercase().contains(&filter_lower)
12814 || s.region.to_lowercase().contains(&filter_lower)
12815 || s.account_id.to_lowercase().contains(&filter_lower)
12816 || s.role_arn.to_lowercase().contains(&filter_lower)
12817 })
12818 .collect()
12819 }
12820
12821 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
12822 if self.tab_filter.is_empty() {
12823 return self.tabs.iter().enumerate().collect();
12824 }
12825 let filter_lower = self.tab_filter.to_lowercase();
12826 self.tabs
12827 .iter()
12828 .enumerate()
12829 .filter(|(_, tab)| {
12830 tab.title.to_lowercase().contains(&filter_lower)
12831 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
12832 })
12833 .collect()
12834 }
12835
12836 pub fn load_aws_profiles() -> Vec<AwsProfile> {
12837 AwsProfile::load_all()
12838 }
12839
12840 pub async fn fetch_profile_accounts(&mut self) {
12841 for profile in &mut self.available_profiles {
12842 if profile.account.is_none() {
12843 let region = profile
12844 .region
12845 .clone()
12846 .unwrap_or_else(|| "us-east-1".to_string());
12847 if let Ok(account) =
12848 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
12849 {
12850 profile.account = Some(account);
12851 }
12852 }
12853 }
12854 }
12855
12856 fn save_current_session(&mut self) {
12857 if self.tabs.is_empty() {
12859 if let Some(ref session) = self.current_session {
12860 let _ = session.delete();
12861 self.current_session = None;
12862 }
12863 return;
12864 }
12865
12866 let session = if let Some(ref mut current) = self.current_session {
12867 current.tabs = self
12869 .tabs
12870 .iter()
12871 .map(|t| SessionTab {
12872 service: format!("{:?}", t.service),
12873 title: t.title.clone(),
12874 breadcrumb: t.breadcrumb.clone(),
12875 filter: match t.service {
12876 Service::CloudWatchLogGroups => {
12877 Some(self.log_groups_state.log_groups.filter.clone())
12878 }
12879 _ => None,
12880 },
12881 selected_item: None,
12882 })
12883 .collect();
12884 current.clone()
12885 } else {
12886 let mut session = Session::new(
12888 self.profile.clone(),
12889 self.region.clone(),
12890 self.config.account_id.clone(),
12891 self.config.role_arn.clone(),
12892 );
12893 session.tabs = self
12894 .tabs
12895 .iter()
12896 .map(|t| SessionTab {
12897 service: format!("{:?}", t.service),
12898 title: t.title.clone(),
12899 breadcrumb: t.breadcrumb.clone(),
12900 filter: match t.service {
12901 Service::CloudWatchLogGroups => {
12902 Some(self.log_groups_state.log_groups.filter.clone())
12903 }
12904 _ => None,
12905 },
12906 selected_item: None,
12907 })
12908 .collect();
12909 self.current_session = Some(session.clone());
12910 session
12911 };
12912
12913 let _ = session.save();
12914 }
12915}
12916
12917#[cfg(test)]
12918mod iam_policy_view_tests {
12919 use super::*;
12920 use test_helpers::*;
12921
12922 #[test]
12923 fn test_enter_opens_policy_view() {
12924 let mut app = test_app();
12925 app.current_service = Service::IamRoles;
12926 app.service_selected = true;
12927 app.mode = Mode::Normal;
12928 app.view_mode = ViewMode::Detail;
12929 app.iam_state.current_role = Some("TestRole".to_string());
12930 app.iam_state.policies.items = vec![IamPolicy {
12931 policy_name: "TestPolicy".to_string(),
12932 policy_type: "Inline".to_string(),
12933 attached_via: "Direct".to_string(),
12934 attached_entities: "1".to_string(),
12935 description: "Test".to_string(),
12936 creation_time: "2023-01-01".to_string(),
12937 edited_time: "2023-01-01".to_string(),
12938 policy_arn: None,
12939 }];
12940 app.iam_state.policies.reset();
12941
12942 app.handle_action(Action::Select);
12943
12944 assert_eq!(app.view_mode, ViewMode::PolicyView);
12945 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
12946 assert_eq!(app.iam_state.policy_scroll, 0);
12947 assert!(app.iam_state.policies.loading);
12948 }
12949
12950 #[test]
12951 fn test_escape_closes_policy_view() {
12952 let mut app = test_app();
12953 app.current_service = Service::IamRoles;
12954 app.service_selected = true;
12955 app.mode = Mode::Normal;
12956 app.view_mode = ViewMode::PolicyView;
12957 app.iam_state.current_role = Some("TestRole".to_string());
12958 app.iam_state.current_policy = Some("TestPolicy".to_string());
12959 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
12960 app.iam_state.policy_scroll = 5;
12961
12962 app.handle_action(Action::PrevPane);
12963
12964 assert_eq!(app.view_mode, ViewMode::Detail);
12965 assert_eq!(app.iam_state.current_policy, None);
12966 assert_eq!(app.iam_state.policy_document, "");
12967 assert_eq!(app.iam_state.policy_scroll, 0);
12968 }
12969
12970 #[test]
12971 fn test_ctrl_d_scrolls_down_in_policy_view() {
12972 let mut app = test_app();
12973 app.current_service = Service::IamRoles;
12974 app.service_selected = true;
12975 app.mode = Mode::Normal;
12976 app.view_mode = ViewMode::PolicyView;
12977 app.iam_state.current_role = Some("TestRole".to_string());
12978 app.iam_state.current_policy = Some("TestPolicy".to_string());
12979 app.iam_state.policy_document = (0..100)
12980 .map(|i| format!("line {}", i))
12981 .collect::<Vec<_>>()
12982 .join("\n");
12983 app.iam_state.policy_scroll = 0;
12984
12985 app.handle_action(Action::ScrollDown);
12986
12987 assert_eq!(app.iam_state.policy_scroll, 10);
12988
12989 app.handle_action(Action::ScrollDown);
12990
12991 assert_eq!(app.iam_state.policy_scroll, 20);
12992 }
12993
12994 #[test]
12995 fn test_ctrl_u_scrolls_up_in_policy_view() {
12996 let mut app = test_app();
12997 app.current_service = Service::IamRoles;
12998 app.service_selected = true;
12999 app.mode = Mode::Normal;
13000 app.view_mode = ViewMode::PolicyView;
13001 app.iam_state.current_role = Some("TestRole".to_string());
13002 app.iam_state.current_policy = Some("TestPolicy".to_string());
13003 app.iam_state.policy_document = (0..100)
13004 .map(|i| format!("line {}", i))
13005 .collect::<Vec<_>>()
13006 .join("\n");
13007 app.iam_state.policy_scroll = 30;
13008
13009 app.handle_action(Action::ScrollUp);
13010
13011 assert_eq!(app.iam_state.policy_scroll, 20);
13012
13013 app.handle_action(Action::ScrollUp);
13014
13015 assert_eq!(app.iam_state.policy_scroll, 10);
13016 }
13017
13018 #[test]
13019 fn test_scroll_does_not_go_negative() {
13020 let mut app = test_app();
13021 app.current_service = Service::IamRoles;
13022 app.service_selected = true;
13023 app.mode = Mode::Normal;
13024 app.view_mode = ViewMode::PolicyView;
13025 app.iam_state.current_role = Some("TestRole".to_string());
13026 app.iam_state.current_policy = Some("TestPolicy".to_string());
13027 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
13028 app.iam_state.policy_scroll = 0;
13029
13030 app.handle_action(Action::ScrollUp);
13031
13032 assert_eq!(app.iam_state.policy_scroll, 0);
13033 }
13034
13035 #[test]
13036 fn test_scroll_does_not_exceed_max() {
13037 let mut app = test_app();
13038 app.current_service = Service::IamRoles;
13039 app.service_selected = true;
13040 app.mode = Mode::Normal;
13041 app.view_mode = ViewMode::PolicyView;
13042 app.iam_state.current_role = Some("TestRole".to_string());
13043 app.iam_state.current_policy = Some("TestPolicy".to_string());
13044 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
13045 app.iam_state.policy_scroll = 0;
13046
13047 app.handle_action(Action::ScrollDown);
13048
13049 assert_eq!(app.iam_state.policy_scroll, 2); }
13051
13052 #[test]
13053 fn test_policy_view_console_url() {
13054 let mut app = test_app();
13055 app.current_service = Service::IamRoles;
13056 app.service_selected = true;
13057 app.view_mode = ViewMode::PolicyView;
13058 app.iam_state.current_role = Some("TestRole".to_string());
13059 app.iam_state.current_policy = Some("TestPolicy".to_string());
13060
13061 let url = app.get_console_url();
13062
13063 assert!(url.contains("us-east-1.console.aws.amazon.com"));
13064 assert!(url.contains("/roles/details/TestRole"));
13065 assert!(url.contains("/editPolicy/TestPolicy"));
13066 assert!(url.contains("step=addPermissions"));
13067 }
13068
13069 #[test]
13070 fn test_esc_from_policy_view_goes_to_role_detail() {
13071 let mut app = test_app();
13072 app.current_service = Service::IamRoles;
13073 app.service_selected = true;
13074 app.mode = Mode::Normal;
13075 app.view_mode = ViewMode::PolicyView;
13076 app.iam_state.current_role = Some("TestRole".to_string());
13077 app.iam_state.current_policy = Some("TestPolicy".to_string());
13078 app.iam_state.policy_document = "test".to_string();
13079 app.iam_state.policy_scroll = 5;
13080
13081 app.handle_action(Action::GoBack);
13082
13083 assert_eq!(app.view_mode, ViewMode::Detail);
13084 assert_eq!(app.iam_state.current_policy, None);
13085 assert_eq!(app.iam_state.policy_document, "");
13086 assert_eq!(app.iam_state.policy_scroll, 0);
13087 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
13088 }
13089
13090 #[test]
13091 fn test_esc_from_role_detail_goes_to_role_list() {
13092 let mut app = test_app();
13093 app.current_service = Service::IamRoles;
13094 app.service_selected = true;
13095 app.mode = Mode::Normal;
13096 app.view_mode = ViewMode::Detail;
13097 app.iam_state.current_role = Some("TestRole".to_string());
13098
13099 app.handle_action(Action::GoBack);
13100
13101 assert_eq!(app.iam_state.current_role, None);
13102 }
13103
13104 #[test]
13105 fn test_right_arrow_expands_policy_row() {
13106 let mut app = test_app();
13107 app.current_service = Service::IamRoles;
13108 app.service_selected = true;
13109 app.mode = Mode::Normal;
13110 app.view_mode = ViewMode::Detail;
13111 app.iam_state.current_role = Some("TestRole".to_string());
13112 app.iam_state.policies.items = vec![IamPolicy {
13113 policy_name: "TestPolicy".to_string(),
13114 policy_type: "Inline".to_string(),
13115 attached_via: "Direct".to_string(),
13116 attached_entities: "1".to_string(),
13117 description: "Test".to_string(),
13118 creation_time: "2023-01-01".to_string(),
13119 edited_time: "2023-01-01".to_string(),
13120 policy_arn: None,
13121 }];
13122 app.iam_state.policies.reset();
13123
13124 app.handle_action(Action::NextPane);
13125
13126 assert_eq!(app.view_mode, ViewMode::Detail);
13128 assert_eq!(app.iam_state.current_policy, None);
13129 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
13130 }
13131}
13132
13133#[cfg(test)]
13134mod tab_filter_tests {
13135 use super::*;
13136 use test_helpers::*;
13137
13138 #[test]
13139 fn test_space_t_opens_tab_picker() {
13140 let mut app = test_app();
13141 app.tabs = vec![
13142 Tab {
13143 service: Service::CloudWatchLogGroups,
13144 title: "Tab 1".to_string(),
13145 breadcrumb: "CloudWatch > Log groups".to_string(),
13146 },
13147 Tab {
13148 service: Service::S3Buckets,
13149 title: "Tab 2".to_string(),
13150 breadcrumb: "S3 › Buckets".to_string(),
13151 },
13152 ];
13153 app.current_tab = 0;
13154
13155 app.handle_action(Action::OpenTabPicker);
13156
13157 assert_eq!(app.mode, Mode::TabPicker);
13158 assert_eq!(app.tab_picker_selected, 0);
13159 }
13160
13161 #[test]
13162 fn test_tab_filter_works() {
13163 let mut app = test_app();
13164 app.tabs = vec![
13165 Tab {
13166 service: Service::CloudWatchLogGroups,
13167 title: "CloudWatch Logs".to_string(),
13168 breadcrumb: "CloudWatch > Log groups".to_string(),
13169 },
13170 Tab {
13171 service: Service::S3Buckets,
13172 title: "S3 Buckets".to_string(),
13173 breadcrumb: "S3 › Buckets".to_string(),
13174 },
13175 Tab {
13176 service: Service::CloudWatchAlarms,
13177 title: "CloudWatch Alarms".to_string(),
13178 breadcrumb: "CloudWatch › Alarms".to_string(),
13179 },
13180 ];
13181 app.mode = Mode::TabPicker;
13182
13183 app.handle_action(Action::FilterInput('s'));
13185 app.handle_action(Action::FilterInput('3'));
13186
13187 let filtered = app.get_filtered_tabs();
13188 assert_eq!(filtered.len(), 1);
13189 assert_eq!(filtered[0].1.title, "S3 Buckets");
13190 }
13191
13192 #[test]
13193 fn test_tab_filter_by_breadcrumb() {
13194 let mut app = test_app();
13195 app.tabs = vec![
13196 Tab {
13197 service: Service::CloudWatchLogGroups,
13198 title: "Tab 1".to_string(),
13199 breadcrumb: "CloudWatch > Log groups".to_string(),
13200 },
13201 Tab {
13202 service: Service::S3Buckets,
13203 title: "Tab 2".to_string(),
13204 breadcrumb: "S3 › Buckets".to_string(),
13205 },
13206 ];
13207 app.mode = Mode::TabPicker;
13208
13209 app.handle_action(Action::FilterInput('c'));
13211 app.handle_action(Action::FilterInput('l'));
13212 app.handle_action(Action::FilterInput('o'));
13213 app.handle_action(Action::FilterInput('u'));
13214 app.handle_action(Action::FilterInput('d'));
13215
13216 let filtered = app.get_filtered_tabs();
13217 assert_eq!(filtered.len(), 1);
13218 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
13219 }
13220
13221 #[test]
13222 fn test_tab_filter_backspace() {
13223 let mut app = test_app();
13224 app.tabs = vec![
13225 Tab {
13226 service: Service::CloudWatchLogGroups,
13227 title: "CloudWatch Logs".to_string(),
13228 breadcrumb: "CloudWatch > Log groups".to_string(),
13229 },
13230 Tab {
13231 service: Service::S3Buckets,
13232 title: "S3 Buckets".to_string(),
13233 breadcrumb: "S3 › Buckets".to_string(),
13234 },
13235 ];
13236 app.mode = Mode::TabPicker;
13237
13238 app.handle_action(Action::FilterInput('s'));
13239 app.handle_action(Action::FilterInput('3'));
13240 assert_eq!(app.tab_filter, "s3");
13241
13242 app.handle_action(Action::FilterBackspace);
13243 assert_eq!(app.tab_filter, "s");
13244
13245 let filtered = app.get_filtered_tabs();
13246 assert_eq!(filtered.len(), 2); }
13248
13249 #[test]
13250 fn test_tab_selection_with_filter() {
13251 let mut app = test_app();
13252 app.tabs = vec![
13253 Tab {
13254 service: Service::CloudWatchLogGroups,
13255 title: "CloudWatch Logs".to_string(),
13256 breadcrumb: "CloudWatch > Log groups".to_string(),
13257 },
13258 Tab {
13259 service: Service::S3Buckets,
13260 title: "S3 Buckets".to_string(),
13261 breadcrumb: "S3 › Buckets".to_string(),
13262 },
13263 ];
13264 app.mode = Mode::TabPicker;
13265 app.current_tab = 0;
13266
13267 app.handle_action(Action::FilterInput('s'));
13269 app.handle_action(Action::FilterInput('3'));
13270
13271 app.handle_action(Action::Select);
13273
13274 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
13276 assert_eq!(app.tab_filter, ""); }
13278}
13279
13280#[cfg(test)]
13281mod region_latency_tests {
13282 use super::*;
13283 use test_helpers::*;
13284
13285 #[test]
13286 fn test_regions_sorted_by_latency() {
13287 let mut app = test_app();
13288
13289 app.region_latencies.insert("us-west-2".to_string(), 50);
13291 app.region_latencies.insert("us-east-1".to_string(), 10);
13292 app.region_latencies.insert("eu-west-1".to_string(), 100);
13293
13294 let filtered = app.get_filtered_regions();
13295
13296 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
13298
13299 assert!(with_latency.len() >= 3);
13300 assert_eq!(with_latency[0].code, "us-east-1");
13301 assert_eq!(with_latency[0].latency_ms, Some(10));
13302 assert_eq!(with_latency[1].code, "us-west-2");
13303 assert_eq!(with_latency[1].latency_ms, Some(50));
13304 assert_eq!(with_latency[2].code, "eu-west-1");
13305 assert_eq!(with_latency[2].latency_ms, Some(100));
13306 }
13307
13308 #[test]
13309 fn test_regions_with_latency_before_without() {
13310 let mut app = test_app();
13311
13312 app.region_latencies.insert("eu-west-1".to_string(), 100);
13314
13315 let filtered = app.get_filtered_regions();
13316
13317 assert_eq!(filtered[0].code, "eu-west-1");
13319 assert_eq!(filtered[0].latency_ms, Some(100));
13320
13321 for region in &filtered[1..] {
13323 assert!(region.latency_ms.is_none());
13324 }
13325 }
13326
13327 #[test]
13328 fn test_region_filter_with_latency() {
13329 let mut app = test_app();
13330
13331 app.region_latencies.insert("us-east-1".to_string(), 10);
13332 app.region_latencies.insert("us-west-2".to_string(), 50);
13333 app.region_filter = "us".to_string();
13334
13335 let filtered = app.get_filtered_regions();
13336
13337 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
13339 assert_eq!(filtered[0].code, "us-east-1");
13340 assert_eq!(filtered[1].code, "us-west-2");
13341 }
13342
13343 #[test]
13344 fn test_latency_persists_across_filters() {
13345 let mut app = test_app();
13346
13347 app.region_latencies.insert("us-east-1".to_string(), 10);
13348
13349 app.region_filter = "eu".to_string();
13351 let filtered = app.get_filtered_regions();
13352 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
13353
13354 app.region_filter.clear();
13356 let all = app.get_filtered_regions();
13357
13358 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
13360 assert_eq!(us_east.latency_ms, Some(10));
13361 }
13362
13363 #[test]
13364 fn test_measure_region_latencies_clears_previous() {
13365 let mut app = test_app();
13366
13367 app.region_latencies.insert("us-east-1".to_string(), 100);
13369 app.region_latencies.insert("eu-west-1".to_string(), 200);
13370
13371 app.measure_region_latencies();
13373
13374 assert!(
13376 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
13377 );
13378 }
13379
13380 #[test]
13381 fn test_regions_with_latency_sorted_first() {
13382 let mut app = test_app();
13383
13384 app.region_latencies.insert("us-east-1".to_string(), 50);
13386 app.region_latencies.insert("eu-west-1".to_string(), 500);
13387
13388 let filtered = app.get_filtered_regions();
13389
13390 assert!(filtered.len() > 2);
13392
13393 assert_eq!(filtered[0].code, "us-east-1");
13395 assert_eq!(filtered[0].latency_ms, Some(50));
13396 assert_eq!(filtered[1].code, "eu-west-1");
13397 assert_eq!(filtered[1].latency_ms, Some(500));
13398
13399 for region in &filtered[2..] {
13401 assert!(region.latency_ms.is_none());
13402 }
13403 }
13404
13405 #[test]
13406 fn test_regions_without_latency_sorted_as_1000ms() {
13407 let mut app = test_app();
13408
13409 app.region_latencies
13411 .insert("ap-southeast-2".to_string(), 1500);
13412 app.region_latencies.insert("us-east-1".to_string(), 50);
13414
13415 let filtered = app.get_filtered_regions();
13416
13417 assert_eq!(filtered[0].code, "us-east-1");
13419 assert_eq!(filtered[0].latency_ms, Some(50));
13420
13421 let slow_region_idx = filtered
13423 .iter()
13424 .position(|r| r.code == "ap-southeast-2")
13425 .unwrap();
13426 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
13430 assert!(region.latency_ms.is_none());
13431 }
13432 }
13433
13434 #[test]
13435 fn test_region_picker_opens_with_latencies() {
13436 let mut app = test_app();
13437
13438 app.region_filter.clear();
13440 app.region_picker_selected = 0;
13441 app.measure_region_latencies();
13442
13443 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
13446 }
13447
13448 #[test]
13449 fn test_ecr_tab_next() {
13450 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
13451 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
13452 }
13453
13454 #[test]
13455 fn test_ecr_tab_switching() {
13456 let mut app = test_app();
13457 app.current_service = Service::EcrRepositories;
13458 app.service_selected = true;
13459 app.ecr_state.tab = EcrTab::Private;
13460
13461 app.handle_action(Action::NextDetailTab);
13462 assert_eq!(app.ecr_state.tab, EcrTab::Public);
13463 assert_eq!(app.ecr_state.repositories.selected, 0);
13464
13465 app.handle_action(Action::NextDetailTab);
13466 assert_eq!(app.ecr_state.tab, EcrTab::Private);
13467 }
13468
13469 #[test]
13470 fn test_ecr_navigation() {
13471 let mut app = test_app();
13472 app.current_service = Service::EcrRepositories;
13473 app.service_selected = true;
13474 app.mode = Mode::Normal;
13475 app.ecr_state.repositories.items = vec![
13476 EcrRepository {
13477 name: "repo1".to_string(),
13478 uri: "uri1".to_string(),
13479 created_at: "2023-01-01".to_string(),
13480 tag_immutability: "MUTABLE".to_string(),
13481 encryption_type: "AES256".to_string(),
13482 },
13483 EcrRepository {
13484 name: "repo2".to_string(),
13485 uri: "uri2".to_string(),
13486 created_at: "2023-01-02".to_string(),
13487 tag_immutability: "IMMUTABLE".to_string(),
13488 encryption_type: "KMS".to_string(),
13489 },
13490 ];
13491
13492 app.handle_action(Action::NextItem);
13493 assert_eq!(app.ecr_state.repositories.selected, 1);
13494
13495 app.handle_action(Action::PrevItem);
13496 assert_eq!(app.ecr_state.repositories.selected, 0);
13497 }
13498
13499 #[test]
13500 fn test_ecr_filter() {
13501 let mut app = test_app();
13502 app.current_service = Service::EcrRepositories;
13503 app.service_selected = true;
13504 app.ecr_state.repositories.items = vec![
13505 EcrRepository {
13506 name: "my-app".to_string(),
13507 uri: "uri1".to_string(),
13508 created_at: "2023-01-01".to_string(),
13509 tag_immutability: "MUTABLE".to_string(),
13510 encryption_type: "AES256".to_string(),
13511 },
13512 EcrRepository {
13513 name: "other-service".to_string(),
13514 uri: "uri2".to_string(),
13515 created_at: "2023-01-02".to_string(),
13516 tag_immutability: "IMMUTABLE".to_string(),
13517 encryption_type: "KMS".to_string(),
13518 },
13519 ];
13520
13521 app.ecr_state.repositories.filter = "app".to_string();
13522 let filtered = filtered_ecr_repositories(&app);
13523 assert_eq!(filtered.len(), 1);
13524 assert_eq!(filtered[0].name, "my-app");
13525 }
13526
13527 #[test]
13528 fn test_ecr_filter_input() {
13529 let mut app = test_app();
13530 app.current_service = Service::EcrRepositories;
13531 app.service_selected = true;
13532 app.mode = Mode::FilterInput;
13533
13534 app.handle_action(Action::FilterInput('t'));
13535 app.handle_action(Action::FilterInput('e'));
13536 app.handle_action(Action::FilterInput('s'));
13537 app.handle_action(Action::FilterInput('t'));
13538 assert_eq!(app.ecr_state.repositories.filter, "test");
13539
13540 app.handle_action(Action::FilterBackspace);
13541 assert_eq!(app.ecr_state.repositories.filter, "tes");
13542 }
13543
13544 #[test]
13545 fn test_ecr_filter_resets_selection() {
13546 let mut app = test_app();
13547 app.current_service = Service::EcrRepositories;
13548 app.service_selected = true;
13549 app.mode = Mode::FilterInput;
13550 app.ecr_state.repositories.items = vec![
13551 EcrRepository {
13552 name: "repo1".to_string(),
13553 uri: "uri1".to_string(),
13554 created_at: "2023-01-01".to_string(),
13555 tag_immutability: "MUTABLE".to_string(),
13556 encryption_type: "AES256".to_string(),
13557 },
13558 EcrRepository {
13559 name: "repo2".to_string(),
13560 uri: "uri2".to_string(),
13561 created_at: "2023-01-02".to_string(),
13562 tag_immutability: "IMMUTABLE".to_string(),
13563 encryption_type: "KMS".to_string(),
13564 },
13565 EcrRepository {
13566 name: "repo3".to_string(),
13567 uri: "uri3".to_string(),
13568 created_at: "2023-01-03".to_string(),
13569 tag_immutability: "MUTABLE".to_string(),
13570 encryption_type: "AES256".to_string(),
13571 },
13572 ];
13573
13574 app.ecr_state.repositories.selected = 2;
13576 assert_eq!(app.ecr_state.repositories.selected, 2);
13577
13578 app.handle_action(Action::FilterInput('t'));
13580 assert_eq!(app.ecr_state.repositories.filter, "t");
13581 assert_eq!(app.ecr_state.repositories.selected, 0);
13582
13583 app.ecr_state.repositories.selected = 1;
13585
13586 app.handle_action(Action::FilterBackspace);
13588 assert_eq!(app.ecr_state.repositories.filter, "");
13589 assert_eq!(app.ecr_state.repositories.selected, 0);
13590 }
13591
13592 #[test]
13593 fn test_ecr_images_filter_resets_selection() {
13594 let mut app = test_app();
13595 app.current_service = Service::EcrRepositories;
13596 app.service_selected = true;
13597 app.mode = Mode::FilterInput;
13598 app.ecr_state.current_repository = Some("test-repo".to_string());
13599 app.ecr_state.images.items = vec![
13600 EcrImage {
13601 tag: "v1.0.0".to_string(),
13602 artifact_type: "container".to_string(),
13603 digest: "sha256:abc123".to_string(),
13604 pushed_at: "2023-01-01".to_string(),
13605 size_bytes: 1000,
13606 uri: "uri1".to_string(),
13607 last_pull_time: "".to_string(),
13608 },
13609 EcrImage {
13610 tag: "v2.0.0".to_string(),
13611 artifact_type: "container".to_string(),
13612 digest: "sha256:def456".to_string(),
13613 pushed_at: "2023-01-02".to_string(),
13614 size_bytes: 2000,
13615 uri: "uri2".to_string(),
13616 last_pull_time: "".to_string(),
13617 },
13618 ];
13619
13620 app.ecr_state.images.selected = 1;
13622 assert_eq!(app.ecr_state.images.selected, 1);
13623
13624 app.handle_action(Action::FilterInput('v'));
13626 assert_eq!(app.ecr_state.images.filter, "v");
13627 assert_eq!(app.ecr_state.images.selected, 0);
13628 }
13629
13630 #[test]
13631 fn test_iam_users_filter_input() {
13632 let mut app = test_app();
13633 app.current_service = Service::IamUsers;
13634 app.service_selected = true;
13635 app.mode = Mode::FilterInput;
13636
13637 app.handle_action(Action::FilterInput('a'));
13638 app.handle_action(Action::FilterInput('d'));
13639 app.handle_action(Action::FilterInput('m'));
13640 app.handle_action(Action::FilterInput('i'));
13641 app.handle_action(Action::FilterInput('n'));
13642 assert_eq!(app.iam_state.users.filter, "admin");
13643
13644 app.handle_action(Action::FilterBackspace);
13645 assert_eq!(app.iam_state.users.filter, "admi");
13646 }
13647
13648 #[test]
13649 fn test_iam_policies_filter_input() {
13650 let mut app = test_app();
13651 app.current_service = Service::IamUsers;
13652 app.service_selected = true;
13653 app.iam_state.current_user = Some("testuser".to_string());
13654 app.mode = Mode::FilterInput;
13655
13656 app.handle_action(Action::FilterInput('r'));
13657 app.handle_action(Action::FilterInput('e'));
13658 app.handle_action(Action::FilterInput('a'));
13659 app.handle_action(Action::FilterInput('d'));
13660 assert_eq!(app.iam_state.policies.filter, "read");
13661
13662 app.handle_action(Action::FilterBackspace);
13663 assert_eq!(app.iam_state.policies.filter, "rea");
13664 }
13665
13666 #[test]
13667 fn test_iam_start_filter() {
13668 let mut app = test_app();
13669 app.current_service = Service::IamUsers;
13670 app.service_selected = true;
13671 app.mode = Mode::Normal;
13672
13673 app.handle_action(Action::StartFilter);
13674 assert_eq!(app.mode, Mode::FilterInput);
13675 }
13676
13677 #[test]
13678 fn test_iam_roles_filter_input() {
13679 let mut app = test_app();
13680 app.current_service = Service::IamRoles;
13681 app.service_selected = true;
13682 app.mode = Mode::FilterInput;
13683
13684 app.handle_action(Action::FilterInput('a'));
13685 app.handle_action(Action::FilterInput('d'));
13686 app.handle_action(Action::FilterInput('m'));
13687 app.handle_action(Action::FilterInput('i'));
13688 app.handle_action(Action::FilterInput('n'));
13689 assert_eq!(app.iam_state.roles.filter, "admin");
13690
13691 app.handle_action(Action::FilterBackspace);
13692 assert_eq!(app.iam_state.roles.filter, "admi");
13693 }
13694
13695 #[test]
13696 fn test_iam_roles_start_filter() {
13697 let mut app = test_app();
13698 app.current_service = Service::IamRoles;
13699 app.service_selected = true;
13700 app.mode = Mode::Normal;
13701
13702 app.handle_action(Action::StartFilter);
13703 assert_eq!(app.mode, Mode::FilterInput);
13704 }
13705
13706 #[test]
13707 fn test_iam_roles_navigation() {
13708 let mut app = test_app();
13709 app.current_service = Service::IamRoles;
13710 app.service_selected = true;
13711 app.mode = Mode::Normal;
13712 app.iam_state.roles.items = (0..10)
13713 .map(|i| IamRole {
13714 role_name: format!("role{}", i),
13715 path: "/".to_string(),
13716 trusted_entities: String::new(),
13717 last_activity: String::new(),
13718 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
13719 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13720 description: String::new(),
13721 max_session_duration: Some(3600),
13722 })
13723 .collect();
13724
13725 assert_eq!(app.iam_state.roles.selected, 0);
13726
13727 app.handle_action(Action::NextItem);
13728 assert_eq!(app.iam_state.roles.selected, 1);
13729
13730 app.handle_action(Action::NextItem);
13731 assert_eq!(app.iam_state.roles.selected, 2);
13732
13733 app.handle_action(Action::PrevItem);
13734 assert_eq!(app.iam_state.roles.selected, 1);
13735 }
13736
13737 #[test]
13738 fn test_iam_roles_page_hotkey() {
13739 let mut app = test_app();
13740 app.current_service = Service::IamRoles;
13741 app.service_selected = true;
13742 app.mode = Mode::Normal;
13743 app.iam_state.roles.page_size = PageSize::Ten;
13744 app.iam_state.roles.items = (0..100)
13745 .map(|i| IamRole {
13746 role_name: format!("role{}", i),
13747 path: "/".to_string(),
13748 trusted_entities: String::new(),
13749 last_activity: String::new(),
13750 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
13751 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13752 description: String::new(),
13753 max_session_duration: Some(3600),
13754 })
13755 .collect();
13756
13757 app.handle_action(Action::FilterInput('2'));
13758 app.handle_action(Action::OpenColumnSelector);
13759 assert_eq!(app.iam_state.roles.selected, 10); }
13761
13762 #[test]
13763 fn test_iam_users_page_hotkey() {
13764 let mut app = test_app();
13765 app.current_service = Service::IamUsers;
13766 app.service_selected = true;
13767 app.mode = Mode::Normal;
13768 app.iam_state.users.page_size = PageSize::Ten;
13769 app.iam_state.users.items = (0..100)
13770 .map(|i| IamUser {
13771 user_name: format!("user{}", i),
13772 path: "/".to_string(),
13773 groups: String::new(),
13774 last_activity: String::new(),
13775 mfa: String::new(),
13776 password_age: String::new(),
13777 console_last_sign_in: String::new(),
13778 access_key_id: String::new(),
13779 active_key_age: String::new(),
13780 access_key_last_used: String::new(),
13781 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
13782 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13783 console_access: String::new(),
13784 signing_certs: String::new(),
13785 })
13786 .collect();
13787
13788 app.handle_action(Action::FilterInput('3'));
13789 app.handle_action(Action::OpenColumnSelector);
13790 assert_eq!(app.iam_state.users.selected, 20); }
13792
13793 #[test]
13794 fn test_ecr_scroll_navigation() {
13795 let mut app = test_app();
13796 app.current_service = Service::EcrRepositories;
13797 app.service_selected = true;
13798 app.ecr_state.repositories.items = (0..20)
13799 .map(|i| EcrRepository {
13800 name: format!("repo{}", i),
13801 uri: format!("uri{}", i),
13802 created_at: "2023-01-01".to_string(),
13803 tag_immutability: "MUTABLE".to_string(),
13804 encryption_type: "AES256".to_string(),
13805 })
13806 .collect();
13807
13808 app.handle_action(Action::ScrollDown);
13809 assert_eq!(app.ecr_state.repositories.selected, 10);
13810
13811 app.handle_action(Action::ScrollUp);
13812 assert_eq!(app.ecr_state.repositories.selected, 0);
13813 }
13814
13815 #[test]
13816 fn test_ecr_tab_switching_triggers_reload() {
13817 let mut app = test_app();
13818 app.current_service = Service::EcrRepositories;
13819 app.service_selected = true;
13820 app.ecr_state.tab = EcrTab::Private;
13821 app.ecr_state.repositories.loading = false;
13822 app.ecr_state.repositories.items = vec![EcrRepository {
13823 name: "private-repo".to_string(),
13824 uri: "uri".to_string(),
13825 created_at: "2023-01-01".to_string(),
13826 tag_immutability: "MUTABLE".to_string(),
13827 encryption_type: "AES256".to_string(),
13828 }];
13829
13830 app.handle_action(Action::NextDetailTab);
13831 assert_eq!(app.ecr_state.tab, EcrTab::Public);
13832 assert!(app.ecr_state.repositories.loading);
13833 assert_eq!(app.ecr_state.repositories.selected, 0);
13834 }
13835
13836 #[test]
13837 fn test_ecr_tab_cycles_between_private_and_public() {
13838 let mut app = test_app();
13839 app.current_service = Service::EcrRepositories;
13840 app.service_selected = true;
13841 app.ecr_state.tab = EcrTab::Private;
13842
13843 app.handle_action(Action::NextDetailTab);
13844 assert_eq!(app.ecr_state.tab, EcrTab::Public);
13845
13846 app.handle_action(Action::NextDetailTab);
13847 assert_eq!(app.ecr_state.tab, EcrTab::Private);
13848 }
13849
13850 #[test]
13851 fn test_page_size_values() {
13852 assert_eq!(PageSize::Ten.value(), 10);
13853 assert_eq!(PageSize::TwentyFive.value(), 25);
13854 assert_eq!(PageSize::Fifty.value(), 50);
13855 assert_eq!(PageSize::OneHundred.value(), 100);
13856 }
13857
13858 #[test]
13859 fn test_page_size_next() {
13860 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
13861 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
13862 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
13863 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
13864 }
13865
13866 #[test]
13867 fn test_ecr_enter_drills_into_repository() {
13868 let mut app = test_app();
13869 app.current_service = Service::EcrRepositories;
13870 app.service_selected = true;
13871 app.mode = Mode::Normal;
13872 app.ecr_state.repositories.items = vec![EcrRepository {
13873 name: "my-repo".to_string(),
13874 uri: "uri".to_string(),
13875 created_at: "2023-01-01".to_string(),
13876 tag_immutability: "MUTABLE".to_string(),
13877 encryption_type: "AES256".to_string(),
13878 }];
13879
13880 app.handle_action(Action::Select);
13881 assert_eq!(
13882 app.ecr_state.current_repository,
13883 Some("my-repo".to_string())
13884 );
13885 assert!(app.ecr_state.repositories.loading);
13886 }
13887
13888 #[test]
13889 fn test_ecr_repository_expansion() {
13890 let mut app = test_app();
13891 app.current_service = Service::EcrRepositories;
13892 app.service_selected = true;
13893 app.ecr_state.repositories.items = vec![EcrRepository {
13894 name: "my-repo".to_string(),
13895 uri: "uri".to_string(),
13896 created_at: "2023-01-01".to_string(),
13897 tag_immutability: "MUTABLE".to_string(),
13898 encryption_type: "AES256".to_string(),
13899 }];
13900 app.ecr_state.repositories.selected = 0;
13901
13902 assert_eq!(app.ecr_state.repositories.expanded_item, None);
13903
13904 app.handle_action(Action::NextPane);
13905 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
13906
13907 app.handle_action(Action::PrevPane);
13908 assert_eq!(app.ecr_state.repositories.expanded_item, None);
13909 }
13910
13911 #[test]
13912 fn test_ecr_ctrl_d_scrolls_down() {
13913 let mut app = test_app();
13914 app.current_service = Service::EcrRepositories;
13915 app.service_selected = true;
13916 app.mode = Mode::Normal;
13917 app.ecr_state.repositories.items = (0..30)
13918 .map(|i| EcrRepository {
13919 name: format!("repo{}", i),
13920 uri: format!("uri{}", i),
13921 created_at: "2023-01-01".to_string(),
13922 tag_immutability: "MUTABLE".to_string(),
13923 encryption_type: "AES256".to_string(),
13924 })
13925 .collect();
13926 app.ecr_state.repositories.selected = 0;
13927
13928 app.handle_action(Action::PageDown);
13929 assert_eq!(app.ecr_state.repositories.selected, 10);
13930 }
13931
13932 #[test]
13933 fn test_ecr_ctrl_u_scrolls_up() {
13934 let mut app = test_app();
13935 app.current_service = Service::EcrRepositories;
13936 app.service_selected = true;
13937 app.mode = Mode::Normal;
13938 app.ecr_state.repositories.items = (0..30)
13939 .map(|i| EcrRepository {
13940 name: format!("repo{}", i),
13941 uri: format!("uri{}", i),
13942 created_at: "2023-01-01".to_string(),
13943 tag_immutability: "MUTABLE".to_string(),
13944 encryption_type: "AES256".to_string(),
13945 })
13946 .collect();
13947 app.ecr_state.repositories.selected = 15;
13948
13949 app.handle_action(Action::PageUp);
13950 assert_eq!(app.ecr_state.repositories.selected, 5);
13951 }
13952
13953 #[test]
13954 fn test_ecr_images_ctrl_d_scrolls_down() {
13955 let mut app = test_app();
13956 app.current_service = Service::EcrRepositories;
13957 app.service_selected = true;
13958 app.mode = Mode::Normal;
13959 app.ecr_state.current_repository = Some("repo".to_string());
13960 app.ecr_state.images.items = (0..30)
13961 .map(|i| EcrImage {
13962 tag: format!("tag{}", i),
13963 artifact_type: "container".to_string(),
13964 pushed_at: "2023-01-01T12:00:00Z".to_string(),
13965 size_bytes: 104857600,
13966 uri: format!("uri{}", i),
13967 digest: format!("sha256:{}", i),
13968 last_pull_time: String::new(),
13969 })
13970 .collect();
13971 app.ecr_state.images.selected = 0;
13972
13973 app.handle_action(Action::PageDown);
13974 assert_eq!(app.ecr_state.images.selected, 10);
13975 }
13976
13977 #[test]
13978 fn test_ecr_esc_goes_back_from_images_to_repos() {
13979 let mut app = test_app();
13980 app.current_service = Service::EcrRepositories;
13981 app.service_selected = true;
13982 app.mode = Mode::Normal;
13983 app.ecr_state.current_repository = Some("my-repo".to_string());
13984 app.ecr_state.images.items = vec![EcrImage {
13985 tag: "latest".to_string(),
13986 artifact_type: "container".to_string(),
13987 pushed_at: "2023-01-01T12:00:00Z".to_string(),
13988 size_bytes: 104857600,
13989 uri: "uri".to_string(),
13990 digest: "sha256:abc".to_string(),
13991 last_pull_time: String::new(),
13992 }];
13993
13994 app.handle_action(Action::GoBack);
13995 assert_eq!(app.ecr_state.current_repository, None);
13996 assert!(app.ecr_state.images.items.is_empty());
13997 }
13998
13999 #[test]
14000 fn test_ecr_esc_collapses_expanded_image_first() {
14001 let mut app = test_app();
14002 app.current_service = Service::EcrRepositories;
14003 app.service_selected = true;
14004 app.mode = Mode::Normal;
14005 app.ecr_state.current_repository = Some("my-repo".to_string());
14006 app.ecr_state.images.expanded_item = Some(0);
14007
14008 app.handle_action(Action::GoBack);
14009 assert_eq!(app.ecr_state.images.expanded_item, None);
14010 assert_eq!(
14011 app.ecr_state.current_repository,
14012 Some("my-repo".to_string())
14013 );
14014 }
14015
14016 #[test]
14017 fn test_pagination_with_lowercase_p() {
14018 let mut app = test_app();
14019 app.current_service = Service::EcrRepositories;
14020 app.service_selected = true;
14021 app.mode = Mode::Normal;
14022 app.ecr_state.repositories.items = (0..100)
14023 .map(|i| EcrRepository {
14024 name: format!("repo{}", i),
14025 uri: format!("uri{}", i),
14026 created_at: "2023-01-01".to_string(),
14027 tag_immutability: "MUTABLE".to_string(),
14028 encryption_type: "AES256".to_string(),
14029 })
14030 .collect();
14031
14032 app.handle_action(Action::FilterInput('2'));
14034 assert_eq!(app.page_input, "2");
14035
14036 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
14040
14041 #[test]
14042 fn test_lowercase_p_without_number_opens_preferences() {
14043 let mut app = test_app();
14044 app.current_service = Service::EcrRepositories;
14045 app.service_selected = true;
14046 app.mode = Mode::Normal;
14047
14048 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
14050 }
14051
14052 #[test]
14053 fn test_ctrl_o_generates_correct_console_url() {
14054 let mut app = test_app();
14055 app.current_service = Service::EcrRepositories;
14056 app.service_selected = true;
14057 app.mode = Mode::Normal;
14058 app.config.account_id = "123456789012".to_string();
14059
14060 let url = app.get_console_url();
14062 assert!(url.contains("ecr/private-registry/repositories"));
14063 assert!(url.contains("region=us-east-1"));
14064
14065 app.ecr_state.current_repository = Some("my-repo".to_string());
14067 let url = app.get_console_url();
14068 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
14069 assert!(url.contains("region=us-east-1"));
14070 }
14071
14072 #[test]
14073 fn test_page_input_display_and_reset() {
14074 let mut app = test_app();
14075 app.current_service = Service::EcrRepositories;
14076 app.service_selected = true;
14077 app.mode = Mode::Normal;
14078 app.ecr_state.repositories.items = (0..100)
14079 .map(|i| EcrRepository {
14080 name: format!("repo{}", i),
14081 uri: format!("uri{}", i),
14082 created_at: "2023-01-01".to_string(),
14083 tag_immutability: "MUTABLE".to_string(),
14084 encryption_type: "AES256".to_string(),
14085 })
14086 .collect();
14087
14088 app.handle_action(Action::FilterInput('2'));
14090 assert_eq!(app.page_input, "2");
14091
14092 app.handle_action(Action::OpenColumnSelector);
14094 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
14097
14098 #[test]
14099 fn test_page_navigation_updates_scroll_offset_for_cfn() {
14100 let mut app = test_app();
14101 app.current_service = Service::CloudFormationStacks;
14102 app.service_selected = true;
14103 app.mode = Mode::Normal;
14104 app.cfn_state.table.items = (0..100)
14105 .map(|i| CfnStack {
14106 name: format!("stack-{}", i),
14107 stack_id: format!(
14108 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
14109 i
14110 ),
14111 status: "CREATE_COMPLETE".to_string(),
14112 created_time: "2023-01-01T00:00:00Z".to_string(),
14113 updated_time: "2023-01-01T00:00:00Z".to_string(),
14114 deleted_time: String::new(),
14115 drift_status: "IN_SYNC".to_string(),
14116 last_drift_check_time: String::new(),
14117 status_reason: String::new(),
14118 description: String::new(),
14119 detailed_status: String::new(),
14120 root_stack: String::new(),
14121 parent_stack: String::new(),
14122 termination_protection: false,
14123 iam_role: String::new(),
14124 tags: vec![],
14125 stack_policy: String::new(),
14126 rollback_monitoring_time: String::new(),
14127 rollback_alarms: vec![],
14128 notification_arns: vec![],
14129 })
14130 .collect();
14131
14132 app.handle_action(Action::FilterInput('2'));
14134 assert_eq!(app.page_input, "2");
14135
14136 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
14141 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
14143 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
14144
14145 let current_page = app.cfn_state.table.scroll_offset / page_size;
14147 assert_eq!(
14148 current_page, 1,
14149 "2p should go to page 2 (0-indexed as 1), not page 3"
14150 ); }
14152
14153 #[test]
14154 fn test_3p_goes_to_page_3_not_page_5() {
14155 let mut app = test_app();
14156 app.current_service = Service::CloudFormationStacks;
14157 app.service_selected = true;
14158 app.mode = Mode::Normal;
14159 app.cfn_state.table.items = (0..200)
14160 .map(|i| CfnStack {
14161 name: format!("stack-{}", i),
14162 stack_id: format!(
14163 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
14164 i
14165 ),
14166 status: "CREATE_COMPLETE".to_string(),
14167 created_time: "2023-01-01T00:00:00Z".to_string(),
14168 updated_time: "2023-01-01T00:00:00Z".to_string(),
14169 deleted_time: String::new(),
14170 drift_status: "IN_SYNC".to_string(),
14171 last_drift_check_time: String::new(),
14172 status_reason: String::new(),
14173 description: String::new(),
14174 detailed_status: String::new(),
14175 root_stack: String::new(),
14176 parent_stack: String::new(),
14177 termination_protection: false,
14178 iam_role: String::new(),
14179 tags: vec![],
14180 stack_policy: String::new(),
14181 rollback_monitoring_time: String::new(),
14182 rollback_alarms: vec![],
14183 notification_arns: vec![],
14184 })
14185 .collect();
14186
14187 app.handle_action(Action::FilterInput('3'));
14189 app.handle_action(Action::OpenColumnSelector);
14190
14191 let page_size = app.cfn_state.table.page_size.value();
14192 let current_page = app.cfn_state.table.scroll_offset / page_size;
14193 assert_eq!(
14194 current_page, 2,
14195 "3p should go to page 3 (0-indexed as 2), not page 5"
14196 );
14197 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
14198 }
14199
14200 #[test]
14201 fn test_log_streams_page_navigation_uses_correct_page_size() {
14202 let mut app = test_app();
14203 app.current_service = Service::CloudWatchLogGroups;
14204 app.view_mode = ViewMode::Detail;
14205 app.service_selected = true;
14206 app.mode = Mode::Normal;
14207 app.log_groups_state.log_streams = (0..100)
14208 .map(|i| LogStream {
14209 name: format!("stream-{}", i),
14210 creation_time: None,
14211 last_event_time: None,
14212 })
14213 .collect();
14214
14215 app.handle_action(Action::FilterInput('2'));
14217 app.handle_action(Action::OpenColumnSelector);
14218
14219 assert_eq!(app.log_groups_state.stream_current_page, 1);
14221 assert_eq!(app.log_groups_state.selected_stream, 0);
14222
14223 assert_eq!(
14225 app.log_groups_state.stream_current_page, 1,
14226 "2p should go to page 2 (0-indexed as 1), not page 3"
14227 );
14228 }
14229
14230 #[test]
14231 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
14232 let mut app = test_app();
14233 app.current_service = Service::EcrRepositories;
14234 app.service_selected = true;
14235 app.mode = Mode::Normal;
14236 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
14238 .map(|i| EcrRepository {
14239 name: format!("repo{}", i),
14240 uri: format!("uri{}", i),
14241 created_at: "2023-01-01".to_string(),
14242 tag_immutability: "MUTABLE".to_string(),
14243 encryption_type: "AES256".to_string(),
14244 })
14245 .collect();
14246
14247 app.handle_action(Action::FilterInput('3'));
14249 app.handle_action(Action::OpenColumnSelector);
14250
14251 assert_eq!(app.ecr_state.repositories.selected, 50);
14253
14254 let page_size = app.ecr_state.repositories.page_size.value();
14255 let current_page = app.ecr_state.repositories.selected / page_size;
14256 assert_eq!(
14257 current_page, 2,
14258 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
14259 );
14260 }
14261
14262 #[test]
14263 fn test_page_navigation_updates_scroll_offset_for_alarms() {
14264 let mut app = test_app();
14265 app.current_service = Service::CloudWatchAlarms;
14266 app.service_selected = true;
14267 app.mode = Mode::Normal;
14268 app.alarms_state.table.items = (0..100)
14269 .map(|i| Alarm {
14270 name: format!("alarm-{}", i),
14271 state: "OK".to_string(),
14272 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
14273 description: String::new(),
14274 metric_name: "CPUUtilization".to_string(),
14275 namespace: "AWS/EC2".to_string(),
14276 statistic: "Average".to_string(),
14277 period: 300,
14278 comparison_operator: "GreaterThanThreshold".to_string(),
14279 threshold: 80.0,
14280 actions_enabled: true,
14281 state_reason: String::new(),
14282 resource: String::new(),
14283 dimensions: String::new(),
14284 expression: String::new(),
14285 alarm_type: "MetricAlarm".to_string(),
14286 cross_account: String::new(),
14287 })
14288 .collect();
14289
14290 app.handle_action(Action::FilterInput('2'));
14292 app.handle_action(Action::OpenColumnSelector);
14293
14294 let page_size = app.alarms_state.table.page_size.value();
14296 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
14298 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
14299 }
14300
14301 #[test]
14302 fn test_ecr_pagination_with_65_repos() {
14303 let mut app = test_app();
14304 app.current_service = Service::EcrRepositories;
14305 app.service_selected = true;
14306 app.mode = Mode::Normal;
14307 app.ecr_state.repositories.items = (0..65)
14308 .map(|i| EcrRepository {
14309 name: format!("repo{:02}", i),
14310 uri: format!("uri{}", i),
14311 created_at: "2023-01-01".to_string(),
14312 tag_immutability: "MUTABLE".to_string(),
14313 encryption_type: "AES256".to_string(),
14314 })
14315 .collect();
14316
14317 assert_eq!(app.ecr_state.repositories.selected, 0);
14319 let page_size = 50;
14320 let current_page = app.ecr_state.repositories.selected / page_size;
14321 assert_eq!(current_page, 0);
14322
14323 app.handle_action(Action::FilterInput('2'));
14325 app.handle_action(Action::OpenColumnSelector);
14326 assert_eq!(app.ecr_state.repositories.selected, 50);
14327
14328 let current_page = app.ecr_state.repositories.selected / page_size;
14330 assert_eq!(current_page, 1);
14331 }
14332
14333 #[test]
14334 fn test_ecr_repos_input_focus_tab_cycling() {
14335 let mut app = test_app();
14336 app.current_service = Service::EcrRepositories;
14337 app.service_selected = true;
14338 app.mode = Mode::FilterInput;
14339 app.ecr_state.input_focus = InputFocus::Filter;
14340
14341 app.handle_action(Action::NextFilterFocus);
14343 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14344
14345 app.handle_action(Action::NextFilterFocus);
14347 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14348
14349 app.handle_action(Action::PrevFilterFocus);
14351 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14352
14353 app.handle_action(Action::PrevFilterFocus);
14355 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14356 }
14357
14358 #[test]
14359 fn test_ecr_images_column_toggle_not_off_by_one() {
14360 use crate::ecr::image::Column as ImageColumn;
14361 let mut app = test_app();
14362 app.current_service = Service::EcrRepositories;
14363 app.service_selected = true;
14364 app.mode = Mode::ColumnSelector;
14365 app.ecr_state.current_repository = Some("test-repo".to_string());
14366
14367 app.ecr_image_visible_column_ids = ImageColumn::ids();
14369 let initial_count = app.ecr_image_visible_column_ids.len();
14370
14371 app.column_selector_index = 0;
14373 app.handle_action(Action::ToggleColumn);
14374
14375 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
14377 assert!(!app
14378 .ecr_image_visible_column_ids
14379 .contains(&ImageColumn::Tag.id()));
14380
14381 app.handle_action(Action::ToggleColumn);
14383 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
14384 assert!(app
14385 .ecr_image_visible_column_ids
14386 .contains(&ImageColumn::Tag.id()));
14387 }
14388
14389 #[test]
14390 fn test_ecr_repos_column_toggle_works() {
14391 let mut app = test_app();
14392 app.current_service = Service::EcrRepositories;
14393 app.service_selected = true;
14394 app.mode = Mode::ColumnSelector;
14395 app.ecr_state.current_repository = None;
14396
14397 app.ecr_repo_visible_column_ids = EcrColumn::ids();
14399 let initial_count = app.ecr_repo_visible_column_ids.len();
14400
14401 app.column_selector_index = 1;
14403 app.handle_action(Action::ToggleColumn);
14404
14405 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
14407 assert!(!app
14408 .ecr_repo_visible_column_ids
14409 .contains(&EcrColumn::Name.id()));
14410
14411 app.handle_action(Action::ToggleColumn);
14413 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
14414 assert!(app
14415 .ecr_repo_visible_column_ids
14416 .contains(&EcrColumn::Name.id()));
14417 }
14418
14419 #[test]
14420 fn test_ecr_repos_pagination_left_right_navigation() {
14421 use crate::ecr::repo::Repository as EcrRepository;
14422 let mut app = test_app();
14423 app.current_service = Service::EcrRepositories;
14424 app.service_selected = true;
14425 app.mode = Mode::FilterInput;
14426 app.ecr_state.input_focus = InputFocus::Pagination;
14427
14428 app.ecr_state.repositories.items = (0..150)
14430 .map(|i| EcrRepository {
14431 name: format!("repo{:03}", i),
14432 uri: format!("uri{}", i),
14433 created_at: "2023-01-01".to_string(),
14434 tag_immutability: "MUTABLE".to_string(),
14435 encryption_type: "AES256".to_string(),
14436 })
14437 .collect();
14438
14439 app.ecr_state.repositories.selected = 0;
14441 eprintln!(
14442 "Initial: selected={}, focus={:?}, mode={:?}",
14443 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
14444 );
14445
14446 app.handle_action(Action::PageDown);
14448 eprintln!(
14449 "After PageDown: selected={}",
14450 app.ecr_state.repositories.selected
14451 );
14452 assert_eq!(app.ecr_state.repositories.selected, 50);
14453
14454 app.handle_action(Action::PageDown);
14456 eprintln!(
14457 "After 2nd PageDown: selected={}",
14458 app.ecr_state.repositories.selected
14459 );
14460 assert_eq!(app.ecr_state.repositories.selected, 100);
14461
14462 app.handle_action(Action::PageDown);
14464 eprintln!(
14465 "After 3rd PageDown: selected={}",
14466 app.ecr_state.repositories.selected
14467 );
14468 assert_eq!(app.ecr_state.repositories.selected, 100);
14469
14470 app.handle_action(Action::PageUp);
14472 eprintln!(
14473 "After PageUp: selected={}",
14474 app.ecr_state.repositories.selected
14475 );
14476 assert_eq!(app.ecr_state.repositories.selected, 50);
14477
14478 app.handle_action(Action::PageUp);
14480 eprintln!(
14481 "After 2nd PageUp: selected={}",
14482 app.ecr_state.repositories.selected
14483 );
14484 assert_eq!(app.ecr_state.repositories.selected, 0);
14485
14486 app.handle_action(Action::PageUp);
14488 eprintln!(
14489 "After 3rd PageUp: selected={}",
14490 app.ecr_state.repositories.selected
14491 );
14492 assert_eq!(app.ecr_state.repositories.selected, 0);
14493 }
14494
14495 #[test]
14496 fn test_ecr_repos_filter_input_when_input_focused() {
14497 use crate::ecr::repo::Repository as EcrRepository;
14498 let mut app = test_app();
14499 app.current_service = Service::EcrRepositories;
14500 app.service_selected = true;
14501 app.mode = Mode::FilterInput;
14502 app.ecr_state.input_focus = InputFocus::Filter;
14503
14504 app.ecr_state.repositories.items = vec![
14506 EcrRepository {
14507 name: "test-repo".to_string(),
14508 uri: "uri1".to_string(),
14509 created_at: "2023-01-01".to_string(),
14510 tag_immutability: "MUTABLE".to_string(),
14511 encryption_type: "AES256".to_string(),
14512 },
14513 EcrRepository {
14514 name: "prod-repo".to_string(),
14515 uri: "uri2".to_string(),
14516 created_at: "2023-01-01".to_string(),
14517 tag_immutability: "MUTABLE".to_string(),
14518 encryption_type: "AES256".to_string(),
14519 },
14520 ];
14521
14522 assert_eq!(app.ecr_state.repositories.filter, "");
14524 app.handle_action(Action::FilterInput('t'));
14525 assert_eq!(app.ecr_state.repositories.filter, "t");
14526 app.handle_action(Action::FilterInput('e'));
14527 assert_eq!(app.ecr_state.repositories.filter, "te");
14528 app.handle_action(Action::FilterInput('s'));
14529 assert_eq!(app.ecr_state.repositories.filter, "tes");
14530 app.handle_action(Action::FilterInput('t'));
14531 assert_eq!(app.ecr_state.repositories.filter, "test");
14532 }
14533
14534 #[test]
14535 fn test_ecr_repos_digit_input_when_pagination_focused() {
14536 use crate::ecr::repo::Repository as EcrRepository;
14537 let mut app = test_app();
14538 app.current_service = Service::EcrRepositories;
14539 app.service_selected = true;
14540 app.mode = Mode::FilterInput;
14541 app.ecr_state.input_focus = InputFocus::Pagination;
14542
14543 app.ecr_state.repositories.items = vec![EcrRepository {
14545 name: "test-repo".to_string(),
14546 uri: "uri1".to_string(),
14547 created_at: "2023-01-01".to_string(),
14548 tag_immutability: "MUTABLE".to_string(),
14549 encryption_type: "AES256".to_string(),
14550 }];
14551
14552 assert_eq!(app.ecr_state.repositories.filter, "");
14554 assert_eq!(app.page_input, "");
14555 app.handle_action(Action::FilterInput('2'));
14556 assert_eq!(app.ecr_state.repositories.filter, "");
14557 assert_eq!(app.page_input, "2");
14558
14559 app.handle_action(Action::FilterInput('a'));
14561 assert_eq!(app.ecr_state.repositories.filter, "");
14562 assert_eq!(app.page_input, "2");
14563 }
14564
14565 #[test]
14566 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
14567 use crate::ecr::repo::Repository as EcrRepository;
14568 let mut app = test_app();
14569 app.current_service = Service::EcrRepositories;
14570 app.service_selected = true;
14571 app.mode = Mode::FilterInput;
14572 app.ecr_state.input_focus = InputFocus::Filter;
14573
14574 app.ecr_state.repositories.items = (0..150)
14576 .map(|i| EcrRepository {
14577 name: format!("repo{:03}", i),
14578 uri: format!("uri{}", i),
14579 created_at: "2023-01-01".to_string(),
14580 tag_immutability: "MUTABLE".to_string(),
14581 encryption_type: "AES256".to_string(),
14582 })
14583 .collect();
14584
14585 app.ecr_state.repositories.selected = 0;
14587
14588 app.handle_action(Action::PageDown);
14590 assert_eq!(
14591 app.ecr_state.repositories.selected, 10,
14592 "Should scroll down by 10"
14593 );
14594
14595 app.handle_action(Action::PageUp);
14596 assert_eq!(
14597 app.ecr_state.repositories.selected, 0,
14598 "Should scroll back up"
14599 );
14600 }
14601
14602 #[test]
14603 fn test_ecr_repos_pagination_control_actually_works() {
14604 use crate::ecr::repo::Repository as EcrRepository;
14605
14606 let mut app = test_app();
14608 app.current_service = Service::EcrRepositories;
14609 app.service_selected = true;
14610 app.mode = Mode::FilterInput;
14611 app.ecr_state.current_repository = None;
14612 app.ecr_state.input_focus = InputFocus::Pagination;
14613
14614 app.ecr_state.repositories.items = (0..100)
14616 .map(|i| EcrRepository {
14617 name: format!("repo{:03}", i),
14618 uri: format!("uri{}", i),
14619 created_at: "2023-01-01".to_string(),
14620 tag_immutability: "MUTABLE".to_string(),
14621 encryption_type: "AES256".to_string(),
14622 })
14623 .collect();
14624
14625 app.ecr_state.repositories.selected = 0;
14626
14627 assert_eq!(app.mode, Mode::FilterInput);
14629 assert_eq!(app.current_service, Service::EcrRepositories);
14630 assert_eq!(app.ecr_state.current_repository, None);
14631 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14632
14633 app.handle_action(Action::PageDown);
14635 assert_eq!(
14636 app.ecr_state.repositories.selected, 50,
14637 "PageDown should move to page 2"
14638 );
14639
14640 app.handle_action(Action::PageUp);
14641 assert_eq!(
14642 app.ecr_state.repositories.selected, 0,
14643 "PageUp should move back to page 1"
14644 );
14645 }
14646
14647 #[test]
14648 fn test_ecr_repos_start_filter_resets_focus_to_input() {
14649 let mut app = test_app();
14650 app.current_service = Service::EcrRepositories;
14651 app.service_selected = true;
14652 app.mode = Mode::Normal;
14653 app.ecr_state.current_repository = None;
14654
14655 app.ecr_state.input_focus = InputFocus::Pagination;
14657
14658 app.handle_action(Action::StartFilter);
14660
14661 assert_eq!(app.mode, Mode::FilterInput);
14663 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14664 }
14665
14666 #[test]
14667 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
14668 use crate::ecr::repo::Repository as EcrRepository;
14669
14670 let mut app = test_app();
14671 app.current_service = Service::EcrRepositories;
14672 app.service_selected = true;
14673 app.mode = Mode::Normal;
14674 app.ecr_state.current_repository = None;
14675
14676 app.ecr_state.repositories.items = (0..100)
14678 .map(|i| EcrRepository {
14679 name: format!("repo{:03}", i),
14680 uri: format!("uri{}", i),
14681 created_at: "2023-01-01".to_string(),
14682 tag_immutability: "MUTABLE".to_string(),
14683 encryption_type: "AES256".to_string(),
14684 })
14685 .collect();
14686
14687 app.ecr_state.repositories.selected = 0;
14688
14689 app.handle_action(Action::StartFilter);
14691 assert_eq!(app.mode, Mode::FilterInput);
14692 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14693
14694 app.handle_action(Action::NextFilterFocus);
14696 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14697
14698 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
14700 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
14701 app.handle_action(Action::PageDown);
14702 eprintln!(
14703 "After PageDown: selected={}",
14704 app.ecr_state.repositories.selected
14705 );
14706
14707 assert_eq!(
14709 app.ecr_state.repositories.selected, 50,
14710 "Right arrow should move to page 2"
14711 );
14712
14713 app.handle_action(Action::PageUp);
14715 assert_eq!(
14716 app.ecr_state.repositories.selected, 0,
14717 "Left arrow should move back to page 1"
14718 );
14719 }
14720
14721 #[test]
14722 fn test_apig_filter_input() {
14723 let mut app = test_app();
14724 app.current_service = Service::ApiGatewayApis;
14725 app.service_selected = true;
14726 app.mode = Mode::FilterInput;
14727
14728 app.handle_action(Action::FilterInput('t'));
14729 app.handle_action(Action::FilterInput('e'));
14730 app.handle_action(Action::FilterInput('s'));
14731 app.handle_action(Action::FilterInput('t'));
14732 assert_eq!(app.apig_state.apis.filter, "test");
14733
14734 app.handle_action(Action::FilterBackspace);
14735 assert_eq!(app.apig_state.apis.filter, "tes");
14736 }
14737
14738 #[test]
14739 fn test_apig_start_filter_enters_filter_mode() {
14740 let mut app = test_app();
14741 app.current_service = Service::ApiGatewayApis;
14742 app.service_selected = true;
14743 app.mode = Mode::Normal;
14744
14745 app.handle_action(Action::StartFilter);
14746 assert_eq!(app.mode, Mode::FilterInput);
14747 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14748 }
14749
14750 #[test]
14751 fn test_apig_input_focus_cycles_with_tab() {
14752 let mut app = test_app();
14753 app.current_service = Service::ApiGatewayApis;
14754 app.service_selected = true;
14755 app.mode = Mode::FilterInput;
14756 app.apig_state.input_focus = InputFocus::Filter;
14757
14758 app.handle_action(Action::NextFilterFocus);
14760 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
14761
14762 app.handle_action(Action::NextFilterFocus);
14764 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14765 }
14766
14767 #[test]
14768 fn test_apig_input_focus_cycles_with_shift_tab() {
14769 let mut app = test_app();
14770 app.current_service = Service::ApiGatewayApis;
14771 app.service_selected = true;
14772 app.mode = Mode::FilterInput;
14773 app.apig_state.input_focus = InputFocus::Filter;
14774
14775 app.handle_action(Action::PrevFilterFocus);
14777 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
14778
14779 app.handle_action(Action::PrevFilterFocus);
14781 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14782 }
14783
14784 #[test]
14785 fn test_apig_exact_user_flow_i_tab_filter() {
14786 let mut app = test_app();
14787 app.current_service = Service::ApiGatewayApis;
14788 app.service_selected = true;
14789 app.mode = Mode::Normal;
14790
14791 app.apig_state.apis.items = vec![
14793 crate::apig::api::RestApi {
14794 id: "api1".to_string(),
14795 name: "test-api".to_string(),
14796 description: "Test API".to_string(),
14797 created_date: "2023-01-01".to_string(),
14798 api_key_source: "HEADER".to_string(),
14799 endpoint_configuration: "REGIONAL".to_string(),
14800 protocol_type: "REST".to_string(),
14801 disable_execute_api_endpoint: false,
14802 status: "AVAILABLE".to_string(),
14803 },
14804 crate::apig::api::RestApi {
14805 id: "api2".to_string(),
14806 name: "prod-api".to_string(),
14807 description: "Production API".to_string(),
14808 created_date: "2023-01-02".to_string(),
14809 api_key_source: "HEADER".to_string(),
14810 endpoint_configuration: "REGIONAL".to_string(),
14811 protocol_type: "REST".to_string(),
14812 disable_execute_api_endpoint: false,
14813 status: "AVAILABLE".to_string(),
14814 },
14815 ];
14816
14817 app.handle_action(Action::StartFilter);
14819 assert_eq!(app.mode, Mode::FilterInput);
14820 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14821
14822 app.handle_action(Action::FilterInput('t'));
14824 app.handle_action(Action::FilterInput('e'));
14825 app.handle_action(Action::FilterInput('s'));
14826 app.handle_action(Action::FilterInput('t'));
14827 assert_eq!(app.apig_state.apis.filter, "test");
14828
14829 app.handle_action(Action::NextFilterFocus);
14831 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
14832
14833 app.handle_action(Action::NextFilterFocus);
14835 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14836 }
14837
14838 #[test]
14839 fn test_apig_row_expansion() {
14840 let mut app = test_app();
14841 app.current_service = Service::ApiGatewayApis;
14842 app.service_selected = true;
14843 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
14844 id: "api1".to_string(),
14845 name: "test-api".to_string(),
14846 description: "Test API".to_string(),
14847 created_date: "2023-01-01".to_string(),
14848 api_key_source: "HEADER".to_string(),
14849 endpoint_configuration: "REGIONAL".to_string(),
14850 protocol_type: "REST".to_string(),
14851 disable_execute_api_endpoint: false,
14852 status: "AVAILABLE".to_string(),
14853 }];
14854 app.apig_state.apis.selected = 0;
14855
14856 assert_eq!(app.apig_state.apis.expanded_item, None);
14857
14858 app.handle_action(Action::NextPane);
14860 assert_eq!(app.apig_state.apis.expanded_item, None);
14861
14862 app.handle_action(Action::ExpandRow);
14864 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
14865
14866 app.handle_action(Action::NextPane);
14868 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
14869
14870 app.handle_action(Action::PrevPane);
14872 assert_eq!(app.apig_state.apis.expanded_item, None);
14873 }
14874
14875 #[test]
14876 fn test_apig_column_preferences() {
14877 let mut app = test_app();
14878 app.current_service = Service::ApiGatewayApis;
14879 app.service_selected = true;
14880
14881 let initial_count = app.apig_api_visible_column_ids.len();
14883 assert_eq!(initial_count, app.apig_api_column_ids.len());
14884
14885 app.handle_action(Action::OpenColumnSelector);
14887 assert_eq!(app.mode, Mode::ColumnSelector);
14888
14889 app.column_selector_index = 1;
14891 let first_col = app.apig_api_column_ids[0];
14892 assert!(app.apig_api_visible_column_ids.contains(&first_col));
14893
14894 app.handle_action(Action::ToggleColumn);
14895
14896 assert!(!app.apig_api_visible_column_ids.contains(&first_col));
14898 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
14899
14900 app.handle_action(Action::ToggleColumn);
14902 assert!(app.apig_api_visible_column_ids.contains(&first_col));
14903 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
14904 }
14905
14906 #[test]
14907 fn test_apig_page_size_preferences() {
14908 use crate::common::PageSize;
14909
14910 let mut app = test_app();
14911 app.current_service = Service::ApiGatewayApis;
14912 app.service_selected = true;
14913
14914 assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
14916
14917 app.handle_action(Action::OpenColumnSelector);
14919
14920 app.column_selector_index = app.apig_api_column_ids.len() + 3;
14925 app.handle_action(Action::ToggleColumn);
14926 assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
14927
14928 app.column_selector_index = app.apig_api_column_ids.len() + 6;
14930 app.handle_action(Action::ToggleColumn);
14931 assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
14932 }
14933
14934 #[test]
14935 fn test_apig_preferences_skip_blank_row() {
14936 let mut app = test_app();
14937 app.current_service = Service::ApiGatewayApis;
14938 app.service_selected = true;
14939 app.mode = Mode::ColumnSelector;
14940
14941 app.column_selector_index = 8;
14943
14944 app.handle_action(Action::NextItem);
14946 assert_eq!(app.column_selector_index, 10);
14947
14948 app.handle_action(Action::PrevItem);
14950 assert_eq!(app.column_selector_index, 8);
14951 }
14952
14953 #[test]
14954 fn test_apig_preferences_ctrl_d_skip_blank_row() {
14955 let mut app = test_app();
14956 app.current_service = Service::ApiGatewayApis;
14957 app.service_selected = true;
14958 app.mode = Mode::ColumnSelector;
14959
14960 app.column_selector_index = 0;
14962
14963 app.handle_action(Action::PageDown);
14965 assert_eq!(app.column_selector_index, 10);
14966
14967 app.handle_action(Action::PageUp);
14969 assert_eq!(app.column_selector_index, 0);
14970 }
14971
14972 #[test]
14973 fn test_apig_preferences_ctrl_u_skip_blank_row() {
14974 let mut app = test_app();
14975 app.current_service = Service::ApiGatewayApis;
14976 app.service_selected = true;
14977 app.mode = Mode::ColumnSelector;
14978
14979 app.column_selector_index = 10;
14981
14982 app.handle_action(Action::PageUp);
14984 assert_eq!(app.column_selector_index, 0);
14985
14986 app.column_selector_index = 14; app.handle_action(Action::PageUp);
14991 assert_eq!(app.column_selector_index, 4);
14992 }
14993
14994 #[test]
14995 fn test_apig_preferences_tab_cycles_sections() {
14996 let mut app = test_app();
14997 app.current_service = Service::ApiGatewayApis;
14998 app.service_selected = true;
14999 app.mode = Mode::ColumnSelector;
15000
15001 app.column_selector_index = 0;
15003
15004 app.handle_action(Action::NextPreferences);
15006 assert_eq!(app.column_selector_index, 10);
15007
15008 app.handle_action(Action::NextPreferences);
15010 assert_eq!(app.column_selector_index, 0);
15011 }
15012
15013 #[test]
15014 fn test_apig_preferences_shift_tab_cycles_sections() {
15015 let mut app = test_app();
15016 app.current_service = Service::ApiGatewayApis;
15017 app.service_selected = true;
15018 app.mode = Mode::ColumnSelector;
15019
15020 app.column_selector_index = 0;
15022
15023 app.handle_action(Action::PrevPreferences);
15025 assert_eq!(app.column_selector_index, 10);
15026
15027 app.handle_action(Action::PrevPreferences);
15029 assert_eq!(app.column_selector_index, 0);
15030 }
15031
15032 #[test]
15033 fn test_apig_arrow_navigation() {
15034 let mut app = test_app();
15035 app.current_service = Service::ApiGatewayApis;
15036 app.service_selected = true;
15037 app.mode = Mode::Normal; app.apig_state.apis.filter = String::new(); app.apig_state.apis.items = vec![
15040 crate::apig::api::RestApi {
15041 id: "api1".to_string(),
15042 name: "test-api-1".to_string(),
15043 description: "Test API 1".to_string(),
15044 created_date: "2023-01-01".to_string(),
15045 api_key_source: "HEADER".to_string(),
15046 endpoint_configuration: "REGIONAL".to_string(),
15047 protocol_type: "REST".to_string(),
15048 disable_execute_api_endpoint: false,
15049 status: "AVAILABLE".to_string(),
15050 },
15051 crate::apig::api::RestApi {
15052 id: "api2".to_string(),
15053 name: "test-api-2".to_string(),
15054 description: "Test API 2".to_string(),
15055 created_date: "2023-01-02".to_string(),
15056 api_key_source: "HEADER".to_string(),
15057 endpoint_configuration: "REGIONAL".to_string(),
15058 protocol_type: "HTTP".to_string(),
15059 disable_execute_api_endpoint: false,
15060 status: "AVAILABLE".to_string(),
15061 },
15062 ];
15063 app.apig_state.apis.selected = 0;
15064
15065 app.handle_action(Action::NextItem);
15067 assert_eq!(app.apig_state.apis.selected, 1);
15068
15069 app.handle_action(Action::PrevItem);
15071 assert_eq!(app.apig_state.apis.selected, 0);
15072 }
15073
15074 #[test]
15075 fn test_apig_collapse_row() {
15076 let mut app = test_app();
15077 app.current_service = Service::ApiGatewayApis;
15078 app.service_selected = true;
15079 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
15080 id: "api1".to_string(),
15081 name: "test-api".to_string(),
15082 description: "Test API".to_string(),
15083 created_date: "2023-01-01".to_string(),
15084 api_key_source: "HEADER".to_string(),
15085 endpoint_configuration: "REGIONAL".to_string(),
15086 protocol_type: "REST".to_string(),
15087 disable_execute_api_endpoint: false,
15088 status: "AVAILABLE".to_string(),
15089 }];
15090 app.apig_state.apis.selected = 0;
15091 app.apig_state.apis.expanded_item = Some(0);
15092
15093 app.handle_action(Action::CollapseRow);
15095 assert_eq!(app.apig_state.apis.expanded_item, None);
15096 }
15097
15098 #[test]
15099 fn test_apig_filter_resets_selection() {
15100 let mut app = test_app();
15101 app.current_service = Service::ApiGatewayApis;
15102 app.service_selected = true;
15103 app.mode = Mode::FilterInput;
15104 app.apig_state.input_focus = InputFocus::Filter;
15105 app.apig_state.apis.items = vec![
15106 crate::apig::api::RestApi {
15107 id: "api1".to_string(),
15108 name: "alpha-api".to_string(),
15109 description: "Alpha API".to_string(),
15110 created_date: "2023-01-01".to_string(),
15111 api_key_source: "HEADER".to_string(),
15112 endpoint_configuration: "REGIONAL".to_string(),
15113 protocol_type: "REST".to_string(),
15114 disable_execute_api_endpoint: false,
15115 status: "AVAILABLE".to_string(),
15116 },
15117 crate::apig::api::RestApi {
15118 id: "api2".to_string(),
15119 name: "beta-api".to_string(),
15120 description: "Beta API".to_string(),
15121 created_date: "2023-01-02".to_string(),
15122 api_key_source: "HEADER".to_string(),
15123 endpoint_configuration: "REGIONAL".to_string(),
15124 protocol_type: "HTTP".to_string(),
15125 disable_execute_api_endpoint: false,
15126 status: "AVAILABLE".to_string(),
15127 },
15128 ];
15129
15130 app.apig_state.apis.selected = 1;
15132 app.apig_state.apis.expanded_item = Some(1);
15133
15134 app.handle_action(Action::FilterInput('a'));
15136
15137 assert_eq!(app.apig_state.apis.selected, 0);
15139 assert_eq!(app.apig_state.apis.expanded_item, None);
15140 assert_eq!(app.apig_state.apis.filter, "a");
15141 }
15142
15143 #[test]
15144 fn test_service_picker_starts_in_normal_mode() {
15145 let app = test_app();
15146 assert_eq!(app.mode, Mode::ServicePicker);
15147 assert!(!app.service_picker.filter_active);
15148 }
15149
15150 #[test]
15151 fn test_service_picker_i_key_activates_filter() {
15152 let mut app = test_app();
15153
15154 assert_eq!(app.mode, Mode::ServicePicker);
15156 assert!(!app.service_picker.filter_active);
15157 assert!(app.service_picker.filter.is_empty());
15158
15159 app.handle_action(Action::EnterFilterMode);
15161
15162 assert_eq!(app.mode, Mode::ServicePicker);
15164 assert!(app.service_picker.filter_active);
15165 assert!(app.service_picker.filter.is_empty());
15166
15167 app.handle_action(Action::FilterInput('s'));
15169 assert_eq!(app.service_picker.filter, "s");
15170 }
15171
15172 #[test]
15173 fn test_service_picker_esc_exits_filter_mode() {
15174 let mut app = test_app();
15175 assert_eq!(app.mode, Mode::ServicePicker);
15176 assert!(!app.service_picker.filter_active);
15177
15178 app.handle_action(Action::EnterFilterMode);
15180 assert!(app.service_picker.filter_active);
15181 assert_eq!(app.mode, Mode::ServicePicker);
15182
15183 app.handle_action(Action::FilterInput('s'));
15185 assert_eq!(app.service_picker.filter, "s");
15186
15187 app.handle_action(Action::ExitFilterMode);
15189 assert!(!app.service_picker.filter_active);
15190 assert_eq!(app.mode, Mode::ServicePicker); app.handle_action(Action::ExitFilterMode);
15194 assert_eq!(app.mode, Mode::Normal);
15195 }
15196
15197 #[test]
15198 fn test_service_picker_navigation_only_works_in_normal_mode() {
15199 let mut app = test_app();
15200 app.service_picker.selected = 0;
15201
15202 assert!(!app.service_picker.filter_active);
15204 app.handle_action(Action::NextItem);
15205 assert_eq!(app.service_picker.selected, 1);
15206
15207 app.handle_action(Action::EnterFilterMode);
15209 assert!(app.service_picker.filter_active);
15210
15211 let prev_selected = app.service_picker.selected;
15213 app.handle_action(Action::NextItem);
15214 assert_eq!(app.service_picker.selected, prev_selected); app.handle_action(Action::ExitFilterMode);
15218 assert!(!app.service_picker.filter_active);
15219
15220 app.handle_action(Action::NextItem);
15222 assert_eq!(app.service_picker.selected, prev_selected + 1);
15223 }
15224
15225 #[test]
15226 fn test_service_picker_typing_filters_services() {
15227 let mut app = test_app();
15228
15229 assert_eq!(app.mode, Mode::ServicePicker);
15231 assert!(!app.service_picker.filter_active);
15232
15233 app.handle_action(Action::EnterFilterMode);
15235 assert!(app.service_picker.filter_active);
15236
15237 app.handle_action(Action::FilterInput('s'));
15239 app.handle_action(Action::FilterInput('3'));
15240
15241 assert_eq!(app.service_picker.filter, "s3");
15242 assert_eq!(app.mode, Mode::ServicePicker);
15243 }
15244
15245 #[test]
15246 fn test_service_picker_resets_on_open() {
15247 let mut app = test_app();
15248
15249 app.service_selected = true;
15251 app.mode = Mode::Normal;
15252
15253 app.service_picker.filter = "previous".to_string();
15255 app.service_picker.filter_active = true;
15256 app.service_picker.selected = 5;
15257
15258 app.handle_action(Action::OpenSpaceMenu);
15260
15261 assert_eq!(app.mode, Mode::SpaceMenu);
15263 assert!(app.service_picker.filter.is_empty());
15264 assert!(!app.service_picker.filter_active);
15265 assert_eq!(app.service_picker.selected, 0);
15266 }
15267
15268 #[test]
15269 fn test_no_pii_in_test_data() {
15270 let test_repo = EcrRepository {
15272 name: "test-repo".to_string(),
15273 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
15274 created_at: "2024-01-01".to_string(),
15275 tag_immutability: "MUTABLE".to_string(),
15276 encryption_type: "AES256".to_string(),
15277 };
15278
15279 assert!(test_repo.uri.starts_with("123456789012"));
15281 assert!(!test_repo.uri.contains("123456789013")); }
15283
15284 #[test]
15285 fn test_lambda_versions_tab_triggers_loading() {
15286 let mut app = test_app();
15287 app.current_service = Service::LambdaFunctions;
15288 app.service_selected = true;
15289
15290 app.lambda_state.current_function = Some("test-function".to_string());
15292 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15293
15294 assert!(app.lambda_state.version_table.items.is_empty());
15296
15297 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15299
15300 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
15303 assert!(app.lambda_state.current_function.is_some());
15304 }
15305
15306 #[test]
15307 fn test_lambda_versions_navigation() {
15308 let mut app = test_app();
15309 app.current_service = Service::LambdaFunctions;
15310 app.service_selected = true;
15311 app.lambda_state.current_function = Some("test-function".to_string());
15312 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15313
15314 app.lambda_state.version_table.items = vec![
15316 LambdaVersion {
15317 version: "3".to_string(),
15318 aliases: "prod".to_string(),
15319 description: "".to_string(),
15320 last_modified: "".to_string(),
15321 architecture: "X86_64".to_string(),
15322 },
15323 LambdaVersion {
15324 version: "2".to_string(),
15325 aliases: "".to_string(),
15326 description: "".to_string(),
15327 last_modified: "".to_string(),
15328 architecture: "X86_64".to_string(),
15329 },
15330 LambdaVersion {
15331 version: "1".to_string(),
15332 aliases: "".to_string(),
15333 description: "".to_string(),
15334 last_modified: "".to_string(),
15335 architecture: "X86_64".to_string(),
15336 },
15337 ];
15338
15339 assert_eq!(app.lambda_state.version_table.items.len(), 3);
15341 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
15342 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
15343
15344 app.lambda_state.version_table.selected = 1;
15346 assert_eq!(app.lambda_state.version_table.selected, 1);
15347 }
15348
15349 #[test]
15350 fn test_lambda_versions_with_aliases() {
15351 let version = LambdaVersion {
15352 version: "35".to_string(),
15353 aliases: "prod, staging".to_string(),
15354 description: "Production version".to_string(),
15355 last_modified: "2024-01-01".to_string(),
15356 architecture: "X86_64".to_string(),
15357 };
15358
15359 assert_eq!(version.aliases, "prod, staging");
15360 assert!(!version.aliases.is_empty());
15361 }
15362
15363 #[test]
15364 fn test_lambda_versions_expansion() {
15365 let mut app = test_app();
15366 app.current_service = Service::LambdaFunctions;
15367 app.service_selected = true;
15368 app.lambda_state.current_function = Some("test-function".to_string());
15369 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15370
15371 app.lambda_state.version_table.items = vec![
15373 LambdaVersion {
15374 version: "2".to_string(),
15375 aliases: "prod".to_string(),
15376 description: "Production".to_string(),
15377 last_modified: "2024-01-01".to_string(),
15378 architecture: "X86_64".to_string(),
15379 },
15380 LambdaVersion {
15381 version: "1".to_string(),
15382 aliases: "".to_string(),
15383 description: "".to_string(),
15384 last_modified: "2024-01-01".to_string(),
15385 architecture: "Arm64".to_string(),
15386 },
15387 ];
15388
15389 app.lambda_state.version_table.selected = 0;
15390
15391 app.lambda_state.version_table.expanded_item = Some(0);
15393 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
15394
15395 app.lambda_state.version_table.selected = 1;
15397 app.lambda_state.version_table.expanded_item = Some(1);
15398 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
15399 }
15400
15401 #[test]
15402 fn test_lambda_versions_page_navigation() {
15403 let mut app = test_app();
15404 app.current_service = Service::LambdaFunctions;
15405 app.service_selected = true;
15406 app.lambda_state.current_function = Some("test-function".to_string());
15407 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15408
15409 app.lambda_state.version_table.items = (1..=30)
15411 .map(|i| LambdaVersion {
15412 version: i.to_string(),
15413 aliases: "".to_string(),
15414 description: "".to_string(),
15415 last_modified: "".to_string(),
15416 architecture: "X86_64".to_string(),
15417 })
15418 .collect();
15419
15420 app.lambda_state.version_table.page_size = PageSize::Ten;
15421 app.lambda_state.version_table.selected = 0;
15422
15423 app.page_input = "2".to_string();
15425 app.handle_action(Action::OpenColumnSelector);
15426
15427 assert_eq!(app.lambda_state.version_table.selected, 10);
15429 }
15430
15431 #[test]
15432 fn test_lambda_versions_pagination_arrow_keys() {
15433 let mut app = test_app();
15434 app.current_service = Service::LambdaFunctions;
15435 app.service_selected = true;
15436 app.lambda_state.current_function = Some("test-function".to_string());
15437 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15438 app.mode = Mode::FilterInput;
15439 app.lambda_state.version_input_focus = InputFocus::Pagination;
15440
15441 app.lambda_state.version_table.items = (1..=30)
15443 .map(|i| LambdaVersion {
15444 version: i.to_string(),
15445 aliases: "".to_string(),
15446 description: "".to_string(),
15447 last_modified: "".to_string(),
15448 architecture: "X86_64".to_string(),
15449 })
15450 .collect();
15451
15452 app.lambda_state.version_table.page_size = PageSize::Ten;
15453 app.lambda_state.version_table.selected = 0;
15454
15455 app.handle_action(Action::PageDown);
15457 assert_eq!(app.lambda_state.version_table.selected, 10);
15458
15459 app.handle_action(Action::PageUp);
15461 assert_eq!(app.lambda_state.version_table.selected, 0);
15462 }
15463
15464 #[test]
15465 fn test_lambda_versions_page_input_in_filter_mode() {
15466 let mut app = test_app();
15467 app.current_service = Service::LambdaFunctions;
15468 app.service_selected = true;
15469 app.lambda_state.current_function = Some("test-function".to_string());
15470 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15471 app.mode = Mode::FilterInput;
15472 app.lambda_state.version_input_focus = InputFocus::Pagination;
15473
15474 app.lambda_state.version_table.items = (1..=30)
15476 .map(|i| LambdaVersion {
15477 version: i.to_string(),
15478 aliases: "".to_string(),
15479 description: "".to_string(),
15480 last_modified: "".to_string(),
15481 architecture: "X86_64".to_string(),
15482 })
15483 .collect();
15484
15485 app.lambda_state.version_table.page_size = PageSize::Ten;
15486 app.lambda_state.version_table.selected = 0;
15487
15488 app.handle_action(Action::FilterInput('2'));
15490 assert_eq!(app.page_input, "2");
15491 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
15495 assert_eq!(app.lambda_state.version_table.selected, 10);
15496 assert_eq!(app.page_input, ""); }
15498
15499 #[test]
15500 fn test_lambda_versions_filter_input() {
15501 let mut app = test_app();
15502 app.current_service = Service::LambdaFunctions;
15503 app.service_selected = true;
15504 app.lambda_state.current_function = Some("test-function".to_string());
15505 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15506 app.mode = Mode::FilterInput;
15507 app.lambda_state.version_input_focus = InputFocus::Filter;
15508
15509 app.lambda_state.version_table.items = vec![
15511 LambdaVersion {
15512 version: "1".to_string(),
15513 aliases: "prod".to_string(),
15514 description: "Production".to_string(),
15515 last_modified: "".to_string(),
15516 architecture: "X86_64".to_string(),
15517 },
15518 LambdaVersion {
15519 version: "2".to_string(),
15520 aliases: "staging".to_string(),
15521 description: "Staging".to_string(),
15522 last_modified: "".to_string(),
15523 architecture: "X86_64".to_string(),
15524 },
15525 ];
15526
15527 app.handle_action(Action::FilterInput('p'));
15529 app.handle_action(Action::FilterInput('r'));
15530 app.handle_action(Action::FilterInput('o'));
15531 app.handle_action(Action::FilterInput('d'));
15532 assert_eq!(app.lambda_state.version_table.filter, "prod");
15533
15534 app.handle_action(Action::FilterBackspace);
15536 assert_eq!(app.lambda_state.version_table.filter, "pro");
15537 }
15538
15539 #[test]
15540 fn test_lambda_aliases_table_expansion() {
15541 use crate::lambda::Alias;
15542
15543 let mut app = test_app();
15544 app.current_service = Service::LambdaFunctions;
15545 app.service_selected = true;
15546 app.lambda_state.current_function = Some("test-function".to_string());
15547 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
15548 app.mode = Mode::Normal;
15549
15550 app.lambda_state.alias_table.items = vec![
15551 Alias {
15552 name: "prod".to_string(),
15553 versions: "1".to_string(),
15554 description: "Production alias".to_string(),
15555 },
15556 Alias {
15557 name: "staging".to_string(),
15558 versions: "2".to_string(),
15559 description: "Staging alias".to_string(),
15560 },
15561 ];
15562
15563 app.lambda_state.alias_table.selected = 0;
15564
15565 app.handle_action(Action::Select);
15567 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
15568 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
15569
15570 app.handle_action(Action::GoBack);
15572 assert_eq!(app.lambda_state.current_alias, None);
15573 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
15574
15575 app.lambda_state.alias_table.selected = 1;
15577 app.handle_action(Action::Select);
15578 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
15579 }
15580
15581 #[test]
15582 fn test_lambda_versions_arrow_key_expansion() {
15583 let mut app = test_app();
15584 app.current_service = Service::LambdaFunctions;
15585 app.service_selected = true;
15586 app.lambda_state.current_function = Some("test-function".to_string());
15587 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15588 app.mode = Mode::Normal;
15589
15590 app.lambda_state.version_table.items = vec![LambdaVersion {
15591 version: "1".to_string(),
15592 aliases: "prod".to_string(),
15593 description: "Production".to_string(),
15594 last_modified: "2024-01-01".to_string(),
15595 architecture: "X86_64".to_string(),
15596 }];
15597
15598 app.lambda_state.version_table.selected = 0;
15599
15600 app.handle_action(Action::NextPane);
15602 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
15603
15604 app.handle_action(Action::PrevPane);
15606 assert_eq!(app.lambda_state.version_table.expanded_item, None);
15607 }
15608
15609 #[test]
15610 fn test_lambda_version_detail_view() {
15611 use crate::lambda::Function;
15612
15613 let mut app = test_app();
15614 app.current_service = Service::LambdaFunctions;
15615 app.service_selected = true;
15616 app.lambda_state.current_function = Some("test-function".to_string());
15617 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15618 app.mode = Mode::Normal;
15619
15620 app.lambda_state.table.items = vec![Function {
15621 name: "test-function".to_string(),
15622 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15623 application: None,
15624 description: "Test".to_string(),
15625 package_type: "Zip".to_string(),
15626 runtime: "python3.12".to_string(),
15627 architecture: "X86_64".to_string(),
15628 code_size: 1024,
15629 code_sha256: "hash".to_string(),
15630 memory_mb: 128,
15631 timeout_seconds: 30,
15632 last_modified: "2024-01-01".to_string(),
15633 layers: vec![],
15634 }];
15635
15636 app.lambda_state.version_table.items = vec![LambdaVersion {
15637 version: "1".to_string(),
15638 aliases: "prod".to_string(),
15639 description: "Production".to_string(),
15640 last_modified: "2024-01-01".to_string(),
15641 architecture: "X86_64".to_string(),
15642 }];
15643
15644 app.lambda_state.version_table.selected = 0;
15645
15646 app.handle_action(Action::Select);
15648 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
15649 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
15650
15651 app.handle_action(Action::GoBack);
15653 assert_eq!(app.lambda_state.current_version, None);
15654 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
15655 }
15656
15657 #[test]
15658 fn test_lambda_version_detail_tabs() {
15659 use crate::lambda::Function;
15660
15661 let mut app = test_app();
15662 app.current_service = Service::LambdaFunctions;
15663 app.service_selected = true;
15664 app.lambda_state.current_function = Some("test-function".to_string());
15665 app.lambda_state.current_version = Some("1".to_string());
15666 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15667 app.mode = Mode::Normal;
15668
15669 app.lambda_state.table.items = vec![Function {
15670 name: "test-function".to_string(),
15671 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15672 application: None,
15673 description: "Test".to_string(),
15674 package_type: "Zip".to_string(),
15675 runtime: "python3.12".to_string(),
15676 architecture: "X86_64".to_string(),
15677 code_size: 1024,
15678 code_sha256: "hash".to_string(),
15679 memory_mb: 128,
15680 timeout_seconds: 30,
15681 last_modified: "2024-01-01".to_string(),
15682 layers: vec![],
15683 }];
15684
15685 app.handle_action(Action::NextDetailTab);
15687 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
15688
15689 app.handle_action(Action::NextDetailTab);
15690 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
15691
15692 app.handle_action(Action::NextDetailTab);
15693 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
15694
15695 app.handle_action(Action::PrevDetailTab);
15697 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
15698
15699 app.handle_action(Action::PrevDetailTab);
15700 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
15701 }
15702
15703 #[test]
15704 fn test_lambda_aliases_arrow_key_expansion() {
15705 use crate::lambda::Alias;
15706
15707 let mut app = test_app();
15708 app.current_service = Service::LambdaFunctions;
15709 app.service_selected = true;
15710 app.lambda_state.current_function = Some("test-function".to_string());
15711 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
15712 app.mode = Mode::Normal;
15713
15714 app.lambda_state.alias_table.items = vec![Alias {
15715 name: "prod".to_string(),
15716 versions: "1".to_string(),
15717 description: "Production alias".to_string(),
15718 }];
15719
15720 app.lambda_state.alias_table.selected = 0;
15721
15722 app.handle_action(Action::NextPane);
15724 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
15725
15726 app.handle_action(Action::PrevPane);
15728 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
15729 }
15730
15731 #[test]
15732 fn test_lambda_functions_arrow_key_expansion() {
15733 use crate::lambda::Function;
15734
15735 let mut app = test_app();
15736 app.current_service = Service::LambdaFunctions;
15737 app.service_selected = true;
15738 app.mode = Mode::Normal;
15739
15740 app.lambda_state.table.items = vec![Function {
15741 name: "test-function".to_string(),
15742 arn: "arn".to_string(),
15743 application: None,
15744 description: "Test".to_string(),
15745 package_type: "Zip".to_string(),
15746 runtime: "python3.12".to_string(),
15747 architecture: "X86_64".to_string(),
15748 code_size: 1024,
15749 code_sha256: "hash".to_string(),
15750 memory_mb: 128,
15751 timeout_seconds: 30,
15752 last_modified: "2024-01-01".to_string(),
15753 layers: vec![],
15754 }];
15755
15756 app.lambda_state.table.selected = 0;
15757
15758 app.handle_action(Action::NextPane);
15760 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
15761
15762 app.handle_action(Action::PrevPane);
15764 assert_eq!(app.lambda_state.table.expanded_item, None);
15765 }
15766
15767 #[test]
15768 fn test_lambda_version_detail_with_application() {
15769 use crate::lambda::Function;
15770
15771 let mut app = test_app();
15772 app.current_service = Service::LambdaFunctions;
15773 app.service_selected = true;
15774 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
15775 app.lambda_state.current_version = Some("1".to_string());
15776 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15777 app.mode = Mode::Normal;
15778
15779 app.lambda_state.table.items = vec![Function {
15780 name: "storefront-studio-beta-api".to_string(),
15781 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
15782 .to_string(),
15783 application: Some("storefront-studio-beta".to_string()),
15784 description: "API function".to_string(),
15785 package_type: "Zip".to_string(),
15786 runtime: "python3.12".to_string(),
15787 architecture: "X86_64".to_string(),
15788 code_size: 1024,
15789 code_sha256: "hash".to_string(),
15790 memory_mb: 128,
15791 timeout_seconds: 30,
15792 last_modified: "2024-01-01".to_string(),
15793 layers: vec![],
15794 }];
15795
15796 assert_eq!(
15798 app.lambda_state.table.items[0].application,
15799 Some("storefront-studio-beta".to_string())
15800 );
15801 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
15802 }
15803
15804 #[test]
15805 fn test_lambda_layer_navigation() {
15806 use crate::lambda::{Function, Layer};
15807
15808 let mut app = test_app();
15809 app.current_service = Service::LambdaFunctions;
15810 app.service_selected = true;
15811 app.lambda_state.current_function = Some("test-function".to_string());
15812 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15813 app.mode = Mode::Normal;
15814
15815 app.lambda_state.table.items = vec![Function {
15816 name: "test-function".to_string(),
15817 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15818 application: None,
15819 description: "Test".to_string(),
15820 package_type: "Zip".to_string(),
15821 runtime: "python3.12".to_string(),
15822 architecture: "X86_64".to_string(),
15823 code_size: 1024,
15824 code_sha256: "hash".to_string(),
15825 memory_mb: 128,
15826 timeout_seconds: 30,
15827 last_modified: "2024-01-01".to_string(),
15828 layers: vec![
15829 Layer {
15830 merge_order: "1".to_string(),
15831 name: "layer1".to_string(),
15832 layer_version: "1".to_string(),
15833 compatible_runtimes: "python3.9".to_string(),
15834 compatible_architectures: "x86_64".to_string(),
15835 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
15836 },
15837 Layer {
15838 merge_order: "2".to_string(),
15839 name: "layer2".to_string(),
15840 layer_version: "2".to_string(),
15841 compatible_runtimes: "python3.9".to_string(),
15842 compatible_architectures: "x86_64".to_string(),
15843 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
15844 },
15845 Layer {
15846 merge_order: "3".to_string(),
15847 name: "layer3".to_string(),
15848 layer_version: "3".to_string(),
15849 compatible_runtimes: "python3.9".to_string(),
15850 compatible_architectures: "x86_64".to_string(),
15851 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
15852 },
15853 ],
15854 }];
15855
15856 assert_eq!(app.lambda_state.layer_selected, 0);
15857
15858 app.handle_action(Action::NextItem);
15859 assert_eq!(app.lambda_state.layer_selected, 1);
15860
15861 app.handle_action(Action::NextItem);
15862 assert_eq!(app.lambda_state.layer_selected, 2);
15863
15864 app.handle_action(Action::NextItem);
15865 assert_eq!(app.lambda_state.layer_selected, 2);
15866
15867 app.handle_action(Action::PrevItem);
15868 assert_eq!(app.lambda_state.layer_selected, 1);
15869
15870 app.handle_action(Action::PrevItem);
15871 assert_eq!(app.lambda_state.layer_selected, 0);
15872
15873 app.handle_action(Action::PrevItem);
15874 assert_eq!(app.lambda_state.layer_selected, 0);
15875 }
15876
15877 #[test]
15878 fn test_lambda_layer_expansion() {
15879 use crate::lambda::{Function, Layer};
15880
15881 let mut app = test_app();
15882 app.current_service = Service::LambdaFunctions;
15883 app.service_selected = true;
15884 app.lambda_state.current_function = Some("test-function".to_string());
15885 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15886 app.mode = Mode::Normal;
15887
15888 app.lambda_state.table.items = vec![Function {
15889 name: "test-function".to_string(),
15890 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15891 application: None,
15892 description: "Test".to_string(),
15893 package_type: "Zip".to_string(),
15894 runtime: "python3.12".to_string(),
15895 architecture: "X86_64".to_string(),
15896 code_size: 1024,
15897 code_sha256: "hash".to_string(),
15898 memory_mb: 128,
15899 timeout_seconds: 30,
15900 last_modified: "2024-01-01".to_string(),
15901 layers: vec![Layer {
15902 merge_order: "1".to_string(),
15903 name: "test-layer".to_string(),
15904 layer_version: "1".to_string(),
15905 compatible_runtimes: "python3.9".to_string(),
15906 compatible_architectures: "x86_64".to_string(),
15907 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
15908 }],
15909 }];
15910
15911 assert_eq!(app.lambda_state.layer_expanded, None);
15912
15913 app.handle_action(Action::NextPane);
15914 assert_eq!(app.lambda_state.layer_expanded, Some(0));
15915
15916 app.handle_action(Action::PrevPane);
15917 assert_eq!(app.lambda_state.layer_expanded, None);
15918
15919 app.handle_action(Action::NextPane);
15920 assert_eq!(app.lambda_state.layer_expanded, Some(0));
15921
15922 app.handle_action(Action::NextPane);
15923 assert_eq!(app.lambda_state.layer_expanded, None);
15924 }
15925
15926 #[test]
15927 fn test_lambda_layer_selection_and_expansion_workflow() {
15928 use crate::lambda::{Function, Layer};
15929
15930 let mut app = test_app();
15931 app.current_service = Service::LambdaFunctions;
15932 app.service_selected = true;
15933 app.lambda_state.current_function = Some("test-function".to_string());
15934 app.lambda_state.detail_tab = LambdaDetailTab::Code;
15935 app.mode = Mode::Normal;
15936
15937 app.lambda_state.table.items = vec![Function {
15938 name: "test-function".to_string(),
15939 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15940 application: None,
15941 description: "Test".to_string(),
15942 package_type: "Zip".to_string(),
15943 runtime: "python3.12".to_string(),
15944 architecture: "X86_64".to_string(),
15945 code_size: 1024,
15946 code_sha256: "hash".to_string(),
15947 memory_mb: 128,
15948 timeout_seconds: 30,
15949 last_modified: "2024-01-01".to_string(),
15950 layers: vec![
15951 Layer {
15952 merge_order: "1".to_string(),
15953 name: "layer1".to_string(),
15954 layer_version: "1".to_string(),
15955 compatible_runtimes: "python3.9".to_string(),
15956 compatible_architectures: "x86_64".to_string(),
15957 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
15958 },
15959 Layer {
15960 merge_order: "2".to_string(),
15961 name: "layer2".to_string(),
15962 layer_version: "2".to_string(),
15963 compatible_runtimes: "python3.9".to_string(),
15964 compatible_architectures: "x86_64".to_string(),
15965 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
15966 },
15967 ],
15968 }];
15969
15970 assert_eq!(app.lambda_state.layer_selected, 0);
15972 assert_eq!(app.lambda_state.layer_expanded, None);
15973
15974 app.handle_action(Action::NextPane);
15976 assert_eq!(app.lambda_state.layer_selected, 0);
15977 assert_eq!(app.lambda_state.layer_expanded, Some(0));
15978
15979 app.handle_action(Action::NextItem);
15981 assert_eq!(app.lambda_state.layer_selected, 1);
15982 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
15986 assert_eq!(app.lambda_state.layer_selected, 1);
15987 assert_eq!(app.lambda_state.layer_expanded, Some(1));
15988
15989 app.handle_action(Action::PrevPane);
15991 assert_eq!(app.lambda_state.layer_selected, 1);
15992 assert_eq!(app.lambda_state.layer_expanded, None);
15993
15994 app.handle_action(Action::PrevItem);
15996 assert_eq!(app.lambda_state.layer_selected, 0);
15997 assert_eq!(app.lambda_state.layer_expanded, None);
15998 }
15999
16000 #[test]
16001 fn test_backtab_cycles_detail_tabs_backward() {
16002 let mut app = test_app();
16003 app.mode = Mode::Normal;
16004
16005 app.current_service = Service::LambdaFunctions;
16007 app.service_selected = true;
16008 app.lambda_state.current_function = Some("test-function".to_string());
16009 app.lambda_state.detail_tab = LambdaDetailTab::Code;
16010
16011 app.handle_action(Action::PrevDetailTab);
16012 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
16013
16014 app.handle_action(Action::PrevDetailTab);
16015 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
16016
16017 app.current_service = Service::IamRoles;
16019 app.iam_state.current_role = Some("test-role".to_string());
16020 app.iam_state.role_tab = RoleTab::Permissions;
16021
16022 app.handle_action(Action::PrevDetailTab);
16023 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
16024
16025 app.current_service = Service::IamUsers;
16027 app.iam_state.current_user = Some("test-user".to_string());
16028 app.iam_state.user_tab = UserTab::Permissions;
16029
16030 app.handle_action(Action::PrevDetailTab);
16031 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
16032
16033 app.current_service = Service::IamUserGroups;
16035 app.iam_state.current_group = Some("test-group".to_string());
16036 app.iam_state.group_tab = GroupTab::Permissions;
16037
16038 app.handle_action(Action::PrevDetailTab);
16039 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
16040
16041 app.current_service = Service::S3Buckets;
16043 app.s3_state.current_bucket = Some("test-bucket".to_string());
16044 app.s3_state.object_tab = S3ObjectTab::Properties;
16045
16046 app.handle_action(Action::PrevDetailTab);
16047 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
16048
16049 app.current_service = Service::EcrRepositories;
16051 app.ecr_state.current_repository = None;
16052 app.ecr_state.tab = EcrTab::Private;
16053
16054 app.handle_action(Action::PrevDetailTab);
16055 assert_eq!(app.ecr_state.tab, EcrTab::Public);
16056
16057 app.current_service = Service::CloudFormationStacks;
16059 app.cfn_state.current_stack = Some("test-stack".to_string());
16060 app.cfn_state.detail_tab = CfnDetailTab::Resources;
16061 }
16062
16063 #[test]
16064 fn test_cloudformation_status_filter_active() {
16065 let filter = CfnStatusFilter::Active;
16066 assert!(filter.matches("CREATE_IN_PROGRESS"));
16067 assert!(filter.matches("UPDATE_IN_PROGRESS"));
16068 assert!(!filter.matches("CREATE_COMPLETE"));
16069 assert!(!filter.matches("DELETE_COMPLETE"));
16070 assert!(!filter.matches("CREATE_FAILED"));
16071 }
16072
16073 #[test]
16074 fn test_cloudformation_status_filter_complete() {
16075 let filter = CfnStatusFilter::Complete;
16076 assert!(filter.matches("CREATE_COMPLETE"));
16077 assert!(filter.matches("UPDATE_COMPLETE"));
16078 assert!(!filter.matches("DELETE_COMPLETE"));
16079 assert!(!filter.matches("CREATE_IN_PROGRESS"));
16080 }
16081
16082 #[test]
16083 fn test_cloudformation_status_filter_failed() {
16084 let filter = CfnStatusFilter::Failed;
16085 assert!(filter.matches("CREATE_FAILED"));
16086 assert!(filter.matches("UPDATE_FAILED"));
16087 assert!(!filter.matches("CREATE_COMPLETE"));
16088 }
16089
16090 #[test]
16091 fn test_cloudformation_status_filter_deleted() {
16092 let filter = CfnStatusFilter::Deleted;
16093 assert!(filter.matches("DELETE_COMPLETE"));
16094 assert!(filter.matches("DELETE_IN_PROGRESS"));
16095 assert!(!filter.matches("CREATE_COMPLETE"));
16096 }
16097
16098 #[test]
16099 fn test_cloudformation_status_filter_in_progress() {
16100 let filter = CfnStatusFilter::InProgress;
16101 assert!(filter.matches("CREATE_IN_PROGRESS"));
16102 assert!(filter.matches("UPDATE_IN_PROGRESS"));
16103 assert!(filter.matches("DELETE_IN_PROGRESS"));
16104 assert!(!filter.matches("CREATE_COMPLETE"));
16105 }
16106
16107 #[test]
16108 fn test_cloudformation_status_filter_cycle() {
16109 let filter = CfnStatusFilter::All;
16110 assert_eq!(filter.next(), CfnStatusFilter::Active);
16111 assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
16112 assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
16113 assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
16114 assert_eq!(
16115 filter.next().next().next().next().next(),
16116 CfnStatusFilter::InProgress
16117 );
16118 assert_eq!(
16119 filter.next().next().next().next().next().next(),
16120 CfnStatusFilter::All
16121 );
16122 }
16123
16124 #[test]
16125 fn test_cloudformation_default_columns() {
16126 let app = test_app();
16127 assert_eq!(app.cfn_visible_column_ids.len(), 4);
16128 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
16129 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
16130 assert!(app
16131 .cfn_visible_column_ids
16132 .contains(&CfnColumn::CreatedTime.id()));
16133 assert!(app
16134 .cfn_visible_column_ids
16135 .contains(&CfnColumn::Description.id()));
16136 }
16137
16138 #[test]
16139 fn test_cloudformation_all_columns() {
16140 let app = test_app();
16141 assert_eq!(app.cfn_column_ids.len(), 10);
16142 }
16143
16144 #[test]
16145 fn test_cloudformation_filter_by_name() {
16146 let mut app = test_app();
16147 app.cfn_state.status_filter = CfnStatusFilter::Complete;
16148 app.cfn_state.table.items = vec![
16149 CfnStack {
16150 name: "my-stack".to_string(),
16151 stack_id: "id1".to_string(),
16152 status: "CREATE_COMPLETE".to_string(),
16153 created_time: "2024-01-01".to_string(),
16154 updated_time: String::new(),
16155 deleted_time: String::new(),
16156 drift_status: String::new(),
16157 last_drift_check_time: String::new(),
16158 status_reason: String::new(),
16159 description: String::new(),
16160 detailed_status: String::new(),
16161 root_stack: String::new(),
16162 parent_stack: String::new(),
16163 termination_protection: false,
16164 iam_role: String::new(),
16165 tags: Vec::new(),
16166 stack_policy: String::new(),
16167 rollback_monitoring_time: String::new(),
16168 rollback_alarms: Vec::new(),
16169 notification_arns: Vec::new(),
16170 },
16171 CfnStack {
16172 name: "other-stack".to_string(),
16173 stack_id: "id2".to_string(),
16174 status: "CREATE_COMPLETE".to_string(),
16175 created_time: "2024-01-02".to_string(),
16176 updated_time: String::new(),
16177 deleted_time: String::new(),
16178 drift_status: String::new(),
16179 last_drift_check_time: String::new(),
16180 status_reason: String::new(),
16181 description: String::new(),
16182 detailed_status: String::new(),
16183 root_stack: String::new(),
16184 parent_stack: String::new(),
16185 termination_protection: false,
16186 iam_role: String::new(),
16187 tags: Vec::new(),
16188 stack_policy: String::new(),
16189 rollback_monitoring_time: String::new(),
16190 rollback_alarms: Vec::new(),
16191 notification_arns: Vec::new(),
16192 },
16193 ];
16194
16195 app.cfn_state.table.filter = "my".to_string();
16196 let filtered = filtered_cloudformation_stacks(&app);
16197 assert_eq!(filtered.len(), 1);
16198 assert_eq!(filtered[0].name, "my-stack");
16199 }
16200
16201 #[test]
16202 fn test_cloudformation_filter_by_description() {
16203 let mut app = test_app();
16204 app.cfn_state.status_filter = CfnStatusFilter::Complete;
16205 app.cfn_state.table.items = vec![CfnStack {
16206 name: "stack1".to_string(),
16207 stack_id: "id1".to_string(),
16208 status: "CREATE_COMPLETE".to_string(),
16209 created_time: "2024-01-01".to_string(),
16210 updated_time: String::new(),
16211 deleted_time: String::new(),
16212 drift_status: String::new(),
16213 last_drift_check_time: String::new(),
16214 status_reason: String::new(),
16215 description: "production stack".to_string(),
16216 detailed_status: String::new(),
16217 root_stack: String::new(),
16218 parent_stack: String::new(),
16219 termination_protection: false,
16220 iam_role: String::new(),
16221 tags: Vec::new(),
16222 stack_policy: String::new(),
16223 rollback_monitoring_time: String::new(),
16224 rollback_alarms: Vec::new(),
16225 notification_arns: Vec::new(),
16226 }];
16227
16228 app.cfn_state.table.filter = "production".to_string();
16229 let filtered = filtered_cloudformation_stacks(&app);
16230 assert_eq!(filtered.len(), 1);
16231 }
16232
16233 #[test]
16234 fn test_cloudformation_status_filter_applied() {
16235 let mut app = test_app();
16236 app.cfn_state.table.items = vec![
16237 CfnStack {
16238 name: "complete-stack".to_string(),
16239 stack_id: "id1".to_string(),
16240 status: "CREATE_COMPLETE".to_string(),
16241 created_time: "2024-01-01".to_string(),
16242 updated_time: String::new(),
16243 deleted_time: String::new(),
16244 drift_status: String::new(),
16245 last_drift_check_time: String::new(),
16246 status_reason: String::new(),
16247 description: String::new(),
16248 detailed_status: String::new(),
16249 root_stack: String::new(),
16250 parent_stack: String::new(),
16251 termination_protection: false,
16252 iam_role: String::new(),
16253 tags: Vec::new(),
16254 stack_policy: String::new(),
16255 rollback_monitoring_time: String::new(),
16256 rollback_alarms: Vec::new(),
16257 notification_arns: Vec::new(),
16258 },
16259 CfnStack {
16260 name: "failed-stack".to_string(),
16261 stack_id: "id2".to_string(),
16262 status: "CREATE_FAILED".to_string(),
16263 created_time: "2024-01-02".to_string(),
16264 updated_time: String::new(),
16265 deleted_time: String::new(),
16266 drift_status: String::new(),
16267 last_drift_check_time: String::new(),
16268 status_reason: String::new(),
16269 description: String::new(),
16270 detailed_status: String::new(),
16271 root_stack: String::new(),
16272 parent_stack: String::new(),
16273 termination_protection: false,
16274 iam_role: String::new(),
16275 tags: Vec::new(),
16276 stack_policy: String::new(),
16277 rollback_monitoring_time: String::new(),
16278 rollback_alarms: Vec::new(),
16279 notification_arns: Vec::new(),
16280 },
16281 ];
16282
16283 app.cfn_state.status_filter = CfnStatusFilter::Complete;
16284 let filtered = filtered_cloudformation_stacks(&app);
16285 assert_eq!(filtered.len(), 1);
16286 assert_eq!(filtered[0].name, "complete-stack");
16287
16288 app.cfn_state.status_filter = CfnStatusFilter::Failed;
16289 let filtered = filtered_cloudformation_stacks(&app);
16290 assert_eq!(filtered.len(), 1);
16291 assert_eq!(filtered[0].name, "failed-stack");
16292 }
16293
16294 #[test]
16295 fn test_cloudformation_default_page_size() {
16296 let app = test_app();
16297 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
16298 }
16299
16300 #[test]
16301 fn test_cloudformation_default_status_filter() {
16302 let app = test_app();
16303 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
16304 }
16305
16306 #[test]
16307 fn test_cloudformation_view_nested_default_false() {
16308 let app = test_app();
16309 assert!(!app.cfn_state.view_nested);
16310 }
16311
16312 #[test]
16313 fn test_cloudformation_pagination_hotkeys() {
16314 let mut app = test_app();
16315 app.current_service = Service::CloudFormationStacks;
16316 app.service_selected = true;
16317 app.cfn_state.status_filter = CfnStatusFilter::All;
16318
16319 for i in 0..150 {
16321 app.cfn_state.table.items.push(CfnStack {
16322 name: format!("stack-{}", i),
16323 stack_id: format!("id-{}", i),
16324 status: "CREATE_COMPLETE".to_string(),
16325 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16326 updated_time: String::new(),
16327 deleted_time: String::new(),
16328 drift_status: String::new(),
16329 last_drift_check_time: String::new(),
16330 status_reason: String::new(),
16331 description: String::new(),
16332 detailed_status: String::new(),
16333 root_stack: String::new(),
16334 parent_stack: String::new(),
16335 termination_protection: false,
16336 iam_role: String::new(),
16337 tags: vec![],
16338 stack_policy: String::new(),
16339 rollback_monitoring_time: String::new(),
16340 rollback_alarms: vec![],
16341 notification_arns: vec![],
16342 });
16343 }
16344
16345 app.go_to_page(2);
16347 assert_eq!(app.cfn_state.table.selected, 50);
16348
16349 app.go_to_page(3);
16351 assert_eq!(app.cfn_state.table.selected, 100);
16352
16353 app.go_to_page(1);
16355 assert_eq!(app.cfn_state.table.selected, 0);
16356 }
16357
16358 #[test]
16359 fn test_cloudformation_tab_cycling_in_filter_mode() {
16360 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
16361 let mut app = test_app();
16362 app.current_service = Service::CloudFormationStacks;
16363 app.service_selected = true;
16364 app.mode = Mode::FilterInput;
16365 app.cfn_state.input_focus = InputFocus::Filter;
16366
16367 app.handle_action(Action::NextFilterFocus);
16369 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
16370
16371 app.handle_action(Action::NextFilterFocus);
16373 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
16374
16375 app.handle_action(Action::NextFilterFocus);
16377 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16378
16379 app.handle_action(Action::NextFilterFocus);
16381 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16382 }
16383
16384 #[test]
16385 fn test_cloudformation_timestamp_format_includes_utc() {
16386 let stack = CfnStack {
16387 name: "test-stack".to_string(),
16388 stack_id: "id-123".to_string(),
16389 status: "CREATE_COMPLETE".to_string(),
16390 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
16391 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
16392 deleted_time: String::new(),
16393 drift_status: String::new(),
16394 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
16395 status_reason: String::new(),
16396 description: String::new(),
16397 detailed_status: String::new(),
16398 root_stack: String::new(),
16399 parent_stack: String::new(),
16400 termination_protection: false,
16401 iam_role: String::new(),
16402 tags: vec![],
16403 stack_policy: String::new(),
16404 rollback_monitoring_time: String::new(),
16405 rollback_alarms: vec![],
16406 notification_arns: vec![],
16407 };
16408
16409 assert!(stack.created_time.contains("(UTC)"));
16410 assert!(stack.updated_time.contains("(UTC)"));
16411 assert!(stack.last_drift_check_time.contains("(UTC)"));
16412 assert_eq!(stack.created_time.len(), 25);
16413 }
16414
16415 #[test]
16416 fn test_cloudformation_enter_drills_into_stack_view() {
16417 let mut app = test_app();
16418 app.current_service = Service::CloudFormationStacks;
16419 app.service_selected = true;
16420 app.mode = Mode::Normal;
16421 app.cfn_state.status_filter = CfnStatusFilter::All;
16422 app.tabs = vec![Tab {
16423 service: Service::CloudFormationStacks,
16424 title: "CloudFormation › Stacks".to_string(),
16425 breadcrumb: "CloudFormation › Stacks".to_string(),
16426 }];
16427 app.current_tab = 0;
16428
16429 app.cfn_state.table.items.push(CfnStack {
16430 name: "test-stack".to_string(),
16431 stack_id: "id-123".to_string(),
16432 status: "CREATE_COMPLETE".to_string(),
16433 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16434 updated_time: String::new(),
16435 deleted_time: String::new(),
16436 drift_status: String::new(),
16437 last_drift_check_time: String::new(),
16438 status_reason: String::new(),
16439 description: String::new(),
16440 detailed_status: String::new(),
16441 root_stack: String::new(),
16442 parent_stack: String::new(),
16443 termination_protection: false,
16444 iam_role: String::new(),
16445 tags: vec![],
16446 stack_policy: String::new(),
16447 rollback_monitoring_time: String::new(),
16448 rollback_alarms: vec![],
16449 notification_arns: vec![],
16450 });
16451
16452 app.cfn_state.table.reset();
16453 assert_eq!(app.cfn_state.current_stack, None);
16454
16455 app.handle_action(Action::Select);
16457 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
16458 }
16459
16460 #[test]
16461 fn test_cloudformation_arrow_keys_expand_collapse() {
16462 let mut app = test_app();
16463 app.current_service = Service::CloudFormationStacks;
16464 app.service_selected = true;
16465 app.mode = Mode::Normal;
16466 app.cfn_state.status_filter = CfnStatusFilter::All;
16467
16468 app.cfn_state.table.items.push(CfnStack {
16469 name: "test-stack".to_string(),
16470 stack_id: "id-123".to_string(),
16471 status: "CREATE_COMPLETE".to_string(),
16472 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16473 updated_time: String::new(),
16474 deleted_time: String::new(),
16475 drift_status: String::new(),
16476 last_drift_check_time: String::new(),
16477 status_reason: String::new(),
16478 description: String::new(),
16479 detailed_status: String::new(),
16480 root_stack: String::new(),
16481 parent_stack: String::new(),
16482 termination_protection: false,
16483 iam_role: String::new(),
16484 tags: vec![],
16485 stack_policy: String::new(),
16486 rollback_monitoring_time: String::new(),
16487 rollback_alarms: vec![],
16488 notification_arns: vec![],
16489 });
16490
16491 app.cfn_state.table.reset();
16492 assert_eq!(app.cfn_state.table.expanded_item, None);
16493
16494 app.handle_action(Action::NextPane);
16496 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
16497
16498 app.handle_action(Action::PrevPane);
16500 assert_eq!(app.cfn_state.table.expanded_item, None);
16501
16502 assert_eq!(app.cfn_state.current_stack, None);
16504 }
16505
16506 #[test]
16507 fn test_cloudformation_tab_cycling() {
16508 use crate::ui::cfn::DetailTab;
16509 let mut app = test_app();
16510 app.current_service = Service::CloudFormationStacks;
16511 app.service_selected = true;
16512 app.mode = Mode::Normal;
16513 app.cfn_state.status_filter = CfnStatusFilter::All;
16514 app.cfn_state.current_stack = Some("test-stack".to_string());
16515
16516 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
16517 }
16518
16519 #[test]
16520 fn test_cloudformation_console_url() {
16521 use crate::ui::cfn::DetailTab;
16522 let mut app = test_app();
16523 app.current_service = Service::CloudFormationStacks;
16524 app.service_selected = true;
16525 app.cfn_state.status_filter = CfnStatusFilter::All;
16526
16527 app.cfn_state.table.items.push(CfnStack {
16528 name: "test-stack".to_string(),
16529 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
16530 .to_string(),
16531 status: "CREATE_COMPLETE".to_string(),
16532 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16533 updated_time: String::new(),
16534 deleted_time: String::new(),
16535 drift_status: String::new(),
16536 last_drift_check_time: String::new(),
16537 status_reason: String::new(),
16538 description: String::new(),
16539 detailed_status: String::new(),
16540 root_stack: String::new(),
16541 parent_stack: String::new(),
16542 termination_protection: false,
16543 iam_role: String::new(),
16544 tags: vec![],
16545 stack_policy: String::new(),
16546 rollback_monitoring_time: String::new(),
16547 rollback_alarms: vec![],
16548 notification_arns: vec![],
16549 });
16550
16551 app.cfn_state.current_stack = Some("test-stack".to_string());
16552
16553 app.cfn_state.detail_tab = DetailTab::StackInfo;
16555 let url = app.get_console_url();
16556 assert!(url.contains("stackinfo"));
16557 assert!(url.contains("arn%3Aaws%3Acloudformation"));
16558
16559 app.cfn_state.detail_tab = DetailTab::Events;
16561 let url = app.get_console_url();
16562 assert!(url.contains("events"));
16563 assert!(url.contains("arn%3Aaws%3Acloudformation"));
16564 }
16565
16566 #[test]
16567 fn test_iam_role_select() {
16568 let mut app = test_app();
16569 app.current_service = Service::IamRoles;
16570 app.service_selected = true;
16571 app.mode = Mode::Normal;
16572
16573 app.iam_state.roles.items = vec![
16574 IamRole {
16575 role_name: "role1".to_string(),
16576 path: "/".to_string(),
16577 trusted_entities: "AWS Service: ec2".to_string(),
16578 last_activity: "-".to_string(),
16579 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
16580 creation_time: "2025-01-01".to_string(),
16581 description: "Test role 1".to_string(),
16582 max_session_duration: Some(3600),
16583 },
16584 IamRole {
16585 role_name: "role2".to_string(),
16586 path: "/".to_string(),
16587 trusted_entities: "AWS Service: lambda".to_string(),
16588 last_activity: "-".to_string(),
16589 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
16590 creation_time: "2025-01-02".to_string(),
16591 description: "Test role 2".to_string(),
16592 max_session_duration: Some(7200),
16593 },
16594 ];
16595
16596 app.iam_state.roles.selected = 0;
16598 app.handle_action(Action::Select);
16599
16600 assert_eq!(
16601 app.iam_state.current_role,
16602 Some("role1".to_string()),
16603 "Should open role detail view"
16604 );
16605 assert_eq!(
16606 app.iam_state.role_tab,
16607 RoleTab::Permissions,
16608 "Should default to Permissions tab"
16609 );
16610 }
16611
16612 #[test]
16613 fn test_iam_role_back_navigation() {
16614 let mut app = test_app();
16615 app.current_service = Service::IamRoles;
16616 app.service_selected = true;
16617 app.iam_state.current_role = Some("test-role".to_string());
16618
16619 app.handle_action(Action::GoBack);
16620
16621 assert_eq!(
16622 app.iam_state.current_role, None,
16623 "Should return to roles list"
16624 );
16625 }
16626
16627 #[test]
16628 fn test_iam_role_tab_navigation() {
16629 let mut app = test_app();
16630 app.current_service = Service::IamRoles;
16631 app.service_selected = true;
16632 app.iam_state.current_role = Some("test-role".to_string());
16633 app.iam_state.role_tab = RoleTab::Permissions;
16634
16635 app.handle_action(Action::NextDetailTab);
16636
16637 assert_eq!(
16638 app.iam_state.role_tab,
16639 RoleTab::TrustRelationships,
16640 "Should move to next tab"
16641 );
16642 }
16643
16644 #[test]
16645 fn test_iam_role_tab_cycle_order() {
16646 let mut app = test_app();
16647 app.current_service = Service::IamRoles;
16648 app.service_selected = true;
16649 app.iam_state.current_role = Some("test-role".to_string());
16650 app.iam_state.role_tab = RoleTab::Permissions;
16651
16652 app.handle_action(Action::NextDetailTab);
16653 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
16654
16655 app.handle_action(Action::NextDetailTab);
16656 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
16657
16658 app.handle_action(Action::NextDetailTab);
16659 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
16660
16661 app.handle_action(Action::NextDetailTab);
16662 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
16663
16664 app.handle_action(Action::NextDetailTab);
16665 assert_eq!(
16666 app.iam_state.role_tab,
16667 RoleTab::Permissions,
16668 "Should cycle back to first tab"
16669 );
16670 }
16671
16672 #[test]
16673 fn test_iam_role_pagination() {
16674 let mut app = test_app();
16675 app.current_service = Service::IamRoles;
16676 app.service_selected = true;
16677 app.iam_state.roles.page_size = PageSize::Ten;
16678
16679 app.iam_state.roles.items = (0..25)
16680 .map(|i| IamRole {
16681 role_name: format!("role{}", i),
16682 path: "/".to_string(),
16683 trusted_entities: "AWS Service: ec2".to_string(),
16684 last_activity: "-".to_string(),
16685 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
16686 creation_time: "2025-01-01".to_string(),
16687 description: format!("Test role {}", i),
16688 max_session_duration: Some(3600),
16689 })
16690 .collect();
16691
16692 app.go_to_page(2);
16694
16695 assert_eq!(
16696 app.iam_state.roles.selected, 10,
16697 "Should select first item of page 2"
16698 );
16699 assert_eq!(
16700 app.iam_state.roles.scroll_offset, 10,
16701 "Should update scroll offset"
16702 );
16703 }
16704
16705 #[test]
16706 fn test_tags_table_populated_on_role_detail() {
16707 let mut app = test_app();
16708 app.current_service = Service::IamRoles;
16709 app.service_selected = true;
16710 app.mode = Mode::Normal;
16711 app.iam_state.roles.items = vec![IamRole {
16712 role_name: "TestRole".to_string(),
16713 path: "/".to_string(),
16714 trusted_entities: String::new(),
16715 last_activity: String::new(),
16716 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
16717 creation_time: "2025-01-01".to_string(),
16718 description: String::new(),
16719 max_session_duration: Some(3600),
16720 }];
16721
16722 app.iam_state.tags.items = vec![
16724 IamRoleTag {
16725 key: "Environment".to_string(),
16726 value: "Production".to_string(),
16727 },
16728 IamRoleTag {
16729 key: "Team".to_string(),
16730 value: "Platform".to_string(),
16731 },
16732 ];
16733
16734 assert_eq!(app.iam_state.tags.items.len(), 2);
16735 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
16736 assert_eq!(app.iam_state.tags.items[0].value, "Production");
16737 assert_eq!(app.iam_state.tags.selected, 0);
16738 }
16739
16740 #[test]
16741 fn test_tags_table_navigation() {
16742 let mut app = test_app();
16743 app.current_service = Service::IamRoles;
16744 app.service_selected = true;
16745 app.mode = Mode::Normal;
16746 app.iam_state.current_role = Some("TestRole".to_string());
16747 app.iam_state.role_tab = RoleTab::Tags;
16748 app.iam_state.tags.items = vec![
16749 IamRoleTag {
16750 key: "Tag1".to_string(),
16751 value: "Value1".to_string(),
16752 },
16753 IamRoleTag {
16754 key: "Tag2".to_string(),
16755 value: "Value2".to_string(),
16756 },
16757 ];
16758
16759 app.handle_action(Action::NextItem);
16760 assert_eq!(app.iam_state.tags.selected, 1);
16761
16762 app.handle_action(Action::PrevItem);
16763 assert_eq!(app.iam_state.tags.selected, 0);
16764 }
16765
16766 #[test]
16767 fn test_last_accessed_table_navigation() {
16768 let mut app = test_app();
16769 app.current_service = Service::IamRoles;
16770 app.service_selected = true;
16771 app.mode = Mode::Normal;
16772 app.iam_state.current_role = Some("TestRole".to_string());
16773 app.iam_state.role_tab = RoleTab::LastAccessed;
16774 app.iam_state.last_accessed_services.items = vec![
16775 LastAccessedService {
16776 service: "S3".to_string(),
16777 policies_granting: "Policy1".to_string(),
16778 last_accessed: "2025-01-01".to_string(),
16779 },
16780 LastAccessedService {
16781 service: "EC2".to_string(),
16782 policies_granting: "Policy2".to_string(),
16783 last_accessed: "2025-01-02".to_string(),
16784 },
16785 ];
16786
16787 app.handle_action(Action::NextItem);
16788 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
16789
16790 app.handle_action(Action::PrevItem);
16791 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
16792 }
16793
16794 #[test]
16795 fn test_cfn_input_focus_next() {
16796 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
16797 let mut app = test_app();
16798 app.current_service = Service::CloudFormationStacks;
16799 app.mode = Mode::FilterInput;
16800 app.cfn_state.input_focus = InputFocus::Filter;
16801
16802 app.handle_action(Action::NextFilterFocus);
16803 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
16804
16805 app.handle_action(Action::NextFilterFocus);
16806 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
16807
16808 app.handle_action(Action::NextFilterFocus);
16809 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16810
16811 app.handle_action(Action::NextFilterFocus);
16812 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16813 }
16814
16815 #[test]
16816 fn test_cfn_input_focus_prev() {
16817 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
16818 let mut app = test_app();
16819 app.current_service = Service::CloudFormationStacks;
16820 app.mode = Mode::FilterInput;
16821 app.cfn_state.input_focus = InputFocus::Filter;
16822
16823 app.handle_action(Action::PrevFilterFocus);
16824 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16825
16826 app.handle_action(Action::PrevFilterFocus);
16827 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
16828
16829 app.handle_action(Action::PrevFilterFocus);
16830 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
16831
16832 app.handle_action(Action::PrevFilterFocus);
16833 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16834 }
16835
16836 #[test]
16837 fn test_cw_logs_input_focus_prev() {
16838 let mut app = test_app();
16839 app.current_service = Service::CloudWatchLogGroups;
16840 app.mode = Mode::FilterInput;
16841 app.view_mode = ViewMode::Detail;
16842 app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
16843 app.log_groups_state.input_focus = InputFocus::Filter;
16844
16845 app.handle_action(Action::PrevFilterFocus);
16846 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
16847
16848 app.handle_action(Action::PrevFilterFocus);
16849 assert_eq!(
16850 app.log_groups_state.input_focus,
16851 InputFocus::Checkbox("ShowExpired")
16852 );
16853
16854 app.handle_action(Action::PrevFilterFocus);
16855 assert_eq!(
16856 app.log_groups_state.input_focus,
16857 InputFocus::Checkbox("ExactMatch")
16858 );
16859
16860 app.handle_action(Action::PrevFilterFocus);
16861 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
16862 }
16863
16864 #[test]
16865 fn test_cw_events_input_focus_prev() {
16866 use crate::ui::cw::logs::EventFilterFocus;
16867 let mut app = test_app();
16868 app.mode = Mode::EventFilterInput;
16869 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
16870
16871 app.handle_action(Action::PrevFilterFocus);
16872 assert_eq!(
16873 app.log_groups_state.event_input_focus,
16874 EventFilterFocus::DateRange
16875 );
16876
16877 app.handle_action(Action::PrevFilterFocus);
16878 assert_eq!(
16879 app.log_groups_state.event_input_focus,
16880 EventFilterFocus::Filter
16881 );
16882 }
16883
16884 #[test]
16885 fn test_cfn_input_focus_cycle_complete() {
16886 let mut app = test_app();
16887 app.current_service = Service::CloudFormationStacks;
16888 app.mode = Mode::FilterInput;
16889 app.cfn_state.input_focus = InputFocus::Filter;
16890
16891 for _ in 0..4 {
16893 app.handle_action(Action::NextFilterFocus);
16894 }
16895 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16896
16897 for _ in 0..4 {
16899 app.handle_action(Action::PrevFilterFocus);
16900 }
16901 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16902 }
16903
16904 #[test]
16905 fn test_cfn_filter_status_arrow_keys() {
16906 use crate::ui::cfn::STATUS_FILTER;
16907 let mut app = test_app();
16908 app.current_service = Service::CloudFormationStacks;
16909 app.mode = Mode::FilterInput;
16910 app.cfn_state.input_focus = STATUS_FILTER;
16911 app.cfn_state.status_filter = CfnStatusFilter::All;
16912
16913 app.handle_action(Action::NextItem);
16914 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
16915
16916 app.handle_action(Action::PrevItem);
16917 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
16918 }
16919
16920 #[test]
16921 fn test_cfn_filter_shift_tab_cycles_backward() {
16922 use crate::ui::cfn::STATUS_FILTER;
16923 let mut app = test_app();
16924 app.current_service = Service::CloudFormationStacks;
16925 app.mode = Mode::FilterInput;
16926 app.cfn_state.input_focus = STATUS_FILTER;
16927
16928 app.handle_action(Action::PrevFilterFocus);
16930 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16931
16932 app.handle_action(Action::PrevFilterFocus);
16934 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16935 }
16936
16937 #[test]
16938 fn test_cfn_pagination_arrow_keys() {
16939 let mut app = test_app();
16940 app.current_service = Service::CloudFormationStacks;
16941 app.mode = Mode::FilterInput;
16942 app.cfn_state.input_focus = InputFocus::Pagination;
16943 app.cfn_state.table.scroll_offset = 0;
16944 app.cfn_state.table.page_size = PageSize::Ten;
16945
16946 app.cfn_state.table.items = (0..30)
16948 .map(|i| CfnStack {
16949 name: format!("stack-{}", i),
16950 stack_id: format!("id-{}", i),
16951 status: "CREATE_COMPLETE".to_string(),
16952 created_time: "2024-01-01".to_string(),
16953 updated_time: String::new(),
16954 deleted_time: String::new(),
16955 drift_status: String::new(),
16956 last_drift_check_time: String::new(),
16957 status_reason: String::new(),
16958 description: String::new(),
16959 detailed_status: String::new(),
16960 root_stack: String::new(),
16961 parent_stack: String::new(),
16962 termination_protection: false,
16963 iam_role: String::new(),
16964 tags: Vec::new(),
16965 stack_policy: String::new(),
16966 rollback_monitoring_time: String::new(),
16967 rollback_alarms: Vec::new(),
16968 notification_arns: Vec::new(),
16969 })
16970 .collect();
16971
16972 app.handle_action(Action::PageDown);
16974 assert_eq!(app.cfn_state.table.scroll_offset, 10);
16975 let page_size = app.cfn_state.table.page_size.value();
16977 let current_page = app.cfn_state.table.scroll_offset / page_size;
16978 assert_eq!(current_page, 1);
16979
16980 app.handle_action(Action::PageUp);
16982 assert_eq!(app.cfn_state.table.scroll_offset, 0);
16983 let current_page = app.cfn_state.table.scroll_offset / page_size;
16984 assert_eq!(current_page, 0);
16985 }
16986
16987 #[test]
16988 fn test_cfn_page_navigation_updates_selection() {
16989 let mut app = test_app();
16990 app.current_service = Service::CloudFormationStacks;
16991 app.mode = Mode::Normal;
16992
16993 app.cfn_state.table.items = (0..30)
16995 .map(|i| CfnStack {
16996 name: format!("stack-{}", i),
16997 stack_id: format!("id-{}", i),
16998 status: "CREATE_COMPLETE".to_string(),
16999 created_time: "2024-01-01".to_string(),
17000 updated_time: String::new(),
17001 deleted_time: String::new(),
17002 drift_status: String::new(),
17003 last_drift_check_time: String::new(),
17004 status_reason: String::new(),
17005 description: String::new(),
17006 detailed_status: String::new(),
17007 root_stack: String::new(),
17008 parent_stack: String::new(),
17009 termination_protection: false,
17010 iam_role: String::new(),
17011 tags: Vec::new(),
17012 stack_policy: String::new(),
17013 rollback_monitoring_time: String::new(),
17014 rollback_alarms: Vec::new(),
17015 notification_arns: Vec::new(),
17016 })
17017 .collect();
17018
17019 app.cfn_state.table.reset();
17020 app.cfn_state.table.scroll_offset = 0;
17021
17022 app.handle_action(Action::PageDown);
17024 assert_eq!(app.cfn_state.table.selected, 10);
17025
17026 app.handle_action(Action::PageDown);
17028 assert_eq!(app.cfn_state.table.selected, 20);
17029
17030 app.handle_action(Action::PageUp);
17032 assert_eq!(app.cfn_state.table.selected, 10);
17033 }
17034
17035 #[test]
17036 fn test_cfn_filter_input_only_when_focused() {
17037 use crate::ui::cfn::STATUS_FILTER;
17038 let mut app = test_app();
17039 app.current_service = Service::CloudFormationStacks;
17040 app.mode = Mode::FilterInput;
17041 app.cfn_state.input_focus = STATUS_FILTER;
17042 app.cfn_state.table.filter = String::new();
17043
17044 app.handle_action(Action::FilterInput('t'));
17046 app.handle_action(Action::FilterInput('e'));
17047 app.handle_action(Action::FilterInput('s'));
17048 app.handle_action(Action::FilterInput('t'));
17049 assert_eq!(app.cfn_state.table.filter, "");
17050
17051 app.cfn_state.input_focus = InputFocus::Filter;
17053 app.handle_action(Action::FilterInput('t'));
17054 app.handle_action(Action::FilterInput('e'));
17055 app.handle_action(Action::FilterInput('s'));
17056 app.handle_action(Action::FilterInput('t'));
17057 assert_eq!(app.cfn_state.table.filter, "test");
17058 }
17059
17060 #[test]
17061 fn test_cfn_input_focus_resets_on_start() {
17062 let mut app = test_app();
17063 app.current_service = Service::CloudFormationStacks;
17064 app.service_selected = true;
17065 app.mode = Mode::Normal;
17066 app.cfn_state.input_focus = InputFocus::Pagination;
17067
17068 app.handle_action(Action::StartFilter);
17070 assert_eq!(app.mode, Mode::FilterInput);
17071 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
17072 }
17073
17074 #[test]
17075 fn test_iam_roles_input_focus_cycles_forward() {
17076 let mut app = test_app();
17077 app.current_service = Service::IamRoles;
17078 app.mode = Mode::FilterInput;
17079 app.iam_state.role_input_focus = InputFocus::Filter;
17080
17081 app.handle_action(Action::NextFilterFocus);
17082 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
17083
17084 app.handle_action(Action::NextFilterFocus);
17085 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
17086 }
17087
17088 #[test]
17089 fn test_iam_roles_input_focus_cycles_backward() {
17090 let mut app = test_app();
17091 app.current_service = Service::IamRoles;
17092 app.mode = Mode::FilterInput;
17093 app.iam_state.role_input_focus = InputFocus::Filter;
17094
17095 app.handle_action(Action::PrevFilterFocus);
17096 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
17097
17098 app.handle_action(Action::PrevFilterFocus);
17099 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
17100 }
17101
17102 #[test]
17103 fn test_iam_roles_filter_input_only_when_focused() {
17104 let mut app = test_app();
17105 app.current_service = Service::IamRoles;
17106 app.mode = Mode::FilterInput;
17107 app.iam_state.role_input_focus = InputFocus::Pagination;
17108 app.iam_state.roles.filter = String::new();
17109
17110 app.handle_action(Action::FilterInput('t'));
17112 app.handle_action(Action::FilterInput('e'));
17113 app.handle_action(Action::FilterInput('s'));
17114 app.handle_action(Action::FilterInput('t'));
17115 assert_eq!(app.iam_state.roles.filter, "");
17116
17117 app.iam_state.role_input_focus = InputFocus::Filter;
17119 app.handle_action(Action::FilterInput('t'));
17120 app.handle_action(Action::FilterInput('e'));
17121 app.handle_action(Action::FilterInput('s'));
17122 app.handle_action(Action::FilterInput('t'));
17123 assert_eq!(app.iam_state.roles.filter, "test");
17124 }
17125
17126 #[test]
17127 fn test_iam_roles_page_down_updates_scroll_offset() {
17128 let mut app = test_app();
17129 app.current_service = Service::IamRoles;
17130 app.mode = Mode::Normal;
17131 app.iam_state.roles.items = (0..50)
17132 .map(|i| IamRole {
17133 role_name: format!("role-{}", i),
17134 path: "/".to_string(),
17135 trusted_entities: "AWS Service".to_string(),
17136 last_activity: "N/A".to_string(),
17137 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
17138 creation_time: "2024-01-01".to_string(),
17139 description: String::new(),
17140 max_session_duration: Some(3600),
17141 })
17142 .collect();
17143
17144 app.iam_state.roles.selected = 0;
17145 app.iam_state.roles.scroll_offset = 0;
17146
17147 app.handle_action(Action::PageDown);
17149 assert_eq!(app.iam_state.roles.selected, 10);
17150 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
17152
17153 app.handle_action(Action::PageDown);
17155 assert_eq!(app.iam_state.roles.selected, 20);
17156 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
17157 }
17158
17159 #[test]
17160 fn test_application_selection_and_deployments_tab() {
17161 use crate::lambda::Application as LambdaApplication;
17162 use LambdaApplicationDetailTab;
17163
17164 let mut app = test_app();
17165 app.current_service = Service::LambdaApplications;
17166 app.service_selected = true;
17167 app.mode = Mode::Normal;
17168
17169 app.lambda_application_state.table.items = vec![LambdaApplication {
17170 name: "test-app".to_string(),
17171 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17172 description: "Test application".to_string(),
17173 status: "CREATE_COMPLETE".to_string(),
17174 last_modified: "2024-01-01".to_string(),
17175 }];
17176
17177 app.handle_action(Action::Select);
17179 assert_eq!(
17180 app.lambda_application_state.current_application,
17181 Some("test-app".to_string())
17182 );
17183 assert_eq!(
17184 app.lambda_application_state.detail_tab,
17185 LambdaApplicationDetailTab::Overview
17186 );
17187
17188 app.handle_action(Action::NextDetailTab);
17190 assert_eq!(
17191 app.lambda_application_state.detail_tab,
17192 LambdaApplicationDetailTab::Deployments
17193 );
17194
17195 app.handle_action(Action::GoBack);
17197 assert_eq!(app.lambda_application_state.current_application, None);
17198 }
17199
17200 #[test]
17201 fn test_application_resources_filter_and_pagination() {
17202 use crate::lambda::Application as LambdaApplication;
17203 use LambdaApplicationDetailTab;
17204
17205 let mut app = test_app();
17206 app.current_service = Service::LambdaApplications;
17207 app.service_selected = true;
17208 app.mode = Mode::Normal;
17209
17210 app.lambda_application_state.table.items = vec![LambdaApplication {
17211 name: "test-app".to_string(),
17212 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17213 description: "Test application".to_string(),
17214 status: "CREATE_COMPLETE".to_string(),
17215 last_modified: "2024-01-01".to_string(),
17216 }];
17217
17218 app.handle_action(Action::Select);
17220 assert_eq!(
17221 app.lambda_application_state.detail_tab,
17222 LambdaApplicationDetailTab::Overview
17223 );
17224
17225 assert!(!app.lambda_application_state.resources.items.is_empty());
17227
17228 app.mode = Mode::FilterInput;
17230 assert_eq!(
17231 app.lambda_application_state.resource_input_focus,
17232 InputFocus::Filter
17233 );
17234
17235 app.handle_action(Action::NextFilterFocus);
17236 assert_eq!(
17237 app.lambda_application_state.resource_input_focus,
17238 InputFocus::Pagination
17239 );
17240
17241 app.handle_action(Action::PrevFilterFocus);
17242 assert_eq!(
17243 app.lambda_application_state.resource_input_focus,
17244 InputFocus::Filter
17245 );
17246 }
17247
17248 #[test]
17249 fn test_application_deployments_filter_and_pagination() {
17250 use crate::lambda::Application as LambdaApplication;
17251 use LambdaApplicationDetailTab;
17252
17253 let mut app = test_app();
17254 app.current_service = Service::LambdaApplications;
17255 app.service_selected = true;
17256 app.mode = Mode::Normal;
17257
17258 app.lambda_application_state.table.items = vec![LambdaApplication {
17259 name: "test-app".to_string(),
17260 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17261 description: "Test application".to_string(),
17262 status: "CREATE_COMPLETE".to_string(),
17263 last_modified: "2024-01-01".to_string(),
17264 }];
17265
17266 app.handle_action(Action::Select);
17268 app.handle_action(Action::NextDetailTab);
17269 assert_eq!(
17270 app.lambda_application_state.detail_tab,
17271 LambdaApplicationDetailTab::Deployments
17272 );
17273
17274 assert!(!app.lambda_application_state.deployments.items.is_empty());
17276
17277 app.mode = Mode::FilterInput;
17279 assert_eq!(
17280 app.lambda_application_state.deployment_input_focus,
17281 InputFocus::Filter
17282 );
17283
17284 app.handle_action(Action::NextFilterFocus);
17285 assert_eq!(
17286 app.lambda_application_state.deployment_input_focus,
17287 InputFocus::Pagination
17288 );
17289
17290 app.handle_action(Action::PrevFilterFocus);
17291 assert_eq!(
17292 app.lambda_application_state.deployment_input_focus,
17293 InputFocus::Filter
17294 );
17295 }
17296
17297 #[test]
17298 fn test_application_resource_expansion() {
17299 use crate::lambda::Application as LambdaApplication;
17300 use LambdaApplicationDetailTab;
17301
17302 let mut app = test_app();
17303 app.current_service = Service::LambdaApplications;
17304 app.service_selected = true;
17305 app.mode = Mode::Normal;
17306
17307 app.lambda_application_state.table.items = vec![LambdaApplication {
17308 name: "test-app".to_string(),
17309 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17310 description: "Test application".to_string(),
17311 status: "CREATE_COMPLETE".to_string(),
17312 last_modified: "2024-01-01".to_string(),
17313 }];
17314
17315 app.handle_action(Action::Select);
17317 assert_eq!(
17318 app.lambda_application_state.detail_tab,
17319 LambdaApplicationDetailTab::Overview
17320 );
17321
17322 app.handle_action(Action::NextPane);
17324 assert_eq!(
17325 app.lambda_application_state.resources.expanded_item,
17326 Some(0)
17327 );
17328
17329 app.handle_action(Action::PrevPane);
17331 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
17332 }
17333
17334 #[test]
17335 fn test_application_deployment_expansion() {
17336 use crate::lambda::Application as LambdaApplication;
17337 use LambdaApplicationDetailTab;
17338
17339 let mut app = test_app();
17340 app.current_service = Service::LambdaApplications;
17341 app.service_selected = true;
17342 app.mode = Mode::Normal;
17343
17344 app.lambda_application_state.table.items = vec![LambdaApplication {
17345 name: "test-app".to_string(),
17346 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17347 description: "Test application".to_string(),
17348 status: "CREATE_COMPLETE".to_string(),
17349 last_modified: "2024-01-01".to_string(),
17350 }];
17351
17352 app.handle_action(Action::Select);
17354 app.handle_action(Action::NextDetailTab);
17355 assert_eq!(
17356 app.lambda_application_state.detail_tab,
17357 LambdaApplicationDetailTab::Deployments
17358 );
17359
17360 app.handle_action(Action::NextPane);
17362 assert_eq!(
17363 app.lambda_application_state.deployments.expanded_item,
17364 Some(0)
17365 );
17366
17367 app.handle_action(Action::PrevPane);
17369 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
17370 }
17371
17372 #[test]
17373 fn test_s3_nested_prefix_expansion() {
17374 use crate::s3::Bucket;
17375 use S3Object;
17376
17377 let mut app = test_app();
17378 app.current_service = Service::S3Buckets;
17379 app.service_selected = true;
17380 app.mode = Mode::Normal;
17381
17382 app.s3_state.buckets.items = vec![Bucket {
17384 name: "test-bucket".to_string(),
17385 region: "us-east-1".to_string(),
17386 creation_date: "2024-01-01".to_string(),
17387 }];
17388
17389 app.s3_state.bucket_preview.insert(
17391 "test-bucket".to_string(),
17392 vec![S3Object {
17393 key: "level1/".to_string(),
17394 size: 0,
17395 last_modified: "".to_string(),
17396 is_prefix: true,
17397 storage_class: "".to_string(),
17398 }],
17399 );
17400
17401 app.s3_state.prefix_preview.insert(
17403 "level1/".to_string(),
17404 vec![S3Object {
17405 key: "level1/level2/".to_string(),
17406 size: 0,
17407 last_modified: "".to_string(),
17408 is_prefix: true,
17409 storage_class: "".to_string(),
17410 }],
17411 );
17412
17413 app.s3_state.selected_row = 0;
17415 app.handle_action(Action::NextPane);
17416 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
17417
17418 app.s3_state.selected_row = 1;
17420 app.handle_action(Action::NextPane);
17421 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
17422
17423 app.s3_state.selected_row = 2;
17425 app.handle_action(Action::NextPane);
17426 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
17427
17428 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
17430 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
17431 }
17432
17433 #[test]
17434 fn test_s3_nested_prefix_collapse() {
17435 use crate::s3::Bucket;
17436 use S3Object;
17437
17438 let mut app = test_app();
17439 app.current_service = Service::S3Buckets;
17440 app.service_selected = true;
17441 app.mode = Mode::Normal;
17442
17443 app.s3_state.buckets.items = vec![Bucket {
17444 name: "test-bucket".to_string(),
17445 region: "us-east-1".to_string(),
17446 creation_date: "2024-01-01".to_string(),
17447 }];
17448
17449 app.s3_state.bucket_preview.insert(
17450 "test-bucket".to_string(),
17451 vec![S3Object {
17452 key: "level1/".to_string(),
17453 size: 0,
17454 last_modified: "".to_string(),
17455 is_prefix: true,
17456 storage_class: "".to_string(),
17457 }],
17458 );
17459
17460 app.s3_state.prefix_preview.insert(
17461 "level1/".to_string(),
17462 vec![S3Object {
17463 key: "level1/level2/".to_string(),
17464 size: 0,
17465 last_modified: "".to_string(),
17466 is_prefix: true,
17467 storage_class: "".to_string(),
17468 }],
17469 );
17470
17471 app.s3_state
17473 .expanded_prefixes
17474 .insert("test-bucket".to_string());
17475 app.s3_state.expanded_prefixes.insert("level1/".to_string());
17476 app.s3_state
17477 .expanded_prefixes
17478 .insert("level1/level2/".to_string());
17479
17480 app.s3_state.selected_row = 2;
17482 app.handle_action(Action::PrevPane);
17483 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
17484 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
17488 app.handle_action(Action::PrevPane);
17489 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
17490 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
17494 app.handle_action(Action::PrevPane);
17495 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
17496 }
17497}
17498
17499#[cfg(test)]
17500mod sqs_tests {
17501 use super::*;
17502 use test_helpers::*;
17503
17504 #[test]
17505 fn test_sqs_filter_input() {
17506 let mut app = test_app();
17507 app.current_service = Service::SqsQueues;
17508 app.service_selected = true;
17509 app.mode = Mode::FilterInput;
17510
17511 app.handle_action(Action::FilterInput('t'));
17512 app.handle_action(Action::FilterInput('e'));
17513 app.handle_action(Action::FilterInput('s'));
17514 app.handle_action(Action::FilterInput('t'));
17515 assert_eq!(app.sqs_state.queues.filter, "test");
17516
17517 app.handle_action(Action::FilterBackspace);
17518 assert_eq!(app.sqs_state.queues.filter, "tes");
17519 }
17520
17521 #[test]
17522 fn test_sqs_start_filter() {
17523 let mut app = test_app();
17524 app.current_service = Service::SqsQueues;
17525 app.service_selected = true;
17526 app.mode = Mode::Normal;
17527
17528 app.handle_action(Action::StartFilter);
17529 assert_eq!(app.mode, Mode::FilterInput);
17530 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
17531 }
17532
17533 #[test]
17534 fn test_sqs_filter_focus_cycling() {
17535 let mut app = test_app();
17536 app.current_service = Service::SqsQueues;
17537 app.service_selected = true;
17538 app.mode = Mode::FilterInput;
17539 app.sqs_state.input_focus = InputFocus::Filter;
17540
17541 app.handle_action(Action::NextFilterFocus);
17542 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
17543
17544 app.handle_action(Action::NextFilterFocus);
17545 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
17546
17547 app.handle_action(Action::PrevFilterFocus);
17548 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
17549 }
17550
17551 #[test]
17552 fn test_sqs_navigation() {
17553 let mut app = test_app();
17554 app.current_service = Service::SqsQueues;
17555 app.service_selected = true;
17556 app.mode = Mode::Normal;
17557 app.sqs_state.queues.items = (0..10)
17558 .map(|i| SqsQueue {
17559 name: format!("queue{}", i),
17560 url: String::new(),
17561 queue_type: "Standard".to_string(),
17562 created_timestamp: String::new(),
17563 messages_available: "0".to_string(),
17564 messages_in_flight: "0".to_string(),
17565 encryption: "Disabled".to_string(),
17566 content_based_deduplication: "Disabled".to_string(),
17567 last_modified_timestamp: String::new(),
17568 visibility_timeout: String::new(),
17569 message_retention_period: String::new(),
17570 maximum_message_size: String::new(),
17571 delivery_delay: String::new(),
17572 receive_message_wait_time: String::new(),
17573 high_throughput_fifo: "N/A".to_string(),
17574 deduplication_scope: "N/A".to_string(),
17575 fifo_throughput_limit: "N/A".to_string(),
17576 dead_letter_queue: "-".to_string(),
17577 messages_delayed: "0".to_string(),
17578 redrive_allow_policy: "-".to_string(),
17579 redrive_policy: "".to_string(),
17580 redrive_task_id: "-".to_string(),
17581 redrive_task_start_time: "-".to_string(),
17582 redrive_task_status: "-".to_string(),
17583 redrive_task_percent: "-".to_string(),
17584 redrive_task_destination: "-".to_string(),
17585 })
17586 .collect();
17587
17588 app.handle_action(Action::NextItem);
17589 assert_eq!(app.sqs_state.queues.selected, 1);
17590
17591 app.handle_action(Action::PrevItem);
17592 assert_eq!(app.sqs_state.queues.selected, 0);
17593 }
17594
17595 #[test]
17596 fn test_sqs_page_navigation() {
17597 let mut app = test_app();
17598 app.current_service = Service::SqsQueues;
17599 app.service_selected = true;
17600 app.mode = Mode::Normal;
17601 app.sqs_state.queues.items = (0..100)
17602 .map(|i| SqsQueue {
17603 name: format!("queue{}", i),
17604 url: String::new(),
17605 queue_type: "Standard".to_string(),
17606 created_timestamp: String::new(),
17607 messages_available: "0".to_string(),
17608 messages_in_flight: "0".to_string(),
17609 encryption: "Disabled".to_string(),
17610 content_based_deduplication: "Disabled".to_string(),
17611 last_modified_timestamp: String::new(),
17612 visibility_timeout: String::new(),
17613 message_retention_period: String::new(),
17614 maximum_message_size: String::new(),
17615 delivery_delay: String::new(),
17616 receive_message_wait_time: String::new(),
17617 high_throughput_fifo: "N/A".to_string(),
17618 deduplication_scope: "N/A".to_string(),
17619 fifo_throughput_limit: "N/A".to_string(),
17620 dead_letter_queue: "-".to_string(),
17621 messages_delayed: "0".to_string(),
17622 redrive_allow_policy: "-".to_string(),
17623 redrive_policy: "".to_string(),
17624 redrive_task_id: "-".to_string(),
17625 redrive_task_start_time: "-".to_string(),
17626 redrive_task_status: "-".to_string(),
17627 redrive_task_percent: "-".to_string(),
17628 redrive_task_destination: "-".to_string(),
17629 })
17630 .collect();
17631
17632 app.handle_action(Action::PageDown);
17633 assert_eq!(app.sqs_state.queues.selected, 10);
17634
17635 app.handle_action(Action::PageUp);
17636 assert_eq!(app.sqs_state.queues.selected, 0);
17637 }
17638
17639 #[test]
17640 fn test_sqs_queue_expansion() {
17641 let mut app = test_app();
17642 app.current_service = Service::SqsQueues;
17643 app.service_selected = true;
17644 app.sqs_state.queues.items = vec![SqsQueue {
17645 name: "my-queue".to_string(),
17646 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
17647 queue_type: "Standard".to_string(),
17648 created_timestamp: "2023-01-01".to_string(),
17649 messages_available: "5".to_string(),
17650 messages_in_flight: "2".to_string(),
17651 encryption: "Enabled".to_string(),
17652 content_based_deduplication: "Disabled".to_string(),
17653 last_modified_timestamp: "2023-01-02".to_string(),
17654 visibility_timeout: "30".to_string(),
17655 message_retention_period: "345600".to_string(),
17656 maximum_message_size: "262144".to_string(),
17657 delivery_delay: "0".to_string(),
17658 receive_message_wait_time: "0".to_string(),
17659 high_throughput_fifo: "N/A".to_string(),
17660 deduplication_scope: "N/A".to_string(),
17661 fifo_throughput_limit: "N/A".to_string(),
17662 dead_letter_queue: "-".to_string(),
17663 messages_delayed: "0".to_string(),
17664 redrive_allow_policy: "-".to_string(),
17665 redrive_policy: "".to_string(),
17666 redrive_task_id: "-".to_string(),
17667 redrive_task_start_time: "-".to_string(),
17668 redrive_task_status: "-".to_string(),
17669 redrive_task_percent: "-".to_string(),
17670 redrive_task_destination: "-".to_string(),
17671 }];
17672 app.sqs_state.queues.selected = 0;
17673
17674 assert_eq!(app.sqs_state.queues.expanded_item, None);
17675
17676 app.handle_action(Action::NextPane);
17678 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
17679
17680 app.handle_action(Action::NextPane);
17682 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
17683
17684 app.handle_action(Action::PrevPane);
17686 assert_eq!(app.sqs_state.queues.expanded_item, None);
17687
17688 app.handle_action(Action::PrevPane);
17690 assert_eq!(app.sqs_state.queues.expanded_item, None);
17691 }
17692
17693 #[test]
17694 fn test_sqs_column_toggle() {
17695 use crate::sqs::queue::Column as SqsColumn;
17696 let mut app = test_app();
17697 app.current_service = Service::SqsQueues;
17698 app.service_selected = true;
17699 app.mode = Mode::ColumnSelector;
17700
17701 app.sqs_visible_column_ids = SqsColumn::ids();
17703 let initial_count = app.sqs_visible_column_ids.len();
17704
17705 app.column_selector_index = 0;
17707 app.handle_action(Action::ToggleColumn);
17708
17709 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
17711 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
17712
17713 app.handle_action(Action::ToggleColumn);
17715 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
17716 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
17717 }
17718
17719 #[test]
17720 fn test_sqs_column_selector_navigation() {
17721 let mut app = test_app();
17722 app.current_service = Service::SqsQueues;
17723 app.service_selected = true;
17724 app.mode = Mode::ColumnSelector;
17725 app.column_selector_index = 0;
17726
17727 let max_index = app.sqs_column_ids.len() - 1;
17729
17730 for _ in 0..max_index {
17732 app.handle_action(Action::NextItem);
17733 }
17734 assert_eq!(app.column_selector_index, max_index);
17735
17736 for _ in 0..max_index {
17738 app.handle_action(Action::PrevItem);
17739 }
17740 assert_eq!(app.column_selector_index, 0);
17741 }
17742
17743 #[test]
17744 fn test_sqs_queue_selection() {
17745 let mut app = test_app();
17746 app.current_service = Service::SqsQueues;
17747 app.service_selected = true;
17748 app.mode = Mode::Normal;
17749 app.sqs_state.queues.items = vec![SqsQueue {
17750 name: "my-queue".to_string(),
17751 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
17752 queue_type: "Standard".to_string(),
17753 created_timestamp: "2023-01-01".to_string(),
17754 messages_available: "5".to_string(),
17755 messages_in_flight: "2".to_string(),
17756 encryption: "Enabled".to_string(),
17757 content_based_deduplication: "Disabled".to_string(),
17758 last_modified_timestamp: "2023-01-02".to_string(),
17759 visibility_timeout: "30".to_string(),
17760 message_retention_period: "345600".to_string(),
17761 maximum_message_size: "262144".to_string(),
17762 delivery_delay: "0".to_string(),
17763 receive_message_wait_time: "0".to_string(),
17764 high_throughput_fifo: "N/A".to_string(),
17765 deduplication_scope: "N/A".to_string(),
17766 fifo_throughput_limit: "N/A".to_string(),
17767 dead_letter_queue: "-".to_string(),
17768 messages_delayed: "0".to_string(),
17769 redrive_allow_policy: "-".to_string(),
17770 redrive_policy: "".to_string(),
17771 redrive_task_id: "-".to_string(),
17772 redrive_task_start_time: "-".to_string(),
17773 redrive_task_status: "-".to_string(),
17774 redrive_task_percent: "-".to_string(),
17775 redrive_task_destination: "-".to_string(),
17776 }];
17777 app.sqs_state.queues.selected = 0;
17778
17779 assert_eq!(app.sqs_state.current_queue, None);
17780
17781 app.handle_action(Action::Select);
17783 assert_eq!(
17784 app.sqs_state.current_queue,
17785 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
17786 );
17787
17788 app.handle_action(Action::GoBack);
17790 assert_eq!(app.sqs_state.current_queue, None);
17791 }
17792
17793 #[test]
17794 fn test_sqs_lambda_triggers_expand_collapse() {
17795 let mut app = test_app();
17796 app.current_service = Service::SqsQueues;
17797 app.service_selected = true;
17798 app.sqs_state.current_queue =
17799 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17800 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17801 app.sqs_state.triggers.items = vec![LambdaTrigger {
17802 uuid: "test-uuid".to_string(),
17803 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
17804 status: "Enabled".to_string(),
17805 last_modified: "2024-01-01T00:00:00Z".to_string(),
17806 }];
17807 app.sqs_state.triggers.selected = 0;
17808
17809 assert_eq!(app.sqs_state.triggers.expanded_item, None);
17810
17811 app.handle_action(Action::NextPane);
17813 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
17814
17815 app.handle_action(Action::PrevPane);
17817 assert_eq!(app.sqs_state.triggers.expanded_item, None);
17818 }
17819
17820 #[test]
17821 fn test_sqs_lambda_triggers_expand_toggle() {
17822 let mut app = test_app();
17823 app.current_service = Service::SqsQueues;
17824 app.service_selected = true;
17825 app.sqs_state.current_queue =
17826 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17827 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17828 app.sqs_state.triggers.items = vec![LambdaTrigger {
17829 uuid: "test-uuid".to_string(),
17830 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
17831 status: "Enabled".to_string(),
17832 last_modified: "2024-01-01T00:00:00Z".to_string(),
17833 }];
17834 app.sqs_state.triggers.selected = 0;
17835
17836 app.handle_action(Action::NextPane);
17838 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
17839
17840 app.handle_action(Action::NextPane);
17842 assert_eq!(app.sqs_state.triggers.expanded_item, None);
17843
17844 app.handle_action(Action::NextPane);
17846 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
17847 }
17848
17849 #[test]
17850 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
17851 use crate::ui::sqs::filtered_lambda_triggers;
17852
17853 let mut app = test_app();
17854 app.current_service = Service::SqsQueues;
17855 app.service_selected = true;
17856 app.sqs_state.current_queue =
17857 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17858 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17859 app.sqs_state.triggers.items = vec![
17860 LambdaTrigger {
17861 uuid: "uuid-3".to_string(),
17862 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
17863 status: "Enabled".to_string(),
17864 last_modified: "2024-03-01T00:00:00Z".to_string(),
17865 },
17866 LambdaTrigger {
17867 uuid: "uuid-1".to_string(),
17868 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
17869 status: "Enabled".to_string(),
17870 last_modified: "2024-01-01T00:00:00Z".to_string(),
17871 },
17872 LambdaTrigger {
17873 uuid: "uuid-2".to_string(),
17874 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
17875 status: "Enabled".to_string(),
17876 last_modified: "2024-02-01T00:00:00Z".to_string(),
17877 },
17878 ];
17879
17880 let sorted = filtered_lambda_triggers(&app);
17881
17882 assert_eq!(sorted.len(), 3);
17884 assert_eq!(sorted[0].uuid, "uuid-1");
17885 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
17886 assert_eq!(sorted[1].uuid, "uuid-2");
17887 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
17888 assert_eq!(sorted[2].uuid, "uuid-3");
17889 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
17890 }
17891
17892 #[test]
17893 fn test_sqs_lambda_triggers_filter_input() {
17894 let mut app = test_app();
17895 app.current_service = Service::SqsQueues;
17896 app.service_selected = true;
17897 app.mode = Mode::FilterInput;
17898 app.sqs_state.current_queue =
17899 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17900 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17901 app.sqs_state.input_focus = InputFocus::Filter;
17902
17903 assert_eq!(app.sqs_state.triggers.filter, "");
17904
17905 app.handle_action(Action::FilterInput('t'));
17907 assert_eq!(app.sqs_state.triggers.filter, "t");
17908
17909 app.handle_action(Action::FilterInput('e'));
17910 assert_eq!(app.sqs_state.triggers.filter, "te");
17911
17912 app.handle_action(Action::FilterInput('s'));
17913 assert_eq!(app.sqs_state.triggers.filter, "tes");
17914
17915 app.handle_action(Action::FilterInput('t'));
17916 assert_eq!(app.sqs_state.triggers.filter, "test");
17917
17918 app.handle_action(Action::FilterBackspace);
17920 assert_eq!(app.sqs_state.triggers.filter, "tes");
17921 }
17922
17923 #[test]
17924 fn test_sqs_lambda_triggers_filter_applied() {
17925 use crate::ui::sqs::filtered_lambda_triggers;
17926
17927 let mut app = test_app();
17928 app.current_service = Service::SqsQueues;
17929 app.service_selected = true;
17930 app.sqs_state.current_queue =
17931 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17932 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17933 app.sqs_state.triggers.items = vec![
17934 LambdaTrigger {
17935 uuid: "uuid-1".to_string(),
17936 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
17937 status: "Enabled".to_string(),
17938 last_modified: "2024-01-01T00:00:00Z".to_string(),
17939 },
17940 LambdaTrigger {
17941 uuid: "uuid-2".to_string(),
17942 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
17943 status: "Enabled".to_string(),
17944 last_modified: "2024-02-01T00:00:00Z".to_string(),
17945 },
17946 LambdaTrigger {
17947 uuid: "uuid-3".to_string(),
17948 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
17949 status: "Enabled".to_string(),
17950 last_modified: "2024-03-01T00:00:00Z".to_string(),
17951 },
17952 ];
17953
17954 let filtered = filtered_lambda_triggers(&app);
17956 assert_eq!(filtered.len(), 3);
17957
17958 app.sqs_state.triggers.filter = "alpha".to_string();
17960 let filtered = filtered_lambda_triggers(&app);
17961 assert_eq!(filtered.len(), 1);
17962 assert_eq!(
17963 filtered[0].arn,
17964 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
17965 );
17966
17967 app.sqs_state.triggers.filter = "test".to_string();
17969 let filtered = filtered_lambda_triggers(&app);
17970 assert_eq!(filtered.len(), 2);
17971 assert_eq!(
17972 filtered[0].arn,
17973 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
17974 );
17975 assert_eq!(
17976 filtered[1].arn,
17977 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
17978 );
17979
17980 app.sqs_state.triggers.filter = "uuid-3".to_string();
17982 let filtered = filtered_lambda_triggers(&app);
17983 assert_eq!(filtered.len(), 1);
17984 assert_eq!(filtered[0].uuid, "uuid-3");
17985 }
17986
17987 #[test]
17988 fn test_sqs_triggers_navigation() {
17989 let mut app = test_app();
17990 app.service_selected = true;
17991 app.mode = Mode::Normal;
17992 app.current_service = Service::SqsQueues;
17993 app.sqs_state.current_queue = Some("test-queue".to_string());
17994 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17995 app.sqs_state.triggers.items = vec![
17996 LambdaTrigger {
17997 uuid: "1".to_string(),
17998 arn: "arn1".to_string(),
17999 status: "Enabled".to_string(),
18000 last_modified: "2024-01-01".to_string(),
18001 },
18002 LambdaTrigger {
18003 uuid: "2".to_string(),
18004 arn: "arn2".to_string(),
18005 status: "Enabled".to_string(),
18006 last_modified: "2024-01-02".to_string(),
18007 },
18008 ];
18009
18010 assert_eq!(app.sqs_state.triggers.selected, 0);
18011 app.next_item();
18012 assert_eq!(app.sqs_state.triggers.selected, 1);
18013 app.prev_item();
18014 assert_eq!(app.sqs_state.triggers.selected, 0);
18015 }
18016
18017 #[test]
18018 fn test_sqs_pipes_navigation() {
18019 let mut app = test_app();
18020 app.service_selected = true;
18021 app.mode = Mode::Normal;
18022 app.current_service = Service::SqsQueues;
18023 app.sqs_state.current_queue = Some("test-queue".to_string());
18024 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
18025 app.sqs_state.pipes.items = vec![
18026 EventBridgePipe {
18027 name: "pipe1".to_string(),
18028 status: "RUNNING".to_string(),
18029 target: "target1".to_string(),
18030 last_modified: "2024-01-01".to_string(),
18031 },
18032 EventBridgePipe {
18033 name: "pipe2".to_string(),
18034 status: "RUNNING".to_string(),
18035 target: "target2".to_string(),
18036 last_modified: "2024-01-02".to_string(),
18037 },
18038 ];
18039
18040 assert_eq!(app.sqs_state.pipes.selected, 0);
18041 app.next_item();
18042 assert_eq!(app.sqs_state.pipes.selected, 1);
18043 app.prev_item();
18044 assert_eq!(app.sqs_state.pipes.selected, 0);
18045 }
18046
18047 #[test]
18048 fn test_sqs_tags_navigation() {
18049 let mut app = test_app();
18050 app.service_selected = true;
18051 app.mode = Mode::Normal;
18052 app.current_service = Service::SqsQueues;
18053 app.sqs_state.current_queue = Some("test-queue".to_string());
18054 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
18055 app.sqs_state.tags.items = vec![
18056 SqsQueueTag {
18057 key: "Env".to_string(),
18058 value: "prod".to_string(),
18059 },
18060 SqsQueueTag {
18061 key: "Team".to_string(),
18062 value: "backend".to_string(),
18063 },
18064 ];
18065
18066 assert_eq!(app.sqs_state.tags.selected, 0);
18067 app.next_item();
18068 assert_eq!(app.sqs_state.tags.selected, 1);
18069 app.prev_item();
18070 assert_eq!(app.sqs_state.tags.selected, 0);
18071 }
18072
18073 #[test]
18074 fn test_sqs_queues_navigation() {
18075 let mut app = test_app();
18076 app.service_selected = true;
18077 app.mode = Mode::Normal;
18078 app.current_service = Service::SqsQueues;
18079 app.sqs_state.queues.items = vec![
18080 SqsQueue {
18081 name: "queue1".to_string(),
18082 url: "url1".to_string(),
18083 queue_type: "Standard".to_string(),
18084 created_timestamp: "".to_string(),
18085 messages_available: "0".to_string(),
18086 messages_in_flight: "0".to_string(),
18087 encryption: "Disabled".to_string(),
18088 content_based_deduplication: "Disabled".to_string(),
18089 last_modified_timestamp: "".to_string(),
18090 visibility_timeout: "".to_string(),
18091 message_retention_period: "".to_string(),
18092 maximum_message_size: "".to_string(),
18093 delivery_delay: "".to_string(),
18094 receive_message_wait_time: "".to_string(),
18095 high_throughput_fifo: "-".to_string(),
18096 deduplication_scope: "-".to_string(),
18097 fifo_throughput_limit: "-".to_string(),
18098 dead_letter_queue: "-".to_string(),
18099 messages_delayed: "0".to_string(),
18100 redrive_allow_policy: "-".to_string(),
18101 redrive_policy: "".to_string(),
18102 redrive_task_id: "-".to_string(),
18103 redrive_task_start_time: "-".to_string(),
18104 redrive_task_status: "-".to_string(),
18105 redrive_task_percent: "-".to_string(),
18106 redrive_task_destination: "-".to_string(),
18107 },
18108 SqsQueue {
18109 name: "queue2".to_string(),
18110 url: "url2".to_string(),
18111 queue_type: "Standard".to_string(),
18112 created_timestamp: "".to_string(),
18113 messages_available: "0".to_string(),
18114 messages_in_flight: "0".to_string(),
18115 encryption: "Disabled".to_string(),
18116 content_based_deduplication: "Disabled".to_string(),
18117 last_modified_timestamp: "".to_string(),
18118 visibility_timeout: "".to_string(),
18119 message_retention_period: "".to_string(),
18120 maximum_message_size: "".to_string(),
18121 delivery_delay: "".to_string(),
18122 receive_message_wait_time: "".to_string(),
18123 high_throughput_fifo: "-".to_string(),
18124 deduplication_scope: "-".to_string(),
18125 fifo_throughput_limit: "-".to_string(),
18126 dead_letter_queue: "-".to_string(),
18127 messages_delayed: "0".to_string(),
18128 redrive_allow_policy: "-".to_string(),
18129 redrive_policy: "".to_string(),
18130 redrive_task_id: "-".to_string(),
18131 redrive_task_start_time: "-".to_string(),
18132 redrive_task_status: "-".to_string(),
18133 redrive_task_percent: "-".to_string(),
18134 redrive_task_destination: "-".to_string(),
18135 },
18136 ];
18137
18138 assert_eq!(app.sqs_state.queues.selected, 0);
18139 app.next_item();
18140 assert_eq!(app.sqs_state.queues.selected, 1);
18141 app.prev_item();
18142 assert_eq!(app.sqs_state.queues.selected, 0);
18143 }
18144
18145 #[test]
18146 fn test_sqs_subscriptions_navigation() {
18147 let mut app = test_app();
18148 app.service_selected = true;
18149 app.mode = Mode::Normal;
18150 app.current_service = Service::SqsQueues;
18151 app.sqs_state.current_queue = Some("test-queue".to_string());
18152 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18153 app.sqs_state.subscriptions.items = vec![
18154 SnsSubscription {
18155 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
18156 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
18157 },
18158 SnsSubscription {
18159 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
18160 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
18161 },
18162 ];
18163
18164 assert_eq!(app.sqs_state.subscriptions.selected, 0);
18165 app.next_item();
18166 assert_eq!(app.sqs_state.subscriptions.selected, 1);
18167 app.prev_item();
18168 assert_eq!(app.sqs_state.subscriptions.selected, 0);
18169 }
18170
18171 #[test]
18172 fn test_sqs_subscription_region_dropdown_navigation() {
18173 let mut app = test_app();
18174 app.service_selected = true;
18175 app.mode = Mode::FilterInput;
18176 app.current_service = Service::SqsQueues;
18177 app.sqs_state.current_queue = Some("test-queue".to_string());
18178 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18179 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
18180
18181 assert_eq!(app.sqs_state.subscription_region_selected, 0);
18182 app.next_item();
18183 assert_eq!(app.sqs_state.subscription_region_selected, 1);
18184 app.next_item();
18185 assert_eq!(app.sqs_state.subscription_region_selected, 2);
18186 app.prev_item();
18187 assert_eq!(app.sqs_state.subscription_region_selected, 1);
18188 app.prev_item();
18189 assert_eq!(app.sqs_state.subscription_region_selected, 0);
18190 }
18191
18192 #[test]
18193 fn test_sqs_subscription_region_selection() {
18194 let mut app = test_app();
18195 app.service_selected = true;
18196 app.mode = Mode::FilterInput;
18197 app.current_service = Service::SqsQueues;
18198 app.sqs_state.current_queue = Some("test-queue".to_string());
18199 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18200 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
18201 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
18204 app.handle_action(Action::ApplyFilter);
18205 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
18206 assert_eq!(app.mode, Mode::Normal);
18207 }
18208
18209 #[test]
18210 fn test_sqs_subscription_region_change_resets_selection() {
18211 let mut app = test_app();
18212 app.service_selected = true;
18213 app.mode = Mode::FilterInput;
18214 app.current_service = Service::SqsQueues;
18215 app.sqs_state.current_queue = Some("test-queue".to_string());
18216 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18217 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
18218 app.sqs_state.subscription_region_selected = 0;
18219 app.sqs_state.subscriptions.selected = 5;
18220
18221 app.handle_action(Action::NextItem);
18222
18223 assert_eq!(app.sqs_state.subscription_region_selected, 1);
18224 assert_eq!(app.sqs_state.subscriptions.selected, 0);
18225 }
18226
18227 #[test]
18228 fn test_s3_object_filter_resets_selection() {
18229 let mut app = test_app();
18230 app.service_selected = true;
18231 app.current_service = Service::S3Buckets;
18232 app.s3_state.current_bucket = Some("test-bucket".to_string());
18233 app.s3_state.selected_row = 5;
18234 app.mode = Mode::FilterInput;
18235
18236 app.handle_action(Action::CloseMenu);
18237
18238 assert_eq!(app.s3_state.selected_row, 0);
18239 assert_eq!(app.mode, Mode::Normal);
18240 }
18241
18242 #[test]
18243 fn test_s3_bucket_filter_resets_selection() {
18244 let mut app = test_app();
18245 app.service_selected = true;
18246 app.current_service = Service::S3Buckets;
18247 app.s3_state.selected_row = 10;
18248 app.mode = Mode::FilterInput;
18249
18250 app.handle_action(Action::CloseMenu);
18251
18252 assert_eq!(app.s3_state.selected_row, 0);
18253 assert_eq!(app.mode, Mode::Normal);
18254 }
18255
18256 #[test]
18257 fn test_s3_selection_stays_in_bounds() {
18258 let mut app = test_app();
18259 app.service_selected = true;
18260 app.current_service = Service::S3Buckets;
18261 app.s3_state.selected_row = 0;
18262 app.s3_state.selected_object = 0;
18263
18264 app.prev_item();
18266
18267 assert_eq!(app.s3_state.selected_row, 0);
18269 assert_eq!(app.s3_state.selected_object, 0);
18270 }
18271
18272 #[test]
18273 fn test_cfn_filter_resets_selection() {
18274 let mut app = test_app();
18275 app.service_selected = true;
18276 app.current_service = Service::CloudFormationStacks;
18277 app.cfn_state.table.selected = 10;
18278 app.mode = Mode::FilterInput;
18279
18280 app.handle_action(Action::CloseMenu);
18281
18282 assert_eq!(app.cfn_state.table.selected, 0);
18283 assert_eq!(app.mode, Mode::Normal);
18284 }
18285
18286 #[test]
18287 fn test_lambda_filter_resets_selection() {
18288 let mut app = test_app();
18289 app.service_selected = true;
18290 app.current_service = Service::LambdaFunctions;
18291 app.lambda_state.table.selected = 8;
18292 app.mode = Mode::FilterInput;
18293
18294 app.handle_action(Action::CloseMenu);
18295
18296 assert_eq!(app.lambda_state.table.selected, 0);
18297 assert_eq!(app.mode, Mode::Normal);
18298 }
18299
18300 #[test]
18301 fn test_sqs_filter_resets_selection() {
18302 let mut app = test_app();
18303 app.service_selected = true;
18304 app.current_service = Service::SqsQueues;
18305 app.sqs_state.queues.selected = 7;
18306 app.mode = Mode::FilterInput;
18307
18308 app.handle_action(Action::CloseMenu);
18309
18310 assert_eq!(app.sqs_state.queues.selected, 0);
18311 assert_eq!(app.mode, Mode::Normal);
18312 }
18313
18314 #[test]
18315 fn test_sqs_queues_list_shows_preferences() {
18316 let mut app = test_app();
18317 app.service_selected = true;
18318 app.current_service = Service::SqsQueues;
18319 app.mode = Mode::Normal;
18320
18321 app.handle_action(Action::OpenColumnSelector);
18322
18323 assert_eq!(app.mode, Mode::ColumnSelector);
18324 }
18325
18326 #[test]
18327 fn test_sqs_queue_policies_tab_no_preferences() {
18328 let mut app = test_app();
18329 app.service_selected = true;
18330 app.current_service = Service::SqsQueues;
18331 app.sqs_state.current_queue = Some("test-queue".to_string());
18332 app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
18333 app.mode = Mode::Normal;
18334
18335 app.handle_action(Action::OpenColumnSelector);
18336
18337 assert_eq!(app.mode, Mode::Normal);
18338 }
18339
18340 #[test]
18341 fn test_sqs_sns_subscriptions_tab_shows_preferences() {
18342 let mut app = test_app();
18343 app.service_selected = true;
18344 app.current_service = Service::SqsQueues;
18345 app.sqs_state.current_queue = Some("test-queue".to_string());
18346 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18347 app.mode = Mode::Normal;
18348
18349 app.handle_action(Action::OpenColumnSelector);
18350
18351 assert_eq!(app.mode, Mode::ColumnSelector);
18352 }
18353
18354 #[test]
18355 fn test_sqs_monitoring_tab_no_preferences() {
18356 let mut app = test_app();
18357 app.service_selected = true;
18358 app.current_service = Service::SqsQueues;
18359 app.sqs_state.current_queue = Some("test-queue".to_string());
18360 app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
18361 app.mode = Mode::Normal;
18362
18363 app.handle_action(Action::OpenColumnSelector);
18364
18365 assert_eq!(app.mode, Mode::Normal);
18366 }
18367
18368 #[test]
18369 fn test_cfn_status_filter_change_resets_selection() {
18370 use crate::ui::cfn::STATUS_FILTER;
18371 let mut app = test_app();
18372 app.service_selected = true;
18373 app.current_service = Service::CloudFormationStacks;
18374 app.mode = Mode::FilterInput;
18375 app.cfn_state.input_focus = STATUS_FILTER;
18376 app.cfn_state.status_filter = CfnStatusFilter::All;
18377 app.cfn_state.table.items = vec![
18378 CfnStack {
18379 name: "stack1".to_string(),
18380 stack_id: "id1".to_string(),
18381 status: "CREATE_COMPLETE".to_string(),
18382 created_time: "2024-01-01".to_string(),
18383 updated_time: String::new(),
18384 deleted_time: String::new(),
18385 drift_status: String::new(),
18386 last_drift_check_time: String::new(),
18387 status_reason: String::new(),
18388 description: String::new(),
18389 detailed_status: String::new(),
18390 root_stack: String::new(),
18391 parent_stack: String::new(),
18392 termination_protection: false,
18393 iam_role: String::new(),
18394 tags: Vec::new(),
18395 stack_policy: String::new(),
18396 rollback_monitoring_time: String::new(),
18397 rollback_alarms: Vec::new(),
18398 notification_arns: Vec::new(),
18399 },
18400 CfnStack {
18401 name: "stack2".to_string(),
18402 stack_id: "id2".to_string(),
18403 status: "UPDATE_IN_PROGRESS".to_string(),
18404 created_time: "2024-01-02".to_string(),
18405 updated_time: String::new(),
18406 deleted_time: String::new(),
18407 drift_status: String::new(),
18408 last_drift_check_time: String::new(),
18409 status_reason: String::new(),
18410 description: String::new(),
18411 detailed_status: String::new(),
18412 root_stack: String::new(),
18413 parent_stack: String::new(),
18414 termination_protection: false,
18415 iam_role: String::new(),
18416 tags: Vec::new(),
18417 stack_policy: String::new(),
18418 rollback_monitoring_time: String::new(),
18419 rollback_alarms: Vec::new(),
18420 notification_arns: Vec::new(),
18421 },
18422 ];
18423 app.cfn_state.table.selected = 1;
18424
18425 app.handle_action(Action::NextItem);
18426
18427 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
18428 assert_eq!(app.cfn_state.table.selected, 0);
18429 }
18430
18431 #[test]
18432 fn test_cfn_view_nested_toggle_resets_selection() {
18433 use crate::ui::cfn::VIEW_NESTED;
18434 let mut app = test_app();
18435 app.service_selected = true;
18436 app.current_service = Service::CloudFormationStacks;
18437 app.mode = Mode::FilterInput;
18438 app.cfn_state.input_focus = VIEW_NESTED;
18439 app.cfn_state.view_nested = false;
18440 app.cfn_state.table.items = vec![CfnStack {
18441 name: "stack1".to_string(),
18442 stack_id: "id1".to_string(),
18443 status: "CREATE_COMPLETE".to_string(),
18444 created_time: "2024-01-01".to_string(),
18445 updated_time: String::new(),
18446 deleted_time: String::new(),
18447 drift_status: String::new(),
18448 last_drift_check_time: String::new(),
18449 status_reason: String::new(),
18450 description: String::new(),
18451 detailed_status: String::new(),
18452 root_stack: String::new(),
18453 parent_stack: String::new(),
18454 termination_protection: false,
18455 iam_role: String::new(),
18456 tags: Vec::new(),
18457 stack_policy: String::new(),
18458 rollback_monitoring_time: String::new(),
18459 rollback_alarms: Vec::new(),
18460 notification_arns: Vec::new(),
18461 }];
18462 app.cfn_state.table.selected = 5;
18463
18464 app.handle_action(Action::ToggleFilterCheckbox);
18465
18466 assert!(app.cfn_state.view_nested);
18467 assert_eq!(app.cfn_state.table.selected, 0);
18468 }
18469
18470 #[test]
18471 fn test_cfn_template_scroll_up() {
18472 let mut app = test_app();
18473 app.service_selected = true;
18474 app.current_service = Service::CloudFormationStacks;
18475 app.cfn_state.current_stack = Some("test-stack".to_string());
18476 app.cfn_state.detail_tab = CfnDetailTab::Template;
18477 app.cfn_state.template_scroll = 20;
18478
18479 app.page_up();
18480
18481 assert_eq!(app.cfn_state.template_scroll, 10);
18482 }
18483
18484 #[test]
18485 fn test_cfn_template_scroll_down() {
18486 let mut app = test_app();
18487 app.service_selected = true;
18488 app.current_service = Service::CloudFormationStacks;
18489 app.cfn_state.current_stack = Some("test-stack".to_string());
18490 app.cfn_state.detail_tab = CfnDetailTab::Template;
18491 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
18492 app.cfn_state.template_scroll = 0;
18493
18494 app.page_down();
18495
18496 assert_eq!(app.cfn_state.template_scroll, 10);
18497 }
18498
18499 #[test]
18500 fn test_cfn_template_scroll_down_respects_max() {
18501 let mut app = test_app();
18502 app.service_selected = true;
18503 app.current_service = Service::CloudFormationStacks;
18504 app.cfn_state.current_stack = Some("test-stack".to_string());
18505 app.cfn_state.detail_tab = CfnDetailTab::Template;
18506 app.cfn_state.template_body = "line1\nline2\nline3".to_string();
18507 app.cfn_state.template_scroll = 0;
18508
18509 app.page_down();
18510
18511 assert_eq!(app.cfn_state.template_scroll, 2);
18513 }
18514
18515 #[test]
18516 fn test_cfn_template_arrow_up() {
18517 let mut app = test_app();
18518 app.service_selected = true;
18519 app.current_service = Service::CloudFormationStacks;
18520 app.mode = Mode::Normal;
18521 app.cfn_state.current_stack = Some("test-stack".to_string());
18522 app.cfn_state.detail_tab = CfnDetailTab::Template;
18523 app.cfn_state.template_scroll = 5;
18524
18525 app.prev_item();
18526
18527 assert_eq!(app.cfn_state.template_scroll, 4);
18528 }
18529
18530 #[test]
18531 fn test_cfn_template_arrow_down() {
18532 let mut app = test_app();
18533 app.service_selected = true;
18534 app.current_service = Service::CloudFormationStacks;
18535 app.mode = Mode::Normal;
18536 app.cfn_state.current_stack = Some("test-stack".to_string());
18537 app.cfn_state.detail_tab = CfnDetailTab::Template;
18538 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
18539 app.cfn_state.template_scroll = 2;
18540
18541 app.next_item();
18542
18543 assert_eq!(app.cfn_state.template_scroll, 3);
18544 }
18545
18546 #[test]
18547 fn test_cfn_template_arrow_down_respects_max() {
18548 let mut app = test_app();
18549 app.service_selected = true;
18550 app.current_service = Service::CloudFormationStacks;
18551 app.mode = Mode::Normal;
18552 app.cfn_state.current_stack = Some("test-stack".to_string());
18553 app.cfn_state.detail_tab = CfnDetailTab::Template;
18554 app.cfn_state.template_body = "line1\nline2".to_string();
18555 app.cfn_state.template_scroll = 1;
18556
18557 app.next_item();
18558
18559 assert_eq!(app.cfn_state.template_scroll, 1);
18561 }
18562}
18563
18564#[cfg(test)]
18565mod lambda_version_tab_tests {
18566 use super::*;
18567 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
18568 use test_helpers::*;
18569
18570 #[test]
18571 fn test_lambda_version_tab_cycling_next() {
18572 let mut app = test_app();
18573 app.current_service = Service::LambdaFunctions;
18574 app.lambda_state.current_function = Some("test-function".to_string());
18575 app.lambda_state.current_version = Some("1".to_string());
18576 app.lambda_state.detail_tab = LambdaDetailTab::Code;
18577
18578 app.handle_action(Action::NextDetailTab);
18580 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
18581 assert!(app.lambda_state.metrics_loading);
18582
18583 app.lambda_state.metrics_loading = false;
18585 app.handle_action(Action::NextDetailTab);
18586 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
18587
18588 app.handle_action(Action::NextDetailTab);
18590 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
18591 }
18592
18593 #[test]
18594 fn test_lambda_version_tab_cycling_prev() {
18595 let mut app = test_app();
18596 app.current_service = Service::LambdaFunctions;
18597 app.lambda_state.current_function = Some("test-function".to_string());
18598 app.lambda_state.current_version = Some("1".to_string());
18599 app.lambda_state.detail_tab = LambdaDetailTab::Code;
18600
18601 app.handle_action(Action::PrevDetailTab);
18603 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
18604
18605 app.handle_action(Action::PrevDetailTab);
18607 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
18608 assert!(app.lambda_state.metrics_loading);
18609
18610 app.lambda_state.metrics_loading = false;
18612 app.handle_action(Action::PrevDetailTab);
18613 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
18614 }
18615
18616 #[test]
18617 fn test_lambda_version_monitor_clears_metrics() {
18618 let mut app = test_app();
18619 app.current_service = Service::LambdaFunctions;
18620 app.lambda_state.current_function = Some("test-function".to_string());
18621 app.lambda_state.current_version = Some("1".to_string());
18622 app.lambda_state.detail_tab = LambdaDetailTab::Code;
18623
18624 app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
18626 app.lambda_state.monitoring_scroll = 5;
18627
18628 app.handle_action(Action::NextDetailTab);
18630
18631 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
18632 assert!(app.lambda_state.metrics_loading);
18633 assert_eq!(app.lambda_state.monitoring_scroll, 0);
18634 assert!(app.lambda_state.metric_data_invocations.is_empty());
18635 }
18636
18637 #[test]
18638 fn test_cfn_parameters_expand_collapse() {
18639 let mut app = test_app();
18640 app.current_service = Service::CloudFormationStacks;
18641 app.service_selected = true;
18642 app.cfn_state.current_stack = Some("test-stack".to_string());
18643 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
18644 app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
18645 key: "Param1".to_string(),
18646 value: "Value1".to_string(),
18647 resolved_value: "Resolved1".to_string(),
18648 }];
18649 app.cfn_state.parameters.reset();
18650
18651 assert_eq!(app.cfn_state.parameters.expanded_item, None);
18652
18653 app.handle_action(Action::NextPane);
18655 assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
18656
18657 app.handle_action(Action::PrevPane);
18659 assert_eq!(app.cfn_state.parameters.expanded_item, None);
18660 }
18661
18662 #[test]
18663 fn test_cfn_parameters_filter_resets_selection() {
18664 let mut app = test_app();
18665 app.current_service = Service::CloudFormationStacks;
18666 app.service_selected = true;
18667 app.cfn_state.current_stack = Some("test-stack".to_string());
18668 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
18669 app.cfn_state.parameters.items = vec![
18670 rusticity_core::cfn::StackParameter {
18671 key: "DatabaseName".to_string(),
18672 value: "mydb".to_string(),
18673 resolved_value: "mydb".to_string(),
18674 },
18675 rusticity_core::cfn::StackParameter {
18676 key: "InstanceType".to_string(),
18677 value: "t2.micro".to_string(),
18678 resolved_value: "t2.micro".to_string(),
18679 },
18680 rusticity_core::cfn::StackParameter {
18681 key: "Environment".to_string(),
18682 value: "production".to_string(),
18683 resolved_value: "production".to_string(),
18684 },
18685 ];
18686 app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
18688 app.cfn_state.parameters_input_focus = InputFocus::Filter;
18689
18690 app.handle_action(Action::FilterInput('D'));
18692 assert_eq!(app.cfn_state.parameters.selected, 0);
18693 assert_eq!(app.cfn_state.parameters.filter, "D");
18694
18695 app.cfn_state.parameters.selected = 1;
18697
18698 app.handle_action(Action::FilterInput('a'));
18700 assert_eq!(app.cfn_state.parameters.selected, 0);
18701 assert_eq!(app.cfn_state.parameters.filter, "Da");
18702
18703 app.cfn_state.parameters.selected = 1;
18705
18706 app.handle_action(Action::FilterBackspace);
18708 assert_eq!(app.cfn_state.parameters.selected, 0);
18709 assert_eq!(app.cfn_state.parameters.filter, "D");
18710 }
18711
18712 #[test]
18713 fn test_cfn_template_tab_no_preferences() {
18714 let mut app = test_app();
18715 app.current_service = Service::CloudFormationStacks;
18716 app.service_selected = true;
18717 app.cfn_state.current_stack = Some("test-stack".to_string());
18718 app.cfn_state.detail_tab = CfnDetailTab::Template;
18719 app.mode = Mode::Normal;
18720
18721 app.handle_action(Action::OpenColumnSelector);
18723 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::GitSync;
18727 app.handle_action(Action::OpenColumnSelector);
18728 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::Parameters;
18732 app.handle_action(Action::OpenColumnSelector);
18733 assert_eq!(app.mode, Mode::ColumnSelector); app.mode = Mode::Normal;
18737 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
18738 app.handle_action(Action::OpenColumnSelector);
18739 assert_eq!(app.mode, Mode::ColumnSelector); }
18741
18742 #[test]
18743 fn test_iam_user_groups_tab_shows_preferences() {
18744 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18745 app.current_service = Service::IamUsers;
18746 app.service_selected = true;
18747 app.mode = Mode::Normal;
18748 app.iam_state.current_user = Some("test-user".to_string());
18749 app.iam_state.user_tab = UserTab::Groups;
18750
18751 app.handle_action(Action::OpenColumnSelector);
18753 assert_eq!(app.mode, Mode::ColumnSelector);
18754 }
18755
18756 #[test]
18757 fn test_iam_user_tags_tab_shows_preferences() {
18758 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18759 app.current_service = Service::IamUsers;
18760 app.service_selected = true;
18761 app.mode = Mode::Normal;
18762 app.iam_state.current_user = Some("test-user".to_string());
18763 app.iam_state.user_tab = UserTab::Tags;
18764
18765 app.handle_action(Action::OpenColumnSelector);
18767 assert_eq!(app.mode, Mode::ColumnSelector);
18768 }
18769
18770 #[test]
18771 fn test_iam_user_last_accessed_tab_shows_preferences() {
18772 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18773 app.current_service = Service::IamUsers;
18774 app.service_selected = true;
18775 app.mode = Mode::Normal;
18776 app.iam_state.current_user = Some("test-user".to_string());
18777 app.iam_state.user_tab = UserTab::LastAccessed;
18778
18779 app.handle_action(Action::OpenColumnSelector);
18781 assert_eq!(app.mode, Mode::ColumnSelector);
18782 }
18783
18784 #[test]
18785 fn test_iam_user_security_credentials_tab_no_preferences() {
18786 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18787 app.current_service = Service::IamUsers;
18788 app.service_selected = true;
18789 app.mode = Mode::Normal;
18790 app.iam_state.current_user = Some("test-user".to_string());
18791 app.iam_state.user_tab = UserTab::SecurityCredentials;
18792
18793 app.handle_action(Action::OpenColumnSelector);
18795 assert_eq!(app.mode, Mode::Normal);
18796 }
18797
18798 #[test]
18799 fn test_iam_user_tabs_without_column_preferences() {
18800 let mut app = test_app();
18801 app.current_service = Service::IamUsers;
18802 app.service_selected = true;
18803 app.iam_state.current_user = Some("test-user".to_string());
18804 app.mode = Mode::Normal;
18805
18806 app.iam_state.user_tab = UserTab::Groups;
18808 app.handle_action(Action::OpenColumnSelector);
18809 assert_eq!(app.mode, Mode::ColumnSelector);
18810 app.mode = Mode::Normal;
18811
18812 app.iam_state.user_tab = UserTab::Tags;
18814 app.handle_action(Action::OpenColumnSelector);
18815 assert_eq!(app.mode, Mode::ColumnSelector);
18816 app.mode = Mode::Normal;
18817
18818 app.iam_state.user_tab = UserTab::SecurityCredentials;
18820 app.handle_action(Action::OpenColumnSelector);
18821 assert_eq!(app.mode, Mode::Normal);
18822
18823 app.iam_state.user_tab = UserTab::LastAccessed;
18825 app.handle_action(Action::OpenColumnSelector);
18826 assert_eq!(app.mode, Mode::ColumnSelector);
18827 app.mode = Mode::Normal;
18828
18829 app.iam_state.user_tab = UserTab::Permissions;
18831 app.handle_action(Action::OpenColumnSelector);
18832 assert_eq!(app.mode, Mode::ColumnSelector);
18833
18834 app.mode = Mode::Normal;
18836 app.iam_state.current_user = None;
18837 app.handle_action(Action::OpenColumnSelector);
18838 assert_eq!(app.mode, Mode::ColumnSelector);
18839 }
18840
18841 #[test]
18842 fn test_iam_role_policies_dropdown_cycling() {
18843 let mut app = test_app();
18844 app.current_service = Service::IamRoles;
18845 app.service_selected = true;
18846 app.iam_state.current_role = Some("test-role".to_string());
18847 app.iam_state.role_tab = RoleTab::Permissions;
18848 app.mode = Mode::FilterInput;
18849 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
18850 app.iam_state.policy_type_filter = "All types".to_string();
18851
18852 app.next_item();
18854 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18855 app.next_item();
18856 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18857 app.next_item();
18858 assert_eq!(app.iam_state.policy_type_filter, "All types");
18859
18860 app.prev_item();
18862 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18863 app.prev_item();
18864 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18865 app.prev_item();
18866 assert_eq!(app.iam_state.policy_type_filter, "All types");
18867 }
18868
18869 #[test]
18870 fn test_iam_user_policies_dropdown_cycling() {
18871 let mut app = test_app();
18872 app.current_service = Service::IamUsers;
18873 app.service_selected = true;
18874 app.iam_state.current_user = Some("test-user".to_string());
18875 app.iam_state.user_tab = UserTab::Permissions;
18876 app.mode = Mode::FilterInput;
18877 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
18878 app.iam_state.policy_type_filter = "All types".to_string();
18879
18880 app.next_item();
18882 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18883 app.next_item();
18884 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18885 app.next_item();
18886 assert_eq!(app.iam_state.policy_type_filter, "All types");
18887
18888 app.prev_item();
18890 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18891 app.prev_item();
18892 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18893 app.prev_item();
18894 assert_eq!(app.iam_state.policy_type_filter, "All types");
18895 }
18896
18897 #[test]
18898 fn test_iam_role_tabs_without_column_preferences() {
18899 let mut app = test_app();
18900 app.current_service = Service::IamRoles;
18901 app.service_selected = true;
18902 app.iam_state.current_role = Some("test-role".to_string());
18903 app.mode = Mode::Normal;
18904
18905 app.iam_state.role_tab = RoleTab::TrustRelationships;
18907 app.handle_action(Action::OpenColumnSelector);
18908 assert_eq!(app.mode, Mode::Normal);
18909
18910 app.iam_state.role_tab = RoleTab::RevokeSessions;
18912 app.handle_action(Action::OpenColumnSelector);
18913 assert_eq!(app.mode, Mode::Normal);
18914
18915 app.iam_state.role_tab = RoleTab::LastAccessed;
18917 app.handle_action(Action::OpenColumnSelector);
18918 assert_eq!(app.mode, Mode::ColumnSelector);
18919
18920 app.mode = Mode::Normal;
18922 app.iam_state.role_tab = RoleTab::Permissions;
18923 app.handle_action(Action::OpenColumnSelector);
18924 assert_eq!(app.mode, Mode::ColumnSelector);
18925
18926 app.mode = Mode::Normal;
18928 app.iam_state.role_tab = RoleTab::Tags;
18929 app.handle_action(Action::OpenColumnSelector);
18930 assert_eq!(app.mode, Mode::ColumnSelector);
18931
18932 app.mode = Mode::Normal;
18934 app.iam_state.current_role = None;
18935 app.handle_action(Action::OpenColumnSelector);
18936 assert_eq!(app.mode, Mode::ColumnSelector);
18937 }
18938
18939 #[test]
18940 fn test_iam_role_tags_tab_cycling() {
18941 let mut app = test_app();
18942 app.current_service = Service::IamRoles;
18943 app.service_selected = true;
18944 app.iam_state.current_role = Some("test-role".to_string());
18945 app.iam_state.role_tab = RoleTab::Tags;
18946 app.mode = Mode::ColumnSelector;
18947 app.column_selector_index = 0;
18948
18949 app.handle_action(Action::NextPreferences);
18951 assert_eq!(app.column_selector_index, 4);
18952
18953 app.handle_action(Action::NextPreferences);
18955 assert_eq!(app.column_selector_index, 0);
18956
18957 app.handle_action(Action::PrevPreferences);
18959 assert_eq!(app.column_selector_index, 4);
18960
18961 app.handle_action(Action::PrevPreferences);
18963 assert_eq!(app.column_selector_index, 0);
18964 }
18965
18966 #[test]
18967 fn test_cfn_outputs_expand_collapse() {
18968 let mut app = test_app();
18969 app.current_service = Service::CloudFormationStacks;
18970 app.service_selected = true;
18971 app.cfn_state.current_stack = Some("test-stack".to_string());
18972 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
18973 app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
18974 key: "Output1".to_string(),
18975 value: "Value1".to_string(),
18976 description: "Description1".to_string(),
18977 export_name: "Export1".to_string(),
18978 }];
18979 app.cfn_state.outputs.reset();
18980
18981 assert_eq!(app.cfn_state.outputs.expanded_item, None);
18982
18983 app.handle_action(Action::NextPane);
18985 assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
18986
18987 app.handle_action(Action::PrevPane);
18989 assert_eq!(app.cfn_state.outputs.expanded_item, None);
18990 }
18991
18992 #[test]
18993 fn test_cfn_outputs_filter_resets_selection() {
18994 let mut app = test_app();
18995 app.current_service = Service::CloudFormationStacks;
18996 app.service_selected = true;
18997 app.cfn_state.current_stack = Some("test-stack".to_string());
18998 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
18999 app.cfn_state.outputs.items = vec![
19000 rusticity_core::cfn::StackOutput {
19001 key: "ApiUrl".to_string(),
19002 value: "https://api.example.com".to_string(),
19003 description: "API endpoint".to_string(),
19004 export_name: "MyApiUrl".to_string(),
19005 },
19006 rusticity_core::cfn::StackOutput {
19007 key: "BucketName".to_string(),
19008 value: "my-bucket".to_string(),
19009 description: "S3 bucket".to_string(),
19010 export_name: "MyBucket".to_string(),
19011 },
19012 ];
19013 app.cfn_state.outputs.reset();
19014 app.cfn_state.outputs.selected = 1;
19015
19016 app.handle_action(Action::StartFilter);
19018 assert_eq!(app.mode, Mode::FilterInput);
19019
19020 app.handle_action(Action::FilterInput('A'));
19022 assert_eq!(app.cfn_state.outputs.selected, 0);
19023 assert_eq!(app.cfn_state.outputs.filter, "A");
19024
19025 app.cfn_state.outputs.selected = 1;
19027 app.handle_action(Action::FilterInput('p'));
19028 assert_eq!(app.cfn_state.outputs.selected, 0);
19029
19030 app.cfn_state.outputs.selected = 1;
19032 app.handle_action(Action::FilterBackspace);
19033 assert_eq!(app.cfn_state.outputs.selected, 0);
19034 }
19035
19036 #[test]
19037 fn test_ec2_service_in_picker() {
19038 let app = test_app();
19039 assert!(app.service_picker.services.contains(&"EC2 › Instances"));
19040 }
19041
19042 #[test]
19043 fn test_ec2_state_filter_cycles() {
19044 let mut app = test_app();
19045 app.current_service = Service::Ec2Instances;
19046 app.service_selected = true;
19047 app.mode = Mode::FilterInput;
19048 app.ec2_state.input_focus = EC2_STATE_FILTER;
19049
19050 let initial = app.ec2_state.state_filter;
19051 assert_eq!(initial, Ec2StateFilter::AllStates);
19052
19053 app.handle_action(Action::ToggleFilterCheckbox);
19055 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19056
19057 app.handle_action(Action::ToggleFilterCheckbox);
19058 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
19059
19060 app.handle_action(Action::ToggleFilterCheckbox);
19061 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
19062
19063 app.handle_action(Action::ToggleFilterCheckbox);
19064 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
19065
19066 app.handle_action(Action::ToggleFilterCheckbox);
19067 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
19068
19069 app.handle_action(Action::ToggleFilterCheckbox);
19070 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
19071
19072 app.handle_action(Action::ToggleFilterCheckbox);
19073 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
19074 }
19075
19076 #[test]
19077 fn test_ec2_filter_resets_table() {
19078 let mut app = test_app();
19079 app.current_service = Service::Ec2Instances;
19080 app.service_selected = true;
19081 app.mode = Mode::FilterInput;
19082 app.ec2_state.input_focus = EC2_STATE_FILTER;
19083 app.ec2_state.table.selected = 5;
19084
19085 app.handle_action(Action::ToggleFilterCheckbox);
19086 assert_eq!(app.ec2_state.table.selected, 0);
19087 }
19088
19089 #[test]
19090 fn test_ec2_columns_visible() {
19091 let app = test_app();
19092 assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
19095
19096 #[test]
19097 fn test_ec2_breadcrumbs() {
19098 let mut app = test_app();
19099 app.current_service = Service::Ec2Instances;
19100 app.service_selected = true;
19101 let breadcrumb = app.breadcrumbs();
19102 assert_eq!(breadcrumb, "EC2 > Instances");
19103 }
19104
19105 #[test]
19106 fn test_ec2_console_url() {
19107 let mut app = test_app();
19108 app.current_service = Service::Ec2Instances;
19109 app.service_selected = true;
19110 let url = app.get_console_url();
19111 assert!(url.contains("ec2"));
19112 assert!(url.contains("Instances"));
19113 }
19114
19115 #[test]
19116 fn test_ec2_filter_handling() {
19117 let mut app = test_app();
19118 app.current_service = Service::Ec2Instances;
19119 app.service_selected = true;
19120 app.mode = Mode::FilterInput;
19121
19122 app.handle_action(Action::FilterInput('t'));
19123 app.handle_action(Action::FilterInput('e'));
19124 app.handle_action(Action::FilterInput('s'));
19125 app.handle_action(Action::FilterInput('t'));
19126
19127 assert_eq!(app.ec2_state.table.filter, "test");
19128
19129 app.handle_action(Action::FilterBackspace);
19130 assert_eq!(app.ec2_state.table.filter, "tes");
19131 }
19132
19133 #[test]
19134 fn test_column_selector_page_down_ec2() {
19135 let mut app = test_app();
19136 app.current_service = Service::Ec2Instances;
19137 app.service_selected = true;
19138 app.mode = Mode::ColumnSelector;
19139 app.column_selector_index = 0;
19140
19141 app.handle_action(Action::PageDown);
19142 assert_eq!(app.column_selector_index, 10);
19143
19144 app.handle_action(Action::PageDown);
19145 assert_eq!(app.column_selector_index, 20);
19146 }
19147
19148 #[test]
19149 fn test_column_selector_page_up_ec2() {
19150 let mut app = test_app();
19151 app.current_service = Service::Ec2Instances;
19152 app.service_selected = true;
19153 app.mode = Mode::ColumnSelector;
19154 app.column_selector_index = 30;
19155
19156 app.handle_action(Action::PageUp);
19157 assert_eq!(app.column_selector_index, 20);
19158
19159 app.handle_action(Action::PageUp);
19160 assert_eq!(app.column_selector_index, 10);
19161 }
19162
19163 #[test]
19164 fn test_ec2_state_filter_dropdown_focus() {
19165 let mut app = test_app();
19166 app.current_service = Service::Ec2Instances;
19167 app.service_selected = true;
19168 app.mode = Mode::FilterInput;
19169
19170 app.handle_action(Action::NextFilterFocus);
19172 assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
19173
19174 app.handle_action(Action::ToggleFilterCheckbox);
19177 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19178 }
19179
19180 #[test]
19181 fn test_column_selector_ctrl_d_scrolling() {
19182 let mut app = test_app();
19183 app.current_service = Service::LambdaFunctions;
19184 app.mode = Mode::ColumnSelector;
19185 app.column_selector_index = 0;
19186
19187 app.handle_action(Action::PageDown);
19189 assert_eq!(app.column_selector_index, 11);
19190
19191 let max = app.get_column_selector_max();
19193 app.handle_action(Action::PageDown);
19194 assert_eq!(app.column_selector_index, max);
19195 }
19196
19197 #[test]
19198 fn test_column_selector_ctrl_u_scrolling() {
19199 let mut app = test_app();
19200 app.current_service = Service::CloudFormationStacks;
19201 app.mode = Mode::ColumnSelector;
19202 app.column_selector_index = 25;
19203
19204 app.handle_action(Action::PageUp);
19205 assert_eq!(app.column_selector_index, 15);
19206
19207 app.handle_action(Action::PageUp);
19208 assert_eq!(app.column_selector_index, 5);
19209 }
19210
19211 #[test]
19212 fn test_prev_preferences_lambda() {
19213 let mut app = test_app();
19214 app.current_service = Service::LambdaFunctions;
19215 app.mode = Mode::ColumnSelector;
19216 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
19217 app.column_selector_index = page_size_idx;
19218
19219 app.handle_action(Action::PrevPreferences);
19220 assert_eq!(app.column_selector_index, 0);
19221
19222 app.handle_action(Action::PrevPreferences);
19223 assert_eq!(app.column_selector_index, page_size_idx);
19224 }
19225
19226 #[test]
19227 fn test_prev_preferences_cloudformation() {
19228 let mut app = test_app();
19229 app.current_service = Service::CloudFormationStacks;
19230 app.mode = Mode::ColumnSelector;
19231 let page_size_idx = app.cfn_column_ids.len() + 2;
19232 app.column_selector_index = page_size_idx;
19233
19234 app.handle_action(Action::PrevPreferences);
19235 assert_eq!(app.column_selector_index, 0);
19236
19237 app.handle_action(Action::PrevPreferences);
19238 assert_eq!(app.column_selector_index, page_size_idx);
19239 }
19240
19241 #[test]
19242 fn test_prev_preferences_alarms() {
19243 let mut app = test_app();
19244 app.current_service = Service::CloudWatchAlarms;
19245 app.mode = Mode::ColumnSelector;
19246 app.column_selector_index = 28; app.handle_action(Action::PrevPreferences);
19249 assert_eq!(app.column_selector_index, 22); app.handle_action(Action::PrevPreferences);
19252 assert_eq!(app.column_selector_index, 18); app.handle_action(Action::PrevPreferences);
19255 assert_eq!(app.column_selector_index, 0); app.handle_action(Action::PrevPreferences);
19258 assert_eq!(app.column_selector_index, 28); }
19260
19261 #[test]
19262 fn test_ec2_page_size_in_preferences() {
19263 let mut app = test_app();
19264 app.current_service = Service::Ec2Instances;
19265 app.mode = Mode::ColumnSelector;
19266 app.ec2_state.table.page_size = PageSize::Fifty;
19267
19268 let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
19271 app.handle_action(Action::ToggleColumn);
19272
19273 assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
19274 }
19275
19276 #[test]
19277 fn test_ec2_next_preferences_with_page_size() {
19278 let mut app = test_app();
19279 app.current_service = Service::Ec2Instances;
19280 app.mode = Mode::ColumnSelector;
19281 app.column_selector_index = 0;
19282
19283 let page_size_idx = app.ec2_column_ids.len() + 2;
19284 app.handle_action(Action::NextPreferences);
19285 assert_eq!(app.column_selector_index, page_size_idx);
19286
19287 app.handle_action(Action::NextPreferences);
19288 assert_eq!(app.column_selector_index, 0);
19289 }
19290
19291 #[test]
19292 fn test_ec2_dropdown_next_item() {
19293 let mut app = test_app();
19294 app.current_service = Service::Ec2Instances;
19295 app.mode = Mode::FilterInput;
19296 app.ec2_state.input_focus = EC2_STATE_FILTER;
19297 app.ec2_state.state_filter = Ec2StateFilter::AllStates;
19298
19299 app.handle_action(Action::NextItem);
19300 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19301
19302 app.handle_action(Action::NextItem);
19303 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
19304 }
19305
19306 #[test]
19307 fn test_ec2_dropdown_prev_item() {
19308 let mut app = test_app();
19309 app.current_service = Service::Ec2Instances;
19310 app.mode = Mode::FilterInput;
19311 app.ec2_state.input_focus = EC2_STATE_FILTER;
19312 app.ec2_state.state_filter = Ec2StateFilter::Stopped;
19313
19314 app.handle_action(Action::PrevItem);
19315 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19316
19317 app.handle_action(Action::PrevItem);
19318 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
19319 }
19320
19321 #[test]
19322 fn test_ec2_dropdown_cycles_with_arrows() {
19323 let mut app = test_app();
19324 app.current_service = Service::Ec2Instances;
19325 app.mode = Mode::FilterInput;
19326 app.ec2_state.input_focus = EC2_STATE_FILTER;
19327 app.ec2_state.state_filter = Ec2StateFilter::Stopping;
19328
19329 app.handle_action(Action::NextItem);
19331 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
19332
19333 app.handle_action(Action::PrevItem);
19335 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
19336 }
19337
19338 #[test]
19339 fn test_collapse_row_ec2_instances() {
19340 let mut app = test_app();
19341 app.current_service = Service::Ec2Instances;
19342 app.ec2_state.table.expanded_item = Some(0);
19343
19344 app.handle_action(Action::CollapseRow);
19345 assert_eq!(app.ec2_state.table.expanded_item, None);
19346 }
19347
19348 #[test]
19349 fn test_collapse_row_ec2_tags() {
19350 let mut app = test_app();
19351 app.current_service = Service::Ec2Instances;
19352 app.ec2_state.current_instance = Some("i-123".to_string());
19353 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
19354 app.ec2_state.tags.expanded_item = Some(1);
19355
19356 app.handle_action(Action::CollapseRow);
19357 assert_eq!(app.ec2_state.tags.expanded_item, None);
19358 }
19359
19360 #[test]
19361 fn test_collapse_row_cloudwatch_log_groups() {
19362 let mut app = test_app();
19363 app.current_service = Service::CloudWatchLogGroups;
19364 app.log_groups_state.log_groups.expanded_item = Some(2);
19365
19366 app.handle_action(Action::CollapseRow);
19367 assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
19368 }
19369
19370 #[test]
19371 fn test_collapse_row_cloudwatch_alarms() {
19372 let mut app = test_app();
19373 app.current_service = Service::CloudWatchAlarms;
19374 app.alarms_state.table.expanded_item = Some(0);
19375
19376 app.handle_action(Action::CollapseRow);
19377 assert_eq!(app.alarms_state.table.expanded_item, None);
19378 }
19379
19380 #[test]
19381 fn test_collapse_row_lambda_functions() {
19382 let mut app = test_app();
19383 app.current_service = Service::LambdaFunctions;
19384 app.lambda_state.table.expanded_item = Some(1);
19385
19386 app.handle_action(Action::CollapseRow);
19387 assert_eq!(app.lambda_state.table.expanded_item, None);
19388 }
19389
19390 #[test]
19391 fn test_collapse_row_cfn_stacks() {
19392 let mut app = test_app();
19393 app.current_service = Service::CloudFormationStacks;
19394 app.cfn_state.table.expanded_item = Some(0);
19395
19396 app.handle_action(Action::CollapseRow);
19397 assert_eq!(app.cfn_state.table.expanded_item, None);
19398 }
19399
19400 #[test]
19401 fn test_collapse_row_cfn_resources() {
19402 let mut app = test_app();
19403 app.current_service = Service::CloudFormationStacks;
19404 app.cfn_state.current_stack = Some("test-stack".to_string());
19405 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
19406 app.cfn_state.resources.expanded_item = Some(2);
19407
19408 app.handle_action(Action::CollapseRow);
19409 assert_eq!(app.cfn_state.resources.expanded_item, None);
19410 }
19411
19412 #[test]
19413 fn test_collapse_row_iam_users() {
19414 let mut app = test_app();
19415 app.current_service = Service::IamUsers;
19416 app.iam_state.users.expanded_item = Some(1);
19417
19418 app.handle_action(Action::CollapseRow);
19419 assert_eq!(app.iam_state.users.expanded_item, None);
19420 }
19421
19422 #[test]
19423 fn test_collapse_row_does_nothing_when_not_expanded() {
19424 let mut app = test_app();
19425 app.current_service = Service::Ec2Instances;
19426 app.ec2_state.table.expanded_item = None;
19427
19428 app.handle_action(Action::CollapseRow);
19429 assert_eq!(app.ec2_state.table.expanded_item, None);
19430 }
19431
19432 #[test]
19433 fn test_s3_collapse_expanded_folder_moves_to_parent() {
19434 let mut app = test_app();
19435 app.current_service = Service::S3Buckets;
19436 app.service_selected = true;
19437 app.mode = Mode::Normal;
19438
19439 app.s3_state.buckets.items = vec![S3Bucket {
19441 name: "bucket1".to_string(),
19442 region: "us-east-1".to_string(),
19443 creation_date: "2024-01-01T00:00:00Z".to_string(),
19444 }];
19445
19446 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19448 app.s3_state.bucket_preview.insert(
19449 "bucket1".to_string(),
19450 vec![S3Object {
19451 key: "folder1/".to_string(),
19452 size: 0,
19453 last_modified: "2024-01-01T00:00:00Z".to_string(),
19454 is_prefix: true,
19455 storage_class: String::new(),
19456 }],
19457 );
19458
19459 app.s3_state
19461 .expanded_prefixes
19462 .insert("folder1/".to_string());
19463 app.s3_state.prefix_preview.insert(
19464 "folder1/".to_string(),
19465 vec![S3Object {
19466 key: "folder1/file.txt".to_string(),
19467 size: 0,
19468 last_modified: "2024-01-01T00:00:00Z".to_string(),
19469 is_prefix: false,
19470 storage_class: String::new(),
19471 }],
19472 );
19473
19474 app.s3_state.selected_row = 1;
19476
19477 app.handle_action(Action::PrevPane);
19479
19480 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
19482 assert_eq!(app.s3_state.selected_row, 0);
19484 }
19485
19486 #[test]
19487 fn test_s3_collapse_hierarchy_level_by_level() {
19488 let mut app = test_app();
19489 app.current_service = Service::S3Buckets;
19490 app.service_selected = true;
19491 app.mode = Mode::Normal;
19492
19493 app.s3_state.buckets.items = vec![S3Bucket {
19495 name: "bucket1".to_string(),
19496 region: "us-east-1".to_string(),
19497 creation_date: "2024-01-01T00:00:00Z".to_string(),
19498 }];
19499
19500 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19502 app.s3_state.bucket_preview.insert(
19503 "bucket1".to_string(),
19504 vec![S3Object {
19505 key: "level1/".to_string(),
19506 size: 0,
19507 last_modified: "2024-01-01T00:00:00Z".to_string(),
19508 is_prefix: true,
19509 storage_class: String::new(),
19510 }],
19511 );
19512
19513 app.s3_state.expanded_prefixes.insert("level1/".to_string());
19515 app.s3_state.prefix_preview.insert(
19516 "level1/".to_string(),
19517 vec![S3Object {
19518 key: "level1/level2/".to_string(),
19519 size: 0,
19520 last_modified: "2024-01-01T00:00:00Z".to_string(),
19521 is_prefix: true,
19522 storage_class: String::new(),
19523 }],
19524 );
19525
19526 app.s3_state
19528 .expanded_prefixes
19529 .insert("level1/level2/".to_string());
19530 app.s3_state.prefix_preview.insert(
19531 "level1/level2/".to_string(),
19532 vec![S3Object {
19533 key: "level1/level2/file.txt".to_string(),
19534 size: 100,
19535 last_modified: "2024-01-01T00:00:00Z".to_string(),
19536 is_prefix: false,
19537 storage_class: String::new(),
19538 }],
19539 );
19540
19541 app.s3_state.selected_row = 3;
19543
19544 app.handle_action(Action::PrevPane);
19546 assert_eq!(app.s3_state.selected_row, 2);
19547
19548 app.handle_action(Action::PrevPane);
19550 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
19551 assert_eq!(app.s3_state.selected_row, 1);
19552
19553 app.handle_action(Action::PrevPane);
19555 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
19556 assert_eq!(app.s3_state.selected_row, 0);
19557
19558 app.handle_action(Action::PrevPane);
19560 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19561 assert_eq!(app.s3_state.selected_row, 0);
19562 }
19563
19564 #[test]
19565 fn test_ec2_instance_detail_tabs_no_preferences() {
19566 let mut app = test_app();
19567 app.current_service = Service::Ec2Instances;
19568 app.service_selected = true;
19569 app.ec2_state.table.expanded_item = Some(0);
19570 app.mode = Mode::Normal;
19571
19572 app.ec2_state.detail_tab = Ec2DetailTab::Details;
19574 app.handle_action(Action::OpenColumnSelector);
19575 assert_eq!(app.mode, Mode::Normal);
19576
19577 app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
19579 app.handle_action(Action::OpenColumnSelector);
19580 assert_eq!(app.mode, Mode::Normal);
19581
19582 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
19584 app.handle_action(Action::OpenColumnSelector);
19585 assert_eq!(app.mode, Mode::Normal);
19586
19587 app.ec2_state.detail_tab = Ec2DetailTab::Security;
19589 app.handle_action(Action::OpenColumnSelector);
19590 assert_eq!(app.mode, Mode::Normal);
19591
19592 app.ec2_state.detail_tab = Ec2DetailTab::Networking;
19594 app.handle_action(Action::OpenColumnSelector);
19595 assert_eq!(app.mode, Mode::Normal);
19596
19597 app.ec2_state.detail_tab = Ec2DetailTab::Storage;
19599 app.handle_action(Action::OpenColumnSelector);
19600 assert_eq!(app.mode, Mode::Normal);
19601
19602 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
19604 app.handle_action(Action::OpenColumnSelector);
19605 assert_eq!(app.mode, Mode::ColumnSelector);
19606 }
19607
19608 #[test]
19609 fn test_log_streams_filter_only_updates_when_focused() {
19610 let mut app = test_app();
19611 app.current_service = Service::CloudWatchLogGroups;
19612 app.service_selected = true;
19613 app.view_mode = ViewMode::Detail;
19614 app.mode = Mode::FilterInput;
19615 app.log_groups_state.stream_filter = "test".to_string();
19616
19617 app.log_groups_state.input_focus = InputFocus::Filter;
19619 app.handle_action(Action::FilterInput('x'));
19620 assert_eq!(app.log_groups_state.stream_filter, "testx");
19621
19622 app.log_groups_state.input_focus = InputFocus::Pagination;
19624 app.handle_action(Action::FilterInput('y'));
19625 assert_eq!(app.log_groups_state.stream_filter, "testx"); }
19627
19628 #[test]
19629 fn test_log_streams_backspace_only_updates_when_focused() {
19630 let mut app = test_app();
19631 app.current_service = Service::CloudWatchLogGroups;
19632 app.service_selected = true;
19633 app.view_mode = ViewMode::Detail;
19634 app.mode = Mode::FilterInput;
19635 app.log_groups_state.stream_filter = "test".to_string();
19636
19637 app.log_groups_state.input_focus = InputFocus::Filter;
19639 app.handle_action(Action::FilterBackspace);
19640 assert_eq!(app.log_groups_state.stream_filter, "tes");
19641
19642 app.log_groups_state.input_focus = InputFocus::Pagination;
19644 app.handle_action(Action::FilterBackspace);
19645 assert_eq!(app.log_groups_state.stream_filter, "tes"); }
19647
19648 #[test]
19649 fn test_log_groups_filter_only_updates_when_focused() {
19650 let mut app = test_app();
19651 app.current_service = Service::CloudWatchLogGroups;
19652 app.service_selected = true;
19653 app.view_mode = ViewMode::List;
19654 app.mode = Mode::FilterInput;
19655 app.log_groups_state.log_groups.filter = "test".to_string();
19656
19657 app.log_groups_state.input_focus = InputFocus::Filter;
19659 app.handle_action(Action::FilterInput('x'));
19660 assert_eq!(app.log_groups_state.log_groups.filter, "testx");
19661
19662 app.log_groups_state.input_focus = InputFocus::Pagination;
19664 app.handle_action(Action::FilterInput('y'));
19665 assert_eq!(app.log_groups_state.log_groups.filter, "testx"); }
19667
19668 #[test]
19669 fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
19670 use S3Bucket;
19671 use S3Object;
19672
19673 let mut app = test_app();
19674 app.current_service = Service::S3Buckets;
19675 app.service_selected = true;
19676
19677 app.s3_state.buckets.items = vec![S3Bucket {
19679 name: "test-bucket".to_string(),
19680 region: "us-east-1".to_string(),
19681 creation_date: String::new(),
19682 }];
19683
19684 app.s3_state
19686 .expanded_prefixes
19687 .insert("test-bucket".to_string());
19688 app.s3_state.bucket_preview.insert(
19689 "test-bucket".to_string(),
19690 vec![S3Object {
19691 key: "folder1/".to_string(),
19692 is_prefix: true,
19693 size: 0,
19694 last_modified: String::new(),
19695 storage_class: String::new(),
19696 }],
19697 );
19698
19699 app.s3_state
19701 .expanded_prefixes
19702 .insert("folder1/".to_string());
19703 app.s3_state.prefix_preview.insert(
19704 "folder1/".to_string(),
19705 vec![S3Object {
19706 key: "folder1/folder2/".to_string(),
19707 is_prefix: true,
19708 size: 0,
19709 last_modified: String::new(),
19710 storage_class: String::new(),
19711 }],
19712 );
19713
19714 app.s3_state.selected_row = 2;
19716
19717 app.handle_action(Action::CollapseRow);
19719
19720 assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
19722 assert_eq!(app.s3_state.selected_row, 1);
19724 }
19725
19726 #[test]
19727 fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
19728 use S3Bucket;
19729 use S3Object;
19730
19731 let mut app = test_app();
19732 app.current_service = Service::S3Buckets;
19733 app.service_selected = true;
19734
19735 app.s3_state.buckets.items = vec![S3Bucket {
19737 name: "test-bucket".to_string(),
19738 region: "us-east-1".to_string(),
19739 creation_date: String::new(),
19740 }];
19741
19742 app.s3_state
19744 .expanded_prefixes
19745 .insert("test-bucket".to_string());
19746 app.s3_state.bucket_preview.insert(
19747 "test-bucket".to_string(),
19748 vec![S3Object {
19749 key: "folder1/".to_string(),
19750 is_prefix: true,
19751 size: 0,
19752 last_modified: String::new(),
19753 storage_class: String::new(),
19754 }],
19755 );
19756
19757 app.s3_state
19759 .expanded_prefixes
19760 .insert("folder1/".to_string());
19761 app.s3_state.prefix_preview.insert(
19762 "folder1/".to_string(),
19763 vec![S3Object {
19764 key: "folder1/file.txt".to_string(),
19765 is_prefix: false,
19766 size: 100,
19767 last_modified: String::new(),
19768 storage_class: String::new(),
19769 }],
19770 );
19771
19772 app.s3_state.selected_row = 1;
19774
19775 app.handle_action(Action::CollapseRow);
19777
19778 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
19780 assert_eq!(app.s3_state.selected_row, 0);
19782 }
19783
19784 #[test]
19785 fn test_log_streams_pagination_limits_table_content() {
19786 let mut app = test_app();
19787 app.current_service = Service::CloudWatchLogGroups;
19788 app.service_selected = true;
19789 app.view_mode = ViewMode::Detail;
19790
19791 app.log_groups_state.log_streams = (0..50)
19793 .map(|i| rusticity_core::LogStream {
19794 name: format!("stream-{}", i),
19795 creation_time: None,
19796 last_event_time: None,
19797 })
19798 .collect();
19799
19800 app.log_groups_state.stream_page_size = 10;
19802 app.log_groups_state.stream_current_page = 0;
19803
19804 assert_eq!(app.log_groups_state.stream_page_size, 10);
19807 assert_eq!(app.log_groups_state.stream_current_page, 0);
19808
19809 app.log_groups_state.stream_current_page = 1;
19811 assert_eq!(app.log_groups_state.stream_current_page, 1);
19812 }
19813
19814 #[test]
19815 fn test_log_streams_page_size_change_resets_page() {
19816 let mut app = test_app();
19817 app.current_service = Service::CloudWatchLogGroups;
19818 app.service_selected = true;
19819 app.view_mode = ViewMode::Detail;
19820 app.mode = Mode::ColumnSelector;
19821
19822 app.log_groups_state.stream_page_size = 10;
19823 app.log_groups_state.stream_current_page = 3;
19824
19825 app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; app.handle_action(Action::ToggleColumn);
19828
19829 assert_eq!(app.log_groups_state.stream_page_size, 25);
19830 assert_eq!(app.log_groups_state.stream_current_page, 0);
19831 }
19832
19833 #[test]
19834 fn test_s3_objects_expanded_rows_stay_visible() {
19835 use S3Object;
19836
19837 let mut app = test_app();
19838 app.current_service = Service::S3Buckets;
19839 app.service_selected = true;
19840 app.mode = Mode::Normal;
19841 app.s3_state.current_bucket = Some("test-bucket".to_string());
19842
19843 app.s3_state.objects = vec![S3Object {
19845 key: "folder1/".to_string(),
19846 is_prefix: true,
19847 size: 0,
19848 last_modified: String::new(),
19849 storage_class: String::new(),
19850 }];
19851
19852 app.s3_state
19854 .expanded_prefixes
19855 .insert("folder1/".to_string());
19856 app.s3_state.prefix_preview.insert(
19857 "folder1/".to_string(),
19858 (0..20)
19859 .map(|i| S3Object {
19860 key: format!("folder1/file{}.txt", i),
19861 is_prefix: false,
19862 size: 100,
19863 last_modified: String::new(),
19864 storage_class: String::new(),
19865 })
19866 .collect(),
19867 );
19868
19869 app.s3_state.object_visible_rows.set(10);
19871 app.s3_state.object_scroll_offset = 0;
19872 app.s3_state.selected_object = 0; for i in 1..=20 {
19876 app.handle_action(Action::NextItem);
19877 assert_eq!(app.s3_state.selected_object, i);
19878
19879 let visible_start = app.s3_state.object_scroll_offset;
19881 let visible_end = visible_start + app.s3_state.object_visible_rows.get();
19882 assert!(
19883 app.s3_state.selected_object >= visible_start
19884 && app.s3_state.selected_object < visible_end,
19885 "Selection {} should be visible in range [{}, {})",
19886 app.s3_state.selected_object,
19887 visible_start,
19888 visible_end
19889 );
19890 }
19891 }
19892
19893 #[test]
19894 fn test_s3_bucket_error_rows_counted_in_total() {
19895 use S3Bucket;
19896
19897 let mut app = test_app();
19898 app.current_service = Service::S3Buckets;
19899 app.service_selected = true;
19900
19901 app.s3_state.buckets.items = vec![
19903 S3Bucket {
19904 name: "bucket1".to_string(),
19905 region: "us-east-1".to_string(),
19906 creation_date: String::new(),
19907 },
19908 S3Bucket {
19909 name: "bucket2".to_string(),
19910 region: "us-east-1".to_string(),
19911 creation_date: String::new(),
19912 },
19913 ];
19914
19915 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19917 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();
19918 app.s3_state
19919 .bucket_errors
19920 .insert("bucket1".to_string(), long_error.clone());
19921
19922 let total = app.calculate_total_bucket_rows();
19924
19925 let error_rows = long_error.len().div_ceil(120);
19927 assert_eq!(total, 2 + error_rows);
19928 }
19929
19930 #[test]
19931 fn test_s3_bucket_with_error_can_be_collapsed() {
19932 use S3Bucket;
19933
19934 let mut app = test_app();
19935 app.current_service = Service::S3Buckets;
19936 app.service_selected = true;
19937 app.mode = Mode::Normal;
19938
19939 app.s3_state.buckets.items = vec![S3Bucket {
19941 name: "bucket1".to_string(),
19942 region: "us-east-1".to_string(),
19943 creation_date: String::new(),
19944 }];
19945
19946 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19948 let error = "service error: PermanentRedirect".to_string();
19949 app.s3_state
19950 .bucket_errors
19951 .insert("bucket1".to_string(), error);
19952
19953 app.s3_state.selected_row = 0;
19955
19956 app.handle_action(Action::CollapseRow);
19958
19959 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19961 assert_eq!(app.s3_state.selected_row, 0);
19963 }
19964
19965 #[test]
19966 fn test_s3_bucket_collapse_on_bucket_row() {
19967 use S3Bucket;
19968
19969 let mut app = test_app();
19970 app.current_service = Service::S3Buckets;
19971 app.service_selected = true;
19972 app.mode = Mode::Normal;
19973
19974 app.s3_state.buckets.items = vec![S3Bucket {
19976 name: "bucket1".to_string(),
19977 region: "us-east-1".to_string(),
19978 creation_date: String::new(),
19979 }];
19980
19981 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19983 let error = "service error: PermanentRedirect".to_string();
19984 app.s3_state
19985 .bucket_errors
19986 .insert("bucket1".to_string(), error);
19987
19988 app.s3_state.selected_row = 0;
19990
19991 app.handle_action(Action::CollapseRow);
19993
19994 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19996 assert_eq!(app.s3_state.selected_row, 0);
19998 }
19999
20000 #[test]
20001 fn test_s3_bucket_collapse_adjusts_scroll_offset() {
20002 use S3Bucket;
20003
20004 let mut app = test_app();
20005 app.current_service = Service::S3Buckets;
20006 app.service_selected = true;
20007 app.mode = Mode::Normal;
20008
20009 app.s3_state.buckets.items = (0..20)
20011 .map(|i| S3Bucket {
20012 name: format!("bucket{}", i),
20013 region: "us-east-1".to_string(),
20014 creation_date: String::new(),
20015 })
20016 .collect();
20017
20018 app.s3_state
20020 .expanded_prefixes
20021 .insert("bucket10".to_string());
20022 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();
20023 app.s3_state
20024 .bucket_errors
20025 .insert("bucket10".to_string(), long_error.clone());
20026
20027 app.s3_state.bucket_visible_rows.set(10);
20029 app.s3_state.bucket_scroll_offset = 10; app.s3_state.selected_row = 10;
20033
20034 app.handle_action(Action::CollapseRow);
20036
20037 assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
20039 assert_eq!(app.s3_state.selected_row, 10);
20041 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
20043 assert!(
20044 app.s3_state.selected_row
20045 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
20046 );
20047 }
20048
20049 #[test]
20050 fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
20051 use S3Bucket;
20052
20053 let mut app = test_app();
20054 app.current_service = Service::S3Buckets;
20055 app.service_selected = true;
20056 app.mode = Mode::Normal;
20057
20058 app.s3_state.buckets.items = vec![
20060 S3Bucket {
20061 name: "bucket1".to_string(),
20062 region: "us-east-1".to_string(),
20063 creation_date: String::new(),
20064 },
20065 S3Bucket {
20066 name: "bucket2".to_string(),
20067 region: "us-east-1".to_string(),
20068 creation_date: String::new(),
20069 },
20070 S3Bucket {
20071 name: "bucket3".to_string(),
20072 region: "us-east-1".to_string(),
20073 creation_date: String::new(),
20074 },
20075 ];
20076
20077 app.s3_state.expanded_prefixes.insert("bucket2".to_string());
20079 app.s3_state.bucket_preview.insert(
20080 "bucket2".to_string(),
20081 vec![
20082 S3Object {
20083 key: "folder1/".to_string(),
20084 is_prefix: true,
20085 size: 0,
20086 last_modified: String::new(),
20087 storage_class: String::new(),
20088 },
20089 S3Object {
20090 key: "file1.txt".to_string(),
20091 is_prefix: false,
20092 size: 100,
20093 last_modified: String::new(),
20094 storage_class: String::new(),
20095 },
20096 ],
20097 );
20098
20099 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
20101 let error = "service error: PermanentRedirect".to_string();
20102 app.s3_state
20103 .bucket_errors
20104 .insert("bucket3".to_string(), error);
20105
20106 app.s3_state.bucket_visible_rows.set(10);
20108 app.s3_state.bucket_scroll_offset = 0;
20109
20110 app.s3_state.selected_row = 3;
20112
20113 app.handle_action(Action::CollapseRow);
20115
20116 assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
20118 assert_eq!(app.s3_state.selected_row, 1);
20120 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
20122 assert!(
20123 app.s3_state.selected_row
20124 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
20125 );
20126 }
20127
20128 #[test]
20129 fn test_s3_collapse_bucket_with_error() {
20130 use S3Bucket;
20131
20132 let mut app = test_app();
20133 app.current_service = Service::S3Buckets;
20134 app.service_selected = true;
20135 app.mode = Mode::Normal;
20136
20137 app.s3_state.buckets.items = vec![
20138 S3Bucket {
20139 name: "bucket1".to_string(),
20140 region: "us-east-1".to_string(),
20141 creation_date: String::new(),
20142 },
20143 S3Bucket {
20144 name: "bucket2".to_string(),
20145 region: "us-east-1".to_string(),
20146 creation_date: String::new(),
20147 },
20148 ];
20149
20150 let error = "service error: unhandled error (PermanentRedirect)".to_string();
20151 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20152 app.s3_state
20153 .bucket_errors
20154 .insert("bucket1".to_string(), error);
20155
20156 app.s3_state.bucket_visible_rows.set(10);
20157 app.s3_state.bucket_scroll_offset = 0;
20158
20159 app.s3_state.selected_row = 0;
20161
20162 app.handle_action(Action::CollapseRow);
20164
20165 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
20167 assert_eq!(app.s3_state.selected_row, 0);
20168 }
20169
20170 #[test]
20171 fn test_s3_collapse_row_with_multiple_error_buckets() {
20172 use S3Bucket;
20173
20174 let mut app = test_app();
20175 app.current_service = Service::S3Buckets;
20176 app.service_selected = true;
20177 app.mode = Mode::Normal;
20178
20179 app.s3_state.buckets.items = vec![
20180 S3Bucket {
20181 name: "bucket1".to_string(),
20182 region: "us-east-1".to_string(),
20183 creation_date: String::new(),
20184 },
20185 S3Bucket {
20186 name: "bucket2".to_string(),
20187 region: "us-east-1".to_string(),
20188 creation_date: String::new(),
20189 },
20190 S3Bucket {
20191 name: "bucket3".to_string(),
20192 region: "us-east-1".to_string(),
20193 creation_date: String::new(),
20194 },
20195 ];
20196
20197 let error = "service error: unhandled error (PermanentRedirect)".to_string();
20198
20199 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20201 app.s3_state
20202 .bucket_errors
20203 .insert("bucket1".to_string(), error.clone());
20204
20205 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
20207 app.s3_state
20208 .bucket_errors
20209 .insert("bucket3".to_string(), error.clone());
20210
20211 app.s3_state.bucket_visible_rows.set(30);
20212 app.s3_state.bucket_scroll_offset = 0;
20213
20214 app.s3_state.selected_row = 2;
20219
20220 app.handle_action(Action::CollapseRow);
20221
20222 assert!(
20224 !app.s3_state.expanded_prefixes.contains("bucket3"),
20225 "bucket3 should be collapsed"
20226 );
20227 assert!(
20228 app.s3_state.expanded_prefixes.contains("bucket1"),
20229 "bucket1 should still be expanded"
20230 );
20231 assert_eq!(app.s3_state.selected_row, 2);
20232 }
20233
20234 #[test]
20235 fn test_s3_collapse_row_nested_only_collapses_one_level() {
20236 use S3Bucket;
20237
20238 let mut app = test_app();
20239 app.current_service = Service::S3Buckets;
20240 app.service_selected = true;
20241 app.mode = Mode::Normal;
20242
20243 app.s3_state.buckets.items = vec![S3Bucket {
20244 name: "bucket1".to_string(),
20245 region: "us-east-1".to_string(),
20246 creation_date: String::new(),
20247 }];
20248
20249 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20251 app.s3_state.bucket_preview.insert(
20252 "bucket1".to_string(),
20253 vec![S3Object {
20254 key: "level1/".to_string(),
20255 size: 0,
20256 last_modified: "2024-01-01T00:00:00Z".to_string(),
20257 is_prefix: true,
20258 storage_class: String::new(),
20259 }],
20260 );
20261
20262 app.s3_state.expanded_prefixes.insert("level1/".to_string());
20264 app.s3_state.prefix_preview.insert(
20265 "level1/".to_string(),
20266 vec![S3Object {
20267 key: "level1/level2/".to_string(),
20268 size: 0,
20269 last_modified: "2024-01-01T00:00:00Z".to_string(),
20270 is_prefix: true,
20271 storage_class: String::new(),
20272 }],
20273 );
20274
20275 app.s3_state
20277 .expanded_prefixes
20278 .insert("level1/level2/".to_string());
20279 app.s3_state.prefix_preview.insert(
20280 "level1/level2/".to_string(),
20281 vec![S3Object {
20282 key: "level1/level2/file.txt".to_string(),
20283 size: 100,
20284 last_modified: "2024-01-01T00:00:00Z".to_string(),
20285 is_prefix: false,
20286 storage_class: String::new(),
20287 }],
20288 );
20289
20290 app.s3_state.bucket_visible_rows.set(10);
20291
20292 app.s3_state.selected_row = 2;
20294
20295 app.handle_action(Action::CollapseRow);
20297
20298 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
20300 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
20302 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
20304 assert_eq!(app.s3_state.selected_row, 1);
20306 }
20307
20308 #[test]
20309 fn test_s3_collapse_row_deeply_nested_file() {
20310 use S3Bucket;
20311
20312 let mut app = test_app();
20313 app.current_service = Service::S3Buckets;
20314 app.service_selected = true;
20315 app.mode = Mode::Normal;
20316
20317 app.s3_state.buckets.items = vec![S3Bucket {
20318 name: "bucket1".to_string(),
20319 region: "us-east-1".to_string(),
20320 creation_date: String::new(),
20321 }];
20322
20323 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20325 app.s3_state.bucket_preview.insert(
20326 "bucket1".to_string(),
20327 vec![S3Object {
20328 key: "level1/".to_string(),
20329 size: 0,
20330 last_modified: "2024-01-01T00:00:00Z".to_string(),
20331 is_prefix: true,
20332 storage_class: String::new(),
20333 }],
20334 );
20335
20336 app.s3_state.expanded_prefixes.insert("level1/".to_string());
20338 app.s3_state.prefix_preview.insert(
20339 "level1/".to_string(),
20340 vec![S3Object {
20341 key: "level1/level2/".to_string(),
20342 size: 0,
20343 last_modified: "2024-01-01T00:00:00Z".to_string(),
20344 is_prefix: true,
20345 storage_class: String::new(),
20346 }],
20347 );
20348
20349 app.s3_state
20351 .expanded_prefixes
20352 .insert("level1/level2/".to_string());
20353 app.s3_state.prefix_preview.insert(
20354 "level1/level2/".to_string(),
20355 vec![S3Object {
20356 key: "level1/level2/file.txt".to_string(),
20357 size: 100,
20358 last_modified: "2024-01-01T00:00:00Z".to_string(),
20359 is_prefix: false,
20360 storage_class: String::new(),
20361 }],
20362 );
20363
20364 app.s3_state.bucket_visible_rows.set(10);
20365
20366 app.s3_state.selected_row = 3;
20368
20369 app.handle_action(Action::CollapseRow);
20371
20372 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
20374 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
20375 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
20376 assert_eq!(app.s3_state.selected_row, 2);
20378 }
20379
20380 #[test]
20381 fn test_s3_bucket_pagination_adjusts_scroll() {
20382 use S3Bucket;
20383
20384 let mut app = test_app();
20385 app.current_service = Service::S3Buckets;
20386 app.service_selected = true;
20387 app.mode = Mode::Normal;
20388
20389 app.s3_state.buckets.items = (0..150)
20391 .map(|i| S3Bucket {
20392 name: format!("bucket{:03}", i),
20393 region: "us-east-1".to_string(),
20394 creation_date: String::new(),
20395 })
20396 .collect();
20397
20398 app.s3_state.bucket_visible_rows.set(20);
20399 app.s3_state.selected_row = 0;
20400 app.s3_state.bucket_scroll_offset = 0;
20401
20402 app.go_to_page(2);
20404
20405 assert_eq!(app.s3_state.selected_row, 50);
20406 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
20408
20409 app.go_to_page(3);
20411
20412 assert_eq!(app.s3_state.selected_row, 100);
20413 assert_eq!(app.s3_state.bucket_scroll_offset, 100);
20414
20415 app.go_to_page(1);
20417
20418 assert_eq!(app.s3_state.selected_row, 0);
20419 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
20420 }
20421
20422 #[test]
20423 fn test_s3_bucket_pagination_uses_page_size() {
20424 use S3Bucket;
20425
20426 let mut app = test_app();
20427 app.current_service = Service::S3Buckets;
20428 app.service_selected = true;
20429 app.mode = Mode::Normal;
20430
20431 app.s3_state.buckets.items = (0..100)
20433 .map(|i| S3Bucket {
20434 name: format!("bucket{:03}", i),
20435 region: "us-east-1".to_string(),
20436 creation_date: String::new(),
20437 })
20438 .collect();
20439
20440 app.s3_state.bucket_visible_rows.set(20);
20441 app.s3_state.selected_row = 0;
20442
20443 assert_eq!(app.s3_state.buckets.page_size.value(), 50);
20445
20446 app.go_to_page(2);
20448 assert_eq!(app.s3_state.selected_row, 50);
20449 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
20450
20451 app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
20453 assert_eq!(app.s3_state.buckets.page_size.value(), 25);
20454
20455 app.go_to_page(2);
20457 assert_eq!(app.s3_state.selected_row, 25);
20458 assert_eq!(app.s3_state.bucket_scroll_offset, 25);
20459 }
20460
20461 #[test]
20462 fn test_s3_bucket_page_size_limits_visible_rows() {
20463 use S3Bucket;
20464
20465 let mut app = test_app();
20466 app.current_service = Service::S3Buckets;
20467 app.service_selected = true;
20468 app.mode = Mode::Normal;
20469
20470 app.s3_state.buckets.items = (0..100)
20472 .map(|i| S3Bucket {
20473 name: format!("bucket{:03}", i),
20474 region: "us-east-1".to_string(),
20475 creation_date: String::new(),
20476 })
20477 .collect();
20478
20479 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20481 assert_eq!(app.s3_state.buckets.page_size.value(), 10);
20482
20483 let total_rows = app.calculate_total_bucket_rows();
20485 assert!(total_rows >= 10, "Should have at least 10 rows");
20489 }
20490
20491 #[test]
20492 fn test_s3_bucket_tab_cycling_in_filter() {
20493 use crate::common::InputFocus;
20494
20495 let mut app = test_app();
20496 app.current_service = Service::S3Buckets;
20497 app.mode = Mode::FilterInput;
20498
20499 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
20501
20502 app.handle_action(Action::NextFilterFocus);
20504 assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
20505
20506 app.handle_action(Action::NextFilterFocus);
20508 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
20509 }
20510
20511 #[test]
20512 fn test_s3_bucket_pagination_navigation_with_arrows() {
20513 use S3Bucket;
20514
20515 let mut app = test_app();
20516 app.current_service = Service::S3Buckets;
20517 app.mode = Mode::FilterInput;
20518
20519 app.s3_state.buckets.items = (0..100)
20521 .map(|i| S3Bucket {
20522 name: format!("bucket{:03}", i),
20523 region: "us-east-1".to_string(),
20524 creation_date: String::new(),
20525 })
20526 .collect();
20527
20528 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20529 app.s3_state.selected_row = 0;
20530
20531 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
20533
20534 app.handle_action(Action::NextItem);
20536 assert_eq!(app.s3_state.selected_row, 10);
20537
20538 app.handle_action(Action::NextItem);
20540 assert_eq!(app.s3_state.selected_row, 20);
20541
20542 app.handle_action(Action::PrevItem);
20544 assert_eq!(app.s3_state.selected_row, 10);
20545 }
20546
20547 #[test]
20548 fn test_s3_bucket_go_to_page_shows_correct_buckets() {
20549 use S3Bucket;
20550
20551 let mut app = test_app();
20552 app.current_service = Service::S3Buckets;
20553 app.service_selected = true;
20554 app.mode = Mode::Normal;
20555
20556 app.s3_state.buckets.items = (0..100)
20558 .map(|i| S3Bucket {
20559 name: format!("bucket{:03}", i),
20560 region: "us-east-1".to_string(),
20561 creation_date: String::new(),
20562 })
20563 .collect();
20564
20565 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20566
20567 app.go_to_page(2);
20569 assert_eq!(app.s3_state.selected_row, 10);
20570
20571 app.go_to_page(5);
20573 assert_eq!(app.s3_state.selected_row, 40);
20574 }
20575
20576 #[test]
20577 fn test_s3_bucket_left_right_arrows_change_pages() {
20578 use S3Bucket;
20579
20580 let mut app = test_app();
20581 app.current_service = Service::S3Buckets;
20582 app.mode = Mode::FilterInput;
20583
20584 app.s3_state.buckets.items = (0..100)
20586 .map(|i| S3Bucket {
20587 name: format!("bucket{:03}", i),
20588 region: "us-east-1".to_string(),
20589 creation_date: String::new(),
20590 })
20591 .collect();
20592
20593 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20594 app.s3_state.selected_row = 0;
20595 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
20596
20597 app.handle_action(Action::PageDown);
20599 assert_eq!(app.s3_state.selected_row, 10);
20600
20601 app.handle_action(Action::PageDown);
20603 assert_eq!(app.s3_state.selected_row, 20);
20604
20605 app.handle_action(Action::PageUp);
20607 assert_eq!(app.s3_state.selected_row, 10);
20608 }
20609
20610 #[test]
20611 fn test_apig_detail_tab_navigation() {
20612 use crate::apig::api::RestApi;
20613 use crate::ui::apig::ApiDetailTab;
20614
20615 let mut app = test_app();
20616 app.current_service = Service::ApiGatewayApis;
20617 app.apig_state.current_api = Some(RestApi {
20618 id: "test123".to_string(),
20619 name: "Test API".to_string(),
20620 description: "Test".to_string(),
20621 created_date: "2024-01-01".to_string(),
20622 api_key_source: "HEADER".to_string(),
20623 endpoint_configuration: "REGIONAL".to_string(),
20624 protocol_type: "REST".to_string(),
20625 disable_execute_api_endpoint: false,
20626 status: "AVAILABLE".to_string(),
20627 });
20628
20629 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20631
20632 app.handle_action(Action::NextDetailTab);
20634 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20635
20636 app.handle_action(Action::PrevDetailTab);
20638 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20639
20640 app.handle_action(Action::NextDetailTab);
20642 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20643
20644 app.handle_action(Action::PrevDetailTab);
20646 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20647
20648 app.handle_action(Action::NextDetailTab);
20650 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20651 }
20652
20653 #[test]
20654 fn test_apig_routes_expand_collapse() {
20655 use crate::apig::api::RestApi;
20656 use crate::apig::route::Route;
20657 use crate::ui::apig::ApiDetailTab;
20658
20659 let mut app = test_app();
20660 app.current_service = Service::ApiGatewayApis;
20661 app.apig_state.current_api = Some(RestApi {
20662 id: "test123".to_string(),
20663 name: "Test API".to_string(),
20664 description: "Test".to_string(),
20665 created_date: "2024-01-01".to_string(),
20666 api_key_source: "HEADER".to_string(),
20667 endpoint_configuration: "REGIONAL".to_string(),
20668 protocol_type: "HTTP".to_string(),
20669 disable_execute_api_endpoint: false,
20670 status: "AVAILABLE".to_string(),
20671 });
20672 app.apig_state.detail_tab = ApiDetailTab::Routes;
20673
20674 let virtual_parent = Route {
20676 route_id: "virtual_/api".to_string(),
20677 route_key: "/api".to_string(),
20678 target: String::new(), authorization_type: String::new(),
20680 api_key_required: false,
20681 display_name: String::new(),
20682 arn: String::new(),
20683 };
20684 let child_route = Route {
20685 route_id: "1".to_string(),
20686 route_key: "/api/users".to_string(),
20687 target: "integration1".to_string(),
20688 authorization_type: "NONE".to_string(),
20689 api_key_required: false,
20690 display_name: String::new(),
20691 arn: String::new(),
20692 };
20693
20694 app.apig_state.routes.items = vec![virtual_parent];
20695 app.apig_state
20696 .route_children
20697 .insert("/api".to_string(), vec![child_route]);
20698
20699 assert!(app.apig_state.expanded_routes.is_empty());
20701
20702 app.apig_state.routes.selected = 0;
20704 app.handle_action(Action::ExpandRow);
20705 assert!(app.apig_state.expanded_routes.contains("/api"));
20706
20707 app.handle_action(Action::CollapseRow);
20709 assert!(!app.apig_state.expanded_routes.contains("/api"));
20710
20711 app.handle_action(Action::ExpandRow);
20713 assert!(app.apig_state.expanded_routes.contains("/api"));
20714 }
20715
20716 #[test]
20717 fn test_apig_routes_navigation() {
20718 use crate::apig::api::RestApi;
20719 use crate::apig::route::Route;
20720 use crate::ui::apig::ApiDetailTab;
20721
20722 let mut app = test_app();
20723 app.mode = Mode::Normal;
20724 app.service_selected = true;
20725 app.current_service = Service::ApiGatewayApis;
20726 app.apig_state.current_api = Some(RestApi {
20727 id: "test123".to_string(),
20728 name: "Test API".to_string(),
20729 description: "Test".to_string(),
20730 created_date: "2024-01-01".to_string(),
20731 api_key_source: "HEADER".to_string(),
20732 endpoint_configuration: "REGIONAL".to_string(),
20733 protocol_type: "HTTP".to_string(),
20734 disable_execute_api_endpoint: false,
20735 status: "AVAILABLE".to_string(),
20736 });
20737 app.apig_state.detail_tab = ApiDetailTab::Routes;
20738 app.apig_state.routes.items = vec![
20739 Route {
20740 route_id: "1".to_string(),
20741 route_key: "/api/users".to_string(),
20742 target: "integration1".to_string(),
20743 authorization_type: "NONE".to_string(),
20744 api_key_required: false,
20745 display_name: String::new(),
20746 arn: String::new(),
20747 },
20748 Route {
20749 route_id: "2".to_string(),
20750 route_key: "/health".to_string(),
20751 target: "integration2".to_string(),
20752 authorization_type: "NONE".to_string(),
20753 api_key_required: false,
20754 display_name: String::new(),
20755 arn: String::new(),
20756 },
20757 ];
20758
20759 assert_eq!(app.apig_state.routes.selected, 0);
20761
20762 app.handle_action(Action::NextItem);
20764 assert_eq!(app.apig_state.routes.selected, 1);
20765
20766 app.handle_action(Action::NextItem);
20768 assert_eq!(app.apig_state.routes.selected, 1);
20769
20770 app.handle_action(Action::PrevItem);
20772 assert_eq!(app.apig_state.routes.selected, 0);
20773
20774 app.handle_action(Action::PrevItem);
20776 assert_eq!(app.apig_state.routes.selected, 0);
20777 }
20778
20779 #[test]
20780 fn test_apig_routes_expand_jumps_to_child() {
20781 use crate::apig::api::RestApi;
20782 use crate::apig::route::Route;
20783 use crate::ui::apig::ApiDetailTab;
20784 use std::collections::HashMap;
20785
20786 let mut app = test_app();
20787 app.mode = Mode::Normal;
20788 app.service_selected = true;
20789 app.current_service = Service::ApiGatewayApis;
20790 app.apig_state.current_api = Some(RestApi {
20791 id: "test123".to_string(),
20792 name: "Test API".to_string(),
20793 description: "Test".to_string(),
20794 created_date: "2024-01-01".to_string(),
20795 api_key_source: "HEADER".to_string(),
20796 endpoint_configuration: "REGIONAL".to_string(),
20797 protocol_type: "HTTP".to_string(),
20798 disable_execute_api_endpoint: false,
20799 status: "AVAILABLE".to_string(),
20800 });
20801 app.apig_state.detail_tab = ApiDetailTab::Routes;
20802
20803 app.apig_state.routes.items = vec![Route {
20805 route_id: "virtual_/api".to_string(),
20806 route_key: "/api".to_string(),
20807 target: String::new(),
20808 authorization_type: String::new(),
20809 api_key_required: false,
20810 display_name: String::new(),
20811 arn: String::new(),
20812 }];
20813
20814 let mut children = HashMap::new();
20815 children.insert(
20816 "/api".to_string(),
20817 vec![Route {
20818 route_id: "1".to_string(),
20819 route_key: "/api/users".to_string(),
20820 target: "integration1".to_string(),
20821 authorization_type: "NONE".to_string(),
20822 api_key_required: false,
20823 display_name: String::new(),
20824 arn: String::new(),
20825 }],
20826 );
20827 app.apig_state.route_children = children;
20828
20829 assert_eq!(app.apig_state.routes.selected, 0);
20831 assert!(!app.apig_state.expanded_routes.contains("/api"));
20832
20833 app.handle_action(Action::ExpandRow);
20835 assert!(app.apig_state.expanded_routes.contains("/api"));
20836 assert_eq!(app.apig_state.routes.selected, 0);
20837
20838 app.handle_action(Action::ExpandRow);
20840 assert_eq!(app.apig_state.routes.selected, 1);
20841 }
20842
20843 #[test]
20844 fn test_apig_filter_only_when_focused() {
20845 use crate::apig::api::RestApi;
20846 use crate::ui::apig::ApiDetailTab;
20847
20848 let mut app = test_app();
20849 app.current_service = Service::ApiGatewayApis;
20850 app.apig_state.current_api = Some(RestApi {
20851 id: "test".to_string(),
20852 name: "Test API".to_string(),
20853 description: "Test".to_string(),
20854 created_date: "2024-01-01".to_string(),
20855 api_key_source: "HEADER".to_string(),
20856 endpoint_configuration: "REGIONAL".to_string(),
20857 protocol_type: "HTTP".to_string(),
20858 disable_execute_api_endpoint: false,
20859 status: "AVAILABLE".to_string(),
20860 });
20861 app.apig_state.detail_tab = ApiDetailTab::Routes;
20862 app.mode = Mode::FilterInput;
20863
20864 app.apig_state.input_focus = InputFocus::Pagination;
20866 app.handle_action(Action::FilterInput('x'));
20867 assert_eq!(app.apig_state.route_filter, "");
20868
20869 app.apig_state.input_focus = InputFocus::Filter;
20871 app.handle_action(Action::FilterInput('x'));
20872 assert_eq!(app.apig_state.route_filter, "x");
20873 }
20874
20875 #[test]
20876 fn test_apig_routes_and_resources_use_same_render_function() {
20877 let source = include_str!("ui/apig.rs");
20879 let render_calls: Vec<_> = source
20880 .match_indices("crate::ui::table::render_tree_table")
20881 .collect();
20882
20883 assert_eq!(
20885 render_calls.len(),
20886 2,
20887 "Both routes and resources must use render_tree_table"
20888 );
20889 }
20890
20891 #[test]
20892 fn test_s3_uses_same_render_function() {
20893 let source = include_str!("ui/s3.rs");
20895 let render_calls: Vec<_> = source
20896 .match_indices("crate::ui::table::render_tree_table")
20897 .collect();
20898
20899 assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
20901 }
20902
20903 #[test]
20904 fn test_search_icon_has_proper_border_spacing() {
20905 use crate::ui::SEARCH_ICON;
20908
20909 assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
20910 assert!(
20911 SEARCH_ICON.ends_with("─"),
20912 "Should end with dash for proper border spacing"
20913 );
20914 assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
20915 }
20916
20917 #[test]
20918 fn test_apig_expand_with_filter() {
20919 use crate::apig::api::RestApi;
20920 use crate::apig::route::Route;
20921 use crate::ui::apig::ApiDetailTab;
20922 use std::collections::HashMap;
20923
20924 let mut app = test_app();
20925 app.current_service = Service::ApiGatewayApis;
20926 app.apig_state.current_api = Some(RestApi {
20927 id: "test123".to_string(),
20928 name: "Test API".to_string(),
20929 description: "Test".to_string(),
20930 created_date: "2024-01-01".to_string(),
20931 api_key_source: "HEADER".to_string(),
20932 endpoint_configuration: "REGIONAL".to_string(),
20933 protocol_type: "HTTP".to_string(),
20934 disable_execute_api_endpoint: false,
20935 status: "AVAILABLE".to_string(),
20936 });
20937 app.apig_state.detail_tab = ApiDetailTab::Routes;
20938
20939 app.apig_state.routes.items = vec![Route {
20941 route_id: "0".to_string(),
20942 route_key: "/api".to_string(),
20943 target: "".to_string(),
20944 authorization_type: "NONE".to_string(),
20945 api_key_required: false,
20946 display_name: "/api".to_string(),
20947 arn: String::new(),
20948 }];
20949
20950 let mut children = HashMap::new();
20951 children.insert(
20952 "/api".to_string(),
20953 vec![Route {
20954 route_id: "1".to_string(),
20955 route_key: "GET".to_string(),
20956 target: "integration1".to_string(),
20957 authorization_type: "NONE".to_string(),
20958 api_key_required: false,
20959 display_name: "GET".to_string(),
20960 arn: String::new(),
20961 }],
20962 );
20963 app.apig_state.route_children = children;
20964
20965 app.apig_state.route_filter = "GET".to_string();
20967
20968 assert_eq!(app.apig_state.routes.selected, 0);
20971 assert!(!app.apig_state.expanded_routes.contains("/api"));
20972
20973 app.handle_action(Action::ExpandRow);
20974
20975 assert!(app.apig_state.expanded_routes.contains("/api"));
20977 }
20978
20979 #[test]
20980 fn test_apig_console_url_routes() {
20981 use crate::apig::api::RestApi;
20982 use crate::apig::route::Route;
20983 use crate::ui::apig::ApiDetailTab;
20984
20985 let mut app = test_app();
20986 app.current_service = Service::ApiGatewayApis;
20987 app.config.region = "us-east-1".to_string();
20988
20989 let url = app.get_console_url();
20991 assert!(url.contains("apigateway/main/apis"));
20992 assert!(url.contains("region=us-east-1"));
20993
20994 app.apig_state.current_api = Some(RestApi {
20996 id: "2todvod3n0".to_string(),
20997 name: "Test API".to_string(),
20998 description: "Test".to_string(),
20999 created_date: "2024-01-01".to_string(),
21000 api_key_source: "HEADER".to_string(),
21001 endpoint_configuration: "REGIONAL".to_string(),
21002 protocol_type: "HTTP".to_string(),
21003 disable_execute_api_endpoint: false,
21004 status: "AVAILABLE".to_string(),
21005 });
21006 app.apig_state.detail_tab = ApiDetailTab::Routes;
21007 app.apig_state.routes.items = vec![Route {
21008 route_id: "eizmisr".to_string(),
21009 route_key: "GET /test".to_string(),
21010 target: "integration1".to_string(),
21011 authorization_type: "NONE".to_string(),
21012 api_key_required: false,
21013 display_name: "GET /test".to_string(),
21014 arn: String::new(),
21015 }];
21016 app.apig_state.routes.selected = 0;
21017
21018 let url = app.get_console_url();
21019 assert!(url.contains("apigateway/main/develop/routes"));
21020 assert!(url.contains("api=2todvod3n0"));
21021 assert!(url.contains("routes=eizmisr"));
21022 assert!(url.contains("region=us-east-1"));
21023 }
21024
21025 #[test]
21026 fn test_apig_console_url_resources() {
21027 use crate::apig::api::RestApi;
21028 use crate::apig::resource::Resource;
21029 use crate::ui::apig::ApiDetailTab;
21030
21031 let mut app = test_app();
21032 app.current_service = Service::ApiGatewayApis;
21033 app.config.region = "us-east-1".to_string();
21034
21035 app.apig_state.current_api = Some(RestApi {
21037 id: "2j9j50ze47".to_string(),
21038 name: "Test API".to_string(),
21039 description: "Test".to_string(),
21040 created_date: "2024-01-01".to_string(),
21041 api_key_source: "HEADER".to_string(),
21042 endpoint_configuration: "REGIONAL".to_string(),
21043 protocol_type: "REST".to_string(),
21044 disable_execute_api_endpoint: false,
21045 status: "AVAILABLE".to_string(),
21046 });
21047 app.apig_state.detail_tab = ApiDetailTab::Routes;
21048 app.apig_state.resources.items = vec![Resource {
21049 id: "abc123".to_string(),
21050 path: "/test".to_string(),
21051 parent_id: None,
21052 methods: vec![],
21053 display_name: "/test".to_string(),
21054 arn: String::new(),
21055 }];
21056 app.apig_state.resources.selected = 0;
21057
21058 let url = app.get_console_url();
21059 assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
21060 assert!(url.contains("api=2j9j50ze47"));
21061 assert!(url.contains("#abc123"));
21062 assert!(url.contains("region=us-east-1"));
21063 }
21064
21065 #[test]
21066 fn test_apig_console_url_routes_parent_vs_leaf() {
21067 use crate::apig::api::RestApi;
21068 use crate::apig::route::Route;
21069 use crate::ui::apig::ApiDetailTab;
21070 use std::collections::HashMap;
21071
21072 let mut app = test_app();
21073 app.current_service = Service::ApiGatewayApis;
21074 app.config.region = "us-east-1".to_string();
21075
21076 app.apig_state.current_api = Some(RestApi {
21077 id: "2todvod3n0".to_string(),
21078 name: "Test API".to_string(),
21079 description: "Test".to_string(),
21080 created_date: "2024-01-01".to_string(),
21081 api_key_source: "HEADER".to_string(),
21082 endpoint_configuration: "REGIONAL".to_string(),
21083 protocol_type: "HTTP".to_string(),
21084 disable_execute_api_endpoint: false,
21085 status: "AVAILABLE".to_string(),
21086 });
21087 app.apig_state.detail_tab = ApiDetailTab::Routes;
21088
21089 app.apig_state.routes.items = vec![Route {
21091 route_id: "parent".to_string(),
21092 route_key: "/v1/get/jobs".to_string(),
21093 target: "".to_string(), authorization_type: "NONE".to_string(),
21095 api_key_required: false,
21096 display_name: "/v1/get/jobs".to_string(),
21097 arn: String::new(),
21098 }];
21099
21100 let mut children = HashMap::new();
21102 children.insert(
21103 "/v1/get/jobs".to_string(),
21104 vec![Route {
21105 route_id: "1iz9vtl".to_string(),
21106 route_key: "GET".to_string(),
21107 target: "integration1".to_string(), authorization_type: "NONE".to_string(),
21109 api_key_required: false,
21110 display_name: "GET".to_string(),
21111 arn: String::new(),
21112 }],
21113 );
21114 app.apig_state.route_children = children;
21115
21116 app.apig_state.routes.selected = 0;
21118 let url = app.get_console_url();
21119 assert!(url.contains("apigateway/main/develop/routes"));
21120 assert!(url.contains("api=2todvod3n0"));
21121 assert!(
21122 !url.contains("routes="),
21123 "Parent node should not include routes parameter"
21124 );
21125
21126 app.apig_state
21128 .expanded_routes
21129 .insert("/v1/get/jobs".to_string());
21130 app.apig_state.routes.selected = 1;
21131 let url = app.get_console_url();
21132 assert!(
21133 url.contains("routes=1iz9vtl"),
21134 "Leaf node should include routes parameter: {}",
21135 url
21136 );
21137 }
21138
21139 #[test]
21140 fn test_apig_preferences_context() {
21141 use crate::apig::api::RestApi;
21142 use crate::ui::apig::ApiDetailTab;
21143
21144 let mut app = test_app();
21145 app.current_service = Service::ApiGatewayApis;
21146
21147 assert!(
21149 app.apig_state.current_api.is_none(),
21150 "Should be in list view"
21151 );
21152
21153 app.apig_state.current_api = Some(RestApi {
21155 id: "test123".to_string(),
21156 name: "Test API".to_string(),
21157 description: "Test".to_string(),
21158 created_date: "2024-01-01".to_string(),
21159 api_key_source: "HEADER".to_string(),
21160 endpoint_configuration: "REGIONAL".to_string(),
21161 protocol_type: "HTTP".to_string(),
21162 disable_execute_api_endpoint: false,
21163 status: "AVAILABLE".to_string(),
21164 });
21165 app.apig_state.detail_tab = ApiDetailTab::Routes;
21166
21167 assert!(
21168 app.apig_state.current_api.is_some(),
21169 "Should be in detail view"
21170 );
21171 }
21173
21174 #[test]
21175 fn test_apig_route_columns() {
21176 use crate::apig::route::Column as RouteColumn;
21177
21178 let cols = RouteColumn::all();
21180 assert_eq!(cols.len(), 5);
21181
21182 assert_eq!(RouteColumn::RouteKey.id(), "route_key");
21184 assert_eq!(RouteColumn::RouteId.id(), "route_id");
21185 assert_eq!(RouteColumn::Arn.id(), "arn");
21186 assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
21187 assert_eq!(RouteColumn::Target.id(), "target");
21188
21189 assert_eq!(
21191 RouteColumn::from_id("route_key"),
21192 Some(RouteColumn::RouteKey)
21193 );
21194 assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
21195 assert_eq!(RouteColumn::from_id("invalid"), None);
21196 }
21197
21198 #[test]
21199 fn test_apig_yank_copies_route_arn() {
21200 use crate::apig::api::RestApi;
21201 use crate::ui::apig::ApiDetailTab;
21202
21203 let mut app = test_app();
21204 app.current_service = Service::ApiGatewayApis;
21205 app.apig_state.current_api = Some(RestApi {
21206 id: "test123".to_string(),
21207 name: "Test API".to_string(),
21208 description: "Test".to_string(),
21209 created_date: "2024-01-01".to_string(),
21210 api_key_source: "HEADER".to_string(),
21211 endpoint_configuration: "REGIONAL".to_string(),
21212 protocol_type: "HTTP".to_string(),
21213 disable_execute_api_endpoint: false,
21214 status: "AVAILABLE".to_string(),
21215 });
21216 app.apig_state.detail_tab = ApiDetailTab::Routes;
21217
21218 app.apig_state.routes.items = vec![
21220 Route {
21221 route_id: "route1".to_string(),
21222 route_key: "GET /users".to_string(),
21223 target: "integrations/abc".to_string(),
21224 authorization_type: "NONE".to_string(),
21225 api_key_required: false,
21226 display_name: "GET /users".to_string(),
21227 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
21228 },
21229 Route {
21230 route_id: "route2".to_string(),
21231 route_key: "POST /users".to_string(),
21232 target: "integrations/def".to_string(),
21233 authorization_type: "AWS_IAM".to_string(),
21234 api_key_required: true,
21235 display_name: "POST /users".to_string(),
21236 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
21237 },
21238 ];
21239
21240 app.apig_state.routes.selected = 0;
21242
21243 assert_eq!(
21245 app.apig_state.routes.items[0].arn,
21246 "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
21247 );
21248 }
21249
21250 #[test]
21251 fn test_apig_yank_ignores_empty_arn() {
21252 use crate::apig::api::RestApi;
21253 use crate::ui::apig::ApiDetailTab;
21254
21255 let mut app = test_app();
21256 app.current_service = Service::ApiGatewayApis;
21257 app.apig_state.current_api = Some(RestApi {
21258 id: "test123".to_string(),
21259 name: "Test API".to_string(),
21260 description: "Test".to_string(),
21261 created_date: "2024-01-01".to_string(),
21262 api_key_source: "HEADER".to_string(),
21263 endpoint_configuration: "REGIONAL".to_string(),
21264 protocol_type: "HTTP".to_string(),
21265 disable_execute_api_endpoint: false,
21266 status: "AVAILABLE".to_string(),
21267 });
21268 app.apig_state.detail_tab = ApiDetailTab::Routes;
21269
21270 app.apig_state.routes.items = vec![Route {
21272 route_id: String::new(),
21273 route_key: "/users".to_string(),
21274 target: String::new(), authorization_type: String::new(),
21276 api_key_required: false,
21277 display_name: "/users".to_string(),
21278 arn: String::new(), }];
21280
21281 app.apig_state.routes.selected = 0;
21282
21283 assert!(
21285 app.apig_state.routes.items[0].arn.is_empty(),
21286 "Virtual parent should have empty ARN"
21287 );
21288 }
21289
21290 #[test]
21291 fn test_apig_route_column_toggle() {
21292 use crate::apig::api::RestApi;
21293 use crate::apig::route::Column as RouteColumn;
21294 use crate::keymap::Action;
21295 use crate::ui::apig::ApiDetailTab;
21296
21297 let mut app = test_app();
21298 app.current_service = Service::ApiGatewayApis;
21299 app.mode = Mode::ColumnSelector;
21300 app.apig_state.current_api = Some(RestApi {
21301 id: "test123".to_string(),
21302 name: "Test API".to_string(),
21303 description: "Test".to_string(),
21304 created_date: "2024-01-01".to_string(),
21305 api_key_source: "HEADER".to_string(),
21306 endpoint_configuration: "REGIONAL".to_string(),
21307 protocol_type: "HTTP".to_string(),
21308 disable_execute_api_endpoint: false,
21309 status: "AVAILABLE".to_string(),
21310 });
21311 app.apig_state.detail_tab = ApiDetailTab::Routes;
21312
21313 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21315
21316 app.column_selector_index = 1;
21318 app.handle_action(Action::ToggleColumn);
21319 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21320 assert!(app
21321 .apig_route_visible_column_ids
21322 .contains(&RouteColumn::RouteKey.id()));
21323
21324 app.column_selector_index = 3;
21326 app.handle_action(Action::ToggleColumn);
21327
21328 assert_eq!(app.apig_route_visible_column_ids.len(), 4);
21330 assert!(!app
21331 .apig_route_visible_column_ids
21332 .contains(&RouteColumn::Arn.id()));
21333
21334 app.handle_action(Action::ToggleColumn);
21336 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21337 assert!(app
21338 .apig_route_visible_column_ids
21339 .contains(&RouteColumn::Arn.id()));
21340 }
21341
21342 #[test]
21343 fn test_apig_resource_column_toggle() {
21344 use crate::apig::api::RestApi;
21345 use crate::apig::resource::Column as ResourceColumn;
21346 use crate::keymap::Action;
21347 use crate::ui::apig::ApiDetailTab;
21348
21349 let mut app = test_app();
21350 app.current_service = Service::ApiGatewayApis;
21351 app.mode = Mode::ColumnSelector;
21352 app.apig_state.current_api = Some(RestApi {
21353 id: "test123".to_string(),
21354 name: "Test API".to_string(),
21355 description: "Test".to_string(),
21356 created_date: "2024-01-01".to_string(),
21357 api_key_source: "HEADER".to_string(),
21358 endpoint_configuration: "REGIONAL".to_string(),
21359 protocol_type: "REST".to_string(), disable_execute_api_endpoint: false,
21361 status: "AVAILABLE".to_string(),
21362 });
21363 app.apig_state.detail_tab = ApiDetailTab::Routes; assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21367
21368 app.column_selector_index = 1;
21370 app.handle_action(Action::ToggleColumn);
21371 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21372 assert!(app
21373 .apig_resource_visible_column_ids
21374 .contains(&ResourceColumn::Path.id()));
21375
21376 app.column_selector_index = 3;
21378 app.handle_action(Action::ToggleColumn);
21379
21380 assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
21382 assert!(!app
21383 .apig_resource_visible_column_ids
21384 .contains(&ResourceColumn::Arn.id()));
21385
21386 app.handle_action(Action::ToggleColumn);
21388 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21389 assert!(app
21390 .apig_resource_visible_column_ids
21391 .contains(&ResourceColumn::Arn.id()));
21392 }
21393
21394 #[test]
21395 fn test_cloudtrail_filter_input() {
21396 let mut app = App::new_without_client("default".to_string(), None);
21397 app.service_selected = true;
21398 app.current_service = Service::CloudTrailEvents;
21399
21400 app.handle_action(Action::StartFilter);
21401 assert_eq!(app.mode, Mode::FilterInput);
21402
21403 app.cloudtrail_state.table.filter = "test".to_string();
21404
21405 assert_eq!(app.cloudtrail_state.table.filter, "test");
21406 }
21407
21408 #[test]
21409 fn test_cloudtrail_row_expansion() {
21410 let mut app = App::new_without_client("default".to_string(), None);
21411 app.service_selected = true;
21412 app.current_service = Service::CloudTrailEvents;
21413 app.cloudtrail_state.table.items = vec![CloudTrailEvent {
21414 event_name: "Event1".to_string(),
21415 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21416 username: "user1".to_string(),
21417 event_source: "s3.amazonaws.com".to_string(),
21418 resource_type: "Bucket".to_string(),
21419 resource_name: "my-bucket".to_string(),
21420 read_only: "false".to_string(),
21421 aws_region: "us-east-1".to_string(),
21422 event_id: "abc123".to_string(),
21423 access_key_id: "AKIA...".to_string(),
21424 source_ip_address: "1.2.3.4".to_string(),
21425 error_code: "".to_string(),
21426 request_id: "req-123".to_string(),
21427 event_type: "AwsApiCall".to_string(),
21428 cloud_trail_event_json: "{}".to_string(),
21429 }];
21430
21431 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21432
21433 app.expand_row();
21434 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
21435
21436 app.collapse_row();
21437 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21438 }
21439
21440 #[test]
21441 fn test_cloudtrail_service_initialization() {
21442 let app = App::new_without_client("default".to_string(), None);
21443 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
21444 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21445 }
21446
21447 #[test]
21448 fn test_cloudtrail_service_name() {
21449 assert_eq!(
21450 Service::CloudTrailEvents.name(),
21451 "CloudTrail › Event History"
21452 );
21453 }
21454
21455 #[test]
21456 fn test_cloudtrail_in_service_picker() {
21457 let app = App::new_without_client("default".to_string(), None);
21458 assert!(app
21459 .service_picker
21460 .services
21461 .contains(&"CloudTrail › Event History"));
21462 }
21463
21464 #[test]
21465 fn test_cloudtrail_service_selection() {
21466 let mut app = App::new_without_client("default".to_string(), None);
21467 app.current_service = Service::CloudTrailEvents;
21468 app.service_selected = true;
21469
21470 assert_eq!(app.current_service, Service::CloudTrailEvents);
21471 assert!(app.service_selected);
21472 }
21473
21474 #[test]
21475 fn test_cloudtrail_filter_resets_selection() {
21476 let mut app = App::new_without_client("default".to_string(), None);
21477 app.service_selected = true;
21478 app.current_service = Service::CloudTrailEvents;
21479 app.cloudtrail_state.table.selected = 5;
21480 app.cloudtrail_state.table.expanded_item = Some(3);
21481
21482 app.handle_action(Action::StartFilter);
21483 app.apply_filter_operation(|_| {});
21484
21485 assert_eq!(app.cloudtrail_state.table.selected, 0);
21486 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21487 }
21488
21489 #[test]
21490 fn test_cloudtrail_column_toggle() {
21491 let mut app = App::new_without_client("default".to_string(), None);
21492 app.service_selected = true;
21493 app.current_service = Service::CloudTrailEvents;
21494 app.mode = Mode::ColumnSelector;
21495
21496 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21498
21499 app.column_selector_index = 7;
21501 app.handle_action(Action::ToggleColumn);
21502
21503 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
21505 }
21506
21507 #[test]
21508 fn test_cloudtrail_tab_cycles_filter_focus() {
21509 let mut app = App::new_without_client("default".to_string(), None);
21510 app.service_selected = true;
21511 app.current_service = Service::CloudTrailEvents;
21512 app.mode = Mode::FilterInput;
21513 app.cloudtrail_state.input_focus = InputFocus::Filter;
21514
21515 app.handle_action(Action::NextFilterFocus);
21517 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21518
21519 app.handle_action(Action::NextFilterFocus);
21521 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21522 }
21523
21524 #[test]
21525 fn test_cloudtrail_shift_tab_cycles_filter_focus() {
21526 let mut app = App::new_without_client("default".to_string(), None);
21527 app.service_selected = true;
21528 app.current_service = Service::CloudTrailEvents;
21529 app.mode = Mode::FilterInput;
21530 app.cloudtrail_state.input_focus = InputFocus::Filter;
21531
21532 app.handle_action(Action::PrevFilterFocus);
21534 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21535
21536 app.handle_action(Action::PrevFilterFocus);
21538 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21539 }
21540
21541 #[test]
21542 fn test_cloudtrail_detail_view_tab_cycles_focus() {
21543 let mut app = App::new_without_client("default".to_string(), None);
21544 app.service_selected = true;
21545 app.current_service = Service::CloudTrailEvents;
21546 app.mode = Mode::Normal;
21547 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21548 event_name: "PutObject".to_string(),
21549 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21550 username: "user".to_string(),
21551 event_source: "s3.amazonaws.com".to_string(),
21552 resource_type: "AWS::S3::Bucket".to_string(),
21553 resource_name: "my-bucket".to_string(),
21554 read_only: "false".to_string(),
21555 aws_region: "us-east-1".to_string(),
21556 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21557 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21558 source_ip_address: "192.0.2.1".to_string(),
21559 error_code: "".to_string(),
21560 request_id: "req-123".to_string(),
21561 event_type: "AwsApiCall".to_string(),
21562 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21563 });
21564
21565 assert_eq!(
21567 app.cloudtrail_state.detail_focus,
21568 CloudTrailDetailFocus::Resources
21569 );
21570
21571 app.handle_action(Action::NextDetailTab);
21573 assert_eq!(
21574 app.cloudtrail_state.detail_focus,
21575 CloudTrailDetailFocus::EventRecord
21576 );
21577
21578 app.handle_action(Action::NextDetailTab);
21580 assert_eq!(
21581 app.cloudtrail_state.detail_focus,
21582 CloudTrailDetailFocus::Resources
21583 );
21584 }
21585
21586 #[test]
21587 fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
21588 let mut app = App::new_without_client("default".to_string(), None);
21589 app.service_selected = true;
21590 app.current_service = Service::CloudTrailEvents;
21591 app.mode = Mode::Normal;
21592 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21593 event_name: "PutObject".to_string(),
21594 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21595 username: "user".to_string(),
21596 event_source: "s3.amazonaws.com".to_string(),
21597 resource_type: "AWS::S3::Bucket".to_string(),
21598 resource_name: "my-bucket".to_string(),
21599 read_only: "false".to_string(),
21600 aws_region: "us-east-1".to_string(),
21601 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21602 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21603 source_ip_address: "192.0.2.1".to_string(),
21604 error_code: "".to_string(),
21605 request_id: "req-123".to_string(),
21606 event_type: "AwsApiCall".to_string(),
21607 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21608 });
21609
21610 assert_eq!(
21612 app.cloudtrail_state.detail_focus,
21613 CloudTrailDetailFocus::Resources
21614 );
21615
21616 app.handle_action(Action::PrevDetailTab);
21618 assert_eq!(
21619 app.cloudtrail_state.detail_focus,
21620 CloudTrailDetailFocus::EventRecord
21621 );
21622
21623 app.handle_action(Action::PrevDetailTab);
21625 assert_eq!(
21626 app.cloudtrail_state.detail_focus,
21627 CloudTrailDetailFocus::Resources
21628 );
21629 }
21630
21631 #[test]
21632 fn test_cloudtrail_json_scroll_with_arrow_keys() {
21633 let mut app = App::new_without_client("default".to_string(), None);
21634 app.service_selected = true;
21635 app.current_service = Service::CloudTrailEvents;
21636 app.mode = Mode::Normal;
21637 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21638 event_name: "PutObject".to_string(),
21639 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21640 username: "user".to_string(),
21641 event_source: "s3.amazonaws.com".to_string(),
21642 resource_type: "AWS::S3::Bucket".to_string(),
21643 resource_name: "my-bucket".to_string(),
21644 read_only: "false".to_string(),
21645 aws_region: "us-east-1".to_string(),
21646 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21647 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21648 source_ip_address: "192.0.2.1".to_string(),
21649 error_code: "".to_string(),
21650 request_id: "req-123".to_string(),
21651 event_type: "AwsApiCall".to_string(),
21652 cloud_trail_event_json: (0..50)
21653 .map(|i| format!("line {}", i))
21654 .collect::<Vec<_>>()
21655 .join("\n"),
21656 });
21657 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21658 app.cloudtrail_state.event_json_scroll = 0;
21659
21660 app.handle_action(Action::NextItem);
21662 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21663
21664 app.handle_action(Action::NextItem);
21665 assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
21666
21667 app.handle_action(Action::PrevItem);
21669 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21670
21671 app.handle_action(Action::PrevItem);
21672 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21673
21674 app.handle_action(Action::PrevItem);
21676 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21677 }
21678
21679 #[test]
21680 fn test_cloudtrail_tab_works_with_no_resources() {
21681 let mut app = App::new_without_client("default".to_string(), None);
21682 app.service_selected = true;
21683 app.current_service = Service::CloudTrailEvents;
21684 app.mode = Mode::Normal;
21685 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21686 event_name: "PutObject".to_string(),
21687 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21688 username: "user".to_string(),
21689 event_source: "s3.amazonaws.com".to_string(),
21690 resource_type: "".to_string(), resource_name: "".to_string(), read_only: "false".to_string(),
21693 aws_region: "us-east-1".to_string(),
21694 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21695 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21696 source_ip_address: "192.0.2.1".to_string(),
21697 error_code: "".to_string(),
21698 request_id: "req-123".to_string(),
21699 event_type: "AwsApiCall".to_string(),
21700 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21701 });
21702 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21703
21704 app.handle_action(Action::NextDetailTab);
21706 assert_eq!(
21707 app.cloudtrail_state.detail_focus,
21708 CloudTrailDetailFocus::Resources
21709 );
21710
21711 app.handle_action(Action::NextDetailTab);
21713 assert_eq!(
21714 app.cloudtrail_state.detail_focus,
21715 CloudTrailDetailFocus::EventRecord
21716 );
21717 }
21718
21719 #[test]
21720 fn test_cloudtrail_resources_expand_collapse() {
21721 let mut app = App::new_without_client("default".to_string(), None);
21722 app.service_selected = true;
21723 app.current_service = Service::CloudTrailEvents;
21724 app.mode = Mode::Normal;
21725 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21726 event_name: "PutObject".to_string(),
21727 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21728 username: "user".to_string(),
21729 event_source: "s3.amazonaws.com".to_string(),
21730 resource_type: "AWS::S3::Bucket".to_string(),
21731 resource_name: "my-bucket".to_string(),
21732 read_only: "false".to_string(),
21733 aws_region: "us-east-1".to_string(),
21734 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21735 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21736 source_ip_address: "192.0.2.1".to_string(),
21737 error_code: "".to_string(),
21738 request_id: "req-123".to_string(),
21739 event_type: "AwsApiCall".to_string(),
21740 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21741 });
21742 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21743 app.cloudtrail_state.resources_expanded_index = None;
21744
21745 app.handle_action(Action::ExpandRow);
21747 assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
21748
21749 app.handle_action(Action::CollapseRow);
21751 assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
21752 }
21753
21754 #[test]
21755 fn test_cloudtrail_event_json_no_column_selector() {
21756 let mut app = App::new_without_client("default".to_string(), None);
21757 app.service_selected = true;
21758 app.current_service = Service::CloudTrailEvents;
21759 app.mode = Mode::Normal;
21760 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21761 event_name: "PutObject".to_string(),
21762 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21763 username: "user".to_string(),
21764 event_source: "s3.amazonaws.com".to_string(),
21765 resource_type: "AWS::S3::Bucket".to_string(),
21766 resource_name: "my-bucket".to_string(),
21767 read_only: "false".to_string(),
21768 aws_region: "us-east-1".to_string(),
21769 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21770 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21771 source_ip_address: "192.0.2.1".to_string(),
21772 error_code: "".to_string(),
21773 request_id: "req-123".to_string(),
21774 event_type: "AwsApiCall".to_string(),
21775 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21776 });
21777 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21778
21779 app.handle_action(Action::OpenColumnSelector);
21781 assert_eq!(app.mode, Mode::Normal);
21782
21783 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21785 app.handle_action(Action::OpenColumnSelector);
21786 assert_eq!(app.mode, Mode::ColumnSelector);
21787 }
21788
21789 #[test]
21790 fn test_cloudtrail_resources_preferences_show_only_3_columns() {
21791 let mut app = App::new_without_client("default".to_string(), None);
21792 app.service_selected = true;
21793 app.current_service = Service::CloudTrailEvents;
21794 app.mode = Mode::ColumnSelector;
21795 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21796 event_name: "PutObject".to_string(),
21797 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21798 username: "user".to_string(),
21799 event_source: "s3.amazonaws.com".to_string(),
21800 resource_type: "AWS::S3::Bucket".to_string(),
21801 resource_name: "my-bucket".to_string(),
21802 read_only: "false".to_string(),
21803 aws_region: "us-east-1".to_string(),
21804 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21805 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21806 source_ip_address: "192.0.2.1".to_string(),
21807 error_code: "".to_string(),
21808 request_id: "req-123".to_string(),
21809 event_type: "AwsApiCall".to_string(),
21810 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21811 });
21812 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21813
21814 assert_eq!(app.get_column_count(), 3);
21816 assert_eq!(app.get_column_selector_max(), 3);
21817
21818 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
21820
21821 app.column_selector_index = 1;
21823 app.handle_action(Action::ToggleColumn);
21824 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21825
21826 app.column_selector_index = 2;
21828 app.handle_action(Action::ToggleColumn);
21829 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21830
21831 app.column_selector_index = 3;
21833 app.handle_action(Action::ToggleColumn);
21834 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21835
21836 app.column_selector_index = 1;
21838 app.handle_action(Action::ToggleColumn);
21839 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21840 }
21841
21842 #[test]
21843 fn test_cloudtrail_resources_preferences_tab_cycles() {
21844 let mut app = App::new_without_client("default".to_string(), None);
21845 app.service_selected = true;
21846 app.current_service = Service::CloudTrailEvents;
21847 app.mode = Mode::ColumnSelector;
21848 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21849 event_name: "PutObject".to_string(),
21850 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21851 username: "user".to_string(),
21852 event_source: "s3.amazonaws.com".to_string(),
21853 resource_type: "AWS::S3::Bucket".to_string(),
21854 resource_name: "my-bucket".to_string(),
21855 read_only: "false".to_string(),
21856 aws_region: "us-east-1".to_string(),
21857 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21858 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21859 source_ip_address: "192.0.2.1".to_string(),
21860 error_code: "".to_string(),
21861 request_id: "req-123".to_string(),
21862 event_type: "AwsApiCall".to_string(),
21863 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21864 });
21865 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21866 app.column_selector_index = 1;
21867
21868 app.handle_action(Action::NextPreferences);
21870 assert_eq!(app.column_selector_index, 0);
21871
21872 app.column_selector_index = 1;
21874 app.handle_action(Action::PrevPreferences);
21875 assert_eq!(app.column_selector_index, 0);
21876 }
21877
21878 #[test]
21879 fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
21880 let visible_cols_3 = 3;
21888 let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
21889 assert_eq!(height_3, 7);
21890
21891 let visible_cols_2 = 2;
21893 let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
21894 assert_eq!(height_2, 6);
21895
21896 let visible_cols_1 = 1;
21898 let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
21899 assert_eq!(height_1, 5);
21900 }
21901
21902 #[test]
21903 fn test_cloudtrail_default_focus_is_resources() {
21904 let app = App::new_without_client("default".to_string(), None);
21905 assert_eq!(
21906 app.cloudtrail_state.detail_focus,
21907 CloudTrailDetailFocus::Resources
21908 );
21909 }
21910
21911 fn setup_cloudtrail_pagination_test() -> App {
21912 let mut app = App::new_without_client("default".to_string(), None);
21913 app.service_selected = true;
21914 app.current_service = Service::CloudTrailEvents;
21915 app.mode = Mode::FilterInput;
21916 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21917 app.cloudtrail_state.table.page_size = PageSize::Ten;
21918 app.cloudtrail_state.table.items = (0..25)
21919 .map(|i| CloudTrailEvent {
21920 event_name: format!("Event{}", i),
21921 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21922 username: "user".to_string(),
21923 event_source: "s3.amazonaws.com".to_string(),
21924 resource_type: "Bucket".to_string(),
21925 resource_name: "bucket".to_string(),
21926 read_only: "false".to_string(),
21927 aws_region: "us-east-1".to_string(),
21928 event_id: "id".to_string(),
21929 access_key_id: "key".to_string(),
21930 source_ip_address: "1.2.3.4".to_string(),
21931 error_code: "".to_string(),
21932 request_id: "req".to_string(),
21933 event_type: "AwsApiCall".to_string(),
21934 cloud_trail_event_json: "{}".to_string(),
21935 })
21936 .collect();
21937 app
21938 }
21939
21940 #[test]
21941 fn test_cloudtrail_pagination_navigation() {
21942 let mut app = setup_cloudtrail_pagination_test();
21944 assert_eq!(app.cloudtrail_state.table.selected, 0);
21945 app.handle_action(Action::NextItem);
21946 assert_eq!(app.cloudtrail_state.table.selected, 10);
21947 app.handle_action(Action::NextItem);
21948 assert_eq!(app.cloudtrail_state.table.selected, 20);
21949 app.handle_action(Action::NextItem);
21950 assert_eq!(app.cloudtrail_state.table.selected, 20);
21951
21952 app.handle_action(Action::PrevItem);
21954 assert_eq!(app.cloudtrail_state.table.selected, 10);
21955 app.handle_action(Action::PrevItem);
21956 assert_eq!(app.cloudtrail_state.table.selected, 0);
21957 app.handle_action(Action::PrevItem);
21958 assert_eq!(app.cloudtrail_state.table.selected, 0);
21959 }
21960
21961 #[test]
21962 fn test_cloudtrail_pagination_navigation_right_arrow() {
21963 let mut app = setup_cloudtrail_pagination_test();
21964 app.current_service = Service::CloudTrailEvents;
21965 app.mode = Mode::FilterInput;
21966 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21967 app.cloudtrail_state.table.page_size = PageSize::Ten;
21968
21969 app.cloudtrail_state.table.items = (0..25)
21971 .map(|i| CloudTrailEvent {
21972 event_name: format!("Event{}", i),
21973 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21974 username: "user".to_string(),
21975 event_source: "s3.amazonaws.com".to_string(),
21976 resource_type: "Bucket".to_string(),
21977 resource_name: "bucket".to_string(),
21978 read_only: "false".to_string(),
21979 aws_region: "us-east-1".to_string(),
21980 event_id: "id".to_string(),
21981 access_key_id: "key".to_string(),
21982 source_ip_address: "1.2.3.4".to_string(),
21983 error_code: "".to_string(),
21984 request_id: "req".to_string(),
21985 event_type: "AwsApiCall".to_string(),
21986 cloud_trail_event_json: "{}".to_string(),
21987 })
21988 .collect();
21989
21990 assert_eq!(app.cloudtrail_state.table.selected, 0);
21992
21993 app.handle_action(Action::NextItem);
21995 assert_eq!(app.cloudtrail_state.table.selected, 10);
21996
21997 app.handle_action(Action::NextItem);
21999 assert_eq!(app.cloudtrail_state.table.selected, 20);
22000
22001 app.handle_action(Action::NextItem);
22003 assert_eq!(app.cloudtrail_state.table.selected, 20);
22004 }
22005
22006 #[test]
22007 fn test_cloudtrail_pagination_navigation_left_arrow() {
22008 let mut app = App::new_without_client("default".to_string(), None);
22009 app.service_selected = true;
22010 app.current_service = Service::CloudTrailEvents;
22011 app.mode = Mode::FilterInput;
22012 app.cloudtrail_state.input_focus = InputFocus::Pagination;
22013 app.cloudtrail_state.table.page_size = PageSize::Ten;
22014
22015 app.cloudtrail_state.table.items = (0..25)
22017 .map(|i| CloudTrailEvent {
22018 event_name: format!("Event{}", i),
22019 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22020 username: "user".to_string(),
22021 event_source: "s3.amazonaws.com".to_string(),
22022 resource_type: "Bucket".to_string(),
22023 resource_name: "bucket".to_string(),
22024 read_only: "false".to_string(),
22025 aws_region: "us-east-1".to_string(),
22026 event_id: "id".to_string(),
22027 access_key_id: "key".to_string(),
22028 source_ip_address: "1.2.3.4".to_string(),
22029 error_code: "".to_string(),
22030 request_id: "req".to_string(),
22031 event_type: "AwsApiCall".to_string(),
22032 cloud_trail_event_json: "{}".to_string(),
22033 })
22034 .collect();
22035
22036 app.cloudtrail_state.table.selected = 20;
22038
22039 app.handle_action(Action::PrevItem);
22041 assert_eq!(app.cloudtrail_state.table.selected, 10);
22042
22043 app.handle_action(Action::PrevItem);
22045 assert_eq!(app.cloudtrail_state.table.selected, 0);
22046
22047 app.handle_action(Action::PrevItem);
22049 assert_eq!(app.cloudtrail_state.table.selected, 0);
22050 }
22051
22052 #[test]
22053 fn test_cloudtrail_arrow_navigation() {
22054 let mut app = App::new_without_client("default".to_string(), None);
22055 app.service_selected = true;
22056 app.current_service = Service::CloudTrailEvents;
22057 app.mode = Mode::Normal;
22058
22059 app.cloudtrail_state.table.items = (0..5)
22061 .map(|i| CloudTrailEvent {
22062 event_name: format!("Event{}", i),
22063 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22064 username: "user".to_string(),
22065 event_source: "s3.amazonaws.com".to_string(),
22066 resource_type: "Bucket".to_string(),
22067 resource_name: "bucket".to_string(),
22068 read_only: "false".to_string(),
22069 aws_region: "us-east-1".to_string(),
22070 event_id: "id".to_string(),
22071 access_key_id: "key".to_string(),
22072 source_ip_address: "1.2.3.4".to_string(),
22073 error_code: "".to_string(),
22074 request_id: "req".to_string(),
22075 event_type: "AwsApiCall".to_string(),
22076 cloud_trail_event_json: "{}".to_string(),
22077 })
22078 .collect();
22079
22080 app.handle_action(Action::NextItem);
22082 assert_eq!(app.cloudtrail_state.table.selected, 1);
22083
22084 app.handle_action(Action::NextItem);
22085 assert_eq!(app.cloudtrail_state.table.selected, 2);
22086
22087 app.handle_action(Action::PrevItem);
22089 assert_eq!(app.cloudtrail_state.table.selected, 1);
22090 }
22091
22092 #[test]
22093 fn test_cloudtrail_page_down_navigation() {
22094 let mut app = App::new_without_client("default".to_string(), None);
22095 app.service_selected = true;
22096 app.current_service = Service::CloudTrailEvents;
22097 app.mode = Mode::Normal;
22098 app.cloudtrail_state.table.page_size = PageSize::Ten;
22099
22100 app.cloudtrail_state.table.items = (0..25)
22102 .map(|i| CloudTrailEvent {
22103 event_name: format!("Event{}", i),
22104 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22105 username: "user".to_string(),
22106 event_source: "s3.amazonaws.com".to_string(),
22107 resource_type: "Bucket".to_string(),
22108 resource_name: "bucket".to_string(),
22109 read_only: "false".to_string(),
22110 aws_region: "us-east-1".to_string(),
22111 event_id: "id".to_string(),
22112 access_key_id: "key".to_string(),
22113 source_ip_address: "1.2.3.4".to_string(),
22114 error_code: "".to_string(),
22115 request_id: "req".to_string(),
22116 event_type: "AwsApiCall".to_string(),
22117 cloud_trail_event_json: "{}".to_string(),
22118 })
22119 .collect();
22120
22121 app.handle_action(Action::PageDown);
22123 assert_eq!(app.cloudtrail_state.table.selected, 10);
22124
22125 app.handle_action(Action::PageDown);
22126 assert_eq!(app.cloudtrail_state.table.selected, 20);
22127 }
22128
22129 #[test]
22130 fn test_cloudtrail_page_up_navigation() {
22131 let mut app = App::new_without_client("default".to_string(), None);
22132 app.service_selected = true;
22133 app.current_service = Service::CloudTrailEvents;
22134 app.mode = Mode::Normal;
22135 app.cloudtrail_state.table.page_size = PageSize::Ten;
22136
22137 app.cloudtrail_state.table.items = (0..25)
22139 .map(|i| CloudTrailEvent {
22140 event_name: format!("Event{}", i),
22141 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22142 username: "user".to_string(),
22143 event_source: "s3.amazonaws.com".to_string(),
22144 resource_type: "Bucket".to_string(),
22145 resource_name: "bucket".to_string(),
22146 read_only: "false".to_string(),
22147 aws_region: "us-east-1".to_string(),
22148 event_id: "id".to_string(),
22149 access_key_id: "key".to_string(),
22150 source_ip_address: "1.2.3.4".to_string(),
22151 error_code: "".to_string(),
22152 request_id: "req".to_string(),
22153 event_type: "AwsApiCall".to_string(),
22154 cloud_trail_event_json: "{}".to_string(),
22155 })
22156 .collect();
22157
22158 app.cloudtrail_state.table.selected = 20;
22160
22161 app.handle_action(Action::PageUp);
22163 assert_eq!(app.cloudtrail_state.table.selected, 10);
22164
22165 app.handle_action(Action::PageUp);
22166 assert_eq!(app.cloudtrail_state.table.selected, 0);
22167 }
22168
22169 #[test]
22170 fn test_cloudtrail_page_size_change_updates_display() {
22171 let mut app = App::new_without_client("default".to_string(), None);
22172 app.service_selected = true;
22173 app.current_service = Service::CloudTrailEvents;
22174 app.mode = Mode::ColumnSelector;
22175
22176 app.cloudtrail_state.table.items = (0..50)
22178 .map(|i| CloudTrailEvent {
22179 event_name: format!("Event{}", i),
22180 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22181 username: "user".to_string(),
22182 event_source: "s3.amazonaws.com".to_string(),
22183 resource_type: "Bucket".to_string(),
22184 resource_name: "bucket".to_string(),
22185 read_only: "false".to_string(),
22186 aws_region: "us-east-1".to_string(),
22187 event_id: "id".to_string(),
22188 access_key_id: "key".to_string(),
22189 source_ip_address: "1.2.3.4".to_string(),
22190 error_code: "".to_string(),
22191 request_id: "req".to_string(),
22192 event_type: "AwsApiCall".to_string(),
22193 cloud_trail_event_json: "{}".to_string(),
22194 })
22195 .collect();
22196
22197 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
22199
22200 app.column_selector_index = 17;
22202 app.handle_action(Action::ToggleColumn);
22203
22204 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22205 }
22207
22208 #[test]
22209 fn test_cloudtrail_all_columns_toggleable() {
22210 let mut app = App::new_without_client("default".to_string(), None);
22211 app.service_selected = true;
22212 app.current_service = Service::CloudTrailEvents;
22213 app.mode = Mode::ColumnSelector;
22214
22215 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22217
22218 for idx in 1..=14 {
22220 app.column_selector_index = idx;
22221 let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
22222
22223 app.handle_action(Action::ToggleColumn);
22224
22225 assert_ne!(
22227 app.cloudtrail_event_visible_column_ids, initial_visible,
22228 "Column at index {} should be toggleable",
22229 idx
22230 );
22231 }
22232 }
22233
22234 #[test]
22235 fn test_cloudtrail_readonly_column_toggleable() {
22236 use crate::cloudtrail::events::CloudTrailEventColumn;
22237
22238 let mut app = App::new_without_client("default".to_string(), None);
22239 app.service_selected = true;
22240 app.current_service = Service::CloudTrailEvents;
22241 app.mode = Mode::ColumnSelector;
22242
22243 let readonly_id = CloudTrailEventColumn::ReadOnly.id();
22246
22247 assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
22249
22250 assert!(!app
22252 .cloudtrail_event_visible_column_ids
22253 .contains(&readonly_id));
22254
22255 app.column_selector_index = 7;
22257 app.handle_action(Action::ToggleColumn);
22258
22259 assert!(
22261 app.cloudtrail_event_visible_column_ids
22262 .contains(&readonly_id),
22263 "ReadOnly column should be toggleable at index 7"
22264 );
22265
22266 app.handle_action(Action::ToggleColumn);
22268
22269 assert!(!app
22271 .cloudtrail_event_visible_column_ids
22272 .contains(&readonly_id));
22273 }
22274
22275 #[test]
22276 fn test_cloudtrail_pagination_limits_displayed_items() {
22277 let mut app = App::new_without_client("default".to_string(), None);
22278 app.service_selected = true;
22279 app.current_service = Service::CloudTrailEvents;
22280
22281 app.cloudtrail_state.table.items = (0..50)
22283 .map(|i| CloudTrailEvent {
22284 event_name: format!("Event{}", i),
22285 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22286 username: "user".to_string(),
22287 event_source: "s3.amazonaws.com".to_string(),
22288 resource_type: "Bucket".to_string(),
22289 resource_name: "bucket".to_string(),
22290 read_only: "false".to_string(),
22291 aws_region: "us-east-1".to_string(),
22292 event_id: "id".to_string(),
22293 access_key_id: "key".to_string(),
22294 source_ip_address: "1.2.3.4".to_string(),
22295 error_code: "".to_string(),
22296 request_id: "req".to_string(),
22297 event_type: "AwsApiCall".to_string(),
22298 cloud_trail_event_json: "{}".to_string(),
22299 })
22300 .collect();
22301
22302 app.cloudtrail_state.table.page_size = PageSize::Ten;
22304
22305 use ratatui::backend::TestBackend;
22307 use ratatui::Terminal;
22308
22309 let backend = TestBackend::new(100, 30);
22310 let mut terminal = Terminal::new(backend).unwrap();
22311
22312 terminal
22313 .draw(|frame| {
22314 let area = frame.area();
22315 crate::ui::cloudtrail::render_events(frame, &app, area);
22316 })
22317 .unwrap();
22318
22319 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22322 }
22323
22324 #[test]
22325 fn test_cloudtrail_readonly_column_selectable_in_preferences() {
22326 let mut app = App::new_without_client("default".to_string(), None);
22327 app.service_selected = true;
22328 app.current_service = Service::CloudTrailEvents;
22329 app.mode = Mode::ColumnSelector;
22330
22331 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22333
22334 app.column_selector_index = 7;
22336
22337 let max_index = app.get_column_selector_max();
22339 assert!(
22340 app.column_selector_index <= max_index,
22341 "Read-only column index {} should be <= max {}",
22342 app.column_selector_index,
22343 max_index
22344 );
22345
22346 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22348 let col = CloudTrailEventColumn::from_id(col_id);
22349 assert!(col.is_some(), "Column should exist at index 7");
22350 assert_eq!(
22351 col.unwrap().default_name(),
22352 "Read-only",
22353 "Column at index 7 should be Read-only"
22354 );
22355 }
22356
22357 #[test]
22358 fn test_cloudtrail_navigate_to_readonly_column() {
22359 let mut app = App::new_without_client("default".to_string(), None);
22360 app.service_selected = true;
22361 app.current_service = Service::CloudTrailEvents;
22362 app.mode = Mode::ColumnSelector;
22363 app.column_selector_index = 1; println!(
22367 "cloudtrail_event_column_ids.len() = {}",
22368 app.cloudtrail_event_column_ids.len()
22369 );
22370 assert_eq!(
22371 app.cloudtrail_event_column_ids.len(),
22372 14,
22373 "Should have 14 columns"
22374 );
22375
22376 let column_count = app.get_column_count();
22378 println!("Column count from get_column_count(): {}", column_count);
22379 assert_eq!(column_count, 14, "Column count should be 14");
22380
22381 assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
22383 assert!(app.is_blank_row_index(15), "Index 15 should be blank");
22384
22385 for _ in 0..6 {
22387 app.handle_action(Action::NextItem);
22388 }
22389
22390 assert_eq!(
22391 app.column_selector_index, 7,
22392 "Should navigate to Read-only column at index 7"
22393 );
22394
22395 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22397 let col = CloudTrailEventColumn::from_id(col_id).unwrap();
22398 assert_eq!(col.default_name(), "Read-only");
22399 }
22400
22401 #[test]
22402 fn test_cloudtrail_navigate_beyond_loaded_items() {
22403 let mut app = App::new_without_client("default".to_string(), None);
22404 app.service_selected = true;
22405 app.current_service = Service::CloudTrailEvents;
22406
22407 app.cloudtrail_state.table.items = (0..50)
22409 .map(|i| CloudTrailEvent {
22410 event_name: format!("Event{}", i),
22411 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22412 username: "user".to_string(),
22413 event_source: "s3.amazonaws.com".to_string(),
22414 resource_type: "Bucket".to_string(),
22415 resource_name: "bucket".to_string(),
22416 read_only: "false".to_string(),
22417 aws_region: "us-east-1".to_string(),
22418 event_id: "id".to_string(),
22419 access_key_id: "key".to_string(),
22420 source_ip_address: "1.2.3.4".to_string(),
22421 error_code: "".to_string(),
22422 request_id: "req".to_string(),
22423 event_type: "AwsApiCall".to_string(),
22424 cloud_trail_event_json: "{}".to_string(),
22425 })
22426 .collect();
22427
22428 let initial_selected = app.cloudtrail_state.table.selected;
22431 app.page_input = "10".to_string();
22432 app.go_to_page(10);
22433
22434 assert_eq!(
22436 app.cloudtrail_state.table.selected, initial_selected,
22437 "Should ignore navigation to page 10 when only 1 page is loaded"
22438 );
22439
22440 app.go_to_page(2);
22442 let page_size = app.cloudtrail_state.table.page_size.value();
22443 assert_eq!(
22444 app.cloudtrail_state.table.selected, page_size,
22445 "Should allow navigation to page 2 (loaded + 1)"
22446 );
22447 }
22448
22449 #[test]
22450 fn test_cloudtrail_page_change_resets_expansion() {
22451 let mut app = App::new_without_client("default".to_string(), None);
22452 app.service_selected = true;
22453 app.current_service = Service::CloudTrailEvents;
22454
22455 app.cloudtrail_state.table.items = (0..100)
22457 .map(|i| CloudTrailEvent {
22458 event_name: format!("Event{}", i),
22459 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22460 username: "user".to_string(),
22461 event_source: "s3.amazonaws.com".to_string(),
22462 resource_type: "Bucket".to_string(),
22463 resource_name: "bucket".to_string(),
22464 read_only: "false".to_string(),
22465 aws_region: "us-east-1".to_string(),
22466 event_id: "id".to_string(),
22467 access_key_id: "key".to_string(),
22468 source_ip_address: "1.2.3.4".to_string(),
22469 error_code: "".to_string(),
22470 request_id: "req".to_string(),
22471 event_type: "AwsApiCall".to_string(),
22472 cloud_trail_event_json: "{}".to_string(),
22473 })
22474 .collect();
22475
22476 app.cloudtrail_state.table.expanded_item = Some(5);
22478 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
22479
22480 app.go_to_page(2);
22482
22483 assert_eq!(
22485 app.cloudtrail_state.table.expanded_item, None,
22486 "Page change should reset expanded_item"
22487 );
22488 }
22489}