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};
42pub use crate::ui::cfn::{
43 change_set_column_ids, change_set_visible_column_ids, event_column_ids,
44 event_visible_column_ids, filtered_change_sets, filtered_cloudformation_stacks,
45 filtered_events, filtered_outputs, filtered_parameters, filtered_resources, output_column_ids,
46 parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
47 EventsView as CfnEventsView, State as CfnState, StatusFilter as CfnStatusFilter,
48};
49pub use crate::ui::cw::alarms::{
50 AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
51};
52pub use crate::ui::cw::logs::{
53 filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
54 DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
55};
56pub use crate::ui::ec2::{
57 DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
58 STATE_FILTER as EC2_STATE_FILTER,
59};
60pub use crate::ui::ecr::{
61 filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
62 FILTER_CONTROLS as ECR_FILTER_CONTROLS,
63};
64use crate::ui::iam::{
65 filtered_iam_policies, filtered_iam_roles, filtered_iam_users, filtered_last_accessed,
66 filtered_tags as filtered_iam_tags, filtered_user_tags, GroupTab, RoleTab, State as IamState,
67 UserTab,
68};
69pub use crate::ui::lambda::{
70 filtered_lambda_applications, filtered_lambda_functions,
71 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
72 DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
73};
74use crate::ui::monitoring::MonitoringState;
75pub use crate::ui::s3::{
76 calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
77 ObjectTab as S3ObjectTab, State as S3State,
78};
79pub use crate::ui::sqs::{
80 extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
81 filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
82 State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
83 SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
84};
85use crate::ui::tree::TreeItem;
86pub use crate::ui::{
87 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
88 StreamColumn, StreamSort, TimeUnit,
89};
90#[cfg(test)]
91use rusticity_core::LogStream;
92use rusticity_core::{
93 AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
94 CloudWatchClient, Ec2Client, EcrClient, IamClient, LambdaClient, S3Client, SqsClient,
95};
96use std::collections::HashMap;
97
98#[derive(Clone)]
99pub struct Tab {
100 pub service: Service,
101 pub title: String,
102 pub breadcrumb: String,
103}
104
105pub struct App {
106 pub running: bool,
107 pub mode: Mode,
108 pub config: AwsConfig,
109 pub cloudwatch_client: CloudWatchClient,
110 pub cloudtrail_client: CloudTrailClient,
111 pub s3_client: S3Client,
112 pub sqs_client: SqsClient,
113 pub alarms_client: AlarmsClient,
114 pub ec2_client: Ec2Client,
115 pub ecr_client: EcrClient,
116 pub apig_client: ApiGatewayClient,
117 pub iam_client: IamClient,
118 pub lambda_client: LambdaClient,
119 pub cloudformation_client: CloudFormationClient,
120 pub current_service: Service,
121 pub tabs: Vec<Tab>,
122 pub current_tab: usize,
123 pub tab_picker_selected: usize,
124 pub tab_filter: String,
125 pub pending_key: Option<char>,
126 pub log_groups_state: CloudWatchLogGroupsState,
127 pub insights_state: CloudWatchInsightsState,
128 pub alarms_state: CloudWatchAlarmsState,
129 pub cloudtrail_state: CloudTrailState,
130 pub s3_state: S3State,
131 pub sqs_state: SqsState,
132 pub ec2_state: Ec2State,
133 pub ecr_state: EcrState,
134 pub apig_state: ApigState,
135 pub lambda_state: LambdaState,
136 pub lambda_application_state: LambdaApplicationState,
137 pub cfn_state: CfnState,
138 pub iam_state: IamState,
139 pub service_picker: ServicePickerState,
140 pub service_selected: bool,
141 pub profile: String,
142 pub region: String,
143 pub region_selector_index: usize,
144 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
145 pub cw_log_group_column_ids: Vec<ColumnId>,
146 pub column_selector_index: usize,
147 pub preference_section: Preferences,
148 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
149 pub cw_log_stream_column_ids: Vec<ColumnId>,
150 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
151 pub cw_log_event_column_ids: Vec<ColumnId>,
152 pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
153 pub cw_log_tag_column_ids: Vec<ColumnId>,
154 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
155 pub cw_alarm_column_ids: Vec<ColumnId>,
156 pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
157 pub cloudtrail_event_column_ids: Vec<ColumnId>,
158 pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
159 pub cloudtrail_resource_column_ids: Vec<ColumnId>,
160 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
161 pub s3_bucket_column_ids: Vec<ColumnId>,
162 pub sqs_visible_column_ids: Vec<ColumnId>,
163 pub sqs_column_ids: Vec<ColumnId>,
164 pub ec2_visible_column_ids: Vec<ColumnId>,
165 pub ec2_column_ids: Vec<ColumnId>,
166 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
167 pub ecr_repo_column_ids: Vec<ColumnId>,
168 pub ecr_image_visible_column_ids: Vec<ColumnId>,
169 pub ecr_image_column_ids: Vec<ColumnId>,
170 pub apig_api_visible_column_ids: Vec<ColumnId>,
171 pub apig_api_column_ids: Vec<ColumnId>,
172 pub apig_route_visible_column_ids: Vec<ColumnId>,
173 pub apig_route_column_ids: Vec<ColumnId>,
174 pub apig_resource_visible_column_ids: Vec<ColumnId>,
175 pub apig_resource_column_ids: Vec<ColumnId>,
176 pub lambda_application_visible_column_ids: Vec<ColumnId>,
177 pub lambda_application_column_ids: Vec<ColumnId>,
178 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
179 pub lambda_deployment_column_ids: Vec<ColumnId>,
180 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
181 pub lambda_resource_column_ids: Vec<ColumnId>,
182 pub cfn_visible_column_ids: Vec<ColumnId>,
183 pub cfn_column_ids: Vec<ColumnId>,
184 pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
185 pub cfn_parameter_column_ids: Vec<ColumnId>,
186 pub cfn_output_visible_column_ids: Vec<ColumnId>,
187 pub cfn_output_column_ids: Vec<ColumnId>,
188 pub cfn_resource_visible_column_ids: Vec<ColumnId>,
189 pub cfn_resource_column_ids: Vec<ColumnId>,
190 pub cfn_event_visible_column_ids: Vec<ColumnId>,
191 pub cfn_event_column_ids: Vec<ColumnId>,
192 pub cfn_change_set_visible_column_ids: Vec<ColumnId>,
193 pub cfn_change_set_column_ids: Vec<ColumnId>,
194 pub iam_user_visible_column_ids: Vec<ColumnId>,
195 pub iam_user_column_ids: Vec<ColumnId>,
196 pub iam_role_visible_column_ids: Vec<ColumnId>,
197 pub iam_role_column_ids: Vec<ColumnId>,
198 pub iam_group_visible_column_ids: Vec<String>,
199 pub iam_group_column_ids: Vec<String>,
200 pub iam_policy_visible_column_ids: Vec<String>,
201 pub iam_policy_column_ids: Vec<String>,
202 pub view_mode: ViewMode,
203 pub error_message: Option<String>,
204 pub error_scroll: usize,
205 pub page_input: String,
206 pub calendar_date: Option<time::Date>,
207 pub calendar_selecting: CalendarField,
208 pub cursor_pos: usize,
209 pub current_session: Option<Session>,
210 pub sessions: Vec<Session>,
211 pub session_picker_selected: usize,
212 pub session_filter: String,
213 pub session_filter_active: bool,
214 pub region_filter: String,
215 pub region_picker_selected: usize,
216 pub region_filter_active: bool,
217 pub region_latencies: std::collections::HashMap<String, u64>,
218 pub profile_filter: String,
219 pub profile_picker_selected: usize,
220 pub profile_filter_active: bool,
221 pub available_profiles: Vec<AwsProfile>,
222 pub snapshot_requested: bool,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq)]
226pub enum CalendarField {
227 StartDate,
228 EndDate,
229}
230
231pub struct CloudWatchInsightsState {
232 pub insights: InsightsState,
233 pub loading: bool,
234}
235
236pub struct CloudWatchAlarmsState {
237 pub table: TableState<Alarm>,
238 pub current_alarm: Option<String>,
239 pub alarm_tab: AlarmTab,
240 pub view_as: AlarmViewMode,
241 pub wrap_lines: bool,
242 pub sort_column: String,
243 pub sort_direction: SortDirection,
244 pub input_focus: InputFocus,
245 pub metric_data: Vec<(i64, f64)>,
246 pub metrics_loading: bool,
247}
248
249#[derive(Debug, Clone)]
250pub struct CloudTrailState {
251 pub table: TableState<CloudTrailEvent>,
252 pub input_focus: InputFocus,
253 pub current_event: Option<CloudTrailEvent>,
254 pub event_json_scroll: usize,
255 pub detail_focus: CloudTrailDetailFocus,
256 pub resources_expanded_index: Option<usize>,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq)]
260pub enum CloudTrailDetailFocus {
261 Resources,
262 EventRecord,
263}
264
265impl CyclicEnum for CloudTrailDetailFocus {
266 const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
267}
268
269impl PageSize {
270 pub fn value(&self) -> usize {
271 match self {
272 PageSize::Ten => 10,
273 PageSize::TwentyFive => 25,
274 PageSize::Fifty => 50,
275 PageSize::OneHundred => 100,
276 }
277 }
278
279 pub fn next(&self) -> Self {
280 match self {
281 PageSize::Ten => PageSize::TwentyFive,
282 PageSize::TwentyFive => PageSize::Fifty,
283 PageSize::Fifty => PageSize::OneHundred,
284 PageSize::OneHundred => PageSize::Ten,
285 }
286 }
287}
288
289pub struct ServicePickerState {
290 pub filter: String,
291 pub filter_active: bool,
292 pub selected: usize,
293 pub services: Vec<&'static str>,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq)]
297pub enum ViewMode {
298 List,
299 Detail,
300 Events,
301 InsightsResults,
302 PolicyView,
303}
304
305#[derive(Debug, Clone, Copy, PartialEq)]
306pub enum Service {
307 ApiGatewayApis,
308 CloudWatchLogGroups,
309 CloudWatchInsights,
310 CloudWatchAlarms,
311 CloudTrailEvents,
312 S3Buckets,
313 SqsQueues,
314 Ec2Instances,
315 EcrRepositories,
316 LambdaFunctions,
317 LambdaApplications,
318 CloudFormationStacks,
319 IamUsers,
320 IamRoles,
321 IamUserGroups,
322}
323
324impl Service {
325 pub fn name(&self) -> &str {
326 match self {
327 Service::ApiGatewayApis => "API Gateway › APIs",
328 Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
329 Service::CloudWatchInsights => "CloudWatch › Logs Insights",
330 Service::CloudWatchAlarms => "CloudWatch › Alarms",
331 Service::CloudTrailEvents => "CloudTrail › Event History",
332 Service::S3Buckets => "S3 › Buckets",
333 Service::SqsQueues => "SQS › Queues",
334 Service::Ec2Instances => "EC2 › Instances",
335 Service::EcrRepositories => "ECR › Repositories",
336 Service::LambdaFunctions => "Lambda › Functions",
337 Service::LambdaApplications => "Lambda › Applications",
338 Service::CloudFormationStacks => "CloudFormation › Stacks",
339 Service::IamUsers => "IAM › Users",
340 Service::IamRoles => "IAM › Roles",
341 Service::IamUserGroups => "IAM › User Groups",
342 }
343 }
344}
345
346pub(crate) fn copy_to_clipboard(text: &str) {
347 use std::io::Write;
348 use std::process::{Command, Stdio};
349 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
350 if let Some(mut stdin) = child.stdin.take() {
351 let _ = stdin.write_all(text.as_bytes());
352 }
353 let _ = child.wait();
354 }
355}
356
357pub(crate) fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
358 if max > 0 {
359 *selected = (*selected + page_size).min(max - 1);
360 }
361}
362
363pub(crate) fn toggle_iam_preference(
364 idx: usize,
365 column_ids: &[String],
366 visible_column_ids: &mut Vec<String>,
367 page_size: &mut PageSize,
368) {
369 if idx > 0 && idx <= column_ids.len() {
370 if let Some(col) = column_ids.get(idx - 1) {
371 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
372 if visible_column_ids.len() > 1 {
374 visible_column_ids.remove(pos);
375 }
376 } else {
377 visible_column_ids.push(col.clone());
378 }
379 }
380 } else if idx == column_ids.len() + 3 {
381 *page_size = PageSize::Ten;
382 } else if idx == column_ids.len() + 4 {
383 *page_size = PageSize::TwentyFive;
384 } else if idx == column_ids.len() + 5 {
385 *page_size = PageSize::Fifty;
386 }
387}
388
389pub(crate) fn toggle_iam_preference_static(
390 idx: usize,
391 column_ids: &[ColumnId],
392 visible_column_ids: &mut Vec<ColumnId>,
393 page_size: &mut PageSize,
394) {
395 if idx > 0 && idx <= column_ids.len() {
396 if let Some(col) = column_ids.get(idx - 1) {
397 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
398 if visible_column_ids.len() > 1 {
400 visible_column_ids.remove(pos);
401 }
402 } else {
403 visible_column_ids.push(*col);
404 }
405 }
406 } else if idx == column_ids.len() + 3 {
407 *page_size = PageSize::Ten;
408 } else if idx == column_ids.len() + 4 {
409 *page_size = PageSize::TwentyFive;
410 } else if idx == column_ids.len() + 5 {
411 *page_size = PageSize::Fifty;
412 }
413}
414
415pub(crate) fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
416 if idx == base_idx {
417 *page_size = PageSize::Ten;
418 } else if idx == base_idx + 1 {
419 *page_size = PageSize::TwentyFive;
420 } else if idx == base_idx + 2 {
421 *page_size = PageSize::Fifty;
422 }
423}
424
425fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
427 let page_size_idx = num_columns + 2;
428 if *current_idx < page_size_idx {
429 *current_idx = page_size_idx;
430 } else {
431 *current_idx = 0;
432 }
433}
434
435fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
437 let page_size_idx = num_columns + 2;
438 if *current_idx >= page_size_idx {
439 *current_idx = 0;
440 } else {
441 *current_idx = page_size_idx;
442 }
443}
444
445impl App {
446 pub fn get_input_focus(&self) -> InputFocus {
447 InputFocus::Filter
448 }
449
450 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
451 if self.current_service == Service::ApiGatewayApis {
452 if self.apig_state.current_api.is_some()
453 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
454 {
455 Some(&mut self.apig_state.route_filter)
456 } else {
457 Some(&mut self.apig_state.apis.filter)
458 }
459 } else if self.current_service == Service::CloudWatchAlarms {
460 Some(&mut self.alarms_state.table.filter)
461 } else if self.current_service == Service::CloudTrailEvents {
462 Some(&mut self.cloudtrail_state.table.filter)
463 } else if self.current_service == Service::Ec2Instances {
464 crate::ec2::actions::get_active_filter_mut(self)
465 } else if self.current_service == Service::S3Buckets {
466 if self.s3_state.current_bucket.is_some() {
467 Some(&mut self.s3_state.object_filter)
468 } else {
469 Some(&mut self.s3_state.buckets.filter)
470 }
471 } else if self.current_service == Service::EcrRepositories {
472 if self.ecr_state.current_repository.is_some() {
473 Some(&mut self.ecr_state.images.filter)
474 } else {
475 Some(&mut self.ecr_state.repositories.filter)
476 }
477 } else if self.current_service == Service::SqsQueues {
478 if self.sqs_state.current_queue.is_some()
479 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
480 {
481 Some(&mut self.sqs_state.triggers.filter)
482 } else if self.sqs_state.current_queue.is_some()
483 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
484 {
485 Some(&mut self.sqs_state.pipes.filter)
486 } else if self.sqs_state.current_queue.is_some()
487 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
488 {
489 Some(&mut self.sqs_state.tags.filter)
490 } else if self.sqs_state.current_queue.is_some()
491 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
492 {
493 Some(&mut self.sqs_state.subscriptions.filter)
494 } else {
495 Some(&mut self.sqs_state.queues.filter)
496 }
497 } else if self.current_service == Service::LambdaFunctions {
498 if self.lambda_state.current_version.is_some()
499 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
500 {
501 Some(&mut self.lambda_state.alias_table.filter)
502 } else if self.lambda_state.current_function.is_some()
503 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
504 {
505 Some(&mut self.lambda_state.version_table.filter)
506 } else if self.lambda_state.current_function.is_some()
507 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
508 {
509 Some(&mut self.lambda_state.alias_table.filter)
510 } else {
511 Some(&mut self.lambda_state.table.filter)
512 }
513 } else if self.current_service == Service::LambdaApplications {
514 if self.lambda_application_state.current_application.is_some() {
515 if self.lambda_application_state.detail_tab
516 == LambdaApplicationDetailTab::Deployments
517 {
518 Some(&mut self.lambda_application_state.deployments.filter)
519 } else {
520 Some(&mut self.lambda_application_state.resources.filter)
521 }
522 } else {
523 Some(&mut self.lambda_application_state.table.filter)
524 }
525 } else if self.current_service == Service::CloudFormationStacks {
526 if self.cfn_state.current_stack.is_some()
527 && self.cfn_state.detail_tab == CfnDetailTab::Resources
528 {
529 Some(&mut self.cfn_state.resources.filter)
530 } else {
531 Some(&mut self.cfn_state.table.filter)
532 }
533 } else if self.current_service == Service::IamUsers {
534 if self.iam_state.current_user.is_some() {
535 if self.iam_state.user_tab == UserTab::Tags {
536 Some(&mut self.iam_state.user_tags.filter)
537 } else {
538 Some(&mut self.iam_state.policies.filter)
539 }
540 } else {
541 Some(&mut self.iam_state.users.filter)
542 }
543 } else if self.current_service == Service::IamRoles {
544 if self.iam_state.current_role.is_some() {
545 if self.iam_state.role_tab == RoleTab::Tags {
546 Some(&mut self.iam_state.tags.filter)
547 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
548 Some(&mut self.iam_state.last_accessed_filter)
549 } else {
550 Some(&mut self.iam_state.policies.filter)
551 }
552 } else {
553 Some(&mut self.iam_state.roles.filter)
554 }
555 } else if self.current_service == Service::IamUserGroups {
556 if self.iam_state.current_group.is_some() {
557 if self.iam_state.group_tab == GroupTab::Permissions {
558 Some(&mut self.iam_state.policies.filter)
559 } else if self.iam_state.group_tab == GroupTab::Users {
560 Some(&mut self.iam_state.group_users.filter)
561 } else {
562 None
563 }
564 } else {
565 Some(&mut self.iam_state.groups.filter)
566 }
567 } else if self.view_mode == ViewMode::List {
568 Some(&mut self.log_groups_state.log_groups.filter)
569 } else if self.view_mode == ViewMode::Detail
570 && self.log_groups_state.detail_tab == DetailTab::LogStreams
571 {
572 Some(&mut self.log_groups_state.stream_filter)
573 } else {
574 None
575 }
576 }
577
578 fn apply_filter_operation<F>(&mut self, op: F)
579 where
580 F: FnOnce(&mut String),
581 {
582 if let Some(filter) = self.get_active_filter_mut() {
583 op(filter);
584 if self.current_service == Service::CloudWatchAlarms {
586 self.alarms_state.table.reset();
587 } else if self.current_service == Service::CloudTrailEvents {
588 self.cloudtrail_state.table.reset();
589 } else if self.current_service == Service::Ec2Instances {
590 crate::ec2::actions::apply_filter_reset(self);
591 } else if self.current_service == Service::S3Buckets {
592 crate::s3::actions::apply_filter_reset(self);
593 } else if self.current_service == Service::EcrRepositories {
594 crate::ecr::actions::apply_filter_reset(self);
595 } else if self.current_service == Service::SqsQueues {
596 self.sqs_state.queues.reset();
597 } else if self.current_service == Service::LambdaFunctions {
598 crate::lambda::functions::apply_filter_reset(self);
599 } else if self.current_service == Service::LambdaApplications {
600 crate::lambda::applications::apply_filter_reset(self);
601 } else if self.current_service == Service::CloudFormationStacks {
602 crate::cfn::actions::apply_filter_reset(self);
603 } else if self.current_service == Service::IamUsers {
604 crate::iam::actions::apply_filter_reset_users(self);
605 } else if self.current_service == Service::IamRoles {
606 crate::iam::actions::apply_filter_reset_roles(self);
607 } else if self.current_service == Service::IamUserGroups {
608 crate::iam::actions::apply_filter_reset_groups(self);
609 } else if self.current_service == Service::CloudWatchLogGroups {
610 if self.view_mode == ViewMode::List {
611 self.log_groups_state.log_groups.reset();
612 } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
613 self.log_groups_state.selected_stream = 0;
614 }
615 } else if self.current_service == Service::ApiGatewayApis {
616 self.apig_state.apis.reset();
617 }
618 }
619 }
620
621 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
622 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
623 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
624
625 std::env::set_var("AWS_PROFILE", &profile_name);
626
627 let config = AwsConfig::new(region).await?;
628 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
629 let cloudtrail_client = CloudTrailClient::new(config.clone());
630 let s3_client = S3Client::new(config.clone());
631 let sqs_client = SqsClient::new(config.clone());
632 let alarms_client = AlarmsClient::new(config.clone());
633 let ec2_client = Ec2Client::new(config.clone());
634 let ecr_client = EcrClient::new(config.clone());
635 let apig_client = ApiGatewayClient::new(config.clone());
636 let iam_client = IamClient::new(config.clone());
637 let lambda_client = LambdaClient::new(config.clone());
638 let cloudformation_client = CloudFormationClient::new(config.clone());
639 let region_name = config.region.clone();
640
641 Ok(Self {
642 running: true,
643 mode: Mode::ServicePicker,
644 config,
645 cloudwatch_client,
646 cloudtrail_client,
647 s3_client,
648 sqs_client,
649 alarms_client,
650 ec2_client,
651 ecr_client,
652 apig_client,
653 iam_client,
654 lambda_client,
655 cloudformation_client,
656 current_service: Service::CloudWatchLogGroups,
657 tabs: Vec::new(),
658 current_tab: 0,
659 tab_picker_selected: 0,
660 tab_filter: String::new(),
661 pending_key: None,
662 log_groups_state: CloudWatchLogGroupsState::new(),
663 insights_state: CloudWatchInsightsState::new(),
664 alarms_state: CloudWatchAlarmsState::new(),
665 cloudtrail_state: CloudTrailState {
666 table: TableState::new(),
667 input_focus: InputFocus::Filter,
668 current_event: None,
669 event_json_scroll: 0,
670 detail_focus: CloudTrailDetailFocus::Resources,
671 resources_expanded_index: None,
672 },
673 s3_state: S3State::new(),
674 sqs_state: SqsState::new(),
675 ec2_state: Ec2State::default(),
676 ecr_state: EcrState::new(),
677 apig_state: ApigState::new(),
678 lambda_state: LambdaState::new(),
679 lambda_application_state: LambdaApplicationState::new(),
680 cfn_state: CfnState::new(),
681 iam_state: IamState::new(),
682 service_picker: ServicePickerState::new(),
683 service_selected: false,
684 profile: profile_name,
685 region: region_name,
686 region_selector_index: 0,
687 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
688 cw_log_group_column_ids: LogGroupColumn::ids(),
689 column_selector_index: 0,
690 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
691 cw_log_stream_column_ids: StreamColumn::ids(),
692 cw_log_event_visible_column_ids: EventColumn::default_visible(),
693 cw_log_event_column_ids: EventColumn::ids(),
694 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
695 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
696 cw_alarm_visible_column_ids: [
697 AlarmColumn::Name,
698 AlarmColumn::State,
699 AlarmColumn::LastStateUpdate,
700 AlarmColumn::Conditions,
701 AlarmColumn::Actions,
702 ]
703 .iter()
704 .map(|c| c.id())
705 .collect(),
706 cw_alarm_column_ids: AlarmColumn::ids(),
707 cloudtrail_event_visible_column_ids: [
708 CloudTrailEventColumn::EventName,
709 CloudTrailEventColumn::EventTime,
710 CloudTrailEventColumn::Username,
711 CloudTrailEventColumn::EventSource,
712 CloudTrailEventColumn::ResourceType,
713 CloudTrailEventColumn::ResourceName,
714 ]
715 .iter()
716 .map(|c| c.id())
717 .collect(),
718 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
719 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
720 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
721 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
722 s3_bucket_column_ids: S3BucketColumn::ids(),
723 sqs_visible_column_ids: [
724 SqsColumn::Name,
725 SqsColumn::Type,
726 SqsColumn::Created,
727 SqsColumn::MessagesAvailable,
728 SqsColumn::MessagesInFlight,
729 SqsColumn::Encryption,
730 SqsColumn::ContentBasedDeduplication,
731 ]
732 .iter()
733 .map(|c| c.id())
734 .collect(),
735 sqs_column_ids: SqsColumn::ids(),
736 ec2_visible_column_ids: [
737 Ec2Column::Name,
738 Ec2Column::InstanceId,
739 Ec2Column::InstanceState,
740 Ec2Column::InstanceType,
741 Ec2Column::StatusCheck,
742 Ec2Column::AlarmStatus,
743 Ec2Column::AvailabilityZone,
744 Ec2Column::PublicIpv4Dns,
745 Ec2Column::PublicIpv4Address,
746 Ec2Column::ElasticIp,
747 Ec2Column::Ipv6Ips,
748 Ec2Column::Monitoring,
749 Ec2Column::SecurityGroupName,
750 Ec2Column::KeyName,
751 Ec2Column::LaunchTime,
752 Ec2Column::PlatformDetails,
753 ]
754 .iter()
755 .map(|c| c.id())
756 .collect(),
757 ec2_column_ids: Ec2Column::ids(),
758 ecr_repo_visible_column_ids: EcrColumn::ids(),
759 ecr_repo_column_ids: EcrColumn::ids(),
760 ecr_image_visible_column_ids: EcrImageColumn::ids(),
761 ecr_image_column_ids: EcrImageColumn::ids(),
762 apig_api_visible_column_ids: ApigColumn::ids(),
763 apig_api_column_ids: ApigColumn::ids(),
764 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
765 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
766 apig_resource_visible_column_ids: ResourceColumn::all()
767 .iter()
768 .map(|c| c.id())
769 .collect(),
770 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
771 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
772 lambda_application_column_ids: LambdaApplicationColumn::ids(),
773 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
774 lambda_deployment_column_ids: DeploymentColumn::ids(),
775 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
776 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
777 cfn_visible_column_ids: [
778 CfnColumn::Name,
779 CfnColumn::Status,
780 CfnColumn::CreatedTime,
781 CfnColumn::Description,
782 ]
783 .iter()
784 .map(|c| c.id())
785 .collect(),
786 cfn_column_ids: CfnColumn::ids(),
787 cfn_parameter_visible_column_ids: parameter_column_ids(),
788 cfn_parameter_column_ids: parameter_column_ids(),
789 cfn_output_visible_column_ids: output_column_ids(),
790 cfn_output_column_ids: output_column_ids(),
791 cfn_resource_visible_column_ids: resource_column_ids(),
792 cfn_resource_column_ids: resource_column_ids(),
793 cfn_event_visible_column_ids: event_visible_column_ids(),
794 cfn_event_column_ids: event_column_ids(),
795 cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
796 cfn_change_set_column_ids: change_set_column_ids(),
797 iam_user_visible_column_ids: UserColumn::visible(),
798 iam_user_column_ids: UserColumn::ids(),
799 iam_role_visible_column_ids: RoleColumn::visible(),
800 iam_role_column_ids: RoleColumn::ids(),
801 iam_group_visible_column_ids: vec![
802 "Group name".to_string(),
803 "Users".to_string(),
804 "Permissions".to_string(),
805 "Creation time".to_string(),
806 ],
807 iam_group_column_ids: vec![
808 "Group name".to_string(),
809 "Path".to_string(),
810 "Users".to_string(),
811 "Permissions".to_string(),
812 "Creation time".to_string(),
813 ],
814 iam_policy_visible_column_ids: vec![
815 "Policy name".to_string(),
816 "Type".to_string(),
817 "Attached via".to_string(),
818 ],
819 iam_policy_column_ids: vec![
820 "Policy name".to_string(),
821 "Type".to_string(),
822 "Attached via".to_string(),
823 "Attached entities".to_string(),
824 "Description".to_string(),
825 "Creation time".to_string(),
826 "Edited time".to_string(),
827 ],
828 preference_section: Preferences::Columns,
829 view_mode: ViewMode::List,
830 error_message: None,
831 error_scroll: 0,
832 page_input: String::new(),
833 calendar_date: None,
834 calendar_selecting: CalendarField::StartDate,
835 cursor_pos: 0,
836 current_session: None,
837 sessions: Vec::new(),
838 session_picker_selected: 0,
839 session_filter: String::new(),
840 session_filter_active: false,
841 region_filter: String::new(),
842 region_filter_active: false,
843 region_picker_selected: 0,
844 region_latencies: std::collections::HashMap::new(),
845 profile_filter: String::new(),
846 profile_filter_active: false,
847 profile_picker_selected: 0,
848 available_profiles: Vec::new(),
849 snapshot_requested: false,
850 })
851 }
852
853 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
854 let config = AwsConfig::dummy(region.clone());
855 Self {
856 running: true,
857 mode: Mode::ServicePicker,
858 config: config.clone(),
859 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
860 cloudtrail_client: CloudTrailClient::new(config.clone()),
861 s3_client: S3Client::new(config.clone()),
862 sqs_client: SqsClient::new(config.clone()),
863 alarms_client: AlarmsClient::new(config.clone()),
864 ec2_client: Ec2Client::new(config.clone()),
865 ecr_client: EcrClient::new(config.clone()),
866 apig_client: ApiGatewayClient::new(config.clone()),
867 iam_client: IamClient::new(config.clone()),
868 lambda_client: LambdaClient::new(config.clone()),
869 cloudformation_client: CloudFormationClient::new(config.clone()),
870 current_service: Service::CloudWatchLogGroups,
871 tabs: Vec::new(),
872 current_tab: 0,
873 tab_picker_selected: 0,
874 tab_filter: String::new(),
875 pending_key: None,
876 log_groups_state: CloudWatchLogGroupsState::new(),
877 insights_state: CloudWatchInsightsState::new(),
878 alarms_state: CloudWatchAlarmsState::new(),
879 s3_state: S3State::new(),
880 cloudtrail_state: CloudTrailState {
881 table: TableState::new(),
882 input_focus: InputFocus::Filter,
883 current_event: None,
884 event_json_scroll: 0,
885 detail_focus: CloudTrailDetailFocus::Resources,
886 resources_expanded_index: None,
887 },
888 sqs_state: SqsState::new(),
889 ec2_state: Ec2State::default(),
890 ecr_state: EcrState::new(),
891 apig_state: ApigState::new(),
892 lambda_state: LambdaState::new(),
893 lambda_application_state: LambdaApplicationState::new(),
894 cfn_state: CfnState::new(),
895 iam_state: IamState::new(),
896 service_picker: ServicePickerState::new(),
897 service_selected: false,
898 profile,
899 region: region.unwrap_or_default(),
900 region_selector_index: 0,
901 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
902 cw_log_group_column_ids: LogGroupColumn::ids(),
903 column_selector_index: 0,
904 preference_section: Preferences::Columns,
905 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
906 cw_log_stream_column_ids: StreamColumn::ids(),
907 cw_log_event_visible_column_ids: EventColumn::default_visible(),
908 cw_log_event_column_ids: EventColumn::ids(),
909 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
910 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
911 cw_alarm_visible_column_ids: [
912 AlarmColumn::Name,
913 AlarmColumn::State,
914 AlarmColumn::LastStateUpdate,
915 AlarmColumn::Conditions,
916 AlarmColumn::Actions,
917 ]
918 .iter()
919 .map(|c| c.id())
920 .collect(),
921 cw_alarm_column_ids: AlarmColumn::ids(),
922 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
923 cloudtrail_event_visible_column_ids: [
924 CloudTrailEventColumn::EventName,
925 CloudTrailEventColumn::EventTime,
926 CloudTrailEventColumn::Username,
927 CloudTrailEventColumn::EventSource,
928 CloudTrailEventColumn::ResourceType,
929 CloudTrailEventColumn::ResourceName,
930 ]
931 .iter()
932 .map(|c| c.id())
933 .collect(),
934 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
935 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
936 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
937 s3_bucket_column_ids: S3BucketColumn::ids(),
938 sqs_visible_column_ids: [
939 SqsColumn::Name,
940 SqsColumn::Type,
941 SqsColumn::Created,
942 SqsColumn::MessagesAvailable,
943 SqsColumn::MessagesInFlight,
944 SqsColumn::Encryption,
945 SqsColumn::ContentBasedDeduplication,
946 ]
947 .iter()
948 .map(|c| c.id())
949 .collect(),
950 sqs_column_ids: SqsColumn::ids(),
951 ec2_visible_column_ids: [
952 Ec2Column::Name,
953 Ec2Column::InstanceId,
954 Ec2Column::InstanceState,
955 Ec2Column::InstanceType,
956 Ec2Column::StatusCheck,
957 Ec2Column::AlarmStatus,
958 Ec2Column::AvailabilityZone,
959 Ec2Column::PublicIpv4Dns,
960 Ec2Column::PublicIpv4Address,
961 Ec2Column::ElasticIp,
962 Ec2Column::Ipv6Ips,
963 Ec2Column::Monitoring,
964 Ec2Column::SecurityGroupName,
965 Ec2Column::KeyName,
966 Ec2Column::LaunchTime,
967 Ec2Column::PlatformDetails,
968 ]
969 .iter()
970 .map(|c| c.id())
971 .collect(),
972 ec2_column_ids: Ec2Column::ids(),
973 ecr_repo_visible_column_ids: EcrColumn::ids(),
974 ecr_repo_column_ids: EcrColumn::ids(),
975 ecr_image_visible_column_ids: EcrImageColumn::ids(),
976 ecr_image_column_ids: EcrImageColumn::ids(),
977 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
978 lambda_application_column_ids: LambdaApplicationColumn::ids(),
979 apig_api_visible_column_ids: ApigColumn::ids(),
980 apig_api_column_ids: ApigColumn::ids(),
981 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
982 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
983 apig_resource_visible_column_ids: ResourceColumn::all()
984 .iter()
985 .map(|c| c.id())
986 .collect(),
987 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
988 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
989 lambda_deployment_column_ids: DeploymentColumn::ids(),
990 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
991 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
992 cfn_visible_column_ids: [
993 CfnColumn::Name,
994 CfnColumn::Status,
995 CfnColumn::CreatedTime,
996 CfnColumn::Description,
997 ]
998 .iter()
999 .map(|c| c.id())
1000 .collect(),
1001 cfn_column_ids: CfnColumn::ids(),
1002 iam_user_visible_column_ids: UserColumn::visible(),
1003 cfn_parameter_visible_column_ids: parameter_column_ids(),
1004 cfn_parameter_column_ids: parameter_column_ids(),
1005 cfn_output_visible_column_ids: output_column_ids(),
1006 cfn_output_column_ids: output_column_ids(),
1007 cfn_resource_visible_column_ids: resource_column_ids(),
1008 cfn_resource_column_ids: resource_column_ids(),
1009 cfn_event_visible_column_ids: event_visible_column_ids(),
1010 cfn_event_column_ids: event_column_ids(),
1011 cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
1012 cfn_change_set_column_ids: change_set_column_ids(),
1013 iam_user_column_ids: UserColumn::ids(),
1014 iam_role_visible_column_ids: RoleColumn::visible(),
1015 iam_role_column_ids: RoleColumn::ids(),
1016 iam_group_visible_column_ids: vec![
1017 "Group name".to_string(),
1018 "Users".to_string(),
1019 "Permissions".to_string(),
1020 "Creation time".to_string(),
1021 ],
1022 iam_group_column_ids: vec![
1023 "Group name".to_string(),
1024 "Path".to_string(),
1025 "Users".to_string(),
1026 "Permissions".to_string(),
1027 "Creation time".to_string(),
1028 ],
1029 iam_policy_visible_column_ids: vec![
1030 "Policy name".to_string(),
1031 "Type".to_string(),
1032 "Attached via".to_string(),
1033 ],
1034 iam_policy_column_ids: vec![
1035 "Policy name".to_string(),
1036 "Type".to_string(),
1037 "Attached via".to_string(),
1038 "Attached entities".to_string(),
1039 "Description".to_string(),
1040 "Creation time".to_string(),
1041 "Edited time".to_string(),
1042 ],
1043 view_mode: ViewMode::List,
1044 error_message: None,
1045 error_scroll: 0,
1046 page_input: String::new(),
1047 calendar_date: None,
1048 calendar_selecting: CalendarField::StartDate,
1049 cursor_pos: 0,
1050 current_session: None,
1051 sessions: Vec::new(),
1052 session_picker_selected: 0,
1053 session_filter: String::new(),
1054 session_filter_active: false,
1055 region_filter: String::new(),
1056 region_filter_active: false,
1057 region_picker_selected: 0,
1058 region_latencies: std::collections::HashMap::new(),
1059 profile_filter: String::new(),
1060 profile_filter_active: false,
1061 profile_picker_selected: 0,
1062 available_profiles: Vec::new(),
1063 snapshot_requested: false,
1064 }
1065 }
1066
1067 pub fn handle_action(&mut self, action: Action) {
1068 match action {
1069 Action::Noop => {}
1070 Action::EnterFilterMode => match self.mode {
1071 Mode::ServicePicker => self.service_picker.filter_active = true,
1072 Mode::RegionPicker => self.region_filter_active = true,
1073 Mode::SessionPicker => self.session_filter_active = true,
1074 Mode::ProfilePicker => self.profile_filter_active = true,
1075 _ => {}
1076 },
1077 Action::ExitFilterMode => match self.mode {
1078 Mode::ServicePicker => {
1079 if self.service_picker.filter_active {
1080 self.service_picker.filter_active = false;
1081 } else {
1082 self.mode = Mode::Normal;
1083 }
1084 }
1085 Mode::RegionPicker => {
1086 if self.region_filter_active {
1087 self.region_filter_active = false;
1088 } else {
1089 self.mode = Mode::Normal;
1090 }
1091 }
1092 Mode::SessionPicker => {
1093 if self.session_filter_active {
1094 self.session_filter_active = false;
1095 } else {
1096 self.mode = Mode::Normal;
1097 }
1098 }
1099 Mode::ProfilePicker => {
1100 if self.profile_filter_active {
1101 self.profile_filter_active = false;
1102 } else {
1103 self.mode = Mode::Normal;
1104 }
1105 }
1106 _ => {}
1107 },
1108 Action::Quit => {
1109 self.save_current_session();
1110 self.running = false;
1111 }
1112 Action::CloseService => {
1113 if !self.tabs.is_empty() {
1114 self.tabs.remove(self.current_tab);
1116
1117 if self.tabs.is_empty() {
1118 self.service_selected = false;
1120 self.current_tab = 0;
1121 self.mode = Mode::ServicePicker;
1122 } else {
1123 if self.current_tab >= self.tabs.len() {
1125 self.current_tab = self.tabs.len() - 1;
1126 }
1127 self.current_service = self.tabs[self.current_tab].service;
1128 self.service_selected = true;
1129 self.mode = Mode::Normal;
1130 }
1131 } else {
1132 self.service_selected = false;
1134 self.mode = Mode::Normal;
1135 }
1136 self.service_picker.filter.clear();
1137 self.service_picker.selected = 0;
1138 }
1139 Action::NextItem => {
1140 let should_navigate = match self.mode {
1142 Mode::ServicePicker => !self.service_picker.filter_active,
1143 Mode::RegionPicker => !self.region_filter_active,
1144 Mode::SessionPicker => !self.session_filter_active,
1145 Mode::ProfilePicker => !self.profile_filter_active,
1146 _ => true,
1147 };
1148 if should_navigate {
1149 self.next_item();
1150 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1151 self.region_filter.push('j');
1153 self.region_picker_selected = 0;
1154 }
1155 }
1156 Action::PrevItem => {
1157 let should_navigate = match self.mode {
1159 Mode::ServicePicker => !self.service_picker.filter_active,
1160 Mode::RegionPicker => !self.region_filter_active,
1161 Mode::SessionPicker => !self.session_filter_active,
1162 Mode::ProfilePicker => !self.profile_filter_active,
1163 _ => true,
1164 };
1165 if should_navigate {
1166 self.prev_item();
1167 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1168 self.region_filter.push('k');
1170 self.region_picker_selected = 0;
1171 }
1172 }
1173 Action::PageUp => self.page_up(),
1174 Action::PageDown => self.page_down(),
1175 Action::NextPane => self.next_pane(),
1176 Action::PrevPane => self.prev_pane(),
1177 Action::CollapseRow => self.collapse_row(),
1178 Action::ExpandRow => self.expand_row(),
1179 Action::Select => self.select_item(),
1180 Action::OpenSpaceMenu => {
1181 self.mode = Mode::SpaceMenu;
1182 self.service_picker.filter.clear();
1183 self.service_picker.filter_active = false;
1184 self.service_picker.selected = 0;
1185 }
1186 Action::CloseMenu => {
1187 self.mode = Mode::Normal;
1188 self.service_picker.filter.clear();
1189 match self.current_service {
1191 Service::S3Buckets => {
1192 crate::s3::actions::reset_on_service_select(self);
1193 }
1194 Service::CloudFormationStacks => {
1195 crate::cfn::actions::apply_filter_reset(self);
1196 }
1197 Service::LambdaFunctions => {
1198 crate::lambda::functions::apply_filter_reset(self);
1199 }
1200 Service::SqsQueues => {
1201 self.sqs_state.queues.reset();
1202 }
1203 Service::IamRoles => {
1204 crate::iam::actions::reset_on_service_select_roles(self);
1205 }
1206 Service::IamUsers => {
1207 crate::iam::actions::reset_on_service_select_users(self);
1208 }
1209 Service::IamUserGroups => {
1210 crate::iam::actions::reset_on_service_select_groups(self);
1211 }
1212 Service::CloudWatchAlarms => {
1213 self.alarms_state.table.reset();
1214 }
1215 Service::Ec2Instances => {
1216 crate::ec2::actions::apply_filter_reset(self);
1217 }
1218 Service::EcrRepositories => {
1219 crate::ecr::actions::apply_filter_reset(self);
1220 }
1221 Service::ApiGatewayApis => {
1222 self.apig_state.apis.reset();
1223 }
1224 Service::LambdaApplications => {
1225 crate::lambda::applications::apply_filter_reset(self);
1226 }
1227 _ => {}
1228 }
1229 }
1230 Action::NextTab => {
1231 if !self.tabs.is_empty() {
1232 self.current_tab = (self.current_tab + 1) % self.tabs.len();
1233 self.current_service = self.tabs[self.current_tab].service;
1234 }
1235 }
1236 Action::PrevTab => {
1237 if !self.tabs.is_empty() {
1238 self.current_tab = if self.current_tab == 0 {
1239 self.tabs.len() - 1
1240 } else {
1241 self.current_tab - 1
1242 };
1243 self.current_service = self.tabs[self.current_tab].service;
1244 }
1245 }
1246 Action::CloseTab => {
1247 if !self.tabs.is_empty() {
1248 self.tabs.remove(self.current_tab);
1249 if self.tabs.is_empty() {
1250 self.service_selected = false;
1252 self.current_tab = 0;
1253 self.service_picker.filter.clear();
1254 self.service_picker.selected = 0;
1255 self.mode = Mode::ServicePicker;
1256 } else {
1257 if self.current_tab >= self.tabs.len() {
1260 self.current_tab = self.tabs.len() - 1;
1261 }
1262 self.current_service = self.tabs[self.current_tab].service;
1263 self.service_selected = true;
1264 self.mode = Mode::Normal;
1265 }
1266 }
1267 }
1268 Action::OpenTabPicker => {
1269 if !self.tabs.is_empty() {
1270 self.tab_picker_selected = self.current_tab;
1271 self.mode = Mode::TabPicker;
1272 } else {
1273 self.mode = Mode::Normal;
1274 }
1275 }
1276 Action::OpenSessionPicker => {
1277 self.save_current_session();
1278 self.sessions = Session::list_all().unwrap_or_default();
1279 self.session_picker_selected = 0;
1280 self.mode = Mode::SessionPicker;
1281 }
1282 Action::LoadSession => {
1283 let filtered_sessions = self.get_filtered_sessions();
1284 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1285 let session = session.clone();
1286 self.profile = session.profile.clone();
1288 self.region = session.region.clone();
1289 self.config.account_id = session.account_id.clone();
1290 self.config.role_arn = session.role_arn.clone();
1291
1292 self.tabs.clear();
1294 for session_tab in &session.tabs {
1295 let service = match session_tab.service.as_str() {
1297 "ApiGatewayApis" => Service::ApiGatewayApis,
1298 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1299 "CloudWatchInsights" => Service::CloudWatchInsights,
1300 "CloudWatchAlarms" => Service::CloudWatchAlarms,
1301 "CloudTrailEvents" => Service::CloudTrailEvents,
1302 "S3Buckets" => Service::S3Buckets,
1303 "SqsQueues" => Service::SqsQueues,
1304 "Ec2Instances" => Service::Ec2Instances,
1305 "EcrRepositories" => Service::EcrRepositories,
1306 "LambdaFunctions" => Service::LambdaFunctions,
1307 "LambdaApplications" => Service::LambdaApplications,
1308 "CloudFormationStacks" => Service::CloudFormationStacks,
1309 "IamUsers" => Service::IamUsers,
1310 "IamRoles" => Service::IamRoles,
1311 "IamUserGroups" => Service::IamUserGroups,
1312 _ => continue,
1313 };
1314
1315 self.tabs.push(Tab {
1316 service,
1317 title: session_tab.title.clone(),
1318 breadcrumb: session_tab.breadcrumb.clone(),
1319 });
1320
1321 if let Some(filter) = &session_tab.filter {
1323 if service == Service::CloudWatchLogGroups {
1324 self.log_groups_state.log_groups.filter = filter.clone();
1325 }
1326 }
1327 }
1328
1329 if !self.tabs.is_empty() {
1330 self.current_tab = 0;
1331 self.current_service = self.tabs[0].service;
1332 self.service_selected = true;
1333 self.current_session = Some(session.clone());
1334 }
1335 }
1336 self.mode = Mode::Normal;
1337 }
1338 Action::SaveSession => {
1339 }
1341 Action::OpenServicePicker => {
1342 if self.mode == Mode::ServicePicker {
1343 self.tabs.push(Tab {
1344 service: Service::S3Buckets,
1345 title: "S3 › Buckets".to_string(),
1346 breadcrumb: "S3 › Buckets".to_string(),
1347 });
1348 self.current_tab = self.tabs.len() - 1;
1349 self.current_service = Service::S3Buckets;
1350 self.view_mode = ViewMode::List;
1351 self.service_selected = true;
1352 self.mode = Mode::Normal;
1353 } else {
1354 self.mode = Mode::ServicePicker;
1355 self.service_picker.filter.clear();
1356 self.service_picker.selected = 0;
1357 }
1358 }
1359 Action::OpenCloudWatch => {
1360 self.current_service = Service::CloudWatchLogGroups;
1361 self.view_mode = ViewMode::List;
1362 self.service_selected = true;
1363 self.mode = Mode::Normal;
1364 }
1365 Action::OpenCloudWatchSplit => {
1366 self.current_service = Service::CloudWatchInsights;
1367 self.view_mode = ViewMode::InsightsResults;
1368 self.service_selected = true;
1369 self.mode = Mode::Normal;
1370 }
1371 Action::OpenCloudWatchAlarms => {
1372 self.current_service = Service::CloudWatchAlarms;
1373 self.view_mode = ViewMode::List;
1374 self.service_selected = true;
1375 self.mode = Mode::Normal;
1376 }
1377 Action::FilterInput(c) => {
1378 if self.mode == Mode::TabPicker {
1379 self.tab_filter.push(c);
1380 self.tab_picker_selected = 0;
1381 } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1382 self.service_picker.filter.push(c);
1383 self.service_picker.selected = 0;
1384 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1385 self.region_filter.push(c);
1386 self.region_picker_selected = 0;
1387 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1388 self.profile_filter.push(c);
1389 self.profile_picker_selected = 0;
1390 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1391 self.session_filter.push(c);
1392 self.session_picker_selected = 0;
1393 } else if self.mode == Mode::InsightsInput {
1394 match self.insights_state.insights.insights_focus {
1395 InsightsFocus::Query => {
1396 self.insights_state.insights.query_text.push(c);
1397 }
1398 InsightsFocus::LogGroupSearch => {
1399 self.insights_state.insights.log_group_search.push(c);
1400 if !self.insights_state.insights.log_group_search.is_empty() {
1402 self.insights_state.insights.log_group_matches = self
1403 .log_groups_state
1404 .log_groups
1405 .items
1406 .iter()
1407 .filter(|g| {
1408 g.name.to_lowercase().contains(
1409 &self
1410 .insights_state
1411 .insights
1412 .log_group_search
1413 .to_lowercase(),
1414 )
1415 })
1416 .take(50)
1417 .map(|g| g.name.clone())
1418 .collect();
1419 self.insights_state.insights.show_dropdown = true;
1420 } else {
1421 self.insights_state.insights.log_group_matches.clear();
1422 self.insights_state.insights.show_dropdown = false;
1423 }
1424 }
1425 _ => {}
1426 }
1427 } else if self.mode == Mode::FilterInput {
1428 let is_pagination_focused = if self.current_service
1430 == Service::LambdaApplications
1431 {
1432 crate::lambda::applications::is_pagination_focused(self)
1433 } else if self.current_service == Service::CloudFormationStacks {
1434 crate::cfn::actions::is_pagination_focused(self)
1435 } else if self.current_service == Service::IamRoles
1436 && self.iam_state.current_role.is_none()
1437 {
1438 self.iam_state.role_input_focus == InputFocus::Pagination
1439 } else if self.view_mode == ViewMode::PolicyView {
1440 self.iam_state.policy_input_focus == InputFocus::Pagination
1441 } else if self.current_service == Service::CloudWatchAlarms {
1442 self.alarms_state.input_focus == InputFocus::Pagination
1443 } else if self.current_service == Service::CloudTrailEvents {
1444 self.cloudtrail_state.input_focus == InputFocus::Pagination
1445 } else if self.current_service == Service::Ec2Instances {
1446 crate::ec2::actions::is_pagination_focused(self)
1447 } else if self.current_service == Service::CloudWatchLogGroups {
1448 self.log_groups_state.input_focus == InputFocus::Pagination
1449 } else if self.current_service == Service::ApiGatewayApis {
1450 self.apig_state.input_focus == InputFocus::Pagination
1451 } else if self.current_service == Service::EcrRepositories
1452 && self.ecr_state.current_repository.is_none()
1453 {
1454 crate::ecr::actions::is_pagination_focused(self)
1455 } else if self.current_service == Service::LambdaFunctions {
1456 crate::lambda::functions::is_pagination_focused(self)
1457 } else if self.current_service == Service::SqsQueues {
1458 if self.sqs_state.current_queue.is_some()
1459 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1460 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1461 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1462 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1463 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1464 {
1465 self.sqs_state.input_focus == InputFocus::Pagination
1466 } else {
1467 false
1468 }
1469 } else {
1470 false
1471 };
1472
1473 if is_pagination_focused && c.is_ascii_digit() {
1474 self.page_input.push(c);
1475 } else if self.current_service == Service::LambdaApplications {
1476 if crate::lambda::applications::is_filter_focused(self) {
1477 self.apply_filter_operation(|f| f.push(c));
1478 }
1479 } else if self.current_service == Service::CloudFormationStacks {
1480 crate::cfn::actions::filter_char_push(self, c);
1481 } else if self.current_service == Service::EcrRepositories
1482 && self.ecr_state.current_repository.is_none()
1483 {
1484 if self.ecr_state.input_focus == InputFocus::Filter {
1485 self.apply_filter_operation(|f| f.push(c));
1486 }
1487 } else if self.current_service == Service::IamRoles
1488 && self.iam_state.current_role.is_none()
1489 {
1490 if self.iam_state.role_input_focus == InputFocus::Filter {
1491 self.apply_filter_operation(|f| f.push(c));
1492 }
1493 } else if self.view_mode == ViewMode::PolicyView {
1494 if self.iam_state.policy_input_focus == InputFocus::Filter {
1495 self.apply_filter_operation(|f| f.push(c));
1496 }
1497 } else if self.current_service == Service::ApiGatewayApis
1498 && self.apig_state.current_api.is_some()
1499 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1500 {
1501 if self.apig_state.input_focus == InputFocus::Filter {
1502 self.apply_filter_operation(|f| f.push(c));
1503 }
1504 } else if self.current_service == Service::LambdaFunctions {
1505 if crate::lambda::functions::filter_char_allowed(self) {
1506 self.apply_filter_operation(|f| f.push(c));
1507 }
1508 } else if self.current_service == Service::SqsQueues
1509 && self.sqs_state.current_queue.is_some()
1510 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1511 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1512 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1513 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1514 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1515 {
1516 if self.sqs_state.input_focus == InputFocus::Filter {
1517 self.apply_filter_operation(|f| f.push(c));
1518 }
1519 } else if self.current_service == Service::Ec2Instances
1520 && self.ec2_state.current_instance.is_some()
1521 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1522 {
1523 crate::ec2::actions::filter_char_push(self, c);
1524 } else if self.current_service == Service::CloudWatchLogGroups {
1525 if self.log_groups_state.input_focus == InputFocus::Filter {
1526 self.apply_filter_operation(|f| f.push(c));
1527 }
1528 } else {
1529 self.apply_filter_operation(|f| f.push(c));
1530 }
1531 } else if self.mode == Mode::EventFilterInput {
1532 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1533 self.log_groups_state.event_filter.push(c);
1534 } else if c.is_ascii_digit() {
1535 self.log_groups_state.relative_amount.push(c);
1536 }
1537 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1538 self.page_input.push(c);
1539 }
1540 }
1541 Action::FilterBackspace => {
1542 if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1543 self.service_picker.filter.pop();
1544 self.service_picker.selected = 0;
1545 } else if self.mode == Mode::TabPicker {
1546 self.tab_filter.pop();
1547 self.tab_picker_selected = 0;
1548 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1549 self.region_filter.pop();
1550 self.region_picker_selected = 0;
1551 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1552 self.profile_filter.pop();
1553 self.profile_picker_selected = 0;
1554 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1555 self.session_filter.pop();
1556 self.session_picker_selected = 0;
1557 } else if self.mode == Mode::InsightsInput {
1558 match self.insights_state.insights.insights_focus {
1559 InsightsFocus::Query => {
1560 self.insights_state.insights.query_text.pop();
1561 }
1562 InsightsFocus::LogGroupSearch => {
1563 self.insights_state.insights.log_group_search.pop();
1564 if !self.insights_state.insights.log_group_search.is_empty() {
1566 self.insights_state.insights.log_group_matches = self
1567 .log_groups_state
1568 .log_groups
1569 .items
1570 .iter()
1571 .filter(|g| {
1572 g.name.to_lowercase().contains(
1573 &self
1574 .insights_state
1575 .insights
1576 .log_group_search
1577 .to_lowercase(),
1578 )
1579 })
1580 .take(50)
1581 .map(|g| g.name.clone())
1582 .collect();
1583 self.insights_state.insights.show_dropdown = true;
1584 } else {
1585 self.insights_state.insights.log_group_matches.clear();
1586 self.insights_state.insights.show_dropdown = false;
1587 }
1588 }
1589 _ => {}
1590 }
1591 } else if self.mode == Mode::FilterInput {
1592 if self.current_service == Service::CloudFormationStacks {
1594 if self.cfn_state.current_stack.is_some()
1595 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1596 {
1597 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1598 self.cfn_state.parameters.filter.pop();
1599 self.cfn_state.parameters.selected = 0;
1600 }
1601 } else if self.cfn_state.current_stack.is_some()
1602 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1603 {
1604 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1605 self.cfn_state.outputs.filter.pop();
1606 self.cfn_state.outputs.selected = 0;
1607 }
1608 } else if self.cfn_state.current_stack.is_some()
1609 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1610 {
1611 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1612 self.cfn_state.resources.filter.pop();
1613 self.cfn_state.resources.selected = 0;
1614 }
1615 } else if self.cfn_state.input_focus == InputFocus::Filter {
1616 self.apply_filter_operation(|f| {
1617 f.pop();
1618 });
1619 }
1620 } else if self.current_service == Service::Ec2Instances
1621 && self.ec2_state.current_instance.is_some()
1622 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1623 {
1624 crate::ec2::actions::filter_char_pop(self);
1625 } else if self.current_service == Service::CloudWatchLogGroups {
1626 if self.log_groups_state.input_focus == InputFocus::Filter {
1627 self.apply_filter_operation(|f| {
1628 f.pop();
1629 });
1630 }
1631 } else {
1632 self.apply_filter_operation(|f| {
1633 f.pop();
1634 });
1635 }
1636 } else if self.mode == Mode::EventFilterInput {
1637 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1638 self.log_groups_state.event_filter.pop();
1639 } else {
1640 self.log_groups_state.relative_amount.pop();
1641 }
1642 }
1643 }
1644 Action::DeleteWord => {
1645 let text = if self.mode == Mode::ServicePicker {
1646 &mut self.service_picker.filter
1647 } else if self.mode == Mode::InsightsInput {
1648 use crate::app::InsightsFocus;
1649 match self.insights_state.insights.insights_focus {
1650 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1651 InsightsFocus::LogGroupSearch => {
1652 &mut self.insights_state.insights.log_group_search
1653 }
1654 _ => return,
1655 }
1656 } else if self.mode == Mode::FilterInput {
1657 if let Some(filter) = self.get_active_filter_mut() {
1658 filter
1659 } else {
1660 return;
1661 }
1662 } else if self.mode == Mode::EventFilterInput {
1663 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1664 &mut self.log_groups_state.event_filter
1665 } else {
1666 &mut self.log_groups_state.relative_amount
1667 }
1668 } else {
1669 return;
1670 };
1671
1672 if text.is_empty() {
1673 return;
1674 }
1675
1676 let mut chars: Vec<char> = text.chars().collect();
1677 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1678 chars.pop();
1679 }
1680 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1681 chars.pop();
1682 }
1683 *text = chars.into_iter().collect();
1684 }
1685 Action::WordLeft => {
1686 }
1688 Action::WordRight => {
1689 }
1691 Action::OpenColumnSelector => {
1692 if self.current_service == Service::CloudFormationStacks
1694 && crate::cfn::actions::block_column_selector(self)
1695 {
1696 return;
1697 }
1698
1699 if self.current_service == Service::IamUsers
1701 && self.iam_state.current_user.is_some()
1702 && self.iam_state.user_tab == UserTab::SecurityCredentials
1703 {
1704 return;
1705 }
1706
1707 if self.current_service == Service::IamRoles
1709 && self.iam_state.current_role.is_some()
1710 && (self.iam_state.role_tab == RoleTab::TrustRelationships
1711 || self.iam_state.role_tab == RoleTab::RevokeSessions)
1712 {
1713 return;
1714 }
1715
1716 if self.current_service == Service::SqsQueues
1718 && self.sqs_state.current_queue.is_some()
1719 && matches!(
1720 self.sqs_state.detail_tab,
1721 SqsQueueDetailTab::QueuePolicies
1722 | SqsQueueDetailTab::Monitoring
1723 | SqsQueueDetailTab::DeadLetterQueue
1724 | SqsQueueDetailTab::Encryption
1725 | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1726 )
1727 {
1728 return;
1729 }
1730
1731 if self.current_service == Service::Ec2Instances
1733 && crate::ec2::actions::block_column_selector(self)
1734 {
1735 return;
1736 }
1737
1738 if self.current_service == Service::CloudTrailEvents
1740 && self.cloudtrail_state.current_event.is_some()
1741 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1742 {
1743 return;
1744 }
1745
1746 if !self.page_input.is_empty() {
1748 if let Ok(page) = self.page_input.parse::<usize>() {
1749 self.go_to_page(page);
1750 }
1751 self.page_input.clear();
1752 } else {
1753 self.mode = Mode::ColumnSelector;
1754 self.column_selector_index = 0;
1755 }
1756 }
1757 Action::ToggleColumn => {
1758 if self.current_service == Service::S3Buckets
1759 && self.s3_state.current_bucket.is_none()
1760 {
1761 crate::s3::actions::toggle_column(self);
1762 } else if self.current_service == Service::CloudWatchAlarms {
1763 let idx = self.column_selector_index;
1767 if (1..=16).contains(&idx) {
1768 if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1770 Self::toggle_column_visibility(
1771 &mut self.cw_alarm_visible_column_ids,
1772 &self.cw_alarm_column_ids,
1773 *col,
1774 );
1775 }
1776 } else if idx == 19 {
1777 self.alarms_state.view_as = AlarmViewMode::Table;
1778 } else if idx == 20 {
1779 self.alarms_state.view_as = AlarmViewMode::Cards;
1780 } else if idx == 23 {
1781 self.alarms_state.table.page_size = PageSize::Ten;
1782 } else if idx == 24 {
1783 self.alarms_state.table.page_size = PageSize::TwentyFive;
1784 } else if idx == 25 {
1785 self.alarms_state.table.page_size = PageSize::Fifty;
1786 } else if idx == 26 {
1787 self.alarms_state.table.page_size = PageSize::OneHundred;
1788 } else if idx == 29 {
1789 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1790 }
1791 } else if self.current_service == Service::CloudTrailEvents {
1792 if self.cloudtrail_state.current_event.is_some()
1793 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
1794 {
1795 let idx = self.column_selector_index;
1797 if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
1798 if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
1799 Self::toggle_column_visibility(
1800 &mut self.cloudtrail_resource_visible_column_ids,
1801 &self.cloudtrail_resource_column_ids,
1802 *col,
1803 );
1804 }
1805 }
1806 } else {
1807 let idx = self.column_selector_index;
1809 if (1..=14).contains(&idx) {
1810 if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
1811 Self::toggle_column_visibility(
1812 &mut self.cloudtrail_event_visible_column_ids,
1813 &self.cloudtrail_event_column_ids,
1814 *col,
1815 );
1816 }
1817 } else if idx == 17 {
1818 self.cloudtrail_state.table.page_size = PageSize::Ten;
1819 self.cloudtrail_state.table.snap_to_page();
1820 } else if idx == 18 {
1821 self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
1822 self.cloudtrail_state.table.snap_to_page();
1823 } else if idx == 19 {
1824 self.cloudtrail_state.table.page_size = PageSize::Fifty;
1825 self.cloudtrail_state.table.snap_to_page();
1826 } else if idx == 20 {
1827 self.cloudtrail_state.table.page_size = PageSize::OneHundred;
1828 self.cloudtrail_state.table.snap_to_page();
1829 }
1830 }
1831 } else if self.current_service == Service::ApiGatewayApis {
1832 if let Some(api) = &self.apig_state.current_api {
1833 use crate::ui::apig::ApiDetailTab;
1834 if self.apig_state.detail_tab == ApiDetailTab::Routes {
1835 if api.protocol_type.to_uppercase() == "REST" {
1837 let idx = self.column_selector_index;
1839 if idx > 1 && idx <= self.apig_resource_column_ids.len() {
1840 if let Some(col) = self.apig_resource_column_ids.get(idx - 1) {
1841 if let Some(pos) = self
1842 .apig_resource_visible_column_ids
1843 .iter()
1844 .position(|c| c == col)
1845 {
1846 self.apig_resource_visible_column_ids.remove(pos);
1847 } else {
1848 self.apig_resource_visible_column_ids.push(*col);
1849 }
1850 }
1851 }
1852 } else {
1853 let idx = self.column_selector_index;
1855 if idx > 1 && idx <= self.apig_route_column_ids.len() {
1856 if let Some(col) = self.apig_route_column_ids.get(idx - 1) {
1857 if let Some(pos) = self
1858 .apig_route_visible_column_ids
1859 .iter()
1860 .position(|c| c == col)
1861 {
1862 self.apig_route_visible_column_ids.remove(pos);
1863 } else {
1864 self.apig_route_visible_column_ids.push(*col);
1865 }
1866 }
1867 }
1868 }
1869 }
1871 } else {
1872 let idx = self.column_selector_index;
1874 if idx > 0 && idx <= self.apig_api_column_ids.len() {
1875 if let Some(col) = self.apig_api_column_ids.get(idx - 1) {
1876 if let Some(pos) = self
1877 .apig_api_visible_column_ids
1878 .iter()
1879 .position(|c| c == col)
1880 {
1881 self.apig_api_visible_column_ids.remove(pos);
1882 } else {
1883 self.apig_api_visible_column_ids.push(*col);
1884 }
1885 }
1886 } else if idx == self.apig_api_column_ids.len() + 3 {
1887 self.apig_state.apis.page_size = PageSize::Ten;
1888 } else if idx == self.apig_api_column_ids.len() + 4 {
1889 self.apig_state.apis.page_size = PageSize::TwentyFive;
1890 } else if idx == self.apig_api_column_ids.len() + 5 {
1891 self.apig_state.apis.page_size = PageSize::Fifty;
1892 } else if idx == self.apig_api_column_ids.len() + 6 {
1893 self.apig_state.apis.page_size = PageSize::OneHundred;
1894 }
1895 }
1896 } else if self.current_service == Service::EcrRepositories {
1897 crate::ecr::actions::toggle_column(self);
1898 } else if self.current_service == Service::Ec2Instances {
1899 crate::ec2::actions::toggle_column(self);
1900 } else if self.current_service == Service::SqsQueues {
1901 if self.sqs_state.current_queue.is_some()
1902 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1903 {
1904 let idx = self.column_selector_index;
1906 if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
1907 if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
1908 if let Some(pos) = self
1909 .sqs_state
1910 .trigger_visible_column_ids
1911 .iter()
1912 .position(|c| c == col)
1913 {
1914 self.sqs_state.trigger_visible_column_ids.remove(pos);
1915 } else {
1916 self.sqs_state.trigger_visible_column_ids.push(col.clone());
1917 }
1918 }
1919 } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
1920 self.sqs_state.triggers.page_size = PageSize::Ten;
1921 } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
1922 self.sqs_state.triggers.page_size = PageSize::TwentyFive;
1923 } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
1924 self.sqs_state.triggers.page_size = PageSize::Fifty;
1925 } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
1926 self.sqs_state.triggers.page_size = PageSize::OneHundred;
1927 }
1928 } else if self.sqs_state.current_queue.is_some()
1929 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1930 {
1931 let idx = self.column_selector_index;
1933 if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
1934 if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
1935 if let Some(pos) = self
1936 .sqs_state
1937 .pipe_visible_column_ids
1938 .iter()
1939 .position(|c| c == col)
1940 {
1941 self.sqs_state.pipe_visible_column_ids.remove(pos);
1942 } else {
1943 self.sqs_state.pipe_visible_column_ids.push(col.clone());
1944 }
1945 }
1946 } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
1947 self.sqs_state.pipes.page_size = PageSize::Ten;
1948 } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
1949 self.sqs_state.pipes.page_size = PageSize::TwentyFive;
1950 } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
1951 self.sqs_state.pipes.page_size = PageSize::Fifty;
1952 } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
1953 self.sqs_state.pipes.page_size = PageSize::OneHundred;
1954 }
1955 } else if self.sqs_state.current_queue.is_some()
1956 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1957 {
1958 let idx = self.column_selector_index;
1960 if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
1961 if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
1962 if let Some(pos) = self
1963 .sqs_state
1964 .tag_visible_column_ids
1965 .iter()
1966 .position(|c| c == col)
1967 {
1968 self.sqs_state.tag_visible_column_ids.remove(pos);
1969 } else {
1970 self.sqs_state.tag_visible_column_ids.push(col.clone());
1971 }
1972 }
1973 } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
1974 self.sqs_state.tags.page_size = PageSize::Ten;
1975 } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
1976 self.sqs_state.tags.page_size = PageSize::TwentyFive;
1977 } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
1978 self.sqs_state.tags.page_size = PageSize::Fifty;
1979 } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
1980 self.sqs_state.tags.page_size = PageSize::OneHundred;
1981 }
1982 } else if self.sqs_state.current_queue.is_some()
1983 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
1984 {
1985 let idx = self.column_selector_index;
1987 if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
1988 if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
1989 if let Some(pos) = self
1990 .sqs_state
1991 .subscription_visible_column_ids
1992 .iter()
1993 .position(|c| c == col)
1994 {
1995 self.sqs_state.subscription_visible_column_ids.remove(pos);
1996 } else {
1997 self.sqs_state
1998 .subscription_visible_column_ids
1999 .push(col.clone());
2000 }
2001 }
2002 } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
2003 self.sqs_state.subscriptions.page_size = PageSize::Ten;
2004 } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
2005 self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
2006 } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
2007 self.sqs_state.subscriptions.page_size = PageSize::Fifty;
2008 } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
2009 self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
2010 }
2011 } else {
2012 let idx = self.column_selector_index;
2014 if let Some(col) = self.sqs_column_ids.get(idx) {
2015 if let Some(pos) =
2016 self.sqs_visible_column_ids.iter().position(|c| c == col)
2017 {
2018 self.sqs_visible_column_ids.remove(pos);
2019 } else {
2020 self.sqs_visible_column_ids.push(*col);
2021 }
2022 } else if idx == self.sqs_column_ids.len() + 2 {
2023 self.sqs_state.queues.page_size = PageSize::Ten;
2024 } else if idx == self.sqs_column_ids.len() + 3 {
2025 self.sqs_state.queues.page_size = PageSize::TwentyFive;
2026 } else if idx == self.sqs_column_ids.len() + 4 {
2027 self.sqs_state.queues.page_size = PageSize::Fifty;
2028 } else if idx == self.sqs_column_ids.len() + 5 {
2029 self.sqs_state.queues.page_size = PageSize::OneHundred;
2030 }
2031 }
2032 } else if self.current_service == Service::LambdaFunctions {
2033 crate::lambda::functions::toggle_column(self);
2034 } else if self.current_service == Service::LambdaApplications {
2035 crate::lambda::applications::toggle_column(self);
2036 } else if self.view_mode == ViewMode::Events {
2037 if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
2038 {
2039 if let Some(pos) = self
2040 .cw_log_event_visible_column_ids
2041 .iter()
2042 .position(|c| c == col)
2043 {
2044 self.cw_log_event_visible_column_ids.remove(pos);
2045 } else {
2046 self.cw_log_event_visible_column_ids.push(*col);
2047 }
2048 }
2049 } else if self.view_mode == ViewMode::Detail {
2050 let idx = self.column_selector_index;
2051 if self.log_groups_state.detail_tab == DetailTab::Tags {
2052 if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
2054 if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
2055 if let Some(pos) = self
2056 .cw_log_tag_visible_column_ids
2057 .iter()
2058 .position(|c| c == col)
2059 {
2060 self.cw_log_tag_visible_column_ids.remove(pos);
2061 } else {
2062 self.cw_log_tag_visible_column_ids.push(*col);
2063 }
2064 }
2065 } else if idx == self.cw_log_tag_column_ids.len() + 3 {
2066 self.log_groups_state.tags.page_size = PageSize::Ten;
2067 } else if idx == self.cw_log_tag_column_ids.len() + 4 {
2068 self.log_groups_state.tags.page_size = PageSize::TwentyFive;
2069 } else if idx == self.cw_log_tag_column_ids.len() + 5 {
2070 self.log_groups_state.tags.page_size = PageSize::Fifty;
2071 } else if idx == self.cw_log_tag_column_ids.len() + 6 {
2072 self.log_groups_state.tags.page_size = PageSize::OneHundred;
2073 }
2074 } else {
2075 if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
2077 if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
2078 if let Some(pos) = self
2079 .cw_log_stream_visible_column_ids
2080 .iter()
2081 .position(|c| c == col)
2082 {
2083 self.cw_log_stream_visible_column_ids.remove(pos);
2084 } else {
2085 self.cw_log_stream_visible_column_ids.push(*col);
2086 }
2087 }
2088 } else if idx == self.cw_log_stream_column_ids.len() + 3 {
2089 self.log_groups_state.stream_page_size = 10;
2090 self.log_groups_state.stream_current_page = 0;
2091 } else if idx == self.cw_log_stream_column_ids.len() + 4 {
2092 self.log_groups_state.stream_page_size = 25;
2093 self.log_groups_state.stream_current_page = 0;
2094 } else if idx == self.cw_log_stream_column_ids.len() + 5 {
2095 self.log_groups_state.stream_page_size = 50;
2096 self.log_groups_state.stream_current_page = 0;
2097 } else if idx == self.cw_log_stream_column_ids.len() + 6 {
2098 self.log_groups_state.stream_page_size = 100;
2099 self.log_groups_state.stream_current_page = 0;
2100 }
2101 }
2102 } else if self.current_service == Service::CloudFormationStacks {
2103 crate::cfn::actions::toggle_column(self);
2104 } else if self.current_service == Service::IamUsers {
2105 let idx = self.column_selector_index;
2106 if self.iam_state.current_user.is_some() {
2107 match self.iam_state.user_tab {
2108 UserTab::Permissions => {
2109 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
2111 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
2112 if let Some(pos) = self
2113 .iam_policy_visible_column_ids
2114 .iter()
2115 .position(|c| c == col)
2116 {
2117 self.iam_policy_visible_column_ids.remove(pos);
2118 } else {
2119 self.iam_policy_visible_column_ids.push(col.clone());
2120 }
2121 }
2122 } else if idx == self.iam_policy_column_ids.len() + 3 {
2123 self.iam_state.policies.page_size = PageSize::Ten;
2124 } else if idx == self.iam_policy_column_ids.len() + 4 {
2125 self.iam_state.policies.page_size = PageSize::TwentyFive;
2126 } else if idx == self.iam_policy_column_ids.len() + 5 {
2127 self.iam_state.policies.page_size = PageSize::Fifty;
2128 }
2129 }
2130 UserTab::Groups => {
2131 toggle_iam_page_size_only(
2132 idx,
2133 5,
2134 &mut self.iam_state.user_group_memberships.page_size,
2135 );
2136 }
2137 UserTab::Tags => {
2138 toggle_iam_page_size_only(
2139 idx,
2140 5,
2141 &mut self.iam_state.user_tags.page_size,
2142 );
2143 }
2144 UserTab::LastAccessed => {
2145 toggle_iam_page_size_only(
2146 idx,
2147 6,
2148 &mut self.iam_state.last_accessed_services.page_size,
2149 );
2150 }
2151 _ => {}
2152 }
2153 } else {
2154 toggle_iam_preference_static(
2156 idx,
2157 &self.iam_user_column_ids,
2158 &mut self.iam_user_visible_column_ids,
2159 &mut self.iam_state.users.page_size,
2160 );
2161 }
2162 } else if self.current_service == Service::IamRoles {
2163 let idx = self.column_selector_index;
2164 if self.iam_state.current_role.is_some() {
2165 match self.iam_state.role_tab {
2166 RoleTab::Permissions => {
2167 toggle_iam_preference(
2169 idx,
2170 &self.iam_policy_column_ids,
2171 &mut self.iam_policy_visible_column_ids,
2172 &mut self.iam_state.policies.page_size,
2173 );
2174 }
2175 RoleTab::LastAccessed => {
2176 toggle_iam_page_size_only(
2178 idx,
2179 6,
2180 &mut self.iam_state.last_accessed_services.page_size,
2181 );
2182 }
2183 _ => {}
2184 }
2185 } else {
2186 toggle_iam_preference_static(
2188 idx,
2189 &self.iam_role_column_ids,
2190 &mut self.iam_role_visible_column_ids,
2191 &mut self.iam_state.roles.page_size,
2192 );
2193 }
2194 } else if self.current_service == Service::IamUserGroups {
2195 toggle_iam_preference(
2196 self.column_selector_index,
2197 &self.iam_group_column_ids,
2198 &mut self.iam_group_visible_column_ids,
2199 &mut self.iam_state.groups.page_size,
2200 );
2201 } else {
2202 let idx = self.column_selector_index;
2203 if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
2204 if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
2205 if let Some(pos) = self
2206 .cw_log_group_visible_column_ids
2207 .iter()
2208 .position(|c| c == col)
2209 {
2210 self.cw_log_group_visible_column_ids.remove(pos);
2211 } else {
2212 self.cw_log_group_visible_column_ids.push(*col);
2213 }
2214 }
2215 } else if idx == self.cw_log_group_column_ids.len() + 3 {
2216 self.log_groups_state.log_groups.page_size = PageSize::Ten;
2217 } else if idx == self.cw_log_group_column_ids.len() + 4 {
2218 self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
2219 } else if idx == self.cw_log_group_column_ids.len() + 5 {
2220 self.log_groups_state.log_groups.page_size = PageSize::Fifty;
2221 } else if idx == self.cw_log_group_column_ids.len() + 6 {
2222 self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2223 }
2224 }
2225 }
2226 Action::NextPreferences => {
2227 if self.current_service == Service::ApiGatewayApis {
2228 cycle_preference_next(
2229 &mut self.column_selector_index,
2230 self.apig_api_column_ids.len(),
2231 );
2232 } else if self.current_service == Service::CloudWatchAlarms {
2233 if self.column_selector_index < 18 {
2235 self.column_selector_index = 18; } else if self.column_selector_index < 22 {
2237 self.column_selector_index = 22; } else if self.column_selector_index < 28 {
2239 self.column_selector_index = 28; } else {
2241 self.column_selector_index = 0; }
2243 } else if self.current_service == Service::EcrRepositories
2244 && self.ecr_state.current_repository.is_some()
2245 {
2246 crate::ecr::actions::next_preferences(self);
2247 } else if self.current_service == Service::LambdaFunctions {
2248 crate::lambda::functions::next_preferences(self);
2249 } else if self.current_service == Service::LambdaApplications {
2250 crate::lambda::applications::next_preferences(self);
2251 } else if self.current_service == Service::CloudFormationStacks {
2252 crate::cfn::actions::next_preferences(self);
2253 } else if self.current_service == Service::CloudTrailEvents {
2254 if self.cloudtrail_state.current_event.is_some()
2255 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2256 {
2257 self.column_selector_index = 0;
2259 } else {
2260 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2261 if self.column_selector_index < page_size_idx {
2262 self.column_selector_index = page_size_idx;
2263 } else {
2264 self.column_selector_index = 0;
2265 }
2266 }
2267 } else if self.current_service == Service::Ec2Instances {
2268 let page_size_idx = self.ec2_column_ids.len() + 2;
2269 if self.column_selector_index < page_size_idx {
2270 self.column_selector_index = page_size_idx;
2271 } else {
2272 self.column_selector_index = 0;
2273 }
2274 } else if self.current_service == Service::IamUsers {
2275 if self.iam_state.current_user.is_some() {
2276 match self.iam_state.user_tab {
2277 UserTab::Permissions => {
2278 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2280 if self.column_selector_index < page_size_idx {
2281 self.column_selector_index = page_size_idx;
2282 } else {
2283 self.column_selector_index = 0;
2284 }
2285 }
2286 UserTab::Groups | UserTab::Tags => {
2287 if self.column_selector_index < 4 {
2289 self.column_selector_index = 4;
2290 } else {
2291 self.column_selector_index = 0;
2292 }
2293 }
2294 UserTab::LastAccessed => {
2295 if self.column_selector_index < 5 {
2297 self.column_selector_index = 5;
2298 } else {
2299 self.column_selector_index = 0;
2300 }
2301 }
2302 _ => {}
2303 }
2304 } else {
2305 let page_size_idx = self.iam_user_column_ids.len() + 2;
2307 if self.column_selector_index < page_size_idx {
2308 self.column_selector_index = page_size_idx;
2309 } else {
2310 self.column_selector_index = 0;
2311 }
2312 }
2313 } else if self.current_service == Service::IamRoles {
2314 if self.iam_state.current_role.is_some() {
2315 match self.iam_state.role_tab {
2316 RoleTab::Permissions => {
2317 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2319 if self.column_selector_index < page_size_idx {
2320 self.column_selector_index = page_size_idx;
2321 } else {
2322 self.column_selector_index = 0;
2323 }
2324 }
2325 RoleTab::Tags => {
2326 if self.column_selector_index < 4 {
2328 self.column_selector_index = 4;
2329 } else {
2330 self.column_selector_index = 0;
2331 }
2332 }
2333 RoleTab::LastAccessed => {
2334 if self.column_selector_index < 5 {
2336 self.column_selector_index = 5;
2337 } else {
2338 self.column_selector_index = 0;
2339 }
2340 }
2341 _ => {}
2342 }
2343 } else {
2344 let page_size_idx = self.iam_role_column_ids.len() + 2;
2346 if self.column_selector_index < page_size_idx {
2347 self.column_selector_index = page_size_idx;
2348 } else {
2349 self.column_selector_index = 0;
2350 }
2351 }
2352 } else if self.current_service == Service::IamUserGroups {
2353 let page_size_idx = self.iam_group_column_ids.len() + 2;
2355 if self.column_selector_index < page_size_idx {
2356 self.column_selector_index = page_size_idx;
2357 } else {
2358 self.column_selector_index = 0;
2359 }
2360 } else if self.current_service == Service::SqsQueues
2361 && self.sqs_state.current_queue.is_some()
2362 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2363 {
2364 let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2366 if self.column_selector_index < page_size_idx {
2367 self.column_selector_index = page_size_idx;
2368 } else {
2369 self.column_selector_index = 0;
2370 }
2371 } else if self.current_service == Service::SqsQueues
2372 && self.sqs_state.current_queue.is_some()
2373 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2374 {
2375 let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2377 if self.column_selector_index < page_size_idx {
2378 self.column_selector_index = page_size_idx;
2379 } else {
2380 self.column_selector_index = 0;
2381 }
2382 } else if self.current_service == Service::SqsQueues
2383 && self.sqs_state.current_queue.is_some()
2384 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2385 {
2386 let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2388 if self.column_selector_index < page_size_idx {
2389 self.column_selector_index = page_size_idx;
2390 } else {
2391 self.column_selector_index = 0;
2392 }
2393 } else if self.current_service == Service::SqsQueues
2394 && self.sqs_state.current_queue.is_some()
2395 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2396 {
2397 let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2399 if self.column_selector_index < page_size_idx {
2400 self.column_selector_index = page_size_idx;
2401 } else {
2402 self.column_selector_index = 0;
2403 }
2404 } else if self.current_service == Service::S3Buckets
2405 && self.s3_state.current_bucket.is_none()
2406 {
2407 crate::s3::actions::next_preferences(self);
2408 } else if self.current_service == Service::CloudWatchLogGroups {
2409 if self.view_mode == ViewMode::Events {
2410 } else if self.view_mode == ViewMode::Detail {
2412 if self.log_groups_state.detail_tab == DetailTab::Tags {
2413 cycle_preference_next(
2415 &mut self.column_selector_index,
2416 self.cw_log_tag_column_ids.len(),
2417 );
2418 } else {
2419 cycle_preference_next(
2421 &mut self.column_selector_index,
2422 self.cw_log_stream_column_ids.len(),
2423 );
2424 }
2425 } else {
2426 cycle_preference_next(
2428 &mut self.column_selector_index,
2429 self.cw_log_group_column_ids.len(),
2430 );
2431 }
2432 }
2433 }
2434 Action::PrevPreferences => {
2435 if self.current_service == Service::ApiGatewayApis {
2436 cycle_preference_prev(
2437 &mut self.column_selector_index,
2438 self.apig_api_column_ids.len(),
2439 );
2440 } else if self.current_service == Service::CloudWatchAlarms {
2441 if self.column_selector_index >= 28 {
2443 self.column_selector_index = 22;
2444 } else if self.column_selector_index >= 22 {
2445 self.column_selector_index = 18;
2446 } else if self.column_selector_index >= 18 {
2447 self.column_selector_index = 0;
2448 } else {
2449 self.column_selector_index = 28;
2450 }
2451 } else if self.current_service == Service::EcrRepositories
2452 && self.ecr_state.current_repository.is_some()
2453 {
2454 crate::ecr::actions::prev_preferences(self);
2455 } else if self.current_service == Service::LambdaFunctions {
2456 crate::lambda::functions::prev_preferences(self);
2457 } else if self.current_service == Service::LambdaApplications {
2458 crate::lambda::applications::prev_preferences(self);
2459 } else if self.current_service == Service::CloudFormationStacks {
2460 crate::cfn::actions::prev_preferences(self);
2461 } else if self.current_service == Service::CloudTrailEvents {
2462 if self.cloudtrail_state.current_event.is_some()
2463 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2464 {
2465 self.column_selector_index = 0;
2467 } else {
2468 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2469 if self.column_selector_index >= page_size_idx {
2470 self.column_selector_index = 0;
2471 } else {
2472 self.column_selector_index = page_size_idx;
2473 }
2474 }
2475 } else if self.current_service == Service::Ec2Instances {
2476 crate::ec2::actions::next_preferences(self);
2477 } else if self.current_service == Service::IamUsers {
2478 if self.iam_state.current_user.is_some() {
2479 match self.iam_state.user_tab {
2480 UserTab::Permissions => {
2481 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2482 if self.column_selector_index >= page_size_idx {
2483 self.column_selector_index = 0;
2484 } else {
2485 self.column_selector_index = page_size_idx;
2486 }
2487 }
2488 UserTab::Groups | UserTab::Tags => {
2489 if self.column_selector_index >= 4 {
2490 self.column_selector_index = 0;
2491 } else {
2492 self.column_selector_index = 4;
2493 }
2494 }
2495 UserTab::LastAccessed => {
2496 if self.column_selector_index >= 5 {
2497 self.column_selector_index = 0;
2498 } else {
2499 self.column_selector_index = 5;
2500 }
2501 }
2502 _ => {}
2503 }
2504 } else {
2505 let page_size_idx = self.iam_user_column_ids.len() + 2;
2506 if self.column_selector_index >= page_size_idx {
2507 self.column_selector_index = 0;
2508 } else {
2509 self.column_selector_index = page_size_idx;
2510 }
2511 }
2512 } else if self.current_service == Service::IamRoles {
2513 if self.iam_state.current_role.is_some() {
2514 match self.iam_state.role_tab {
2515 RoleTab::Permissions => {
2516 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2517 if self.column_selector_index >= page_size_idx {
2518 self.column_selector_index = 0;
2519 } else {
2520 self.column_selector_index = page_size_idx;
2521 }
2522 }
2523 RoleTab::Tags => {
2524 if self.column_selector_index >= 4 {
2525 self.column_selector_index = 0;
2526 } else {
2527 self.column_selector_index = 4;
2528 }
2529 }
2530 RoleTab::LastAccessed => {
2531 if self.column_selector_index >= 5 {
2532 self.column_selector_index = 0;
2533 } else {
2534 self.column_selector_index = 5;
2535 }
2536 }
2537 _ => {}
2538 }
2539 } else {
2540 let page_size_idx = self.iam_role_column_ids.len() + 2;
2541 if self.column_selector_index >= page_size_idx {
2542 self.column_selector_index = 0;
2543 } else {
2544 self.column_selector_index = page_size_idx;
2545 }
2546 }
2547 } else if self.current_service == Service::IamUserGroups {
2548 let page_size_idx = self.iam_group_column_ids.len() + 2;
2549 if self.column_selector_index >= page_size_idx {
2550 self.column_selector_index = 0;
2551 } else {
2552 self.column_selector_index = page_size_idx;
2553 }
2554 } else if self.current_service == Service::SqsQueues
2555 && self.sqs_state.current_queue.is_some()
2556 {
2557 let page_size_idx = match self.sqs_state.detail_tab {
2558 SqsQueueDetailTab::LambdaTriggers => {
2559 self.sqs_state.trigger_column_ids.len() + 2
2560 }
2561 SqsQueueDetailTab::EventBridgePipes => {
2562 self.sqs_state.pipe_column_ids.len() + 2
2563 }
2564 SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
2565 SqsQueueDetailTab::SnsSubscriptions => {
2566 self.sqs_state.subscription_column_ids.len() + 2
2567 }
2568 _ => 0,
2569 };
2570 if page_size_idx > 0 {
2571 if self.column_selector_index >= page_size_idx {
2572 self.column_selector_index = 0;
2573 } else {
2574 self.column_selector_index = page_size_idx;
2575 }
2576 }
2577 } else if self.current_service == Service::S3Buckets
2578 && self.s3_state.current_bucket.is_none()
2579 {
2580 crate::s3::actions::prev_preferences(self);
2581 } else if self.current_service == Service::CloudWatchLogGroups {
2582 if self.view_mode == ViewMode::Events {
2583 } else if self.view_mode == ViewMode::Detail {
2585 if self.log_groups_state.detail_tab == DetailTab::Tags {
2586 cycle_preference_prev(
2588 &mut self.column_selector_index,
2589 self.cw_log_tag_column_ids.len(),
2590 );
2591 } else {
2592 cycle_preference_prev(
2594 &mut self.column_selector_index,
2595 self.cw_log_stream_column_ids.len(),
2596 );
2597 }
2598 } else {
2599 cycle_preference_prev(
2601 &mut self.column_selector_index,
2602 self.cw_log_group_column_ids.len(),
2603 );
2604 }
2605 }
2606 }
2607 Action::CloseColumnSelector => {
2608 self.mode = Mode::Normal;
2609 self.preference_section = Preferences::Columns;
2610 }
2611 Action::NextDetailTab => {
2612 if self.current_service == Service::CloudTrailEvents
2613 && self.cloudtrail_state.current_event.is_some()
2614 {
2615 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
2616 } else if self.current_service == Service::ApiGatewayApis
2617 && self.apig_state.current_api.is_some()
2618 {
2619 self.apig_state.detail_tab = self.apig_state.detail_tab.next();
2620 } else if self.current_service == Service::SqsQueues
2621 && self.sqs_state.current_queue.is_some()
2622 {
2623 self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
2624 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2625 self.sqs_state.set_metrics_loading(true);
2626 self.sqs_state.set_monitoring_scroll(0);
2627 self.sqs_state.clear_metrics();
2628 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
2629 self.sqs_state.triggers.loading = true;
2630 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
2631 self.sqs_state.pipes.loading = true;
2632 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
2633 self.sqs_state.tags.loading = true;
2634 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
2635 self.sqs_state.subscriptions.loading = true;
2636 }
2637 } else if self.current_service == Service::Ec2Instances
2638 && self.ec2_state.current_instance.is_some()
2639 {
2640 crate::ec2::actions::next_detail_tab(self);
2641 } else if self.current_service == Service::LambdaApplications
2642 && self.lambda_application_state.current_application.is_some()
2643 {
2644 crate::lambda::applications::next_detail_tab(self);
2645 } else if self.current_service == Service::IamRoles
2646 && self.iam_state.current_role.is_some()
2647 {
2648 crate::iam::actions::next_detail_tab_roles(self);
2649 } else if self.current_service == Service::IamUsers
2650 && self.iam_state.current_user.is_some()
2651 {
2652 crate::iam::actions::next_detail_tab_users(self);
2653 } else if self.current_service == Service::IamUserGroups
2654 && self.iam_state.current_group.is_some()
2655 {
2656 crate::iam::actions::next_detail_tab_groups(self);
2657 } else if self.view_mode == ViewMode::Detail {
2658 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
2659 if self.log_groups_state.detail_tab == DetailTab::Tags {
2660 self.log_groups_state.tags.loading = true;
2661 }
2662 } else if self.current_service == Service::S3Buckets {
2663 crate::s3::actions::next_detail_tab(self);
2664 } else if self.current_service == Service::CloudWatchAlarms {
2665 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2666 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2667 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2668 };
2669 self.alarms_state.table.reset();
2670 } else if self.current_service == Service::EcrRepositories
2671 && self.ecr_state.current_repository.is_none()
2672 {
2673 crate::ecr::actions::next_detail_tab(self);
2674 } else if self.current_service == Service::LambdaFunctions
2675 && self.lambda_state.current_function.is_some()
2676 {
2677 crate::lambda::functions::next_detail_tab(self);
2678 } else if self.current_service == Service::CloudFormationStacks
2679 && self.cfn_state.current_stack.is_some()
2680 {
2681 crate::cfn::actions::next_detail_tab(self);
2682 }
2683 }
2684 Action::PrevDetailTab => {
2685 if self.current_service == Service::CloudTrailEvents
2686 && self.cloudtrail_state.current_event.is_some()
2687 {
2688 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
2689 } else if self.current_service == Service::ApiGatewayApis
2690 && self.apig_state.current_api.is_some()
2691 {
2692 self.apig_state.detail_tab = self.apig_state.detail_tab.prev();
2693 } else if self.current_service == Service::SqsQueues
2694 && self.sqs_state.current_queue.is_some()
2695 {
2696 self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
2697 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2698 self.sqs_state.set_metrics_loading(true);
2699 self.sqs_state.set_monitoring_scroll(0);
2700 self.sqs_state.clear_metrics();
2701 }
2702 } else if self.current_service == Service::Ec2Instances
2703 && self.ec2_state.current_instance.is_some()
2704 {
2705 crate::ec2::actions::prev_detail_tab(self);
2706 } else if self.current_service == Service::LambdaApplications
2707 && self.lambda_application_state.current_application.is_some()
2708 {
2709 crate::lambda::applications::prev_detail_tab(self);
2710 } else if self.current_service == Service::IamRoles
2711 && self.iam_state.current_role.is_some()
2712 {
2713 crate::iam::actions::prev_detail_tab_roles(self);
2714 } else if self.current_service == Service::IamUsers
2715 && self.iam_state.current_user.is_some()
2716 {
2717 crate::iam::actions::prev_detail_tab_users(self);
2718 } else if self.current_service == Service::IamUserGroups
2719 && self.iam_state.current_group.is_some()
2720 {
2721 crate::iam::actions::prev_detail_tab_groups(self);
2722 } else if self.view_mode == ViewMode::Detail {
2723 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
2724 } else if self.current_service == Service::S3Buckets {
2725 crate::s3::actions::prev_detail_tab(self);
2726 } else if self.current_service == Service::CloudWatchAlarms {
2727 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2728 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2729 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2730 };
2731 } else if self.current_service == Service::EcrRepositories
2732 && self.ecr_state.current_repository.is_none()
2733 {
2734 crate::ecr::actions::prev_detail_tab(self);
2735 } else if self.current_service == Service::LambdaFunctions
2736 && self.lambda_state.current_function.is_some()
2737 {
2738 crate::lambda::functions::prev_detail_tab(self);
2739 } else if self.current_service == Service::CloudFormationStacks
2740 && self.cfn_state.current_stack.is_some()
2741 {
2742 crate::cfn::actions::prev_detail_tab(self);
2743 }
2744 }
2745 Action::StartFilter => {
2746 if !self.service_selected && self.tabs.is_empty() {
2748 return;
2749 }
2750
2751 if self.current_service == Service::CloudWatchInsights {
2752 self.mode = Mode::InsightsInput;
2753 } else if self.current_service == Service::CloudWatchAlarms {
2754 self.mode = Mode::FilterInput;
2755 } else if self.current_service == Service::CloudTrailEvents {
2756 self.mode = Mode::FilterInput;
2757 self.cloudtrail_state.input_focus = InputFocus::Filter;
2758 } else if self.current_service == Service::S3Buckets {
2759 self.mode = Mode::FilterInput;
2760 crate::s3::actions::start_filter(self);
2761 } else if self.current_service == Service::ApiGatewayApis
2762 || self.current_service == Service::EcrRepositories
2763 || self.current_service == Service::IamUsers
2764 || self.current_service == Service::IamUserGroups
2765 {
2766 self.mode = Mode::FilterInput;
2767 if self.current_service == Service::ApiGatewayApis {
2768 self.apig_state.input_focus = InputFocus::Filter;
2769 } else if self.current_service == Service::EcrRepositories
2770 && self.ecr_state.current_repository.is_none()
2771 {
2772 self.ecr_state.input_focus = InputFocus::Filter;
2773 }
2774 } else if self.current_service == Service::LambdaFunctions {
2775 self.mode = Mode::FilterInput;
2776 crate::lambda::functions::start_filter(self);
2777 } else if self.current_service == Service::LambdaApplications {
2778 self.mode = Mode::FilterInput;
2779 crate::lambda::applications::start_filter(self);
2780 } else if self.current_service == Service::IamRoles {
2781 self.mode = Mode::FilterInput;
2782 } else if self.current_service == Service::CloudFormationStacks {
2783 self.mode = Mode::FilterInput;
2784 crate::cfn::actions::start_filter(self);
2785 } else if self.current_service == Service::SqsQueues {
2786 self.mode = Mode::FilterInput;
2787 self.sqs_state.input_focus = InputFocus::Filter;
2788 } else if self.view_mode == ViewMode::List
2789 || (self.view_mode == ViewMode::Detail
2790 && (self.log_groups_state.detail_tab == DetailTab::LogStreams
2791 || self.log_groups_state.detail_tab == DetailTab::Tags))
2792 {
2793 self.mode = Mode::FilterInput;
2794 self.log_groups_state.filter_mode = true;
2795 self.log_groups_state.input_focus = InputFocus::Filter;
2796 } else if self.view_mode == ViewMode::Events {
2797 self.mode = Mode::EventFilterInput;
2798 }
2799 }
2800 Action::StartEventFilter => {
2801 if self.current_service == Service::CloudWatchInsights {
2802 self.mode = Mode::InsightsInput;
2803 } else if self.view_mode == ViewMode::List {
2804 self.mode = Mode::FilterInput;
2805 self.log_groups_state.filter_mode = true;
2806 self.log_groups_state.input_focus = InputFocus::Filter;
2807 } else if self.view_mode == ViewMode::Events {
2808 self.mode = Mode::EventFilterInput;
2809 } else if self.view_mode == ViewMode::Detail
2810 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2811 {
2812 self.mode = Mode::FilterInput;
2813 self.log_groups_state.filter_mode = true;
2814 self.log_groups_state.input_focus = InputFocus::Filter;
2815 }
2816 }
2817 Action::NextFilterFocus => {
2818 if self.current_service == Service::CloudTrailEvents
2819 && self.cloudtrail_state.current_event.is_some()
2820 {
2821 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
2822 } else if self.mode == Mode::FilterInput
2823 && self.current_service == Service::S3Buckets
2824 {
2825 crate::s3::actions::next_filter_focus(self);
2826 } else if self.mode == Mode::FilterInput
2827 && self.current_service == Service::Ec2Instances
2828 {
2829 crate::ec2::actions::next_filter_focus(self);
2830 } else if self.mode == Mode::FilterInput
2831 && self.current_service == Service::LambdaApplications
2832 {
2833 crate::lambda::applications::next_filter_focus(self);
2834 } else if self.mode == Mode::FilterInput
2835 && self.current_service == Service::IamRoles
2836 && self.iam_state.current_role.is_some()
2837 {
2838 use crate::ui::iam::POLICY_FILTER_CONTROLS;
2839 self.iam_state.policy_input_focus = self
2840 .iam_state
2841 .policy_input_focus
2842 .next(&POLICY_FILTER_CONTROLS);
2843 } else if self.mode == Mode::FilterInput
2844 && self.current_service == Service::IamRoles
2845 && self.iam_state.current_role.is_none()
2846 {
2847 use crate::ui::iam::ROLE_FILTER_CONTROLS;
2848 self.iam_state.role_input_focus =
2849 self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
2850 } else if self.mode == Mode::FilterInput
2851 && self.current_service == Service::IamUsers
2852 && self.iam_state.current_user.is_some()
2853 {
2854 use crate::ui::iam::{
2855 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
2856 USER_SIMPLE_FILTER_CONTROLS,
2857 };
2858 if self.iam_state.user_tab == UserTab::Permissions {
2859 self.iam_state.policy_input_focus = self
2860 .iam_state
2861 .policy_input_focus
2862 .next(&POLICY_FILTER_CONTROLS);
2863 } else if self.iam_state.user_tab == UserTab::LastAccessed {
2864 self.iam_state.last_accessed_input_focus = self
2865 .iam_state
2866 .last_accessed_input_focus
2867 .next(&USER_LAST_ACCESSED_FILTER_CONTROLS);
2868 } else {
2869 self.iam_state.user_input_focus = self
2870 .iam_state
2871 .user_input_focus
2872 .next(&USER_SIMPLE_FILTER_CONTROLS);
2873 }
2874 } else if self.mode == Mode::FilterInput
2875 && self.current_service == Service::IamUserGroups
2876 {
2877 use crate::ui::iam::GROUP_FILTER_CONTROLS;
2878 self.iam_state.group_input_focus = self
2879 .iam_state
2880 .group_input_focus
2881 .next(&GROUP_FILTER_CONTROLS);
2882 } else if self.mode == Mode::InsightsInput {
2883 use crate::app::InsightsFocus;
2884 self.insights_state.insights.insights_focus =
2885 match self.insights_state.insights.insights_focus {
2886 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
2887 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
2888 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
2889 InsightsFocus::Query => InsightsFocus::QueryLanguage,
2890 };
2891 } else if self.mode == Mode::FilterInput
2892 && self.current_service == Service::CloudFormationStacks
2893 {
2894 crate::cfn::actions::next_filter_focus(self);
2895 } else if self.mode == Mode::FilterInput
2896 && self.current_service == Service::SqsQueues
2897 {
2898 if self.sqs_state.current_queue.is_some()
2899 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2900 {
2901 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2902 self.sqs_state.input_focus = self
2903 .sqs_state
2904 .input_focus
2905 .next(SUBSCRIPTION_FILTER_CONTROLS);
2906 } else {
2907 use crate::ui::sqs::FILTER_CONTROLS;
2908 self.sqs_state.input_focus =
2909 self.sqs_state.input_focus.next(FILTER_CONTROLS);
2910 }
2911 } else if self.mode == Mode::FilterInput
2912 && self.current_service == Service::CloudWatchLogGroups
2913 {
2914 use crate::ui::cw::logs::FILTER_CONTROLS;
2915 self.log_groups_state.input_focus =
2916 self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
2917 } else if self.mode == Mode::EventFilterInput {
2918 self.log_groups_state.event_input_focus =
2919 self.log_groups_state.event_input_focus.next();
2920 } else if self.mode == Mode::FilterInput
2921 && self.current_service == Service::CloudWatchAlarms
2922 {
2923 use crate::ui::cw::alarms::FILTER_CONTROLS;
2924 self.alarms_state.input_focus =
2925 self.alarms_state.input_focus.next(&FILTER_CONTROLS);
2926 } else if self.mode == Mode::FilterInput
2927 && self.current_service == Service::CloudTrailEvents
2928 {
2929 const FILTER_CONTROLS: [InputFocus; 2] =
2930 [InputFocus::Filter, InputFocus::Pagination];
2931 self.cloudtrail_state.input_focus =
2932 self.cloudtrail_state.input_focus.next(&FILTER_CONTROLS);
2933 } else if self.mode == Mode::FilterInput
2934 && self.current_service == Service::ApiGatewayApis
2935 {
2936 use crate::ui::apig::FILTER_CONTROLS;
2937 self.apig_state.input_focus =
2938 self.apig_state.input_focus.next(&FILTER_CONTROLS);
2939 } else if self.mode == Mode::FilterInput
2940 && self.current_service == Service::EcrRepositories
2941 && self.ecr_state.current_repository.is_none()
2942 {
2943 crate::ecr::actions::next_filter_focus(self);
2944 } else if self.mode == Mode::FilterInput
2945 && self.current_service == Service::LambdaFunctions
2946 {
2947 crate::lambda::functions::next_filter_focus(self);
2948 }
2949 }
2950 Action::PrevFilterFocus => {
2951 if self.current_service == Service::CloudTrailEvents
2952 && self.cloudtrail_state.current_event.is_some()
2953 {
2954 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
2955 } else if self.mode == Mode::FilterInput
2956 && self.current_service == Service::ApiGatewayApis
2957 {
2958 use crate::ui::apig::FILTER_CONTROLS;
2959 self.apig_state.input_focus =
2960 self.apig_state.input_focus.prev(&FILTER_CONTROLS);
2961 } else if self.mode == Mode::FilterInput
2962 && self.current_service == Service::S3Buckets
2963 {
2964 crate::s3::actions::prev_filter_focus(self);
2965 } else if self.mode == Mode::FilterInput
2966 && self.current_service == Service::Ec2Instances
2967 {
2968 crate::ec2::actions::prev_filter_focus(self);
2969 } else if self.mode == Mode::FilterInput
2970 && self.current_service == Service::LambdaApplications
2971 {
2972 crate::lambda::applications::prev_filter_focus(self);
2973 } else if self.mode == Mode::FilterInput
2974 && self.current_service == Service::CloudFormationStacks
2975 {
2976 crate::cfn::actions::prev_filter_focus(self);
2977 } else if self.mode == Mode::FilterInput
2978 && self.current_service == Service::SqsQueues
2979 {
2980 if self.sqs_state.current_queue.is_some()
2981 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2982 {
2983 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2984 self.sqs_state.input_focus = self
2985 .sqs_state
2986 .input_focus
2987 .prev(SUBSCRIPTION_FILTER_CONTROLS);
2988 } else {
2989 use crate::ui::sqs::FILTER_CONTROLS;
2990 self.sqs_state.input_focus =
2991 self.sqs_state.input_focus.prev(FILTER_CONTROLS);
2992 }
2993 } else if self.mode == Mode::FilterInput
2994 && self.current_service == Service::IamRoles
2995 && self.iam_state.current_role.is_none()
2996 {
2997 use crate::ui::iam::ROLE_FILTER_CONTROLS;
2998 self.iam_state.role_input_focus =
2999 self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
3000 } else if self.mode == Mode::FilterInput
3001 && self.current_service == Service::IamUsers
3002 && self.iam_state.current_user.is_some()
3003 {
3004 use crate::ui::iam::{
3005 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3006 USER_SIMPLE_FILTER_CONTROLS,
3007 };
3008 if self.iam_state.user_tab == UserTab::Permissions {
3009 self.iam_state.policy_input_focus = self
3010 .iam_state
3011 .policy_input_focus
3012 .prev(&POLICY_FILTER_CONTROLS);
3013 } else if self.iam_state.user_tab == UserTab::LastAccessed {
3014 self.iam_state.last_accessed_input_focus = self
3015 .iam_state
3016 .last_accessed_input_focus
3017 .prev(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3018 } else {
3019 self.iam_state.user_input_focus = self
3020 .iam_state
3021 .user_input_focus
3022 .prev(&USER_SIMPLE_FILTER_CONTROLS);
3023 }
3024 } else if self.mode == Mode::FilterInput
3025 && self.current_service == Service::IamUserGroups
3026 {
3027 use crate::ui::iam::GROUP_FILTER_CONTROLS;
3028 self.iam_state.group_input_focus = self
3029 .iam_state
3030 .group_input_focus
3031 .prev(&GROUP_FILTER_CONTROLS);
3032 } else if self.mode == Mode::FilterInput
3033 && self.current_service == Service::CloudWatchLogGroups
3034 {
3035 use crate::ui::cw::logs::FILTER_CONTROLS;
3036 self.log_groups_state.input_focus =
3037 self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
3038 } else if self.mode == Mode::EventFilterInput {
3039 self.log_groups_state.event_input_focus =
3040 self.log_groups_state.event_input_focus.prev();
3041 } else if self.mode == Mode::FilterInput
3042 && self.current_service == Service::IamRoles
3043 && self.iam_state.current_role.is_some()
3044 {
3045 use crate::ui::iam::POLICY_FILTER_CONTROLS;
3046 self.iam_state.policy_input_focus = self
3047 .iam_state
3048 .policy_input_focus
3049 .prev(&POLICY_FILTER_CONTROLS);
3050 } else if self.mode == Mode::FilterInput
3051 && self.current_service == Service::CloudWatchAlarms
3052 {
3053 use crate::ui::cw::alarms::FILTER_CONTROLS;
3054 self.alarms_state.input_focus =
3055 self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
3056 } else if self.mode == Mode::FilterInput
3057 && self.current_service == Service::CloudTrailEvents
3058 {
3059 const FILTER_CONTROLS: [InputFocus; 2] =
3060 [InputFocus::Filter, InputFocus::Pagination];
3061 self.cloudtrail_state.input_focus =
3062 self.cloudtrail_state.input_focus.prev(&FILTER_CONTROLS);
3063 } else if self.mode == Mode::FilterInput
3064 && self.current_service == Service::EcrRepositories
3065 && self.ecr_state.current_repository.is_none()
3066 {
3067 crate::ecr::actions::prev_filter_focus(self);
3068 } else if self.mode == Mode::FilterInput
3069 && self.current_service == Service::LambdaFunctions
3070 {
3071 crate::lambda::functions::prev_filter_focus(self);
3072 }
3073 }
3074 Action::ToggleFilterCheckbox => {
3075 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
3076 crate::ec2::actions::toggle_filter_checkbox(self);
3077 } else if self.mode == Mode::InsightsInput {
3078 use crate::app::InsightsFocus;
3079 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3080 && self.insights_state.insights.show_dropdown
3081 && !self.insights_state.insights.log_group_matches.is_empty()
3082 {
3083 let selected_idx = self.insights_state.insights.dropdown_selected;
3084 if let Some(group_name) = self
3085 .insights_state
3086 .insights
3087 .log_group_matches
3088 .get(selected_idx)
3089 {
3090 let group_name = group_name.clone();
3091 if let Some(pos) = self
3092 .insights_state
3093 .insights
3094 .selected_log_groups
3095 .iter()
3096 .position(|g| g == &group_name)
3097 {
3098 self.insights_state.insights.selected_log_groups.remove(pos);
3099 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
3100 self.insights_state
3101 .insights
3102 .selected_log_groups
3103 .push(group_name);
3104 }
3105 }
3106 }
3107 } else if self.mode == Mode::FilterInput
3108 && self.current_service == Service::CloudFormationStacks
3109 {
3110 crate::cfn::actions::toggle_filter_checkbox(self);
3111 } else if self.mode == Mode::FilterInput
3112 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3113 {
3114 match self.log_groups_state.input_focus {
3115 InputFocus::Checkbox("ExactMatch") => {
3116 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
3117 }
3118 InputFocus::Checkbox("ShowExpired") => {
3119 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
3120 }
3121 _ => {}
3122 }
3123 } else if self.mode == Mode::EventFilterInput
3124 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
3125 {
3126 self.log_groups_state.relative_unit =
3127 self.log_groups_state.relative_unit.next();
3128 }
3129 }
3130 Action::CycleSortColumn => {
3131 if self.view_mode == ViewMode::Detail
3132 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3133 {
3134 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
3135 StreamSort::Name => StreamSort::CreationTime,
3136 StreamSort::CreationTime => StreamSort::LastEventTime,
3137 StreamSort::LastEventTime => StreamSort::Name,
3138 };
3139 }
3140 }
3141 Action::ToggleSortDirection => {
3142 if self.view_mode == ViewMode::Detail
3143 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3144 {
3145 self.log_groups_state.stream_sort_desc =
3146 !self.log_groups_state.stream_sort_desc;
3147 }
3148 }
3149 Action::ScrollUp => {
3150 if self.mode == Mode::ErrorModal {
3151 self.error_scroll = self.error_scroll.saturating_sub(1);
3152 } else if self.current_service == Service::CloudTrailEvents
3153 && self.cloudtrail_state.current_event.is_some()
3154 {
3155 self.cloudtrail_state.event_json_scroll =
3156 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
3157 } else if self.current_service == Service::LambdaFunctions
3158 && self.lambda_state.current_function.is_some()
3159 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3160 && !self.lambda_state.is_metrics_loading()
3161 {
3162 self.lambda_state.set_monitoring_scroll(
3163 self.lambda_state.monitoring_scroll().saturating_sub(1),
3164 );
3165 } else if self.current_service == Service::Ec2Instances {
3166 crate::ec2::actions::scroll_up(self);
3167 } else if self.current_service == Service::SqsQueues
3168 && self.sqs_state.current_queue.is_some()
3169 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3170 && !self.sqs_state.is_metrics_loading()
3171 {
3172 self.sqs_state.set_monitoring_scroll(
3173 self.sqs_state.monitoring_scroll().saturating_sub(1),
3174 );
3175 } else if self.view_mode == ViewMode::PolicyView {
3176 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
3177 } else if self.current_service == Service::IamRoles
3178 && self.iam_state.current_role.is_some()
3179 && self.iam_state.role_tab == RoleTab::TrustRelationships
3180 {
3181 self.iam_state.trust_policy_scroll =
3182 self.iam_state.trust_policy_scroll.saturating_sub(10);
3183 } else if self.view_mode == ViewMode::Events {
3184 if self.log_groups_state.event_scroll_offset == 0
3185 && self.log_groups_state.has_older_events
3186 {
3187 self.log_groups_state.loading = true;
3188 } else {
3189 self.log_groups_state.event_scroll_offset =
3190 self.log_groups_state.event_scroll_offset.saturating_sub(1);
3191 }
3192 } else if self.view_mode == ViewMode::InsightsResults {
3193 self.insights_state.insights.results_selected = self
3194 .insights_state
3195 .insights
3196 .results_selected
3197 .saturating_sub(1);
3198 } else if self.view_mode == ViewMode::Detail {
3199 self.log_groups_state.selected_stream =
3200 self.log_groups_state.selected_stream.saturating_sub(1);
3201 self.log_groups_state.expanded_stream = None;
3202 } else if self.view_mode == ViewMode::List
3203 && self.current_service == Service::CloudWatchLogGroups
3204 {
3205 self.log_groups_state.log_groups.selected =
3206 self.log_groups_state.log_groups.selected.saturating_sub(1);
3207 self.log_groups_state.log_groups.snap_to_page();
3208 } else if self.current_service == Service::EcrRepositories {
3209 crate::ecr::actions::scroll_up(self);
3210 }
3211 }
3212 Action::ScrollDown => {
3213 if self.mode == Mode::ErrorModal {
3214 if let Some(error_msg) = &self.error_message {
3215 let lines = error_msg.lines().count();
3216 let max_scroll = lines.saturating_sub(1);
3217 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
3218 }
3219 } else if self.current_service == Service::CloudTrailEvents
3220 && self.cloudtrail_state.current_event.is_some()
3221 {
3222 if let Some(event) = &self.cloudtrail_state.current_event {
3223 let lines = event.cloud_trail_event_json.lines().count();
3224 let max_scroll = lines.saturating_sub(1);
3225 self.cloudtrail_state.event_json_scroll =
3226 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
3227 }
3228 } else if self.current_service == Service::SqsQueues
3229 && self.sqs_state.current_queue.is_some()
3230 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3231 {
3232 self.sqs_state
3233 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
3234 } else if self.view_mode == ViewMode::PolicyView {
3235 let lines = self.iam_state.policy_document.lines().count();
3236 let max_scroll = lines.saturating_sub(1);
3237 self.iam_state.policy_scroll =
3238 (self.iam_state.policy_scroll + 10).min(max_scroll);
3239 } else if self.current_service == Service::IamRoles
3240 && self.iam_state.current_role.is_some()
3241 && self.iam_state.role_tab == RoleTab::TrustRelationships
3242 {
3243 let lines = self.iam_state.trust_policy_document.lines().count();
3244 let max_scroll = lines.saturating_sub(1);
3245 self.iam_state.trust_policy_scroll =
3246 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
3247 } else if self.view_mode == ViewMode::Events {
3248 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3249 if self.log_groups_state.event_scroll_offset >= max_scroll {
3250 } else {
3252 self.log_groups_state.event_scroll_offset =
3253 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3254 }
3255 } else if self.view_mode == ViewMode::InsightsResults {
3256 let max = self
3257 .insights_state
3258 .insights
3259 .query_results
3260 .len()
3261 .saturating_sub(1);
3262 self.insights_state.insights.results_selected =
3263 (self.insights_state.insights.results_selected + 1).min(max);
3264 } else if self.view_mode == ViewMode::Detail {
3265 let filtered_streams = filtered_log_streams(self);
3266 let max = filtered_streams.len().saturating_sub(1);
3267 self.log_groups_state.selected_stream =
3268 (self.log_groups_state.selected_stream + 1).min(max);
3269 } else if self.view_mode == ViewMode::List
3270 && self.current_service == Service::CloudWatchLogGroups
3271 {
3272 let filtered_groups = filtered_log_groups(self);
3273 self.log_groups_state
3274 .log_groups
3275 .next_item(filtered_groups.len());
3276 } else if self.current_service == Service::EcrRepositories {
3277 crate::ecr::actions::scroll_down(self);
3278 }
3279 }
3280
3281 Action::MeasureLatency => {
3282 if self.mode == Mode::RegionPicker {
3283 self.measure_region_latencies();
3284 }
3285 }
3286 Action::Refresh => {
3287 if self.mode == Mode::ProfilePicker {
3288 self.log_groups_state.loading = true;
3289 self.log_groups_state.loading_message = "Refreshing...".to_string();
3290 } else if self.mode == Mode::RegionPicker {
3291 } else if self.mode == Mode::SessionPicker {
3293 self.sessions = Session::list_all().unwrap_or_default();
3294 } else if self.current_service == Service::CloudWatchAlarms
3295 && self.alarms_state.current_alarm.is_some()
3296 {
3297 self.alarms_state.metrics_loading = true;
3299 } else if self.current_service == Service::CloudWatchInsights
3300 && !self.insights_state.insights.selected_log_groups.is_empty()
3301 {
3302 self.log_groups_state.loading = true;
3303 self.insights_state.insights.query_completed = true;
3304 } else if self.current_service == Service::LambdaFunctions {
3305 crate::lambda::functions::refresh(self);
3306 } else if self.current_service == Service::LambdaApplications {
3307 crate::lambda::applications::refresh(self);
3308 } else if matches!(
3309 self.view_mode,
3310 ViewMode::Events | ViewMode::Detail | ViewMode::List
3311 ) {
3312 self.log_groups_state.loading = true;
3313 }
3314 }
3315 Action::Yank => {
3316 if self.mode == Mode::ErrorModal {
3317 if let Some(error) = &self.error_message {
3319 copy_to_clipboard(error);
3320 }
3321 } else if self.view_mode == ViewMode::Events {
3322 if let Some(event) = self
3323 .log_groups_state
3324 .log_events
3325 .get(self.log_groups_state.event_scroll_offset)
3326 {
3327 copy_to_clipboard(&event.message);
3328 }
3329 } else if self.current_service == Service::EcrRepositories {
3330 crate::ecr::actions::yank(self);
3331 } else if self.current_service == Service::LambdaFunctions {
3332 crate::lambda::functions::yank(self);
3333 } else if self.current_service == Service::CloudFormationStacks {
3334 crate::cfn::actions::yank(self);
3335 } else if self.current_service == Service::IamUsers {
3336 crate::iam::actions::yank_users(self);
3337 } else if self.current_service == Service::IamRoles {
3338 crate::iam::actions::yank_roles(self);
3339 } else if self.current_service == Service::IamUserGroups {
3340 if self.iam_state.current_group.is_some() {
3341 if let Some(group_name) = &self.iam_state.current_group {
3342 let arn = iam::format_arn(&self.config.account_id, "group", group_name);
3343 copy_to_clipboard(&arn);
3344 }
3345 } else {
3346 let filtered_groups: Vec<_> = self
3347 .iam_state
3348 .groups
3349 .items
3350 .iter()
3351 .filter(|g| {
3352 if self.iam_state.groups.filter.is_empty() {
3353 true
3354 } else {
3355 g.group_name
3356 .to_lowercase()
3357 .contains(&self.iam_state.groups.filter.to_lowercase())
3358 }
3359 })
3360 .collect();
3361 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
3362 let arn = iam::format_arn(
3363 &self.config.account_id,
3364 "group",
3365 &group.group_name,
3366 );
3367 copy_to_clipboard(&arn);
3368 }
3369 }
3370 } else if self.current_service == Service::SqsQueues {
3371 if self.sqs_state.current_queue.is_some() {
3372 if let Some(queue) = self
3374 .sqs_state
3375 .queues
3376 .items
3377 .iter()
3378 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
3379 {
3380 let arn = format!(
3381 "arn:aws:sqs:{}:{}:{}",
3382 extract_region(&queue.url),
3383 extract_account_id(&queue.url),
3384 queue.name
3385 );
3386 copy_to_clipboard(&arn);
3387 }
3388 } else {
3389 let filtered_queues = filtered_queues(
3391 &self.sqs_state.queues.items,
3392 &self.sqs_state.queues.filter,
3393 );
3394 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
3395 let arn = format!(
3396 "arn:aws:sqs:{}:{}:{}",
3397 extract_region(&queue.url),
3398 extract_account_id(&queue.url),
3399 queue.name
3400 );
3401 copy_to_clipboard(&arn);
3402 }
3403 }
3404 } else if self.current_service == Service::ApiGatewayApis {
3405 if let Some(_api) = &self.apig_state.current_api {
3406 use crate::ui::apig::ApiDetailTab;
3407 if self.apig_state.detail_tab == ApiDetailTab::Routes {
3408 let (filtered_routes, _) = crate::ui::apig::filter_tree_items(
3410 &self.apig_state.routes.items,
3411 &self.apig_state.route_children,
3412 &self.apig_state.route_filter,
3413 );
3414 let filtered_refs: Vec<&Route> = filtered_routes.iter().collect();
3415 if let Some(route) = self.apig_state.routes.get_selected(&filtered_refs)
3416 {
3417 if !route.arn.is_empty() {
3418 copy_to_clipboard(&route.arn);
3419 }
3420 }
3421 }
3422 }
3423 }
3424 }
3425 Action::CopyToClipboard => {
3426 self.snapshot_requested = true;
3428 }
3429 Action::RetryLoad => {
3430 self.error_message = None;
3431 self.mode = Mode::Normal;
3432 self.log_groups_state.loading = true;
3433 }
3434 Action::ApplyFilter => {
3435 if self.mode == Mode::FilterInput
3436 && self.current_service == Service::SqsQueues
3437 && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
3438 {
3439 let regions = AwsRegion::all();
3440 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
3441 self.sqs_state.subscription_region_filter = region.code.to_string();
3442 }
3443 self.mode = Mode::Normal;
3444 } else if self.mode == Mode::InsightsInput {
3445 use crate::app::InsightsFocus;
3446 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3447 && self.insights_state.insights.show_dropdown
3448 {
3449 self.insights_state.insights.show_dropdown = false;
3451 self.mode = Mode::Normal;
3452 if !self.insights_state.insights.selected_log_groups.is_empty() {
3453 self.log_groups_state.loading = true;
3454 self.insights_state.insights.query_completed = true;
3455 }
3456 }
3457 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
3458 if let Ok(page) = self.page_input.parse::<usize>() {
3459 self.go_to_page(page);
3460 }
3461 self.page_input.clear();
3462 } else {
3463 self.mode = Mode::Normal;
3464 self.log_groups_state.filter_mode = false;
3465 }
3466 }
3467 Action::ToggleExactMatch => {
3468 if self.view_mode == ViewMode::Detail
3469 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3470 {
3471 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
3472 }
3473 }
3474 Action::ToggleShowExpired => {
3475 if self.view_mode == ViewMode::Detail
3476 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3477 {
3478 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
3479 }
3480 }
3481 Action::GoBack => {
3482 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
3484 self.mode = Mode::Normal;
3485 self.service_picker.filter.clear();
3486 }
3487 else if self.current_service == Service::ApiGatewayApis
3489 && self.apig_state.current_api.is_some()
3490 {
3491 self.apig_state.current_api = None;
3492 self.apig_state.routes.items.clear();
3493 self.apig_state.detail_tab = crate::ui::apig::ApiDetailTab::Routes;
3494 self.update_current_tab_breadcrumb();
3495 }
3496 else if self.current_service == Service::S3Buckets
3498 && self.s3_state.current_bucket.is_some()
3499 {
3500 crate::s3::actions::go_back(self);
3501 }
3502 else if self.current_service == Service::EcrRepositories
3504 && self.ecr_state.current_repository.is_some()
3505 {
3506 crate::ecr::actions::go_back(self);
3507 }
3508 else if self.current_service == Service::Ec2Instances
3510 && self.ec2_state.current_instance.is_some()
3511 {
3512 crate::ec2::actions::go_back(self);
3513 }
3514 else if self.current_service == Service::SqsQueues
3516 && self.sqs_state.current_queue.is_some()
3517 {
3518 self.sqs_state.current_queue = None;
3519 }
3520 else if self.current_service == Service::IamUsers
3522 && self.iam_state.current_user.is_some()
3523 {
3524 crate::iam::actions::go_back_users(self);
3525 } else if self.current_service == Service::IamUserGroups
3526 && self.iam_state.current_group.is_some()
3527 {
3528 crate::iam::actions::go_back_groups(self);
3529 } else if self.current_service == Service::IamRoles {
3530 crate::iam::actions::go_back_roles(self);
3531 }
3532 else if self.current_service == Service::LambdaFunctions
3534 && (self.lambda_state.current_version.is_some()
3535 || self.lambda_state.current_alias.is_some()
3536 || self.lambda_state.current_function.is_some())
3537 {
3538 crate::lambda::functions::go_back(self);
3539 }
3540 else if self.current_service == Service::CloudWatchAlarms
3542 && self.alarms_state.current_alarm.is_some()
3543 {
3544 self.alarms_state.current_alarm = None;
3545 self.alarms_state.metric_data.clear();
3546 self.view_mode = ViewMode::List;
3547 self.update_current_tab_breadcrumb();
3548 }
3549 else if self.current_service == Service::LambdaApplications
3551 && self.lambda_application_state.current_application.is_some()
3552 {
3553 crate::lambda::applications::go_back(self);
3554 }
3555 else if self.current_service == Service::CloudFormationStacks
3557 && self.cfn_state.current_stack.is_some()
3558 {
3559 crate::cfn::actions::go_back(self);
3560 }
3561 else if self.current_service == Service::CloudTrailEvents
3563 && self.cloudtrail_state.current_event.is_some()
3564 {
3565 self.cloudtrail_state.current_event = None;
3566 self.update_current_tab_breadcrumb();
3567 }
3568 else if self.view_mode == ViewMode::InsightsResults {
3570 if self.insights_state.insights.expanded_result.is_some() {
3571 self.insights_state.insights.expanded_result = None;
3572 }
3573 }
3574 else if self.current_service == Service::CloudWatchAlarms {
3576 if self.alarms_state.table.has_expanded_item() {
3577 self.alarms_state.table.collapse();
3578 }
3579 }
3580 else if self.current_service == Service::Ec2Instances {
3582 crate::ec2::actions::collapse_row(self);
3583 }
3584 else if self.view_mode == ViewMode::Events {
3586 if self.log_groups_state.expanded_event.is_some() {
3587 self.log_groups_state.expanded_event = None;
3588 } else {
3589 self.view_mode = ViewMode::Detail;
3590 self.log_groups_state.event_filter.clear();
3591 }
3592 }
3593 else if self.view_mode == ViewMode::Detail {
3595 self.view_mode = ViewMode::List;
3596 self.log_groups_state.stream_filter.clear();
3597 self.log_groups_state.exact_match = false;
3598 self.log_groups_state.show_expired = false;
3599 }
3600 }
3601 Action::OpenInConsole | Action::OpenInBrowser => {
3602 let url = self.get_console_url();
3603 let _ = webbrowser::open(&url);
3604 }
3605 Action::ShowHelp => {
3606 self.mode = Mode::HelpModal;
3607 }
3608 Action::OpenRegionPicker => {
3609 self.region_filter.clear();
3610 self.region_picker_selected = 0;
3611 self.mode = Mode::RegionPicker;
3612 }
3613 Action::OpenProfilePicker => {
3614 self.profile_filter.clear();
3615 self.profile_picker_selected = 0;
3616 self.available_profiles = Self::load_aws_profiles();
3617 self.mode = Mode::ProfilePicker;
3618 }
3619 Action::OpenCalendar => {
3620 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
3621 self.calendar_selecting = CalendarField::StartDate;
3622 self.mode = Mode::CalendarPicker;
3623 }
3624 Action::CloseCalendar => {
3625 self.mode = Mode::Normal;
3626 self.calendar_date = None;
3627 }
3628 Action::CalendarPrevDay => {
3629 if let Some(date) = self.calendar_date {
3630 self.calendar_date = date.checked_sub(time::Duration::days(1));
3631 }
3632 }
3633 Action::CalendarNextDay => {
3634 if let Some(date) = self.calendar_date {
3635 self.calendar_date = date.checked_add(time::Duration::days(1));
3636 }
3637 }
3638 Action::CalendarPrevWeek => {
3639 if let Some(date) = self.calendar_date {
3640 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
3641 }
3642 }
3643 Action::CalendarNextWeek => {
3644 if let Some(date) = self.calendar_date {
3645 self.calendar_date = date.checked_add(time::Duration::weeks(1));
3646 }
3647 }
3648 Action::CalendarPrevMonth => {
3649 if let Some(date) = self.calendar_date {
3650 self.calendar_date = Some(if date.month() == time::Month::January {
3651 date.replace_month(time::Month::December)
3652 .unwrap()
3653 .replace_year(date.year() - 1)
3654 .unwrap()
3655 } else {
3656 date.replace_month(date.month().previous()).unwrap()
3657 });
3658 }
3659 }
3660 Action::CalendarNextMonth => {
3661 if let Some(date) = self.calendar_date {
3662 self.calendar_date = Some(if date.month() == time::Month::December {
3663 date.replace_month(time::Month::January)
3664 .unwrap()
3665 .replace_year(date.year() + 1)
3666 .unwrap()
3667 } else {
3668 date.replace_month(date.month().next()).unwrap()
3669 });
3670 }
3671 }
3672 Action::CalendarSelect => {
3673 if let Some(date) = self.calendar_date {
3674 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
3675 .unix_timestamp()
3676 * 1000;
3677 match self.calendar_selecting {
3678 CalendarField::StartDate => {
3679 self.log_groups_state.start_time = Some(timestamp);
3680 self.calendar_selecting = CalendarField::EndDate;
3681 }
3682 CalendarField::EndDate => {
3683 self.log_groups_state.end_time = Some(timestamp);
3684 self.mode = Mode::Normal;
3685 self.calendar_date = None;
3686 }
3687 }
3688 }
3689 }
3690 }
3691 }
3692
3693 pub fn filtered_services(&self) -> Vec<&'static str> {
3694 let mut services = if self.service_picker.filter.is_empty() {
3695 self.service_picker.services.clone()
3696 } else {
3697 self.service_picker
3698 .services
3699 .iter()
3700 .filter(|s| {
3701 s.to_lowercase()
3702 .contains(&self.service_picker.filter.to_lowercase())
3703 })
3704 .copied()
3705 .collect()
3706 };
3707 services.sort();
3708 services
3709 }
3710
3711 pub fn breadcrumbs(&self) -> String {
3712 if !self.service_selected {
3713 return String::new();
3714 }
3715
3716 let mut parts = vec![];
3717
3718 match self.current_service {
3719 Service::CloudWatchLogGroups => {
3720 parts.push("CloudWatch".to_string());
3721 parts.push("Log groups".to_string());
3722
3723 if self.view_mode != ViewMode::List {
3724 if let Some(group) = selected_log_group(self) {
3725 parts.push(group.name.clone());
3726 }
3727 }
3728
3729 if self.view_mode == ViewMode::Events {
3730 if let Some(stream) = self
3731 .log_groups_state
3732 .log_streams
3733 .get(self.log_groups_state.selected_stream)
3734 {
3735 parts.push(stream.name.clone());
3736 }
3737 }
3738 }
3739 Service::CloudWatchInsights => {
3740 parts.push("CloudWatch".to_string());
3741 parts.push("Insights".to_string());
3742 }
3743 Service::CloudWatchAlarms => {
3744 parts.push("CloudWatch".to_string());
3745 parts.push("Alarms".to_string());
3746 }
3747 Service::CloudTrailEvents => {
3748 parts.push("CloudTrail".to_string());
3749 parts.push("Event History".to_string());
3750 }
3751 Service::S3Buckets => {
3752 parts.extend(crate::s3::actions::breadcrumb(self));
3753 }
3754 Service::SqsQueues => {
3755 parts.push("SQS".to_string());
3756 parts.push("Queues".to_string());
3757 }
3758 Service::EcrRepositories => {
3759 parts.extend(crate::ecr::actions::breadcrumb(self));
3760 }
3761 Service::LambdaFunctions => {
3762 parts.extend(crate::lambda::functions::breadcrumb(self));
3763 }
3764 Service::LambdaApplications => {
3765 parts.extend(crate::lambda::applications::breadcrumb());
3766 }
3767 Service::CloudFormationStacks => {
3768 parts.extend(crate::cfn::actions::breadcrumb(self));
3769 }
3770 Service::IamUsers => {
3771 parts.extend(crate::iam::actions::breadcrumb_users());
3772 }
3773 Service::IamRoles => {
3774 parts.extend(crate::iam::actions::breadcrumb_roles(self));
3775 }
3776 Service::IamUserGroups => {
3777 parts.extend(crate::iam::actions::breadcrumb_groups(self));
3778 }
3779 Service::Ec2Instances => {
3780 parts.extend(crate::ec2::actions::breadcrumb(self));
3781 }
3782 Service::ApiGatewayApis => {
3783 parts.push("API Gateway".to_string());
3784 if let Some(api) = &self.apig_state.current_api {
3785 parts.push(api.name.clone());
3786 } else {
3787 parts.push("APIs".to_string());
3788 }
3789 }
3790 }
3791
3792 parts.join(" > ")
3793 }
3794
3795 pub fn update_current_tab_breadcrumb(&mut self) {
3796 if !self.tabs.is_empty() {
3797 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
3798 }
3799 }
3800
3801 pub fn get_console_url(&self) -> String {
3802 use crate::cw;
3803
3804 match self.current_service {
3805 Service::CloudWatchLogGroups => {
3806 if self.view_mode == ViewMode::Events {
3807 if let Some(group) = selected_log_group(self) {
3808 if let Some(stream) = self
3809 .log_groups_state
3810 .log_streams
3811 .get(self.log_groups_state.selected_stream)
3812 {
3813 return cw::logs::console_url_stream(
3814 &self.config.region,
3815 &group.name,
3816 &stream.name,
3817 );
3818 }
3819 }
3820 } else if self.view_mode == ViewMode::Detail {
3821 if let Some(group) = selected_log_group(self) {
3822 return cw::logs::console_url_detail(&self.config.region, &group.name);
3823 }
3824 }
3825 cw::logs::console_url_list(&self.config.region)
3826 }
3827 Service::CloudWatchInsights => cw::insights::console_url(
3828 &self.config.region,
3829 &self.config.account_id,
3830 &self.insights_state.insights.query_text,
3831 &self.insights_state.insights.selected_log_groups,
3832 ),
3833 Service::CloudWatchAlarms => {
3834 let view_type = match self.alarms_state.view_as {
3835 AlarmViewMode::Table | AlarmViewMode::Detail => "table",
3836 AlarmViewMode::Cards => "card",
3837 };
3838 cw::alarms::console_url(
3839 &self.config.region,
3840 view_type,
3841 self.alarms_state.table.page_size.value(),
3842 &self.alarms_state.sort_column,
3843 self.alarms_state.sort_direction.as_str(),
3844 )
3845 }
3846 Service::CloudTrailEvents => {
3847 if let Some(event) = &self.cloudtrail_state.current_event {
3848 format!(
3849 "https://{}.console.aws.amazon.com/cloudtrailv2/home?region={}#/events/{}",
3850 self.config.region, self.config.region, event.event_id
3851 )
3852 } else {
3853 format!(
3854 "https://{}.console.aws.amazon.com/cloudtrail/home?region={}#/events",
3855 self.config.region, self.config.region
3856 )
3857 }
3858 }
3859 Service::S3Buckets => crate::s3::actions::console_url(self),
3860 Service::SqsQueues => {
3861 if let Some(queue_url) = &self.sqs_state.current_queue {
3862 console_url_queue_detail(&self.config.region, queue_url)
3863 } else {
3864 console_url_queues(&self.config.region)
3865 }
3866 }
3867 Service::EcrRepositories => crate::ecr::actions::console_url(self),
3868 Service::LambdaFunctions => crate::lambda::functions::console_url(self),
3869 Service::LambdaApplications => crate::lambda::applications::console_url(self),
3870 Service::CloudFormationStacks => crate::cfn::actions::console_url(self),
3871 Service::IamUsers => crate::iam::actions::console_url_users(self),
3872 Service::IamRoles => crate::iam::actions::console_url_roles(self),
3873 Service::IamUserGroups => crate::iam::actions::console_url_groups(self),
3874 Service::Ec2Instances => crate::ec2::actions::console_url(self),
3875 Service::ApiGatewayApis => {
3876 use crate::apig;
3877 if let Some(api) = &self.apig_state.current_api {
3878 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
3879 let protocol = api.protocol_type.to_uppercase();
3880 if protocol == "REST" {
3881 let (filtered_items, _) = crate::ui::apig::filter_tree_items(
3883 &self.apig_state.resources.items,
3884 &self.apig_state.resource_children,
3885 &self.apig_state.route_filter,
3886 );
3887
3888 let resource_id =
3889 if self.apig_state.resources.selected < filtered_items.len() {
3890 let mut current_row = 0;
3892 self.find_resource_at_row(
3893 &filtered_items,
3894 self.apig_state.resources.selected,
3895 &mut current_row,
3896 )
3897 } else {
3898 None
3899 };
3900 apig::console_url_resources(
3901 &self.config.region,
3902 &api.id,
3903 resource_id.as_deref(),
3904 )
3905 } else {
3906 let (filtered_items, filtered_children) =
3908 crate::ui::apig::filter_tree_items(
3909 &self.apig_state.routes.items,
3910 &self.apig_state.route_children,
3911 &self.apig_state.route_filter,
3912 );
3913
3914 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
3915 &filtered_items,
3916 &self.apig_state.expanded_routes,
3917 &filtered_children,
3918 );
3919
3920 let route_id = if self.apig_state.routes.selected < total_rows {
3921 let mut current_row = 0;
3923 self.find_route_id_at_row_with_children(
3924 &filtered_items,
3925 &filtered_children,
3926 self.apig_state.routes.selected,
3927 &mut current_row,
3928 )
3929 } else {
3930 None
3931 };
3932 apig::console_url_routes(
3933 &self.config.region,
3934 &api.id,
3935 route_id.as_deref(),
3936 )
3937 }
3938 } else {
3939 apig::console_url_api(&self.config.region, &api.id)
3940 }
3941 } else {
3942 apig::console_url_apis(&self.config.region)
3943 }
3944 }
3945 }
3946 }
3947
3948 pub fn calculate_total_bucket_rows(&self) -> usize {
3949 calculate_total_bucket_rows(self)
3950 }
3951
3952 fn calculate_total_object_rows(&self) -> usize {
3953 calculate_total_object_rows(self)
3954 }
3955
3956 fn get_column_selector_max(&self) -> usize {
3957 if self.current_service == Service::ApiGatewayApis {
3958 self.apig_api_column_ids.len() + 6
3959 } else if self.current_service == Service::S3Buckets
3960 && self.s3_state.current_bucket.is_none()
3961 {
3962 crate::s3::actions::column_selector_max(self)
3963 } else if self.view_mode == ViewMode::Events {
3964 self.cw_log_event_column_ids.len() - 1
3965 } else if self.view_mode == ViewMode::Detail {
3966 self.cw_log_stream_column_ids.len() + 6
3967 } else if self.current_service == Service::CloudWatchAlarms {
3968 29
3969 } else if self.current_service == Service::CloudTrailEvents {
3970 if self.cloudtrail_state.current_event.is_some()
3971 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
3972 {
3973 self.cloudtrail_resource_column_ids.len()
3974 } else {
3975 self.cloudtrail_event_column_ids.len() + 6
3976 }
3977 } else if self.current_service == Service::Ec2Instances {
3978 crate::ec2::actions::column_selector_max(self)
3979 } else if self.current_service == Service::EcrRepositories {
3980 crate::ecr::actions::column_selector_max(self)
3981 } else if self.current_service == Service::SqsQueues {
3982 self.sqs_column_ids.len() - 1
3983 } else if self.current_service == Service::LambdaFunctions {
3984 crate::lambda::functions::column_selector_max(self)
3985 } else if self.current_service == Service::LambdaApplications {
3986 crate::lambda::applications::column_selector_max(self)
3987 } else if self.current_service == Service::CloudFormationStacks {
3988 crate::cfn::actions::column_selector_max(self)
3989 } else if self.current_service == Service::IamUsers {
3990 if self.iam_state.current_user.is_some() {
3991 self.iam_policy_column_ids.len() + 5
3992 } else {
3993 self.iam_user_column_ids.len() + 5
3994 }
3995 } else if self.current_service == Service::IamRoles {
3996 if self.iam_state.current_role.is_some() {
3997 self.iam_policy_column_ids.len() + 5
3998 } else {
3999 self.iam_role_column_ids.len() + 5
4000 }
4001 } else {
4002 self.cw_log_group_column_ids.len() + 6
4003 }
4004 }
4005
4006 fn get_column_count(&self) -> usize {
4007 if self.current_service == Service::ApiGatewayApis {
4008 self.apig_api_column_ids.len()
4009 } else if self.current_service == Service::S3Buckets
4010 && self.s3_state.current_bucket.is_none()
4011 {
4012 crate::s3::actions::column_count(self)
4013 } else if self.view_mode == ViewMode::Events {
4014 self.cw_log_event_column_ids.len()
4015 } else if self.view_mode == ViewMode::Detail {
4016 self.cw_log_stream_column_ids.len()
4017 } else if self.current_service == Service::CloudWatchAlarms {
4018 14
4019 } else if self.current_service == Service::CloudTrailEvents {
4020 if self.cloudtrail_state.current_event.is_some()
4021 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4022 {
4023 self.cloudtrail_resource_column_ids.len()
4024 } else {
4025 self.cloudtrail_event_column_ids.len()
4026 }
4027 } else if self.current_service == Service::Ec2Instances {
4028 crate::ec2::actions::column_count(self)
4029 } else if self.current_service == Service::EcrRepositories {
4030 crate::ecr::actions::column_count(self)
4031 } else if self.current_service == Service::SqsQueues {
4032 self.sqs_column_ids.len()
4033 } else if self.current_service == Service::LambdaFunctions {
4034 crate::lambda::functions::column_count(self)
4035 } else if self.current_service == Service::LambdaApplications {
4036 crate::lambda::applications::column_count(self)
4037 } else if self.current_service == Service::CloudFormationStacks {
4038 crate::cfn::actions::column_count(self)
4039 } else if self.current_service == Service::IamUsers {
4040 if self.iam_state.current_user.is_some() {
4041 self.iam_policy_column_ids.len()
4042 } else {
4043 self.iam_user_column_ids.len()
4044 }
4045 } else if self.current_service == Service::IamRoles {
4046 if self.iam_state.current_role.is_some() {
4047 self.iam_policy_column_ids.len()
4048 } else {
4049 self.iam_role_column_ids.len()
4050 }
4051 } else {
4052 self.cw_log_group_column_ids.len()
4053 }
4054 }
4055
4056 fn is_blank_row_index(&self, idx: usize) -> bool {
4057 let column_count = self.get_column_count();
4058 idx == column_count + 1
4060 }
4061
4062 fn next_item(&mut self) {
4063 match self.mode {
4064 Mode::FilterInput => {
4065 if self.current_service == Service::S3Buckets
4066 && crate::s3::actions::is_pagination_focused(self)
4067 {
4068 crate::s3::actions::page_down_filter_input(self);
4069 } else if self.current_service == Service::CloudTrailEvents
4070 && self.cloudtrail_state.input_focus == InputFocus::Pagination
4071 {
4072 let page_size = self.cloudtrail_state.table.page_size.value();
4074 let total_items = self.cloudtrail_state.table.items.len();
4075 let current_page = self.cloudtrail_state.table.selected / page_size;
4076 let total_pages = total_items.div_ceil(page_size);
4077 if current_page + 1 < total_pages {
4078 self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
4079 }
4080 } else if self.current_service == Service::CloudFormationStacks {
4081 crate::cfn::actions::next_item_filter_input(self);
4082 } else if self.current_service == Service::IamUsers
4083 && self.iam_state.current_user.is_some()
4084 {
4085 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
4086 if self.iam_state.user_tab == UserTab::Permissions
4087 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
4088 {
4089 self.cycle_policy_type_next();
4090 } else if self.iam_state.user_tab == UserTab::LastAccessed
4091 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
4092 {
4093 self.iam_state.last_accessed_history_filter =
4094 self.iam_state.last_accessed_history_filter.next();
4095 self.iam_state.last_accessed_services.reset();
4096 }
4097 } else if self.current_service == Service::IamRoles
4098 && self.iam_state.current_role.is_some()
4099 && self.iam_state.role_tab == RoleTab::Permissions
4100 {
4101 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
4102 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
4103 self.cycle_policy_type_next();
4104 }
4105 } else if self.current_service == Service::Ec2Instances {
4106 crate::ec2::actions::toggle_state_filter_next(self);
4107 } else if self.current_service == Service::SqsQueues {
4108 use crate::ui::sqs::SUBSCRIPTION_REGION;
4109 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
4110 let regions = AwsRegion::all();
4111 self.sqs_state.subscription_region_selected =
4112 (self.sqs_state.subscription_region_selected + 1)
4113 .min(regions.len() - 1);
4114 self.sqs_state.subscriptions.reset();
4115 }
4116 }
4117 }
4118 Mode::RegionPicker => {
4119 let filtered = self.get_filtered_regions();
4120 if !filtered.is_empty() {
4121 self.region_picker_selected =
4122 (self.region_picker_selected + 1).min(filtered.len() - 1);
4123 }
4124 }
4125 Mode::ProfilePicker => {
4126 let filtered = self.get_filtered_profiles();
4127 if !filtered.is_empty() {
4128 self.profile_picker_selected =
4129 (self.profile_picker_selected + 1).min(filtered.len() - 1);
4130 }
4131 }
4132 Mode::SessionPicker => {
4133 let filtered = self.get_filtered_sessions();
4134 if !filtered.is_empty() {
4135 self.session_picker_selected =
4136 (self.session_picker_selected + 1).min(filtered.len() - 1);
4137 }
4138 }
4139 Mode::InsightsInput => {
4140 use crate::app::InsightsFocus;
4141 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4142 && self.insights_state.insights.show_dropdown
4143 && !self.insights_state.insights.log_group_matches.is_empty()
4144 {
4145 let max = self.insights_state.insights.log_group_matches.len() - 1;
4146 self.insights_state.insights.dropdown_selected =
4147 (self.insights_state.insights.dropdown_selected + 1).min(max);
4148 }
4149 }
4150 Mode::ColumnSelector => {
4151 let max = self.get_column_selector_max();
4152 let mut next_idx = (self.column_selector_index + 1).min(max);
4153 if self.is_blank_row_index(next_idx) {
4155 next_idx = (next_idx + 1).min(max);
4156 }
4157 self.column_selector_index = next_idx;
4158 }
4159 Mode::ServicePicker => {
4160 let filtered = self.filtered_services();
4161 if !filtered.is_empty() {
4162 self.service_picker.selected =
4163 (self.service_picker.selected + 1).min(filtered.len() - 1);
4164 }
4165 }
4166 Mode::TabPicker => {
4167 let filtered = self.get_filtered_tabs();
4168 if !filtered.is_empty() {
4169 self.tab_picker_selected =
4170 (self.tab_picker_selected + 1).min(filtered.len() - 1);
4171 }
4172 }
4173 Mode::Normal => {
4174 if !self.service_selected {
4175 let filtered = self.filtered_services();
4176 if !filtered.is_empty() {
4177 self.service_picker.selected =
4178 (self.service_picker.selected + 1).min(filtered.len() - 1);
4179 }
4180 } else if self.current_service == Service::S3Buckets {
4181 if self.s3_state.current_bucket.is_some() {
4182 if self.s3_state.object_tab == S3ObjectTab::Properties {
4183 self.s3_state.properties_scroll =
4185 self.s3_state.properties_scroll.saturating_add(1);
4186 } else {
4187 let total_rows = self.calculate_total_object_rows();
4189 let max = total_rows.saturating_sub(1);
4190 self.s3_state.selected_object =
4191 (self.s3_state.selected_object + 1).min(max);
4192
4193 let visible_rows = self.s3_state.object_visible_rows.get();
4195 if self.s3_state.selected_object
4196 >= self.s3_state.object_scroll_offset + visible_rows
4197 {
4198 self.s3_state.object_scroll_offset =
4199 self.s3_state.selected_object - visible_rows + 1;
4200 }
4201 }
4202 } else {
4203 let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
4205 if total_rows > 0 {
4206 self.s3_state.selected_row =
4207 (self.s3_state.selected_row + 1).min(total_rows - 1);
4208
4209 let visible_rows = self.s3_state.bucket_visible_rows.get();
4211 if self.s3_state.selected_row
4212 >= self.s3_state.bucket_scroll_offset + visible_rows
4213 {
4214 self.s3_state.bucket_scroll_offset =
4215 self.s3_state.selected_row - visible_rows + 1;
4216 }
4217 }
4218 }
4219 } else if self.view_mode == ViewMode::InsightsResults {
4220 let max = self
4221 .insights_state
4222 .insights
4223 .query_results
4224 .len()
4225 .saturating_sub(1);
4226 if self.insights_state.insights.results_selected < max {
4227 self.insights_state.insights.results_selected += 1;
4228 }
4229 } else if self.current_service == Service::ApiGatewayApis {
4230 if let Some(api) = &self.apig_state.current_api {
4231 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4232 let protocol = api.protocol_type.to_uppercase();
4233 if protocol == "REST" {
4234 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4236 &self.apig_state.resources.items,
4237 &self.apig_state.expanded_resources,
4238 &self.apig_state.resource_children,
4239 );
4240 if total_rows > 0 {
4241 self.apig_state.resources.selected =
4242 (self.apig_state.resources.selected + 1)
4243 .min(total_rows - 1);
4244 }
4245 } else {
4246 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4248 &self.apig_state.routes.items,
4249 &self.apig_state.expanded_routes,
4250 &self.apig_state.route_children,
4251 );
4252 if total_rows > 0 {
4253 self.apig_state.routes.selected =
4254 (self.apig_state.routes.selected + 1).min(total_rows - 1);
4255 }
4256 }
4257 }
4258 } else {
4259 let filtered = crate::ui::apig::filtered_apis(self);
4260 if !filtered.is_empty() {
4261 self.apig_state.apis.next_item(filtered.len());
4262 }
4263 }
4264 } else if self.view_mode == ViewMode::PolicyView {
4265 let lines = self.iam_state.policy_document.lines().count();
4266 let max_scroll = lines.saturating_sub(1);
4267 self.iam_state.policy_scroll =
4268 (self.iam_state.policy_scroll + 1).min(max_scroll);
4269 } else if self.current_service == Service::SqsQueues
4270 && self.sqs_state.current_queue.is_some()
4271 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
4272 {
4273 let lines = self.sqs_state.policy_document.lines().count();
4274 let max_scroll = lines.saturating_sub(1);
4275 self.sqs_state.policy_scroll =
4276 (self.sqs_state.policy_scroll + 1).min(max_scroll);
4277 } else if self.current_service == Service::LambdaFunctions
4278 && self.lambda_state.current_function.is_some()
4279 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4280 && !self.lambda_state.is_metrics_loading()
4281 {
4282 self.lambda_state
4283 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
4284 } else if self.current_service == Service::SqsQueues
4285 && self.sqs_state.current_queue.is_some()
4286 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4287 && !self.sqs_state.is_metrics_loading()
4288 {
4289 self.sqs_state
4290 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
4291 } else if self.view_mode == ViewMode::Events {
4292 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
4293 if self.log_groups_state.event_scroll_offset >= max_scroll {
4294 } else {
4296 self.log_groups_state.event_scroll_offset =
4297 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
4298 }
4299 } else if self.current_service == Service::CloudWatchLogGroups {
4300 if self.view_mode == ViewMode::List {
4301 let filtered_groups = filtered_log_groups(self);
4302 self.log_groups_state
4303 .log_groups
4304 .next_item(filtered_groups.len());
4305 } else if self.view_mode == ViewMode::Detail {
4306 let filtered_streams = filtered_log_streams(self);
4307 if !filtered_streams.is_empty() {
4308 let max = filtered_streams.len() - 1;
4309 if self.log_groups_state.selected_stream >= max {
4310 } else {
4312 self.log_groups_state.selected_stream =
4313 (self.log_groups_state.selected_stream + 1).min(max);
4314 self.log_groups_state.expanded_stream = None;
4315 }
4316 }
4317 }
4318 } else if self.current_service == Service::CloudWatchAlarms {
4319 let filtered_alarms = match self.alarms_state.alarm_tab {
4320 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4321 AlarmTab::InAlarm => self
4322 .alarms_state
4323 .table
4324 .items
4325 .iter()
4326 .filter(|a| a.state.to_uppercase() == "ALARM")
4327 .count(),
4328 };
4329 if filtered_alarms > 0 {
4330 self.alarms_state.table.next_item(filtered_alarms);
4331 }
4332 } else if self.current_service == Service::CloudTrailEvents {
4333 if self.cloudtrail_state.current_event.is_some()
4334 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
4335 {
4336 if let Some(event) = &self.cloudtrail_state.current_event {
4338 let line_count = event.cloud_trail_event_json.lines().count();
4339 let max_scroll = line_count.saturating_sub(1);
4340 self.cloudtrail_state.event_json_scroll =
4341 (self.cloudtrail_state.event_json_scroll + 1).min(max_scroll);
4342 }
4343 } else {
4344 let page_size = self.cloudtrail_state.table.page_size.value();
4346 let current_page = self.cloudtrail_state.table.selected / page_size;
4347 let page_start = current_page * page_size;
4348 let page_end = page_start + page_size;
4349 let filtered_count = self.cloudtrail_state.table.items.len();
4350
4351 if self.cloudtrail_state.table.selected < filtered_count.saturating_sub(1) {
4353 self.cloudtrail_state.table.selected =
4354 (self.cloudtrail_state.table.selected + 1)
4355 .min(page_end - 1)
4356 .min(filtered_count - 1);
4357 }
4358 }
4359 } else if self.current_service == Service::Ec2Instances {
4360 crate::ec2::actions::next_item(self);
4361 } else if self.current_service == Service::EcrRepositories {
4362 crate::ecr::actions::next_item(self);
4363 } else if self.current_service == Service::SqsQueues {
4364 if self.sqs_state.current_queue.is_some()
4365 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4366 {
4367 let filtered = filtered_lambda_triggers(self);
4368 if !filtered.is_empty() {
4369 self.sqs_state.triggers.next_item(filtered.len());
4370 }
4371 } else if self.sqs_state.current_queue.is_some()
4372 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4373 {
4374 let filtered = filtered_eventbridge_pipes(self);
4375 if !filtered.is_empty() {
4376 self.sqs_state.pipes.next_item(filtered.len());
4377 }
4378 } else if self.sqs_state.current_queue.is_some()
4379 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4380 {
4381 let filtered = filtered_tags(self);
4382 if !filtered.is_empty() {
4383 self.sqs_state.tags.next_item(filtered.len());
4384 }
4385 } else if self.sqs_state.current_queue.is_some()
4386 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4387 {
4388 let filtered = filtered_subscriptions(self);
4389 if !filtered.is_empty() {
4390 self.sqs_state.subscriptions.next_item(filtered.len());
4391 }
4392 } else {
4393 let filtered_queues = filtered_queues(
4394 &self.sqs_state.queues.items,
4395 &self.sqs_state.queues.filter,
4396 );
4397 if !filtered_queues.is_empty() {
4398 self.sqs_state.queues.next_item(filtered_queues.len());
4399 }
4400 }
4401 } else if self.current_service == Service::LambdaFunctions {
4402 crate::lambda::functions::next_item(self);
4403 } else if self.current_service == Service::LambdaApplications {
4404 crate::lambda::applications::next_item(self);
4405 } else if self.current_service == Service::CloudFormationStacks {
4406 crate::cfn::actions::next_item(self);
4407 } else if self.current_service == Service::IamUsers {
4408 if self.iam_state.current_user.is_some() {
4409 if self.iam_state.user_tab == UserTab::Tags {
4410 let filtered = filtered_user_tags(self);
4411 if !filtered.is_empty() {
4412 self.iam_state.user_tags.next_item(filtered.len());
4413 }
4414 } else {
4415 let filtered = filtered_iam_policies(self);
4416 if !filtered.is_empty() {
4417 self.iam_state.policies.next_item(filtered.len());
4418 }
4419 }
4420 } else {
4421 let filtered = filtered_iam_users(self);
4422 if !filtered.is_empty() {
4423 self.iam_state.users.next_item(filtered.len());
4424 }
4425 }
4426 } else if self.current_service == Service::IamRoles {
4427 if self.iam_state.current_role.is_some() {
4428 if self.iam_state.role_tab == RoleTab::TrustRelationships {
4429 let lines = self.iam_state.trust_policy_document.lines().count();
4430 let max_scroll = lines.saturating_sub(1);
4431 self.iam_state.trust_policy_scroll =
4432 (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
4433 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4434 self.iam_state.revoke_sessions_scroll =
4435 (self.iam_state.revoke_sessions_scroll + 1).min(19);
4436 } else if self.iam_state.role_tab == RoleTab::Tags {
4437 let filtered = filtered_iam_tags(self);
4438 if !filtered.is_empty() {
4439 self.iam_state.tags.next_item(filtered.len());
4440 }
4441 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4442 let filtered = filtered_last_accessed(self);
4443 if !filtered.is_empty() {
4444 self.iam_state
4445 .last_accessed_services
4446 .next_item(filtered.len());
4447 }
4448 } else {
4449 let filtered = filtered_iam_policies(self);
4450 if !filtered.is_empty() {
4451 self.iam_state.policies.next_item(filtered.len());
4452 }
4453 }
4454 } else {
4455 let filtered = filtered_iam_roles(self);
4456 if !filtered.is_empty() {
4457 self.iam_state.roles.next_item(filtered.len());
4458 }
4459 }
4460 } else if self.current_service == Service::IamUserGroups {
4461 if self.iam_state.current_group.is_some() {
4462 if self.iam_state.group_tab == GroupTab::Users {
4463 let filtered: Vec<_> = self
4464 .iam_state
4465 .group_users
4466 .items
4467 .iter()
4468 .filter(|u| {
4469 if self.iam_state.group_users.filter.is_empty() {
4470 true
4471 } else {
4472 u.user_name.to_lowercase().contains(
4473 &self.iam_state.group_users.filter.to_lowercase(),
4474 )
4475 }
4476 })
4477 .collect();
4478 if !filtered.is_empty() {
4479 self.iam_state.group_users.next_item(filtered.len());
4480 }
4481 } else if self.iam_state.group_tab == GroupTab::Permissions {
4482 let filtered = filtered_iam_policies(self);
4483 if !filtered.is_empty() {
4484 self.iam_state.policies.next_item(filtered.len());
4485 }
4486 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4487 let filtered = filtered_last_accessed(self);
4488 if !filtered.is_empty() {
4489 self.iam_state
4490 .last_accessed_services
4491 .next_item(filtered.len());
4492 }
4493 }
4494 } else {
4495 let filtered: Vec<_> = self
4496 .iam_state
4497 .groups
4498 .items
4499 .iter()
4500 .filter(|g| {
4501 if self.iam_state.groups.filter.is_empty() {
4502 true
4503 } else {
4504 g.group_name
4505 .to_lowercase()
4506 .contains(&self.iam_state.groups.filter.to_lowercase())
4507 }
4508 })
4509 .collect();
4510 if !filtered.is_empty() {
4511 self.iam_state.groups.next_item(filtered.len());
4512 }
4513 }
4514 }
4515 }
4516 _ => {}
4517 }
4518 }
4519
4520 fn prev_item(&mut self) {
4521 match self.mode {
4522 Mode::FilterInput => {
4523 if self.current_service == Service::S3Buckets
4524 && crate::s3::actions::is_pagination_focused(self)
4525 {
4526 crate::s3::actions::page_up_filter_input(self);
4527 } else if self.current_service == Service::CloudTrailEvents
4528 && self.cloudtrail_state.input_focus == InputFocus::Pagination
4529 {
4530 let page_size = self.cloudtrail_state.table.page_size.value();
4532 let current_page = self.cloudtrail_state.table.selected / page_size;
4533 if current_page > 0 {
4534 self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
4535 }
4536 } else if self.current_service == Service::CloudFormationStacks {
4537 crate::cfn::actions::prev_item_filter_input(self);
4538 } else if self.current_service == Service::IamUsers
4539 && self.iam_state.current_user.is_some()
4540 {
4541 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
4542 if self.iam_state.user_tab == UserTab::Permissions
4543 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
4544 {
4545 self.cycle_policy_type_prev();
4546 } else if self.iam_state.user_tab == UserTab::LastAccessed
4547 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
4548 {
4549 self.iam_state.last_accessed_history_filter =
4550 self.iam_state.last_accessed_history_filter.prev();
4551 self.iam_state.last_accessed_services.reset();
4552 }
4553 } else if self.current_service == Service::IamRoles
4554 && self.iam_state.current_role.is_some()
4555 && self.iam_state.role_tab == RoleTab::Permissions
4556 {
4557 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
4558 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
4559 self.cycle_policy_type_prev();
4560 }
4561 } else if self.current_service == Service::Ec2Instances {
4562 crate::ec2::actions::toggle_state_filter_prev(self);
4563 } else if self.current_service == Service::SqsQueues {
4564 use crate::ui::sqs::SUBSCRIPTION_REGION;
4565 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
4566 self.sqs_state.subscription_region_selected = self
4567 .sqs_state
4568 .subscription_region_selected
4569 .saturating_sub(1);
4570 self.sqs_state.subscriptions.reset();
4571 }
4572 }
4573 }
4574 Mode::RegionPicker => {
4575 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
4576 }
4577 Mode::ProfilePicker => {
4578 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
4579 }
4580 Mode::SessionPicker => {
4581 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
4582 }
4583 Mode::InsightsInput => {
4584 use crate::app::InsightsFocus;
4585 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4586 && self.insights_state.insights.show_dropdown
4587 && !self.insights_state.insights.log_group_matches.is_empty()
4588 {
4589 self.insights_state.insights.dropdown_selected = self
4590 .insights_state
4591 .insights
4592 .dropdown_selected
4593 .saturating_sub(1);
4594 }
4595 }
4596 Mode::ColumnSelector => {
4597 let mut prev_idx = self.column_selector_index.saturating_sub(1);
4598 if self.is_blank_row_index(prev_idx) {
4600 prev_idx = prev_idx.saturating_sub(1);
4601 }
4602 self.column_selector_index = prev_idx;
4603 }
4604 Mode::ServicePicker => {
4605 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
4606 }
4607 Mode::TabPicker => {
4608 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
4609 }
4610 Mode::Normal => {
4611 if !self.service_selected {
4612 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
4613 } else if self.current_service == Service::S3Buckets {
4614 if self.s3_state.current_bucket.is_some() {
4615 if self.s3_state.object_tab == S3ObjectTab::Properties {
4616 self.s3_state.properties_scroll =
4617 self.s3_state.properties_scroll.saturating_sub(1);
4618 } else {
4619 self.s3_state.selected_object =
4620 self.s3_state.selected_object.saturating_sub(1);
4621
4622 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4624 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4625 }
4626 }
4627 } else {
4628 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
4629
4630 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4632 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4633 }
4634 }
4635 } else if self.current_service == Service::ApiGatewayApis {
4636 if let Some(api) = &self.apig_state.current_api {
4637 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4638 let protocol = api.protocol_type.to_uppercase();
4639 if protocol == "REST" {
4640 if self.apig_state.resources.selected > 0 {
4642 self.apig_state.resources.selected -= 1;
4643 }
4644 } else {
4645 if self.apig_state.routes.selected > 0 {
4647 self.apig_state.routes.selected -= 1;
4648 }
4649 }
4650 }
4651 } else {
4652 self.apig_state.apis.prev_item();
4653 }
4654 } else if self.view_mode == ViewMode::InsightsResults {
4655 if self.insights_state.insights.results_selected > 0 {
4656 self.insights_state.insights.results_selected -= 1;
4657 }
4658 } else if self.view_mode == ViewMode::PolicyView {
4659 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
4660 } else if self.current_service == Service::SqsQueues
4661 && self.sqs_state.current_queue.is_some()
4662 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
4663 {
4664 } else if self.current_service == Service::LambdaFunctions
4665 && self.lambda_state.current_function.is_some()
4666 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4667 && !self.lambda_state.is_metrics_loading()
4668 {
4669 crate::lambda::functions::scroll_up(self);
4670 } else if self.current_service == Service::SqsQueues
4671 && self.sqs_state.current_queue.is_some()
4672 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4673 && !self.sqs_state.is_metrics_loading()
4674 {
4675 self.sqs_state.set_monitoring_scroll(
4676 self.sqs_state.monitoring_scroll().saturating_sub(1),
4677 );
4678 } else if self.view_mode == ViewMode::Events {
4679 if self.log_groups_state.event_scroll_offset == 0 {
4680 if self.log_groups_state.has_older_events {
4681 self.log_groups_state.loading = true;
4682 }
4683 } else {
4685 self.log_groups_state.event_scroll_offset =
4686 self.log_groups_state.event_scroll_offset.saturating_sub(1);
4687 }
4688 } else if self.current_service == Service::CloudWatchLogGroups {
4689 if self.view_mode == ViewMode::List {
4690 self.log_groups_state.log_groups.prev_item();
4691 } else if self.view_mode == ViewMode::Detail
4692 && self.log_groups_state.selected_stream > 0
4693 {
4694 self.log_groups_state.selected_stream =
4695 self.log_groups_state.selected_stream.saturating_sub(1);
4696 self.log_groups_state.expanded_stream = None;
4697 }
4698 } else if self.current_service == Service::CloudWatchAlarms {
4699 self.alarms_state.table.prev_item();
4700 } else if self.current_service == Service::CloudTrailEvents {
4701 if self.cloudtrail_state.current_event.is_some()
4702 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
4703 {
4704 self.cloudtrail_state.event_json_scroll =
4706 self.cloudtrail_state.event_json_scroll.saturating_sub(1);
4707 } else {
4708 let page_size = self.cloudtrail_state.table.page_size.value();
4710 let current_page = self.cloudtrail_state.table.selected / page_size;
4711 let page_start = current_page * page_size;
4712
4713 if self.cloudtrail_state.table.selected > page_start {
4715 self.cloudtrail_state.table.selected -= 1;
4716 }
4717 }
4718 } else if self.current_service == Service::Ec2Instances {
4719 if self.ec2_state.current_instance.is_some()
4720 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4721 {
4722 self.ec2_state.tags.prev_item();
4723 } else {
4724 self.ec2_state.table.prev_item();
4725 }
4726 } else if self.current_service == Service::EcrRepositories {
4727 crate::ecr::actions::prev_item(self);
4728 } else if self.current_service == Service::SqsQueues {
4729 if self.sqs_state.current_queue.is_some()
4730 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4731 {
4732 self.sqs_state.triggers.prev_item();
4733 } else if self.sqs_state.current_queue.is_some()
4734 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4735 {
4736 self.sqs_state.pipes.prev_item();
4737 } else if self.sqs_state.current_queue.is_some()
4738 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4739 {
4740 self.sqs_state.tags.prev_item();
4741 } else if self.sqs_state.current_queue.is_some()
4742 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4743 {
4744 self.sqs_state.subscriptions.prev_item();
4745 } else {
4746 self.sqs_state.queues.prev_item();
4747 }
4748 } else if self.current_service == Service::LambdaFunctions {
4749 crate::lambda::functions::prev_item(self);
4750 } else if self.current_service == Service::LambdaApplications {
4751 crate::lambda::applications::prev_item(self);
4752 } else if self.current_service == Service::CloudFormationStacks {
4753 crate::cfn::actions::prev_item(self);
4754 } else if self.current_service == Service::IamUsers {
4755 self.iam_state.users.prev_item();
4756 } else if self.current_service == Service::IamRoles {
4757 if self.iam_state.current_role.is_some() {
4758 if self.iam_state.role_tab == RoleTab::TrustRelationships {
4759 self.iam_state.trust_policy_scroll =
4760 self.iam_state.trust_policy_scroll.saturating_sub(1);
4761 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4762 self.iam_state.revoke_sessions_scroll =
4763 self.iam_state.revoke_sessions_scroll.saturating_sub(1);
4764 } else if self.iam_state.role_tab == RoleTab::Tags {
4765 self.iam_state.tags.prev_item();
4766 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4767 self.iam_state.last_accessed_services.prev_item();
4768 } else {
4769 self.iam_state.policies.prev_item();
4770 }
4771 } else {
4772 self.iam_state.roles.prev_item();
4773 }
4774 } else if self.current_service == Service::IamUserGroups {
4775 if self.iam_state.current_group.is_some() {
4776 if self.iam_state.group_tab == GroupTab::Users {
4777 self.iam_state.group_users.prev_item();
4778 } else if self.iam_state.group_tab == GroupTab::Permissions {
4779 self.iam_state.policies.prev_item();
4780 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4781 self.iam_state.last_accessed_services.prev_item();
4782 }
4783 } else {
4784 self.iam_state.groups.prev_item();
4785 }
4786 }
4787 }
4788 _ => {}
4789 }
4790 }
4791
4792 fn page_down(&mut self) {
4793 if self.current_service == Service::CloudTrailEvents
4794 && self.cloudtrail_state.current_event.is_some()
4795 {
4796 if let Some(event) = &self.cloudtrail_state.current_event {
4797 let lines = event.cloud_trail_event_json.lines().count();
4798 let max_scroll = lines.saturating_sub(1);
4799 self.cloudtrail_state.event_json_scroll =
4800 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
4801 }
4802 } else if self.mode == Mode::ColumnSelector {
4803 let max = self.get_column_selector_max();
4804 let mut next_idx = (self.column_selector_index + 10).min(max);
4805 if self.is_blank_row_index(next_idx) {
4807 next_idx = (next_idx + 1).min(max);
4808 }
4809 self.column_selector_index = next_idx;
4810 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
4811 crate::s3::actions::page_down_filter_input(self);
4812 } else if self.mode == Mode::FilterInput
4813 && self.current_service == Service::CloudFormationStacks
4814 {
4815 crate::cfn::actions::page_down_filter_input(self);
4816 } else if self.mode == Mode::FilterInput
4817 && self.current_service == Service::IamRoles
4818 && self.iam_state.current_role.is_none()
4819 {
4820 let page_size = self.iam_state.roles.page_size.value();
4821 let filtered_count = filtered_iam_roles(self).len();
4822 self.iam_state.role_input_focus.handle_page_down(
4823 &mut self.iam_state.roles.selected,
4824 &mut self.iam_state.roles.scroll_offset,
4825 page_size,
4826 filtered_count,
4827 );
4828 } else if self.mode == Mode::FilterInput
4829 && self.current_service == Service::CloudWatchAlarms
4830 {
4831 let page_size = self.alarms_state.table.page_size.value();
4832 let filtered_count = self.alarms_state.table.items.len();
4833 self.alarms_state.input_focus.handle_page_down(
4834 &mut self.alarms_state.table.selected,
4835 &mut self.alarms_state.table.scroll_offset,
4836 page_size,
4837 filtered_count,
4838 );
4839 } else if self.mode == Mode::FilterInput
4840 && self.current_service == Service::CloudTrailEvents
4841 {
4842 let page_size = self.cloudtrail_state.table.page_size.value();
4843 let filtered_count = self.cloudtrail_state.table.items.len();
4844 self.cloudtrail_state.input_focus.handle_page_down(
4845 &mut self.cloudtrail_state.table.selected,
4846 &mut self.cloudtrail_state.table.scroll_offset,
4847 page_size,
4848 filtered_count,
4849 );
4850 } else if self.mode == Mode::FilterInput
4851 && self.current_service == Service::CloudWatchLogGroups
4852 {
4853 if self.view_mode == ViewMode::List {
4854 let filtered = filtered_log_groups(self);
4856 let page_size = self.log_groups_state.log_groups.page_size.value();
4857 let filtered_count = filtered.len();
4858 self.log_groups_state.input_focus.handle_page_down(
4859 &mut self.log_groups_state.log_groups.selected,
4860 &mut self.log_groups_state.log_groups.scroll_offset,
4861 page_size,
4862 filtered_count,
4863 );
4864 } else {
4865 let filtered = filtered_log_streams(self);
4867 let page_size = self.log_groups_state.stream_page_size;
4868 let filtered_count = filtered.len();
4869 self.log_groups_state.input_focus.handle_page_down(
4870 &mut self.log_groups_state.selected_stream,
4871 &mut self.log_groups_state.stream_current_page,
4872 page_size,
4873 filtered_count,
4874 );
4875 self.log_groups_state.expanded_stream = None;
4876 }
4877 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4878 {
4879 crate::lambda::functions::page_down_filter_input(self);
4880 } else if self.mode == Mode::FilterInput
4881 && self.current_service == Service::LambdaApplications
4882 {
4883 crate::lambda::applications::page_down_filter_input(self);
4884 } else if self.mode == Mode::FilterInput
4885 && self.current_service == Service::EcrRepositories
4886 && self.ecr_state.current_repository.is_none()
4887 {
4888 crate::ecr::actions::page_down_filter_input(self);
4889 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4890 let page_size = self.iam_state.policies.page_size.value();
4891 let filtered_count = filtered_iam_policies(self).len();
4892 self.iam_state.policy_input_focus.handle_page_down(
4893 &mut self.iam_state.policies.selected,
4894 &mut self.iam_state.policies.scroll_offset,
4895 page_size,
4896 filtered_count,
4897 );
4898 } else if self.view_mode == ViewMode::PolicyView {
4899 let lines = self.iam_state.policy_document.lines().count();
4900 let max_scroll = lines.saturating_sub(1);
4901 self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
4902 } else if self.current_service == Service::CloudFormationStacks
4903 && self.cfn_state.current_stack.is_some()
4904 && self.cfn_state.detail_tab == CfnDetailTab::Template
4905 {
4906 crate::cfn::actions::scroll_down_template_fast(self);
4907 } else if self.current_service == Service::LambdaFunctions
4908 && self.lambda_state.current_function.is_some()
4909 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4910 && !self.lambda_state.is_metrics_loading()
4911 {
4912 crate::lambda::functions::scroll_down(self);
4913 } else if self.current_service == Service::Ec2Instances {
4914 crate::ec2::actions::scroll_down(self);
4915 } else if self.current_service == Service::SqsQueues
4916 && self.sqs_state.current_queue.is_some()
4917 {
4918 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
4919 self.sqs_state
4920 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
4921 } else {
4922 let lines = self.sqs_state.policy_document.lines().count();
4923 let max_scroll = lines.saturating_sub(1);
4924 self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
4925 }
4926 } else if self.current_service == Service::IamRoles
4927 && self.iam_state.current_role.is_some()
4928 && self.iam_state.role_tab == RoleTab::TrustRelationships
4929 {
4930 let lines = self.iam_state.trust_policy_document.lines().count();
4931 let max_scroll = lines.saturating_sub(1);
4932 self.iam_state.trust_policy_scroll =
4933 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
4934 } else if self.current_service == Service::IamRoles
4935 && self.iam_state.current_role.is_some()
4936 && self.iam_state.role_tab == RoleTab::RevokeSessions
4937 {
4938 self.iam_state.revoke_sessions_scroll =
4939 (self.iam_state.revoke_sessions_scroll + 10).min(19);
4940 } else if self.mode == Mode::Normal {
4941 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4942 {
4943 let total_rows = self.calculate_total_bucket_rows();
4944 self.s3_state.selected_row = self
4945 .s3_state
4946 .selected_row
4947 .saturating_add(10)
4948 .min(total_rows.saturating_sub(1));
4949
4950 let visible_rows = self.s3_state.bucket_visible_rows.get();
4952 if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
4953 self.s3_state.bucket_scroll_offset =
4954 self.s3_state.selected_row - visible_rows + 1;
4955 }
4956 } else if self.current_service == Service::S3Buckets
4957 && self.s3_state.current_bucket.is_some()
4958 {
4959 let total_rows = self.calculate_total_object_rows();
4960 self.s3_state.selected_object = self
4961 .s3_state
4962 .selected_object
4963 .saturating_add(10)
4964 .min(total_rows.saturating_sub(1));
4965
4966 let visible_rows = self.s3_state.object_visible_rows.get();
4968 if self.s3_state.selected_object
4969 >= self.s3_state.object_scroll_offset + visible_rows
4970 {
4971 self.s3_state.object_scroll_offset =
4972 self.s3_state.selected_object - visible_rows + 1;
4973 }
4974 } else if self.current_service == Service::CloudWatchLogGroups
4975 && self.view_mode == ViewMode::List
4976 {
4977 let filtered = filtered_log_groups(self);
4978 self.log_groups_state.log_groups.page_down(filtered.len());
4979 } else if self.current_service == Service::CloudWatchLogGroups
4980 && self.view_mode == ViewMode::Detail
4981 {
4982 let len = filtered_log_streams(self).len();
4983 nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
4984 } else if self.view_mode == ViewMode::Events {
4985 let max = self.log_groups_state.log_events.len();
4986 nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
4987 } else if self.view_mode == ViewMode::InsightsResults {
4988 let max = self.insights_state.insights.query_results.len();
4989 nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
4990 } else if self.current_service == Service::CloudWatchAlarms {
4991 let filtered = match self.alarms_state.alarm_tab {
4992 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4993 AlarmTab::InAlarm => self
4994 .alarms_state
4995 .table
4996 .items
4997 .iter()
4998 .filter(|a| a.state.to_uppercase() == "ALARM")
4999 .count(),
5000 };
5001 if filtered > 0 {
5002 self.alarms_state.table.page_down(filtered);
5003 }
5004 } else if self.current_service == Service::CloudTrailEvents {
5005 let filtered_count = self.cloudtrail_state.table.items.len();
5006 if filtered_count > 0 {
5007 self.cloudtrail_state.table.page_down(filtered_count);
5008 }
5009 } else if self.current_service == Service::Ec2Instances {
5010 crate::ec2::actions::page_down_normal(self);
5011 } else if self.current_service == Service::EcrRepositories {
5012 crate::ecr::actions::page_down_normal(self);
5013 } else if self.current_service == Service::SqsQueues {
5014 let filtered =
5015 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter);
5016 self.sqs_state.queues.page_down(filtered.len());
5017 } else if self.current_service == Service::LambdaFunctions {
5018 crate::lambda::functions::page_down_normal(self);
5019 } else if self.current_service == Service::LambdaApplications {
5020 crate::lambda::applications::page_down_normal(self);
5021 } else if self.current_service == Service::CloudFormationStacks {
5022 crate::cfn::actions::page_down_normal(self);
5023 } else if self.current_service == Service::IamUsers {
5024 let len = filtered_iam_users(self).len();
5025 nav_page_down(&mut self.iam_state.users.selected, len, 10);
5026 } else if self.current_service == Service::IamRoles {
5027 if self.iam_state.current_role.is_some() {
5028 let filtered = filtered_iam_policies(self);
5029 if !filtered.is_empty() {
5030 self.iam_state.policies.page_down(filtered.len());
5031 }
5032 } else {
5033 let filtered = filtered_iam_roles(self);
5034 self.iam_state.roles.page_down(filtered.len());
5035 }
5036 } else if self.current_service == Service::IamUserGroups {
5037 if self.iam_state.current_group.is_some() {
5038 if self.iam_state.group_tab == GroupTab::Users {
5039 let filtered: Vec<_> = self
5040 .iam_state
5041 .group_users
5042 .items
5043 .iter()
5044 .filter(|u| {
5045 if self.iam_state.group_users.filter.is_empty() {
5046 true
5047 } else {
5048 u.user_name
5049 .to_lowercase()
5050 .contains(&self.iam_state.group_users.filter.to_lowercase())
5051 }
5052 })
5053 .collect();
5054 if !filtered.is_empty() {
5055 self.iam_state.group_users.page_down(filtered.len());
5056 }
5057 } else if self.iam_state.group_tab == GroupTab::Permissions {
5058 let filtered = filtered_iam_policies(self);
5059 if !filtered.is_empty() {
5060 self.iam_state.policies.page_down(filtered.len());
5061 }
5062 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5063 let filtered = filtered_last_accessed(self);
5064 if !filtered.is_empty() {
5065 self.iam_state
5066 .last_accessed_services
5067 .page_down(filtered.len());
5068 }
5069 }
5070 } else {
5071 let filtered: Vec<_> = self
5072 .iam_state
5073 .groups
5074 .items
5075 .iter()
5076 .filter(|g| {
5077 if self.iam_state.groups.filter.is_empty() {
5078 true
5079 } else {
5080 g.group_name
5081 .to_lowercase()
5082 .contains(&self.iam_state.groups.filter.to_lowercase())
5083 }
5084 })
5085 .collect();
5086 if !filtered.is_empty() {
5087 self.iam_state.groups.page_down(filtered.len());
5088 }
5089 }
5090 }
5091 }
5092 }
5093
5094 fn cycle_policy_type_next(&mut self) {
5095 let types = ["All types", "AWS managed", "Customer managed"];
5096 let current_idx = types
5097 .iter()
5098 .position(|&t| t == self.iam_state.policy_type_filter)
5099 .unwrap_or(0);
5100 let next_idx = (current_idx + 1) % types.len();
5101 self.iam_state.policy_type_filter = types[next_idx].to_string();
5102 self.iam_state.policies.reset();
5103 }
5104
5105 fn cycle_policy_type_prev(&mut self) {
5106 let types = ["All types", "AWS managed", "Customer managed"];
5107 let current_idx = types
5108 .iter()
5109 .position(|&t| t == self.iam_state.policy_type_filter)
5110 .unwrap_or(0);
5111 let prev_idx = if current_idx == 0 {
5112 types.len() - 1
5113 } else {
5114 current_idx - 1
5115 };
5116 self.iam_state.policy_type_filter = types[prev_idx].to_string();
5117 self.iam_state.policies.reset();
5118 }
5119
5120 fn page_up(&mut self) {
5121 if self.current_service == Service::CloudTrailEvents
5122 && self.cloudtrail_state.current_event.is_some()
5123 {
5124 self.cloudtrail_state.event_json_scroll =
5125 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
5126 } else if self.mode == Mode::ColumnSelector {
5127 let mut prev_idx = self.column_selector_index.saturating_sub(10);
5128 if self.is_blank_row_index(prev_idx) {
5130 prev_idx = prev_idx.saturating_sub(1);
5131 }
5132 self.column_selector_index = prev_idx;
5133 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
5134 crate::s3::actions::page_up_filter_input(self);
5135 } else if self.mode == Mode::FilterInput
5136 && self.current_service == Service::CloudFormationStacks
5137 {
5138 crate::cfn::actions::page_up_filter_input(self);
5139 } else if self.mode == Mode::FilterInput
5140 && self.current_service == Service::IamRoles
5141 && self.iam_state.current_role.is_none()
5142 {
5143 let page_size = self.iam_state.roles.page_size.value();
5144 self.iam_state.role_input_focus.handle_page_up(
5145 &mut self.iam_state.roles.selected,
5146 &mut self.iam_state.roles.scroll_offset,
5147 page_size,
5148 );
5149 } else if self.mode == Mode::FilterInput
5150 && self.current_service == Service::CloudWatchAlarms
5151 {
5152 let page_size = self.alarms_state.table.page_size.value();
5153 self.alarms_state.input_focus.handle_page_up(
5154 &mut self.alarms_state.table.selected,
5155 &mut self.alarms_state.table.scroll_offset,
5156 page_size,
5157 );
5158 } else if self.mode == Mode::FilterInput
5159 && self.current_service == Service::CloudTrailEvents
5160 {
5161 let page_size = self.cloudtrail_state.table.page_size.value();
5162 self.cloudtrail_state.input_focus.handle_page_up(
5163 &mut self.cloudtrail_state.table.selected,
5164 &mut self.cloudtrail_state.table.scroll_offset,
5165 page_size,
5166 );
5167 } else if self.mode == Mode::FilterInput
5168 && self.current_service == Service::CloudWatchLogGroups
5169 {
5170 if self.view_mode == ViewMode::List {
5171 let page_size = self.log_groups_state.log_groups.page_size.value();
5173 self.log_groups_state.input_focus.handle_page_up(
5174 &mut self.log_groups_state.log_groups.selected,
5175 &mut self.log_groups_state.log_groups.scroll_offset,
5176 page_size,
5177 );
5178 } else {
5179 let page_size = self.log_groups_state.stream_page_size;
5181 self.log_groups_state.input_focus.handle_page_up(
5182 &mut self.log_groups_state.selected_stream,
5183 &mut self.log_groups_state.stream_current_page,
5184 page_size,
5185 );
5186 self.log_groups_state.expanded_stream = None;
5187 }
5188 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5189 {
5190 crate::lambda::functions::page_up_filter_input(self);
5191 } else if self.mode == Mode::FilterInput
5192 && self.current_service == Service::LambdaApplications
5193 {
5194 crate::lambda::applications::page_up_filter_input(self);
5195 } else if self.mode == Mode::FilterInput
5196 && self.current_service == Service::EcrRepositories
5197 && self.ecr_state.current_repository.is_none()
5198 {
5199 crate::ecr::actions::page_up_filter_input(self);
5200 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5201 let page_size = self.iam_state.policies.page_size.value();
5202 self.iam_state.policy_input_focus.handle_page_up(
5203 &mut self.iam_state.policies.selected,
5204 &mut self.iam_state.policies.scroll_offset,
5205 page_size,
5206 );
5207 } else if self.view_mode == ViewMode::PolicyView {
5208 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
5209 } else if self.current_service == Service::CloudFormationStacks
5210 && self.cfn_state.current_stack.is_some()
5211 && self.cfn_state.detail_tab == CfnDetailTab::Template
5212 {
5213 crate::cfn::actions::scroll_up_template_fast(self);
5214 } else if self.current_service == Service::SqsQueues
5215 && self.sqs_state.current_queue.is_some()
5216 {
5217 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5218 self.sqs_state
5219 .set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
5220 } else {
5221 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
5222 }
5223 } else if self.current_service == Service::IamRoles
5224 && self.iam_state.current_role.is_some()
5225 && self.iam_state.role_tab == RoleTab::TrustRelationships
5226 {
5227 self.iam_state.trust_policy_scroll =
5228 self.iam_state.trust_policy_scroll.saturating_sub(10);
5229 } else if self.current_service == Service::IamRoles
5230 && self.iam_state.current_role.is_some()
5231 && self.iam_state.role_tab == RoleTab::RevokeSessions
5232 {
5233 self.iam_state.revoke_sessions_scroll =
5234 self.iam_state.revoke_sessions_scroll.saturating_sub(10);
5235 } else if self.mode == Mode::Normal {
5236 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5237 {
5238 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
5239
5240 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5242 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5243 }
5244 } else if self.current_service == Service::S3Buckets
5245 && self.s3_state.current_bucket.is_some()
5246 {
5247 self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
5248
5249 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5251 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5252 }
5253 } else if self.current_service == Service::CloudWatchLogGroups
5254 && self.view_mode == ViewMode::List
5255 {
5256 self.log_groups_state.log_groups.page_up();
5257 } else if self.current_service == Service::CloudWatchLogGroups
5258 && self.view_mode == ViewMode::Detail
5259 {
5260 self.log_groups_state.selected_stream =
5261 self.log_groups_state.selected_stream.saturating_sub(10);
5262 } else if self.view_mode == ViewMode::Events {
5263 if self.log_groups_state.event_scroll_offset < 10
5264 && self.log_groups_state.has_older_events
5265 {
5266 self.log_groups_state.loading = true;
5267 }
5268 self.log_groups_state.event_scroll_offset =
5269 self.log_groups_state.event_scroll_offset.saturating_sub(10);
5270 } else if self.view_mode == ViewMode::InsightsResults {
5271 self.insights_state.insights.results_selected = self
5272 .insights_state
5273 .insights
5274 .results_selected
5275 .saturating_sub(10);
5276 } else if self.current_service == Service::CloudWatchAlarms {
5277 self.alarms_state.table.page_up();
5278 } else if self.current_service == Service::CloudTrailEvents {
5279 self.cloudtrail_state.table.page_up();
5280 } else if self.current_service == Service::Ec2Instances {
5281 crate::ec2::actions::page_up_normal(self);
5282 } else if self.current_service == Service::EcrRepositories {
5283 crate::ecr::actions::page_up_normal(self);
5284 } else if self.current_service == Service::SqsQueues {
5285 self.sqs_state.queues.page_up();
5286 } else if self.current_service == Service::LambdaFunctions {
5287 crate::lambda::functions::page_up_normal(self);
5288 } else if self.current_service == Service::LambdaApplications {
5289 crate::lambda::applications::page_up_normal(self);
5290 } else if self.current_service == Service::CloudFormationStacks {
5291 crate::cfn::actions::page_up_normal(self);
5292 } else if self.current_service == Service::IamUsers {
5293 self.iam_state.users.page_up();
5294 } else if self.current_service == Service::IamRoles {
5295 if self.iam_state.current_role.is_some() {
5296 self.iam_state.policies.page_up();
5297 } else {
5298 self.iam_state.roles.page_up();
5299 }
5300 }
5301 }
5302 }
5303
5304 fn next_pane(&mut self) {
5305 if self.current_service == Service::S3Buckets {
5306 if self.s3_state.current_bucket.is_some() {
5307 let mut visual_idx = 0;
5310 let mut found_obj: Option<S3Object> = None;
5311
5312 fn check_nested(
5314 obj: &S3Object,
5315 visual_idx: &mut usize,
5316 target_idx: usize,
5317 expanded_prefixes: &std::collections::HashSet<String>,
5318 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5319 found_obj: &mut Option<S3Object>,
5320 ) {
5321 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5322 if let Some(preview) = prefix_preview.get(&obj.key) {
5323 for nested_obj in preview {
5324 if *visual_idx == target_idx {
5325 *found_obj = Some(nested_obj.clone());
5326 return;
5327 }
5328 *visual_idx += 1;
5329
5330 check_nested(
5332 nested_obj,
5333 visual_idx,
5334 target_idx,
5335 expanded_prefixes,
5336 prefix_preview,
5337 found_obj,
5338 );
5339 if found_obj.is_some() {
5340 return;
5341 }
5342 }
5343 } else {
5344 *visual_idx += 1;
5346 }
5347 }
5348 }
5349
5350 for obj in &self.s3_state.objects {
5351 if visual_idx == self.s3_state.selected_object {
5352 found_obj = Some(obj.clone());
5353 break;
5354 }
5355 visual_idx += 1;
5356
5357 check_nested(
5359 obj,
5360 &mut visual_idx,
5361 self.s3_state.selected_object,
5362 &self.s3_state.expanded_prefixes,
5363 &self.s3_state.prefix_preview,
5364 &mut found_obj,
5365 );
5366 if found_obj.is_some() {
5367 break;
5368 }
5369 }
5370
5371 if let Some(obj) = found_obj {
5372 if obj.is_prefix {
5373 if !self.s3_state.expanded_prefixes.contains(&obj.key) {
5374 self.s3_state.expanded_prefixes.insert(obj.key.clone());
5375 if !self.s3_state.prefix_preview.contains_key(&obj.key) {
5377 self.s3_state.buckets.loading = true;
5378 }
5379 }
5380 if self.s3_state.expanded_prefixes.contains(&obj.key) {
5382 if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
5383 if !preview.is_empty() {
5384 self.s3_state.selected_object += 1;
5385 }
5386 }
5387 }
5388 }
5389 }
5390 } else {
5391 let mut row_idx = 0;
5393 let mut found = false;
5394 for bucket in &self.s3_state.buckets.items {
5395 if row_idx == self.s3_state.selected_row {
5396 if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
5398 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
5399 if !self.s3_state.bucket_preview.contains_key(&bucket.name)
5400 && !self.s3_state.bucket_errors.contains_key(&bucket.name)
5401 {
5402 self.s3_state.buckets.loading = true;
5403 }
5404 }
5405 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5407 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5408 if !preview.is_empty() {
5409 self.s3_state.selected_row = row_idx + 1;
5410 let visible = self.s3_state.bucket_visible_rows.get();
5412 if self.s3_state.selected_row
5413 >= self.s3_state.bucket_scroll_offset + visible
5414 {
5415 self.s3_state.bucket_scroll_offset =
5416 self.s3_state.selected_row.saturating_sub(visible - 1);
5417 }
5418 }
5419 }
5420 }
5421 break;
5422 }
5423 row_idx += 1;
5424
5425 if self.s3_state.bucket_errors.contains_key(&bucket.name)
5427 && self.s3_state.expanded_prefixes.contains(&bucket.name)
5428 {
5429 if let Some(err) = self.s3_state.bucket_errors.get(&bucket.name) {
5430 let max_width = 120;
5431 let error_rows = if err.len() > max_width {
5432 err.len().div_ceil(max_width)
5433 } else {
5434 1
5435 };
5436 row_idx += error_rows;
5437 }
5438 continue;
5439 }
5440
5441 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5442 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5443 #[allow(clippy::too_many_arguments)]
5445 fn check_nested_expansion(
5446 objects: &[S3Object],
5447 row_idx: &mut usize,
5448 target_row: usize,
5449 expanded_prefixes: &mut std::collections::HashSet<String>,
5450 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5451 found: &mut bool,
5452 loading: &mut bool,
5453 selected_row: &mut usize,
5454 ) {
5455 for obj in objects {
5456 if *row_idx == target_row {
5457 if obj.is_prefix {
5459 if !expanded_prefixes.contains(&obj.key) {
5460 expanded_prefixes.insert(obj.key.clone());
5461 if !prefix_preview.contains_key(&obj.key) {
5462 *loading = true;
5463 }
5464 }
5465 if expanded_prefixes.contains(&obj.key) {
5467 if let Some(preview) = prefix_preview.get(&obj.key)
5468 {
5469 if !preview.is_empty() {
5470 *selected_row = *row_idx + 1;
5471 }
5472 }
5473 }
5474 }
5475 *found = true;
5476 return;
5477 }
5478 *row_idx += 1;
5479
5480 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5482 if let Some(nested) = prefix_preview.get(&obj.key) {
5483 check_nested_expansion(
5484 nested,
5485 row_idx,
5486 target_row,
5487 expanded_prefixes,
5488 prefix_preview,
5489 found,
5490 loading,
5491 selected_row,
5492 );
5493 if *found {
5494 return;
5495 }
5496 } else {
5497 *row_idx += 1; }
5499 }
5500 }
5501 }
5502
5503 check_nested_expansion(
5504 preview,
5505 &mut row_idx,
5506 self.s3_state.selected_row,
5507 &mut self.s3_state.expanded_prefixes,
5508 &self.s3_state.prefix_preview,
5509 &mut found,
5510 &mut self.s3_state.buckets.loading,
5511 &mut self.s3_state.selected_row,
5512 );
5513 if found || row_idx > self.s3_state.selected_row {
5514 break;
5515 }
5516 } else {
5517 if row_idx > self.s3_state.selected_row {
5520 break;
5521 }
5522 }
5523 }
5524 if found {
5525 break;
5526 }
5527 }
5528 }
5529 } else if self.view_mode == ViewMode::InsightsResults {
5530 let max_cols = self
5532 .insights_state
5533 .insights
5534 .query_results
5535 .first()
5536 .map(|r| r.len())
5537 .unwrap_or(0);
5538 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
5539 self.insights_state.insights.results_horizontal_scroll += 1;
5540 }
5541 } else if self.current_service == Service::CloudWatchLogGroups
5542 && self.view_mode == ViewMode::List
5543 {
5544 if self.log_groups_state.log_groups.expanded_item
5546 != Some(self.log_groups_state.log_groups.selected)
5547 {
5548 self.log_groups_state.log_groups.expanded_item =
5549 Some(self.log_groups_state.log_groups.selected);
5550 }
5551 } else if self.current_service == Service::CloudWatchLogGroups
5552 && self.view_mode == ViewMode::Detail
5553 {
5554 if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
5556 {
5557 self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
5558 }
5559 } else if self.view_mode == ViewMode::Events {
5560 if self.log_groups_state.expanded_event
5563 != Some(self.log_groups_state.event_scroll_offset)
5564 {
5565 self.log_groups_state.expanded_event =
5566 Some(self.log_groups_state.event_scroll_offset);
5567 }
5568 } else if self.current_service == Service::CloudWatchAlarms {
5569 if !self.alarms_state.table.is_expanded() {
5571 self.alarms_state.table.toggle_expand();
5572 }
5573 } else if self.current_service == Service::Ec2Instances {
5574 crate::ec2::actions::expand_row(self);
5575 } else if self.current_service == Service::EcrRepositories {
5576 crate::ecr::actions::next_pane(self);
5577 } else if self.current_service == Service::SqsQueues {
5578 if self.sqs_state.current_queue.is_some()
5579 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5580 {
5581 self.sqs_state.triggers.toggle_expand();
5582 } else if self.sqs_state.current_queue.is_some()
5583 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5584 {
5585 self.sqs_state.pipes.toggle_expand();
5586 } else if self.sqs_state.current_queue.is_some()
5587 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5588 {
5589 self.sqs_state.tags.toggle_expand();
5590 } else if self.sqs_state.current_queue.is_some()
5591 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5592 {
5593 self.sqs_state.subscriptions.toggle_expand();
5594 } else {
5595 self.sqs_state.queues.expand();
5596 }
5597 } else if self.current_service == Service::LambdaFunctions {
5598 crate::lambda::functions::expand_row(self);
5599 } else if self.current_service == Service::LambdaApplications {
5600 crate::lambda::applications::expand_row(self);
5601 } else if self.current_service == Service::CloudFormationStacks
5602 && self.cfn_state.current_stack.is_none()
5603 {
5604 self.cfn_state.table.toggle_expand();
5605 } else if self.current_service == Service::CloudFormationStacks
5606 && self.cfn_state.current_stack.is_some()
5607 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5608 {
5609 self.cfn_state.parameters.toggle_expand();
5610 } else if self.current_service == Service::CloudFormationStacks
5611 && self.cfn_state.current_stack.is_some()
5612 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5613 {
5614 self.cfn_state.outputs.toggle_expand();
5615 } else if self.current_service == Service::CloudFormationStacks
5616 && self.cfn_state.current_stack.is_some()
5617 && self.cfn_state.detail_tab == CfnDetailTab::Resources
5618 {
5619 self.cfn_state.resources.toggle_expand();
5620 } else if self.current_service == Service::IamUsers {
5621 if self.iam_state.current_user.is_some() {
5622 if self.iam_state.user_tab == UserTab::Tags {
5623 if self.iam_state.user_tags.expanded_item
5624 != Some(self.iam_state.user_tags.selected)
5625 {
5626 self.iam_state.user_tags.expanded_item =
5627 Some(self.iam_state.user_tags.selected);
5628 }
5629 } else if self.iam_state.policies.expanded_item
5630 != Some(self.iam_state.policies.selected)
5631 {
5632 self.iam_state.policies.toggle_expand();
5633 }
5634 } else if !self.iam_state.users.is_expanded() {
5635 self.iam_state.users.toggle_expand();
5636 }
5637 } else if self.current_service == Service::IamRoles {
5638 if self.iam_state.current_role.is_some() {
5639 if self.iam_state.role_tab == RoleTab::Tags {
5641 if !self.iam_state.tags.is_expanded() {
5642 self.iam_state.tags.expand();
5643 }
5644 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5645 if !self.iam_state.last_accessed_services.is_expanded() {
5646 self.iam_state.last_accessed_services.expand();
5647 }
5648 } else if !self.iam_state.policies.is_expanded() {
5649 self.iam_state.policies.expand();
5650 }
5651 } else if !self.iam_state.roles.is_expanded() {
5652 self.iam_state.roles.expand();
5653 }
5654 } else if self.current_service == Service::IamUserGroups {
5655 if self.iam_state.current_group.is_some() {
5656 if self.iam_state.group_tab == GroupTab::Users {
5657 if !self.iam_state.group_users.is_expanded() {
5658 self.iam_state.group_users.expand();
5659 }
5660 } else if self.iam_state.group_tab == GroupTab::Permissions {
5661 if !self.iam_state.policies.is_expanded() {
5662 self.iam_state.policies.expand();
5663 }
5664 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
5665 && !self.iam_state.last_accessed_services.is_expanded()
5666 {
5667 self.iam_state.last_accessed_services.expand();
5668 }
5669 } else if !self.iam_state.groups.is_expanded() {
5670 self.iam_state.groups.expand();
5671 }
5672 }
5673 }
5674
5675 fn go_to_page(&mut self, page: usize) {
5676 if page == 0 {
5677 return;
5678 }
5679
5680 match self.current_service {
5681 Service::CloudWatchAlarms => {
5682 let alarm_page_size = self.alarms_state.table.page_size.value();
5683 let target = (page - 1) * alarm_page_size;
5684 let filtered_count = match self.alarms_state.alarm_tab {
5685 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5686 AlarmTab::InAlarm => self
5687 .alarms_state
5688 .table
5689 .items
5690 .iter()
5691 .filter(|a| a.state.to_uppercase() == "ALARM")
5692 .count(),
5693 };
5694 let max_offset = filtered_count.saturating_sub(alarm_page_size);
5695 self.alarms_state.table.scroll_offset = target.min(max_offset);
5696 self.alarms_state.table.selected = self
5697 .alarms_state
5698 .table
5699 .scroll_offset
5700 .min(filtered_count.saturating_sub(1));
5701 }
5702 Service::CloudTrailEvents => {
5703 let page_size = self.cloudtrail_state.table.page_size.value();
5704 let filtered_count = self.cloudtrail_state.table.items.len();
5705 let max_page = (filtered_count / page_size) + 1; if page <= max_page {
5709 let target = (page - 1) * page_size;
5710 self.cloudtrail_state.table.scroll_offset = target;
5711 self.cloudtrail_state.table.selected = target;
5712 self.cloudtrail_state.table.expanded_item = None; }
5714 }
5716 Service::CloudWatchLogGroups => match self.view_mode {
5717 ViewMode::Events => {
5718 let page_size = 20;
5719 let target = (page - 1) * page_size;
5720 let max = self.log_groups_state.log_events.len().saturating_sub(1);
5721 self.log_groups_state.event_scroll_offset = target.min(max);
5722 }
5723 ViewMode::Detail => {
5724 let page_size = self.log_groups_state.stream_page_size;
5725 self.log_groups_state.stream_current_page = (page - 1).min(
5726 self.log_groups_state
5727 .log_streams
5728 .len()
5729 .div_ceil(page_size)
5730 .saturating_sub(1),
5731 );
5732 self.log_groups_state.selected_stream = 0;
5733 }
5734 ViewMode::List => {
5735 let total = self.log_groups_state.log_groups.items.len();
5736 self.log_groups_state.log_groups.goto_page(page, total);
5737 }
5738 _ => {}
5739 },
5740 Service::EcrRepositories => {
5741 crate::ecr::actions::go_to_page(self, page);
5742 }
5743 Service::SqsQueues => {
5744 let filtered_count =
5745 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter)
5746 .len();
5747 self.sqs_state.queues.goto_page(page, filtered_count);
5748 }
5749 Service::S3Buckets => {
5750 if self.s3_state.current_bucket.is_some() {
5751 let page_size = 50; let target = (page - 1) * page_size;
5753 let total_rows = self.calculate_total_object_rows();
5754 let max = total_rows.saturating_sub(1);
5755 self.s3_state.selected_object = target.min(max);
5756 } else {
5757 let page_size = self.s3_state.buckets.page_size.value();
5758 let target = (page - 1) * page_size;
5759 let total_rows = self.calculate_total_bucket_rows();
5760 let max = total_rows.saturating_sub(1);
5761 self.s3_state.selected_row = target.min(max);
5762 self.s3_state.bucket_scroll_offset =
5764 target.min(total_rows.saturating_sub(page_size));
5765 }
5766 }
5767 Service::LambdaFunctions => {
5768 crate::lambda::functions::go_to_page(self, page);
5769 }
5770 Service::LambdaApplications => {
5771 crate::lambda::applications::go_to_page(self, page);
5772 }
5773 Service::CloudFormationStacks => {
5774 crate::cfn::actions::go_to_page(self, page);
5775 }
5776 Service::IamUsers => {
5777 let filtered_count = filtered_iam_users(self).len();
5778 self.iam_state.users.goto_page(page, filtered_count);
5779 }
5780 Service::IamRoles => {
5781 let filtered_count = filtered_iam_roles(self).len();
5782 self.iam_state.roles.goto_page(page, filtered_count);
5783 }
5784 _ => {}
5785 }
5786 }
5787
5788 fn prev_pane(&mut self) {
5789 if self.current_service == Service::S3Buckets {
5790 if self.s3_state.current_bucket.is_some() {
5791 let mut visual_idx = 0;
5794 let mut found_obj: Option<S3Object> = None;
5795 let mut parent_idx: Option<usize> = None;
5796
5797 #[allow(clippy::too_many_arguments)]
5799 fn find_with_parent(
5800 objects: &[S3Object],
5801 visual_idx: &mut usize,
5802 target_idx: usize,
5803 expanded_prefixes: &std::collections::HashSet<String>,
5804 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5805 found_obj: &mut Option<S3Object>,
5806 parent_idx: &mut Option<usize>,
5807 current_parent: Option<usize>,
5808 ) {
5809 for obj in objects {
5810 if *visual_idx == target_idx {
5811 *found_obj = Some(obj.clone());
5812 *parent_idx = current_parent;
5813 return;
5814 }
5815 let obj_idx = *visual_idx;
5816 *visual_idx += 1;
5817
5818 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5820 if let Some(preview) = prefix_preview.get(&obj.key) {
5821 find_with_parent(
5822 preview,
5823 visual_idx,
5824 target_idx,
5825 expanded_prefixes,
5826 prefix_preview,
5827 found_obj,
5828 parent_idx,
5829 Some(obj_idx),
5830 );
5831 if found_obj.is_some() {
5832 return;
5833 }
5834 }
5835 }
5836 }
5837 }
5838
5839 find_with_parent(
5840 &self.s3_state.objects,
5841 &mut visual_idx,
5842 self.s3_state.selected_object,
5843 &self.s3_state.expanded_prefixes,
5844 &self.s3_state.prefix_preview,
5845 &mut found_obj,
5846 &mut parent_idx,
5847 None,
5848 );
5849
5850 if let Some(obj) = found_obj {
5851 if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
5852 self.s3_state.expanded_prefixes.remove(&obj.key);
5854 if let Some(parent) = parent_idx {
5855 self.s3_state.selected_object = parent;
5856 }
5857 } else if let Some(parent) = parent_idx {
5858 self.s3_state.selected_object = parent;
5860 }
5861 }
5862
5863 let visible_rows = self.s3_state.object_visible_rows.get();
5865 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5866 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5867 } else if self.s3_state.selected_object
5868 >= self.s3_state.object_scroll_offset + visible_rows
5869 {
5870 self.s3_state.object_scroll_offset = self
5871 .s3_state
5872 .selected_object
5873 .saturating_sub(visible_rows - 1);
5874 }
5875 } else {
5876 let mut row_idx = 0;
5878 for bucket in &self.s3_state.buckets.items {
5879 if row_idx == self.s3_state.selected_row {
5880 self.s3_state.expanded_prefixes.remove(&bucket.name);
5882 break;
5883 }
5884 row_idx += 1;
5885 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5886 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5887 #[allow(clippy::too_many_arguments)]
5889 fn check_nested_collapse(
5890 objects: &[S3Object],
5891 row_idx: &mut usize,
5892 target_row: usize,
5893 expanded_prefixes: &mut std::collections::HashSet<String>,
5894 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5895 found: &mut bool,
5896 selected_row: &mut usize,
5897 parent_row: usize,
5898 ) {
5899 for obj in objects {
5900 let current_row = *row_idx;
5901 if *row_idx == target_row {
5902 if obj.is_prefix {
5904 if expanded_prefixes.contains(&obj.key) {
5905 expanded_prefixes.remove(&obj.key);
5907 *selected_row = parent_row;
5908 } else {
5909 *selected_row = parent_row;
5911 }
5912 } else {
5913 *selected_row = parent_row;
5915 }
5916 *found = true;
5917 return;
5918 }
5919 *row_idx += 1;
5920
5921 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5923 if let Some(nested) = prefix_preview.get(&obj.key) {
5924 check_nested_collapse(
5925 nested,
5926 row_idx,
5927 target_row,
5928 expanded_prefixes,
5929 prefix_preview,
5930 found,
5931 selected_row,
5932 current_row,
5933 );
5934 if *found {
5935 return;
5936 }
5937 } else {
5938 *row_idx += 1; }
5940 }
5941 }
5942 }
5943
5944 let mut found = false;
5945 let parent_row = row_idx - 1; check_nested_collapse(
5947 preview,
5948 &mut row_idx,
5949 self.s3_state.selected_row,
5950 &mut self.s3_state.expanded_prefixes,
5951 &self.s3_state.prefix_preview,
5952 &mut found,
5953 &mut self.s3_state.selected_row,
5954 parent_row,
5955 );
5956 if found {
5957 let visible_rows = self.s3_state.bucket_visible_rows.get();
5959 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5960 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5961 } else if self.s3_state.selected_row
5962 >= self.s3_state.bucket_scroll_offset + visible_rows
5963 {
5964 self.s3_state.bucket_scroll_offset =
5965 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
5966 }
5967 return;
5968 }
5969 } else {
5970 row_idx += 1;
5971 }
5972 }
5973 }
5974
5975 let visible_rows = self.s3_state.bucket_visible_rows.get();
5977 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5978 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5979 } else if self.s3_state.selected_row
5980 >= self.s3_state.bucket_scroll_offset + visible_rows
5981 {
5982 self.s3_state.bucket_scroll_offset =
5983 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
5984 }
5985 }
5986 } else if self.view_mode == ViewMode::InsightsResults {
5987 self.insights_state.insights.results_horizontal_scroll = self
5989 .insights_state
5990 .insights
5991 .results_horizontal_scroll
5992 .saturating_sub(1);
5993 } else if self.current_service == Service::CloudWatchLogGroups
5994 && self.view_mode == ViewMode::List
5995 {
5996 if self.log_groups_state.log_groups.has_expanded_item() {
5998 self.log_groups_state.log_groups.collapse();
5999 }
6000 } else if self.current_service == Service::CloudWatchLogGroups
6001 && self.view_mode == ViewMode::Detail
6002 {
6003 if self.log_groups_state.expanded_stream.is_some() {
6005 self.log_groups_state.expanded_stream = None;
6006 }
6007 } else if self.view_mode == ViewMode::Events {
6008 if self.log_groups_state.expanded_event.is_some() {
6010 self.log_groups_state.expanded_event = None;
6011 }
6012 } else if self.current_service == Service::CloudWatchAlarms {
6013 self.alarms_state.table.collapse();
6015 } else if self.current_service == Service::Ec2Instances {
6016 crate::ec2::actions::prev_pane(self);
6017 } else if self.current_service == Service::ApiGatewayApis {
6018 self.apig_state.apis.collapse();
6019 } else if self.current_service == Service::EcrRepositories {
6020 crate::ecr::actions::prev_pane(self);
6021 } else if self.current_service == Service::SqsQueues {
6022 if self.sqs_state.current_queue.is_some()
6023 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6024 {
6025 self.sqs_state.triggers.collapse();
6026 } else if self.sqs_state.current_queue.is_some()
6027 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6028 {
6029 self.sqs_state.pipes.collapse();
6030 } else if self.sqs_state.current_queue.is_some()
6031 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6032 {
6033 self.sqs_state.tags.collapse();
6034 } else if self.sqs_state.current_queue.is_some()
6035 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6036 {
6037 self.sqs_state.subscriptions.collapse();
6038 } else {
6039 self.sqs_state.queues.collapse();
6040 }
6041 } else if self.current_service == Service::LambdaFunctions {
6042 crate::lambda::functions::prev_pane(self);
6043 } else if self.current_service == Service::LambdaApplications {
6044 crate::lambda::applications::prev_pane(self);
6045 } else if self.current_service == Service::CloudFormationStacks {
6046 crate::cfn::actions::prev_pane(self);
6047 } else if self.current_service == Service::IamUsers {
6048 if self.iam_state.users.has_expanded_item() {
6049 self.iam_state.users.collapse();
6050 }
6051 } else if self.current_service == Service::IamRoles {
6052 if self.view_mode == ViewMode::PolicyView {
6053 self.view_mode = ViewMode::Detail;
6055 self.iam_state.current_policy = None;
6056 self.iam_state.policy_document.clear();
6057 self.iam_state.policy_scroll = 0;
6058 } else if self.iam_state.current_role.is_some() {
6059 if self.iam_state.role_tab == RoleTab::Tags
6060 && self.iam_state.tags.has_expanded_item()
6061 {
6062 self.iam_state.tags.collapse();
6063 } else if self.iam_state.role_tab == RoleTab::LastAccessed
6064 && self
6065 .iam_state
6066 .last_accessed_services
6067 .expanded_item
6068 .is_some()
6069 {
6070 self.iam_state.last_accessed_services.collapse();
6071 } else if self.iam_state.policies.has_expanded_item() {
6072 self.iam_state.policies.collapse();
6073 }
6074 } else if self.iam_state.roles.has_expanded_item() {
6075 self.iam_state.roles.collapse();
6076 }
6077 } else if self.current_service == Service::IamUserGroups {
6078 if self.iam_state.current_group.is_some() {
6079 if self.iam_state.group_tab == GroupTab::Users
6080 && self.iam_state.group_users.has_expanded_item()
6081 {
6082 self.iam_state.group_users.collapse();
6083 } else if self.iam_state.group_tab == GroupTab::Permissions
6084 && self.iam_state.policies.has_expanded_item()
6085 {
6086 self.iam_state.policies.collapse();
6087 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6088 && self
6089 .iam_state
6090 .last_accessed_services
6091 .expanded_item
6092 .is_some()
6093 {
6094 self.iam_state.last_accessed_services.collapse();
6095 }
6096 } else if self.iam_state.groups.has_expanded_item() {
6097 self.iam_state.groups.collapse();
6098 }
6099 }
6100 }
6101
6102 fn collapse_row(&mut self) {
6103 match self.current_service {
6104 Service::S3Buckets => {
6105 if self.s3_state.current_bucket.is_none() {
6106 let filtered_buckets: Vec<_> = self
6108 .s3_state
6109 .buckets
6110 .items
6111 .iter()
6112 .filter(|b| {
6113 if self.s3_state.buckets.filter.is_empty() {
6114 true
6115 } else {
6116 b.name
6117 .to_lowercase()
6118 .contains(&self.s3_state.buckets.filter.to_lowercase())
6119 }
6120 })
6121 .collect();
6122
6123 let mut row_idx = 0;
6125
6126 for bucket in filtered_buckets {
6127 if row_idx == self.s3_state.selected_row {
6128 self.s3_state.expanded_prefixes.remove(&bucket.name);
6130 break;
6132 }
6133 row_idx += 1;
6134 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6135 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
6137 continue;
6139 }
6140 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6141 #[allow(clippy::too_many_arguments)]
6143 fn check_nested_collapse(
6144 objects: &[S3Object],
6145 row_idx: &mut usize,
6146 target_row: usize,
6147 expanded_prefixes: &mut std::collections::HashSet<String>,
6148 prefix_preview: &std::collections::HashMap<
6149 String,
6150 Vec<S3Object>,
6151 >,
6152 found: &mut bool,
6153 selected_row: &mut usize,
6154 parent_row: usize,
6155 ) {
6156 for obj in objects {
6157 let current_row = *row_idx;
6158 if *row_idx == target_row {
6159 if obj.is_prefix {
6161 if expanded_prefixes.contains(&obj.key) {
6162 expanded_prefixes.remove(&obj.key);
6164 *selected_row = parent_row;
6165 } else {
6166 *selected_row = parent_row;
6168 }
6169 } else {
6170 *selected_row = parent_row;
6172 }
6173 *found = true;
6174 return;
6175 }
6176 *row_idx += 1;
6177
6178 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6180 if let Some(nested) = prefix_preview.get(&obj.key) {
6181 check_nested_collapse(
6182 nested,
6183 row_idx,
6184 target_row,
6185 expanded_prefixes,
6186 prefix_preview,
6187 found,
6188 selected_row,
6189 current_row,
6190 );
6191 if *found {
6192 return;
6193 }
6194 } else {
6195 *row_idx += 1; }
6197 }
6198 }
6199 }
6200
6201 let mut found = false;
6202 let parent_row = row_idx - 1; check_nested_collapse(
6204 preview,
6205 &mut row_idx,
6206 self.s3_state.selected_row,
6207 &mut self.s3_state.expanded_prefixes,
6208 &self.s3_state.prefix_preview,
6209 &mut found,
6210 &mut self.s3_state.selected_row,
6211 parent_row,
6212 );
6213 if found {
6214 let visible_rows = self.s3_state.bucket_visible_rows.get();
6216 if self.s3_state.selected_row
6217 < self.s3_state.bucket_scroll_offset
6218 {
6219 self.s3_state.bucket_scroll_offset =
6220 self.s3_state.selected_row;
6221 } else if self.s3_state.selected_row
6222 >= self.s3_state.bucket_scroll_offset + visible_rows
6223 {
6224 self.s3_state.bucket_scroll_offset = self
6225 .s3_state
6226 .selected_row
6227 .saturating_sub(visible_rows - 1);
6228 }
6229 return;
6230 }
6231 } else {
6232 row_idx += 1;
6233 }
6234 }
6235 }
6236
6237 let visible_rows = self.s3_state.bucket_visible_rows.get();
6239 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6240 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6241 } else if self.s3_state.selected_row
6242 >= self.s3_state.bucket_scroll_offset + visible_rows
6243 {
6244 self.s3_state.bucket_scroll_offset =
6245 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6246 }
6247 }
6248 }
6249 Service::CloudWatchLogGroups => {
6250 if self.view_mode == ViewMode::Events {
6251 if let Some(idx) = self.log_groups_state.expanded_event {
6252 self.log_groups_state.expanded_event = None;
6253 self.log_groups_state.selected_event = idx;
6254 }
6255 } else if self.view_mode == ViewMode::Detail {
6256 if let Some(idx) = self.log_groups_state.expanded_stream {
6257 self.log_groups_state.expanded_stream = None;
6258 self.log_groups_state.selected_stream = idx;
6259 }
6260 } else {
6261 self.log_groups_state.log_groups.collapse();
6262 }
6263 }
6264 Service::CloudWatchAlarms => self.alarms_state.table.collapse(),
6265 Service::Ec2Instances => {
6266 crate::ec2::actions::collapse_row(self);
6267 }
6268 Service::EcrRepositories => {
6269 crate::ecr::actions::collapse_row(self);
6270 }
6271 Service::LambdaFunctions => crate::lambda::functions::collapse_row(self),
6272 Service::LambdaApplications => crate::lambda::applications::prev_pane(self),
6273 Service::SqsQueues => self.sqs_state.queues.collapse(),
6274 Service::CloudFormationStacks => {
6275 crate::cfn::actions::collapse_row(self);
6276 }
6277 Service::IamUsers => {
6278 if self.iam_state.current_user.is_some() {
6279 match self.iam_state.user_tab {
6280 crate::ui::iam::UserTab::Permissions => {
6281 self.iam_state.policies.collapse();
6282 }
6283 crate::ui::iam::UserTab::Groups => {
6284 self.iam_state.user_group_memberships.collapse();
6285 }
6286 crate::ui::iam::UserTab::Tags => {
6287 self.iam_state.user_tags.collapse();
6288 }
6289 _ => {}
6290 }
6291 } else {
6292 self.iam_state.users.collapse();
6293 }
6294 }
6295 Service::IamRoles => {
6296 if self.iam_state.current_role.is_some() {
6297 match self.iam_state.role_tab {
6298 crate::ui::iam::RoleTab::Permissions => {
6299 self.iam_state.policies.collapse();
6300 }
6301 crate::ui::iam::RoleTab::Tags => {
6302 self.iam_state.tags.collapse();
6303 }
6304 _ => {}
6305 }
6306 } else {
6307 self.iam_state.roles.collapse();
6308 }
6309 }
6310 Service::IamUserGroups => self.iam_state.groups.collapse(),
6311 Service::ApiGatewayApis => {
6312 if let Some(api) = &self.apig_state.current_api {
6313 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
6314 let protocol = api.protocol_type.to_uppercase();
6315 if protocol == "REST" {
6316 let (filtered_items, _filtered_children) =
6318 crate::ui::apig::filter_tree_items(
6319 &self.apig_state.resources.items,
6320 &self.apig_state.resource_children,
6321 &self.apig_state.route_filter,
6322 );
6323
6324 let selected_row = self.apig_state.resources.selected;
6325 let mut current_row = 0;
6326 if let Some(resource_id) = self.find_resource_at_row(
6327 &filtered_items,
6328 selected_row,
6329 &mut current_row,
6330 ) {
6331 if self.apig_state.expanded_resources.contains(&resource_id) {
6332 self.apig_state.expanded_resources.remove(&resource_id);
6333 } else if let Some(parent_row) =
6334 self.find_resource_parent_row(&filtered_items, &resource_id)
6335 {
6336 self.apig_state.resources.selected = parent_row;
6337 }
6338 }
6339 } else {
6340 let (filtered_items, _filtered_children) =
6342 crate::ui::apig::filter_tree_items(
6343 &self.apig_state.routes.items,
6344 &self.apig_state.route_children,
6345 &self.apig_state.route_filter,
6346 );
6347
6348 let selected_row = self.apig_state.routes.selected;
6349 let mut current_row = 0;
6350 if let Some(route_key) = self.find_route_at_row(
6351 &filtered_items,
6352 selected_row,
6353 &mut current_row,
6354 ) {
6355 if self.apig_state.expanded_routes.contains(&route_key) {
6356 self.apig_state.expanded_routes.remove(&route_key);
6357 } else if let Some(parent_row) =
6358 self.find_parent_row(&filtered_items, &route_key)
6359 {
6360 self.apig_state.routes.selected = parent_row;
6361 }
6362 }
6363 }
6364 }
6365 } else {
6366 self.apig_state.apis.collapse();
6367 }
6368 }
6369 Service::CloudTrailEvents => {
6370 if self.cloudtrail_state.current_event.is_some()
6371 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
6372 {
6373 self.cloudtrail_state.resources_expanded_index = None;
6375 } else {
6376 self.cloudtrail_state.table.collapse();
6377 }
6378 }
6379 _ => {}
6380 }
6381 }
6382
6383 fn expand_row(&mut self) {
6384 match self.current_service {
6385 Service::S3Buckets => {
6386 if self.s3_state.current_bucket.is_none() {
6387 let filtered_buckets: Vec<_> = self
6389 .s3_state
6390 .buckets
6391 .items
6392 .iter()
6393 .filter(|b| {
6394 if self.s3_state.buckets.filter.is_empty() {
6395 true
6396 } else {
6397 b.name
6398 .to_lowercase()
6399 .contains(&self.s3_state.buckets.filter.to_lowercase())
6400 }
6401 })
6402 .collect();
6403
6404 fn check_nested_expand(
6406 objects: &[S3Object],
6407 row_idx: &mut usize,
6408 target_row: usize,
6409 expanded_prefixes: &mut std::collections::HashSet<String>,
6410 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6411 ) -> Option<(bool, usize)> {
6412 for obj in objects {
6413 if *row_idx == target_row {
6414 if obj.is_prefix {
6415 if expanded_prefixes.contains(&obj.key) {
6417 expanded_prefixes.remove(&obj.key);
6418 return Some((true, *row_idx));
6419 } else {
6420 expanded_prefixes.insert(obj.key.clone());
6421 return Some((true, *row_idx + 1)); }
6423 }
6424 return Some((false, *row_idx));
6425 }
6426 *row_idx += 1;
6427
6428 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6430 if let Some(nested) = prefix_preview.get(&obj.key) {
6431 if let Some(result) = check_nested_expand(
6432 nested,
6433 row_idx,
6434 target_row,
6435 expanded_prefixes,
6436 prefix_preview,
6437 ) {
6438 return Some(result);
6439 }
6440 }
6441 }
6442 }
6443 None
6444 }
6445
6446 let mut row_idx = 0;
6447 for bucket in filtered_buckets {
6448 if row_idx == self.s3_state.selected_row {
6449 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6450 if let Some(preview) =
6452 self.s3_state.bucket_preview.get(&bucket.name)
6453 {
6454 if !preview.is_empty() {
6455 self.s3_state.selected_row = row_idx + 1;
6456 let visible = self.s3_state.bucket_visible_rows.get();
6457 if self.s3_state.selected_row
6458 >= self.s3_state.bucket_scroll_offset + visible
6459 {
6460 self.s3_state.bucket_scroll_offset = self
6461 .s3_state
6462 .selected_row
6463 .saturating_sub(visible - 1);
6464 }
6465 }
6466 } else {
6468 }
6470 } else {
6471 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
6472 self.s3_state.buckets.loading = true;
6473 if let Some(preview) =
6475 self.s3_state.bucket_preview.get(&bucket.name)
6476 {
6477 if !preview.is_empty() {
6478 self.s3_state.selected_row = row_idx + 1;
6479 let visible = self.s3_state.bucket_visible_rows.get();
6480 if self.s3_state.selected_row
6481 >= self.s3_state.bucket_scroll_offset + visible
6482 {
6483 self.s3_state.bucket_scroll_offset = self
6484 .s3_state
6485 .selected_row
6486 .saturating_sub(visible - 1);
6487 }
6488 }
6489 }
6490 }
6491 return;
6492 }
6493 row_idx += 1;
6494
6495 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6496 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
6497 continue;
6498 }
6499 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6500 if let Some((loading, new_row)) = check_nested_expand(
6501 preview,
6502 &mut row_idx,
6503 self.s3_state.selected_row,
6504 &mut self.s3_state.expanded_prefixes,
6505 &self.s3_state.prefix_preview,
6506 ) {
6507 self.s3_state.selected_row = new_row;
6508 if loading {
6509 self.s3_state.buckets.loading = true;
6510 }
6511 let visible = self.s3_state.bucket_visible_rows.get();
6513 if self.s3_state.selected_row
6514 >= self.s3_state.bucket_scroll_offset + visible
6515 {
6516 self.s3_state.bucket_scroll_offset =
6517 self.s3_state.selected_row.saturating_sub(visible - 1);
6518 } else if self.s3_state.selected_row
6519 < self.s3_state.bucket_scroll_offset
6520 {
6521 self.s3_state.bucket_scroll_offset =
6522 self.s3_state.selected_row;
6523 }
6524 return;
6525 }
6526 }
6527 }
6528 }
6529 } else {
6530 fn check_object_expand(
6532 objects: &[S3Object],
6533 row_idx: &mut usize,
6534 target_row: usize,
6535 expanded_prefixes: &mut std::collections::HashSet<String>,
6536 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6537 ) -> Option<(bool, usize)> {
6538 for obj in objects {
6539 if *row_idx == target_row {
6540 if obj.is_prefix {
6541 if expanded_prefixes.contains(&obj.key) {
6542 expanded_prefixes.remove(&obj.key);
6543 return Some((true, *row_idx));
6544 } else {
6545 expanded_prefixes.insert(obj.key.clone());
6546 return Some((true, *row_idx + 1));
6547 }
6548 }
6549 return Some((false, *row_idx));
6550 }
6551 *row_idx += 1;
6552
6553 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6554 if let Some(nested) = prefix_preview.get(&obj.key) {
6555 if let Some(result) = check_object_expand(
6556 nested,
6557 row_idx,
6558 target_row,
6559 expanded_prefixes,
6560 prefix_preview,
6561 ) {
6562 return Some(result);
6563 }
6564 }
6565 }
6566 }
6567 None
6568 }
6569
6570 let mut row_idx = 0;
6571 if let Some((loading, new_row)) = check_object_expand(
6572 &self.s3_state.objects,
6573 &mut row_idx,
6574 self.s3_state.selected_object,
6575 &mut self.s3_state.expanded_prefixes,
6576 &self.s3_state.prefix_preview,
6577 ) {
6578 self.s3_state.selected_object = new_row;
6579 if loading {
6580 self.s3_state.buckets.loading = true;
6581 }
6582 }
6583 }
6584 }
6585 Service::ApiGatewayApis => {
6586 if let Some(api) = &self.apig_state.current_api {
6587 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
6588 let protocol = api.protocol_type.to_uppercase();
6589 if protocol == "REST" {
6590 let (filtered_items, filtered_children) =
6592 crate::ui::apig::filter_tree_items(
6593 &self.apig_state.resources.items,
6594 &self.apig_state.resource_children,
6595 &self.apig_state.route_filter,
6596 );
6597
6598 let selected_row = self.apig_state.resources.selected;
6599 let mut current_row = 0;
6600 if let Some(resource_id) = self.find_resource_at_row(
6601 &filtered_items,
6602 selected_row,
6603 &mut current_row,
6604 ) {
6605 if self.apig_state.expanded_resources.contains(&resource_id) {
6606 let total_rows =
6607 crate::ui::tree::TreeRenderer::count_visible_rows(
6608 &filtered_items,
6609 &self.apig_state.expanded_resources,
6610 &filtered_children,
6611 );
6612 if selected_row + 1 < total_rows {
6613 self.apig_state.resources.selected = selected_row + 1;
6614 }
6615 } else {
6616 self.apig_state.expanded_resources.insert(resource_id);
6617 }
6618 }
6619 } else {
6620 let (filtered_items, filtered_children) =
6622 crate::ui::apig::filter_tree_items(
6623 &self.apig_state.routes.items,
6624 &self.apig_state.route_children,
6625 &self.apig_state.route_filter,
6626 );
6627
6628 let selected_row = self.apig_state.routes.selected;
6629 let mut current_row = 0;
6630 if let Some(route_key) = self.find_route_at_row(
6631 &filtered_items,
6632 selected_row,
6633 &mut current_row,
6634 ) {
6635 if self.apig_state.expanded_routes.contains(&route_key) {
6636 let total_rows =
6637 crate::ui::tree::TreeRenderer::count_visible_rows(
6638 &filtered_items,
6639 &self.apig_state.expanded_routes,
6640 &filtered_children,
6641 );
6642 if selected_row + 1 < total_rows {
6643 self.apig_state.routes.selected = selected_row + 1;
6644 }
6645 } else {
6646 self.apig_state.expanded_routes.insert(route_key);
6647 }
6648 }
6649 }
6650 }
6651 } else {
6652 self.apig_state.apis.expand();
6653 }
6654 }
6655 Service::CloudTrailEvents => {
6656 if self.cloudtrail_state.current_event.is_some()
6657 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
6658 {
6659 self.cloudtrail_state.resources_expanded_index = Some(0);
6661 } else {
6662 self.cloudtrail_state.table.expand();
6663 }
6664 }
6665 Service::CloudFormationStacks => {
6666 crate::cfn::actions::expand_row(self);
6667 }
6668 _ => {
6669 self.next_pane();
6671 }
6672 }
6673 }
6674
6675 fn find_route_at_row(
6676 &self,
6677 routes: &[Route],
6678 target_row: usize,
6679 current_row: &mut usize,
6680 ) -> Option<String> {
6681 for route in routes {
6682 if *current_row == target_row {
6683 return Some(route.route_key.clone());
6684 }
6685 *current_row += 1;
6686
6687 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
6689 if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
6690 if let Some(key) = self.find_route_at_row(children, target_row, current_row) {
6691 return Some(key);
6692 }
6693 }
6694 }
6695 }
6696 None
6697 }
6698
6699 fn find_route_id_at_row_with_children(
6700 &self,
6701 routes: &[Route],
6702 children_map: &HashMap<String, Vec<Route>>,
6703 target_row: usize,
6704 current_row: &mut usize,
6705 ) -> Option<String> {
6706 for route in routes {
6707 if *current_row == target_row {
6708 if !route.target.is_empty() {
6710 return Some(route.route_id.clone());
6711 } else {
6712 return None;
6713 }
6714 }
6715 *current_row += 1;
6716
6717 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
6719 if let Some(children) = children_map.get(&route.route_key) {
6720 if let Some(id) = self.find_route_id_at_row_with_children(
6721 children,
6722 children_map,
6723 target_row,
6724 current_row,
6725 ) {
6726 return Some(id);
6727 }
6728 }
6729 }
6730 }
6731 None
6732 }
6733
6734 fn find_parent_row(&self, routes: &[Route], child_key: &str) -> Option<usize> {
6735 let mut current_row = 0;
6736 self.find_parent_row_recursive(routes, child_key, &mut current_row)
6737 }
6738
6739 fn find_parent_row_recursive(
6740 &self,
6741 routes: &[Route],
6742 child_key: &str,
6743 current_row: &mut usize,
6744 ) -> Option<usize> {
6745 for route in routes {
6746 let parent_row = *current_row;
6747 *current_row += 1;
6748
6749 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
6751 if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
6752 for child in children {
6754 if child.route_key == child_key {
6755 return Some(parent_row);
6756 }
6757 }
6758
6759 if let Some(row) =
6761 self.find_parent_row_recursive(children, child_key, current_row)
6762 {
6763 return Some(row);
6764 }
6765 }
6766 }
6767 }
6768 None
6769 }
6770
6771 fn find_resource_at_row(
6772 &self,
6773 resources: &[ApigResource],
6774 target_row: usize,
6775 current_row: &mut usize,
6776 ) -> Option<String> {
6777 for resource in resources {
6778 if *current_row == target_row {
6779 return Some(resource.id.clone());
6780 }
6781 *current_row += 1;
6782
6783 if self.apig_state.expanded_resources.contains(&resource.id) {
6784 if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
6785 if let Some(id) = self.find_resource_at_row(children, target_row, current_row) {
6786 return Some(id);
6787 }
6788 }
6789 }
6790 }
6791 None
6792 }
6793
6794 fn find_resource_parent_row(
6795 &self,
6796 resources: &[ApigResource],
6797 child_id: &str,
6798 ) -> Option<usize> {
6799 let mut current_row = 0;
6800 self.find_resource_parent_row_recursive(resources, child_id, &mut current_row)
6801 }
6802
6803 fn find_resource_parent_row_recursive(
6804 &self,
6805 resources: &[ApigResource],
6806 child_id: &str,
6807 current_row: &mut usize,
6808 ) -> Option<usize> {
6809 for resource in resources {
6810 let parent_row = *current_row;
6811 *current_row += 1;
6812
6813 if self.apig_state.expanded_resources.contains(&resource.id) {
6814 if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
6815 for child in children {
6816 if child.id == child_id {
6817 return Some(parent_row);
6818 }
6819 }
6820
6821 if let Some(row) =
6822 self.find_resource_parent_row_recursive(children, child_id, current_row)
6823 {
6824 return Some(row);
6825 }
6826 }
6827 }
6828 }
6829 None
6830 }
6831
6832 fn select_item(&mut self) {
6833 if self.mode == Mode::RegionPicker {
6834 if self.region_filter_active {
6836 self.region_filter_active = false;
6837 return;
6838 }
6839 let filtered = self.get_filtered_regions();
6840 if let Some(region) = filtered.get(self.region_picker_selected) {
6841 if !self.tabs.is_empty() {
6843 let mut session = Session::new(
6844 self.profile.clone(),
6845 self.region.clone(),
6846 self.config.account_id.clone(),
6847 self.config.role_arn.clone(),
6848 );
6849
6850 for tab in &self.tabs {
6851 session.tabs.push(SessionTab {
6852 service: format!("{:?}", tab.service),
6853 title: tab.title.clone(),
6854 breadcrumb: tab.breadcrumb.clone(),
6855 filter: None,
6856 selected_item: None,
6857 });
6858 }
6859
6860 let _ = session.save();
6861 }
6862
6863 self.region = region.code.to_string();
6864 self.config.region = region.code.to_string();
6865
6866 self.tabs.clear();
6868 self.current_tab = 0;
6869 self.service_selected = false;
6870
6871 self.mode = Mode::Normal;
6872 }
6873 } else if self.mode == Mode::ProfilePicker {
6874 let filtered = self.get_filtered_profiles();
6875 if let Some(profile) = filtered.get(self.profile_picker_selected) {
6876 let profile_name = profile.name.clone();
6877 let profile_region = profile.region.clone();
6878
6879 self.profile = profile_name.clone();
6880 std::env::set_var("AWS_PROFILE", &profile_name);
6881
6882 if let Some(region) = profile_region {
6884 self.region = region;
6885 }
6886
6887 self.mode = Mode::Normal;
6888 }
6890 } else if self.mode == Mode::ServicePicker {
6891 if self.service_picker.filter_active {
6893 self.service_picker.filter_active = false;
6894 return;
6895 }
6896
6897 let filtered = self.filtered_services();
6898 if let Some(&service) = filtered.get(self.service_picker.selected) {
6899 let new_service = match service {
6900 "API Gateway › APIs" => Service::ApiGatewayApis,
6901 "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
6902 "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
6903 "CloudWatch › Alarms" => Service::CloudWatchAlarms,
6904 "CloudTrail › Event History" => Service::CloudTrailEvents,
6905 "CloudFormation › Stacks" => Service::CloudFormationStacks,
6906 "EC2 › Instances" => Service::Ec2Instances,
6907 "ECR › Repositories" => Service::EcrRepositories,
6908 "IAM › Users" => Service::IamUsers,
6909 "IAM › Roles" => Service::IamRoles,
6910 "IAM › User Groups" => Service::IamUserGroups,
6911 "Lambda › Functions" => Service::LambdaFunctions,
6912 "Lambda › Applications" => Service::LambdaApplications,
6913 "S3 › Buckets" => Service::S3Buckets,
6914 "SQS › Queues" => Service::SqsQueues,
6915 _ => return,
6916 };
6917
6918 self.tabs.push(Tab {
6920 service: new_service,
6921 title: service.to_string(),
6922 breadcrumb: service.to_string(),
6923 });
6924 self.current_tab = self.tabs.len() - 1;
6925 self.current_service = new_service;
6926 self.view_mode = ViewMode::List;
6927 self.service_selected = true;
6928 self.mode = Mode::Normal;
6929 }
6930 } else if self.mode == Mode::TabPicker {
6931 let filtered = self.get_filtered_tabs();
6932 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
6933 self.current_tab = idx;
6934 self.current_service = self.tabs[idx].service;
6935 self.mode = Mode::Normal;
6936 self.tab_filter.clear();
6937 }
6938 } else if self.mode == Mode::SessionPicker {
6939 let filtered = self.get_filtered_sessions();
6940 if let Some(&session) = filtered.get(self.session_picker_selected) {
6941 let session = session.clone();
6942
6943 self.current_session = Some(session.clone());
6945 self.profile = session.profile.clone();
6946 self.region = session.region.clone();
6947 self.config.region = session.region.clone();
6948 self.config.account_id = session.account_id.clone();
6949 self.config.role_arn = session.role_arn.clone();
6950
6951 self.tabs = session
6953 .tabs
6954 .iter()
6955 .map(|st| Tab {
6956 service: match st.service.as_str() {
6957 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
6958 "CloudWatchInsights" => Service::CloudWatchInsights,
6959 "CloudWatchAlarms" => Service::CloudWatchAlarms,
6960 "S3Buckets" => Service::S3Buckets,
6961 "CloudTrailEvents" => Service::CloudTrailEvents,
6962 "SqsQueues" => Service::SqsQueues,
6963 _ => Service::CloudWatchLogGroups,
6964 },
6965 title: st.title.clone(),
6966 breadcrumb: st.breadcrumb.clone(),
6967 })
6968 .collect();
6969
6970 if !self.tabs.is_empty() {
6971 self.current_tab = 0;
6972 self.current_service = self.tabs[0].service;
6973 self.service_selected = true;
6974 }
6975
6976 self.mode = Mode::Normal;
6977 }
6978 } else if self.mode == Mode::InsightsInput {
6979 use crate::app::InsightsFocus;
6981 match self.insights_state.insights.insights_focus {
6982 InsightsFocus::Query => {
6983 self.insights_state.insights.query_text.push('\n');
6985 self.insights_state.insights.query_cursor_line += 1;
6986 self.insights_state.insights.query_cursor_col = 0;
6987 }
6988 InsightsFocus::LogGroupSearch => {
6989 self.insights_state.insights.show_dropdown =
6991 !self.insights_state.insights.show_dropdown;
6992 }
6993 _ => {}
6994 }
6995 } else if self.mode == Mode::Normal {
6996 if !self.service_selected {
6998 let filtered = self.filtered_services();
6999 if let Some(&service) = filtered.get(self.service_picker.selected) {
7000 match service {
7001 "CloudWatch › Log Groups" => {
7002 self.current_service = Service::CloudWatchLogGroups;
7003 self.view_mode = ViewMode::List;
7004 self.service_selected = true;
7005 }
7006 "CloudWatch › Logs Insights" => {
7007 self.current_service = Service::CloudWatchInsights;
7008 self.view_mode = ViewMode::InsightsResults;
7009 self.service_selected = true;
7010 }
7011 "CloudWatch › Alarms" => {
7012 self.current_service = Service::CloudWatchAlarms;
7013 self.view_mode = ViewMode::List;
7014 self.service_selected = true;
7015 }
7016 "S3 › Buckets" => {
7017 self.current_service = Service::S3Buckets;
7018 self.view_mode = ViewMode::List;
7019 self.service_selected = true;
7020 }
7021 "EC2 › Instances" => {
7022 self.current_service = Service::Ec2Instances;
7023 self.view_mode = ViewMode::List;
7024 self.service_selected = true;
7025 }
7026 "ECR › Repositories" => {
7027 self.current_service = Service::EcrRepositories;
7028 self.view_mode = ViewMode::List;
7029 self.service_selected = true;
7030 }
7031 "Lambda › Functions" => {
7032 self.current_service = Service::LambdaFunctions;
7033 self.view_mode = ViewMode::List;
7034 self.service_selected = true;
7035 }
7036 "Lambda › Applications" => {
7037 self.current_service = Service::LambdaApplications;
7038 self.view_mode = ViewMode::List;
7039 self.service_selected = true;
7040 }
7041 _ => {}
7042 }
7043 }
7044 return;
7045 }
7046
7047 if self.view_mode == ViewMode::InsightsResults {
7049 if self.insights_state.insights.expanded_result
7051 == Some(self.insights_state.insights.results_selected)
7052 {
7053 self.insights_state.insights.expanded_result = None;
7054 } else {
7055 self.insights_state.insights.expanded_result =
7056 Some(self.insights_state.insights.results_selected);
7057 }
7058 } else if self.current_service == Service::S3Buckets {
7059 if self.s3_state.current_bucket.is_none() {
7060 let filtered_buckets: Vec<_> = self
7062 .s3_state
7063 .buckets
7064 .items
7065 .iter()
7066 .filter(|b| {
7067 if self.s3_state.buckets.filter.is_empty() {
7068 true
7069 } else {
7070 b.name
7071 .to_lowercase()
7072 .contains(&self.s3_state.buckets.filter.to_lowercase())
7073 }
7074 })
7075 .collect();
7076
7077 let mut row_idx = 0;
7079 for bucket in filtered_buckets {
7080 if row_idx == self.s3_state.selected_row {
7081 self.s3_state.current_bucket = Some(bucket.name.clone());
7083 self.s3_state.prefix_stack.clear();
7084 self.s3_state.buckets.loading = true;
7085 return;
7086 }
7087 row_idx += 1;
7088
7089 if self.s3_state.bucket_errors.contains_key(&bucket.name)
7091 && self.s3_state.expanded_prefixes.contains(&bucket.name)
7092 {
7093 continue;
7094 }
7095
7096 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7097 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7098 for obj in preview {
7099 if row_idx == self.s3_state.selected_row {
7100 if obj.is_prefix {
7102 self.s3_state.current_bucket =
7103 Some(bucket.name.clone());
7104 self.s3_state.prefix_stack = vec![obj.key.clone()];
7105 self.s3_state.buckets.loading = true;
7106 }
7107 return;
7108 }
7109 row_idx += 1;
7110
7111 if obj.is_prefix
7113 && self.s3_state.expanded_prefixes.contains(&obj.key)
7114 {
7115 if let Some(nested) =
7116 self.s3_state.prefix_preview.get(&obj.key)
7117 {
7118 for nested_obj in nested {
7119 if row_idx == self.s3_state.selected_row {
7120 if nested_obj.is_prefix {
7122 self.s3_state.current_bucket =
7123 Some(bucket.name.clone());
7124 self.s3_state.prefix_stack = vec![
7126 obj.key.clone(),
7127 nested_obj.key.clone(),
7128 ];
7129 self.s3_state.buckets.loading = true;
7130 }
7131 return;
7132 }
7133 row_idx += 1;
7134 }
7135 } else {
7136 row_idx += 1;
7137 }
7138 }
7139 }
7140 } else {
7141 row_idx += 1;
7142 }
7143 }
7144 }
7145 } else {
7146 let mut visual_idx = 0;
7148 let mut found_obj: Option<S3Object> = None;
7149
7150 fn check_nested_select(
7152 obj: &S3Object,
7153 visual_idx: &mut usize,
7154 target_idx: usize,
7155 expanded_prefixes: &std::collections::HashSet<String>,
7156 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7157 found_obj: &mut Option<S3Object>,
7158 ) {
7159 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7160 if let Some(preview) = prefix_preview.get(&obj.key) {
7161 for nested_obj in preview {
7162 if *visual_idx == target_idx {
7163 *found_obj = Some(nested_obj.clone());
7164 return;
7165 }
7166 *visual_idx += 1;
7167
7168 check_nested_select(
7170 nested_obj,
7171 visual_idx,
7172 target_idx,
7173 expanded_prefixes,
7174 prefix_preview,
7175 found_obj,
7176 );
7177 if found_obj.is_some() {
7178 return;
7179 }
7180 }
7181 } else {
7182 *visual_idx += 1;
7184 }
7185 }
7186 }
7187
7188 for obj in &self.s3_state.objects {
7189 if visual_idx == self.s3_state.selected_object {
7190 found_obj = Some(obj.clone());
7191 break;
7192 }
7193 visual_idx += 1;
7194
7195 check_nested_select(
7197 obj,
7198 &mut visual_idx,
7199 self.s3_state.selected_object,
7200 &self.s3_state.expanded_prefixes,
7201 &self.s3_state.prefix_preview,
7202 &mut found_obj,
7203 );
7204 if found_obj.is_some() {
7205 break;
7206 }
7207 }
7208
7209 if let Some(obj) = found_obj {
7210 if obj.is_prefix {
7211 self.s3_state.prefix_stack.push(obj.key.clone());
7213 self.s3_state.buckets.loading = true;
7214 }
7215 }
7216 }
7217 } else if self.current_service == Service::ApiGatewayApis {
7218 if self.apig_state.current_api.is_none() {
7219 let filtered_apis = crate::ui::apig::filtered_apis(self);
7221 if let Some(api) = self.apig_state.apis.get_selected(&filtered_apis) {
7222 let protocol = api.protocol_type.to_uppercase();
7223 self.apig_state.current_api = Some((*api).clone());
7224 if protocol == "REST" {
7225 self.apig_state.resources.loading = true;
7226 } else {
7227 self.apig_state.routes.loading = true;
7228 }
7229 self.update_current_tab_breadcrumb();
7230 }
7231 }
7232 } else if self.current_service == Service::CloudFormationStacks {
7233 if self.cfn_state.current_stack.is_none() {
7234 let filtered_stacks = filtered_cloudformation_stacks(self);
7236 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
7237 let stack_name = stack.name.clone();
7238 let mut tags = stack.tags.clone();
7239 tags.sort_by(|a, b| a.0.cmp(&b.0));
7240
7241 self.cfn_state.current_stack = Some(stack_name);
7242 self.cfn_state.tags.items = tags;
7243 self.cfn_state.tags.reset();
7244 self.cfn_state.table.loading = true;
7245 self.update_current_tab_breadcrumb();
7246 }
7247 }
7248 } else if self.current_service == Service::CloudWatchAlarms {
7249 if self.alarms_state.current_alarm.is_none() {
7250 let filtered_alarms: Vec<_> = self.alarms_state.table.items.iter().collect();
7251 if let Some(alarm) = self.alarms_state.table.get_selected(&filtered_alarms) {
7252 self.alarms_state.current_alarm = Some(alarm.name.clone());
7253 self.alarms_state.metrics_loading = true;
7254 self.view_mode = ViewMode::Detail;
7255 self.update_current_tab_breadcrumb();
7256 }
7257 }
7258 } else if self.current_service == Service::CloudTrailEvents {
7259 if self.cloudtrail_state.current_event.is_none() {
7260 let filtered_events: Vec<_> =
7261 self.cloudtrail_state.table.items.iter().collect();
7262 if let Some(event) = self.cloudtrail_state.table.get_selected(&filtered_events)
7263 {
7264 self.cloudtrail_state.current_event = Some((*event).clone());
7265 self.cloudtrail_state.event_json_scroll = 0;
7266 self.update_current_tab_breadcrumb();
7267 }
7268 }
7269 } else if self.current_service == Service::EcrRepositories {
7270 crate::ecr::actions::select_item(self);
7271 } else if self.current_service == Service::Ec2Instances {
7272 crate::ec2::actions::select_item(self);
7273 } else if self.current_service == Service::SqsQueues {
7274 if self.sqs_state.current_queue.is_none() {
7275 let filtered_queues = filtered_queues(
7276 &self.sqs_state.queues.items,
7277 &self.sqs_state.queues.filter,
7278 );
7279 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
7280 self.sqs_state.current_queue = Some(queue.url.clone());
7281
7282 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
7283 self.sqs_state.metrics_loading = true;
7284 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
7285 self.sqs_state.triggers.loading = true;
7286 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
7287 self.sqs_state.pipes.loading = true;
7288 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
7289 self.sqs_state.tags.loading = true;
7290 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
7291 self.sqs_state.subscriptions.loading = true;
7292 }
7293 }
7294 }
7295 } else if self.current_service == Service::IamUsers {
7296 if self.iam_state.current_user.is_some() {
7297 if self.iam_state.user_tab == UserTab::Permissions {
7299 let filtered = filtered_iam_policies(self);
7300 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
7301 self.iam_state.current_policy = Some(policy.policy_name.clone());
7302 self.iam_state.policy_scroll = 0;
7303 self.view_mode = ViewMode::PolicyView;
7304 self.iam_state.policies.loading = true;
7305 self.update_current_tab_breadcrumb();
7306 }
7307 }
7308 } else if self.iam_state.current_user.is_none() {
7309 let filtered_users = filtered_iam_users(self);
7310 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
7311 self.iam_state.current_user = Some(user.user_name.clone());
7312 self.iam_state.user_tab = UserTab::Permissions;
7313 self.iam_state.policies.reset();
7314 self.update_current_tab_breadcrumb();
7315 }
7316 }
7317 } else if self.current_service == Service::IamRoles {
7318 if self.iam_state.current_role.is_some() {
7319 if self.iam_state.role_tab == RoleTab::Permissions {
7321 let filtered = filtered_iam_policies(self);
7322 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
7323 self.iam_state.current_policy = Some(policy.policy_name.clone());
7324 self.iam_state.policy_scroll = 0;
7325 self.view_mode = ViewMode::PolicyView;
7326 self.iam_state.policies.loading = true;
7327 self.update_current_tab_breadcrumb();
7328 }
7329 }
7330 } else if self.iam_state.current_role.is_none() {
7331 let filtered_roles = filtered_iam_roles(self);
7332 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
7333 self.iam_state.current_role = Some(role.role_name.clone());
7334 self.iam_state.role_tab = RoleTab::Permissions;
7335 self.iam_state.policies.reset();
7336 self.update_current_tab_breadcrumb();
7337 }
7338 }
7339 } else if self.current_service == Service::IamUserGroups {
7340 if self.iam_state.current_group.is_none() {
7341 let filtered_groups: Vec<_> = self
7342 .iam_state
7343 .groups
7344 .items
7345 .iter()
7346 .filter(|g| {
7347 if self.iam_state.groups.filter.is_empty() {
7348 true
7349 } else {
7350 g.group_name
7351 .to_lowercase()
7352 .contains(&self.iam_state.groups.filter.to_lowercase())
7353 }
7354 })
7355 .collect();
7356 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
7357 self.iam_state.current_group = Some(group.group_name.clone());
7358 self.update_current_tab_breadcrumb();
7359 }
7360 }
7361 } else if self.current_service == Service::LambdaFunctions {
7362 crate::lambda::functions::select_item(self);
7363 } else if self.current_service == Service::LambdaApplications {
7364 crate::lambda::applications::select_item(self);
7365 } else if self.current_service == Service::CloudWatchLogGroups {
7366 if self.view_mode == ViewMode::List {
7367 let filtered_groups = filtered_log_groups(self);
7369 if let Some(selected_group) =
7370 filtered_groups.get(self.log_groups_state.log_groups.selected)
7371 {
7372 if let Some(actual_idx) = self
7373 .log_groups_state
7374 .log_groups
7375 .items
7376 .iter()
7377 .position(|g| g.name == selected_group.name)
7378 {
7379 self.log_groups_state.log_groups.selected = actual_idx;
7380 }
7381 }
7382 self.view_mode = ViewMode::Detail;
7383 self.log_groups_state.log_streams.clear();
7384 self.log_groups_state.tags.items.clear();
7385 self.log_groups_state.tags.reset();
7386 self.log_groups_state.selected_stream = 0;
7387 self.log_groups_state.loading = true;
7388 self.column_selector_index = 0;
7389 self.update_current_tab_breadcrumb();
7390 } else if self.view_mode == ViewMode::Detail {
7391 let filtered_streams = filtered_log_streams(self);
7393 if let Some(selected_stream) =
7394 filtered_streams.get(self.log_groups_state.selected_stream)
7395 {
7396 if let Some(actual_idx) = self
7397 .log_groups_state
7398 .log_streams
7399 .iter()
7400 .position(|s| s.name == selected_stream.name)
7401 {
7402 self.log_groups_state.selected_stream = actual_idx;
7403 }
7404 }
7405 self.view_mode = ViewMode::Events;
7406 self.update_current_tab_breadcrumb();
7407 self.log_groups_state.log_events.clear();
7408 self.log_groups_state.event_scroll_offset = 0;
7409 self.log_groups_state.next_backward_token = None;
7410 self.log_groups_state.loading = true;
7411 } else if self.view_mode == ViewMode::Events {
7412 if self.log_groups_state.expanded_event
7414 == Some(self.log_groups_state.event_scroll_offset)
7415 {
7416 self.log_groups_state.expanded_event = None;
7417 } else {
7418 self.log_groups_state.expanded_event =
7419 Some(self.log_groups_state.event_scroll_offset);
7420 }
7421 }
7422 } else if self.current_service == Service::CloudWatchAlarms
7423 && self.view_mode != ViewMode::Detail
7424 {
7425 self.alarms_state.table.toggle_expand();
7427 } else if self.current_service == Service::CloudWatchInsights {
7428 if !self.insights_state.insights.selected_log_groups.is_empty() {
7430 self.log_groups_state.loading = true;
7431 self.insights_state.insights.query_completed = true;
7432 }
7433 }
7434 }
7435 }
7436
7437 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
7438 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
7439 Ok(())
7440 }
7441
7442 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
7443 let alarms = self.alarms_client.list_alarms().await?;
7444 self.alarms_state.table.items = alarms
7445 .into_iter()
7446 .map(
7447 |(
7448 name,
7449 state,
7450 state_updated,
7451 description,
7452 metric_name,
7453 namespace,
7454 statistic,
7455 period,
7456 comparison,
7457 threshold,
7458 actions_enabled,
7459 state_reason,
7460 resource,
7461 dimensions,
7462 expression,
7463 alarm_type,
7464 cross_account,
7465 )| Alarm {
7466 name,
7467 state,
7468 state_updated_timestamp: state_updated,
7469 description,
7470 metric_name,
7471 namespace,
7472 statistic,
7473 period,
7474 comparison_operator: comparison,
7475 threshold,
7476 actions_enabled,
7477 state_reason,
7478 resource,
7479 dimensions,
7480 expression,
7481 alarm_type,
7482 cross_account,
7483 },
7484 )
7485 .collect();
7486 Ok(())
7487 }
7488
7489 pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
7490 let (events, next_token) = self.cloudtrail_client.lookup_events(None, None).await?;
7491 self.cloudtrail_state.table.items = events
7492 .into_iter()
7493 .map(
7494 |(
7495 event_name,
7496 event_time,
7497 username,
7498 event_source,
7499 resource_type,
7500 resource_name,
7501 read_only,
7502 aws_region,
7503 event_id,
7504 access_key_id,
7505 source_ip_address,
7506 error_code,
7507 request_id,
7508 event_type,
7509 cloud_trail_event_json,
7510 )| CloudTrailEvent {
7511 event_name,
7512 event_time,
7513 username,
7514 event_source,
7515 resource_type,
7516 resource_name,
7517 read_only,
7518 aws_region,
7519 event_id,
7520 access_key_id,
7521 source_ip_address,
7522 error_code,
7523 request_id,
7524 event_type,
7525 cloud_trail_event_json,
7526 },
7527 )
7528 .collect();
7529 self.cloudtrail_state.table.next_token = next_token;
7530 Ok(())
7531 }
7532
7533 pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
7534 if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
7535 let (events, next_token) = self
7537 .cloudtrail_client
7538 .lookup_events(None, Some(token))
7539 .await?;
7540 self.cloudtrail_state
7541 .table
7542 .items
7543 .extend(events.into_iter().map(
7544 |(
7545 event_name,
7546 event_time,
7547 username,
7548 event_source,
7549 resource_type,
7550 resource_name,
7551 read_only,
7552 aws_region,
7553 event_id,
7554 access_key_id,
7555 source_ip_address,
7556 error_code,
7557 request_id,
7558 event_type,
7559 cloud_trail_event_json,
7560 )| CloudTrailEvent {
7561 event_name,
7562 event_time,
7563 username,
7564 event_source,
7565 resource_type,
7566 resource_name,
7567 read_only,
7568 aws_region,
7569 event_id,
7570 access_key_id,
7571 source_ip_address,
7572 error_code,
7573 request_id,
7574 event_type,
7575 cloud_trail_event_json,
7576 },
7577 ));
7578 self.cloudtrail_state.table.next_token = next_token;
7579 }
7580 Ok(())
7581 }
7582
7583 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
7584 if let Some(bucket_name) = &self.s3_state.current_bucket {
7585 let bucket_region = if let Some(bucket) = self
7587 .s3_state
7588 .buckets
7589 .items
7590 .iter_mut()
7591 .find(|b| &b.name == bucket_name)
7592 {
7593 if bucket.region.is_empty() {
7594 let region = self.s3_client.get_bucket_location(bucket_name).await?;
7596 bucket.region = region.clone();
7597 region
7598 } else {
7599 bucket.region.clone()
7600 }
7601 } else {
7602 self.config.region.clone()
7603 };
7604
7605 let prefix = self
7606 .s3_state
7607 .prefix_stack
7608 .last()
7609 .cloned()
7610 .unwrap_or_default();
7611 let objects = self
7612 .s3_client
7613 .list_objects(bucket_name, &bucket_region, &prefix)
7614 .await?;
7615 self.s3_state.objects = objects
7616 .into_iter()
7617 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
7618 key,
7619 size,
7620 last_modified: modified,
7621 is_prefix,
7622 storage_class,
7623 })
7624 .collect();
7625 self.s3_state.selected_object = 0;
7626 }
7627 Ok(())
7628 }
7629
7630 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
7631 let stored_region = self
7632 .s3_state
7633 .buckets
7634 .items
7635 .iter()
7636 .find(|b| b.name == bucket_name)
7637 .and_then(|b| {
7638 if b.region.is_empty() {
7639 None
7640 } else {
7641 Some(b.region.clone())
7642 }
7643 });
7644
7645 let bucket_region = match stored_region {
7646 Some(r) => r,
7647 None => {
7648 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
7649 if let Some(b) = self
7651 .s3_state
7652 .buckets
7653 .items
7654 .iter_mut()
7655 .find(|b| b.name == bucket_name)
7656 {
7657 b.region = r.clone();
7658 }
7659 r
7660 }
7661 };
7662 let objects = self
7663 .s3_client
7664 .list_objects(&bucket_name, &bucket_region, "")
7665 .await?;
7666 let preview: Vec<S3Object> = objects
7667 .into_iter()
7668 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
7669 key,
7670 size,
7671 last_modified: modified,
7672 is_prefix,
7673 storage_class,
7674 })
7675 .collect();
7676 self.s3_state
7677 .bucket_preview
7678 .insert(bucket_name.clone(), preview);
7679 self.after_bucket_preview_loaded(&bucket_name);
7680 Ok(())
7681 }
7682
7683 pub fn after_bucket_preview_loaded(&mut self, bucket_name: &str) {
7687 if self.s3_state.current_bucket.is_some() {
7688 return; }
7690 let mut row_idx = 0usize;
7692 for b in &self.s3_state.buckets.items {
7693 if b.name == bucket_name {
7694 if self.s3_state.selected_row == row_idx {
7696 if let Some(preview) = self.s3_state.bucket_preview.get(bucket_name) {
7697 if !preview.is_empty() {
7698 self.s3_state.selected_row = row_idx + 1;
7699 let visible = self.s3_state.bucket_visible_rows.get();
7700 if self.s3_state.selected_row
7701 >= self.s3_state.bucket_scroll_offset + visible
7702 {
7703 self.s3_state.bucket_scroll_offset =
7704 self.s3_state.selected_row.saturating_sub(visible - 1);
7705 }
7706 }
7707 }
7708 }
7709 return;
7710 }
7711 row_idx += 1;
7712 if self.s3_state.expanded_prefixes.contains(&b.name) {
7714 if let Some(p) = self.s3_state.bucket_preview.get(&b.name) {
7715 row_idx += p.len();
7716 }
7717 }
7718 }
7719 }
7720
7721 pub async fn load_prefix_preview(
7722 &mut self,
7723 bucket_name: String,
7724 prefix: String,
7725 ) -> anyhow::Result<()> {
7726 let stored_region = self
7727 .s3_state
7728 .buckets
7729 .items
7730 .iter()
7731 .find(|b| b.name == bucket_name)
7732 .and_then(|b| {
7733 if b.region.is_empty() {
7734 None
7735 } else {
7736 Some(b.region.clone())
7737 }
7738 });
7739
7740 let bucket_region = match stored_region {
7741 Some(r) => r,
7742 None => {
7743 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
7744 if let Some(b) = self
7745 .s3_state
7746 .buckets
7747 .items
7748 .iter_mut()
7749 .find(|b| b.name == bucket_name)
7750 {
7751 b.region = r.clone();
7752 }
7753 r
7754 }
7755 };
7756 let objects = self
7757 .s3_client
7758 .list_objects(&bucket_name, &bucket_region, &prefix)
7759 .await?;
7760 let preview: Vec<S3Object> = objects
7761 .into_iter()
7762 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
7763 key,
7764 size,
7765 last_modified: modified,
7766 is_prefix,
7767 storage_class,
7768 })
7769 .collect();
7770 self.s3_state.prefix_preview.insert(prefix, preview);
7771 Ok(())
7772 }
7773
7774 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
7775 let repos = match self.ecr_state.tab {
7776 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
7777 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
7778 };
7779
7780 self.ecr_state.repositories.items = repos
7781 .into_iter()
7782 .map(|r| EcrRepository {
7783 name: r.name,
7784 uri: r.uri,
7785 created_at: r.created_at,
7786 tag_immutability: r.tag_immutability,
7787 encryption_type: r.encryption_type,
7788 })
7789 .collect();
7790
7791 self.ecr_state
7792 .repositories
7793 .items
7794 .sort_by(|a, b| a.name.cmp(&b.name));
7795 Ok(())
7796 }
7797
7798 pub async fn load_apis(&mut self) -> anyhow::Result<()> {
7799 let apis = self.apig_client.list_rest_apis().await?;
7800
7801 self.apig_state.apis.items = apis
7802 .into_iter()
7803 .map(|a| crate::apig::api::RestApi {
7804 id: a.id,
7805 name: a.name,
7806 description: a.description,
7807 created_date: a.created_date,
7808 api_key_source: a.api_key_source,
7809 endpoint_configuration: a.endpoint_configuration,
7810 protocol_type: a.protocol_type,
7811 disable_execute_api_endpoint: a.disable_execute_api_endpoint,
7812 status: a.status,
7813 })
7814 .collect();
7815
7816 self.apig_state
7817 .apis
7818 .items
7819 .sort_by(|a, b| a.name.cmp(&b.name));
7820 Ok(())
7821 }
7822
7823 pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
7824 let instances = self.ec2_client.list_instances().await?;
7825
7826 self.ec2_state.table.items = instances
7827 .into_iter()
7828 .map(|i| Ec2Instance {
7829 instance_id: i.instance_id,
7830 name: i.name,
7831 state: i.state,
7832 instance_type: i.instance_type,
7833 availability_zone: i.availability_zone,
7834 public_ipv4_dns: i.public_ipv4_dns,
7835 public_ipv4_address: i.public_ipv4_address,
7836 elastic_ip: i.elastic_ip,
7837 ipv6_ips: i.ipv6_ips,
7838 monitoring: i.monitoring,
7839 security_groups: i.security_groups,
7840 key_name: i.key_name,
7841 launch_time: i.launch_time,
7842 platform_details: i.platform_details,
7843 status_checks: i.status_checks,
7844 alarm_status: i.alarm_status,
7845 private_dns_name: i.private_dns_name,
7846 private_ip_address: i.private_ip_address,
7847 security_group_ids: String::new(),
7848 owner_id: String::new(),
7849 volume_id: String::new(),
7850 root_device_name: String::new(),
7851 root_device_type: String::new(),
7852 ebs_optimized: String::new(),
7853 image_id: String::new(),
7854 kernel_id: String::new(),
7855 ramdisk_id: String::new(),
7856 ami_launch_index: String::new(),
7857 reservation_id: String::new(),
7858 vpc_id: String::new(),
7859 subnet_ids: String::new(),
7860 instance_lifecycle: String::new(),
7861 architecture: String::new(),
7862 virtualization_type: String::new(),
7863 platform: String::new(),
7864 iam_instance_profile_arn: String::new(),
7865 tenancy: String::new(),
7866 affinity: String::new(),
7867 host_id: String::new(),
7868 placement_group: String::new(),
7869 partition_number: String::new(),
7870 capacity_reservation_id: String::new(),
7871 state_transition_reason_code: String::new(),
7872 state_transition_reason_message: String::new(),
7873 stop_hibernation_behavior: String::new(),
7874 outpost_arn: String::new(),
7875 product_codes: String::new(),
7876 availability_zone_id: String::new(),
7877 imdsv2: String::new(),
7878 usage_operation: String::new(),
7879 managed: String::new(),
7880 operator: String::new(),
7881 })
7882 .collect();
7883
7884 self.ec2_state
7886 .table
7887 .items
7888 .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
7889 Ok(())
7890 }
7891
7892 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
7893 if let Some(repo_name) = &self.ecr_state.current_repository {
7894 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
7895 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
7896
7897 self.ecr_state.images.items = images
7898 .into_iter()
7899 .map(|i| EcrImage {
7900 tag: i.tag,
7901 artifact_type: i.artifact_type,
7902 pushed_at: i.pushed_at,
7903 size_bytes: i.size_bytes,
7904 uri: i.uri,
7905 digest: i.digest,
7906 last_pull_time: i.last_pull_time,
7907 })
7908 .collect();
7909
7910 self.ecr_state
7911 .images
7912 .items
7913 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
7914 }
7915 }
7916 Ok(())
7917 }
7918
7919 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
7920 let stacks = self
7921 .cloudformation_client
7922 .list_stacks(self.cfn_state.view_nested)
7923 .await?;
7924
7925 let mut stacks: Vec<CfnStack> = stacks
7926 .into_iter()
7927 .map(|s| CfnStack {
7928 name: s.name,
7929 stack_id: s.stack_id,
7930 status: s.status,
7931 created_time: s.created_time,
7932 updated_time: s.updated_time,
7933 deleted_time: s.deleted_time,
7934 drift_status: s.drift_status,
7935 last_drift_check_time: s.last_drift_check_time,
7936 status_reason: s.status_reason,
7937 description: s.description,
7938 detailed_status: String::new(),
7939 root_stack: String::new(),
7940 parent_stack: String::new(),
7941 termination_protection: false,
7942 iam_role: String::new(),
7943 tags: Vec::new(),
7944 stack_policy: String::new(),
7945 rollback_monitoring_time: String::new(),
7946 rollback_alarms: Vec::new(),
7947 notification_arns: Vec::new(),
7948 })
7949 .collect();
7950
7951 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
7953
7954 self.cfn_state.table.items = stacks;
7955
7956 Ok(())
7957 }
7958
7959 pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
7960 let template = self.cloudformation_client.get_template(stack_name).await?;
7961 self.cfn_state.template_body = template;
7962 self.cfn_state.template_scroll = 0;
7963 Ok(())
7964 }
7965
7966 pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
7967 let mut parameters = self
7968 .cloudformation_client
7969 .get_stack_parameters(stack_name)
7970 .await?;
7971 parameters.sort_by(|a, b| a.key.cmp(&b.key));
7972 self.cfn_state.parameters.items = parameters;
7973 self.cfn_state.parameters.reset();
7974 Ok(())
7975 }
7976
7977 pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
7978 let outputs = self
7979 .cloudformation_client
7980 .get_stack_outputs(stack_name)
7981 .await?;
7982 self.cfn_state.outputs.items = outputs;
7983 self.cfn_state.outputs.reset();
7984 Ok(())
7985 }
7986
7987 pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
7988 let resources = self
7989 .cloudformation_client
7990 .get_stack_resources(stack_name)
7991 .await?;
7992 self.cfn_state.resources.items = resources;
7993 self.cfn_state.resources.reset();
7994 Ok(())
7995 }
7996
7997 pub async fn load_cfn_events(&mut self, stack_name: &str) -> anyhow::Result<()> {
7998 let events = self
7999 .cloudformation_client
8000 .list_stack_events(stack_name)
8001 .await?;
8002 self.cfn_state.events.items = events;
8003 self.cfn_state.events.reset();
8004 Ok(())
8005 }
8006
8007 pub async fn load_cfn_change_sets(&mut self, stack_name: &str) -> anyhow::Result<()> {
8008 let change_sets = self
8009 .cloudformation_client
8010 .list_change_sets(stack_name)
8011 .await?;
8012 self.cfn_state.change_sets.items = change_sets;
8013 self.cfn_state.change_sets.reset();
8014 Ok(())
8015 }
8016
8017 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
8018 let attached_policies = self
8020 .iam_client
8021 .list_attached_role_policies(role_name)
8022 .await
8023 .map_err(|e| anyhow::anyhow!(e))?;
8024
8025 let mut policies: Vec<IamPolicy> = Vec::new();
8026 for p in attached_policies {
8027 let (creation_time, edited_time) = if let Some(arn) = p.policy_arn() {
8028 self.iam_client
8029 .get_policy_metadata(arn)
8030 .await
8031 .unwrap_or_else(|_| ("-".to_string(), "-".to_string()))
8032 } else {
8033 ("-".to_string(), "-".to_string())
8034 };
8035 policies.push(IamPolicy {
8036 policy_name: p.policy_name().unwrap_or("").to_string(),
8037 policy_type: "Managed".to_string(),
8038 attached_via: "Direct".to_string(),
8039 attached_entities: "-".to_string(),
8040 description: "-".to_string(),
8041 creation_time,
8042 edited_time,
8043 policy_arn: p.policy_arn().map(|s| s.to_string()),
8044 });
8045 }
8046
8047 let inline_policy_names = self
8049 .iam_client
8050 .list_role_policies(role_name)
8051 .await
8052 .map_err(|e| anyhow::anyhow!(e))?;
8053
8054 for policy_name in inline_policy_names {
8055 policies.push(IamPolicy {
8056 policy_name,
8057 policy_type: "Inline".to_string(),
8058 attached_via: "Direct".to_string(),
8059 attached_entities: "-".to_string(),
8060 description: "-".to_string(),
8061 creation_time: "-".to_string(),
8062 edited_time: "-".to_string(),
8063 policy_arn: None,
8064 });
8065 }
8066
8067 self.iam_state.policies.items = policies;
8068
8069 Ok(())
8070 }
8071
8072 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
8073 let attached_policies = self
8074 .iam_client
8075 .list_attached_group_policies(group_name)
8076 .await
8077 .map_err(|e| anyhow::anyhow!(e))?;
8078
8079 let mut policies: Vec<IamPolicy> = attached_policies
8080 .into_iter()
8081 .map(|p| IamPolicy {
8082 policy_name: p.policy_name().unwrap_or("").to_string(),
8083 policy_type: "AWS managed".to_string(),
8084 attached_via: "Direct".to_string(),
8085 attached_entities: "-".to_string(),
8086 description: "-".to_string(),
8087 creation_time: "-".to_string(),
8088 edited_time: "-".to_string(),
8089 policy_arn: p.policy_arn().map(|s| s.to_string()),
8090 })
8091 .collect();
8092
8093 let inline_policy_names = self
8094 .iam_client
8095 .list_group_policies(group_name)
8096 .await
8097 .map_err(|e| anyhow::anyhow!(e))?;
8098
8099 for policy_name in inline_policy_names {
8100 policies.push(IamPolicy {
8101 policy_name,
8102 policy_type: "Inline".to_string(),
8103 attached_via: "Direct".to_string(),
8104 attached_entities: "-".to_string(),
8105 description: "-".to_string(),
8106 creation_time: "-".to_string(),
8107 edited_time: "-".to_string(),
8108 policy_arn: None,
8109 });
8110 }
8111
8112 self.iam_state.policies.items = policies;
8113
8114 Ok(())
8115 }
8116
8117 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
8118 let users = self
8119 .iam_client
8120 .get_group_users(group_name)
8121 .await
8122 .map_err(|e| anyhow::anyhow!(e))?;
8123
8124 let group_users: Vec<IamGroupUser> = users
8125 .into_iter()
8126 .map(|u| {
8127 let creation_time = {
8128 let dt = u.create_date();
8129 let timestamp = dt.secs();
8130 let datetime =
8131 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
8132 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
8133 };
8134
8135 IamGroupUser {
8136 user_name: u.user_name().to_string(),
8137 groups: String::new(),
8138 last_activity: String::new(),
8139 creation_time,
8140 }
8141 })
8142 .collect();
8143
8144 self.iam_state.group_users.items = group_users;
8145
8146 Ok(())
8147 }
8148
8149 pub async fn load_policy_document(
8150 &mut self,
8151 role_name: &str,
8152 policy_name: &str,
8153 ) -> anyhow::Result<()> {
8154 let policy = self
8156 .iam_state
8157 .policies
8158 .items
8159 .iter()
8160 .find(|p| p.policy_name == policy_name)
8161 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
8162
8163 let document = if let Some(policy_arn) = &policy.policy_arn {
8164 self.iam_client
8166 .get_policy_version(policy_arn)
8167 .await
8168 .map_err(|e| anyhow::anyhow!(e))?
8169 } else {
8170 self.iam_client
8172 .get_role_policy(role_name, policy_name)
8173 .await
8174 .map_err(|e| anyhow::anyhow!(e))?
8175 };
8176
8177 self.iam_state.policy_document = document;
8178
8179 Ok(())
8180 }
8181
8182 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
8183 let document = self
8184 .iam_client
8185 .get_role(role_name)
8186 .await
8187 .map_err(|e| anyhow::anyhow!(e))?;
8188
8189 self.iam_state.trust_policy_document = document;
8190
8191 Ok(())
8192 }
8193
8194 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
8195 self.iam_state.last_accessed_services.items = vec![];
8197 self.iam_state.last_accessed_services.selected = 0;
8198
8199 Ok(())
8200 }
8201
8202 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
8203 let tags = self
8204 .iam_client
8205 .list_role_tags(role_name)
8206 .await
8207 .map_err(|e| anyhow::anyhow!(e))?;
8208 self.iam_state.tags.items = tags
8209 .into_iter()
8210 .map(|(k, v)| IamRoleTag { key: k, value: v })
8211 .collect();
8212 self.iam_state.tags.reset();
8213 Ok(())
8214 }
8215
8216 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
8217 let tags = self
8218 .iam_client
8219 .list_user_tags(user_name)
8220 .await
8221 .map_err(|e| anyhow::anyhow!(e))?;
8222 self.iam_state.user_tags.items = tags
8223 .into_iter()
8224 .map(|(k, v)| IamUserTag { key: k, value: v })
8225 .collect();
8226 self.iam_state.user_tags.reset();
8227 Ok(())
8228 }
8229
8230 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
8231 if let Some(group) = self
8232 .log_groups_state
8233 .log_groups
8234 .items
8235 .get(self.log_groups_state.log_groups.selected)
8236 {
8237 self.log_groups_state.log_streams =
8238 self.cloudwatch_client.list_log_streams(&group.name).await?;
8239 self.log_groups_state.selected_stream = 0;
8240 }
8241 Ok(())
8242 }
8243
8244 pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
8245 if let Some(group) = self
8246 .log_groups_state
8247 .log_groups
8248 .items
8249 .get(self.log_groups_state.log_groups.selected)
8250 {
8251 let arn = if let Some(arn) = &group.log_group_arn {
8253 arn.clone()
8254 } else if let Some(arn) = &group.arn {
8255 arn.clone()
8256 } else {
8257 let account_id = if self.config.account_id.is_empty() {
8259 "*"
8260 } else {
8261 &self.config.account_id
8262 };
8263 format!(
8264 "arn:aws:logs:{}:{}:log-group:{}",
8265 self.config.region, account_id, group.name
8266 )
8267 };
8268
8269 let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
8270 self.log_groups_state.tags.items = tags;
8271 self.log_groups_state.tags.selected = 0;
8272 self.log_groups_state.tags.scroll_offset = 0;
8273 }
8274 Ok(())
8275 }
8276
8277 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
8278 if let Some(group) = self
8279 .log_groups_state
8280 .log_groups
8281 .items
8282 .get(self.log_groups_state.log_groups.selected)
8283 {
8284 if let Some(stream) = self
8285 .log_groups_state
8286 .log_streams
8287 .get(self.log_groups_state.selected_stream)
8288 {
8289 let (start_time, end_time) =
8291 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
8292 let now = chrono::Utc::now().timestamp_millis();
8293 let duration_ms = match self.log_groups_state.relative_unit {
8294 TimeUnit::Minutes => amount * 60 * 1000,
8295 TimeUnit::Hours => amount * 60 * 60 * 1000,
8296 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
8297 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
8298 };
8299 (Some(now - duration_ms), Some(now))
8300 } else {
8301 (None, None)
8302 };
8303
8304 let (mut events, has_more, token) = self
8305 .cloudwatch_client
8306 .get_log_events(
8307 &group.name,
8308 &stream.name,
8309 self.log_groups_state.next_backward_token.clone(),
8310 start_time,
8311 end_time,
8312 )
8313 .await?;
8314
8315 if self.log_groups_state.next_backward_token.is_some() {
8316 events.append(&mut self.log_groups_state.log_events);
8318 self.log_groups_state.event_scroll_offset = 0;
8319 } else {
8320 self.log_groups_state.event_scroll_offset = 0;
8322 }
8323
8324 self.log_groups_state.log_events = events;
8325 self.log_groups_state.has_older_events =
8326 has_more && self.log_groups_state.log_events.len() >= 25;
8327 self.log_groups_state.next_backward_token = token;
8328 self.log_groups_state.selected_event = 0;
8329 }
8330 }
8331 Ok(())
8332 }
8333
8334 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
8335 if self.insights_state.insights.selected_log_groups.is_empty() {
8336 return Err(anyhow::anyhow!(
8337 "No log groups selected. Please select at least one log group."
8338 ));
8339 }
8340
8341 let now = chrono::Utc::now().timestamp_millis();
8342 let amount = self
8343 .insights_state
8344 .insights
8345 .insights_relative_amount
8346 .parse::<i64>()
8347 .unwrap_or(1);
8348 let duration_ms = match self.insights_state.insights.insights_relative_unit {
8349 TimeUnit::Minutes => amount * 60 * 1000,
8350 TimeUnit::Hours => amount * 60 * 60 * 1000,
8351 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
8352 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
8353 };
8354 let start_time = now - duration_ms;
8355
8356 let query_id = self
8357 .cloudwatch_client
8358 .start_query(
8359 self.insights_state.insights.selected_log_groups.clone(),
8360 self.insights_state.insights.query_text.trim().to_string(),
8361 start_time,
8362 now,
8363 )
8364 .await?;
8365
8366 for _ in 0..60 {
8368 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
8369 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
8370
8371 if status == "Complete" {
8372 self.insights_state.insights.query_results = results;
8373 self.insights_state.insights.query_completed = true;
8374 self.insights_state.insights.results_selected = 0;
8375 self.insights_state.insights.expanded_result = None;
8376 self.view_mode = ViewMode::InsightsResults;
8377 return Ok(());
8378 } else if status == "Failed" || status == "Cancelled" {
8379 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
8380 }
8381 }
8382
8383 Err(anyhow::anyhow!("Query timeout"))
8384 }
8385
8386 pub(crate) fn toggle_column_visibility<T: PartialEq + Copy>(
8388 visible_columns: &mut Vec<T>,
8389 _all_columns: &[T],
8390 column_to_toggle: T,
8391 ) {
8392 if let Some(pos) = visible_columns.iter().position(|c| c == &column_to_toggle) {
8393 if visible_columns.len() > 1 {
8395 visible_columns.remove(pos);
8396 }
8397 } else {
8398 visible_columns.push(column_to_toggle);
8399 }
8400 }
8401}
8402
8403impl CloudWatchInsightsState {
8404 fn new() -> Self {
8405 Self {
8406 insights: InsightsState::default(),
8407 loading: false,
8408 }
8409 }
8410}
8411
8412impl CloudWatchAlarmsState {
8413 fn new() -> Self {
8414 Self {
8415 table: TableState::new(),
8416 current_alarm: None,
8417 alarm_tab: AlarmTab::AllAlarms,
8418 view_as: AlarmViewMode::Table,
8419 wrap_lines: false,
8420 sort_column: "Last state update".to_string(),
8421 sort_direction: SortDirection::Asc,
8422 input_focus: InputFocus::Filter,
8423 metric_data: Vec::new(),
8424 metrics_loading: false,
8425 }
8426 }
8427}
8428
8429impl ServicePickerState {
8430 fn new() -> Self {
8431 Self {
8432 filter: String::new(),
8433 filter_active: false,
8434 selected: 0,
8435 services: vec![
8436 "API Gateway › APIs",
8437 "CloudWatch › Log Groups",
8438 "CloudWatch › Logs Insights",
8439 "CloudWatch › Alarms",
8440 "CloudTrail › Event History",
8441 "CloudFormation › Stacks",
8442 "EC2 › Instances",
8443 "ECR › Repositories",
8444 "IAM › Users",
8445 "IAM › Roles",
8446 "IAM › User Groups",
8447 "Lambda › Functions",
8448 "Lambda › Applications",
8449 "S3 › Buckets",
8450 "SQS › Queues",
8451 ],
8452 }
8453 }
8454}
8455
8456#[cfg(test)]
8457mod test_helpers {
8458 use super::*;
8459
8460 pub fn test_app() -> App {
8462 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
8463 }
8464
8465 pub fn test_app_no_region() -> App {
8466 App::new_without_client("test".to_string(), None)
8467 }
8468}
8469
8470#[cfg(test)]
8471mod tests {
8472 use super::*;
8473 use crate::keymap::Action;
8474 use test_helpers::*;
8475
8476 #[test]
8477 fn test_next_tab_cycles_forward() {
8478 let mut app = test_app();
8479 app.tabs = vec![
8480 Tab {
8481 service: Service::CloudWatchLogGroups,
8482 title: "CloudWatch › Log Groups".to_string(),
8483 breadcrumb: "CloudWatch › Log Groups".to_string(),
8484 },
8485 Tab {
8486 service: Service::CloudWatchInsights,
8487 title: "CloudWatch › Logs Insights".to_string(),
8488 breadcrumb: "CloudWatch › Logs Insights".to_string(),
8489 },
8490 Tab {
8491 service: Service::CloudWatchAlarms,
8492 title: "CloudWatch › Alarms".to_string(),
8493 breadcrumb: "CloudWatch › Alarms".to_string(),
8494 },
8495 ];
8496 app.current_tab = 0;
8497
8498 app.handle_action(Action::NextTab);
8499 assert_eq!(app.current_tab, 1);
8500 assert_eq!(app.current_service, Service::CloudWatchInsights);
8501
8502 app.handle_action(Action::NextTab);
8503 assert_eq!(app.current_tab, 2);
8504 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8505
8506 app.handle_action(Action::NextTab);
8508 assert_eq!(app.current_tab, 0);
8509 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
8510 }
8511
8512 #[test]
8513 fn test_prev_tab_cycles_backward() {
8514 let mut app = test_app();
8515 app.tabs = vec![
8516 Tab {
8517 service: Service::CloudWatchLogGroups,
8518 title: "CloudWatch › Log Groups".to_string(),
8519 breadcrumb: "CloudWatch › Log Groups".to_string(),
8520 },
8521 Tab {
8522 service: Service::CloudWatchInsights,
8523 title: "CloudWatch › Logs Insights".to_string(),
8524 breadcrumb: "CloudWatch › Logs Insights".to_string(),
8525 },
8526 Tab {
8527 service: Service::CloudWatchAlarms,
8528 title: "CloudWatch › Alarms".to_string(),
8529 breadcrumb: "CloudWatch › Alarms".to_string(),
8530 },
8531 ];
8532 app.current_tab = 2;
8533
8534 app.handle_action(Action::PrevTab);
8535 assert_eq!(app.current_tab, 1);
8536 assert_eq!(app.current_service, Service::CloudWatchInsights);
8537
8538 app.handle_action(Action::PrevTab);
8539 assert_eq!(app.current_tab, 0);
8540 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
8541
8542 app.handle_action(Action::PrevTab);
8544 assert_eq!(app.current_tab, 2);
8545 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8546 }
8547
8548 #[test]
8549 fn test_close_tab_removes_current() {
8550 let mut app = test_app();
8551 app.tabs = vec![
8552 Tab {
8553 service: Service::CloudWatchLogGroups,
8554 title: "CloudWatch › Log Groups".to_string(),
8555 breadcrumb: "CloudWatch › Log Groups".to_string(),
8556 },
8557 Tab {
8558 service: Service::CloudWatchInsights,
8559 title: "CloudWatch › Logs Insights".to_string(),
8560 breadcrumb: "CloudWatch › Logs Insights".to_string(),
8561 },
8562 Tab {
8563 service: Service::CloudWatchAlarms,
8564 title: "CloudWatch › Alarms".to_string(),
8565 breadcrumb: "CloudWatch › Alarms".to_string(),
8566 },
8567 ];
8568 app.current_tab = 1;
8569 app.service_selected = true;
8570
8571 app.handle_action(Action::CloseTab);
8572 assert_eq!(app.tabs.len(), 2);
8573 assert_eq!(app.current_tab, 1);
8574 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8575 }
8576
8577 #[test]
8578 fn test_close_last_tab_exits_service() {
8579 let mut app = test_app();
8580 app.tabs = vec![Tab {
8581 service: Service::CloudWatchLogGroups,
8582 title: "CloudWatch › Log Groups".to_string(),
8583 breadcrumb: "CloudWatch › Log Groups".to_string(),
8584 }];
8585 app.current_tab = 0;
8586 app.service_selected = true;
8587
8588 app.handle_action(Action::CloseTab);
8589 assert_eq!(app.tabs.len(), 0);
8590 assert!(!app.service_selected);
8591 assert_eq!(app.current_tab, 0);
8592 }
8593
8594 #[test]
8595 fn test_close_service_removes_current_tab() {
8596 let mut app = test_app();
8597 app.tabs = vec![
8598 Tab {
8599 service: Service::CloudWatchLogGroups,
8600 title: "CloudWatch › Log Groups".to_string(),
8601 breadcrumb: "CloudWatch › Log Groups".to_string(),
8602 },
8603 Tab {
8604 service: Service::CloudWatchInsights,
8605 title: "CloudWatch › Logs Insights".to_string(),
8606 breadcrumb: "CloudWatch › Logs Insights".to_string(),
8607 },
8608 Tab {
8609 service: Service::CloudWatchAlarms,
8610 title: "CloudWatch › Alarms".to_string(),
8611 breadcrumb: "CloudWatch › Alarms".to_string(),
8612 },
8613 ];
8614 app.current_tab = 1;
8615 app.service_selected = true;
8616
8617 app.handle_action(Action::CloseService);
8618
8619 assert_eq!(app.tabs.len(), 2);
8621 assert_eq!(app.current_tab, 1);
8623 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8624 assert!(app.service_selected);
8626 assert_eq!(app.mode, Mode::Normal);
8627 }
8628
8629 #[test]
8630 fn test_close_service_last_tab_shows_picker() {
8631 let mut app = test_app();
8632 app.tabs = vec![Tab {
8633 service: Service::CloudWatchLogGroups,
8634 title: "CloudWatch › Log Groups".to_string(),
8635 breadcrumb: "CloudWatch › Log Groups".to_string(),
8636 }];
8637 app.current_tab = 0;
8638 app.service_selected = true;
8639
8640 app.handle_action(Action::CloseService);
8641
8642 assert_eq!(app.tabs.len(), 0);
8644 assert!(!app.service_selected);
8646 assert_eq!(app.mode, Mode::ServicePicker);
8647 }
8648
8649 #[test]
8650 fn test_open_tab_picker_with_tabs() {
8651 let mut app = test_app();
8652 app.tabs = vec![
8653 Tab {
8654 service: Service::CloudWatchLogGroups,
8655 title: "CloudWatch › Log Groups".to_string(),
8656 breadcrumb: "CloudWatch › Log Groups".to_string(),
8657 },
8658 Tab {
8659 service: Service::CloudWatchInsights,
8660 title: "CloudWatch › Logs Insights".to_string(),
8661 breadcrumb: "CloudWatch › Logs Insights".to_string(),
8662 },
8663 ];
8664 app.current_tab = 1;
8665
8666 app.handle_action(Action::OpenTabPicker);
8667 assert_eq!(app.mode, Mode::TabPicker);
8668 assert_eq!(app.tab_picker_selected, 1);
8669 }
8670
8671 #[test]
8672 fn test_open_tab_picker_without_tabs() {
8673 let mut app = test_app();
8674 app.tabs = vec![];
8675
8676 app.handle_action(Action::OpenTabPicker);
8677 assert_eq!(app.mode, Mode::Normal);
8678 }
8679
8680 #[test]
8681 fn test_pending_key_state() {
8682 let mut app = test_app();
8683 assert_eq!(app.pending_key, None);
8684
8685 app.pending_key = Some('g');
8686 assert_eq!(app.pending_key, Some('g'));
8687 }
8688
8689 #[test]
8690 fn test_tab_breadcrumb_updates() {
8691 let mut app = test_app();
8692 app.tabs = vec![Tab {
8693 service: Service::CloudWatchLogGroups,
8694 title: "CloudWatch › Log Groups".to_string(),
8695 breadcrumb: "CloudWatch > Log groups".to_string(),
8696 }];
8697 app.current_tab = 0;
8698 app.service_selected = true;
8699 app.current_service = Service::CloudWatchLogGroups;
8700
8701 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
8703
8704 app.log_groups_state
8706 .log_groups
8707 .items
8708 .push(rusticity_core::LogGroup {
8709 name: "/aws/lambda/test".to_string(),
8710 creation_time: None,
8711 stored_bytes: Some(1024),
8712 retention_days: None,
8713 log_class: None,
8714 arn: None,
8715 log_group_arn: None,
8716 deletion_protection_enabled: None,
8717 });
8718 app.log_groups_state.log_groups.reset();
8719 app.view_mode = ViewMode::Detail;
8720 app.update_current_tab_breadcrumb();
8721
8722 assert_eq!(
8724 app.tabs[0].breadcrumb,
8725 "CloudWatch > Log groups > /aws/lambda/test"
8726 );
8727 }
8728
8729 #[test]
8730 fn test_s3_bucket_column_selector_navigation() {
8731 let mut app = test_app();
8732 app.current_service = Service::S3Buckets;
8733 app.mode = Mode::ColumnSelector;
8734 app.column_selector_index = 0;
8735
8736 app.handle_action(Action::NextItem);
8738 assert_eq!(app.column_selector_index, 1);
8739
8740 app.handle_action(Action::NextItem);
8741 assert_eq!(app.column_selector_index, 2);
8742
8743 app.handle_action(Action::NextItem);
8744 assert_eq!(app.column_selector_index, 3);
8745
8746 for _ in 0..10 {
8748 app.handle_action(Action::NextItem);
8749 }
8750 assert_eq!(app.column_selector_index, 9);
8751
8752 app.handle_action(Action::PrevItem);
8754 assert_eq!(app.column_selector_index, 8);
8755
8756 app.handle_action(Action::PrevItem);
8757 assert_eq!(app.column_selector_index, 7);
8758
8759 for _ in 0..10 {
8761 app.handle_action(Action::PrevItem);
8762 }
8763 assert_eq!(app.column_selector_index, 0);
8764 }
8765
8766 #[test]
8767 fn test_cloudwatch_alarms_state_initialized() {
8768 let app = test_app();
8769
8770 assert_eq!(app.alarms_state.table.items.len(), 0);
8772 assert_eq!(app.alarms_state.table.selected, 0);
8773 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
8774 assert!(!app.alarms_state.table.loading);
8775 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
8776 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
8777 }
8778
8779 #[test]
8780 fn test_cloudwatch_alarms_service_selection() {
8781 let mut app = test_app();
8782
8783 app.current_service = Service::CloudWatchAlarms;
8785 app.service_selected = true;
8786
8787 assert_eq!(app.current_service, Service::CloudWatchAlarms);
8788 assert!(app.service_selected);
8789 }
8790
8791 #[test]
8792 fn test_cloudwatch_alarms_column_preferences() {
8793 let app = test_app();
8794
8795 assert!(!app.cw_alarm_column_ids.is_empty());
8797 assert!(!app.cw_alarm_visible_column_ids.is_empty());
8798
8799 assert!(app
8801 .cw_alarm_visible_column_ids
8802 .contains(&AlarmColumn::Name.id()));
8803 assert!(app
8804 .cw_alarm_visible_column_ids
8805 .contains(&AlarmColumn::State.id()));
8806 }
8807
8808 #[test]
8809 fn test_s3_bucket_navigation_without_expansion() {
8810 let mut app = test_app();
8811 app.current_service = Service::S3Buckets;
8812 app.service_selected = true;
8813 app.mode = Mode::Normal;
8814
8815 app.s3_state.buckets.items = vec![
8817 S3Bucket {
8818 name: "bucket1".to_string(),
8819 region: "us-east-1".to_string(),
8820 creation_date: "2024-01-01T00:00:00Z".to_string(),
8821 },
8822 S3Bucket {
8823 name: "bucket2".to_string(),
8824 region: "us-east-1".to_string(),
8825 creation_date: "2024-01-02T00:00:00Z".to_string(),
8826 },
8827 S3Bucket {
8828 name: "bucket3".to_string(),
8829 region: "us-east-1".to_string(),
8830 creation_date: "2024-01-03T00:00:00Z".to_string(),
8831 },
8832 ];
8833 app.s3_state.selected_row = 0;
8834
8835 app.handle_action(Action::NextItem);
8837 assert_eq!(app.s3_state.selected_row, 1);
8838
8839 app.handle_action(Action::NextItem);
8840 assert_eq!(app.s3_state.selected_row, 2);
8841
8842 app.handle_action(Action::NextItem);
8844 assert_eq!(app.s3_state.selected_row, 2);
8845
8846 app.handle_action(Action::PrevItem);
8848 assert_eq!(app.s3_state.selected_row, 1);
8849
8850 app.handle_action(Action::PrevItem);
8851 assert_eq!(app.s3_state.selected_row, 0);
8852
8853 app.handle_action(Action::PrevItem);
8855 assert_eq!(app.s3_state.selected_row, 0);
8856 }
8857
8858 #[test]
8859 fn test_s3_bucket_navigation_with_expansion() {
8860 let mut app = test_app();
8861 app.current_service = Service::S3Buckets;
8862 app.service_selected = true;
8863 app.mode = Mode::Normal;
8864
8865 app.s3_state.buckets.items = vec![
8867 S3Bucket {
8868 name: "bucket1".to_string(),
8869 region: "us-east-1".to_string(),
8870 creation_date: "2024-01-01T00:00:00Z".to_string(),
8871 },
8872 S3Bucket {
8873 name: "bucket2".to_string(),
8874 region: "us-east-1".to_string(),
8875 creation_date: "2024-01-02T00:00:00Z".to_string(),
8876 },
8877 ];
8878
8879 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
8881 app.s3_state.bucket_preview.insert(
8882 "bucket1".to_string(),
8883 vec![
8884 S3Object {
8885 key: "file1.txt".to_string(),
8886 size: 100,
8887 last_modified: "2024-01-01T00:00:00Z".to_string(),
8888 is_prefix: false,
8889 storage_class: "STANDARD".to_string(),
8890 },
8891 S3Object {
8892 key: "folder/".to_string(),
8893 size: 0,
8894 last_modified: "2024-01-01T00:00:00Z".to_string(),
8895 is_prefix: true,
8896 storage_class: String::new(),
8897 },
8898 ],
8899 );
8900
8901 app.s3_state.selected_row = 0;
8902
8903 app.handle_action(Action::NextItem);
8906 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
8909 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
8912 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
8916 assert_eq!(app.s3_state.selected_row, 3);
8917 }
8918
8919 #[test]
8920 fn test_s3_bucket_navigation_with_nested_expansion() {
8921 let mut app = test_app();
8922 app.current_service = Service::S3Buckets;
8923 app.service_selected = true;
8924 app.mode = Mode::Normal;
8925
8926 app.s3_state.buckets.items = vec![S3Bucket {
8928 name: "bucket1".to_string(),
8929 region: "us-east-1".to_string(),
8930 creation_date: "2024-01-01T00:00:00Z".to_string(),
8931 }];
8932
8933 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
8935 app.s3_state.bucket_preview.insert(
8936 "bucket1".to_string(),
8937 vec![S3Object {
8938 key: "folder/".to_string(),
8939 size: 0,
8940 last_modified: "2024-01-01T00:00:00Z".to_string(),
8941 is_prefix: true,
8942 storage_class: String::new(),
8943 }],
8944 );
8945
8946 app.s3_state.expanded_prefixes.insert("folder/".to_string());
8948 app.s3_state.prefix_preview.insert(
8949 "folder/".to_string(),
8950 vec![
8951 S3Object {
8952 key: "folder/file1.txt".to_string(),
8953 size: 100,
8954 last_modified: "2024-01-01T00:00:00Z".to_string(),
8955 is_prefix: false,
8956 storage_class: "STANDARD".to_string(),
8957 },
8958 S3Object {
8959 key: "folder/file2.txt".to_string(),
8960 size: 200,
8961 last_modified: "2024-01-01T00:00:00Z".to_string(),
8962 is_prefix: false,
8963 storage_class: "STANDARD".to_string(),
8964 },
8965 ],
8966 );
8967
8968 app.s3_state.selected_row = 0;
8969
8970 app.handle_action(Action::NextItem);
8972 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
8975 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
8978 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
8982 assert_eq!(app.s3_state.selected_row, 3);
8983 }
8984
8985 #[test]
8986 fn test_calculate_total_bucket_rows() {
8987 let mut app = test_app();
8988
8989 assert_eq!(app.calculate_total_bucket_rows(), 0);
8991
8992 app.s3_state.buckets.items = vec![
8994 S3Bucket {
8995 name: "bucket1".to_string(),
8996 region: "us-east-1".to_string(),
8997 creation_date: "2024-01-01T00:00:00Z".to_string(),
8998 },
8999 S3Bucket {
9000 name: "bucket2".to_string(),
9001 region: "us-east-1".to_string(),
9002 creation_date: "2024-01-02T00:00:00Z".to_string(),
9003 },
9004 ];
9005 assert_eq!(app.calculate_total_bucket_rows(), 2);
9006
9007 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9009 app.s3_state.bucket_preview.insert(
9010 "bucket1".to_string(),
9011 vec![
9012 S3Object {
9013 key: "file1.txt".to_string(),
9014 size: 100,
9015 last_modified: "2024-01-01T00:00:00Z".to_string(),
9016 is_prefix: false,
9017 storage_class: "STANDARD".to_string(),
9018 },
9019 S3Object {
9020 key: "file2.txt".to_string(),
9021 size: 200,
9022 last_modified: "2024-01-01T00:00:00Z".to_string(),
9023 is_prefix: false,
9024 storage_class: "STANDARD".to_string(),
9025 },
9026 S3Object {
9027 key: "folder/".to_string(),
9028 size: 0,
9029 last_modified: "2024-01-01T00:00:00Z".to_string(),
9030 is_prefix: true,
9031 storage_class: String::new(),
9032 },
9033 ],
9034 );
9035 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
9039 app.s3_state.prefix_preview.insert(
9040 "folder/".to_string(),
9041 vec![
9042 S3Object {
9043 key: "folder/nested1.txt".to_string(),
9044 size: 50,
9045 last_modified: "2024-01-01T00:00:00Z".to_string(),
9046 is_prefix: false,
9047 storage_class: "STANDARD".to_string(),
9048 },
9049 S3Object {
9050 key: "folder/nested2.txt".to_string(),
9051 size: 75,
9052 last_modified: "2024-01-01T00:00:00Z".to_string(),
9053 is_prefix: false,
9054 storage_class: "STANDARD".to_string(),
9055 },
9056 ],
9057 );
9058 assert_eq!(app.calculate_total_bucket_rows(), 7); }
9060
9061 #[test]
9062 fn test_calculate_total_object_rows() {
9063 let mut app = test_app();
9064 app.s3_state.current_bucket = Some("test-bucket".to_string());
9065
9066 assert_eq!(app.calculate_total_object_rows(), 0);
9068
9069 app.s3_state.objects = vec![
9071 S3Object {
9072 key: "file1.txt".to_string(),
9073 size: 100,
9074 last_modified: "2024-01-01T00:00:00Z".to_string(),
9075 is_prefix: false,
9076 storage_class: "STANDARD".to_string(),
9077 },
9078 S3Object {
9079 key: "folder/".to_string(),
9080 size: 0,
9081 last_modified: "2024-01-01T00:00:00Z".to_string(),
9082 is_prefix: true,
9083 storage_class: String::new(),
9084 },
9085 ];
9086 assert_eq!(app.calculate_total_object_rows(), 2);
9087
9088 app.s3_state.expanded_prefixes.insert("folder/".to_string());
9090 app.s3_state.prefix_preview.insert(
9091 "folder/".to_string(),
9092 vec![
9093 S3Object {
9094 key: "folder/file2.txt".to_string(),
9095 size: 200,
9096 last_modified: "2024-01-01T00:00:00Z".to_string(),
9097 is_prefix: false,
9098 storage_class: "STANDARD".to_string(),
9099 },
9100 S3Object {
9101 key: "folder/subfolder/".to_string(),
9102 size: 0,
9103 last_modified: "2024-01-01T00:00:00Z".to_string(),
9104 is_prefix: true,
9105 storage_class: String::new(),
9106 },
9107 ],
9108 );
9109 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
9113 .expanded_prefixes
9114 .insert("folder/subfolder/".to_string());
9115 app.s3_state.prefix_preview.insert(
9116 "folder/subfolder/".to_string(),
9117 vec![S3Object {
9118 key: "folder/subfolder/deep.txt".to_string(),
9119 size: 50,
9120 last_modified: "2024-01-01T00:00:00Z".to_string(),
9121 is_prefix: false,
9122 storage_class: "STANDARD".to_string(),
9123 }],
9124 );
9125 assert_eq!(app.calculate_total_object_rows(), 5); }
9127
9128 #[test]
9129 fn test_s3_object_navigation_with_deep_nesting() {
9130 let mut app = test_app();
9131 app.current_service = Service::S3Buckets;
9132 app.service_selected = true;
9133 app.mode = Mode::Normal;
9134 app.s3_state.current_bucket = Some("test-bucket".to_string());
9135
9136 app.s3_state.objects = vec![S3Object {
9138 key: "folder1/".to_string(),
9139 size: 0,
9140 last_modified: "2024-01-01T00:00:00Z".to_string(),
9141 is_prefix: true,
9142 storage_class: String::new(),
9143 }];
9144
9145 app.s3_state
9147 .expanded_prefixes
9148 .insert("folder1/".to_string());
9149 app.s3_state.prefix_preview.insert(
9150 "folder1/".to_string(),
9151 vec![S3Object {
9152 key: "folder1/folder2/".to_string(),
9153 size: 0,
9154 last_modified: "2024-01-01T00:00:00Z".to_string(),
9155 is_prefix: true,
9156 storage_class: String::new(),
9157 }],
9158 );
9159
9160 app.s3_state
9162 .expanded_prefixes
9163 .insert("folder1/folder2/".to_string());
9164 app.s3_state.prefix_preview.insert(
9165 "folder1/folder2/".to_string(),
9166 vec![S3Object {
9167 key: "folder1/folder2/file.txt".to_string(),
9168 size: 100,
9169 last_modified: "2024-01-01T00:00:00Z".to_string(),
9170 is_prefix: false,
9171 storage_class: "STANDARD".to_string(),
9172 }],
9173 );
9174
9175 app.s3_state.selected_object = 0;
9176
9177 app.handle_action(Action::NextItem);
9179 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
9182 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
9186 assert_eq!(app.s3_state.selected_object, 2);
9187 }
9188
9189 #[test]
9190 fn test_s3_expand_nested_folder_in_objects_view() {
9191 let mut app = test_app();
9192 app.current_service = Service::S3Buckets;
9193 app.service_selected = true;
9194 app.mode = Mode::Normal;
9195 app.s3_state.current_bucket = Some("test-bucket".to_string());
9196
9197 app.s3_state.objects = vec![S3Object {
9199 key: "parent/".to_string(),
9200 size: 0,
9201 last_modified: "2024-01-01T00:00:00Z".to_string(),
9202 is_prefix: true,
9203 storage_class: String::new(),
9204 }];
9205
9206 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9208 app.s3_state.prefix_preview.insert(
9209 "parent/".to_string(),
9210 vec![S3Object {
9211 key: "parent/child/".to_string(),
9212 size: 0,
9213 last_modified: "2024-01-01T00:00:00Z".to_string(),
9214 is_prefix: true,
9215 storage_class: String::new(),
9216 }],
9217 );
9218
9219 app.s3_state.selected_object = 1;
9221
9222 app.handle_action(Action::NextPane);
9224
9225 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
9227 assert!(app.s3_state.buckets.loading); }
9229
9230 #[test]
9231 fn test_s3_drill_into_nested_folder() {
9232 let mut app = test_app();
9233 app.current_service = Service::S3Buckets;
9234 app.service_selected = true;
9235 app.mode = Mode::Normal;
9236 app.s3_state.current_bucket = Some("test-bucket".to_string());
9237
9238 app.s3_state.objects = vec![S3Object {
9240 key: "parent/".to_string(),
9241 size: 0,
9242 last_modified: "2024-01-01T00:00:00Z".to_string(),
9243 is_prefix: true,
9244 storage_class: String::new(),
9245 }];
9246
9247 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9249 app.s3_state.prefix_preview.insert(
9250 "parent/".to_string(),
9251 vec![S3Object {
9252 key: "parent/child/".to_string(),
9253 size: 0,
9254 last_modified: "2024-01-01T00:00:00Z".to_string(),
9255 is_prefix: true,
9256 storage_class: String::new(),
9257 }],
9258 );
9259
9260 app.s3_state.selected_object = 1;
9262
9263 app.handle_action(Action::Select);
9265
9266 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
9268 assert!(app.s3_state.buckets.loading); }
9270
9271 #[test]
9272 fn test_s3_esc_pops_navigation_stack() {
9273 let mut app = test_app();
9274 app.current_service = Service::S3Buckets;
9275 app.s3_state.current_bucket = Some("test-bucket".to_string());
9276 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
9277
9278 app.handle_action(Action::GoBack);
9280 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
9281 assert!(app.s3_state.buckets.loading);
9282
9283 app.s3_state.buckets.loading = false;
9285 app.handle_action(Action::GoBack);
9286 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
9287 assert!(app.s3_state.buckets.loading);
9288
9289 app.s3_state.buckets.loading = false;
9291 app.handle_action(Action::GoBack);
9292 assert_eq!(app.s3_state.current_bucket, None);
9293 }
9294
9295 #[test]
9296 fn test_s3_esc_from_bucket_root_exits() {
9297 let mut app = test_app();
9298 app.current_service = Service::S3Buckets;
9299 app.s3_state.current_bucket = Some("test-bucket".to_string());
9300 app.s3_state.prefix_stack = vec![];
9301
9302 app.handle_action(Action::GoBack);
9304 assert_eq!(app.s3_state.current_bucket, None);
9305 assert_eq!(app.s3_state.objects.len(), 0);
9306 }
9307
9308 #[test]
9309 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
9310 let mut app = test_app();
9311 app.current_service = Service::S3Buckets;
9312 app.service_selected = true;
9313 app.mode = Mode::Normal;
9314
9315 app.s3_state.buckets.items = vec![S3Bucket {
9317 name: "test-bucket".to_string(),
9318 region: "us-east-1".to_string(),
9319 creation_date: "2024-01-01".to_string(),
9320 }];
9321
9322 app.s3_state
9324 .expanded_prefixes
9325 .insert("test-bucket".to_string());
9326 app.s3_state.bucket_preview.insert(
9327 "test-bucket".to_string(),
9328 vec![S3Object {
9329 key: "parent/".to_string(),
9330 size: 0,
9331 last_modified: "2024-01-01".to_string(),
9332 is_prefix: true,
9333 storage_class: String::new(),
9334 }],
9335 );
9336
9337 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9339 app.s3_state.prefix_preview.insert(
9340 "parent/".to_string(),
9341 vec![S3Object {
9342 key: "parent/child/".to_string(),
9343 size: 0,
9344 last_modified: "2024-01-01".to_string(),
9345 is_prefix: true,
9346 storage_class: String::new(),
9347 }],
9348 );
9349
9350 app.s3_state.selected_row = 2;
9352
9353 app.handle_action(Action::Select);
9355
9356 assert_eq!(
9358 app.s3_state.prefix_stack,
9359 vec!["parent/".to_string(), "parent/child/".to_string()]
9360 );
9361 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
9362 assert!(app.s3_state.buckets.loading);
9363
9364 app.s3_state.buckets.loading = false;
9366 app.handle_action(Action::GoBack);
9367 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
9368 assert!(app.s3_state.buckets.loading);
9369
9370 app.s3_state.buckets.loading = false;
9372 app.handle_action(Action::GoBack);
9373 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
9374 assert!(app.s3_state.buckets.loading);
9375
9376 app.s3_state.buckets.loading = false;
9378 app.handle_action(Action::GoBack);
9379 assert_eq!(app.s3_state.current_bucket, None);
9380 }
9381
9382 #[test]
9383 fn test_region_picker_fuzzy_filter() {
9384 let mut app = test_app();
9385 app.region_latencies.insert("us-east-1".to_string(), 10);
9386 app.region_filter = "vir".to_string();
9387 let filtered = app.get_filtered_regions();
9388 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
9389 }
9390
9391 #[test]
9392 fn test_profile_picker_loads_profiles() {
9393 let profiles = App::load_aws_profiles();
9394 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
9396 }
9397
9398 #[test]
9399 fn test_profile_with_region_uses_it() {
9400 let mut app = test_app_no_region();
9401 app.available_profiles = vec![AwsProfile {
9402 name: "test-profile".to_string(),
9403 region: Some("eu-west-1".to_string()),
9404 account: Some("123456789".to_string()),
9405 role_arn: None,
9406 source_profile: None,
9407 }];
9408 app.profile_picker_selected = 0;
9409 app.mode = Mode::ProfilePicker;
9410
9411 let filtered = app.get_filtered_profiles();
9413 if let Some(profile) = filtered.first() {
9414 let profile_name = profile.name.clone();
9415 let profile_region = profile.region.clone();
9416
9417 app.profile = profile_name;
9418 if let Some(region) = profile_region {
9419 app.region = region;
9420 }
9421 }
9422
9423 assert_eq!(app.profile, "test-profile");
9424 assert_eq!(app.region, "eu-west-1");
9425 }
9426
9427 #[test]
9428 fn test_profile_without_region_keeps_unknown() {
9429 let mut app = test_app_no_region();
9430 let initial_region = app.region.clone();
9431
9432 app.available_profiles = vec![AwsProfile {
9433 name: "test-profile".to_string(),
9434 region: None,
9435 account: None,
9436 role_arn: None,
9437 source_profile: None,
9438 }];
9439 app.profile_picker_selected = 0;
9440 app.mode = Mode::ProfilePicker;
9441
9442 let filtered = app.get_filtered_profiles();
9443 if let Some(profile) = filtered.first() {
9444 let profile_name = profile.name.clone();
9445 let profile_region = profile.region.clone();
9446
9447 app.profile = profile_name;
9448 if let Some(region) = profile_region {
9449 app.region = region;
9450 }
9451 }
9452
9453 assert_eq!(app.profile, "test-profile");
9454 assert_eq!(app.region, initial_region); }
9456
9457 #[test]
9458 fn test_region_selection_closes_all_tabs() {
9459 let mut app = test_app();
9460
9461 app.tabs.push(Tab {
9463 service: Service::CloudWatchLogGroups,
9464 title: "CloudWatch".to_string(),
9465 breadcrumb: "CloudWatch".to_string(),
9466 });
9467 app.tabs.push(Tab {
9468 service: Service::S3Buckets,
9469 title: "S3".to_string(),
9470 breadcrumb: "S3".to_string(),
9471 });
9472 app.service_selected = true;
9473 app.current_tab = 1;
9474
9475 app.region_latencies.insert("eu-west-1".to_string(), 50);
9477
9478 app.mode = Mode::RegionPicker;
9480 app.region_picker_selected = 0;
9481
9482 let filtered = app.get_filtered_regions();
9483 if let Some(region) = filtered.first() {
9484 app.region = region.code.to_string();
9485 app.tabs.clear();
9486 app.current_tab = 0;
9487 app.service_selected = false;
9488 app.mode = Mode::Normal;
9489 }
9490
9491 assert_eq!(app.tabs.len(), 0);
9492 assert_eq!(app.current_tab, 0);
9493 assert!(!app.service_selected);
9494 assert_eq!(app.region, "eu-west-1");
9495 }
9496
9497 #[test]
9498 fn test_region_picker_can_be_closed_without_selection() {
9499 let mut app = test_app();
9500 let initial_region = app.region.clone();
9501
9502 app.mode = Mode::RegionPicker;
9503
9504 app.mode = Mode::Normal;
9506
9507 assert_eq!(app.region, initial_region);
9509 }
9510
9511 #[test]
9512 fn test_session_filter_works() {
9513 let mut app = test_app();
9514
9515 app.sessions = vec![
9516 Session {
9517 id: "1".to_string(),
9518 timestamp: "2024-01-01".to_string(),
9519 profile: "prod-profile".to_string(),
9520 region: "us-east-1".to_string(),
9521 account_id: "123456789".to_string(),
9522 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
9523 tabs: vec![],
9524 },
9525 Session {
9526 id: "2".to_string(),
9527 timestamp: "2024-01-02".to_string(),
9528 profile: "dev-profile".to_string(),
9529 region: "eu-west-1".to_string(),
9530 account_id: "987654321".to_string(),
9531 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
9532 tabs: vec![],
9533 },
9534 ];
9535
9536 app.session_filter = "prod".to_string();
9538 let filtered = app.get_filtered_sessions();
9539 assert_eq!(filtered.len(), 1);
9540 assert_eq!(filtered[0].profile, "prod-profile");
9541
9542 app.session_filter = "eu".to_string();
9544 let filtered = app.get_filtered_sessions();
9545 assert_eq!(filtered.len(), 1);
9546 assert_eq!(filtered[0].region, "eu-west-1");
9547
9548 app.session_filter.clear();
9550 let filtered = app.get_filtered_sessions();
9551 assert_eq!(filtered.len(), 2);
9552 }
9553
9554 #[test]
9555 fn test_profile_picker_shows_account() {
9556 let mut app = test_app_no_region();
9557 app.available_profiles = vec![AwsProfile {
9558 name: "test-profile".to_string(),
9559 region: Some("us-east-1".to_string()),
9560 account: Some("123456789".to_string()),
9561 role_arn: None,
9562 source_profile: None,
9563 }];
9564
9565 let filtered = app.get_filtered_profiles();
9566 assert_eq!(filtered.len(), 1);
9567 assert_eq!(filtered[0].account, Some("123456789".to_string()));
9568 }
9569
9570 #[test]
9571 fn test_profile_without_account() {
9572 let mut app = test_app_no_region();
9573 app.available_profiles = vec![AwsProfile {
9574 name: "test-profile".to_string(),
9575 region: Some("us-east-1".to_string()),
9576 account: None,
9577 role_arn: None,
9578 source_profile: None,
9579 }];
9580
9581 let filtered = app.get_filtered_profiles();
9582 assert_eq!(filtered.len(), 1);
9583 assert_eq!(filtered[0].account, None);
9584 }
9585
9586 #[test]
9587 fn test_profile_with_all_fields() {
9588 let mut app = test_app_no_region();
9589 app.available_profiles = vec![AwsProfile {
9590 name: "prod-profile".to_string(),
9591 region: Some("us-west-2".to_string()),
9592 account: Some("123456789".to_string()),
9593 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
9594 source_profile: Some("base-profile".to_string()),
9595 }];
9596
9597 let filtered = app.get_filtered_profiles();
9598 assert_eq!(filtered.len(), 1);
9599 assert_eq!(filtered[0].name, "prod-profile");
9600 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
9601 assert_eq!(filtered[0].account, Some("123456789".to_string()));
9602 assert_eq!(
9603 filtered[0].role_arn,
9604 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
9605 );
9606 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
9607 }
9608
9609 #[test]
9610 fn test_profile_filter_by_source_profile() {
9611 let mut app = test_app_no_region();
9612 app.available_profiles = vec![
9613 AwsProfile {
9614 name: "profile1".to_string(),
9615 region: None,
9616 account: None,
9617 role_arn: None,
9618 source_profile: Some("base".to_string()),
9619 },
9620 AwsProfile {
9621 name: "profile2".to_string(),
9622 region: None,
9623 account: None,
9624 role_arn: None,
9625 source_profile: Some("other".to_string()),
9626 },
9627 ];
9628
9629 app.profile_filter = "base".to_string();
9630 let filtered = app.get_filtered_profiles();
9631 assert_eq!(filtered.len(), 1);
9632 assert_eq!(filtered[0].name, "profile1");
9633 }
9634
9635 #[test]
9636 fn test_profile_filter_by_role() {
9637 let mut app = test_app_no_region();
9638 app.available_profiles = vec![
9639 AwsProfile {
9640 name: "admin-profile".to_string(),
9641 region: None,
9642 account: None,
9643 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
9644 source_profile: None,
9645 },
9646 AwsProfile {
9647 name: "dev-profile".to_string(),
9648 region: None,
9649 account: None,
9650 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
9651 source_profile: None,
9652 },
9653 ];
9654
9655 app.profile_filter = "Admin".to_string();
9656 let filtered = app.get_filtered_profiles();
9657 assert_eq!(filtered.len(), 1);
9658 assert_eq!(filtered[0].name, "admin-profile");
9659 }
9660
9661 #[test]
9662 fn test_profiles_sorted_by_name() {
9663 let mut app = test_app_no_region();
9664 app.available_profiles = vec![
9665 AwsProfile {
9666 name: "zebra-profile".to_string(),
9667 region: None,
9668 account: None,
9669 role_arn: None,
9670 source_profile: None,
9671 },
9672 AwsProfile {
9673 name: "alpha-profile".to_string(),
9674 region: None,
9675 account: None,
9676 role_arn: None,
9677 source_profile: None,
9678 },
9679 AwsProfile {
9680 name: "beta-profile".to_string(),
9681 region: None,
9682 account: None,
9683 role_arn: None,
9684 source_profile: None,
9685 },
9686 ];
9687
9688 let filtered = app.get_filtered_profiles();
9689 assert_eq!(filtered.len(), 3);
9690 assert_eq!(filtered[0].name, "alpha-profile");
9691 assert_eq!(filtered[1].name, "beta-profile");
9692 assert_eq!(filtered[2].name, "zebra-profile");
9693 }
9694
9695 #[test]
9696 fn test_profile_with_role_arn() {
9697 let mut app = test_app_no_region();
9698 app.available_profiles = vec![AwsProfile {
9699 name: "role-profile".to_string(),
9700 region: Some("us-east-1".to_string()),
9701 account: Some("123456789".to_string()),
9702 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
9703 source_profile: None,
9704 }];
9705
9706 let filtered = app.get_filtered_profiles();
9707 assert_eq!(filtered.len(), 1);
9708 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
9709 }
9710
9711 #[test]
9712 fn test_profile_with_user_arn() {
9713 let mut app = test_app_no_region();
9714 app.available_profiles = vec![AwsProfile {
9715 name: "user-profile".to_string(),
9716 region: Some("us-east-1".to_string()),
9717 account: Some("123456789".to_string()),
9718 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
9719 source_profile: None,
9720 }];
9721
9722 let filtered = app.get_filtered_profiles();
9723 assert_eq!(filtered.len(), 1);
9724 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
9725 }
9726
9727 #[test]
9728 fn test_filtered_profiles_also_sorted() {
9729 let mut app = test_app_no_region();
9730 app.available_profiles = vec![
9731 AwsProfile {
9732 name: "prod-zebra".to_string(),
9733 region: Some("us-east-1".to_string()),
9734 account: None,
9735 role_arn: None,
9736 source_profile: None,
9737 },
9738 AwsProfile {
9739 name: "prod-alpha".to_string(),
9740 region: Some("us-east-1".to_string()),
9741 account: None,
9742 role_arn: None,
9743 source_profile: None,
9744 },
9745 AwsProfile {
9746 name: "dev-profile".to_string(),
9747 region: Some("us-west-2".to_string()),
9748 account: None,
9749 role_arn: None,
9750 source_profile: None,
9751 },
9752 ];
9753
9754 app.profile_filter = "prod".to_string();
9755 let filtered = app.get_filtered_profiles();
9756 assert_eq!(filtered.len(), 2);
9757 assert_eq!(filtered[0].name, "prod-alpha");
9758 assert_eq!(filtered[1].name, "prod-zebra");
9759 }
9760
9761 #[test]
9762 fn test_profile_picker_has_all_columns() {
9763 let mut app = test_app_no_region();
9764 app.available_profiles = vec![AwsProfile {
9765 name: "test".to_string(),
9766 region: Some("us-east-1".to_string()),
9767 account: Some("123456789".to_string()),
9768 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
9769 source_profile: Some("base".to_string()),
9770 }];
9771
9772 let filtered = app.get_filtered_profiles();
9773 assert_eq!(filtered.len(), 1);
9774 assert!(filtered[0].name == "test");
9775 assert!(filtered[0].region.is_some());
9776 assert!(filtered[0].account.is_some());
9777 assert!(filtered[0].role_arn.is_some());
9778 assert!(filtered[0].source_profile.is_some());
9779 }
9780
9781 #[test]
9782 fn test_session_picker_shows_tab_count() {
9783 let mut app = test_app_no_region();
9784 app.sessions = vec![Session {
9785 id: "1".to_string(),
9786 timestamp: "2024-01-01".to_string(),
9787 profile: "test".to_string(),
9788 region: "us-east-1".to_string(),
9789 account_id: "123".to_string(),
9790 role_arn: String::new(),
9791 tabs: vec![
9792 SessionTab {
9793 service: "CloudWatch".to_string(),
9794 title: "Logs".to_string(),
9795 breadcrumb: String::new(),
9796 filter: None,
9797 selected_item: None,
9798 },
9799 SessionTab {
9800 service: "S3".to_string(),
9801 title: "Buckets".to_string(),
9802 breadcrumb: String::new(),
9803 filter: None,
9804 selected_item: None,
9805 },
9806 ],
9807 }];
9808
9809 let filtered = app.get_filtered_sessions();
9810 assert_eq!(filtered.len(), 1);
9811 assert_eq!(filtered[0].tabs.len(), 2);
9812 }
9813
9814 #[test]
9815 fn test_start_background_data_fetch_loads_profiles() {
9816 let mut app = test_app_no_region();
9817 assert!(app.available_profiles.is_empty());
9818
9819 app.available_profiles = App::load_aws_profiles();
9821
9822 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
9824 }
9825
9826 #[test]
9827 fn test_refresh_in_profile_picker() {
9828 let mut app = test_app_no_region();
9829 app.mode = Mode::ProfilePicker;
9830 app.available_profiles = vec![AwsProfile {
9831 name: "test".to_string(),
9832 region: None,
9833 account: None,
9834 role_arn: None,
9835 source_profile: None,
9836 }];
9837
9838 app.handle_action(Action::Refresh);
9839
9840 assert!(app.log_groups_state.loading);
9842 assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
9843 }
9844
9845 #[test]
9846 fn test_refresh_sets_loading_for_profile_picker() {
9847 let mut app = test_app_no_region();
9848 app.mode = Mode::ProfilePicker;
9849
9850 assert!(!app.log_groups_state.loading);
9851
9852 app.handle_action(Action::Refresh);
9853
9854 assert!(app.log_groups_state.loading);
9855 }
9856
9857 #[test]
9858 fn test_profiles_loaded_on_demand() {
9859 let mut app = test_app_no_region();
9860
9861 assert!(app.available_profiles.is_empty());
9863
9864 app.available_profiles = App::load_aws_profiles();
9866
9867 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
9869 }
9870
9871 #[test]
9872 fn test_profile_accounts_not_fetched_automatically() {
9873 let mut app = test_app_no_region();
9874 app.available_profiles = App::load_aws_profiles();
9875
9876 for profile in &app.available_profiles {
9878 assert!(profile.account.is_none() || profile.account.is_some());
9881 }
9882 }
9883
9884 #[test]
9885 fn test_ctrl_r_triggers_account_fetch() {
9886 let mut app = test_app_no_region();
9887 app.mode = Mode::ProfilePicker;
9888 app.available_profiles = vec![AwsProfile {
9889 name: "test".to_string(),
9890 region: Some("us-east-1".to_string()),
9891 account: None,
9892 role_arn: None,
9893 source_profile: None,
9894 }];
9895
9896 assert!(app.available_profiles[0].account.is_none());
9898
9899 app.handle_action(Action::Refresh);
9901
9902 assert!(app.log_groups_state.loading);
9904 }
9905
9906 #[test]
9907 fn test_refresh_in_region_picker() {
9908 let mut app = test_app_no_region();
9909 app.mode = Mode::RegionPicker;
9910
9911 let initial_latencies = app.region_latencies.len();
9912 app.handle_action(Action::Refresh);
9913
9914 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
9916 }
9917
9918 #[test]
9919 fn test_refresh_in_session_picker() {
9920 let mut app = test_app_no_region();
9921 app.mode = Mode::SessionPicker;
9922 app.sessions = vec![];
9923
9924 app.handle_action(Action::Refresh);
9925
9926 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
9928 }
9929
9930 #[test]
9931 fn test_session_picker_selection() {
9932 let mut app = test_app();
9933
9934 app.sessions = vec![Session {
9935 id: "1".to_string(),
9936 timestamp: "2024-01-01".to_string(),
9937 profile: "prod-profile".to_string(),
9938 region: "us-west-2".to_string(),
9939 account_id: "123456789".to_string(),
9940 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
9941 tabs: vec![SessionTab {
9942 service: "CloudWatchLogGroups".to_string(),
9943 title: "Log Groups".to_string(),
9944 breadcrumb: "CloudWatch › Log Groups".to_string(),
9945 filter: Some("test".to_string()),
9946 selected_item: None,
9947 }],
9948 }];
9949
9950 app.mode = Mode::SessionPicker;
9951 app.session_picker_selected = 0;
9952
9953 app.handle_action(Action::Select);
9955
9956 assert_eq!(app.mode, Mode::Normal);
9957 assert_eq!(app.profile, "prod-profile");
9958 assert_eq!(app.region, "us-west-2");
9959 assert_eq!(app.config.account_id, "123456789");
9960 assert_eq!(app.tabs.len(), 1);
9961 assert_eq!(app.tabs[0].title, "Log Groups");
9962 }
9963
9964 #[test]
9965 fn test_save_session_creates_session() {
9966 let mut app =
9967 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
9968 app.config.account_id = "123456789".to_string();
9969 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
9970
9971 app.tabs.push(Tab {
9972 service: Service::CloudWatchLogGroups,
9973 title: "Log Groups".to_string(),
9974 breadcrumb: "CloudWatch › Log Groups".to_string(),
9975 });
9976
9977 app.save_current_session();
9978
9979 assert!(app.current_session.is_some());
9980 let session = app.current_session.clone().unwrap();
9981 assert_eq!(session.profile, "test-profile");
9982 assert_eq!(session.region, "us-east-1");
9983 assert_eq!(session.account_id, "123456789");
9984 assert_eq!(session.tabs.len(), 1);
9985
9986 let _ = session.delete();
9988 }
9989
9990 #[test]
9991 fn test_save_session_updates_existing() {
9992 let mut app =
9993 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
9994 app.config.account_id = "123456789".to_string();
9995 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
9996
9997 app.current_session = Some(Session {
9998 id: "existing".to_string(),
9999 timestamp: "2024-01-01".to_string(),
10000 profile: "test-profile".to_string(),
10001 region: "us-east-1".to_string(),
10002 account_id: "123456789".to_string(),
10003 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10004 tabs: vec![],
10005 });
10006
10007 app.tabs.push(Tab {
10008 service: Service::CloudWatchLogGroups,
10009 title: "Log Groups".to_string(),
10010 breadcrumb: "CloudWatch › Log Groups".to_string(),
10011 });
10012
10013 app.save_current_session();
10014
10015 let session = app.current_session.clone().unwrap();
10016 assert_eq!(session.id, "existing");
10017 assert_eq!(session.tabs.len(), 1);
10018
10019 let _ = session.delete();
10021 }
10022
10023 #[test]
10024 fn test_save_session_skips_empty_tabs() {
10025 let mut app =
10026 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10027 app.config.account_id = "123456789".to_string();
10028
10029 app.save_current_session();
10030
10031 assert!(app.current_session.is_none());
10032 }
10033
10034 #[test]
10035 fn test_save_session_deletes_when_tabs_closed() {
10036 let mut app =
10037 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10038 app.config.account_id = "123456789".to_string();
10039 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10040
10041 app.current_session = Some(Session {
10043 id: "test_delete".to_string(),
10044 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
10045 profile: "test-profile".to_string(),
10046 region: "us-east-1".to_string(),
10047 account_id: "123456789".to_string(),
10048 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10049 tabs: vec![],
10050 });
10051
10052 app.save_current_session();
10054
10055 assert!(app.current_session.is_none());
10056 }
10057
10058 #[test]
10059 fn test_closing_all_tabs_deletes_session() {
10060 let mut app =
10061 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10062 app.config.account_id = "123456789".to_string();
10063 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10064
10065 app.tabs.push(Tab {
10067 service: Service::CloudWatchLogGroups,
10068 title: "Log Groups".to_string(),
10069 breadcrumb: "CloudWatch › Log Groups".to_string(),
10070 });
10071
10072 app.save_current_session();
10074 assert!(app.current_session.is_some());
10075 let session_id = app.current_session.as_ref().unwrap().id.clone();
10076
10077 app.tabs.clear();
10079
10080 app.save_current_session();
10082 assert!(app.current_session.is_none());
10083
10084 let _ = Session::load(&session_id).map(|s| s.delete());
10086 }
10087
10088 #[test]
10089 fn test_credential_error_opens_profile_picker() {
10090 let mut app = App::new_without_client("default".to_string(), None);
10092 let error_str = "Unable to load credentials from any source";
10093
10094 if error_str.contains("credentials") {
10095 app.available_profiles = App::load_aws_profiles();
10096 app.mode = Mode::ProfilePicker;
10097 }
10098
10099 assert_eq!(app.mode, Mode::ProfilePicker);
10100 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10102 }
10103
10104 #[test]
10105 fn test_non_credential_error_shows_error_modal() {
10106 let mut app = App::new_without_client("default".to_string(), None);
10107 let error_str = "Network timeout";
10108
10109 if !error_str.contains("credentials") {
10110 app.error_message = Some(error_str.to_string());
10111 app.mode = Mode::ErrorModal;
10112 }
10113
10114 assert_eq!(app.mode, Mode::ErrorModal);
10115 assert!(app.error_message.is_some());
10116 }
10117
10118 #[tokio::test]
10119 async fn test_profile_selection_loads_credentials() {
10120 std::env::set_var("AWS_PROFILE", "default");
10122
10123 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
10125
10126 if let Ok(app) = result {
10127 assert!(!app.config.account_id.is_empty());
10129 assert!(!app.config.role_arn.is_empty());
10130 assert_eq!(app.profile, "default");
10131 assert_eq!(app.config.region, "us-east-1");
10132 }
10133 }
10135
10136 #[test]
10137 fn test_new_app_shows_service_picker_with_no_tabs() {
10138 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
10139
10140 assert!(!app.service_selected);
10142 assert_eq!(app.mode, Mode::ServicePicker);
10144 assert!(app.tabs.is_empty());
10146 }
10147
10148 #[tokio::test]
10149 async fn test_aws_profile_env_var_read_before_config_load() {
10150 std::env::set_var("AWS_PROFILE", "test-profile");
10152
10153 let profile_name = None
10155 .or_else(|| std::env::var("AWS_PROFILE").ok())
10156 .unwrap_or_else(|| "default".to_string());
10157
10158 assert_eq!(profile_name, "test-profile");
10160
10161 std::env::set_var("AWS_PROFILE", &profile_name);
10163
10164 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
10166
10167 std::env::remove_var("AWS_PROFILE");
10168 }
10169
10170 #[test]
10171 fn test_next_preferences_cloudformation() {
10172 let mut app = test_app();
10173 app.current_service = Service::CloudFormationStacks;
10174 app.mode = Mode::ColumnSelector;
10175 app.column_selector_index = 0;
10176
10177 let page_size_idx = app.cfn_column_ids.len() + 2;
10179 app.handle_action(Action::NextPreferences);
10180 assert_eq!(app.column_selector_index, page_size_idx);
10181
10182 app.handle_action(Action::NextPreferences);
10184 assert_eq!(app.column_selector_index, 0);
10185 }
10186
10187 #[test]
10188 fn test_s3_preferences_tab_cycling() {
10189 let mut app = test_app();
10190 app.current_service = Service::S3Buckets;
10191 app.mode = Mode::ColumnSelector;
10192 app.column_selector_index = 0;
10193
10194 let page_size_idx = app.s3_bucket_column_ids.len() + 2;
10195
10196 app.handle_action(Action::NextPreferences);
10198 assert_eq!(app.column_selector_index, page_size_idx);
10199
10200 app.handle_action(Action::NextPreferences);
10202 assert_eq!(app.column_selector_index, 0);
10203
10204 app.handle_action(Action::PrevPreferences);
10206 assert_eq!(app.column_selector_index, page_size_idx);
10207
10208 app.handle_action(Action::PrevPreferences);
10210 assert_eq!(app.column_selector_index, 0);
10211 }
10212
10213 #[test]
10214 fn test_s3_filter_resets_selection() {
10215 let mut app = test_app();
10216 app.current_service = Service::S3Buckets;
10217 app.service_selected = true;
10218
10219 app.s3_state.buckets.items = vec![
10221 S3Bucket {
10222 name: "bucket-1".to_string(),
10223 region: "us-east-1".to_string(),
10224 creation_date: "2023-01-01".to_string(),
10225 },
10226 S3Bucket {
10227 name: "bucket-2".to_string(),
10228 region: "us-east-1".to_string(),
10229 creation_date: "2023-01-02".to_string(),
10230 },
10231 S3Bucket {
10232 name: "other-bucket".to_string(),
10233 region: "us-east-1".to_string(),
10234 creation_date: "2023-01-03".to_string(),
10235 },
10236 ];
10237
10238 app.s3_state.selected_row = 1;
10240 app.s3_state.bucket_scroll_offset = 1;
10241
10242 app.mode = Mode::FilterInput;
10244 app.apply_filter_operation(|f| f.push_str("bucket-"));
10245
10246 assert_eq!(app.s3_state.selected_row, 0);
10248 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
10249 assert_eq!(app.s3_state.buckets.filter, "bucket-");
10250 }
10251
10252 #[test]
10253 fn test_s3_navigation_respects_filter() {
10254 let mut app = test_app();
10255 app.current_service = Service::S3Buckets;
10256 app.service_selected = true;
10257 app.mode = Mode::Normal;
10258
10259 app.s3_state.buckets.items = vec![
10261 S3Bucket {
10262 name: "prod-bucket".to_string(),
10263 region: "us-east-1".to_string(),
10264 creation_date: "2023-01-01".to_string(),
10265 },
10266 S3Bucket {
10267 name: "dev-bucket".to_string(),
10268 region: "us-east-1".to_string(),
10269 creation_date: "2023-01-02".to_string(),
10270 },
10271 S3Bucket {
10272 name: "prod-logs".to_string(),
10273 region: "us-east-1".to_string(),
10274 creation_date: "2023-01-03".to_string(),
10275 },
10276 ];
10277
10278 app.s3_state.buckets.filter = "prod".to_string();
10280
10281 assert_eq!(app.s3_state.selected_row, 0);
10283
10284 app.handle_action(Action::NextItem);
10286 assert_eq!(app.s3_state.selected_row, 1);
10287
10288 app.handle_action(Action::NextItem);
10290 assert_eq!(app.s3_state.selected_row, 1);
10291
10292 app.handle_action(Action::PrevItem);
10294 assert_eq!(app.s3_state.selected_row, 0);
10295 }
10296
10297 #[test]
10298 fn test_next_preferences_lambda_functions() {
10299 let mut app = test_app();
10300 app.current_service = Service::LambdaFunctions;
10301 app.mode = Mode::ColumnSelector;
10302 app.column_selector_index = 0;
10303
10304 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
10305 app.handle_action(Action::NextPreferences);
10306 assert_eq!(app.column_selector_index, page_size_idx);
10307
10308 app.handle_action(Action::NextPreferences);
10309 assert_eq!(app.column_selector_index, 0);
10310 }
10311
10312 #[test]
10313 fn test_next_preferences_lambda_applications() {
10314 let mut app = test_app();
10315 app.current_service = Service::LambdaApplications;
10316 app.mode = Mode::ColumnSelector;
10317 app.column_selector_index = 0;
10318
10319 let page_size_idx = app.lambda_application_column_ids.len() + 2;
10320 app.handle_action(Action::NextPreferences);
10321 assert_eq!(app.column_selector_index, page_size_idx);
10322
10323 app.handle_action(Action::NextPreferences);
10324 assert_eq!(app.column_selector_index, 0);
10325 }
10326
10327 #[test]
10328 fn test_next_preferences_ecr_images() {
10329 let mut app = test_app();
10330 app.current_service = Service::EcrRepositories;
10331 app.ecr_state.current_repository = Some("test-repo".to_string());
10332 app.mode = Mode::ColumnSelector;
10333 app.column_selector_index = 0;
10334
10335 let page_size_idx = app.ecr_image_column_ids.len() + 2;
10336 app.handle_action(Action::NextPreferences);
10337 assert_eq!(app.column_selector_index, page_size_idx);
10338
10339 app.handle_action(Action::NextPreferences);
10340 assert_eq!(app.column_selector_index, 0);
10341 }
10342
10343 #[test]
10344 fn test_next_preferences_cloudwatch_log_groups() {
10345 let mut app = test_app();
10346 app.current_service = Service::CloudWatchLogGroups;
10347 app.view_mode = ViewMode::List;
10348 app.mode = Mode::ColumnSelector;
10349 app.column_selector_index = 0;
10350
10351 let page_size_idx = app.cw_log_group_column_ids.len() + 2;
10353 app.handle_action(Action::NextPreferences);
10354 assert_eq!(app.column_selector_index, page_size_idx);
10355
10356 app.handle_action(Action::NextPreferences);
10358 assert_eq!(app.column_selector_index, 0);
10359
10360 app.handle_action(Action::PrevPreferences);
10362 assert_eq!(app.column_selector_index, page_size_idx);
10363
10364 app.handle_action(Action::PrevPreferences);
10366 assert_eq!(app.column_selector_index, 0);
10367 }
10368
10369 #[test]
10370 fn test_next_preferences_cloudwatch_log_streams() {
10371 let mut app = test_app();
10372 app.current_service = Service::CloudWatchLogGroups;
10373 app.view_mode = ViewMode::Detail;
10374 app.mode = Mode::ColumnSelector;
10375 app.column_selector_index = 0;
10376
10377 let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
10379 app.handle_action(Action::NextPreferences);
10380 assert_eq!(app.column_selector_index, page_size_idx);
10381
10382 app.handle_action(Action::NextPreferences);
10384 assert_eq!(app.column_selector_index, 0);
10385 }
10386
10387 #[test]
10388 fn test_cloudformation_next_item() {
10389 let mut app = test_app();
10390 app.current_service = Service::CloudFormationStacks;
10391 app.service_selected = true;
10392 app.mode = Mode::Normal;
10393 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10394 app.cfn_state.table.items = vec![
10395 CfnStack {
10396 name: "stack1".to_string(),
10397 stack_id: "id1".to_string(),
10398 status: "CREATE_COMPLETE".to_string(),
10399 created_time: "2024-01-01".to_string(),
10400 updated_time: String::new(),
10401 deleted_time: String::new(),
10402 drift_status: String::new(),
10403 last_drift_check_time: String::new(),
10404 status_reason: String::new(),
10405 description: String::new(),
10406 detailed_status: String::new(),
10407 root_stack: String::new(),
10408 parent_stack: String::new(),
10409 termination_protection: false,
10410 iam_role: String::new(),
10411 tags: Vec::new(),
10412 stack_policy: String::new(),
10413 rollback_monitoring_time: String::new(),
10414 rollback_alarms: Vec::new(),
10415 notification_arns: Vec::new(),
10416 },
10417 CfnStack {
10418 name: "stack2".to_string(),
10419 stack_id: "id2".to_string(),
10420 status: "UPDATE_COMPLETE".to_string(),
10421 created_time: "2024-01-02".to_string(),
10422 updated_time: String::new(),
10423 deleted_time: String::new(),
10424 drift_status: String::new(),
10425 last_drift_check_time: String::new(),
10426 status_reason: String::new(),
10427 description: String::new(),
10428 detailed_status: String::new(),
10429 root_stack: String::new(),
10430 parent_stack: String::new(),
10431 termination_protection: false,
10432 iam_role: String::new(),
10433 tags: Vec::new(),
10434 stack_policy: String::new(),
10435 rollback_monitoring_time: String::new(),
10436 rollback_alarms: Vec::new(),
10437 notification_arns: Vec::new(),
10438 },
10439 ];
10440 app.cfn_state.table.reset();
10441
10442 app.handle_action(Action::NextItem);
10443 assert_eq!(app.cfn_state.table.selected, 1);
10444
10445 app.handle_action(Action::NextItem);
10446 assert_eq!(app.cfn_state.table.selected, 1); }
10448
10449 #[test]
10450 fn test_cloudformation_prev_item() {
10451 let mut app = test_app();
10452 app.current_service = Service::CloudFormationStacks;
10453 app.service_selected = true;
10454 app.mode = Mode::Normal;
10455 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10456 app.cfn_state.table.items = vec![
10457 CfnStack {
10458 name: "stack1".to_string(),
10459 stack_id: "id1".to_string(),
10460 status: "CREATE_COMPLETE".to_string(),
10461 created_time: "2024-01-01".to_string(),
10462 updated_time: String::new(),
10463 deleted_time: String::new(),
10464 drift_status: String::new(),
10465 last_drift_check_time: String::new(),
10466 status_reason: String::new(),
10467 description: String::new(),
10468 detailed_status: String::new(),
10469 root_stack: String::new(),
10470 parent_stack: String::new(),
10471 termination_protection: false,
10472 iam_role: String::new(),
10473 tags: Vec::new(),
10474 stack_policy: String::new(),
10475 rollback_monitoring_time: String::new(),
10476 rollback_alarms: Vec::new(),
10477 notification_arns: Vec::new(),
10478 },
10479 CfnStack {
10480 name: "stack2".to_string(),
10481 stack_id: "id2".to_string(),
10482 status: "UPDATE_COMPLETE".to_string(),
10483 created_time: "2024-01-02".to_string(),
10484 updated_time: String::new(),
10485 deleted_time: String::new(),
10486 drift_status: String::new(),
10487 last_drift_check_time: String::new(),
10488 status_reason: String::new(),
10489 description: String::new(),
10490 detailed_status: String::new(),
10491 root_stack: String::new(),
10492 parent_stack: String::new(),
10493 termination_protection: false,
10494 iam_role: String::new(),
10495 tags: Vec::new(),
10496 stack_policy: String::new(),
10497 rollback_monitoring_time: String::new(),
10498 rollback_alarms: Vec::new(),
10499 notification_arns: Vec::new(),
10500 },
10501 ];
10502 app.cfn_state.table.selected = 1;
10503
10504 app.handle_action(Action::PrevItem);
10505 assert_eq!(app.cfn_state.table.selected, 0);
10506
10507 app.handle_action(Action::PrevItem);
10508 assert_eq!(app.cfn_state.table.selected, 0); }
10510
10511 #[test]
10512 fn test_cloudformation_page_down() {
10513 let mut app = test_app();
10514 app.current_service = Service::CloudFormationStacks;
10515 app.service_selected = true;
10516 app.mode = Mode::Normal;
10517 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10518
10519 for i in 0..20 {
10521 app.cfn_state.table.items.push(CfnStack {
10522 name: format!("stack{}", i),
10523 stack_id: format!("id{}", i),
10524 status: "CREATE_COMPLETE".to_string(),
10525 created_time: format!("2024-01-{:02}", i + 1),
10526 updated_time: String::new(),
10527 deleted_time: String::new(),
10528 drift_status: String::new(),
10529 last_drift_check_time: String::new(),
10530 status_reason: String::new(),
10531 description: String::new(),
10532 detailed_status: String::new(),
10533 root_stack: String::new(),
10534 parent_stack: String::new(),
10535 termination_protection: false,
10536 iam_role: String::new(),
10537 tags: Vec::new(),
10538 stack_policy: String::new(),
10539 rollback_monitoring_time: String::new(),
10540 rollback_alarms: Vec::new(),
10541 notification_arns: Vec::new(),
10542 });
10543 }
10544 app.cfn_state.table.reset();
10545
10546 app.handle_action(Action::PageDown);
10547 assert_eq!(app.cfn_state.table.selected, 10);
10548
10549 app.handle_action(Action::PageDown);
10550 assert_eq!(app.cfn_state.table.selected, 19); }
10552
10553 #[test]
10554 fn test_cloudformation_page_up() {
10555 let mut app = test_app();
10556 app.current_service = Service::CloudFormationStacks;
10557 app.service_selected = true;
10558 app.mode = Mode::Normal;
10559 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10560
10561 for i in 0..20 {
10563 app.cfn_state.table.items.push(CfnStack {
10564 name: format!("stack{}", i),
10565 stack_id: format!("id{}", i),
10566 status: "CREATE_COMPLETE".to_string(),
10567 created_time: format!("2024-01-{:02}", i + 1),
10568 updated_time: String::new(),
10569 deleted_time: String::new(),
10570 drift_status: String::new(),
10571 last_drift_check_time: String::new(),
10572 status_reason: String::new(),
10573 description: String::new(),
10574 detailed_status: String::new(),
10575 root_stack: String::new(),
10576 parent_stack: String::new(),
10577 termination_protection: false,
10578 iam_role: String::new(),
10579 tags: Vec::new(),
10580 stack_policy: String::new(),
10581 rollback_monitoring_time: String::new(),
10582 rollback_alarms: Vec::new(),
10583 notification_arns: Vec::new(),
10584 });
10585 }
10586 app.cfn_state.table.selected = 15;
10587
10588 app.handle_action(Action::PageUp);
10589 assert_eq!(app.cfn_state.table.selected, 5);
10590
10591 app.handle_action(Action::PageUp);
10592 assert_eq!(app.cfn_state.table.selected, 0); }
10594
10595 #[test]
10596 fn test_cloudformation_filter_input() {
10597 let mut app = test_app();
10598 app.current_service = Service::CloudFormationStacks;
10599 app.service_selected = true;
10600 app.mode = Mode::Normal;
10601
10602 app.handle_action(Action::StartFilter);
10603 assert_eq!(app.mode, Mode::FilterInput);
10604
10605 app.cfn_state.table.filter = "test".to_string();
10607 assert_eq!(app.cfn_state.table.filter, "test");
10608 }
10609
10610 #[test]
10611 fn test_cloudformation_filter_applies() {
10612 let mut app = test_app();
10613 app.current_service = Service::CloudFormationStacks;
10614 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10615 app.cfn_state.table.items = vec![
10616 CfnStack {
10617 name: "prod-stack".to_string(),
10618 stack_id: "id1".to_string(),
10619 status: "CREATE_COMPLETE".to_string(),
10620 created_time: "2024-01-01".to_string(),
10621 updated_time: String::new(),
10622 deleted_time: String::new(),
10623 drift_status: String::new(),
10624 last_drift_check_time: String::new(),
10625 status_reason: String::new(),
10626 description: "Production stack".to_string(),
10627 detailed_status: String::new(),
10628 root_stack: String::new(),
10629 parent_stack: String::new(),
10630 termination_protection: false,
10631 iam_role: String::new(),
10632 tags: Vec::new(),
10633 stack_policy: String::new(),
10634 rollback_monitoring_time: String::new(),
10635 rollback_alarms: Vec::new(),
10636 notification_arns: Vec::new(),
10637 },
10638 CfnStack {
10639 name: "dev-stack".to_string(),
10640 stack_id: "id2".to_string(),
10641 status: "UPDATE_COMPLETE".to_string(),
10642 created_time: "2024-01-02".to_string(),
10643 updated_time: String::new(),
10644 deleted_time: String::new(),
10645 drift_status: String::new(),
10646 last_drift_check_time: String::new(),
10647 status_reason: String::new(),
10648 description: "Development stack".to_string(),
10649 detailed_status: String::new(),
10650 root_stack: String::new(),
10651 parent_stack: String::new(),
10652 termination_protection: false,
10653 iam_role: String::new(),
10654 tags: Vec::new(),
10655 stack_policy: String::new(),
10656 rollback_monitoring_time: String::new(),
10657 rollback_alarms: Vec::new(),
10658 notification_arns: Vec::new(),
10659 },
10660 ];
10661 app.cfn_state.table.filter = "prod".to_string();
10662
10663 let filtered = filtered_cloudformation_stacks(&app);
10664 assert_eq!(filtered.len(), 1);
10665 assert_eq!(filtered[0].name, "prod-stack");
10666 }
10667
10668 #[test]
10669 fn test_cloudformation_right_arrow_expands() {
10670 let mut app = test_app();
10671 app.current_service = Service::CloudFormationStacks;
10672 app.service_selected = true;
10673 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10674 app.cfn_state.table.items = vec![CfnStack {
10675 name: "test-stack".to_string(),
10676 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
10677 .to_string(),
10678 status: "CREATE_COMPLETE".to_string(),
10679 created_time: "2024-01-01".to_string(),
10680 updated_time: String::new(),
10681 deleted_time: String::new(),
10682 drift_status: String::new(),
10683 last_drift_check_time: String::new(),
10684 status_reason: String::new(),
10685 description: "Test stack".to_string(),
10686 detailed_status: String::new(),
10687 root_stack: String::new(),
10688 parent_stack: String::new(),
10689 termination_protection: false,
10690 iam_role: String::new(),
10691 tags: Vec::new(),
10692 stack_policy: String::new(),
10693 rollback_monitoring_time: String::new(),
10694 rollback_alarms: Vec::new(),
10695 notification_arns: Vec::new(),
10696 }];
10697 app.cfn_state.table.reset();
10698
10699 assert_eq!(app.cfn_state.table.expanded_item, None);
10700
10701 app.handle_action(Action::NextPane);
10702 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
10703 }
10704
10705 #[test]
10706 fn test_cloudformation_left_arrow_collapses() {
10707 let mut app = test_app();
10708 app.current_service = Service::CloudFormationStacks;
10709 app.service_selected = true;
10710 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10711 app.cfn_state.table.items = vec![CfnStack {
10712 name: "test-stack".to_string(),
10713 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
10714 .to_string(),
10715 status: "CREATE_COMPLETE".to_string(),
10716 created_time: "2024-01-01".to_string(),
10717 updated_time: String::new(),
10718 deleted_time: String::new(),
10719 drift_status: String::new(),
10720 last_drift_check_time: String::new(),
10721 status_reason: String::new(),
10722 description: "Test stack".to_string(),
10723 detailed_status: String::new(),
10724 root_stack: String::new(),
10725 parent_stack: String::new(),
10726 termination_protection: false,
10727 iam_role: String::new(),
10728 tags: Vec::new(),
10729 stack_policy: String::new(),
10730 rollback_monitoring_time: String::new(),
10731 rollback_alarms: Vec::new(),
10732 notification_arns: Vec::new(),
10733 }];
10734 app.cfn_state.table.reset();
10735 app.cfn_state.table.expanded_item = Some(0);
10736
10737 app.handle_action(Action::PrevPane);
10738 assert_eq!(app.cfn_state.table.expanded_item, None);
10739 }
10740
10741 #[test]
10742 fn test_cloudformation_enter_drills_into_stack() {
10743 let mut app = test_app();
10744 app.current_service = Service::CloudFormationStacks;
10745 app.service_selected = true;
10746 app.mode = Mode::Normal;
10747 app.tabs = vec![Tab {
10748 service: Service::CloudFormationStacks,
10749 title: "CloudFormation › Stacks".to_string(),
10750 breadcrumb: "CloudFormation › Stacks".to_string(),
10751 }];
10752 app.current_tab = 0;
10753 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10754 app.cfn_state.table.items = vec![CfnStack {
10755 name: "test-stack".to_string(),
10756 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
10757 .to_string(),
10758 status: "CREATE_COMPLETE".to_string(),
10759 created_time: "2024-01-01".to_string(),
10760 updated_time: String::new(),
10761 deleted_time: String::new(),
10762 drift_status: String::new(),
10763 last_drift_check_time: String::new(),
10764 status_reason: String::new(),
10765 description: "Test stack".to_string(),
10766 detailed_status: String::new(),
10767 root_stack: String::new(),
10768 parent_stack: String::new(),
10769 termination_protection: false,
10770 iam_role: String::new(),
10771 tags: Vec::new(),
10772 stack_policy: String::new(),
10773 rollback_monitoring_time: String::new(),
10774 rollback_alarms: Vec::new(),
10775 notification_arns: Vec::new(),
10776 }];
10777 app.cfn_state.table.reset();
10778
10779 let filtered = filtered_cloudformation_stacks(&app);
10781 assert_eq!(filtered.len(), 1);
10782 assert_eq!(filtered[0].name, "test-stack");
10783
10784 assert_eq!(app.cfn_state.current_stack, None);
10785
10786 app.handle_action(Action::Select);
10788 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
10789 }
10790
10791 #[test]
10792 fn test_cloudformation_copy_to_clipboard() {
10793 let mut app = test_app();
10794 app.current_service = Service::CloudFormationStacks;
10795 app.service_selected = true;
10796 app.mode = Mode::Normal;
10797 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10798 app.cfn_state.table.items = vec![
10799 CfnStack {
10800 name: "stack1".to_string(),
10801 stack_id: "id1".to_string(),
10802 status: "CREATE_COMPLETE".to_string(),
10803 created_time: "2024-01-01".to_string(),
10804 updated_time: String::new(),
10805 deleted_time: String::new(),
10806 drift_status: String::new(),
10807 last_drift_check_time: String::new(),
10808 status_reason: String::new(),
10809 description: String::new(),
10810 detailed_status: String::new(),
10811 root_stack: String::new(),
10812 parent_stack: String::new(),
10813 termination_protection: false,
10814 iam_role: String::new(),
10815 tags: Vec::new(),
10816 stack_policy: String::new(),
10817 rollback_monitoring_time: String::new(),
10818 rollback_alarms: Vec::new(),
10819 notification_arns: Vec::new(),
10820 },
10821 CfnStack {
10822 name: "stack2".to_string(),
10823 stack_id: "id2".to_string(),
10824 status: "UPDATE_COMPLETE".to_string(),
10825 created_time: "2024-01-02".to_string(),
10826 updated_time: String::new(),
10827 deleted_time: String::new(),
10828 drift_status: String::new(),
10829 last_drift_check_time: String::new(),
10830 status_reason: String::new(),
10831 description: String::new(),
10832 detailed_status: String::new(),
10833 root_stack: String::new(),
10834 parent_stack: String::new(),
10835 termination_protection: false,
10836 iam_role: String::new(),
10837 tags: Vec::new(),
10838 stack_policy: String::new(),
10839 rollback_monitoring_time: String::new(),
10840 rollback_alarms: Vec::new(),
10841 notification_arns: Vec::new(),
10842 },
10843 ];
10844
10845 assert!(!app.snapshot_requested);
10846 app.handle_action(Action::CopyToClipboard);
10847
10848 assert!(app.snapshot_requested);
10850 }
10851
10852 #[test]
10853 fn test_cloudformation_expansion_shows_all_visible_columns() {
10854 let mut app = test_app();
10855 app.current_service = Service::CloudFormationStacks;
10856 app.cfn_state.status_filter = CfnStatusFilter::Complete;
10857 app.cfn_state.table.items = vec![CfnStack {
10858 name: "test-stack".to_string(),
10859 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
10860 .to_string(),
10861 status: "CREATE_COMPLETE".to_string(),
10862 created_time: "2024-01-01".to_string(),
10863 updated_time: "2024-01-02".to_string(),
10864 deleted_time: String::new(),
10865 drift_status: "IN_SYNC".to_string(),
10866 last_drift_check_time: "2024-01-03".to_string(),
10867 status_reason: String::new(),
10868 description: "Test description".to_string(),
10869 detailed_status: String::new(),
10870 root_stack: String::new(),
10871 parent_stack: String::new(),
10872 termination_protection: false,
10873 iam_role: String::new(),
10874 tags: Vec::new(),
10875 stack_policy: String::new(),
10876 rollback_monitoring_time: String::new(),
10877 rollback_alarms: Vec::new(),
10878 notification_arns: Vec::new(),
10879 }];
10880
10881 app.cfn_visible_column_ids = [
10883 CfnColumn::Name,
10884 CfnColumn::Status,
10885 CfnColumn::CreatedTime,
10886 CfnColumn::Description,
10887 ]
10888 .iter()
10889 .map(|c| c.id())
10890 .collect();
10891
10892 app.cfn_state.table.expanded_item = Some(0);
10893
10894 assert_eq!(app.cfn_visible_column_ids.len(), 4);
10897 assert!(app.cfn_state.table.has_expanded_item());
10898 }
10899
10900 #[test]
10901 fn test_cloudformation_empty_list_shows_page_1() {
10902 let mut app = test_app();
10903 app.current_service = Service::CloudFormationStacks;
10904 app.cfn_state.table.items = vec![];
10905
10906 let filtered = filtered_cloudformation_stacks(&app);
10907 assert_eq!(filtered.len(), 0);
10908
10909 let page_size = app.cfn_state.table.page_size.value();
10911 let total_pages = filtered.len().div_ceil(page_size);
10912 assert_eq!(total_pages, 0);
10913
10914 }
10917}
10918
10919impl App {
10920 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
10921 let mut all = AwsRegion::all();
10922
10923 for region in &mut all {
10925 region.latency_ms = self.region_latencies.get(region.code).copied();
10926 }
10927
10928 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
10930 all
10931 } else {
10932 let filter_lower = self.region_filter.to_lowercase();
10933 all.into_iter()
10934 .filter(|r| {
10935 r.name.to_lowercase().contains(&filter_lower)
10936 || r.code.to_lowercase().contains(&filter_lower)
10937 || r.group.to_lowercase().contains(&filter_lower)
10938 })
10939 .collect()
10940 };
10941
10942 let mut sorted = filtered;
10944 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
10945 sorted
10946 }
10947
10948 pub fn measure_region_latencies(&mut self) {
10949 use std::time::Instant;
10950 self.region_latencies.clear();
10951
10952 let regions = AwsRegion::all();
10953 let start_all = Instant::now();
10954 tracing::info!("Starting latency measurement for {} regions", regions.len());
10955
10956 let handles: Vec<_> = regions
10957 .iter()
10958 .map(|region| {
10959 let code = region.code.to_string();
10960 std::thread::spawn(move || {
10961 let endpoint = format!("https://sts.{}.amazonaws.com", code);
10963 let start = Instant::now();
10964
10965 match ureq::get(&endpoint)
10966 .timeout(std::time::Duration::from_secs(2))
10967 .call()
10968 {
10969 Ok(_) => {
10970 let latency = start.elapsed().as_millis() as u64;
10971 Some((code, latency))
10972 }
10973 Err(e) => {
10974 tracing::debug!("Failed to measure {}: {}", code, e);
10975 Some((code, 9999))
10976 }
10977 }
10978 })
10979 })
10980 .collect();
10981
10982 for handle in handles {
10983 if let Ok(Some((code, latency))) = handle.join() {
10984 self.region_latencies.insert(code, latency);
10985 }
10986 }
10987
10988 tracing::info!(
10989 "Measured {} regions in {:?}",
10990 self.region_latencies.len(),
10991 start_all.elapsed()
10992 );
10993 }
10994
10995 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
10996 filter_profiles(&self.available_profiles, &self.profile_filter)
10997 }
10998
10999 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
11000 if self.session_filter.is_empty() {
11001 return self.sessions.iter().collect();
11002 }
11003 let filter_lower = self.session_filter.to_lowercase();
11004 self.sessions
11005 .iter()
11006 .filter(|s| {
11007 s.profile.to_lowercase().contains(&filter_lower)
11008 || s.region.to_lowercase().contains(&filter_lower)
11009 || s.account_id.to_lowercase().contains(&filter_lower)
11010 || s.role_arn.to_lowercase().contains(&filter_lower)
11011 })
11012 .collect()
11013 }
11014
11015 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
11016 if self.tab_filter.is_empty() {
11017 return self.tabs.iter().enumerate().collect();
11018 }
11019 let filter_lower = self.tab_filter.to_lowercase();
11020 self.tabs
11021 .iter()
11022 .enumerate()
11023 .filter(|(_, tab)| {
11024 tab.title.to_lowercase().contains(&filter_lower)
11025 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
11026 })
11027 .collect()
11028 }
11029
11030 pub fn load_aws_profiles() -> Vec<AwsProfile> {
11031 AwsProfile::load_all()
11032 }
11033
11034 pub async fn fetch_profile_accounts(&mut self) {
11035 for profile in &mut self.available_profiles {
11036 if profile.account.is_none() {
11037 let region = profile
11038 .region
11039 .clone()
11040 .unwrap_or_else(|| "us-east-1".to_string());
11041 if let Ok(account) =
11042 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
11043 {
11044 profile.account = Some(account);
11045 }
11046 }
11047 }
11048 }
11049
11050 fn save_current_session(&mut self) {
11051 if self.tabs.is_empty() {
11053 if let Some(ref session) = self.current_session {
11054 let _ = session.delete();
11055 self.current_session = None;
11056 }
11057 return;
11058 }
11059
11060 let session = if let Some(ref mut current) = self.current_session {
11061 current.tabs = self
11063 .tabs
11064 .iter()
11065 .map(|t| SessionTab {
11066 service: format!("{:?}", t.service),
11067 title: t.title.clone(),
11068 breadcrumb: t.breadcrumb.clone(),
11069 filter: match t.service {
11070 Service::CloudWatchLogGroups => {
11071 Some(self.log_groups_state.log_groups.filter.clone())
11072 }
11073 _ => None,
11074 },
11075 selected_item: None,
11076 })
11077 .collect();
11078 current.clone()
11079 } else {
11080 let mut session = Session::new(
11082 self.profile.clone(),
11083 self.region.clone(),
11084 self.config.account_id.clone(),
11085 self.config.role_arn.clone(),
11086 );
11087 session.tabs = self
11088 .tabs
11089 .iter()
11090 .map(|t| SessionTab {
11091 service: format!("{:?}", t.service),
11092 title: t.title.clone(),
11093 breadcrumb: t.breadcrumb.clone(),
11094 filter: match t.service {
11095 Service::CloudWatchLogGroups => {
11096 Some(self.log_groups_state.log_groups.filter.clone())
11097 }
11098 _ => None,
11099 },
11100 selected_item: None,
11101 })
11102 .collect();
11103 self.current_session = Some(session.clone());
11104 session
11105 };
11106
11107 let _ = session.save();
11108 }
11109}
11110
11111#[cfg(test)]
11112mod iam_policy_view_tests {
11113 use super::*;
11114 use test_helpers::*;
11115
11116 #[test]
11117 fn test_enter_opens_policy_view() {
11118 let mut app = test_app();
11119 app.current_service = Service::IamRoles;
11120 app.service_selected = true;
11121 app.mode = Mode::Normal;
11122 app.view_mode = ViewMode::Detail;
11123 app.iam_state.current_role = Some("TestRole".to_string());
11124 app.iam_state.policies.items = vec![IamPolicy {
11125 policy_name: "TestPolicy".to_string(),
11126 policy_type: "Inline".to_string(),
11127 attached_via: "Direct".to_string(),
11128 attached_entities: "1".to_string(),
11129 description: "Test".to_string(),
11130 creation_time: "2023-01-01".to_string(),
11131 edited_time: "2023-01-01".to_string(),
11132 policy_arn: None,
11133 }];
11134 app.iam_state.policies.reset();
11135
11136 app.handle_action(Action::Select);
11137
11138 assert_eq!(app.view_mode, ViewMode::PolicyView);
11139 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
11140 assert_eq!(app.iam_state.policy_scroll, 0);
11141 assert!(app.iam_state.policies.loading);
11142 }
11143
11144 #[test]
11145 fn test_escape_closes_policy_view() {
11146 let mut app = test_app();
11147 app.current_service = Service::IamRoles;
11148 app.service_selected = true;
11149 app.mode = Mode::Normal;
11150 app.view_mode = ViewMode::PolicyView;
11151 app.iam_state.current_role = Some("TestRole".to_string());
11152 app.iam_state.current_policy = Some("TestPolicy".to_string());
11153 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
11154 app.iam_state.policy_scroll = 5;
11155
11156 app.handle_action(Action::PrevPane);
11157
11158 assert_eq!(app.view_mode, ViewMode::Detail);
11159 assert_eq!(app.iam_state.current_policy, None);
11160 assert_eq!(app.iam_state.policy_document, "");
11161 assert_eq!(app.iam_state.policy_scroll, 0);
11162 }
11163
11164 #[test]
11165 fn test_ctrl_d_scrolls_down_in_policy_view() {
11166 let mut app = test_app();
11167 app.current_service = Service::IamRoles;
11168 app.service_selected = true;
11169 app.mode = Mode::Normal;
11170 app.view_mode = ViewMode::PolicyView;
11171 app.iam_state.current_role = Some("TestRole".to_string());
11172 app.iam_state.current_policy = Some("TestPolicy".to_string());
11173 app.iam_state.policy_document = (0..100)
11174 .map(|i| format!("line {}", i))
11175 .collect::<Vec<_>>()
11176 .join("\n");
11177 app.iam_state.policy_scroll = 0;
11178
11179 app.handle_action(Action::ScrollDown);
11180
11181 assert_eq!(app.iam_state.policy_scroll, 10);
11182
11183 app.handle_action(Action::ScrollDown);
11184
11185 assert_eq!(app.iam_state.policy_scroll, 20);
11186 }
11187
11188 #[test]
11189 fn test_ctrl_u_scrolls_up_in_policy_view() {
11190 let mut app = test_app();
11191 app.current_service = Service::IamRoles;
11192 app.service_selected = true;
11193 app.mode = Mode::Normal;
11194 app.view_mode = ViewMode::PolicyView;
11195 app.iam_state.current_role = Some("TestRole".to_string());
11196 app.iam_state.current_policy = Some("TestPolicy".to_string());
11197 app.iam_state.policy_document = (0..100)
11198 .map(|i| format!("line {}", i))
11199 .collect::<Vec<_>>()
11200 .join("\n");
11201 app.iam_state.policy_scroll = 30;
11202
11203 app.handle_action(Action::ScrollUp);
11204
11205 assert_eq!(app.iam_state.policy_scroll, 20);
11206
11207 app.handle_action(Action::ScrollUp);
11208
11209 assert_eq!(app.iam_state.policy_scroll, 10);
11210 }
11211
11212 #[test]
11213 fn test_scroll_does_not_go_negative() {
11214 let mut app = test_app();
11215 app.current_service = Service::IamRoles;
11216 app.service_selected = true;
11217 app.mode = Mode::Normal;
11218 app.view_mode = ViewMode::PolicyView;
11219 app.iam_state.current_role = Some("TestRole".to_string());
11220 app.iam_state.current_policy = Some("TestPolicy".to_string());
11221 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11222 app.iam_state.policy_scroll = 0;
11223
11224 app.handle_action(Action::ScrollUp);
11225
11226 assert_eq!(app.iam_state.policy_scroll, 0);
11227 }
11228
11229 #[test]
11230 fn test_scroll_does_not_exceed_max() {
11231 let mut app = test_app();
11232 app.current_service = Service::IamRoles;
11233 app.service_selected = true;
11234 app.mode = Mode::Normal;
11235 app.view_mode = ViewMode::PolicyView;
11236 app.iam_state.current_role = Some("TestRole".to_string());
11237 app.iam_state.current_policy = Some("TestPolicy".to_string());
11238 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11239 app.iam_state.policy_scroll = 0;
11240
11241 app.handle_action(Action::ScrollDown);
11242
11243 assert_eq!(app.iam_state.policy_scroll, 2); }
11245
11246 #[test]
11247 fn test_policy_view_console_url() {
11248 let mut app = test_app();
11249 app.current_service = Service::IamRoles;
11250 app.service_selected = true;
11251 app.view_mode = ViewMode::PolicyView;
11252 app.iam_state.current_role = Some("TestRole".to_string());
11253 app.iam_state.current_policy = Some("TestPolicy".to_string());
11254
11255 let url = app.get_console_url();
11256
11257 assert!(url.contains("us-east-1.console.aws.amazon.com"));
11258 assert!(url.contains("/roles/details/TestRole"));
11259 assert!(url.contains("/editPolicy/TestPolicy"));
11260 assert!(url.contains("step=addPermissions"));
11261 }
11262
11263 #[test]
11264 fn test_esc_from_policy_view_goes_to_role_detail() {
11265 let mut app = test_app();
11266 app.current_service = Service::IamRoles;
11267 app.service_selected = true;
11268 app.mode = Mode::Normal;
11269 app.view_mode = ViewMode::PolicyView;
11270 app.iam_state.current_role = Some("TestRole".to_string());
11271 app.iam_state.current_policy = Some("TestPolicy".to_string());
11272 app.iam_state.policy_document = "test".to_string();
11273 app.iam_state.policy_scroll = 5;
11274
11275 app.handle_action(Action::GoBack);
11276
11277 assert_eq!(app.view_mode, ViewMode::Detail);
11278 assert_eq!(app.iam_state.current_policy, None);
11279 assert_eq!(app.iam_state.policy_document, "");
11280 assert_eq!(app.iam_state.policy_scroll, 0);
11281 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
11282 }
11283
11284 #[test]
11285 fn test_esc_from_role_detail_goes_to_role_list() {
11286 let mut app = test_app();
11287 app.current_service = Service::IamRoles;
11288 app.service_selected = true;
11289 app.mode = Mode::Normal;
11290 app.view_mode = ViewMode::Detail;
11291 app.iam_state.current_role = Some("TestRole".to_string());
11292
11293 app.handle_action(Action::GoBack);
11294
11295 assert_eq!(app.iam_state.current_role, None);
11296 }
11297
11298 #[test]
11299 fn test_right_arrow_expands_policy_row() {
11300 let mut app = test_app();
11301 app.current_service = Service::IamRoles;
11302 app.service_selected = true;
11303 app.mode = Mode::Normal;
11304 app.view_mode = ViewMode::Detail;
11305 app.iam_state.current_role = Some("TestRole".to_string());
11306 app.iam_state.policies.items = vec![IamPolicy {
11307 policy_name: "TestPolicy".to_string(),
11308 policy_type: "Inline".to_string(),
11309 attached_via: "Direct".to_string(),
11310 attached_entities: "1".to_string(),
11311 description: "Test".to_string(),
11312 creation_time: "2023-01-01".to_string(),
11313 edited_time: "2023-01-01".to_string(),
11314 policy_arn: None,
11315 }];
11316 app.iam_state.policies.reset();
11317
11318 app.handle_action(Action::NextPane);
11319
11320 assert_eq!(app.view_mode, ViewMode::Detail);
11322 assert_eq!(app.iam_state.current_policy, None);
11323 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
11324 }
11325
11326 #[test]
11327 fn test_managed_policy_has_creation_and_edit_time() {
11328 use crate::iam::Policy as IamPolicy;
11332 let managed = IamPolicy {
11333 policy_name: "MyManagedPolicy".to_string(),
11334 policy_type: "Managed".to_string(),
11335 attached_via: "Direct".to_string(),
11336 attached_entities: "-".to_string(),
11337 description: "-".to_string(),
11338 creation_time: "2024-01-01 00:00:00 (UTC)".to_string(),
11339 edited_time: "2024-06-01 00:00:00 (UTC)".to_string(),
11340 policy_arn: Some("arn:aws:iam::123:policy/MyManagedPolicy".to_string()),
11341 };
11342 assert_ne!(
11343 managed.creation_time, "-",
11344 "Managed policy must have a real creation time, not '-'"
11345 );
11346 assert_ne!(
11347 managed.edited_time, "-",
11348 "Managed policy must have a real edit time, not '-'"
11349 );
11350
11351 let inline = IamPolicy {
11353 policy_name: "InlinePolicy".to_string(),
11354 policy_type: "Inline".to_string(),
11355 attached_via: "Direct".to_string(),
11356 attached_entities: "-".to_string(),
11357 description: "-".to_string(),
11358 creation_time: "-".to_string(),
11359 edited_time: "-".to_string(),
11360 policy_arn: None,
11361 };
11362 assert_eq!(
11363 inline.creation_time, "-",
11364 "Inline policies have no creation time"
11365 );
11366 }
11367}
11368
11369#[cfg(test)]
11370mod tab_filter_tests {
11371 use super::*;
11372 use test_helpers::*;
11373
11374 #[test]
11375 fn test_space_t_opens_tab_picker() {
11376 let mut app = test_app();
11377 app.tabs = vec![
11378 Tab {
11379 service: Service::CloudWatchLogGroups,
11380 title: "Tab 1".to_string(),
11381 breadcrumb: "CloudWatch > Log groups".to_string(),
11382 },
11383 Tab {
11384 service: Service::S3Buckets,
11385 title: "Tab 2".to_string(),
11386 breadcrumb: "S3 › Buckets".to_string(),
11387 },
11388 ];
11389 app.current_tab = 0;
11390
11391 app.handle_action(Action::OpenTabPicker);
11392
11393 assert_eq!(app.mode, Mode::TabPicker);
11394 assert_eq!(app.tab_picker_selected, 0);
11395 }
11396
11397 #[test]
11398 fn test_tab_filter_works() {
11399 let mut app = test_app();
11400 app.tabs = vec![
11401 Tab {
11402 service: Service::CloudWatchLogGroups,
11403 title: "CloudWatch Logs".to_string(),
11404 breadcrumb: "CloudWatch > Log groups".to_string(),
11405 },
11406 Tab {
11407 service: Service::S3Buckets,
11408 title: "S3 Buckets".to_string(),
11409 breadcrumb: "S3 › Buckets".to_string(),
11410 },
11411 Tab {
11412 service: Service::CloudWatchAlarms,
11413 title: "CloudWatch Alarms".to_string(),
11414 breadcrumb: "CloudWatch › Alarms".to_string(),
11415 },
11416 ];
11417 app.mode = Mode::TabPicker;
11418
11419 app.handle_action(Action::FilterInput('s'));
11421 app.handle_action(Action::FilterInput('3'));
11422
11423 let filtered = app.get_filtered_tabs();
11424 assert_eq!(filtered.len(), 1);
11425 assert_eq!(filtered[0].1.title, "S3 Buckets");
11426 }
11427
11428 #[test]
11429 fn test_tab_filter_by_breadcrumb() {
11430 let mut app = test_app();
11431 app.tabs = vec![
11432 Tab {
11433 service: Service::CloudWatchLogGroups,
11434 title: "Tab 1".to_string(),
11435 breadcrumb: "CloudWatch > Log groups".to_string(),
11436 },
11437 Tab {
11438 service: Service::S3Buckets,
11439 title: "Tab 2".to_string(),
11440 breadcrumb: "S3 › Buckets".to_string(),
11441 },
11442 ];
11443 app.mode = Mode::TabPicker;
11444
11445 app.handle_action(Action::FilterInput('c'));
11447 app.handle_action(Action::FilterInput('l'));
11448 app.handle_action(Action::FilterInput('o'));
11449 app.handle_action(Action::FilterInput('u'));
11450 app.handle_action(Action::FilterInput('d'));
11451
11452 let filtered = app.get_filtered_tabs();
11453 assert_eq!(filtered.len(), 1);
11454 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
11455 }
11456
11457 #[test]
11458 fn test_tab_filter_backspace() {
11459 let mut app = test_app();
11460 app.tabs = vec![
11461 Tab {
11462 service: Service::CloudWatchLogGroups,
11463 title: "CloudWatch Logs".to_string(),
11464 breadcrumb: "CloudWatch > Log groups".to_string(),
11465 },
11466 Tab {
11467 service: Service::S3Buckets,
11468 title: "S3 Buckets".to_string(),
11469 breadcrumb: "S3 › Buckets".to_string(),
11470 },
11471 ];
11472 app.mode = Mode::TabPicker;
11473
11474 app.handle_action(Action::FilterInput('s'));
11475 app.handle_action(Action::FilterInput('3'));
11476 assert_eq!(app.tab_filter, "s3");
11477
11478 app.handle_action(Action::FilterBackspace);
11479 assert_eq!(app.tab_filter, "s");
11480
11481 let filtered = app.get_filtered_tabs();
11482 assert_eq!(filtered.len(), 2); }
11484
11485 #[test]
11486 fn test_tab_selection_with_filter() {
11487 let mut app = test_app();
11488 app.tabs = vec![
11489 Tab {
11490 service: Service::CloudWatchLogGroups,
11491 title: "CloudWatch Logs".to_string(),
11492 breadcrumb: "CloudWatch > Log groups".to_string(),
11493 },
11494 Tab {
11495 service: Service::S3Buckets,
11496 title: "S3 Buckets".to_string(),
11497 breadcrumb: "S3 › Buckets".to_string(),
11498 },
11499 ];
11500 app.mode = Mode::TabPicker;
11501 app.current_tab = 0;
11502
11503 app.handle_action(Action::FilterInput('s'));
11505 app.handle_action(Action::FilterInput('3'));
11506
11507 app.handle_action(Action::Select);
11509
11510 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
11512 assert_eq!(app.tab_filter, ""); }
11514}
11515
11516#[cfg(test)]
11517mod region_latency_tests {
11518 use super::*;
11519 use test_helpers::*;
11520
11521 #[test]
11522 fn test_regions_sorted_by_latency() {
11523 let mut app = test_app();
11524
11525 app.region_latencies.insert("us-west-2".to_string(), 50);
11527 app.region_latencies.insert("us-east-1".to_string(), 10);
11528 app.region_latencies.insert("eu-west-1".to_string(), 100);
11529
11530 let filtered = app.get_filtered_regions();
11531
11532 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
11534
11535 assert!(with_latency.len() >= 3);
11536 assert_eq!(with_latency[0].code, "us-east-1");
11537 assert_eq!(with_latency[0].latency_ms, Some(10));
11538 assert_eq!(with_latency[1].code, "us-west-2");
11539 assert_eq!(with_latency[1].latency_ms, Some(50));
11540 assert_eq!(with_latency[2].code, "eu-west-1");
11541 assert_eq!(with_latency[2].latency_ms, Some(100));
11542 }
11543
11544 #[test]
11545 fn test_regions_with_latency_before_without() {
11546 let mut app = test_app();
11547
11548 app.region_latencies.insert("eu-west-1".to_string(), 100);
11550
11551 let filtered = app.get_filtered_regions();
11552
11553 assert_eq!(filtered[0].code, "eu-west-1");
11555 assert_eq!(filtered[0].latency_ms, Some(100));
11556
11557 for region in &filtered[1..] {
11559 assert!(region.latency_ms.is_none());
11560 }
11561 }
11562
11563 #[test]
11564 fn test_region_filter_with_latency() {
11565 let mut app = test_app();
11566
11567 app.region_latencies.insert("us-east-1".to_string(), 10);
11568 app.region_latencies.insert("us-west-2".to_string(), 50);
11569 app.region_filter = "us".to_string();
11570
11571 let filtered = app.get_filtered_regions();
11572
11573 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
11575 assert_eq!(filtered[0].code, "us-east-1");
11576 assert_eq!(filtered[1].code, "us-west-2");
11577 }
11578
11579 #[test]
11580 fn test_latency_persists_across_filters() {
11581 let mut app = test_app();
11582
11583 app.region_latencies.insert("us-east-1".to_string(), 10);
11584
11585 app.region_filter = "eu".to_string();
11587 let filtered = app.get_filtered_regions();
11588 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
11589
11590 app.region_filter.clear();
11592 let all = app.get_filtered_regions();
11593
11594 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
11596 assert_eq!(us_east.latency_ms, Some(10));
11597 }
11598
11599 #[test]
11600 fn test_measure_region_latencies_clears_previous() {
11601 let mut app = test_app();
11602
11603 app.region_latencies.insert("us-east-1".to_string(), 100);
11605 app.region_latencies.insert("eu-west-1".to_string(), 200);
11606
11607 app.measure_region_latencies();
11609
11610 assert!(
11612 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
11613 );
11614 }
11615
11616 #[test]
11617 fn test_regions_with_latency_sorted_first() {
11618 let mut app = test_app();
11619
11620 app.region_latencies.insert("us-east-1".to_string(), 50);
11622 app.region_latencies.insert("eu-west-1".to_string(), 500);
11623
11624 let filtered = app.get_filtered_regions();
11625
11626 assert!(filtered.len() > 2);
11628
11629 assert_eq!(filtered[0].code, "us-east-1");
11631 assert_eq!(filtered[0].latency_ms, Some(50));
11632 assert_eq!(filtered[1].code, "eu-west-1");
11633 assert_eq!(filtered[1].latency_ms, Some(500));
11634
11635 for region in &filtered[2..] {
11637 assert!(region.latency_ms.is_none());
11638 }
11639 }
11640
11641 #[test]
11642 fn test_regions_without_latency_sorted_as_1000ms() {
11643 let mut app = test_app();
11644
11645 app.region_latencies
11647 .insert("ap-southeast-2".to_string(), 1500);
11648 app.region_latencies.insert("us-east-1".to_string(), 50);
11650
11651 let filtered = app.get_filtered_regions();
11652
11653 assert_eq!(filtered[0].code, "us-east-1");
11655 assert_eq!(filtered[0].latency_ms, Some(50));
11656
11657 let slow_region_idx = filtered
11659 .iter()
11660 .position(|r| r.code == "ap-southeast-2")
11661 .unwrap();
11662 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
11666 assert!(region.latency_ms.is_none());
11667 }
11668 }
11669
11670 #[test]
11671 fn test_region_picker_opens_with_latencies() {
11672 let mut app = test_app();
11673
11674 app.region_filter.clear();
11676 app.region_picker_selected = 0;
11677 app.measure_region_latencies();
11678
11679 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
11682 }
11683
11684 #[test]
11685 fn test_region_picker_enter_in_filter_mode_exits_insert_not_selects() {
11686 let mut app = test_app();
11690 app.mode = Mode::RegionPicker;
11691 app.region_filter_active = true; app.region_filter = "us".to_string();
11693 let original_region = app.region.clone();
11694
11695 app.handle_action(Action::Select);
11697
11698 assert!(
11699 !app.region_filter_active,
11700 "Filter must be deactivated (switch to NORMAL/table focus)"
11701 );
11702 assert_eq!(
11703 app.mode,
11704 Mode::RegionPicker,
11705 "Must stay in RegionPicker, not close it"
11706 );
11707 assert_eq!(
11708 app.region, original_region,
11709 "Region must NOT change when Enter is pressed in INSERT mode"
11710 );
11711 }
11712
11713 #[test]
11714 fn test_region_picker_enter_in_normal_mode_selects_region() {
11715 let mut app = test_app();
11717 app.mode = Mode::RegionPicker;
11718 app.region_filter_active = false; app.region_filter.clear();
11720
11721 app.handle_action(Action::Select);
11722
11723 assert_eq!(
11725 app.mode,
11726 Mode::Normal,
11727 "Must close picker after selecting region in NORMAL mode"
11728 );
11729 }
11730
11731 #[test]
11732 fn test_region_filter_can_type_any_character_in_insert_mode() {
11733 use crate::keymap::{handle_key, Mode as KMode};
11737 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
11738
11739 let mut app = test_app();
11740 app.mode = Mode::RegionPicker;
11741 app.region_filter_active = true;
11742 app.region_filter.clear();
11743
11744 let t_key = KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE);
11746 let o_key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
11747 let k_key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
11748
11749 if let Some(action) = handle_key(t_key, KMode::RegionPicker) {
11751 app.handle_action(action);
11752 }
11753 if let Some(action) = handle_key(o_key, KMode::RegionPicker) {
11754 app.handle_action(action);
11755 }
11756 if let Some(action) = handle_key(k_key, KMode::RegionPicker) {
11757 app.handle_action(action);
11758 }
11759
11760 assert_eq!(
11761 app.region_filter, "tok",
11762 "'k' must be typed into filter (not navigate up) when filter is active"
11763 );
11764 }
11765
11766 #[test]
11767 fn test_ecr_tab_next() {
11768 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
11769 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
11770 }
11771
11772 #[test]
11773 fn test_ecr_tab_switching() {
11774 let mut app = test_app();
11775 app.current_service = Service::EcrRepositories;
11776 app.service_selected = true;
11777 app.ecr_state.tab = EcrTab::Private;
11778
11779 app.handle_action(Action::NextDetailTab);
11780 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11781 assert_eq!(app.ecr_state.repositories.selected, 0);
11782
11783 app.handle_action(Action::NextDetailTab);
11784 assert_eq!(app.ecr_state.tab, EcrTab::Private);
11785 }
11786
11787 #[test]
11788 fn test_ecr_navigation() {
11789 let mut app = test_app();
11790 app.current_service = Service::EcrRepositories;
11791 app.service_selected = true;
11792 app.mode = Mode::Normal;
11793 app.ecr_state.repositories.items = vec![
11794 EcrRepository {
11795 name: "repo1".to_string(),
11796 uri: "uri1".to_string(),
11797 created_at: "2023-01-01".to_string(),
11798 tag_immutability: "MUTABLE".to_string(),
11799 encryption_type: "AES256".to_string(),
11800 },
11801 EcrRepository {
11802 name: "repo2".to_string(),
11803 uri: "uri2".to_string(),
11804 created_at: "2023-01-02".to_string(),
11805 tag_immutability: "IMMUTABLE".to_string(),
11806 encryption_type: "KMS".to_string(),
11807 },
11808 ];
11809
11810 app.handle_action(Action::NextItem);
11811 assert_eq!(app.ecr_state.repositories.selected, 1);
11812
11813 app.handle_action(Action::PrevItem);
11814 assert_eq!(app.ecr_state.repositories.selected, 0);
11815 }
11816
11817 #[test]
11818 fn test_ecr_filter() {
11819 let mut app = test_app();
11820 app.current_service = Service::EcrRepositories;
11821 app.service_selected = true;
11822 app.ecr_state.repositories.items = vec![
11823 EcrRepository {
11824 name: "my-app".to_string(),
11825 uri: "uri1".to_string(),
11826 created_at: "2023-01-01".to_string(),
11827 tag_immutability: "MUTABLE".to_string(),
11828 encryption_type: "AES256".to_string(),
11829 },
11830 EcrRepository {
11831 name: "other-service".to_string(),
11832 uri: "uri2".to_string(),
11833 created_at: "2023-01-02".to_string(),
11834 tag_immutability: "IMMUTABLE".to_string(),
11835 encryption_type: "KMS".to_string(),
11836 },
11837 ];
11838
11839 app.ecr_state.repositories.filter = "app".to_string();
11840 let filtered = filtered_ecr_repositories(&app);
11841 assert_eq!(filtered.len(), 1);
11842 assert_eq!(filtered[0].name, "my-app");
11843 }
11844
11845 #[test]
11846 fn test_ecr_filter_input() {
11847 let mut app = test_app();
11848 app.current_service = Service::EcrRepositories;
11849 app.service_selected = true;
11850 app.mode = Mode::FilterInput;
11851
11852 app.handle_action(Action::FilterInput('t'));
11853 app.handle_action(Action::FilterInput('e'));
11854 app.handle_action(Action::FilterInput('s'));
11855 app.handle_action(Action::FilterInput('t'));
11856 assert_eq!(app.ecr_state.repositories.filter, "test");
11857
11858 app.handle_action(Action::FilterBackspace);
11859 assert_eq!(app.ecr_state.repositories.filter, "tes");
11860 }
11861
11862 #[test]
11863 fn test_ecr_filter_resets_selection() {
11864 let mut app = test_app();
11865 app.current_service = Service::EcrRepositories;
11866 app.service_selected = true;
11867 app.mode = Mode::FilterInput;
11868 app.ecr_state.repositories.items = vec![
11869 EcrRepository {
11870 name: "repo1".to_string(),
11871 uri: "uri1".to_string(),
11872 created_at: "2023-01-01".to_string(),
11873 tag_immutability: "MUTABLE".to_string(),
11874 encryption_type: "AES256".to_string(),
11875 },
11876 EcrRepository {
11877 name: "repo2".to_string(),
11878 uri: "uri2".to_string(),
11879 created_at: "2023-01-02".to_string(),
11880 tag_immutability: "IMMUTABLE".to_string(),
11881 encryption_type: "KMS".to_string(),
11882 },
11883 EcrRepository {
11884 name: "repo3".to_string(),
11885 uri: "uri3".to_string(),
11886 created_at: "2023-01-03".to_string(),
11887 tag_immutability: "MUTABLE".to_string(),
11888 encryption_type: "AES256".to_string(),
11889 },
11890 ];
11891
11892 app.ecr_state.repositories.selected = 2;
11894 assert_eq!(app.ecr_state.repositories.selected, 2);
11895
11896 app.handle_action(Action::FilterInput('t'));
11898 assert_eq!(app.ecr_state.repositories.filter, "t");
11899 assert_eq!(app.ecr_state.repositories.selected, 0);
11900
11901 app.ecr_state.repositories.selected = 1;
11903
11904 app.handle_action(Action::FilterBackspace);
11906 assert_eq!(app.ecr_state.repositories.filter, "");
11907 assert_eq!(app.ecr_state.repositories.selected, 0);
11908 }
11909
11910 #[test]
11911 fn test_ecr_images_filter_resets_selection() {
11912 let mut app = test_app();
11913 app.current_service = Service::EcrRepositories;
11914 app.service_selected = true;
11915 app.mode = Mode::FilterInput;
11916 app.ecr_state.current_repository = Some("test-repo".to_string());
11917 app.ecr_state.images.items = vec![
11918 EcrImage {
11919 tag: "v1.0.0".to_string(),
11920 artifact_type: "container".to_string(),
11921 digest: "sha256:abc123".to_string(),
11922 pushed_at: "2023-01-01".to_string(),
11923 size_bytes: 1000,
11924 uri: "uri1".to_string(),
11925 last_pull_time: "".to_string(),
11926 },
11927 EcrImage {
11928 tag: "v2.0.0".to_string(),
11929 artifact_type: "container".to_string(),
11930 digest: "sha256:def456".to_string(),
11931 pushed_at: "2023-01-02".to_string(),
11932 size_bytes: 2000,
11933 uri: "uri2".to_string(),
11934 last_pull_time: "".to_string(),
11935 },
11936 ];
11937
11938 app.ecr_state.images.selected = 1;
11940 assert_eq!(app.ecr_state.images.selected, 1);
11941
11942 app.handle_action(Action::FilterInput('v'));
11944 assert_eq!(app.ecr_state.images.filter, "v");
11945 assert_eq!(app.ecr_state.images.selected, 0);
11946 }
11947
11948 #[test]
11949 fn test_iam_users_filter_input() {
11950 let mut app = test_app();
11951 app.current_service = Service::IamUsers;
11952 app.service_selected = true;
11953 app.mode = Mode::FilterInput;
11954
11955 app.handle_action(Action::FilterInput('a'));
11956 app.handle_action(Action::FilterInput('d'));
11957 app.handle_action(Action::FilterInput('m'));
11958 app.handle_action(Action::FilterInput('i'));
11959 app.handle_action(Action::FilterInput('n'));
11960 assert_eq!(app.iam_state.users.filter, "admin");
11961
11962 app.handle_action(Action::FilterBackspace);
11963 assert_eq!(app.iam_state.users.filter, "admi");
11964 }
11965
11966 #[test]
11967 fn test_iam_policies_filter_input() {
11968 let mut app = test_app();
11969 app.current_service = Service::IamUsers;
11970 app.service_selected = true;
11971 app.iam_state.current_user = Some("testuser".to_string());
11972 app.mode = Mode::FilterInput;
11973
11974 app.handle_action(Action::FilterInput('r'));
11975 app.handle_action(Action::FilterInput('e'));
11976 app.handle_action(Action::FilterInput('a'));
11977 app.handle_action(Action::FilterInput('d'));
11978 assert_eq!(app.iam_state.policies.filter, "read");
11979
11980 app.handle_action(Action::FilterBackspace);
11981 assert_eq!(app.iam_state.policies.filter, "rea");
11982 }
11983
11984 #[test]
11985 fn test_iam_start_filter() {
11986 let mut app = test_app();
11987 app.current_service = Service::IamUsers;
11988 app.service_selected = true;
11989 app.mode = Mode::Normal;
11990
11991 app.handle_action(Action::StartFilter);
11992 assert_eq!(app.mode, Mode::FilterInput);
11993 }
11994
11995 #[test]
11996 fn test_iam_roles_filter_input() {
11997 let mut app = test_app();
11998 app.current_service = Service::IamRoles;
11999 app.service_selected = true;
12000 app.mode = Mode::FilterInput;
12001
12002 app.handle_action(Action::FilterInput('a'));
12003 app.handle_action(Action::FilterInput('d'));
12004 app.handle_action(Action::FilterInput('m'));
12005 app.handle_action(Action::FilterInput('i'));
12006 app.handle_action(Action::FilterInput('n'));
12007 assert_eq!(app.iam_state.roles.filter, "admin");
12008
12009 app.handle_action(Action::FilterBackspace);
12010 assert_eq!(app.iam_state.roles.filter, "admi");
12011 }
12012
12013 #[test]
12014 fn test_iam_roles_start_filter() {
12015 let mut app = test_app();
12016 app.current_service = Service::IamRoles;
12017 app.service_selected = true;
12018 app.mode = Mode::Normal;
12019
12020 app.handle_action(Action::StartFilter);
12021 assert_eq!(app.mode, Mode::FilterInput);
12022 }
12023
12024 #[test]
12025 fn test_iam_roles_navigation() {
12026 let mut app = test_app();
12027 app.current_service = Service::IamRoles;
12028 app.service_selected = true;
12029 app.mode = Mode::Normal;
12030 app.iam_state.roles.items = (0..10)
12031 .map(|i| IamRole {
12032 role_name: format!("role{}", i),
12033 path: "/".to_string(),
12034 trusted_entities: String::new(),
12035 last_activity: String::new(),
12036 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12037 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12038 description: String::new(),
12039 max_session_duration: Some(3600),
12040 })
12041 .collect();
12042
12043 assert_eq!(app.iam_state.roles.selected, 0);
12044
12045 app.handle_action(Action::NextItem);
12046 assert_eq!(app.iam_state.roles.selected, 1);
12047
12048 app.handle_action(Action::NextItem);
12049 assert_eq!(app.iam_state.roles.selected, 2);
12050
12051 app.handle_action(Action::PrevItem);
12052 assert_eq!(app.iam_state.roles.selected, 1);
12053 }
12054
12055 #[test]
12056 fn test_iam_roles_page_hotkey() {
12057 let mut app = test_app();
12058 app.current_service = Service::IamRoles;
12059 app.service_selected = true;
12060 app.mode = Mode::Normal;
12061 app.iam_state.roles.page_size = PageSize::Ten;
12062 app.iam_state.roles.items = (0..100)
12063 .map(|i| IamRole {
12064 role_name: format!("role{}", i),
12065 path: "/".to_string(),
12066 trusted_entities: String::new(),
12067 last_activity: String::new(),
12068 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12069 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12070 description: String::new(),
12071 max_session_duration: Some(3600),
12072 })
12073 .collect();
12074
12075 app.handle_action(Action::FilterInput('2'));
12076 app.handle_action(Action::OpenColumnSelector);
12077 assert_eq!(app.iam_state.roles.selected, 10); }
12079
12080 #[test]
12081 fn test_iam_users_page_hotkey() {
12082 let mut app = test_app();
12083 app.current_service = Service::IamUsers;
12084 app.service_selected = true;
12085 app.mode = Mode::Normal;
12086 app.iam_state.users.page_size = PageSize::Ten;
12087 app.iam_state.users.items = (0..100)
12088 .map(|i| IamUser {
12089 user_name: format!("user{}", i),
12090 path: "/".to_string(),
12091 groups: String::new(),
12092 last_activity: String::new(),
12093 mfa: String::new(),
12094 password_age: String::new(),
12095 console_last_sign_in: String::new(),
12096 access_key_id: String::new(),
12097 active_key_age: String::new(),
12098 access_key_last_used: String::new(),
12099 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
12100 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12101 console_access: String::new(),
12102 signing_certs: String::new(),
12103 })
12104 .collect();
12105
12106 app.handle_action(Action::FilterInput('3'));
12107 app.handle_action(Action::OpenColumnSelector);
12108 assert_eq!(app.iam_state.users.selected, 20); }
12110
12111 #[test]
12112 fn test_ecr_scroll_navigation() {
12113 let mut app = test_app();
12114 app.current_service = Service::EcrRepositories;
12115 app.service_selected = true;
12116 app.ecr_state.repositories.items = (0..20)
12117 .map(|i| EcrRepository {
12118 name: format!("repo{}", i),
12119 uri: format!("uri{}", i),
12120 created_at: "2023-01-01".to_string(),
12121 tag_immutability: "MUTABLE".to_string(),
12122 encryption_type: "AES256".to_string(),
12123 })
12124 .collect();
12125
12126 app.handle_action(Action::ScrollDown);
12127 assert_eq!(app.ecr_state.repositories.selected, 10);
12128
12129 app.handle_action(Action::ScrollUp);
12130 assert_eq!(app.ecr_state.repositories.selected, 0);
12131 }
12132
12133 #[test]
12134 fn test_ecr_tab_switching_triggers_reload() {
12135 let mut app = test_app();
12136 app.current_service = Service::EcrRepositories;
12137 app.service_selected = true;
12138 app.ecr_state.tab = EcrTab::Private;
12139 app.ecr_state.repositories.loading = false;
12140 app.ecr_state.repositories.items = vec![EcrRepository {
12141 name: "private-repo".to_string(),
12142 uri: "uri".to_string(),
12143 created_at: "2023-01-01".to_string(),
12144 tag_immutability: "MUTABLE".to_string(),
12145 encryption_type: "AES256".to_string(),
12146 }];
12147
12148 app.handle_action(Action::NextDetailTab);
12149 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12150 assert!(app.ecr_state.repositories.loading);
12151 assert_eq!(app.ecr_state.repositories.selected, 0);
12152 }
12153
12154 #[test]
12155 fn test_ecr_tab_cycles_between_private_and_public() {
12156 let mut app = test_app();
12157 app.current_service = Service::EcrRepositories;
12158 app.service_selected = true;
12159 app.ecr_state.tab = EcrTab::Private;
12160
12161 app.handle_action(Action::NextDetailTab);
12162 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12163
12164 app.handle_action(Action::NextDetailTab);
12165 assert_eq!(app.ecr_state.tab, EcrTab::Private);
12166 }
12167
12168 #[test]
12169 fn test_page_size_values() {
12170 assert_eq!(PageSize::Ten.value(), 10);
12171 assert_eq!(PageSize::TwentyFive.value(), 25);
12172 assert_eq!(PageSize::Fifty.value(), 50);
12173 assert_eq!(PageSize::OneHundred.value(), 100);
12174 }
12175
12176 #[test]
12177 fn test_page_size_next() {
12178 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
12179 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
12180 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
12181 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
12182 }
12183
12184 #[test]
12185 fn test_ecr_enter_drills_into_repository() {
12186 let mut app = test_app();
12187 app.current_service = Service::EcrRepositories;
12188 app.service_selected = true;
12189 app.mode = Mode::Normal;
12190 app.ecr_state.repositories.items = vec![EcrRepository {
12191 name: "my-repo".to_string(),
12192 uri: "uri".to_string(),
12193 created_at: "2023-01-01".to_string(),
12194 tag_immutability: "MUTABLE".to_string(),
12195 encryption_type: "AES256".to_string(),
12196 }];
12197
12198 app.handle_action(Action::Select);
12199 assert_eq!(
12200 app.ecr_state.current_repository,
12201 Some("my-repo".to_string())
12202 );
12203 assert!(app.ecr_state.repositories.loading);
12204 }
12205
12206 #[test]
12207 fn test_ecr_repository_expansion() {
12208 let mut app = test_app();
12209 app.current_service = Service::EcrRepositories;
12210 app.service_selected = true;
12211 app.ecr_state.repositories.items = vec![EcrRepository {
12212 name: "my-repo".to_string(),
12213 uri: "uri".to_string(),
12214 created_at: "2023-01-01".to_string(),
12215 tag_immutability: "MUTABLE".to_string(),
12216 encryption_type: "AES256".to_string(),
12217 }];
12218 app.ecr_state.repositories.selected = 0;
12219
12220 assert_eq!(app.ecr_state.repositories.expanded_item, None);
12221
12222 app.handle_action(Action::NextPane);
12223 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
12224
12225 app.handle_action(Action::PrevPane);
12226 assert_eq!(app.ecr_state.repositories.expanded_item, None);
12227 }
12228
12229 #[test]
12230 fn test_ecr_ctrl_d_scrolls_down() {
12231 let mut app = test_app();
12232 app.current_service = Service::EcrRepositories;
12233 app.service_selected = true;
12234 app.mode = Mode::Normal;
12235 app.ecr_state.repositories.items = (0..30)
12236 .map(|i| EcrRepository {
12237 name: format!("repo{}", i),
12238 uri: format!("uri{}", i),
12239 created_at: "2023-01-01".to_string(),
12240 tag_immutability: "MUTABLE".to_string(),
12241 encryption_type: "AES256".to_string(),
12242 })
12243 .collect();
12244 app.ecr_state.repositories.selected = 0;
12245
12246 app.handle_action(Action::PageDown);
12247 assert_eq!(app.ecr_state.repositories.selected, 10);
12248 }
12249
12250 #[test]
12251 fn test_ecr_ctrl_u_scrolls_up() {
12252 let mut app = test_app();
12253 app.current_service = Service::EcrRepositories;
12254 app.service_selected = true;
12255 app.mode = Mode::Normal;
12256 app.ecr_state.repositories.items = (0..30)
12257 .map(|i| EcrRepository {
12258 name: format!("repo{}", i),
12259 uri: format!("uri{}", i),
12260 created_at: "2023-01-01".to_string(),
12261 tag_immutability: "MUTABLE".to_string(),
12262 encryption_type: "AES256".to_string(),
12263 })
12264 .collect();
12265 app.ecr_state.repositories.selected = 15;
12266
12267 app.handle_action(Action::PageUp);
12268 assert_eq!(app.ecr_state.repositories.selected, 5);
12269 }
12270
12271 #[test]
12272 fn test_ecr_images_ctrl_d_scrolls_down() {
12273 let mut app = test_app();
12274 app.current_service = Service::EcrRepositories;
12275 app.service_selected = true;
12276 app.mode = Mode::Normal;
12277 app.ecr_state.current_repository = Some("repo".to_string());
12278 app.ecr_state.images.items = (0..30)
12279 .map(|i| EcrImage {
12280 tag: format!("tag{}", i),
12281 artifact_type: "container".to_string(),
12282 pushed_at: "2023-01-01T12:00:00Z".to_string(),
12283 size_bytes: 104857600,
12284 uri: format!("uri{}", i),
12285 digest: format!("sha256:{}", i),
12286 last_pull_time: String::new(),
12287 })
12288 .collect();
12289 app.ecr_state.images.selected = 0;
12290
12291 app.handle_action(Action::PageDown);
12292 assert_eq!(app.ecr_state.images.selected, 10);
12293 }
12294
12295 #[test]
12296 fn test_ecr_esc_goes_back_from_images_to_repos() {
12297 let mut app = test_app();
12298 app.current_service = Service::EcrRepositories;
12299 app.service_selected = true;
12300 app.mode = Mode::Normal;
12301 app.ecr_state.current_repository = Some("my-repo".to_string());
12302 app.ecr_state.images.items = vec![EcrImage {
12303 tag: "latest".to_string(),
12304 artifact_type: "container".to_string(),
12305 pushed_at: "2023-01-01T12:00:00Z".to_string(),
12306 size_bytes: 104857600,
12307 uri: "uri".to_string(),
12308 digest: "sha256:abc".to_string(),
12309 last_pull_time: String::new(),
12310 }];
12311
12312 app.handle_action(Action::GoBack);
12313 assert_eq!(app.ecr_state.current_repository, None);
12314 assert!(app.ecr_state.images.items.is_empty());
12315 }
12316
12317 #[test]
12318 fn test_ecr_esc_collapses_expanded_image_first() {
12319 let mut app = test_app();
12320 app.current_service = Service::EcrRepositories;
12321 app.service_selected = true;
12322 app.mode = Mode::Normal;
12323 app.ecr_state.current_repository = Some("my-repo".to_string());
12324 app.ecr_state.images.expanded_item = Some(0);
12325
12326 app.handle_action(Action::GoBack);
12327 assert_eq!(app.ecr_state.images.expanded_item, None);
12328 assert_eq!(
12329 app.ecr_state.current_repository,
12330 Some("my-repo".to_string())
12331 );
12332 }
12333
12334 #[test]
12335 fn test_pagination_with_lowercase_p() {
12336 let mut app = test_app();
12337 app.current_service = Service::EcrRepositories;
12338 app.service_selected = true;
12339 app.mode = Mode::Normal;
12340 app.ecr_state.repositories.items = (0..100)
12341 .map(|i| EcrRepository {
12342 name: format!("repo{}", i),
12343 uri: format!("uri{}", i),
12344 created_at: "2023-01-01".to_string(),
12345 tag_immutability: "MUTABLE".to_string(),
12346 encryption_type: "AES256".to_string(),
12347 })
12348 .collect();
12349
12350 app.handle_action(Action::FilterInput('2'));
12352 assert_eq!(app.page_input, "2");
12353
12354 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
12358
12359 #[test]
12360 fn test_lowercase_p_without_number_opens_preferences() {
12361 let mut app = test_app();
12362 app.current_service = Service::EcrRepositories;
12363 app.service_selected = true;
12364 app.mode = Mode::Normal;
12365
12366 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
12368 }
12369
12370 #[test]
12371 fn test_ctrl_o_generates_correct_console_url() {
12372 let mut app = test_app();
12373 app.current_service = Service::EcrRepositories;
12374 app.service_selected = true;
12375 app.mode = Mode::Normal;
12376 app.config.account_id = "123456789012".to_string();
12377
12378 let url = app.get_console_url();
12380 assert!(url.contains("ecr/private-registry/repositories"));
12381 assert!(url.contains("region=us-east-1"));
12382
12383 app.ecr_state.current_repository = Some("my-repo".to_string());
12385 let url = app.get_console_url();
12386 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
12387 assert!(url.contains("region=us-east-1"));
12388 }
12389
12390 #[test]
12391 fn test_page_input_display_and_reset() {
12392 let mut app = test_app();
12393 app.current_service = Service::EcrRepositories;
12394 app.service_selected = true;
12395 app.mode = Mode::Normal;
12396 app.ecr_state.repositories.items = (0..100)
12397 .map(|i| EcrRepository {
12398 name: format!("repo{}", i),
12399 uri: format!("uri{}", i),
12400 created_at: "2023-01-01".to_string(),
12401 tag_immutability: "MUTABLE".to_string(),
12402 encryption_type: "AES256".to_string(),
12403 })
12404 .collect();
12405
12406 app.handle_action(Action::FilterInput('2'));
12408 assert_eq!(app.page_input, "2");
12409
12410 app.handle_action(Action::OpenColumnSelector);
12412 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
12415
12416 #[test]
12417 fn test_page_navigation_updates_scroll_offset_for_cfn() {
12418 let mut app = test_app();
12419 app.current_service = Service::CloudFormationStacks;
12420 app.service_selected = true;
12421 app.mode = Mode::Normal;
12422 app.cfn_state.table.items = (0..100)
12423 .map(|i| CfnStack {
12424 name: format!("stack-{}", i),
12425 stack_id: format!(
12426 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
12427 i
12428 ),
12429 status: "CREATE_COMPLETE".to_string(),
12430 created_time: "2023-01-01T00:00:00Z".to_string(),
12431 updated_time: "2023-01-01T00:00:00Z".to_string(),
12432 deleted_time: String::new(),
12433 drift_status: "IN_SYNC".to_string(),
12434 last_drift_check_time: String::new(),
12435 status_reason: String::new(),
12436 description: String::new(),
12437 detailed_status: String::new(),
12438 root_stack: String::new(),
12439 parent_stack: String::new(),
12440 termination_protection: false,
12441 iam_role: String::new(),
12442 tags: vec![],
12443 stack_policy: String::new(),
12444 rollback_monitoring_time: String::new(),
12445 rollback_alarms: vec![],
12446 notification_arns: vec![],
12447 })
12448 .collect();
12449
12450 app.handle_action(Action::FilterInput('2'));
12452 assert_eq!(app.page_input, "2");
12453
12454 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
12459 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
12461 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
12462
12463 let current_page = app.cfn_state.table.scroll_offset / page_size;
12465 assert_eq!(
12466 current_page, 1,
12467 "2p should go to page 2 (0-indexed as 1), not page 3"
12468 ); }
12470
12471 #[test]
12472 fn test_3p_goes_to_page_3_not_page_5() {
12473 let mut app = test_app();
12474 app.current_service = Service::CloudFormationStacks;
12475 app.service_selected = true;
12476 app.mode = Mode::Normal;
12477 app.cfn_state.table.items = (0..200)
12478 .map(|i| CfnStack {
12479 name: format!("stack-{}", i),
12480 stack_id: format!(
12481 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
12482 i
12483 ),
12484 status: "CREATE_COMPLETE".to_string(),
12485 created_time: "2023-01-01T00:00:00Z".to_string(),
12486 updated_time: "2023-01-01T00:00:00Z".to_string(),
12487 deleted_time: String::new(),
12488 drift_status: "IN_SYNC".to_string(),
12489 last_drift_check_time: String::new(),
12490 status_reason: String::new(),
12491 description: String::new(),
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![],
12498 stack_policy: String::new(),
12499 rollback_monitoring_time: String::new(),
12500 rollback_alarms: vec![],
12501 notification_arns: vec![],
12502 })
12503 .collect();
12504
12505 app.handle_action(Action::FilterInput('3'));
12507 app.handle_action(Action::OpenColumnSelector);
12508
12509 let page_size = app.cfn_state.table.page_size.value();
12510 let current_page = app.cfn_state.table.scroll_offset / page_size;
12511 assert_eq!(
12512 current_page, 2,
12513 "3p should go to page 3 (0-indexed as 2), not page 5"
12514 );
12515 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
12516 }
12517
12518 #[test]
12519 fn test_log_streams_page_navigation_uses_correct_page_size() {
12520 let mut app = test_app();
12521 app.current_service = Service::CloudWatchLogGroups;
12522 app.view_mode = ViewMode::Detail;
12523 app.service_selected = true;
12524 app.mode = Mode::Normal;
12525 app.log_groups_state.log_streams = (0..100)
12526 .map(|i| LogStream {
12527 name: format!("stream-{}", i),
12528 creation_time: None,
12529 last_event_time: None,
12530 })
12531 .collect();
12532
12533 app.handle_action(Action::FilterInput('2'));
12535 app.handle_action(Action::OpenColumnSelector);
12536
12537 assert_eq!(app.log_groups_state.stream_current_page, 1);
12539 assert_eq!(app.log_groups_state.selected_stream, 0);
12540
12541 assert_eq!(
12543 app.log_groups_state.stream_current_page, 1,
12544 "2p should go to page 2 (0-indexed as 1), not page 3"
12545 );
12546 }
12547
12548 #[test]
12549 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
12550 let mut app = test_app();
12551 app.current_service = Service::EcrRepositories;
12552 app.service_selected = true;
12553 app.mode = Mode::Normal;
12554 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
12556 .map(|i| EcrRepository {
12557 name: format!("repo{}", i),
12558 uri: format!("uri{}", i),
12559 created_at: "2023-01-01".to_string(),
12560 tag_immutability: "MUTABLE".to_string(),
12561 encryption_type: "AES256".to_string(),
12562 })
12563 .collect();
12564
12565 app.handle_action(Action::FilterInput('3'));
12567 app.handle_action(Action::OpenColumnSelector);
12568
12569 assert_eq!(app.ecr_state.repositories.selected, 50);
12571
12572 let page_size = app.ecr_state.repositories.page_size.value();
12573 let current_page = app.ecr_state.repositories.selected / page_size;
12574 assert_eq!(
12575 current_page, 2,
12576 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
12577 );
12578 }
12579
12580 #[test]
12581 fn test_page_navigation_updates_scroll_offset_for_alarms() {
12582 let mut app = test_app();
12583 app.current_service = Service::CloudWatchAlarms;
12584 app.service_selected = true;
12585 app.mode = Mode::Normal;
12586 app.alarms_state.table.items = (0..100)
12587 .map(|i| Alarm {
12588 name: format!("alarm-{}", i),
12589 state: "OK".to_string(),
12590 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
12591 description: String::new(),
12592 metric_name: "CPUUtilization".to_string(),
12593 namespace: "AWS/EC2".to_string(),
12594 statistic: "Average".to_string(),
12595 period: 300,
12596 comparison_operator: "GreaterThanThreshold".to_string(),
12597 threshold: 80.0,
12598 actions_enabled: true,
12599 state_reason: String::new(),
12600 resource: String::new(),
12601 dimensions: String::new(),
12602 expression: String::new(),
12603 alarm_type: "MetricAlarm".to_string(),
12604 cross_account: String::new(),
12605 })
12606 .collect();
12607
12608 app.handle_action(Action::FilterInput('2'));
12610 app.handle_action(Action::OpenColumnSelector);
12611
12612 let page_size = app.alarms_state.table.page_size.value();
12614 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
12616 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
12617 }
12618
12619 #[test]
12620 fn test_ecr_pagination_with_65_repos() {
12621 let mut app = test_app();
12622 app.current_service = Service::EcrRepositories;
12623 app.service_selected = true;
12624 app.mode = Mode::Normal;
12625 app.ecr_state.repositories.items = (0..65)
12626 .map(|i| EcrRepository {
12627 name: format!("repo{:02}", i),
12628 uri: format!("uri{}", i),
12629 created_at: "2023-01-01".to_string(),
12630 tag_immutability: "MUTABLE".to_string(),
12631 encryption_type: "AES256".to_string(),
12632 })
12633 .collect();
12634
12635 assert_eq!(app.ecr_state.repositories.selected, 0);
12637 let page_size = 50;
12638 let current_page = app.ecr_state.repositories.selected / page_size;
12639 assert_eq!(current_page, 0);
12640
12641 app.handle_action(Action::FilterInput('2'));
12643 app.handle_action(Action::OpenColumnSelector);
12644 assert_eq!(app.ecr_state.repositories.selected, 50);
12645
12646 let current_page = app.ecr_state.repositories.selected / page_size;
12648 assert_eq!(current_page, 1);
12649 }
12650
12651 #[test]
12652 fn test_ecr_repos_input_focus_tab_cycling() {
12653 let mut app = test_app();
12654 app.current_service = Service::EcrRepositories;
12655 app.service_selected = true;
12656 app.mode = Mode::FilterInput;
12657 app.ecr_state.input_focus = InputFocus::Filter;
12658
12659 app.handle_action(Action::NextFilterFocus);
12661 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
12662
12663 app.handle_action(Action::NextFilterFocus);
12665 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
12666
12667 app.handle_action(Action::PrevFilterFocus);
12669 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
12670
12671 app.handle_action(Action::PrevFilterFocus);
12673 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
12674 }
12675
12676 #[test]
12677 fn test_ecr_images_column_toggle_not_off_by_one() {
12678 use crate::ecr::image::Column as ImageColumn;
12679 let mut app = test_app();
12680 app.current_service = Service::EcrRepositories;
12681 app.service_selected = true;
12682 app.mode = Mode::ColumnSelector;
12683 app.ecr_state.current_repository = Some("test-repo".to_string());
12684
12685 app.ecr_image_visible_column_ids = ImageColumn::ids();
12687 let initial_count = app.ecr_image_visible_column_ids.len();
12688
12689 app.column_selector_index = 0;
12691 app.handle_action(Action::ToggleColumn);
12692
12693 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
12695 assert!(!app
12696 .ecr_image_visible_column_ids
12697 .contains(&ImageColumn::Tag.id()));
12698
12699 app.handle_action(Action::ToggleColumn);
12701 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
12702 assert!(app
12703 .ecr_image_visible_column_ids
12704 .contains(&ImageColumn::Tag.id()));
12705 }
12706
12707 #[test]
12708 fn test_ecr_repos_column_toggle_works() {
12709 let mut app = test_app();
12710 app.current_service = Service::EcrRepositories;
12711 app.service_selected = true;
12712 app.mode = Mode::ColumnSelector;
12713 app.ecr_state.current_repository = None;
12714
12715 app.ecr_repo_visible_column_ids = EcrColumn::ids();
12717 let initial_count = app.ecr_repo_visible_column_ids.len();
12718
12719 app.column_selector_index = 1;
12721 app.handle_action(Action::ToggleColumn);
12722
12723 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
12725 assert!(!app
12726 .ecr_repo_visible_column_ids
12727 .contains(&EcrColumn::Name.id()));
12728
12729 app.handle_action(Action::ToggleColumn);
12731 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
12732 assert!(app
12733 .ecr_repo_visible_column_ids
12734 .contains(&EcrColumn::Name.id()));
12735 }
12736
12737 #[test]
12738 fn test_ecr_repos_pagination_left_right_navigation() {
12739 use crate::ecr::repo::Repository as EcrRepository;
12740 let mut app = test_app();
12741 app.current_service = Service::EcrRepositories;
12742 app.service_selected = true;
12743 app.mode = Mode::FilterInput;
12744 app.ecr_state.input_focus = InputFocus::Pagination;
12745
12746 app.ecr_state.repositories.items = (0..150)
12748 .map(|i| EcrRepository {
12749 name: format!("repo{:03}", i),
12750 uri: format!("uri{}", i),
12751 created_at: "2023-01-01".to_string(),
12752 tag_immutability: "MUTABLE".to_string(),
12753 encryption_type: "AES256".to_string(),
12754 })
12755 .collect();
12756
12757 app.ecr_state.repositories.selected = 0;
12759 eprintln!(
12760 "Initial: selected={}, focus={:?}, mode={:?}",
12761 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
12762 );
12763
12764 app.handle_action(Action::PageDown);
12766 eprintln!(
12767 "After PageDown: selected={}",
12768 app.ecr_state.repositories.selected
12769 );
12770 assert_eq!(app.ecr_state.repositories.selected, 50);
12771
12772 app.handle_action(Action::PageDown);
12774 eprintln!(
12775 "After 2nd PageDown: selected={}",
12776 app.ecr_state.repositories.selected
12777 );
12778 assert_eq!(app.ecr_state.repositories.selected, 100);
12779
12780 app.handle_action(Action::PageDown);
12782 eprintln!(
12783 "After 3rd PageDown: selected={}",
12784 app.ecr_state.repositories.selected
12785 );
12786 assert_eq!(app.ecr_state.repositories.selected, 100);
12787
12788 app.handle_action(Action::PageUp);
12790 eprintln!(
12791 "After PageUp: selected={}",
12792 app.ecr_state.repositories.selected
12793 );
12794 assert_eq!(app.ecr_state.repositories.selected, 50);
12795
12796 app.handle_action(Action::PageUp);
12798 eprintln!(
12799 "After 2nd PageUp: selected={}",
12800 app.ecr_state.repositories.selected
12801 );
12802 assert_eq!(app.ecr_state.repositories.selected, 0);
12803
12804 app.handle_action(Action::PageUp);
12806 eprintln!(
12807 "After 3rd PageUp: selected={}",
12808 app.ecr_state.repositories.selected
12809 );
12810 assert_eq!(app.ecr_state.repositories.selected, 0);
12811 }
12812
12813 #[test]
12814 fn test_ecr_repos_filter_input_when_input_focused() {
12815 use crate::ecr::repo::Repository as EcrRepository;
12816 let mut app = test_app();
12817 app.current_service = Service::EcrRepositories;
12818 app.service_selected = true;
12819 app.mode = Mode::FilterInput;
12820 app.ecr_state.input_focus = InputFocus::Filter;
12821
12822 app.ecr_state.repositories.items = vec![
12824 EcrRepository {
12825 name: "test-repo".to_string(),
12826 uri: "uri1".to_string(),
12827 created_at: "2023-01-01".to_string(),
12828 tag_immutability: "MUTABLE".to_string(),
12829 encryption_type: "AES256".to_string(),
12830 },
12831 EcrRepository {
12832 name: "prod-repo".to_string(),
12833 uri: "uri2".to_string(),
12834 created_at: "2023-01-01".to_string(),
12835 tag_immutability: "MUTABLE".to_string(),
12836 encryption_type: "AES256".to_string(),
12837 },
12838 ];
12839
12840 assert_eq!(app.ecr_state.repositories.filter, "");
12842 app.handle_action(Action::FilterInput('t'));
12843 assert_eq!(app.ecr_state.repositories.filter, "t");
12844 app.handle_action(Action::FilterInput('e'));
12845 assert_eq!(app.ecr_state.repositories.filter, "te");
12846 app.handle_action(Action::FilterInput('s'));
12847 assert_eq!(app.ecr_state.repositories.filter, "tes");
12848 app.handle_action(Action::FilterInput('t'));
12849 assert_eq!(app.ecr_state.repositories.filter, "test");
12850 }
12851
12852 #[test]
12853 fn test_ecr_repos_digit_input_when_pagination_focused() {
12854 use crate::ecr::repo::Repository as EcrRepository;
12855 let mut app = test_app();
12856 app.current_service = Service::EcrRepositories;
12857 app.service_selected = true;
12858 app.mode = Mode::FilterInput;
12859 app.ecr_state.input_focus = InputFocus::Pagination;
12860
12861 app.ecr_state.repositories.items = vec![EcrRepository {
12863 name: "test-repo".to_string(),
12864 uri: "uri1".to_string(),
12865 created_at: "2023-01-01".to_string(),
12866 tag_immutability: "MUTABLE".to_string(),
12867 encryption_type: "AES256".to_string(),
12868 }];
12869
12870 assert_eq!(app.ecr_state.repositories.filter, "");
12872 assert_eq!(app.page_input, "");
12873 app.handle_action(Action::FilterInput('2'));
12874 assert_eq!(app.ecr_state.repositories.filter, "");
12875 assert_eq!(app.page_input, "2");
12876
12877 app.handle_action(Action::FilterInput('a'));
12879 assert_eq!(app.ecr_state.repositories.filter, "");
12880 assert_eq!(app.page_input, "2");
12881 }
12882
12883 #[test]
12884 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
12885 use crate::ecr::repo::Repository as EcrRepository;
12886 let mut app = test_app();
12887 app.current_service = Service::EcrRepositories;
12888 app.service_selected = true;
12889 app.mode = Mode::FilterInput;
12890 app.ecr_state.input_focus = InputFocus::Filter;
12891
12892 app.ecr_state.repositories.items = (0..150)
12894 .map(|i| EcrRepository {
12895 name: format!("repo{:03}", i),
12896 uri: format!("uri{}", i),
12897 created_at: "2023-01-01".to_string(),
12898 tag_immutability: "MUTABLE".to_string(),
12899 encryption_type: "AES256".to_string(),
12900 })
12901 .collect();
12902
12903 app.ecr_state.repositories.selected = 0;
12905
12906 app.handle_action(Action::PageDown);
12908 assert_eq!(
12909 app.ecr_state.repositories.selected, 10,
12910 "Should scroll down by 10"
12911 );
12912
12913 app.handle_action(Action::PageUp);
12914 assert_eq!(
12915 app.ecr_state.repositories.selected, 0,
12916 "Should scroll back up"
12917 );
12918 }
12919
12920 #[test]
12921 fn test_ecr_repos_pagination_control_actually_works() {
12922 use crate::ecr::repo::Repository as EcrRepository;
12923
12924 let mut app = test_app();
12926 app.current_service = Service::EcrRepositories;
12927 app.service_selected = true;
12928 app.mode = Mode::FilterInput;
12929 app.ecr_state.current_repository = None;
12930 app.ecr_state.input_focus = InputFocus::Pagination;
12931
12932 app.ecr_state.repositories.items = (0..100)
12934 .map(|i| EcrRepository {
12935 name: format!("repo{:03}", i),
12936 uri: format!("uri{}", i),
12937 created_at: "2023-01-01".to_string(),
12938 tag_immutability: "MUTABLE".to_string(),
12939 encryption_type: "AES256".to_string(),
12940 })
12941 .collect();
12942
12943 app.ecr_state.repositories.selected = 0;
12944
12945 assert_eq!(app.mode, Mode::FilterInput);
12947 assert_eq!(app.current_service, Service::EcrRepositories);
12948 assert_eq!(app.ecr_state.current_repository, None);
12949 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
12950
12951 app.handle_action(Action::PageDown);
12953 assert_eq!(
12954 app.ecr_state.repositories.selected, 50,
12955 "PageDown should move to page 2"
12956 );
12957
12958 app.handle_action(Action::PageUp);
12959 assert_eq!(
12960 app.ecr_state.repositories.selected, 0,
12961 "PageUp should move back to page 1"
12962 );
12963 }
12964
12965 #[test]
12966 fn test_ecr_repos_can_reach_last_page_when_not_multiple_of_page_size() {
12967 use crate::ecr::repo::Repository as EcrRepository;
12968 let mut app = test_app();
12969 app.current_service = Service::EcrRepositories;
12970 app.service_selected = true;
12971 app.mode = Mode::FilterInput;
12972 app.ecr_state.input_focus = InputFocus::Pagination;
12973
12974 app.ecr_state.repositories.items = (0..754)
12976 .map(|i| EcrRepository {
12977 name: format!("repo{:03}", i),
12978 uri: format!("uri{}", i),
12979 created_at: "2023-01-01".to_string(),
12980 tag_immutability: "MUTABLE".to_string(),
12981 encryption_type: "AES256".to_string(),
12982 })
12983 .collect();
12984
12985 app.ecr_state.repositories.selected = 700;
12987
12988 app.handle_action(Action::PageDown);
12990 assert_eq!(
12991 app.ecr_state.repositories.selected, 750,
12992 "Should reach last page (750) but got {}",
12993 app.ecr_state.repositories.selected
12994 );
12995
12996 app.handle_action(Action::PageDown);
12998 assert_eq!(app.ecr_state.repositories.selected, 750);
12999 }
13000
13001 #[test]
13002 fn test_ecr_repos_start_filter_resets_focus_to_input() {
13003 let mut app = test_app();
13004 app.current_service = Service::EcrRepositories;
13005 app.service_selected = true;
13006 app.mode = Mode::Normal;
13007 app.ecr_state.current_repository = None;
13008
13009 app.ecr_state.input_focus = InputFocus::Pagination;
13011
13012 app.handle_action(Action::StartFilter);
13014
13015 assert_eq!(app.mode, Mode::FilterInput);
13017 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13018 }
13019
13020 #[test]
13021 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
13022 use crate::ecr::repo::Repository as EcrRepository;
13023
13024 let mut app = test_app();
13025 app.current_service = Service::EcrRepositories;
13026 app.service_selected = true;
13027 app.mode = Mode::Normal;
13028 app.ecr_state.current_repository = None;
13029
13030 app.ecr_state.repositories.items = (0..100)
13032 .map(|i| EcrRepository {
13033 name: format!("repo{:03}", i),
13034 uri: format!("uri{}", i),
13035 created_at: "2023-01-01".to_string(),
13036 tag_immutability: "MUTABLE".to_string(),
13037 encryption_type: "AES256".to_string(),
13038 })
13039 .collect();
13040
13041 app.ecr_state.repositories.selected = 0;
13042
13043 app.handle_action(Action::StartFilter);
13045 assert_eq!(app.mode, Mode::FilterInput);
13046 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13047
13048 app.handle_action(Action::NextFilterFocus);
13050 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13051
13052 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
13054 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
13055 app.handle_action(Action::PageDown);
13056 eprintln!(
13057 "After PageDown: selected={}",
13058 app.ecr_state.repositories.selected
13059 );
13060
13061 assert_eq!(
13063 app.ecr_state.repositories.selected, 50,
13064 "Right arrow should move to page 2"
13065 );
13066
13067 app.handle_action(Action::PageUp);
13069 assert_eq!(
13070 app.ecr_state.repositories.selected, 0,
13071 "Left arrow should move back to page 1"
13072 );
13073 }
13074
13075 #[test]
13076 fn test_apig_filter_input() {
13077 let mut app = test_app();
13078 app.current_service = Service::ApiGatewayApis;
13079 app.service_selected = true;
13080 app.mode = Mode::FilterInput;
13081
13082 app.handle_action(Action::FilterInput('t'));
13083 app.handle_action(Action::FilterInput('e'));
13084 app.handle_action(Action::FilterInput('s'));
13085 app.handle_action(Action::FilterInput('t'));
13086 assert_eq!(app.apig_state.apis.filter, "test");
13087
13088 app.handle_action(Action::FilterBackspace);
13089 assert_eq!(app.apig_state.apis.filter, "tes");
13090 }
13091
13092 #[test]
13093 fn test_apig_start_filter_enters_filter_mode() {
13094 let mut app = test_app();
13095 app.current_service = Service::ApiGatewayApis;
13096 app.service_selected = true;
13097 app.mode = Mode::Normal;
13098
13099 app.handle_action(Action::StartFilter);
13100 assert_eq!(app.mode, Mode::FilterInput);
13101 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13102 }
13103
13104 #[test]
13105 fn test_apig_input_focus_cycles_with_tab() {
13106 let mut app = test_app();
13107 app.current_service = Service::ApiGatewayApis;
13108 app.service_selected = true;
13109 app.mode = Mode::FilterInput;
13110 app.apig_state.input_focus = InputFocus::Filter;
13111
13112 app.handle_action(Action::NextFilterFocus);
13114 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13115
13116 app.handle_action(Action::NextFilterFocus);
13118 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13119 }
13120
13121 #[test]
13122 fn test_apig_input_focus_cycles_with_shift_tab() {
13123 let mut app = test_app();
13124 app.current_service = Service::ApiGatewayApis;
13125 app.service_selected = true;
13126 app.mode = Mode::FilterInput;
13127 app.apig_state.input_focus = InputFocus::Filter;
13128
13129 app.handle_action(Action::PrevFilterFocus);
13131 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13132
13133 app.handle_action(Action::PrevFilterFocus);
13135 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13136 }
13137
13138 #[test]
13139 fn test_apig_exact_user_flow_i_tab_filter() {
13140 let mut app = test_app();
13141 app.current_service = Service::ApiGatewayApis;
13142 app.service_selected = true;
13143 app.mode = Mode::Normal;
13144
13145 app.apig_state.apis.items = vec![
13147 crate::apig::api::RestApi {
13148 id: "api1".to_string(),
13149 name: "test-api".to_string(),
13150 description: "Test API".to_string(),
13151 created_date: "2023-01-01".to_string(),
13152 api_key_source: "HEADER".to_string(),
13153 endpoint_configuration: "REGIONAL".to_string(),
13154 protocol_type: "REST".to_string(),
13155 disable_execute_api_endpoint: false,
13156 status: "AVAILABLE".to_string(),
13157 },
13158 crate::apig::api::RestApi {
13159 id: "api2".to_string(),
13160 name: "prod-api".to_string(),
13161 description: "Production API".to_string(),
13162 created_date: "2023-01-02".to_string(),
13163 api_key_source: "HEADER".to_string(),
13164 endpoint_configuration: "REGIONAL".to_string(),
13165 protocol_type: "REST".to_string(),
13166 disable_execute_api_endpoint: false,
13167 status: "AVAILABLE".to_string(),
13168 },
13169 ];
13170
13171 app.handle_action(Action::StartFilter);
13173 assert_eq!(app.mode, Mode::FilterInput);
13174 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13175
13176 app.handle_action(Action::FilterInput('t'));
13178 app.handle_action(Action::FilterInput('e'));
13179 app.handle_action(Action::FilterInput('s'));
13180 app.handle_action(Action::FilterInput('t'));
13181 assert_eq!(app.apig_state.apis.filter, "test");
13182
13183 app.handle_action(Action::NextFilterFocus);
13185 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13186
13187 app.handle_action(Action::NextFilterFocus);
13189 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13190 }
13191
13192 #[test]
13193 fn test_apig_row_expansion() {
13194 let mut app = test_app();
13195 app.current_service = Service::ApiGatewayApis;
13196 app.service_selected = true;
13197 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
13198 id: "api1".to_string(),
13199 name: "test-api".to_string(),
13200 description: "Test API".to_string(),
13201 created_date: "2023-01-01".to_string(),
13202 api_key_source: "HEADER".to_string(),
13203 endpoint_configuration: "REGIONAL".to_string(),
13204 protocol_type: "REST".to_string(),
13205 disable_execute_api_endpoint: false,
13206 status: "AVAILABLE".to_string(),
13207 }];
13208 app.apig_state.apis.selected = 0;
13209
13210 assert_eq!(app.apig_state.apis.expanded_item, None);
13211
13212 app.handle_action(Action::NextPane);
13214 assert_eq!(app.apig_state.apis.expanded_item, None);
13215
13216 app.handle_action(Action::ExpandRow);
13218 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
13219
13220 app.handle_action(Action::NextPane);
13222 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
13223
13224 app.handle_action(Action::PrevPane);
13226 assert_eq!(app.apig_state.apis.expanded_item, None);
13227 }
13228
13229 #[test]
13230 fn test_apig_column_preferences() {
13231 let mut app = test_app();
13232 app.current_service = Service::ApiGatewayApis;
13233 app.service_selected = true;
13234
13235 let initial_count = app.apig_api_visible_column_ids.len();
13237 assert_eq!(initial_count, app.apig_api_column_ids.len());
13238
13239 app.handle_action(Action::OpenColumnSelector);
13241 assert_eq!(app.mode, Mode::ColumnSelector);
13242
13243 app.column_selector_index = 1;
13245 let first_col = app.apig_api_column_ids[0];
13246 assert!(app.apig_api_visible_column_ids.contains(&first_col));
13247
13248 app.handle_action(Action::ToggleColumn);
13249
13250 assert!(!app.apig_api_visible_column_ids.contains(&first_col));
13252 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
13253
13254 app.handle_action(Action::ToggleColumn);
13256 assert!(app.apig_api_visible_column_ids.contains(&first_col));
13257 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
13258 }
13259
13260 #[test]
13261 fn test_apig_page_size_preferences() {
13262 use crate::common::PageSize;
13263
13264 let mut app = test_app();
13265 app.current_service = Service::ApiGatewayApis;
13266 app.service_selected = true;
13267
13268 assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
13270
13271 app.handle_action(Action::OpenColumnSelector);
13273
13274 app.column_selector_index = app.apig_api_column_ids.len() + 3;
13279 app.handle_action(Action::ToggleColumn);
13280 assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
13281
13282 app.column_selector_index = app.apig_api_column_ids.len() + 6;
13284 app.handle_action(Action::ToggleColumn);
13285 assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
13286 }
13287
13288 #[test]
13289 fn test_apig_preferences_skip_blank_row() {
13290 let mut app = test_app();
13291 app.current_service = Service::ApiGatewayApis;
13292 app.service_selected = true;
13293 app.mode = Mode::ColumnSelector;
13294
13295 app.column_selector_index = 8;
13297
13298 app.handle_action(Action::NextItem);
13300 assert_eq!(app.column_selector_index, 10);
13301
13302 app.handle_action(Action::PrevItem);
13304 assert_eq!(app.column_selector_index, 8);
13305 }
13306
13307 #[test]
13308 fn test_apig_preferences_ctrl_d_skip_blank_row() {
13309 let mut app = test_app();
13310 app.current_service = Service::ApiGatewayApis;
13311 app.service_selected = true;
13312 app.mode = Mode::ColumnSelector;
13313
13314 app.column_selector_index = 0;
13316
13317 app.handle_action(Action::PageDown);
13319 assert_eq!(app.column_selector_index, 10);
13320
13321 app.handle_action(Action::PageUp);
13323 assert_eq!(app.column_selector_index, 0);
13324 }
13325
13326 #[test]
13327 fn test_apig_preferences_ctrl_u_skip_blank_row() {
13328 let mut app = test_app();
13329 app.current_service = Service::ApiGatewayApis;
13330 app.service_selected = true;
13331 app.mode = Mode::ColumnSelector;
13332
13333 app.column_selector_index = 10;
13335
13336 app.handle_action(Action::PageUp);
13338 assert_eq!(app.column_selector_index, 0);
13339
13340 app.column_selector_index = 14; app.handle_action(Action::PageUp);
13345 assert_eq!(app.column_selector_index, 4);
13346 }
13347
13348 #[test]
13349 fn test_apig_preferences_tab_cycles_sections() {
13350 let mut app = test_app();
13351 app.current_service = Service::ApiGatewayApis;
13352 app.service_selected = true;
13353 app.mode = Mode::ColumnSelector;
13354
13355 app.column_selector_index = 0;
13357
13358 app.handle_action(Action::NextPreferences);
13360 assert_eq!(app.column_selector_index, 10);
13361
13362 app.handle_action(Action::NextPreferences);
13364 assert_eq!(app.column_selector_index, 0);
13365 }
13366
13367 #[test]
13368 fn test_apig_preferences_shift_tab_cycles_sections() {
13369 let mut app = test_app();
13370 app.current_service = Service::ApiGatewayApis;
13371 app.service_selected = true;
13372 app.mode = Mode::ColumnSelector;
13373
13374 app.column_selector_index = 0;
13376
13377 app.handle_action(Action::PrevPreferences);
13379 assert_eq!(app.column_selector_index, 10);
13380
13381 app.handle_action(Action::PrevPreferences);
13383 assert_eq!(app.column_selector_index, 0);
13384 }
13385
13386 #[test]
13387 fn test_apig_arrow_navigation() {
13388 let mut app = test_app();
13389 app.current_service = Service::ApiGatewayApis;
13390 app.service_selected = true;
13391 app.mode = Mode::Normal; app.apig_state.apis.filter = String::new(); app.apig_state.apis.items = vec![
13394 crate::apig::api::RestApi {
13395 id: "api1".to_string(),
13396 name: "test-api-1".to_string(),
13397 description: "Test API 1".to_string(),
13398 created_date: "2023-01-01".to_string(),
13399 api_key_source: "HEADER".to_string(),
13400 endpoint_configuration: "REGIONAL".to_string(),
13401 protocol_type: "REST".to_string(),
13402 disable_execute_api_endpoint: false,
13403 status: "AVAILABLE".to_string(),
13404 },
13405 crate::apig::api::RestApi {
13406 id: "api2".to_string(),
13407 name: "test-api-2".to_string(),
13408 description: "Test API 2".to_string(),
13409 created_date: "2023-01-02".to_string(),
13410 api_key_source: "HEADER".to_string(),
13411 endpoint_configuration: "REGIONAL".to_string(),
13412 protocol_type: "HTTP".to_string(),
13413 disable_execute_api_endpoint: false,
13414 status: "AVAILABLE".to_string(),
13415 },
13416 ];
13417 app.apig_state.apis.selected = 0;
13418
13419 app.handle_action(Action::NextItem);
13421 assert_eq!(app.apig_state.apis.selected, 1);
13422
13423 app.handle_action(Action::PrevItem);
13425 assert_eq!(app.apig_state.apis.selected, 0);
13426 }
13427
13428 #[test]
13429 fn test_apig_collapse_row() {
13430 let mut app = test_app();
13431 app.current_service = Service::ApiGatewayApis;
13432 app.service_selected = true;
13433 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
13434 id: "api1".to_string(),
13435 name: "test-api".to_string(),
13436 description: "Test API".to_string(),
13437 created_date: "2023-01-01".to_string(),
13438 api_key_source: "HEADER".to_string(),
13439 endpoint_configuration: "REGIONAL".to_string(),
13440 protocol_type: "REST".to_string(),
13441 disable_execute_api_endpoint: false,
13442 status: "AVAILABLE".to_string(),
13443 }];
13444 app.apig_state.apis.selected = 0;
13445 app.apig_state.apis.expanded_item = Some(0);
13446
13447 app.handle_action(Action::CollapseRow);
13449 assert_eq!(app.apig_state.apis.expanded_item, None);
13450 }
13451
13452 #[test]
13453 fn test_apig_filter_resets_selection() {
13454 let mut app = test_app();
13455 app.current_service = Service::ApiGatewayApis;
13456 app.service_selected = true;
13457 app.mode = Mode::FilterInput;
13458 app.apig_state.input_focus = InputFocus::Filter;
13459 app.apig_state.apis.items = vec![
13460 crate::apig::api::RestApi {
13461 id: "api1".to_string(),
13462 name: "alpha-api".to_string(),
13463 description: "Alpha API".to_string(),
13464 created_date: "2023-01-01".to_string(),
13465 api_key_source: "HEADER".to_string(),
13466 endpoint_configuration: "REGIONAL".to_string(),
13467 protocol_type: "REST".to_string(),
13468 disable_execute_api_endpoint: false,
13469 status: "AVAILABLE".to_string(),
13470 },
13471 crate::apig::api::RestApi {
13472 id: "api2".to_string(),
13473 name: "beta-api".to_string(),
13474 description: "Beta API".to_string(),
13475 created_date: "2023-01-02".to_string(),
13476 api_key_source: "HEADER".to_string(),
13477 endpoint_configuration: "REGIONAL".to_string(),
13478 protocol_type: "HTTP".to_string(),
13479 disable_execute_api_endpoint: false,
13480 status: "AVAILABLE".to_string(),
13481 },
13482 ];
13483
13484 app.apig_state.apis.selected = 1;
13486 app.apig_state.apis.expanded_item = Some(1);
13487
13488 app.handle_action(Action::FilterInput('a'));
13490
13491 assert_eq!(app.apig_state.apis.selected, 0);
13493 assert_eq!(app.apig_state.apis.expanded_item, None);
13494 assert_eq!(app.apig_state.apis.filter, "a");
13495 }
13496
13497 #[test]
13498 fn test_service_picker_starts_in_normal_mode() {
13499 let app = test_app();
13500 assert_eq!(app.mode, Mode::ServicePicker);
13501 assert!(!app.service_picker.filter_active);
13502 }
13503
13504 #[test]
13505 fn test_service_picker_i_key_activates_filter() {
13506 let mut app = test_app();
13507
13508 assert_eq!(app.mode, Mode::ServicePicker);
13510 assert!(!app.service_picker.filter_active);
13511 assert!(app.service_picker.filter.is_empty());
13512
13513 app.handle_action(Action::EnterFilterMode);
13515
13516 assert_eq!(app.mode, Mode::ServicePicker);
13518 assert!(app.service_picker.filter_active);
13519 assert!(app.service_picker.filter.is_empty());
13520
13521 app.handle_action(Action::FilterInput('s'));
13523 assert_eq!(app.service_picker.filter, "s");
13524 }
13525
13526 #[test]
13527 fn test_service_picker_esc_exits_filter_mode() {
13528 let mut app = test_app();
13529 assert_eq!(app.mode, Mode::ServicePicker);
13530 assert!(!app.service_picker.filter_active);
13531
13532 app.handle_action(Action::EnterFilterMode);
13534 assert!(app.service_picker.filter_active);
13535 assert_eq!(app.mode, Mode::ServicePicker);
13536
13537 app.handle_action(Action::FilterInput('s'));
13539 assert_eq!(app.service_picker.filter, "s");
13540
13541 app.handle_action(Action::ExitFilterMode);
13543 assert!(!app.service_picker.filter_active);
13544 assert_eq!(app.mode, Mode::ServicePicker); app.handle_action(Action::ExitFilterMode);
13548 assert_eq!(app.mode, Mode::Normal);
13549 }
13550
13551 #[test]
13552 fn test_service_picker_navigation_only_works_in_normal_mode() {
13553 let mut app = test_app();
13554 app.service_picker.selected = 0;
13555
13556 assert!(!app.service_picker.filter_active);
13558 app.handle_action(Action::NextItem);
13559 assert_eq!(app.service_picker.selected, 1);
13560
13561 app.handle_action(Action::EnterFilterMode);
13563 assert!(app.service_picker.filter_active);
13564
13565 let prev_selected = app.service_picker.selected;
13567 app.handle_action(Action::NextItem);
13568 assert_eq!(app.service_picker.selected, prev_selected); app.handle_action(Action::ExitFilterMode);
13572 assert!(!app.service_picker.filter_active);
13573
13574 app.handle_action(Action::NextItem);
13576 assert_eq!(app.service_picker.selected, prev_selected + 1);
13577 }
13578
13579 #[test]
13580 fn test_service_picker_typing_filters_services() {
13581 let mut app = test_app();
13582
13583 assert_eq!(app.mode, Mode::ServicePicker);
13585 assert!(!app.service_picker.filter_active);
13586
13587 app.handle_action(Action::EnterFilterMode);
13589 assert!(app.service_picker.filter_active);
13590
13591 app.handle_action(Action::FilterInput('s'));
13593 app.handle_action(Action::FilterInput('3'));
13594
13595 assert_eq!(app.service_picker.filter, "s3");
13596 assert_eq!(app.mode, Mode::ServicePicker);
13597 }
13598
13599 #[test]
13600 fn test_service_picker_enter_exits_insert_mode() {
13601 let mut app = test_app();
13602 assert_eq!(app.mode, Mode::ServicePicker);
13603 assert!(!app.service_selected);
13604
13605 app.handle_action(Action::EnterFilterMode);
13607 assert!(app.service_picker.filter_active);
13608
13609 app.handle_action(Action::FilterInput('s'));
13611 app.handle_action(Action::FilterInput('3'));
13612
13613 app.handle_action(Action::Select);
13615 assert!(!app.service_selected, "Should not select service");
13616 assert!(!app.service_picker.filter_active, "Should exit INSERT mode");
13617 assert_eq!(app.mode, Mode::ServicePicker);
13618
13619 app.handle_action(Action::Select);
13621 assert!(app.service_selected, "Should select service in NORMAL mode");
13622 assert_eq!(app.mode, Mode::Normal);
13623 }
13624
13625 #[test]
13626 fn test_service_picker_resets_on_open() {
13627 let mut app = test_app();
13628
13629 app.service_selected = true;
13631 app.mode = Mode::Normal;
13632
13633 app.service_picker.filter = "previous".to_string();
13635 app.service_picker.filter_active = true;
13636 app.service_picker.selected = 5;
13637
13638 app.handle_action(Action::OpenSpaceMenu);
13640
13641 assert_eq!(app.mode, Mode::SpaceMenu);
13643 assert!(app.service_picker.filter.is_empty());
13644 assert!(!app.service_picker.filter_active);
13645 assert_eq!(app.service_picker.selected, 0);
13646 }
13647
13648 #[test]
13649 fn test_no_pii_in_test_data() {
13650 let test_repo = EcrRepository {
13652 name: "test-repo".to_string(),
13653 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
13654 created_at: "2024-01-01".to_string(),
13655 tag_immutability: "MUTABLE".to_string(),
13656 encryption_type: "AES256".to_string(),
13657 };
13658
13659 assert!(test_repo.uri.starts_with("123456789012"));
13661 assert!(!test_repo.uri.contains("123456789013")); }
13663
13664 #[test]
13665 fn test_lambda_versions_tab_triggers_loading() {
13666 let mut app = test_app();
13667 app.current_service = Service::LambdaFunctions;
13668 app.service_selected = true;
13669
13670 app.lambda_state.current_function = Some("test-function".to_string());
13672 app.lambda_state.detail_tab = LambdaDetailTab::Code;
13673
13674 assert!(app.lambda_state.version_table.items.is_empty());
13676
13677 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13679
13680 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
13683 assert!(app.lambda_state.current_function.is_some());
13684 }
13685
13686 #[test]
13687 fn test_lambda_versions_navigation() {
13688 let mut app = test_app();
13689 app.current_service = Service::LambdaFunctions;
13690 app.service_selected = true;
13691 app.lambda_state.current_function = Some("test-function".to_string());
13692 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13693
13694 app.lambda_state.version_table.items = vec![
13696 LambdaVersion {
13697 version: "3".to_string(),
13698 aliases: "prod".to_string(),
13699 description: "".to_string(),
13700 last_modified: "".to_string(),
13701 architecture: "X86_64".to_string(),
13702 },
13703 LambdaVersion {
13704 version: "2".to_string(),
13705 aliases: "".to_string(),
13706 description: "".to_string(),
13707 last_modified: "".to_string(),
13708 architecture: "X86_64".to_string(),
13709 },
13710 LambdaVersion {
13711 version: "1".to_string(),
13712 aliases: "".to_string(),
13713 description: "".to_string(),
13714 last_modified: "".to_string(),
13715 architecture: "X86_64".to_string(),
13716 },
13717 ];
13718
13719 assert_eq!(app.lambda_state.version_table.items.len(), 3);
13721 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
13722 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
13723
13724 app.lambda_state.version_table.selected = 1;
13726 assert_eq!(app.lambda_state.version_table.selected, 1);
13727 }
13728
13729 #[test]
13730 fn test_lambda_versions_with_aliases() {
13731 let version = LambdaVersion {
13732 version: "35".to_string(),
13733 aliases: "prod, staging".to_string(),
13734 description: "Production version".to_string(),
13735 last_modified: "2024-01-01".to_string(),
13736 architecture: "X86_64".to_string(),
13737 };
13738
13739 assert_eq!(version.aliases, "prod, staging");
13740 assert!(!version.aliases.is_empty());
13741 }
13742
13743 #[test]
13744 fn test_lambda_versions_expansion() {
13745 let mut app = test_app();
13746 app.current_service = Service::LambdaFunctions;
13747 app.service_selected = true;
13748 app.lambda_state.current_function = Some("test-function".to_string());
13749 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13750
13751 app.lambda_state.version_table.items = vec![
13753 LambdaVersion {
13754 version: "2".to_string(),
13755 aliases: "prod".to_string(),
13756 description: "Production".to_string(),
13757 last_modified: "2024-01-01".to_string(),
13758 architecture: "X86_64".to_string(),
13759 },
13760 LambdaVersion {
13761 version: "1".to_string(),
13762 aliases: "".to_string(),
13763 description: "".to_string(),
13764 last_modified: "2024-01-01".to_string(),
13765 architecture: "Arm64".to_string(),
13766 },
13767 ];
13768
13769 app.lambda_state.version_table.selected = 0;
13770
13771 app.lambda_state.version_table.expanded_item = Some(0);
13773 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
13774
13775 app.lambda_state.version_table.selected = 1;
13777 app.lambda_state.version_table.expanded_item = Some(1);
13778 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
13779 }
13780
13781 #[test]
13782 fn test_lambda_versions_page_navigation() {
13783 let mut app = test_app();
13784 app.current_service = Service::LambdaFunctions;
13785 app.service_selected = true;
13786 app.lambda_state.current_function = Some("test-function".to_string());
13787 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13788
13789 app.lambda_state.version_table.items = (1..=30)
13791 .map(|i| LambdaVersion {
13792 version: i.to_string(),
13793 aliases: "".to_string(),
13794 description: "".to_string(),
13795 last_modified: "".to_string(),
13796 architecture: "X86_64".to_string(),
13797 })
13798 .collect();
13799
13800 app.lambda_state.version_table.page_size = PageSize::Ten;
13801 app.lambda_state.version_table.selected = 0;
13802
13803 app.page_input = "2".to_string();
13805 app.handle_action(Action::OpenColumnSelector);
13806
13807 assert_eq!(app.lambda_state.version_table.selected, 10);
13809 }
13810
13811 #[test]
13812 fn test_lambda_versions_pagination_arrow_keys() {
13813 let mut app = test_app();
13814 app.current_service = Service::LambdaFunctions;
13815 app.service_selected = true;
13816 app.lambda_state.current_function = Some("test-function".to_string());
13817 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13818 app.mode = Mode::FilterInput;
13819 app.lambda_state.version_input_focus = InputFocus::Pagination;
13820
13821 app.lambda_state.version_table.items = (1..=30)
13823 .map(|i| LambdaVersion {
13824 version: i.to_string(),
13825 aliases: "".to_string(),
13826 description: "".to_string(),
13827 last_modified: "".to_string(),
13828 architecture: "X86_64".to_string(),
13829 })
13830 .collect();
13831
13832 app.lambda_state.version_table.page_size = PageSize::Ten;
13833 app.lambda_state.version_table.selected = 0;
13834
13835 app.handle_action(Action::PageDown);
13837 assert_eq!(app.lambda_state.version_table.selected, 10);
13838
13839 app.handle_action(Action::PageUp);
13841 assert_eq!(app.lambda_state.version_table.selected, 0);
13842 }
13843
13844 #[test]
13845 fn test_lambda_versions_page_input_in_filter_mode() {
13846 let mut app = test_app();
13847 app.current_service = Service::LambdaFunctions;
13848 app.service_selected = true;
13849 app.lambda_state.current_function = Some("test-function".to_string());
13850 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13851 app.mode = Mode::FilterInput;
13852 app.lambda_state.version_input_focus = InputFocus::Pagination;
13853
13854 app.lambda_state.version_table.items = (1..=30)
13856 .map(|i| LambdaVersion {
13857 version: i.to_string(),
13858 aliases: "".to_string(),
13859 description: "".to_string(),
13860 last_modified: "".to_string(),
13861 architecture: "X86_64".to_string(),
13862 })
13863 .collect();
13864
13865 app.lambda_state.version_table.page_size = PageSize::Ten;
13866 app.lambda_state.version_table.selected = 0;
13867
13868 app.handle_action(Action::FilterInput('2'));
13870 assert_eq!(app.page_input, "2");
13871 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
13875 assert_eq!(app.lambda_state.version_table.selected, 10);
13876 assert_eq!(app.page_input, ""); }
13878
13879 #[test]
13880 fn test_lambda_versions_filter_input() {
13881 let mut app = test_app();
13882 app.current_service = Service::LambdaFunctions;
13883 app.service_selected = true;
13884 app.lambda_state.current_function = Some("test-function".to_string());
13885 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13886 app.mode = Mode::FilterInput;
13887 app.lambda_state.version_input_focus = InputFocus::Filter;
13888
13889 app.lambda_state.version_table.items = vec![
13891 LambdaVersion {
13892 version: "1".to_string(),
13893 aliases: "prod".to_string(),
13894 description: "Production".to_string(),
13895 last_modified: "".to_string(),
13896 architecture: "X86_64".to_string(),
13897 },
13898 LambdaVersion {
13899 version: "2".to_string(),
13900 aliases: "staging".to_string(),
13901 description: "Staging".to_string(),
13902 last_modified: "".to_string(),
13903 architecture: "X86_64".to_string(),
13904 },
13905 ];
13906
13907 app.handle_action(Action::FilterInput('p'));
13909 app.handle_action(Action::FilterInput('r'));
13910 app.handle_action(Action::FilterInput('o'));
13911 app.handle_action(Action::FilterInput('d'));
13912 assert_eq!(app.lambda_state.version_table.filter, "prod");
13913
13914 app.handle_action(Action::FilterBackspace);
13916 assert_eq!(app.lambda_state.version_table.filter, "pro");
13917 }
13918
13919 #[test]
13920 fn test_lambda_aliases_table_expansion() {
13921 use crate::lambda::Alias;
13922
13923 let mut app = test_app();
13924 app.current_service = Service::LambdaFunctions;
13925 app.service_selected = true;
13926 app.lambda_state.current_function = Some("test-function".to_string());
13927 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
13928 app.mode = Mode::Normal;
13929
13930 app.lambda_state.alias_table.items = vec![
13931 Alias {
13932 name: "prod".to_string(),
13933 versions: "1".to_string(),
13934 description: "Production alias".to_string(),
13935 },
13936 Alias {
13937 name: "staging".to_string(),
13938 versions: "2".to_string(),
13939 description: "Staging alias".to_string(),
13940 },
13941 ];
13942
13943 app.lambda_state.alias_table.selected = 0;
13944
13945 app.handle_action(Action::Select);
13947 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
13948 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
13949
13950 app.handle_action(Action::GoBack);
13952 assert_eq!(app.lambda_state.current_alias, None);
13953 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
13954
13955 app.lambda_state.alias_table.selected = 1;
13957 app.handle_action(Action::Select);
13958 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
13959 }
13960
13961 #[test]
13962 fn test_lambda_versions_arrow_key_expansion() {
13963 let mut app = test_app();
13964 app.current_service = Service::LambdaFunctions;
13965 app.service_selected = true;
13966 app.lambda_state.current_function = Some("test-function".to_string());
13967 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13968 app.mode = Mode::Normal;
13969
13970 app.lambda_state.version_table.items = vec![LambdaVersion {
13971 version: "1".to_string(),
13972 aliases: "prod".to_string(),
13973 description: "Production".to_string(),
13974 last_modified: "2024-01-01".to_string(),
13975 architecture: "X86_64".to_string(),
13976 }];
13977
13978 app.lambda_state.version_table.selected = 0;
13979
13980 app.handle_action(Action::NextPane);
13982 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
13983
13984 app.handle_action(Action::PrevPane);
13986 assert_eq!(app.lambda_state.version_table.expanded_item, None);
13987 }
13988
13989 #[test]
13990 fn test_lambda_version_detail_view() {
13991 use crate::lambda::Function;
13992
13993 let mut app = test_app();
13994 app.current_service = Service::LambdaFunctions;
13995 app.service_selected = true;
13996 app.lambda_state.current_function = Some("test-function".to_string());
13997 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
13998 app.mode = Mode::Normal;
13999
14000 app.lambda_state.table.items = vec![Function {
14001 name: "test-function".to_string(),
14002 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14003 application: None,
14004 description: "Test".to_string(),
14005 package_type: "Zip".to_string(),
14006 runtime: "python3.12".to_string(),
14007 architecture: "X86_64".to_string(),
14008 code_size: 1024,
14009 code_sha256: "hash".to_string(),
14010 memory_mb: 128,
14011 timeout_seconds: 30,
14012 last_modified: "2024-01-01".to_string(),
14013 layers: vec![],
14014 }];
14015
14016 app.lambda_state.version_table.items = vec![LambdaVersion {
14017 version: "1".to_string(),
14018 aliases: "prod".to_string(),
14019 description: "Production".to_string(),
14020 last_modified: "2024-01-01".to_string(),
14021 architecture: "X86_64".to_string(),
14022 }];
14023
14024 app.lambda_state.version_table.selected = 0;
14025
14026 app.handle_action(Action::Select);
14028 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
14029 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14030
14031 app.handle_action(Action::GoBack);
14033 assert_eq!(app.lambda_state.current_version, None);
14034 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14035 }
14036
14037 #[test]
14038 fn test_lambda_version_detail_tabs() {
14039 use crate::lambda::Function;
14040
14041 let mut app = test_app();
14042 app.current_service = Service::LambdaFunctions;
14043 app.service_selected = true;
14044 app.lambda_state.current_function = Some("test-function".to_string());
14045 app.lambda_state.current_version = Some("1".to_string());
14046 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14047 app.mode = Mode::Normal;
14048
14049 app.lambda_state.table.items = vec![Function {
14050 name: "test-function".to_string(),
14051 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14052 application: None,
14053 description: "Test".to_string(),
14054 package_type: "Zip".to_string(),
14055 runtime: "python3.12".to_string(),
14056 architecture: "X86_64".to_string(),
14057 code_size: 1024,
14058 code_sha256: "hash".to_string(),
14059 memory_mb: 128,
14060 timeout_seconds: 30,
14061 last_modified: "2024-01-01".to_string(),
14062 layers: vec![],
14063 }];
14064
14065 app.handle_action(Action::NextDetailTab);
14067 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14068
14069 app.handle_action(Action::NextDetailTab);
14070 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14071
14072 app.handle_action(Action::NextDetailTab);
14073 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14074
14075 app.handle_action(Action::PrevDetailTab);
14077 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14078
14079 app.handle_action(Action::PrevDetailTab);
14080 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14081 }
14082
14083 #[test]
14084 fn test_lambda_aliases_arrow_key_expansion() {
14085 use crate::lambda::Alias;
14086
14087 let mut app = test_app();
14088 app.current_service = Service::LambdaFunctions;
14089 app.service_selected = true;
14090 app.lambda_state.current_function = Some("test-function".to_string());
14091 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
14092 app.mode = Mode::Normal;
14093
14094 app.lambda_state.alias_table.items = vec![Alias {
14095 name: "prod".to_string(),
14096 versions: "1".to_string(),
14097 description: "Production alias".to_string(),
14098 }];
14099
14100 app.lambda_state.alias_table.selected = 0;
14101
14102 app.handle_action(Action::NextPane);
14104 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
14105
14106 app.handle_action(Action::PrevPane);
14108 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
14109 }
14110
14111 #[test]
14112 fn test_lambda_functions_arrow_key_expansion() {
14113 use crate::lambda::Function;
14114
14115 let mut app = test_app();
14116 app.current_service = Service::LambdaFunctions;
14117 app.service_selected = true;
14118 app.mode = Mode::Normal;
14119
14120 app.lambda_state.table.items = vec![Function {
14121 name: "test-function".to_string(),
14122 arn: "arn".to_string(),
14123 application: None,
14124 description: "Test".to_string(),
14125 package_type: "Zip".to_string(),
14126 runtime: "python3.12".to_string(),
14127 architecture: "X86_64".to_string(),
14128 code_size: 1024,
14129 code_sha256: "hash".to_string(),
14130 memory_mb: 128,
14131 timeout_seconds: 30,
14132 last_modified: "2024-01-01".to_string(),
14133 layers: vec![],
14134 }];
14135
14136 app.lambda_state.table.selected = 0;
14137
14138 app.handle_action(Action::NextPane);
14140 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
14141
14142 app.handle_action(Action::PrevPane);
14144 assert_eq!(app.lambda_state.table.expanded_item, None);
14145 }
14146
14147 #[test]
14148 fn test_lambda_version_detail_with_application() {
14149 use crate::lambda::Function;
14150
14151 let mut app = test_app();
14152 app.current_service = Service::LambdaFunctions;
14153 app.service_selected = true;
14154 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
14155 app.lambda_state.current_version = Some("1".to_string());
14156 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14157 app.mode = Mode::Normal;
14158
14159 app.lambda_state.table.items = vec![Function {
14160 name: "storefront-studio-beta-api".to_string(),
14161 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
14162 .to_string(),
14163 application: Some("storefront-studio-beta".to_string()),
14164 description: "API function".to_string(),
14165 package_type: "Zip".to_string(),
14166 runtime: "python3.12".to_string(),
14167 architecture: "X86_64".to_string(),
14168 code_size: 1024,
14169 code_sha256: "hash".to_string(),
14170 memory_mb: 128,
14171 timeout_seconds: 30,
14172 last_modified: "2024-01-01".to_string(),
14173 layers: vec![],
14174 }];
14175
14176 assert_eq!(
14178 app.lambda_state.table.items[0].application,
14179 Some("storefront-studio-beta".to_string())
14180 );
14181 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
14182 }
14183
14184 #[test]
14185 fn test_lambda_layer_navigation() {
14186 use crate::lambda::{Function, Layer};
14187
14188 let mut app = test_app();
14189 app.current_service = Service::LambdaFunctions;
14190 app.service_selected = true;
14191 app.lambda_state.current_function = Some("test-function".to_string());
14192 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14193 app.mode = Mode::Normal;
14194
14195 app.lambda_state.table.items = vec![Function {
14196 name: "test-function".to_string(),
14197 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14198 application: None,
14199 description: "Test".to_string(),
14200 package_type: "Zip".to_string(),
14201 runtime: "python3.12".to_string(),
14202 architecture: "X86_64".to_string(),
14203 code_size: 1024,
14204 code_sha256: "hash".to_string(),
14205 memory_mb: 128,
14206 timeout_seconds: 30,
14207 last_modified: "2024-01-01".to_string(),
14208 layers: vec![
14209 Layer {
14210 merge_order: "1".to_string(),
14211 name: "layer1".to_string(),
14212 layer_version: "1".to_string(),
14213 compatible_runtimes: "python3.9".to_string(),
14214 compatible_architectures: "x86_64".to_string(),
14215 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
14216 },
14217 Layer {
14218 merge_order: "2".to_string(),
14219 name: "layer2".to_string(),
14220 layer_version: "2".to_string(),
14221 compatible_runtimes: "python3.9".to_string(),
14222 compatible_architectures: "x86_64".to_string(),
14223 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
14224 },
14225 Layer {
14226 merge_order: "3".to_string(),
14227 name: "layer3".to_string(),
14228 layer_version: "3".to_string(),
14229 compatible_runtimes: "python3.9".to_string(),
14230 compatible_architectures: "x86_64".to_string(),
14231 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
14232 },
14233 ],
14234 }];
14235
14236 assert_eq!(app.lambda_state.layer_selected, 0);
14237
14238 app.handle_action(Action::NextItem);
14239 assert_eq!(app.lambda_state.layer_selected, 1);
14240
14241 app.handle_action(Action::NextItem);
14242 assert_eq!(app.lambda_state.layer_selected, 2);
14243
14244 app.handle_action(Action::NextItem);
14245 assert_eq!(app.lambda_state.layer_selected, 2);
14246
14247 app.handle_action(Action::PrevItem);
14248 assert_eq!(app.lambda_state.layer_selected, 1);
14249
14250 app.handle_action(Action::PrevItem);
14251 assert_eq!(app.lambda_state.layer_selected, 0);
14252
14253 app.handle_action(Action::PrevItem);
14254 assert_eq!(app.lambda_state.layer_selected, 0);
14255 }
14256
14257 #[test]
14258 fn test_lambda_layer_expansion() {
14259 use crate::lambda::{Function, Layer};
14260
14261 let mut app = test_app();
14262 app.current_service = Service::LambdaFunctions;
14263 app.service_selected = true;
14264 app.lambda_state.current_function = Some("test-function".to_string());
14265 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14266 app.mode = Mode::Normal;
14267
14268 app.lambda_state.table.items = vec![Function {
14269 name: "test-function".to_string(),
14270 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14271 application: None,
14272 description: "Test".to_string(),
14273 package_type: "Zip".to_string(),
14274 runtime: "python3.12".to_string(),
14275 architecture: "X86_64".to_string(),
14276 code_size: 1024,
14277 code_sha256: "hash".to_string(),
14278 memory_mb: 128,
14279 timeout_seconds: 30,
14280 last_modified: "2024-01-01".to_string(),
14281 layers: vec![Layer {
14282 merge_order: "1".to_string(),
14283 name: "test-layer".to_string(),
14284 layer_version: "1".to_string(),
14285 compatible_runtimes: "python3.9".to_string(),
14286 compatible_architectures: "x86_64".to_string(),
14287 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
14288 }],
14289 }];
14290
14291 assert_eq!(app.lambda_state.layer_expanded, None);
14292
14293 app.handle_action(Action::NextPane);
14294 assert_eq!(app.lambda_state.layer_expanded, Some(0));
14295
14296 app.handle_action(Action::PrevPane);
14297 assert_eq!(app.lambda_state.layer_expanded, None);
14298
14299 app.handle_action(Action::NextPane);
14300 assert_eq!(app.lambda_state.layer_expanded, Some(0));
14301
14302 app.handle_action(Action::NextPane);
14303 assert_eq!(app.lambda_state.layer_expanded, None);
14304 }
14305
14306 #[test]
14307 fn test_lambda_layer_selection_and_expansion_workflow() {
14308 use crate::lambda::{Function, Layer};
14309
14310 let mut app = test_app();
14311 app.current_service = Service::LambdaFunctions;
14312 app.service_selected = true;
14313 app.lambda_state.current_function = Some("test-function".to_string());
14314 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14315 app.mode = Mode::Normal;
14316
14317 app.lambda_state.table.items = vec![Function {
14318 name: "test-function".to_string(),
14319 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14320 application: None,
14321 description: "Test".to_string(),
14322 package_type: "Zip".to_string(),
14323 runtime: "python3.12".to_string(),
14324 architecture: "X86_64".to_string(),
14325 code_size: 1024,
14326 code_sha256: "hash".to_string(),
14327 memory_mb: 128,
14328 timeout_seconds: 30,
14329 last_modified: "2024-01-01".to_string(),
14330 layers: vec![
14331 Layer {
14332 merge_order: "1".to_string(),
14333 name: "layer1".to_string(),
14334 layer_version: "1".to_string(),
14335 compatible_runtimes: "python3.9".to_string(),
14336 compatible_architectures: "x86_64".to_string(),
14337 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
14338 },
14339 Layer {
14340 merge_order: "2".to_string(),
14341 name: "layer2".to_string(),
14342 layer_version: "2".to_string(),
14343 compatible_runtimes: "python3.9".to_string(),
14344 compatible_architectures: "x86_64".to_string(),
14345 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
14346 },
14347 ],
14348 }];
14349
14350 assert_eq!(app.lambda_state.layer_selected, 0);
14352 assert_eq!(app.lambda_state.layer_expanded, None);
14353
14354 app.handle_action(Action::NextPane);
14356 assert_eq!(app.lambda_state.layer_selected, 0);
14357 assert_eq!(app.lambda_state.layer_expanded, Some(0));
14358
14359 app.handle_action(Action::NextItem);
14361 assert_eq!(app.lambda_state.layer_selected, 1);
14362 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
14366 assert_eq!(app.lambda_state.layer_selected, 1);
14367 assert_eq!(app.lambda_state.layer_expanded, Some(1));
14368
14369 app.handle_action(Action::PrevPane);
14371 assert_eq!(app.lambda_state.layer_selected, 1);
14372 assert_eq!(app.lambda_state.layer_expanded, None);
14373
14374 app.handle_action(Action::PrevItem);
14376 assert_eq!(app.lambda_state.layer_selected, 0);
14377 assert_eq!(app.lambda_state.layer_expanded, None);
14378 }
14379
14380 #[test]
14381 fn test_backtab_cycles_detail_tabs_backward() {
14382 let mut app = test_app();
14383 app.mode = Mode::Normal;
14384
14385 app.current_service = Service::LambdaFunctions;
14387 app.service_selected = true;
14388 app.lambda_state.current_function = Some("test-function".to_string());
14389 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14390
14391 app.handle_action(Action::PrevDetailTab);
14392 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14393
14394 app.handle_action(Action::PrevDetailTab);
14395 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14396
14397 app.current_service = Service::IamRoles;
14399 app.iam_state.current_role = Some("test-role".to_string());
14400 app.iam_state.role_tab = RoleTab::Permissions;
14401
14402 app.handle_action(Action::PrevDetailTab);
14403 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
14404
14405 app.current_service = Service::IamUsers;
14407 app.iam_state.current_user = Some("test-user".to_string());
14408 app.iam_state.user_tab = UserTab::Permissions;
14409
14410 app.handle_action(Action::PrevDetailTab);
14411 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
14412
14413 app.current_service = Service::IamUserGroups;
14415 app.iam_state.current_group = Some("test-group".to_string());
14416 app.iam_state.group_tab = GroupTab::Permissions;
14417
14418 app.handle_action(Action::PrevDetailTab);
14419 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
14420
14421 app.current_service = Service::S3Buckets;
14423 app.s3_state.current_bucket = Some("test-bucket".to_string());
14424 app.s3_state.object_tab = S3ObjectTab::Properties;
14425
14426 app.handle_action(Action::PrevDetailTab);
14427 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
14428
14429 app.current_service = Service::EcrRepositories;
14431 app.ecr_state.current_repository = None;
14432 app.ecr_state.tab = EcrTab::Private;
14433
14434 app.handle_action(Action::PrevDetailTab);
14435 assert_eq!(app.ecr_state.tab, EcrTab::Public);
14436
14437 app.current_service = Service::CloudFormationStacks;
14439 app.cfn_state.current_stack = Some("test-stack".to_string());
14440 app.cfn_state.detail_tab = CfnDetailTab::Resources;
14441 }
14442
14443 #[test]
14444 fn test_cloudformation_status_filter_active() {
14445 let filter = CfnStatusFilter::Active;
14446 assert!(filter.matches("CREATE_IN_PROGRESS"));
14447 assert!(filter.matches("UPDATE_IN_PROGRESS"));
14448 assert!(!filter.matches("CREATE_COMPLETE"));
14449 assert!(!filter.matches("DELETE_COMPLETE"));
14450 assert!(!filter.matches("CREATE_FAILED"));
14451 }
14452
14453 #[test]
14454 fn test_cloudformation_status_filter_complete() {
14455 let filter = CfnStatusFilter::Complete;
14456 assert!(filter.matches("CREATE_COMPLETE"));
14457 assert!(filter.matches("UPDATE_COMPLETE"));
14458 assert!(!filter.matches("DELETE_COMPLETE"));
14459 assert!(!filter.matches("CREATE_IN_PROGRESS"));
14460 }
14461
14462 #[test]
14463 fn test_cloudformation_status_filter_failed() {
14464 let filter = CfnStatusFilter::Failed;
14465 assert!(filter.matches("CREATE_FAILED"));
14466 assert!(filter.matches("UPDATE_FAILED"));
14467 assert!(!filter.matches("CREATE_COMPLETE"));
14468 }
14469
14470 #[test]
14471 fn test_cloudformation_status_filter_deleted() {
14472 let filter = CfnStatusFilter::Deleted;
14473 assert!(filter.matches("DELETE_COMPLETE"));
14474 assert!(filter.matches("DELETE_IN_PROGRESS"));
14475 assert!(!filter.matches("CREATE_COMPLETE"));
14476 }
14477
14478 #[test]
14479 fn test_cloudformation_status_filter_in_progress() {
14480 let filter = CfnStatusFilter::InProgress;
14481 assert!(filter.matches("CREATE_IN_PROGRESS"));
14482 assert!(filter.matches("UPDATE_IN_PROGRESS"));
14483 assert!(filter.matches("DELETE_IN_PROGRESS"));
14484 assert!(!filter.matches("CREATE_COMPLETE"));
14485 }
14486
14487 #[test]
14488 fn test_cloudformation_status_filter_cycle() {
14489 let filter = CfnStatusFilter::All;
14490 assert_eq!(filter.next(), CfnStatusFilter::Active);
14491 assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
14492 assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
14493 assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
14494 assert_eq!(
14495 filter.next().next().next().next().next(),
14496 CfnStatusFilter::InProgress
14497 );
14498 assert_eq!(
14499 filter.next().next().next().next().next().next(),
14500 CfnStatusFilter::All
14501 );
14502 }
14503
14504 #[test]
14505 fn test_cloudformation_default_columns() {
14506 let app = test_app();
14507 assert_eq!(app.cfn_visible_column_ids.len(), 4);
14508 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
14509 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
14510 assert!(app
14511 .cfn_visible_column_ids
14512 .contains(&CfnColumn::CreatedTime.id()));
14513 assert!(app
14514 .cfn_visible_column_ids
14515 .contains(&CfnColumn::Description.id()));
14516 }
14517
14518 #[test]
14519 fn test_cloudformation_all_columns() {
14520 let app = test_app();
14521 assert_eq!(app.cfn_column_ids.len(), 10);
14522 }
14523
14524 #[test]
14525 fn test_cloudformation_filter_by_name() {
14526 let mut app = test_app();
14527 app.cfn_state.status_filter = CfnStatusFilter::Complete;
14528 app.cfn_state.table.items = vec![
14529 CfnStack {
14530 name: "my-stack".to_string(),
14531 stack_id: "id1".to_string(),
14532 status: "CREATE_COMPLETE".to_string(),
14533 created_time: "2024-01-01".to_string(),
14534 updated_time: String::new(),
14535 deleted_time: String::new(),
14536 drift_status: String::new(),
14537 last_drift_check_time: String::new(),
14538 status_reason: String::new(),
14539 description: String::new(),
14540 detailed_status: String::new(),
14541 root_stack: String::new(),
14542 parent_stack: String::new(),
14543 termination_protection: false,
14544 iam_role: String::new(),
14545 tags: Vec::new(),
14546 stack_policy: String::new(),
14547 rollback_monitoring_time: String::new(),
14548 rollback_alarms: Vec::new(),
14549 notification_arns: Vec::new(),
14550 },
14551 CfnStack {
14552 name: "other-stack".to_string(),
14553 stack_id: "id2".to_string(),
14554 status: "CREATE_COMPLETE".to_string(),
14555 created_time: "2024-01-02".to_string(),
14556 updated_time: String::new(),
14557 deleted_time: String::new(),
14558 drift_status: String::new(),
14559 last_drift_check_time: String::new(),
14560 status_reason: String::new(),
14561 description: String::new(),
14562 detailed_status: String::new(),
14563 root_stack: String::new(),
14564 parent_stack: String::new(),
14565 termination_protection: false,
14566 iam_role: String::new(),
14567 tags: Vec::new(),
14568 stack_policy: String::new(),
14569 rollback_monitoring_time: String::new(),
14570 rollback_alarms: Vec::new(),
14571 notification_arns: Vec::new(),
14572 },
14573 ];
14574
14575 app.cfn_state.table.filter = "my".to_string();
14576 let filtered = filtered_cloudformation_stacks(&app);
14577 assert_eq!(filtered.len(), 1);
14578 assert_eq!(filtered[0].name, "my-stack");
14579 }
14580
14581 #[test]
14582 fn test_cloudformation_filter_by_description() {
14583 let mut app = test_app();
14584 app.cfn_state.status_filter = CfnStatusFilter::Complete;
14585 app.cfn_state.table.items = vec![CfnStack {
14586 name: "stack1".to_string(),
14587 stack_id: "id1".to_string(),
14588 status: "CREATE_COMPLETE".to_string(),
14589 created_time: "2024-01-01".to_string(),
14590 updated_time: String::new(),
14591 deleted_time: String::new(),
14592 drift_status: String::new(),
14593 last_drift_check_time: String::new(),
14594 status_reason: String::new(),
14595 description: "production stack".to_string(),
14596 detailed_status: String::new(),
14597 root_stack: String::new(),
14598 parent_stack: String::new(),
14599 termination_protection: false,
14600 iam_role: String::new(),
14601 tags: Vec::new(),
14602 stack_policy: String::new(),
14603 rollback_monitoring_time: String::new(),
14604 rollback_alarms: Vec::new(),
14605 notification_arns: Vec::new(),
14606 }];
14607
14608 app.cfn_state.table.filter = "production".to_string();
14609 let filtered = filtered_cloudformation_stacks(&app);
14610 assert_eq!(filtered.len(), 1);
14611 }
14612
14613 #[test]
14614 fn test_cloudformation_status_filter_applied() {
14615 let mut app = test_app();
14616 app.cfn_state.table.items = vec![
14617 CfnStack {
14618 name: "complete-stack".to_string(),
14619 stack_id: "id1".to_string(),
14620 status: "CREATE_COMPLETE".to_string(),
14621 created_time: "2024-01-01".to_string(),
14622 updated_time: String::new(),
14623 deleted_time: String::new(),
14624 drift_status: String::new(),
14625 last_drift_check_time: String::new(),
14626 status_reason: String::new(),
14627 description: String::new(),
14628 detailed_status: String::new(),
14629 root_stack: String::new(),
14630 parent_stack: String::new(),
14631 termination_protection: false,
14632 iam_role: String::new(),
14633 tags: Vec::new(),
14634 stack_policy: String::new(),
14635 rollback_monitoring_time: String::new(),
14636 rollback_alarms: Vec::new(),
14637 notification_arns: Vec::new(),
14638 },
14639 CfnStack {
14640 name: "failed-stack".to_string(),
14641 stack_id: "id2".to_string(),
14642 status: "CREATE_FAILED".to_string(),
14643 created_time: "2024-01-02".to_string(),
14644 updated_time: String::new(),
14645 deleted_time: String::new(),
14646 drift_status: String::new(),
14647 last_drift_check_time: String::new(),
14648 status_reason: String::new(),
14649 description: String::new(),
14650 detailed_status: String::new(),
14651 root_stack: String::new(),
14652 parent_stack: String::new(),
14653 termination_protection: false,
14654 iam_role: String::new(),
14655 tags: Vec::new(),
14656 stack_policy: String::new(),
14657 rollback_monitoring_time: String::new(),
14658 rollback_alarms: Vec::new(),
14659 notification_arns: Vec::new(),
14660 },
14661 ];
14662
14663 app.cfn_state.status_filter = CfnStatusFilter::Complete;
14664 let filtered = filtered_cloudformation_stacks(&app);
14665 assert_eq!(filtered.len(), 1);
14666 assert_eq!(filtered[0].name, "complete-stack");
14667
14668 app.cfn_state.status_filter = CfnStatusFilter::Failed;
14669 let filtered = filtered_cloudformation_stacks(&app);
14670 assert_eq!(filtered.len(), 1);
14671 assert_eq!(filtered[0].name, "failed-stack");
14672 }
14673
14674 #[test]
14675 fn test_cloudformation_default_page_size() {
14676 let app = test_app();
14677 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
14678 }
14679
14680 #[test]
14681 fn test_cloudformation_default_status_filter() {
14682 let app = test_app();
14683 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
14684 }
14685
14686 #[test]
14687 fn test_cloudformation_view_nested_default_false() {
14688 let app = test_app();
14689 assert!(!app.cfn_state.view_nested);
14690 }
14691
14692 #[test]
14693 fn test_cloudformation_pagination_hotkeys() {
14694 let mut app = test_app();
14695 app.current_service = Service::CloudFormationStacks;
14696 app.service_selected = true;
14697 app.cfn_state.status_filter = CfnStatusFilter::All;
14698
14699 for i in 0..150 {
14701 app.cfn_state.table.items.push(CfnStack {
14702 name: format!("stack-{}", i),
14703 stack_id: format!("id-{}", i),
14704 status: "CREATE_COMPLETE".to_string(),
14705 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14706 updated_time: String::new(),
14707 deleted_time: String::new(),
14708 drift_status: String::new(),
14709 last_drift_check_time: String::new(),
14710 status_reason: String::new(),
14711 description: String::new(),
14712 detailed_status: String::new(),
14713 root_stack: String::new(),
14714 parent_stack: String::new(),
14715 termination_protection: false,
14716 iam_role: String::new(),
14717 tags: vec![],
14718 stack_policy: String::new(),
14719 rollback_monitoring_time: String::new(),
14720 rollback_alarms: vec![],
14721 notification_arns: vec![],
14722 });
14723 }
14724
14725 app.go_to_page(2);
14727 assert_eq!(app.cfn_state.table.selected, 50);
14728
14729 app.go_to_page(3);
14731 assert_eq!(app.cfn_state.table.selected, 100);
14732
14733 app.go_to_page(1);
14735 assert_eq!(app.cfn_state.table.selected, 0);
14736 }
14737
14738 #[test]
14739 fn test_cloudformation_tab_cycling_in_filter_mode() {
14740 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
14741 let mut app = test_app();
14742 app.current_service = Service::CloudFormationStacks;
14743 app.service_selected = true;
14744 app.mode = Mode::FilterInput;
14745 app.cfn_state.input_focus = InputFocus::Filter;
14746
14747 app.handle_action(Action::NextFilterFocus);
14749 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
14750
14751 app.handle_action(Action::NextFilterFocus);
14753 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
14754
14755 app.handle_action(Action::NextFilterFocus);
14757 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
14758
14759 app.handle_action(Action::NextFilterFocus);
14761 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
14762 }
14763
14764 #[test]
14765 fn test_cloudformation_timestamp_format_includes_utc() {
14766 let stack = CfnStack {
14767 name: "test-stack".to_string(),
14768 stack_id: "id-123".to_string(),
14769 status: "CREATE_COMPLETE".to_string(),
14770 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
14771 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
14772 deleted_time: String::new(),
14773 drift_status: String::new(),
14774 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
14775 status_reason: String::new(),
14776 description: String::new(),
14777 detailed_status: String::new(),
14778 root_stack: String::new(),
14779 parent_stack: String::new(),
14780 termination_protection: false,
14781 iam_role: String::new(),
14782 tags: vec![],
14783 stack_policy: String::new(),
14784 rollback_monitoring_time: String::new(),
14785 rollback_alarms: vec![],
14786 notification_arns: vec![],
14787 };
14788
14789 assert!(stack.created_time.contains("(UTC)"));
14790 assert!(stack.updated_time.contains("(UTC)"));
14791 assert!(stack.last_drift_check_time.contains("(UTC)"));
14792 assert_eq!(stack.created_time.len(), 25);
14793 }
14794
14795 #[test]
14796 fn test_cloudformation_enter_drills_into_stack_view() {
14797 let mut app = test_app();
14798 app.current_service = Service::CloudFormationStacks;
14799 app.service_selected = true;
14800 app.mode = Mode::Normal;
14801 app.cfn_state.status_filter = CfnStatusFilter::All;
14802 app.tabs = vec![Tab {
14803 service: Service::CloudFormationStacks,
14804 title: "CloudFormation › Stacks".to_string(),
14805 breadcrumb: "CloudFormation › Stacks".to_string(),
14806 }];
14807 app.current_tab = 0;
14808
14809 app.cfn_state.table.items.push(CfnStack {
14810 name: "test-stack".to_string(),
14811 stack_id: "id-123".to_string(),
14812 status: "CREATE_COMPLETE".to_string(),
14813 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14814 updated_time: String::new(),
14815 deleted_time: String::new(),
14816 drift_status: String::new(),
14817 last_drift_check_time: String::new(),
14818 status_reason: String::new(),
14819 description: String::new(),
14820 detailed_status: String::new(),
14821 root_stack: String::new(),
14822 parent_stack: String::new(),
14823 termination_protection: false,
14824 iam_role: String::new(),
14825 tags: vec![],
14826 stack_policy: String::new(),
14827 rollback_monitoring_time: String::new(),
14828 rollback_alarms: vec![],
14829 notification_arns: vec![],
14830 });
14831
14832 app.cfn_state.table.reset();
14833 assert_eq!(app.cfn_state.current_stack, None);
14834
14835 app.handle_action(Action::Select);
14837 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
14838 }
14839
14840 #[test]
14841 fn test_cloudformation_arrow_keys_expand_collapse() {
14842 let mut app = test_app();
14843 app.current_service = Service::CloudFormationStacks;
14844 app.service_selected = true;
14845 app.mode = Mode::Normal;
14846 app.cfn_state.status_filter = CfnStatusFilter::All;
14847
14848 app.cfn_state.table.items.push(CfnStack {
14849 name: "test-stack".to_string(),
14850 stack_id: "id-123".to_string(),
14851 status: "CREATE_COMPLETE".to_string(),
14852 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14853 updated_time: String::new(),
14854 deleted_time: String::new(),
14855 drift_status: String::new(),
14856 last_drift_check_time: String::new(),
14857 status_reason: String::new(),
14858 description: String::new(),
14859 detailed_status: String::new(),
14860 root_stack: String::new(),
14861 parent_stack: String::new(),
14862 termination_protection: false,
14863 iam_role: String::new(),
14864 tags: vec![],
14865 stack_policy: String::new(),
14866 rollback_monitoring_time: String::new(),
14867 rollback_alarms: vec![],
14868 notification_arns: vec![],
14869 });
14870
14871 app.cfn_state.table.reset();
14872 assert_eq!(app.cfn_state.table.expanded_item, None);
14873
14874 app.handle_action(Action::NextPane);
14876 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
14877
14878 app.handle_action(Action::PrevPane);
14880 assert_eq!(app.cfn_state.table.expanded_item, None);
14881
14882 assert_eq!(app.cfn_state.current_stack, None);
14884 }
14885
14886 #[test]
14887 fn test_cloudformation_tab_cycling() {
14888 use crate::ui::cfn::DetailTab;
14889 let mut app = test_app();
14890 app.current_service = Service::CloudFormationStacks;
14891 app.service_selected = true;
14892 app.mode = Mode::Normal;
14893 app.cfn_state.status_filter = CfnStatusFilter::All;
14894 app.cfn_state.current_stack = Some("test-stack".to_string());
14895
14896 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
14897 }
14898
14899 #[test]
14900 fn test_cloudformation_console_url() {
14901 use crate::ui::cfn::DetailTab;
14902 let mut app = test_app();
14903 app.current_service = Service::CloudFormationStacks;
14904 app.service_selected = true;
14905 app.cfn_state.status_filter = CfnStatusFilter::All;
14906
14907 app.cfn_state.table.items.push(CfnStack {
14908 name: "test-stack".to_string(),
14909 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
14910 .to_string(),
14911 status: "CREATE_COMPLETE".to_string(),
14912 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
14913 updated_time: String::new(),
14914 deleted_time: String::new(),
14915 drift_status: String::new(),
14916 last_drift_check_time: String::new(),
14917 status_reason: String::new(),
14918 description: String::new(),
14919 detailed_status: String::new(),
14920 root_stack: String::new(),
14921 parent_stack: String::new(),
14922 termination_protection: false,
14923 iam_role: String::new(),
14924 tags: vec![],
14925 stack_policy: String::new(),
14926 rollback_monitoring_time: String::new(),
14927 rollback_alarms: vec![],
14928 notification_arns: vec![],
14929 });
14930
14931 app.cfn_state.current_stack = Some("test-stack".to_string());
14932
14933 app.cfn_state.detail_tab = DetailTab::StackInfo;
14935 let url = app.get_console_url();
14936 assert!(url.contains("stackinfo"));
14937 assert!(url.contains("arn%3Aaws%3Acloudformation"));
14938
14939 app.cfn_state.detail_tab = DetailTab::Events;
14941 let url = app.get_console_url();
14942 assert!(url.contains("events"));
14943 assert!(url.contains("arn%3Aaws%3Acloudformation"));
14944 }
14945
14946 #[test]
14947 fn test_iam_role_select() {
14948 let mut app = test_app();
14949 app.current_service = Service::IamRoles;
14950 app.service_selected = true;
14951 app.mode = Mode::Normal;
14952
14953 app.iam_state.roles.items = vec![
14954 IamRole {
14955 role_name: "role1".to_string(),
14956 path: "/".to_string(),
14957 trusted_entities: "AWS Service: ec2".to_string(),
14958 last_activity: "-".to_string(),
14959 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
14960 creation_time: "2025-01-01".to_string(),
14961 description: "Test role 1".to_string(),
14962 max_session_duration: Some(3600),
14963 },
14964 IamRole {
14965 role_name: "role2".to_string(),
14966 path: "/".to_string(),
14967 trusted_entities: "AWS Service: lambda".to_string(),
14968 last_activity: "-".to_string(),
14969 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
14970 creation_time: "2025-01-02".to_string(),
14971 description: "Test role 2".to_string(),
14972 max_session_duration: Some(7200),
14973 },
14974 ];
14975
14976 app.iam_state.roles.selected = 0;
14978 app.handle_action(Action::Select);
14979
14980 assert_eq!(
14981 app.iam_state.current_role,
14982 Some("role1".to_string()),
14983 "Should open role detail view"
14984 );
14985 assert_eq!(
14986 app.iam_state.role_tab,
14987 RoleTab::Permissions,
14988 "Should default to Permissions tab"
14989 );
14990 }
14991
14992 #[test]
14993 fn test_iam_role_back_navigation() {
14994 let mut app = test_app();
14995 app.current_service = Service::IamRoles;
14996 app.service_selected = true;
14997 app.iam_state.current_role = Some("test-role".to_string());
14998
14999 app.handle_action(Action::GoBack);
15000
15001 assert_eq!(
15002 app.iam_state.current_role, None,
15003 "Should return to roles list"
15004 );
15005 }
15006
15007 #[test]
15008 fn test_iam_role_tab_navigation() {
15009 let mut app = test_app();
15010 app.current_service = Service::IamRoles;
15011 app.service_selected = true;
15012 app.iam_state.current_role = Some("test-role".to_string());
15013 app.iam_state.role_tab = RoleTab::Permissions;
15014
15015 app.handle_action(Action::NextDetailTab);
15016
15017 assert_eq!(
15018 app.iam_state.role_tab,
15019 RoleTab::TrustRelationships,
15020 "Should move to next tab"
15021 );
15022 }
15023
15024 #[test]
15025 fn test_iam_role_tab_cycle_order() {
15026 let mut app = test_app();
15027 app.current_service = Service::IamRoles;
15028 app.service_selected = true;
15029 app.iam_state.current_role = Some("test-role".to_string());
15030 app.iam_state.role_tab = RoleTab::Permissions;
15031
15032 app.handle_action(Action::NextDetailTab);
15033 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
15034
15035 app.handle_action(Action::NextDetailTab);
15036 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
15037
15038 app.handle_action(Action::NextDetailTab);
15039 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
15040
15041 app.handle_action(Action::NextDetailTab);
15042 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
15043
15044 app.handle_action(Action::NextDetailTab);
15045 assert_eq!(
15046 app.iam_state.role_tab,
15047 RoleTab::Permissions,
15048 "Should cycle back to first tab"
15049 );
15050 }
15051
15052 #[test]
15053 fn test_iam_role_pagination() {
15054 let mut app = test_app();
15055 app.current_service = Service::IamRoles;
15056 app.service_selected = true;
15057 app.iam_state.roles.page_size = PageSize::Ten;
15058
15059 app.iam_state.roles.items = (0..25)
15060 .map(|i| IamRole {
15061 role_name: format!("role{}", i),
15062 path: "/".to_string(),
15063 trusted_entities: "AWS Service: ec2".to_string(),
15064 last_activity: "-".to_string(),
15065 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
15066 creation_time: "2025-01-01".to_string(),
15067 description: format!("Test role {}", i),
15068 max_session_duration: Some(3600),
15069 })
15070 .collect();
15071
15072 app.go_to_page(2);
15074
15075 assert_eq!(
15076 app.iam_state.roles.selected, 10,
15077 "Should select first item of page 2"
15078 );
15079 assert_eq!(
15080 app.iam_state.roles.scroll_offset, 10,
15081 "Should update scroll offset"
15082 );
15083 }
15084
15085 #[test]
15086 fn test_tags_table_populated_on_role_detail() {
15087 let mut app = test_app();
15088 app.current_service = Service::IamRoles;
15089 app.service_selected = true;
15090 app.mode = Mode::Normal;
15091 app.iam_state.roles.items = vec![IamRole {
15092 role_name: "TestRole".to_string(),
15093 path: "/".to_string(),
15094 trusted_entities: String::new(),
15095 last_activity: String::new(),
15096 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
15097 creation_time: "2025-01-01".to_string(),
15098 description: String::new(),
15099 max_session_duration: Some(3600),
15100 }];
15101
15102 app.iam_state.tags.items = vec![
15104 IamRoleTag {
15105 key: "Environment".to_string(),
15106 value: "Production".to_string(),
15107 },
15108 IamRoleTag {
15109 key: "Team".to_string(),
15110 value: "Platform".to_string(),
15111 },
15112 ];
15113
15114 assert_eq!(app.iam_state.tags.items.len(), 2);
15115 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
15116 assert_eq!(app.iam_state.tags.items[0].value, "Production");
15117 assert_eq!(app.iam_state.tags.selected, 0);
15118 }
15119
15120 #[test]
15121 fn test_tags_table_navigation() {
15122 let mut app = test_app();
15123 app.current_service = Service::IamRoles;
15124 app.service_selected = true;
15125 app.mode = Mode::Normal;
15126 app.iam_state.current_role = Some("TestRole".to_string());
15127 app.iam_state.role_tab = RoleTab::Tags;
15128 app.iam_state.tags.items = vec![
15129 IamRoleTag {
15130 key: "Tag1".to_string(),
15131 value: "Value1".to_string(),
15132 },
15133 IamRoleTag {
15134 key: "Tag2".to_string(),
15135 value: "Value2".to_string(),
15136 },
15137 ];
15138
15139 app.handle_action(Action::NextItem);
15140 assert_eq!(app.iam_state.tags.selected, 1);
15141
15142 app.handle_action(Action::PrevItem);
15143 assert_eq!(app.iam_state.tags.selected, 0);
15144 }
15145
15146 #[test]
15147 fn test_last_accessed_table_navigation() {
15148 let mut app = test_app();
15149 app.current_service = Service::IamRoles;
15150 app.service_selected = true;
15151 app.mode = Mode::Normal;
15152 app.iam_state.current_role = Some("TestRole".to_string());
15153 app.iam_state.role_tab = RoleTab::LastAccessed;
15154 app.iam_state.last_accessed_services.items = vec![
15155 LastAccessedService {
15156 service: "S3".to_string(),
15157 policies_granting: "Policy1".to_string(),
15158 last_accessed: "2025-01-01".to_string(),
15159 },
15160 LastAccessedService {
15161 service: "EC2".to_string(),
15162 policies_granting: "Policy2".to_string(),
15163 last_accessed: "2025-01-02".to_string(),
15164 },
15165 ];
15166
15167 app.handle_action(Action::NextItem);
15168 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
15169
15170 app.handle_action(Action::PrevItem);
15171 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
15172 }
15173
15174 #[test]
15175 fn test_cfn_input_focus_next() {
15176 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15177 let mut app = test_app();
15178 app.current_service = Service::CloudFormationStacks;
15179 app.mode = Mode::FilterInput;
15180 app.cfn_state.input_focus = InputFocus::Filter;
15181
15182 app.handle_action(Action::NextFilterFocus);
15183 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15184
15185 app.handle_action(Action::NextFilterFocus);
15186 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15187
15188 app.handle_action(Action::NextFilterFocus);
15189 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15190
15191 app.handle_action(Action::NextFilterFocus);
15192 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15193 }
15194
15195 #[test]
15196 fn test_cfn_input_focus_prev() {
15197 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15198 let mut app = test_app();
15199 app.current_service = Service::CloudFormationStacks;
15200 app.mode = Mode::FilterInput;
15201 app.cfn_state.input_focus = InputFocus::Filter;
15202
15203 app.handle_action(Action::PrevFilterFocus);
15204 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15205
15206 app.handle_action(Action::PrevFilterFocus);
15207 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15208
15209 app.handle_action(Action::PrevFilterFocus);
15210 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15211
15212 app.handle_action(Action::PrevFilterFocus);
15213 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15214 }
15215
15216 #[test]
15217 fn test_cw_logs_input_focus_prev() {
15218 let mut app = test_app();
15219 app.current_service = Service::CloudWatchLogGroups;
15220 app.mode = Mode::FilterInput;
15221 app.view_mode = ViewMode::Detail;
15222 app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
15223 app.log_groups_state.input_focus = InputFocus::Filter;
15224
15225 app.handle_action(Action::PrevFilterFocus);
15226 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
15227
15228 app.handle_action(Action::PrevFilterFocus);
15229 assert_eq!(
15230 app.log_groups_state.input_focus,
15231 InputFocus::Checkbox("ShowExpired")
15232 );
15233
15234 app.handle_action(Action::PrevFilterFocus);
15235 assert_eq!(
15236 app.log_groups_state.input_focus,
15237 InputFocus::Checkbox("ExactMatch")
15238 );
15239
15240 app.handle_action(Action::PrevFilterFocus);
15241 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
15242 }
15243
15244 #[test]
15245 fn test_cw_events_input_focus_prev() {
15246 use crate::ui::cw::logs::EventFilterFocus;
15247 let mut app = test_app();
15248 app.mode = Mode::EventFilterInput;
15249 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
15250
15251 app.handle_action(Action::PrevFilterFocus);
15252 assert_eq!(
15253 app.log_groups_state.event_input_focus,
15254 EventFilterFocus::DateRange
15255 );
15256
15257 app.handle_action(Action::PrevFilterFocus);
15258 assert_eq!(
15259 app.log_groups_state.event_input_focus,
15260 EventFilterFocus::Filter
15261 );
15262 }
15263
15264 #[test]
15265 fn test_cfn_input_focus_cycle_complete() {
15266 let mut app = test_app();
15267 app.current_service = Service::CloudFormationStacks;
15268 app.mode = Mode::FilterInput;
15269 app.cfn_state.input_focus = InputFocus::Filter;
15270
15271 for _ in 0..4 {
15273 app.handle_action(Action::NextFilterFocus);
15274 }
15275 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15276
15277 for _ in 0..4 {
15279 app.handle_action(Action::PrevFilterFocus);
15280 }
15281 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15282 }
15283
15284 #[test]
15285 fn test_cfn_resources_pagination_left_right_arrows() {
15286 let mut app = test_app();
15290 app.current_service = Service::CloudFormationStacks;
15291 app.service_selected = true;
15292 app.mode = Mode::FilterInput;
15293 app.cfn_state.current_stack = Some("test-stack".to_string());
15294 app.cfn_state.detail_tab = CfnDetailTab::Resources;
15295 app.cfn_state.resources_input_focus = InputFocus::Pagination;
15296
15297 app.cfn_state.resources.items = (0..60)
15299 .map(|i| rusticity_core::cfn::StackResource {
15300 logical_id: format!("Resource{}", i),
15301 physical_id: format!("phys{}", i),
15302 resource_type: "AWS::S3::Bucket".to_string(),
15303 status: String::new(),
15304 module_info: String::new(),
15305 })
15306 .collect();
15307 app.cfn_state.resources.page_size = crate::common::PageSize::Ten;
15308 app.cfn_state.resources.selected = 0;
15309
15310 app.handle_action(Action::PageDown);
15312 assert_eq!(
15313 app.cfn_state.resources.selected, 10,
15314 "Right arrow with pagination focus must move to page 2 of Resources"
15315 );
15316
15317 app.handle_action(Action::PageUp);
15319 assert_eq!(
15320 app.cfn_state.resources.selected, 0,
15321 "Left arrow with pagination focus must move back to page 1 of Resources"
15322 );
15323 }
15324
15325 #[test]
15326 fn test_cfn_filter_status_arrow_keys() {
15327 use crate::ui::cfn::STATUS_FILTER;
15328 let mut app = test_app();
15329 app.current_service = Service::CloudFormationStacks;
15330 app.mode = Mode::FilterInput;
15331 app.cfn_state.input_focus = STATUS_FILTER;
15332 app.cfn_state.status_filter = CfnStatusFilter::All;
15333
15334 app.handle_action(Action::NextItem);
15335 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
15336
15337 app.handle_action(Action::PrevItem);
15338 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
15339 }
15340
15341 #[test]
15342 fn test_cfn_filter_shift_tab_cycles_backward() {
15343 use crate::ui::cfn::STATUS_FILTER;
15344 let mut app = test_app();
15345 app.current_service = Service::CloudFormationStacks;
15346 app.mode = Mode::FilterInput;
15347 app.cfn_state.input_focus = STATUS_FILTER;
15348
15349 app.handle_action(Action::PrevFilterFocus);
15351 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15352
15353 app.handle_action(Action::PrevFilterFocus);
15355 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15356 }
15357
15358 #[test]
15359 fn test_cfn_pagination_arrow_keys() {
15360 let mut app = test_app();
15361 app.current_service = Service::CloudFormationStacks;
15362 app.mode = Mode::FilterInput;
15363 app.cfn_state.input_focus = InputFocus::Pagination;
15364 app.cfn_state.table.scroll_offset = 0;
15365 app.cfn_state.table.page_size = PageSize::Ten;
15366
15367 app.cfn_state.table.items = (0..30)
15369 .map(|i| CfnStack {
15370 name: format!("stack-{}", i),
15371 stack_id: format!("id-{}", i),
15372 status: "CREATE_COMPLETE".to_string(),
15373 created_time: "2024-01-01".to_string(),
15374 updated_time: String::new(),
15375 deleted_time: String::new(),
15376 drift_status: String::new(),
15377 last_drift_check_time: String::new(),
15378 status_reason: String::new(),
15379 description: String::new(),
15380 detailed_status: String::new(),
15381 root_stack: String::new(),
15382 parent_stack: String::new(),
15383 termination_protection: false,
15384 iam_role: String::new(),
15385 tags: Vec::new(),
15386 stack_policy: String::new(),
15387 rollback_monitoring_time: String::new(),
15388 rollback_alarms: Vec::new(),
15389 notification_arns: Vec::new(),
15390 })
15391 .collect();
15392
15393 app.handle_action(Action::PageDown);
15395 assert_eq!(app.cfn_state.table.scroll_offset, 10);
15396 let page_size = app.cfn_state.table.page_size.value();
15398 let current_page = app.cfn_state.table.scroll_offset / page_size;
15399 assert_eq!(current_page, 1);
15400
15401 app.handle_action(Action::PageUp);
15403 assert_eq!(app.cfn_state.table.scroll_offset, 0);
15404 let current_page = app.cfn_state.table.scroll_offset / page_size;
15405 assert_eq!(current_page, 0);
15406 }
15407
15408 #[test]
15409 fn test_cfn_page_navigation_updates_selection() {
15410 let mut app = test_app();
15411 app.current_service = Service::CloudFormationStacks;
15412 app.mode = Mode::Normal;
15413
15414 app.cfn_state.table.items = (0..30)
15416 .map(|i| CfnStack {
15417 name: format!("stack-{}", i),
15418 stack_id: format!("id-{}", i),
15419 status: "CREATE_COMPLETE".to_string(),
15420 created_time: "2024-01-01".to_string(),
15421 updated_time: String::new(),
15422 deleted_time: String::new(),
15423 drift_status: String::new(),
15424 last_drift_check_time: String::new(),
15425 status_reason: String::new(),
15426 description: String::new(),
15427 detailed_status: String::new(),
15428 root_stack: String::new(),
15429 parent_stack: String::new(),
15430 termination_protection: false,
15431 iam_role: String::new(),
15432 tags: Vec::new(),
15433 stack_policy: String::new(),
15434 rollback_monitoring_time: String::new(),
15435 rollback_alarms: Vec::new(),
15436 notification_arns: Vec::new(),
15437 })
15438 .collect();
15439
15440 app.cfn_state.table.reset();
15441 app.cfn_state.table.scroll_offset = 0;
15442
15443 app.handle_action(Action::PageDown);
15445 assert_eq!(app.cfn_state.table.selected, 10);
15446
15447 app.handle_action(Action::PageDown);
15449 assert_eq!(app.cfn_state.table.selected, 20);
15450
15451 app.handle_action(Action::PageUp);
15453 assert_eq!(app.cfn_state.table.selected, 10);
15454 }
15455
15456 #[test]
15457 fn test_cfn_filter_input_only_when_focused() {
15458 use crate::ui::cfn::STATUS_FILTER;
15459 let mut app = test_app();
15460 app.current_service = Service::CloudFormationStacks;
15461 app.mode = Mode::FilterInput;
15462 app.cfn_state.input_focus = STATUS_FILTER;
15463 app.cfn_state.table.filter = String::new();
15464
15465 app.handle_action(Action::FilterInput('t'));
15467 app.handle_action(Action::FilterInput('e'));
15468 app.handle_action(Action::FilterInput('s'));
15469 app.handle_action(Action::FilterInput('t'));
15470 assert_eq!(app.cfn_state.table.filter, "");
15471
15472 app.cfn_state.input_focus = InputFocus::Filter;
15474 app.handle_action(Action::FilterInput('t'));
15475 app.handle_action(Action::FilterInput('e'));
15476 app.handle_action(Action::FilterInput('s'));
15477 app.handle_action(Action::FilterInput('t'));
15478 assert_eq!(app.cfn_state.table.filter, "test");
15479 }
15480
15481 #[test]
15482 fn test_cfn_input_focus_resets_on_start() {
15483 let mut app = test_app();
15484 app.current_service = Service::CloudFormationStacks;
15485 app.service_selected = true;
15486 app.mode = Mode::Normal;
15487 app.cfn_state.input_focus = InputFocus::Pagination;
15488
15489 app.handle_action(Action::StartFilter);
15491 assert_eq!(app.mode, Mode::FilterInput);
15492 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15493 }
15494
15495 #[test]
15496 fn test_iam_roles_input_focus_cycles_forward() {
15497 let mut app = test_app();
15498 app.current_service = Service::IamRoles;
15499 app.mode = Mode::FilterInput;
15500 app.iam_state.role_input_focus = InputFocus::Filter;
15501
15502 app.handle_action(Action::NextFilterFocus);
15503 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
15504
15505 app.handle_action(Action::NextFilterFocus);
15506 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
15507 }
15508
15509 #[test]
15510 fn test_iam_roles_input_focus_cycles_backward() {
15511 let mut app = test_app();
15512 app.current_service = Service::IamRoles;
15513 app.mode = Mode::FilterInput;
15514 app.iam_state.role_input_focus = InputFocus::Filter;
15515
15516 app.handle_action(Action::PrevFilterFocus);
15517 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
15518
15519 app.handle_action(Action::PrevFilterFocus);
15520 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
15521 }
15522
15523 #[test]
15524 fn test_iam_roles_filter_input_only_when_focused() {
15525 let mut app = test_app();
15526 app.current_service = Service::IamRoles;
15527 app.mode = Mode::FilterInput;
15528 app.iam_state.role_input_focus = InputFocus::Pagination;
15529 app.iam_state.roles.filter = String::new();
15530
15531 app.handle_action(Action::FilterInput('t'));
15533 app.handle_action(Action::FilterInput('e'));
15534 app.handle_action(Action::FilterInput('s'));
15535 app.handle_action(Action::FilterInput('t'));
15536 assert_eq!(app.iam_state.roles.filter, "");
15537
15538 app.iam_state.role_input_focus = InputFocus::Filter;
15540 app.handle_action(Action::FilterInput('t'));
15541 app.handle_action(Action::FilterInput('e'));
15542 app.handle_action(Action::FilterInput('s'));
15543 app.handle_action(Action::FilterInput('t'));
15544 assert_eq!(app.iam_state.roles.filter, "test");
15545 }
15546
15547 #[test]
15548 fn test_iam_roles_page_down_updates_scroll_offset() {
15549 let mut app = test_app();
15550 app.current_service = Service::IamRoles;
15551 app.mode = Mode::Normal;
15552 app.iam_state.roles.items = (0..50)
15553 .map(|i| IamRole {
15554 role_name: format!("role-{}", i),
15555 path: "/".to_string(),
15556 trusted_entities: "AWS Service".to_string(),
15557 last_activity: "N/A".to_string(),
15558 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
15559 creation_time: "2024-01-01".to_string(),
15560 description: String::new(),
15561 max_session_duration: Some(3600),
15562 })
15563 .collect();
15564
15565 app.iam_state.roles.selected = 0;
15566 app.iam_state.roles.scroll_offset = 0;
15567
15568 app.handle_action(Action::PageDown);
15570 assert_eq!(app.iam_state.roles.selected, 10);
15571 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
15573
15574 app.handle_action(Action::PageDown);
15576 assert_eq!(app.iam_state.roles.selected, 20);
15577 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
15578 }
15579
15580 #[test]
15581 fn test_application_selection_and_deployments_tab() {
15582 use crate::lambda::Application as LambdaApplication;
15583 use LambdaApplicationDetailTab;
15584
15585 let mut app = test_app();
15586 app.current_service = Service::LambdaApplications;
15587 app.service_selected = true;
15588 app.mode = Mode::Normal;
15589
15590 app.lambda_application_state.table.items = vec![LambdaApplication {
15591 name: "test-app".to_string(),
15592 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15593 description: "Test application".to_string(),
15594 status: "CREATE_COMPLETE".to_string(),
15595 last_modified: "2024-01-01".to_string(),
15596 }];
15597
15598 app.handle_action(Action::Select);
15600 assert_eq!(
15601 app.lambda_application_state.current_application,
15602 Some("test-app".to_string())
15603 );
15604 assert_eq!(
15605 app.lambda_application_state.detail_tab,
15606 LambdaApplicationDetailTab::Overview
15607 );
15608
15609 app.handle_action(Action::NextDetailTab);
15611 assert_eq!(
15612 app.lambda_application_state.detail_tab,
15613 LambdaApplicationDetailTab::Deployments
15614 );
15615
15616 app.handle_action(Action::GoBack);
15618 assert_eq!(app.lambda_application_state.current_application, None);
15619 }
15620
15621 #[test]
15622 fn test_application_resources_filter_and_pagination() {
15623 use crate::lambda::Application as LambdaApplication;
15624 use LambdaApplicationDetailTab;
15625
15626 let mut app = test_app();
15627 app.current_service = Service::LambdaApplications;
15628 app.service_selected = true;
15629 app.mode = Mode::Normal;
15630
15631 app.lambda_application_state.table.items = vec![LambdaApplication {
15632 name: "test-app".to_string(),
15633 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15634 description: "Test application".to_string(),
15635 status: "CREATE_COMPLETE".to_string(),
15636 last_modified: "2024-01-01".to_string(),
15637 }];
15638
15639 app.handle_action(Action::Select);
15641 assert_eq!(
15642 app.lambda_application_state.detail_tab,
15643 LambdaApplicationDetailTab::Overview
15644 );
15645
15646 assert!(!app.lambda_application_state.resources.items.is_empty());
15648
15649 app.mode = Mode::FilterInput;
15651 assert_eq!(
15652 app.lambda_application_state.resource_input_focus,
15653 InputFocus::Filter
15654 );
15655
15656 app.handle_action(Action::NextFilterFocus);
15657 assert_eq!(
15658 app.lambda_application_state.resource_input_focus,
15659 InputFocus::Pagination
15660 );
15661
15662 app.handle_action(Action::PrevFilterFocus);
15663 assert_eq!(
15664 app.lambda_application_state.resource_input_focus,
15665 InputFocus::Filter
15666 );
15667 }
15668
15669 #[test]
15670 fn test_application_deployments_filter_and_pagination() {
15671 use crate::lambda::Application as LambdaApplication;
15672 use LambdaApplicationDetailTab;
15673
15674 let mut app = test_app();
15675 app.current_service = Service::LambdaApplications;
15676 app.service_selected = true;
15677 app.mode = Mode::Normal;
15678
15679 app.lambda_application_state.table.items = vec![LambdaApplication {
15680 name: "test-app".to_string(),
15681 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15682 description: "Test application".to_string(),
15683 status: "CREATE_COMPLETE".to_string(),
15684 last_modified: "2024-01-01".to_string(),
15685 }];
15686
15687 app.handle_action(Action::Select);
15689 app.handle_action(Action::NextDetailTab);
15690 assert_eq!(
15691 app.lambda_application_state.detail_tab,
15692 LambdaApplicationDetailTab::Deployments
15693 );
15694
15695 assert!(!app.lambda_application_state.deployments.items.is_empty());
15697
15698 app.mode = Mode::FilterInput;
15700 assert_eq!(
15701 app.lambda_application_state.deployment_input_focus,
15702 InputFocus::Filter
15703 );
15704
15705 app.handle_action(Action::NextFilterFocus);
15706 assert_eq!(
15707 app.lambda_application_state.deployment_input_focus,
15708 InputFocus::Pagination
15709 );
15710
15711 app.handle_action(Action::PrevFilterFocus);
15712 assert_eq!(
15713 app.lambda_application_state.deployment_input_focus,
15714 InputFocus::Filter
15715 );
15716 }
15717
15718 #[test]
15719 fn test_application_resource_expansion() {
15720 use crate::lambda::Application as LambdaApplication;
15721 use LambdaApplicationDetailTab;
15722
15723 let mut app = test_app();
15724 app.current_service = Service::LambdaApplications;
15725 app.service_selected = true;
15726 app.mode = Mode::Normal;
15727
15728 app.lambda_application_state.table.items = vec![LambdaApplication {
15729 name: "test-app".to_string(),
15730 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15731 description: "Test application".to_string(),
15732 status: "CREATE_COMPLETE".to_string(),
15733 last_modified: "2024-01-01".to_string(),
15734 }];
15735
15736 app.handle_action(Action::Select);
15738 assert_eq!(
15739 app.lambda_application_state.detail_tab,
15740 LambdaApplicationDetailTab::Overview
15741 );
15742
15743 app.handle_action(Action::NextPane);
15745 assert_eq!(
15746 app.lambda_application_state.resources.expanded_item,
15747 Some(0)
15748 );
15749
15750 app.handle_action(Action::PrevPane);
15752 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
15753 }
15754
15755 #[test]
15756 fn test_application_deployment_expansion() {
15757 use crate::lambda::Application as LambdaApplication;
15758 use LambdaApplicationDetailTab;
15759
15760 let mut app = test_app();
15761 app.current_service = Service::LambdaApplications;
15762 app.service_selected = true;
15763 app.mode = Mode::Normal;
15764
15765 app.lambda_application_state.table.items = vec![LambdaApplication {
15766 name: "test-app".to_string(),
15767 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
15768 description: "Test application".to_string(),
15769 status: "CREATE_COMPLETE".to_string(),
15770 last_modified: "2024-01-01".to_string(),
15771 }];
15772
15773 app.handle_action(Action::Select);
15775 app.handle_action(Action::NextDetailTab);
15776 assert_eq!(
15777 app.lambda_application_state.detail_tab,
15778 LambdaApplicationDetailTab::Deployments
15779 );
15780
15781 app.handle_action(Action::NextPane);
15783 assert_eq!(
15784 app.lambda_application_state.deployments.expanded_item,
15785 Some(0)
15786 );
15787
15788 app.handle_action(Action::PrevPane);
15790 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
15791 }
15792
15793 #[test]
15794 fn test_s3_nested_prefix_expansion() {
15795 use crate::s3::Bucket;
15796 use S3Object;
15797
15798 let mut app = test_app();
15799 app.current_service = Service::S3Buckets;
15800 app.service_selected = true;
15801 app.mode = Mode::Normal;
15802
15803 app.s3_state.buckets.items = vec![Bucket {
15805 name: "test-bucket".to_string(),
15806 region: "us-east-1".to_string(),
15807 creation_date: "2024-01-01".to_string(),
15808 }];
15809
15810 app.s3_state.bucket_preview.insert(
15812 "test-bucket".to_string(),
15813 vec![S3Object {
15814 key: "level1/".to_string(),
15815 size: 0,
15816 last_modified: "".to_string(),
15817 is_prefix: true,
15818 storage_class: "".to_string(),
15819 }],
15820 );
15821
15822 app.s3_state.prefix_preview.insert(
15824 "level1/".to_string(),
15825 vec![S3Object {
15826 key: "level1/level2/".to_string(),
15827 size: 0,
15828 last_modified: "".to_string(),
15829 is_prefix: true,
15830 storage_class: "".to_string(),
15831 }],
15832 );
15833
15834 app.s3_state.selected_row = 0;
15836 app.handle_action(Action::NextPane);
15837 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
15838
15839 app.s3_state.selected_row = 1;
15841 app.handle_action(Action::NextPane);
15842 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
15843
15844 app.s3_state.selected_row = 2;
15846 app.handle_action(Action::NextPane);
15847 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
15848
15849 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
15851 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
15852 }
15853
15854 #[test]
15855 fn test_s3_nested_prefix_collapse() {
15856 use crate::s3::Bucket;
15857 use S3Object;
15858
15859 let mut app = test_app();
15860 app.current_service = Service::S3Buckets;
15861 app.service_selected = true;
15862 app.mode = Mode::Normal;
15863
15864 app.s3_state.buckets.items = vec![Bucket {
15865 name: "test-bucket".to_string(),
15866 region: "us-east-1".to_string(),
15867 creation_date: "2024-01-01".to_string(),
15868 }];
15869
15870 app.s3_state.bucket_preview.insert(
15871 "test-bucket".to_string(),
15872 vec![S3Object {
15873 key: "level1/".to_string(),
15874 size: 0,
15875 last_modified: "".to_string(),
15876 is_prefix: true,
15877 storage_class: "".to_string(),
15878 }],
15879 );
15880
15881 app.s3_state.prefix_preview.insert(
15882 "level1/".to_string(),
15883 vec![S3Object {
15884 key: "level1/level2/".to_string(),
15885 size: 0,
15886 last_modified: "".to_string(),
15887 is_prefix: true,
15888 storage_class: "".to_string(),
15889 }],
15890 );
15891
15892 app.s3_state
15894 .expanded_prefixes
15895 .insert("test-bucket".to_string());
15896 app.s3_state.expanded_prefixes.insert("level1/".to_string());
15897 app.s3_state
15898 .expanded_prefixes
15899 .insert("level1/level2/".to_string());
15900
15901 app.s3_state.selected_row = 2;
15903 app.handle_action(Action::PrevPane);
15904 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
15905 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
15909 app.handle_action(Action::PrevPane);
15910 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
15911 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
15915 app.handle_action(Action::PrevPane);
15916 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
15917 }
15918}
15919
15920#[cfg(test)]
15921mod sqs_tests {
15922 use super::*;
15923 use test_helpers::*;
15924
15925 #[test]
15926 fn test_sqs_filter_input() {
15927 let mut app = test_app();
15928 app.current_service = Service::SqsQueues;
15929 app.service_selected = true;
15930 app.mode = Mode::FilterInput;
15931
15932 app.handle_action(Action::FilterInput('t'));
15933 app.handle_action(Action::FilterInput('e'));
15934 app.handle_action(Action::FilterInput('s'));
15935 app.handle_action(Action::FilterInput('t'));
15936 assert_eq!(app.sqs_state.queues.filter, "test");
15937
15938 app.handle_action(Action::FilterBackspace);
15939 assert_eq!(app.sqs_state.queues.filter, "tes");
15940 }
15941
15942 #[test]
15943 fn test_sqs_start_filter() {
15944 let mut app = test_app();
15945 app.current_service = Service::SqsQueues;
15946 app.service_selected = true;
15947 app.mode = Mode::Normal;
15948
15949 app.handle_action(Action::StartFilter);
15950 assert_eq!(app.mode, Mode::FilterInput);
15951 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
15952 }
15953
15954 #[test]
15955 fn test_sqs_filter_focus_cycling() {
15956 let mut app = test_app();
15957 app.current_service = Service::SqsQueues;
15958 app.service_selected = true;
15959 app.mode = Mode::FilterInput;
15960 app.sqs_state.input_focus = InputFocus::Filter;
15961
15962 app.handle_action(Action::NextFilterFocus);
15963 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
15964
15965 app.handle_action(Action::NextFilterFocus);
15966 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
15967
15968 app.handle_action(Action::PrevFilterFocus);
15969 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
15970 }
15971
15972 #[test]
15973 fn test_sqs_navigation() {
15974 let mut app = test_app();
15975 app.current_service = Service::SqsQueues;
15976 app.service_selected = true;
15977 app.mode = Mode::Normal;
15978 app.sqs_state.queues.items = (0..10)
15979 .map(|i| SqsQueue {
15980 name: format!("queue{}", i),
15981 url: String::new(),
15982 queue_type: "Standard".to_string(),
15983 created_timestamp: String::new(),
15984 messages_available: "0".to_string(),
15985 messages_in_flight: "0".to_string(),
15986 encryption: "Disabled".to_string(),
15987 content_based_deduplication: "Disabled".to_string(),
15988 last_modified_timestamp: String::new(),
15989 visibility_timeout: String::new(),
15990 message_retention_period: String::new(),
15991 maximum_message_size: String::new(),
15992 delivery_delay: String::new(),
15993 receive_message_wait_time: String::new(),
15994 high_throughput_fifo: "N/A".to_string(),
15995 deduplication_scope: "N/A".to_string(),
15996 fifo_throughput_limit: "N/A".to_string(),
15997 dead_letter_queue: "-".to_string(),
15998 messages_delayed: "0".to_string(),
15999 redrive_allow_policy: "-".to_string(),
16000 redrive_policy: "".to_string(),
16001 redrive_task_id: "-".to_string(),
16002 redrive_task_start_time: "-".to_string(),
16003 redrive_task_status: "-".to_string(),
16004 redrive_task_percent: "-".to_string(),
16005 redrive_task_destination: "-".to_string(),
16006 })
16007 .collect();
16008
16009 app.handle_action(Action::NextItem);
16010 assert_eq!(app.sqs_state.queues.selected, 1);
16011
16012 app.handle_action(Action::PrevItem);
16013 assert_eq!(app.sqs_state.queues.selected, 0);
16014 }
16015
16016 #[test]
16017 fn test_sqs_page_navigation() {
16018 let mut app = test_app();
16019 app.current_service = Service::SqsQueues;
16020 app.service_selected = true;
16021 app.mode = Mode::Normal;
16022 app.sqs_state.queues.items = (0..100)
16023 .map(|i| SqsQueue {
16024 name: format!("queue{}", i),
16025 url: String::new(),
16026 queue_type: "Standard".to_string(),
16027 created_timestamp: String::new(),
16028 messages_available: "0".to_string(),
16029 messages_in_flight: "0".to_string(),
16030 encryption: "Disabled".to_string(),
16031 content_based_deduplication: "Disabled".to_string(),
16032 last_modified_timestamp: String::new(),
16033 visibility_timeout: String::new(),
16034 message_retention_period: String::new(),
16035 maximum_message_size: String::new(),
16036 delivery_delay: String::new(),
16037 receive_message_wait_time: String::new(),
16038 high_throughput_fifo: "N/A".to_string(),
16039 deduplication_scope: "N/A".to_string(),
16040 fifo_throughput_limit: "N/A".to_string(),
16041 dead_letter_queue: "-".to_string(),
16042 messages_delayed: "0".to_string(),
16043 redrive_allow_policy: "-".to_string(),
16044 redrive_policy: "".to_string(),
16045 redrive_task_id: "-".to_string(),
16046 redrive_task_start_time: "-".to_string(),
16047 redrive_task_status: "-".to_string(),
16048 redrive_task_percent: "-".to_string(),
16049 redrive_task_destination: "-".to_string(),
16050 })
16051 .collect();
16052
16053 app.handle_action(Action::PageDown);
16054 assert_eq!(app.sqs_state.queues.selected, 10);
16055
16056 app.handle_action(Action::PageUp);
16057 assert_eq!(app.sqs_state.queues.selected, 0);
16058 }
16059
16060 #[test]
16061 fn test_sqs_queue_expansion() {
16062 let mut app = test_app();
16063 app.current_service = Service::SqsQueues;
16064 app.service_selected = true;
16065 app.sqs_state.queues.items = vec![SqsQueue {
16066 name: "my-queue".to_string(),
16067 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
16068 queue_type: "Standard".to_string(),
16069 created_timestamp: "2023-01-01".to_string(),
16070 messages_available: "5".to_string(),
16071 messages_in_flight: "2".to_string(),
16072 encryption: "Enabled".to_string(),
16073 content_based_deduplication: "Disabled".to_string(),
16074 last_modified_timestamp: "2023-01-02".to_string(),
16075 visibility_timeout: "30".to_string(),
16076 message_retention_period: "345600".to_string(),
16077 maximum_message_size: "262144".to_string(),
16078 delivery_delay: "0".to_string(),
16079 receive_message_wait_time: "0".to_string(),
16080 high_throughput_fifo: "N/A".to_string(),
16081 deduplication_scope: "N/A".to_string(),
16082 fifo_throughput_limit: "N/A".to_string(),
16083 dead_letter_queue: "-".to_string(),
16084 messages_delayed: "0".to_string(),
16085 redrive_allow_policy: "-".to_string(),
16086 redrive_policy: "".to_string(),
16087 redrive_task_id: "-".to_string(),
16088 redrive_task_start_time: "-".to_string(),
16089 redrive_task_status: "-".to_string(),
16090 redrive_task_percent: "-".to_string(),
16091 redrive_task_destination: "-".to_string(),
16092 }];
16093 app.sqs_state.queues.selected = 0;
16094
16095 assert_eq!(app.sqs_state.queues.expanded_item, None);
16096
16097 app.handle_action(Action::NextPane);
16099 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
16100
16101 app.handle_action(Action::NextPane);
16103 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
16104
16105 app.handle_action(Action::PrevPane);
16107 assert_eq!(app.sqs_state.queues.expanded_item, None);
16108
16109 app.handle_action(Action::PrevPane);
16111 assert_eq!(app.sqs_state.queues.expanded_item, None);
16112 }
16113
16114 #[test]
16115 fn test_sqs_column_toggle() {
16116 use crate::sqs::queue::Column as SqsColumn;
16117 let mut app = test_app();
16118 app.current_service = Service::SqsQueues;
16119 app.service_selected = true;
16120 app.mode = Mode::ColumnSelector;
16121
16122 app.sqs_visible_column_ids = SqsColumn::ids();
16124 let initial_count = app.sqs_visible_column_ids.len();
16125
16126 app.column_selector_index = 0;
16128 app.handle_action(Action::ToggleColumn);
16129
16130 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
16132 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
16133
16134 app.handle_action(Action::ToggleColumn);
16136 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
16137 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
16138 }
16139
16140 #[test]
16141 fn test_sqs_column_selector_navigation() {
16142 let mut app = test_app();
16143 app.current_service = Service::SqsQueues;
16144 app.service_selected = true;
16145 app.mode = Mode::ColumnSelector;
16146 app.column_selector_index = 0;
16147
16148 let max_index = app.sqs_column_ids.len() - 1;
16150
16151 for _ in 0..max_index {
16153 app.handle_action(Action::NextItem);
16154 }
16155 assert_eq!(app.column_selector_index, max_index);
16156
16157 for _ in 0..max_index {
16159 app.handle_action(Action::PrevItem);
16160 }
16161 assert_eq!(app.column_selector_index, 0);
16162 }
16163
16164 #[test]
16165 fn test_sqs_queue_selection() {
16166 let mut app = test_app();
16167 app.current_service = Service::SqsQueues;
16168 app.service_selected = true;
16169 app.mode = Mode::Normal;
16170 app.sqs_state.queues.items = vec![SqsQueue {
16171 name: "my-queue".to_string(),
16172 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
16173 queue_type: "Standard".to_string(),
16174 created_timestamp: "2023-01-01".to_string(),
16175 messages_available: "5".to_string(),
16176 messages_in_flight: "2".to_string(),
16177 encryption: "Enabled".to_string(),
16178 content_based_deduplication: "Disabled".to_string(),
16179 last_modified_timestamp: "2023-01-02".to_string(),
16180 visibility_timeout: "30".to_string(),
16181 message_retention_period: "345600".to_string(),
16182 maximum_message_size: "262144".to_string(),
16183 delivery_delay: "0".to_string(),
16184 receive_message_wait_time: "0".to_string(),
16185 high_throughput_fifo: "N/A".to_string(),
16186 deduplication_scope: "N/A".to_string(),
16187 fifo_throughput_limit: "N/A".to_string(),
16188 dead_letter_queue: "-".to_string(),
16189 messages_delayed: "0".to_string(),
16190 redrive_allow_policy: "-".to_string(),
16191 redrive_policy: "".to_string(),
16192 redrive_task_id: "-".to_string(),
16193 redrive_task_start_time: "-".to_string(),
16194 redrive_task_status: "-".to_string(),
16195 redrive_task_percent: "-".to_string(),
16196 redrive_task_destination: "-".to_string(),
16197 }];
16198 app.sqs_state.queues.selected = 0;
16199
16200 assert_eq!(app.sqs_state.current_queue, None);
16201
16202 app.handle_action(Action::Select);
16204 assert_eq!(
16205 app.sqs_state.current_queue,
16206 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
16207 );
16208
16209 app.handle_action(Action::GoBack);
16211 assert_eq!(app.sqs_state.current_queue, None);
16212 }
16213
16214 #[test]
16215 fn test_sqs_lambda_triggers_expand_collapse() {
16216 let mut app = test_app();
16217 app.current_service = Service::SqsQueues;
16218 app.service_selected = true;
16219 app.sqs_state.current_queue =
16220 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16221 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16222 app.sqs_state.triggers.items = vec![LambdaTrigger {
16223 uuid: "test-uuid".to_string(),
16224 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
16225 status: "Enabled".to_string(),
16226 last_modified: "2024-01-01T00:00:00Z".to_string(),
16227 }];
16228 app.sqs_state.triggers.selected = 0;
16229
16230 assert_eq!(app.sqs_state.triggers.expanded_item, None);
16231
16232 app.handle_action(Action::NextPane);
16234 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16235
16236 app.handle_action(Action::PrevPane);
16238 assert_eq!(app.sqs_state.triggers.expanded_item, None);
16239 }
16240
16241 #[test]
16242 fn test_sqs_lambda_triggers_expand_toggle() {
16243 let mut app = test_app();
16244 app.current_service = Service::SqsQueues;
16245 app.service_selected = true;
16246 app.sqs_state.current_queue =
16247 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16248 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16249 app.sqs_state.triggers.items = vec![LambdaTrigger {
16250 uuid: "test-uuid".to_string(),
16251 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
16252 status: "Enabled".to_string(),
16253 last_modified: "2024-01-01T00:00:00Z".to_string(),
16254 }];
16255 app.sqs_state.triggers.selected = 0;
16256
16257 app.handle_action(Action::NextPane);
16259 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16260
16261 app.handle_action(Action::NextPane);
16263 assert_eq!(app.sqs_state.triggers.expanded_item, None);
16264
16265 app.handle_action(Action::NextPane);
16267 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16268 }
16269
16270 #[test]
16271 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
16272 use crate::ui::sqs::filtered_lambda_triggers;
16273
16274 let mut app = test_app();
16275 app.current_service = Service::SqsQueues;
16276 app.service_selected = true;
16277 app.sqs_state.current_queue =
16278 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16279 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16280 app.sqs_state.triggers.items = vec![
16281 LambdaTrigger {
16282 uuid: "uuid-3".to_string(),
16283 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
16284 status: "Enabled".to_string(),
16285 last_modified: "2024-03-01T00:00:00Z".to_string(),
16286 },
16287 LambdaTrigger {
16288 uuid: "uuid-1".to_string(),
16289 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
16290 status: "Enabled".to_string(),
16291 last_modified: "2024-01-01T00:00:00Z".to_string(),
16292 },
16293 LambdaTrigger {
16294 uuid: "uuid-2".to_string(),
16295 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
16296 status: "Enabled".to_string(),
16297 last_modified: "2024-02-01T00:00:00Z".to_string(),
16298 },
16299 ];
16300
16301 let sorted = filtered_lambda_triggers(&app);
16302
16303 assert_eq!(sorted.len(), 3);
16305 assert_eq!(sorted[0].uuid, "uuid-1");
16306 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
16307 assert_eq!(sorted[1].uuid, "uuid-2");
16308 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
16309 assert_eq!(sorted[2].uuid, "uuid-3");
16310 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
16311 }
16312
16313 #[test]
16314 fn test_sqs_lambda_triggers_filter_input() {
16315 let mut app = test_app();
16316 app.current_service = Service::SqsQueues;
16317 app.service_selected = true;
16318 app.mode = Mode::FilterInput;
16319 app.sqs_state.current_queue =
16320 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16321 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16322 app.sqs_state.input_focus = InputFocus::Filter;
16323
16324 assert_eq!(app.sqs_state.triggers.filter, "");
16325
16326 app.handle_action(Action::FilterInput('t'));
16328 assert_eq!(app.sqs_state.triggers.filter, "t");
16329
16330 app.handle_action(Action::FilterInput('e'));
16331 assert_eq!(app.sqs_state.triggers.filter, "te");
16332
16333 app.handle_action(Action::FilterInput('s'));
16334 assert_eq!(app.sqs_state.triggers.filter, "tes");
16335
16336 app.handle_action(Action::FilterInput('t'));
16337 assert_eq!(app.sqs_state.triggers.filter, "test");
16338
16339 app.handle_action(Action::FilterBackspace);
16341 assert_eq!(app.sqs_state.triggers.filter, "tes");
16342 }
16343
16344 #[test]
16345 fn test_sqs_lambda_triggers_filter_applied() {
16346 use crate::ui::sqs::filtered_lambda_triggers;
16347
16348 let mut app = test_app();
16349 app.current_service = Service::SqsQueues;
16350 app.service_selected = true;
16351 app.sqs_state.current_queue =
16352 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16353 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16354 app.sqs_state.triggers.items = vec![
16355 LambdaTrigger {
16356 uuid: "uuid-1".to_string(),
16357 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
16358 status: "Enabled".to_string(),
16359 last_modified: "2024-01-01T00:00:00Z".to_string(),
16360 },
16361 LambdaTrigger {
16362 uuid: "uuid-2".to_string(),
16363 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
16364 status: "Enabled".to_string(),
16365 last_modified: "2024-02-01T00:00:00Z".to_string(),
16366 },
16367 LambdaTrigger {
16368 uuid: "uuid-3".to_string(),
16369 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
16370 status: "Enabled".to_string(),
16371 last_modified: "2024-03-01T00:00:00Z".to_string(),
16372 },
16373 ];
16374
16375 let filtered = filtered_lambda_triggers(&app);
16377 assert_eq!(filtered.len(), 3);
16378
16379 app.sqs_state.triggers.filter = "alpha".to_string();
16381 let filtered = filtered_lambda_triggers(&app);
16382 assert_eq!(filtered.len(), 1);
16383 assert_eq!(
16384 filtered[0].arn,
16385 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
16386 );
16387
16388 app.sqs_state.triggers.filter = "test".to_string();
16390 let filtered = filtered_lambda_triggers(&app);
16391 assert_eq!(filtered.len(), 2);
16392 assert_eq!(
16393 filtered[0].arn,
16394 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
16395 );
16396 assert_eq!(
16397 filtered[1].arn,
16398 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
16399 );
16400
16401 app.sqs_state.triggers.filter = "uuid-3".to_string();
16403 let filtered = filtered_lambda_triggers(&app);
16404 assert_eq!(filtered.len(), 1);
16405 assert_eq!(filtered[0].uuid, "uuid-3");
16406 }
16407
16408 #[test]
16409 fn test_sqs_triggers_navigation() {
16410 let mut app = test_app();
16411 app.service_selected = true;
16412 app.mode = Mode::Normal;
16413 app.current_service = Service::SqsQueues;
16414 app.sqs_state.current_queue = Some("test-queue".to_string());
16415 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16416 app.sqs_state.triggers.items = vec![
16417 LambdaTrigger {
16418 uuid: "1".to_string(),
16419 arn: "arn1".to_string(),
16420 status: "Enabled".to_string(),
16421 last_modified: "2024-01-01".to_string(),
16422 },
16423 LambdaTrigger {
16424 uuid: "2".to_string(),
16425 arn: "arn2".to_string(),
16426 status: "Enabled".to_string(),
16427 last_modified: "2024-01-02".to_string(),
16428 },
16429 ];
16430
16431 assert_eq!(app.sqs_state.triggers.selected, 0);
16432 app.next_item();
16433 assert_eq!(app.sqs_state.triggers.selected, 1);
16434 app.prev_item();
16435 assert_eq!(app.sqs_state.triggers.selected, 0);
16436 }
16437
16438 #[test]
16439 fn test_sqs_pipes_navigation() {
16440 let mut app = test_app();
16441 app.service_selected = true;
16442 app.mode = Mode::Normal;
16443 app.current_service = Service::SqsQueues;
16444 app.sqs_state.current_queue = Some("test-queue".to_string());
16445 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
16446 app.sqs_state.pipes.items = vec![
16447 EventBridgePipe {
16448 name: "pipe1".to_string(),
16449 status: "RUNNING".to_string(),
16450 target: "target1".to_string(),
16451 last_modified: "2024-01-01".to_string(),
16452 },
16453 EventBridgePipe {
16454 name: "pipe2".to_string(),
16455 status: "RUNNING".to_string(),
16456 target: "target2".to_string(),
16457 last_modified: "2024-01-02".to_string(),
16458 },
16459 ];
16460
16461 assert_eq!(app.sqs_state.pipes.selected, 0);
16462 app.next_item();
16463 assert_eq!(app.sqs_state.pipes.selected, 1);
16464 app.prev_item();
16465 assert_eq!(app.sqs_state.pipes.selected, 0);
16466 }
16467
16468 #[test]
16469 fn test_sqs_tags_navigation() {
16470 let mut app = test_app();
16471 app.service_selected = true;
16472 app.mode = Mode::Normal;
16473 app.current_service = Service::SqsQueues;
16474 app.sqs_state.current_queue = Some("test-queue".to_string());
16475 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
16476 app.sqs_state.tags.items = vec![
16477 SqsQueueTag {
16478 key: "Env".to_string(),
16479 value: "prod".to_string(),
16480 },
16481 SqsQueueTag {
16482 key: "Team".to_string(),
16483 value: "backend".to_string(),
16484 },
16485 ];
16486
16487 assert_eq!(app.sqs_state.tags.selected, 0);
16488 app.next_item();
16489 assert_eq!(app.sqs_state.tags.selected, 1);
16490 app.prev_item();
16491 assert_eq!(app.sqs_state.tags.selected, 0);
16492 }
16493
16494 #[test]
16495 fn test_sqs_queues_navigation() {
16496 let mut app = test_app();
16497 app.service_selected = true;
16498 app.mode = Mode::Normal;
16499 app.current_service = Service::SqsQueues;
16500 app.sqs_state.queues.items = vec![
16501 SqsQueue {
16502 name: "queue1".to_string(),
16503 url: "url1".to_string(),
16504 queue_type: "Standard".to_string(),
16505 created_timestamp: "".to_string(),
16506 messages_available: "0".to_string(),
16507 messages_in_flight: "0".to_string(),
16508 encryption: "Disabled".to_string(),
16509 content_based_deduplication: "Disabled".to_string(),
16510 last_modified_timestamp: "".to_string(),
16511 visibility_timeout: "".to_string(),
16512 message_retention_period: "".to_string(),
16513 maximum_message_size: "".to_string(),
16514 delivery_delay: "".to_string(),
16515 receive_message_wait_time: "".to_string(),
16516 high_throughput_fifo: "-".to_string(),
16517 deduplication_scope: "-".to_string(),
16518 fifo_throughput_limit: "-".to_string(),
16519 dead_letter_queue: "-".to_string(),
16520 messages_delayed: "0".to_string(),
16521 redrive_allow_policy: "-".to_string(),
16522 redrive_policy: "".to_string(),
16523 redrive_task_id: "-".to_string(),
16524 redrive_task_start_time: "-".to_string(),
16525 redrive_task_status: "-".to_string(),
16526 redrive_task_percent: "-".to_string(),
16527 redrive_task_destination: "-".to_string(),
16528 },
16529 SqsQueue {
16530 name: "queue2".to_string(),
16531 url: "url2".to_string(),
16532 queue_type: "Standard".to_string(),
16533 created_timestamp: "".to_string(),
16534 messages_available: "0".to_string(),
16535 messages_in_flight: "0".to_string(),
16536 encryption: "Disabled".to_string(),
16537 content_based_deduplication: "Disabled".to_string(),
16538 last_modified_timestamp: "".to_string(),
16539 visibility_timeout: "".to_string(),
16540 message_retention_period: "".to_string(),
16541 maximum_message_size: "".to_string(),
16542 delivery_delay: "".to_string(),
16543 receive_message_wait_time: "".to_string(),
16544 high_throughput_fifo: "-".to_string(),
16545 deduplication_scope: "-".to_string(),
16546 fifo_throughput_limit: "-".to_string(),
16547 dead_letter_queue: "-".to_string(),
16548 messages_delayed: "0".to_string(),
16549 redrive_allow_policy: "-".to_string(),
16550 redrive_policy: "".to_string(),
16551 redrive_task_id: "-".to_string(),
16552 redrive_task_start_time: "-".to_string(),
16553 redrive_task_status: "-".to_string(),
16554 redrive_task_percent: "-".to_string(),
16555 redrive_task_destination: "-".to_string(),
16556 },
16557 ];
16558
16559 assert_eq!(app.sqs_state.queues.selected, 0);
16560 app.next_item();
16561 assert_eq!(app.sqs_state.queues.selected, 1);
16562 app.prev_item();
16563 assert_eq!(app.sqs_state.queues.selected, 0);
16564 }
16565
16566 #[test]
16567 fn test_sqs_subscriptions_navigation() {
16568 let mut app = test_app();
16569 app.service_selected = true;
16570 app.mode = Mode::Normal;
16571 app.current_service = Service::SqsQueues;
16572 app.sqs_state.current_queue = Some("test-queue".to_string());
16573 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16574 app.sqs_state.subscriptions.items = vec![
16575 SnsSubscription {
16576 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
16577 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
16578 },
16579 SnsSubscription {
16580 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
16581 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
16582 },
16583 ];
16584
16585 assert_eq!(app.sqs_state.subscriptions.selected, 0);
16586 app.next_item();
16587 assert_eq!(app.sqs_state.subscriptions.selected, 1);
16588 app.prev_item();
16589 assert_eq!(app.sqs_state.subscriptions.selected, 0);
16590 }
16591
16592 #[test]
16593 fn test_sqs_subscription_region_dropdown_navigation() {
16594 let mut app = test_app();
16595 app.service_selected = true;
16596 app.mode = Mode::FilterInput;
16597 app.current_service = Service::SqsQueues;
16598 app.sqs_state.current_queue = Some("test-queue".to_string());
16599 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16600 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
16601
16602 assert_eq!(app.sqs_state.subscription_region_selected, 0);
16603 app.next_item();
16604 assert_eq!(app.sqs_state.subscription_region_selected, 1);
16605 app.next_item();
16606 assert_eq!(app.sqs_state.subscription_region_selected, 2);
16607 app.prev_item();
16608 assert_eq!(app.sqs_state.subscription_region_selected, 1);
16609 app.prev_item();
16610 assert_eq!(app.sqs_state.subscription_region_selected, 0);
16611 }
16612
16613 #[test]
16614 fn test_sqs_subscription_region_selection() {
16615 let mut app = test_app();
16616 app.service_selected = true;
16617 app.mode = Mode::FilterInput;
16618 app.current_service = Service::SqsQueues;
16619 app.sqs_state.current_queue = Some("test-queue".to_string());
16620 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16621 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
16622 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
16625 app.handle_action(Action::ApplyFilter);
16626 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
16627 assert_eq!(app.mode, Mode::Normal);
16628 }
16629
16630 #[test]
16631 fn test_sqs_subscription_region_change_resets_selection() {
16632 let mut app = test_app();
16633 app.service_selected = true;
16634 app.mode = Mode::FilterInput;
16635 app.current_service = Service::SqsQueues;
16636 app.sqs_state.current_queue = Some("test-queue".to_string());
16637 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16638 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
16639 app.sqs_state.subscription_region_selected = 0;
16640 app.sqs_state.subscriptions.selected = 5;
16641
16642 app.handle_action(Action::NextItem);
16643
16644 assert_eq!(app.sqs_state.subscription_region_selected, 1);
16645 assert_eq!(app.sqs_state.subscriptions.selected, 0);
16646 }
16647
16648 #[test]
16649 fn test_s3_object_filter_resets_selection() {
16650 let mut app = test_app();
16651 app.service_selected = true;
16652 app.current_service = Service::S3Buckets;
16653 app.s3_state.current_bucket = Some("test-bucket".to_string());
16654 app.s3_state.selected_row = 5;
16655 app.mode = Mode::FilterInput;
16656
16657 app.handle_action(Action::CloseMenu);
16658
16659 assert_eq!(app.s3_state.selected_row, 0);
16660 assert_eq!(app.mode, Mode::Normal);
16661 }
16662
16663 #[test]
16664 fn test_s3_bucket_filter_resets_selection() {
16665 let mut app = test_app();
16666 app.service_selected = true;
16667 app.current_service = Service::S3Buckets;
16668 app.s3_state.selected_row = 10;
16669 app.mode = Mode::FilterInput;
16670
16671 app.handle_action(Action::CloseMenu);
16672
16673 assert_eq!(app.s3_state.selected_row, 0);
16674 assert_eq!(app.mode, Mode::Normal);
16675 }
16676
16677 #[test]
16678 fn test_s3_selection_stays_in_bounds() {
16679 let mut app = test_app();
16680 app.service_selected = true;
16681 app.current_service = Service::S3Buckets;
16682 app.s3_state.selected_row = 0;
16683 app.s3_state.selected_object = 0;
16684
16685 app.prev_item();
16687
16688 assert_eq!(app.s3_state.selected_row, 0);
16690 assert_eq!(app.s3_state.selected_object, 0);
16691 }
16692
16693 #[test]
16694 fn test_cfn_filter_resets_selection() {
16695 let mut app = test_app();
16696 app.service_selected = true;
16697 app.current_service = Service::CloudFormationStacks;
16698 app.cfn_state.table.selected = 10;
16699 app.mode = Mode::FilterInput;
16700
16701 app.handle_action(Action::CloseMenu);
16702
16703 assert_eq!(app.cfn_state.table.selected, 0);
16704 assert_eq!(app.mode, Mode::Normal);
16705 }
16706
16707 #[test]
16708 fn test_lambda_filter_resets_selection() {
16709 let mut app = test_app();
16710 app.service_selected = true;
16711 app.current_service = Service::LambdaFunctions;
16712 app.lambda_state.table.selected = 8;
16713 app.mode = Mode::FilterInput;
16714
16715 app.handle_action(Action::CloseMenu);
16716
16717 assert_eq!(app.lambda_state.table.selected, 0);
16718 assert_eq!(app.mode, Mode::Normal);
16719 }
16720
16721 #[test]
16722 fn test_sqs_filter_resets_selection() {
16723 let mut app = test_app();
16724 app.service_selected = true;
16725 app.current_service = Service::SqsQueues;
16726 app.sqs_state.queues.selected = 7;
16727 app.mode = Mode::FilterInput;
16728
16729 app.handle_action(Action::CloseMenu);
16730
16731 assert_eq!(app.sqs_state.queues.selected, 0);
16732 assert_eq!(app.mode, Mode::Normal);
16733 }
16734
16735 #[test]
16736 fn test_sqs_queues_list_shows_preferences() {
16737 let mut app = test_app();
16738 app.service_selected = true;
16739 app.current_service = Service::SqsQueues;
16740 app.mode = Mode::Normal;
16741
16742 app.handle_action(Action::OpenColumnSelector);
16743
16744 assert_eq!(app.mode, Mode::ColumnSelector);
16745 }
16746
16747 #[test]
16748 fn test_sqs_queue_policies_tab_no_preferences() {
16749 let mut app = test_app();
16750 app.service_selected = true;
16751 app.current_service = Service::SqsQueues;
16752 app.sqs_state.current_queue = Some("test-queue".to_string());
16753 app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
16754 app.mode = Mode::Normal;
16755
16756 app.handle_action(Action::OpenColumnSelector);
16757
16758 assert_eq!(app.mode, Mode::Normal);
16759 }
16760
16761 #[test]
16762 fn test_sqs_sns_subscriptions_tab_shows_preferences() {
16763 let mut app = test_app();
16764 app.service_selected = true;
16765 app.current_service = Service::SqsQueues;
16766 app.sqs_state.current_queue = Some("test-queue".to_string());
16767 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
16768 app.mode = Mode::Normal;
16769
16770 app.handle_action(Action::OpenColumnSelector);
16771
16772 assert_eq!(app.mode, Mode::ColumnSelector);
16773 }
16774
16775 #[test]
16776 fn test_sqs_monitoring_tab_no_preferences() {
16777 let mut app = test_app();
16778 app.service_selected = true;
16779 app.current_service = Service::SqsQueues;
16780 app.sqs_state.current_queue = Some("test-queue".to_string());
16781 app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
16782 app.mode = Mode::Normal;
16783
16784 app.handle_action(Action::OpenColumnSelector);
16785
16786 assert_eq!(app.mode, Mode::Normal);
16787 }
16788
16789 #[test]
16790 fn test_cfn_status_filter_change_resets_selection() {
16791 use crate::ui::cfn::STATUS_FILTER;
16792 let mut app = test_app();
16793 app.service_selected = true;
16794 app.current_service = Service::CloudFormationStacks;
16795 app.mode = Mode::FilterInput;
16796 app.cfn_state.input_focus = STATUS_FILTER;
16797 app.cfn_state.status_filter = CfnStatusFilter::All;
16798 app.cfn_state.table.items = vec![
16799 CfnStack {
16800 name: "stack1".to_string(),
16801 stack_id: "id1".to_string(),
16802 status: "CREATE_COMPLETE".to_string(),
16803 created_time: "2024-01-01".to_string(),
16804 updated_time: String::new(),
16805 deleted_time: String::new(),
16806 drift_status: String::new(),
16807 last_drift_check_time: String::new(),
16808 status_reason: String::new(),
16809 description: String::new(),
16810 detailed_status: String::new(),
16811 root_stack: String::new(),
16812 parent_stack: String::new(),
16813 termination_protection: false,
16814 iam_role: String::new(),
16815 tags: Vec::new(),
16816 stack_policy: String::new(),
16817 rollback_monitoring_time: String::new(),
16818 rollback_alarms: Vec::new(),
16819 notification_arns: Vec::new(),
16820 },
16821 CfnStack {
16822 name: "stack2".to_string(),
16823 stack_id: "id2".to_string(),
16824 status: "UPDATE_IN_PROGRESS".to_string(),
16825 created_time: "2024-01-02".to_string(),
16826 updated_time: String::new(),
16827 deleted_time: String::new(),
16828 drift_status: String::new(),
16829 last_drift_check_time: String::new(),
16830 status_reason: String::new(),
16831 description: String::new(),
16832 detailed_status: String::new(),
16833 root_stack: String::new(),
16834 parent_stack: String::new(),
16835 termination_protection: false,
16836 iam_role: String::new(),
16837 tags: Vec::new(),
16838 stack_policy: String::new(),
16839 rollback_monitoring_time: String::new(),
16840 rollback_alarms: Vec::new(),
16841 notification_arns: Vec::new(),
16842 },
16843 ];
16844 app.cfn_state.table.selected = 1;
16845
16846 app.handle_action(Action::NextItem);
16847
16848 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
16849 assert_eq!(app.cfn_state.table.selected, 0);
16850 }
16851
16852 #[test]
16853 fn test_cfn_view_nested_toggle_resets_selection() {
16854 use crate::ui::cfn::VIEW_NESTED;
16855 let mut app = test_app();
16856 app.service_selected = true;
16857 app.current_service = Service::CloudFormationStacks;
16858 app.mode = Mode::FilterInput;
16859 app.cfn_state.input_focus = VIEW_NESTED;
16860 app.cfn_state.view_nested = false;
16861 app.cfn_state.table.items = vec![CfnStack {
16862 name: "stack1".to_string(),
16863 stack_id: "id1".to_string(),
16864 status: "CREATE_COMPLETE".to_string(),
16865 created_time: "2024-01-01".to_string(),
16866 updated_time: String::new(),
16867 deleted_time: String::new(),
16868 drift_status: String::new(),
16869 last_drift_check_time: String::new(),
16870 status_reason: String::new(),
16871 description: String::new(),
16872 detailed_status: String::new(),
16873 root_stack: String::new(),
16874 parent_stack: String::new(),
16875 termination_protection: false,
16876 iam_role: String::new(),
16877 tags: Vec::new(),
16878 stack_policy: String::new(),
16879 rollback_monitoring_time: String::new(),
16880 rollback_alarms: Vec::new(),
16881 notification_arns: Vec::new(),
16882 }];
16883 app.cfn_state.table.selected = 5;
16884
16885 app.handle_action(Action::ToggleFilterCheckbox);
16886
16887 assert!(app.cfn_state.view_nested);
16888 assert_eq!(app.cfn_state.table.selected, 0);
16889 }
16890
16891 #[test]
16892 fn test_cfn_template_scroll_up() {
16893 let mut app = test_app();
16894 app.service_selected = true;
16895 app.current_service = Service::CloudFormationStacks;
16896 app.cfn_state.current_stack = Some("test-stack".to_string());
16897 app.cfn_state.detail_tab = CfnDetailTab::Template;
16898 app.cfn_state.template_scroll = 20;
16899
16900 app.page_up();
16901
16902 assert_eq!(app.cfn_state.template_scroll, 10);
16903 }
16904
16905 #[test]
16906 fn test_cfn_template_scroll_down() {
16907 let mut app = test_app();
16908 app.service_selected = true;
16909 app.current_service = Service::CloudFormationStacks;
16910 app.cfn_state.current_stack = Some("test-stack".to_string());
16911 app.cfn_state.detail_tab = CfnDetailTab::Template;
16912 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
16913 app.cfn_state.template_scroll = 0;
16914
16915 app.page_down();
16916
16917 assert_eq!(app.cfn_state.template_scroll, 10);
16918 }
16919
16920 #[test]
16921 fn test_cfn_template_scroll_down_respects_max() {
16922 let mut app = test_app();
16923 app.service_selected = true;
16924 app.current_service = Service::CloudFormationStacks;
16925 app.cfn_state.current_stack = Some("test-stack".to_string());
16926 app.cfn_state.detail_tab = CfnDetailTab::Template;
16927 app.cfn_state.template_body = "line1\nline2\nline3".to_string();
16928 app.cfn_state.template_scroll = 0;
16929
16930 app.page_down();
16931
16932 assert_eq!(app.cfn_state.template_scroll, 2);
16934 }
16935
16936 #[test]
16937 fn test_cfn_template_arrow_up() {
16938 let mut app = test_app();
16939 app.service_selected = true;
16940 app.current_service = Service::CloudFormationStacks;
16941 app.mode = Mode::Normal;
16942 app.cfn_state.current_stack = Some("test-stack".to_string());
16943 app.cfn_state.detail_tab = CfnDetailTab::Template;
16944 app.cfn_state.template_scroll = 5;
16945
16946 app.prev_item();
16947
16948 assert_eq!(app.cfn_state.template_scroll, 4);
16949 }
16950
16951 #[test]
16952 fn test_cfn_template_arrow_down() {
16953 let mut app = test_app();
16954 app.service_selected = true;
16955 app.current_service = Service::CloudFormationStacks;
16956 app.mode = Mode::Normal;
16957 app.cfn_state.current_stack = Some("test-stack".to_string());
16958 app.cfn_state.detail_tab = CfnDetailTab::Template;
16959 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
16960 app.cfn_state.template_scroll = 2;
16961
16962 app.next_item();
16963
16964 assert_eq!(app.cfn_state.template_scroll, 3);
16965 }
16966
16967 #[test]
16968 fn test_cfn_template_arrow_down_respects_max() {
16969 let mut app = test_app();
16970 app.service_selected = true;
16971 app.current_service = Service::CloudFormationStacks;
16972 app.mode = Mode::Normal;
16973 app.cfn_state.current_stack = Some("test-stack".to_string());
16974 app.cfn_state.detail_tab = CfnDetailTab::Template;
16975 app.cfn_state.template_body = "line1\nline2".to_string();
16976 app.cfn_state.template_scroll = 1;
16977
16978 app.next_item();
16979
16980 assert_eq!(app.cfn_state.template_scroll, 1);
16982 }
16983
16984 #[test]
16985 fn test_cfn_yank_on_template_tab_copies_template_body() {
16986 let mut app = test_app();
16989 app.current_service = Service::CloudFormationStacks;
16990 app.cfn_state.current_stack = Some("my-stack".to_string());
16991 app.cfn_state.detail_tab = CfnDetailTab::Template;
16992 app.cfn_state.template_body = "AWSTemplateFormatVersion: '2010-09-09'".to_string();
16993 assert!(!app.cfn_state.template_body.is_empty());
16997 crate::cfn::actions::yank(&app); }
16999
17000 #[test]
17001 fn test_cfn_change_sets_console_url_includes_events_view_graph() {
17002 use crate::cfn::console_url_stack_detail_with_tab;
17003 let url = console_url_stack_detail_with_tab(
17004 "us-east-1",
17005 "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
17006 &CfnDetailTab::ChangeSets,
17007 );
17008 assert!(
17009 url.contains("eventsView=graph"),
17010 "ChangeSets URL must include eventsView=graph, got: {url}"
17011 );
17012 assert!(url.contains("changesets"), "URL must contain 'changesets'");
17013 }
17014
17015 #[test]
17016 fn test_cfn_other_tabs_console_url_no_events_view_graph() {
17017 use crate::cfn::console_url_stack_detail_with_tab;
17018 for tab in [
17019 CfnDetailTab::StackInfo,
17020 CfnDetailTab::Events,
17021 CfnDetailTab::Resources,
17022 ] {
17023 let url = console_url_stack_detail_with_tab(
17024 "us-east-1",
17025 "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
17026 &tab,
17027 );
17028 assert!(
17029 !url.contains("eventsView=graph"),
17030 "{tab:?} URL must NOT include eventsView=graph, got: {url}"
17031 );
17032 }
17033 }
17034
17035 #[test]
17036 fn test_cfn_change_sets_expand_collapse() {
17037 let mut app = test_app();
17038 app.current_service = Service::CloudFormationStacks;
17039 app.service_selected = true;
17040 app.cfn_state.current_stack = Some("my-stack".to_string());
17041 app.cfn_state.detail_tab = CfnDetailTab::ChangeSets;
17042 app.cfn_state.change_sets.items = vec![rusticity_core::cfn::StackChangeSet {
17043 name: "cs-001".to_string(),
17044 change_set_id: "id-001".to_string(),
17045 created_time: String::new(),
17046 status: "CREATE_COMPLETE".to_string(),
17047 description: String::new(),
17048 root_change_set_id: String::new(),
17049 parent_change_set_id: String::new(),
17050 }];
17051 app.cfn_state.change_sets.selected = 0;
17052
17053 assert_eq!(app.cfn_state.change_sets.expanded_item, None);
17054 app.handle_action(Action::ExpandRow);
17055 assert_eq!(
17056 app.cfn_state.change_sets.expanded_item,
17057 Some(0),
17058 "Enter must expand change set row"
17059 );
17060 app.handle_action(Action::CollapseRow);
17061 assert_eq!(
17062 app.cfn_state.change_sets.expanded_item, None,
17063 "Left arrow must collapse change set row"
17064 );
17065 }
17066
17067 #[test]
17068 fn test_cfn_events_toggle_column_hides_visible_column() {
17069 let mut app = test_app();
17072 app.current_service = Service::CloudFormationStacks;
17073 app.mode = Mode::ColumnSelector;
17074 app.cfn_state.current_stack = Some("my-stack".to_string());
17075 app.cfn_state.detail_tab = CfnDetailTab::Events;
17076
17077 let first_col = app.cfn_event_column_ids[0];
17079 assert!(
17080 app.cfn_event_visible_column_ids.contains(&first_col),
17081 "First column must be visible before toggle"
17082 );
17083
17084 app.column_selector_index = 1; app.handle_action(Action::ToggleColumn);
17086
17087 assert!(
17088 !app.cfn_event_visible_column_ids.contains(&first_col),
17089 "ToggleColumn must hide the first Events column"
17090 );
17091
17092 app.handle_action(Action::ToggleColumn);
17094 assert!(
17095 app.cfn_event_visible_column_ids.contains(&first_col),
17096 "ToggleColumn again must re-show the column"
17097 );
17098 }
17099
17100 #[test]
17101 fn test_cfn_events_toggle_column_keeps_at_least_one_visible() {
17102 let mut app = test_app();
17104 app.current_service = Service::CloudFormationStacks;
17105 app.mode = Mode::ColumnSelector;
17106 app.cfn_state.current_stack = Some("my-stack".to_string());
17107 app.cfn_state.detail_tab = CfnDetailTab::Events;
17108
17109 app.cfn_event_visible_column_ids = vec![app.cfn_event_column_ids[0]];
17111 app.column_selector_index = 1;
17112
17113 app.handle_action(Action::ToggleColumn);
17114
17115 assert!(
17117 !app.cfn_event_visible_column_ids.is_empty(),
17118 "At least one column must remain visible"
17119 );
17120 }
17121
17122 #[test]
17123 fn test_space_menu_o_opens_service_picker() {
17124 let mut app = test_app();
17126 app.handle_action(Action::OpenSpaceMenu);
17127 assert_eq!(app.mode, Mode::SpaceMenu);
17128
17129 app.handle_action(Action::OpenServicePicker);
17130 assert_eq!(
17131 app.mode,
17132 Mode::ServicePicker,
17133 "'o' from SpaceMenu must open ServicePicker"
17134 );
17135 }
17136
17137 #[test]
17138 fn test_space_menu_c_closes_service() {
17139 let mut app = test_app();
17140 app.current_service = Service::CloudFormationStacks;
17141 app.service_selected = true;
17142 app.tabs.push(Tab {
17143 service: Service::CloudFormationStacks,
17144 title: "CFN".to_string(),
17145 breadcrumb: "CloudFormation".to_string(),
17146 });
17147 app.current_tab = 0;
17148
17149 app.handle_action(Action::OpenSpaceMenu);
17150 app.handle_action(Action::CloseService);
17151 assert_ne!(
17153 app.mode,
17154 Mode::SpaceMenu,
17155 "CloseService must exit SpaceMenu"
17156 );
17157 }
17158}
17159
17160#[cfg(test)]
17161mod lambda_version_tab_tests {
17162 use super::*;
17163 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
17164 use test_helpers::*;
17165
17166 #[test]
17167 fn test_lambda_version_tab_cycling_next() {
17168 let mut app = test_app();
17169 app.current_service = Service::LambdaFunctions;
17170 app.lambda_state.current_function = Some("test-function".to_string());
17171 app.lambda_state.current_version = Some("1".to_string());
17172 app.lambda_state.detail_tab = LambdaDetailTab::Code;
17173
17174 app.handle_action(Action::NextDetailTab);
17176 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17177 assert!(app.lambda_state.metrics_loading);
17178
17179 app.lambda_state.metrics_loading = false;
17181 app.handle_action(Action::NextDetailTab);
17182 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
17183
17184 app.handle_action(Action::NextDetailTab);
17186 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
17187 }
17188
17189 #[test]
17190 fn test_lambda_version_tab_cycling_prev() {
17191 let mut app = test_app();
17192 app.current_service = Service::LambdaFunctions;
17193 app.lambda_state.current_function = Some("test-function".to_string());
17194 app.lambda_state.current_version = Some("1".to_string());
17195 app.lambda_state.detail_tab = LambdaDetailTab::Code;
17196
17197 app.handle_action(Action::PrevDetailTab);
17199 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
17200
17201 app.handle_action(Action::PrevDetailTab);
17203 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17204 assert!(app.lambda_state.metrics_loading);
17205
17206 app.lambda_state.metrics_loading = false;
17208 app.handle_action(Action::PrevDetailTab);
17209 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
17210 }
17211
17212 #[test]
17213 fn test_lambda_version_monitor_clears_metrics() {
17214 let mut app = test_app();
17215 app.current_service = Service::LambdaFunctions;
17216 app.lambda_state.current_function = Some("test-function".to_string());
17217 app.lambda_state.current_version = Some("1".to_string());
17218 app.lambda_state.detail_tab = LambdaDetailTab::Code;
17219
17220 app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
17222 app.lambda_state.monitoring_scroll = 5;
17223
17224 app.handle_action(Action::NextDetailTab);
17226
17227 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17228 assert!(app.lambda_state.metrics_loading);
17229 assert_eq!(app.lambda_state.monitoring_scroll, 0);
17230 assert!(app.lambda_state.metric_data_invocations.is_empty());
17231 }
17232
17233 #[test]
17234 fn test_cfn_parameters_expand_collapse() {
17235 let mut app = test_app();
17236 app.current_service = Service::CloudFormationStacks;
17237 app.service_selected = true;
17238 app.cfn_state.current_stack = Some("test-stack".to_string());
17239 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17240 app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
17241 key: "Param1".to_string(),
17242 value: "Value1".to_string(),
17243 resolved_value: "Resolved1".to_string(),
17244 }];
17245 app.cfn_state.parameters.reset();
17246
17247 assert_eq!(app.cfn_state.parameters.expanded_item, None);
17248
17249 app.handle_action(Action::NextPane);
17251 assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
17252
17253 app.handle_action(Action::PrevPane);
17255 assert_eq!(app.cfn_state.parameters.expanded_item, None);
17256 }
17257
17258 #[test]
17259 fn test_cfn_parameters_filter_resets_selection() {
17260 let mut app = test_app();
17261 app.current_service = Service::CloudFormationStacks;
17262 app.service_selected = true;
17263 app.cfn_state.current_stack = Some("test-stack".to_string());
17264 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17265 app.cfn_state.parameters.items = vec![
17266 rusticity_core::cfn::StackParameter {
17267 key: "DatabaseName".to_string(),
17268 value: "mydb".to_string(),
17269 resolved_value: "mydb".to_string(),
17270 },
17271 rusticity_core::cfn::StackParameter {
17272 key: "InstanceType".to_string(),
17273 value: "t2.micro".to_string(),
17274 resolved_value: "t2.micro".to_string(),
17275 },
17276 rusticity_core::cfn::StackParameter {
17277 key: "Environment".to_string(),
17278 value: "production".to_string(),
17279 resolved_value: "production".to_string(),
17280 },
17281 ];
17282 app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
17284 app.cfn_state.parameters_input_focus = InputFocus::Filter;
17285
17286 app.handle_action(Action::FilterInput('D'));
17288 assert_eq!(app.cfn_state.parameters.selected, 0);
17289 assert_eq!(app.cfn_state.parameters.filter, "D");
17290
17291 app.cfn_state.parameters.selected = 1;
17293
17294 app.handle_action(Action::FilterInput('a'));
17296 assert_eq!(app.cfn_state.parameters.selected, 0);
17297 assert_eq!(app.cfn_state.parameters.filter, "Da");
17298
17299 app.cfn_state.parameters.selected = 1;
17301
17302 app.handle_action(Action::FilterBackspace);
17304 assert_eq!(app.cfn_state.parameters.selected, 0);
17305 assert_eq!(app.cfn_state.parameters.filter, "D");
17306 }
17307
17308 #[test]
17309 fn test_cfn_template_tab_no_preferences() {
17310 let mut app = test_app();
17311 app.current_service = Service::CloudFormationStacks;
17312 app.service_selected = true;
17313 app.cfn_state.current_stack = Some("test-stack".to_string());
17314 app.cfn_state.detail_tab = CfnDetailTab::Template;
17315 app.mode = Mode::Normal;
17316
17317 app.handle_action(Action::OpenColumnSelector);
17319 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::GitSync;
17323 app.handle_action(Action::OpenColumnSelector);
17324 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17328 app.handle_action(Action::OpenColumnSelector);
17329 assert_eq!(app.mode, Mode::ColumnSelector); app.mode = Mode::Normal;
17333 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17334 app.handle_action(Action::OpenColumnSelector);
17335 assert_eq!(app.mode, Mode::ColumnSelector); }
17337
17338 #[test]
17339 fn test_iam_user_groups_tab_shows_preferences() {
17340 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17341 app.current_service = Service::IamUsers;
17342 app.service_selected = true;
17343 app.mode = Mode::Normal;
17344 app.iam_state.current_user = Some("test-user".to_string());
17345 app.iam_state.user_tab = UserTab::Groups;
17346
17347 app.handle_action(Action::OpenColumnSelector);
17349 assert_eq!(app.mode, Mode::ColumnSelector);
17350 }
17351
17352 #[test]
17353 fn test_iam_user_tags_tab_shows_preferences() {
17354 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17355 app.current_service = Service::IamUsers;
17356 app.service_selected = true;
17357 app.mode = Mode::Normal;
17358 app.iam_state.current_user = Some("test-user".to_string());
17359 app.iam_state.user_tab = UserTab::Tags;
17360
17361 app.handle_action(Action::OpenColumnSelector);
17363 assert_eq!(app.mode, Mode::ColumnSelector);
17364 }
17365
17366 #[test]
17367 fn test_iam_user_last_accessed_tab_shows_preferences() {
17368 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17369 app.current_service = Service::IamUsers;
17370 app.service_selected = true;
17371 app.mode = Mode::Normal;
17372 app.iam_state.current_user = Some("test-user".to_string());
17373 app.iam_state.user_tab = UserTab::LastAccessed;
17374
17375 app.handle_action(Action::OpenColumnSelector);
17377 assert_eq!(app.mode, Mode::ColumnSelector);
17378 }
17379
17380 #[test]
17381 fn test_iam_user_security_credentials_tab_no_preferences() {
17382 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17383 app.current_service = Service::IamUsers;
17384 app.service_selected = true;
17385 app.mode = Mode::Normal;
17386 app.iam_state.current_user = Some("test-user".to_string());
17387 app.iam_state.user_tab = UserTab::SecurityCredentials;
17388
17389 app.handle_action(Action::OpenColumnSelector);
17391 assert_eq!(app.mode, Mode::Normal);
17392 }
17393
17394 #[test]
17395 fn test_iam_user_tabs_without_column_preferences() {
17396 let mut app = test_app();
17397 app.current_service = Service::IamUsers;
17398 app.service_selected = true;
17399 app.iam_state.current_user = Some("test-user".to_string());
17400 app.mode = Mode::Normal;
17401
17402 app.iam_state.user_tab = UserTab::Groups;
17404 app.handle_action(Action::OpenColumnSelector);
17405 assert_eq!(app.mode, Mode::ColumnSelector);
17406 app.mode = Mode::Normal;
17407
17408 app.iam_state.user_tab = UserTab::Tags;
17410 app.handle_action(Action::OpenColumnSelector);
17411 assert_eq!(app.mode, Mode::ColumnSelector);
17412 app.mode = Mode::Normal;
17413
17414 app.iam_state.user_tab = UserTab::SecurityCredentials;
17416 app.handle_action(Action::OpenColumnSelector);
17417 assert_eq!(app.mode, Mode::Normal);
17418
17419 app.iam_state.user_tab = UserTab::LastAccessed;
17421 app.handle_action(Action::OpenColumnSelector);
17422 assert_eq!(app.mode, Mode::ColumnSelector);
17423 app.mode = Mode::Normal;
17424
17425 app.iam_state.user_tab = UserTab::Permissions;
17427 app.handle_action(Action::OpenColumnSelector);
17428 assert_eq!(app.mode, Mode::ColumnSelector);
17429
17430 app.mode = Mode::Normal;
17432 app.iam_state.current_user = None;
17433 app.handle_action(Action::OpenColumnSelector);
17434 assert_eq!(app.mode, Mode::ColumnSelector);
17435 }
17436
17437 #[test]
17438 fn test_iam_role_policies_dropdown_cycling() {
17439 let mut app = test_app();
17440 app.current_service = Service::IamRoles;
17441 app.service_selected = true;
17442 app.iam_state.current_role = Some("test-role".to_string());
17443 app.iam_state.role_tab = RoleTab::Permissions;
17444 app.mode = Mode::FilterInput;
17445 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
17446 app.iam_state.policy_type_filter = "All types".to_string();
17447
17448 app.next_item();
17450 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17451 app.next_item();
17452 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17453 app.next_item();
17454 assert_eq!(app.iam_state.policy_type_filter, "All types");
17455
17456 app.prev_item();
17458 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17459 app.prev_item();
17460 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17461 app.prev_item();
17462 assert_eq!(app.iam_state.policy_type_filter, "All types");
17463 }
17464
17465 #[test]
17466 fn test_iam_user_policies_dropdown_cycling() {
17467 let mut app = test_app();
17468 app.current_service = Service::IamUsers;
17469 app.service_selected = true;
17470 app.iam_state.current_user = Some("test-user".to_string());
17471 app.iam_state.user_tab = UserTab::Permissions;
17472 app.mode = Mode::FilterInput;
17473 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
17474 app.iam_state.policy_type_filter = "All types".to_string();
17475
17476 app.next_item();
17478 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17479 app.next_item();
17480 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17481 app.next_item();
17482 assert_eq!(app.iam_state.policy_type_filter, "All types");
17483
17484 app.prev_item();
17486 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17487 app.prev_item();
17488 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17489 app.prev_item();
17490 assert_eq!(app.iam_state.policy_type_filter, "All types");
17491 }
17492
17493 #[test]
17494 fn test_iam_role_tabs_without_column_preferences() {
17495 let mut app = test_app();
17496 app.current_service = Service::IamRoles;
17497 app.service_selected = true;
17498 app.iam_state.current_role = Some("test-role".to_string());
17499 app.mode = Mode::Normal;
17500
17501 app.iam_state.role_tab = RoleTab::TrustRelationships;
17503 app.handle_action(Action::OpenColumnSelector);
17504 assert_eq!(app.mode, Mode::Normal);
17505
17506 app.iam_state.role_tab = RoleTab::RevokeSessions;
17508 app.handle_action(Action::OpenColumnSelector);
17509 assert_eq!(app.mode, Mode::Normal);
17510
17511 app.iam_state.role_tab = RoleTab::LastAccessed;
17513 app.handle_action(Action::OpenColumnSelector);
17514 assert_eq!(app.mode, Mode::ColumnSelector);
17515
17516 app.mode = Mode::Normal;
17518 app.iam_state.role_tab = RoleTab::Permissions;
17519 app.handle_action(Action::OpenColumnSelector);
17520 assert_eq!(app.mode, Mode::ColumnSelector);
17521
17522 app.mode = Mode::Normal;
17524 app.iam_state.role_tab = RoleTab::Tags;
17525 app.handle_action(Action::OpenColumnSelector);
17526 assert_eq!(app.mode, Mode::ColumnSelector);
17527
17528 app.mode = Mode::Normal;
17530 app.iam_state.current_role = None;
17531 app.handle_action(Action::OpenColumnSelector);
17532 assert_eq!(app.mode, Mode::ColumnSelector);
17533 }
17534
17535 #[test]
17536 fn test_iam_role_tags_tab_cycling() {
17537 let mut app = test_app();
17538 app.current_service = Service::IamRoles;
17539 app.service_selected = true;
17540 app.iam_state.current_role = Some("test-role".to_string());
17541 app.iam_state.role_tab = RoleTab::Tags;
17542 app.mode = Mode::ColumnSelector;
17543 app.column_selector_index = 0;
17544
17545 app.handle_action(Action::NextPreferences);
17547 assert_eq!(app.column_selector_index, 4);
17548
17549 app.handle_action(Action::NextPreferences);
17551 assert_eq!(app.column_selector_index, 0);
17552
17553 app.handle_action(Action::PrevPreferences);
17555 assert_eq!(app.column_selector_index, 4);
17556
17557 app.handle_action(Action::PrevPreferences);
17559 assert_eq!(app.column_selector_index, 0);
17560 }
17561
17562 #[test]
17563 fn test_cfn_outputs_expand_collapse() {
17564 let mut app = test_app();
17565 app.current_service = Service::CloudFormationStacks;
17566 app.service_selected = true;
17567 app.cfn_state.current_stack = Some("test-stack".to_string());
17568 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17569 app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
17570 key: "Output1".to_string(),
17571 value: "Value1".to_string(),
17572 description: "Description1".to_string(),
17573 export_name: "Export1".to_string(),
17574 }];
17575 app.cfn_state.outputs.reset();
17576
17577 assert_eq!(app.cfn_state.outputs.expanded_item, None);
17578
17579 app.handle_action(Action::NextPane);
17581 assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
17582
17583 app.handle_action(Action::PrevPane);
17585 assert_eq!(app.cfn_state.outputs.expanded_item, None);
17586 }
17587
17588 #[test]
17589 fn test_cfn_outputs_filter_resets_selection() {
17590 let mut app = test_app();
17591 app.current_service = Service::CloudFormationStacks;
17592 app.service_selected = true;
17593 app.cfn_state.current_stack = Some("test-stack".to_string());
17594 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17595 app.cfn_state.outputs.items = vec![
17596 rusticity_core::cfn::StackOutput {
17597 key: "ApiUrl".to_string(),
17598 value: "https://api.example.com".to_string(),
17599 description: "API endpoint".to_string(),
17600 export_name: "MyApiUrl".to_string(),
17601 },
17602 rusticity_core::cfn::StackOutput {
17603 key: "BucketName".to_string(),
17604 value: "my-bucket".to_string(),
17605 description: "S3 bucket".to_string(),
17606 export_name: "MyBucket".to_string(),
17607 },
17608 ];
17609 app.cfn_state.outputs.reset();
17610 app.cfn_state.outputs.selected = 1;
17611
17612 app.handle_action(Action::StartFilter);
17614 assert_eq!(app.mode, Mode::FilterInput);
17615
17616 app.handle_action(Action::FilterInput('A'));
17618 assert_eq!(app.cfn_state.outputs.selected, 0);
17619 assert_eq!(app.cfn_state.outputs.filter, "A");
17620
17621 app.cfn_state.outputs.selected = 1;
17623 app.handle_action(Action::FilterInput('p'));
17624 assert_eq!(app.cfn_state.outputs.selected, 0);
17625
17626 app.cfn_state.outputs.selected = 1;
17628 app.handle_action(Action::FilterBackspace);
17629 assert_eq!(app.cfn_state.outputs.selected, 0);
17630 }
17631
17632 #[test]
17633 fn test_ec2_service_in_picker() {
17634 let app = test_app();
17635 assert!(app.service_picker.services.contains(&"EC2 › Instances"));
17636 }
17637
17638 #[test]
17639 fn test_ec2_state_filter_cycles() {
17640 let mut app = test_app();
17641 app.current_service = Service::Ec2Instances;
17642 app.service_selected = true;
17643 app.mode = Mode::FilterInput;
17644 app.ec2_state.input_focus = EC2_STATE_FILTER;
17645
17646 let initial = app.ec2_state.state_filter;
17647 assert_eq!(initial, Ec2StateFilter::AllStates);
17648
17649 app.handle_action(Action::ToggleFilterCheckbox);
17651 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17652
17653 app.handle_action(Action::ToggleFilterCheckbox);
17654 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
17655
17656 app.handle_action(Action::ToggleFilterCheckbox);
17657 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
17658
17659 app.handle_action(Action::ToggleFilterCheckbox);
17660 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
17661
17662 app.handle_action(Action::ToggleFilterCheckbox);
17663 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
17664
17665 app.handle_action(Action::ToggleFilterCheckbox);
17666 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
17667
17668 app.handle_action(Action::ToggleFilterCheckbox);
17669 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
17670 }
17671
17672 #[test]
17673 fn test_ec2_filter_resets_table() {
17674 let mut app = test_app();
17675 app.current_service = Service::Ec2Instances;
17676 app.service_selected = true;
17677 app.mode = Mode::FilterInput;
17678 app.ec2_state.input_focus = EC2_STATE_FILTER;
17679 app.ec2_state.table.selected = 5;
17680
17681 app.handle_action(Action::ToggleFilterCheckbox);
17682 assert_eq!(app.ec2_state.table.selected, 0);
17683 }
17684
17685 #[test]
17686 fn test_ec2_columns_visible() {
17687 let app = test_app();
17688 assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
17691
17692 #[test]
17693 fn test_ec2_breadcrumbs() {
17694 let mut app = test_app();
17695 app.current_service = Service::Ec2Instances;
17696 app.service_selected = true;
17697 let breadcrumb = app.breadcrumbs();
17698 assert_eq!(breadcrumb, "EC2 > Instances");
17699 }
17700
17701 #[test]
17702 fn test_ec2_console_url() {
17703 let mut app = test_app();
17704 app.current_service = Service::Ec2Instances;
17705 app.service_selected = true;
17706 let url = app.get_console_url();
17707 assert!(url.contains("ec2"));
17708 assert!(url.contains("Instances"));
17709 }
17710
17711 #[test]
17712 fn test_ec2_filter_handling() {
17713 let mut app = test_app();
17714 app.current_service = Service::Ec2Instances;
17715 app.service_selected = true;
17716 app.mode = Mode::FilterInput;
17717
17718 app.handle_action(Action::FilterInput('t'));
17719 app.handle_action(Action::FilterInput('e'));
17720 app.handle_action(Action::FilterInput('s'));
17721 app.handle_action(Action::FilterInput('t'));
17722
17723 assert_eq!(app.ec2_state.table.filter, "test");
17724
17725 app.handle_action(Action::FilterBackspace);
17726 assert_eq!(app.ec2_state.table.filter, "tes");
17727 }
17728
17729 #[test]
17730 fn test_column_selector_page_down_ec2() {
17731 let mut app = test_app();
17732 app.current_service = Service::Ec2Instances;
17733 app.service_selected = true;
17734 app.mode = Mode::ColumnSelector;
17735 app.column_selector_index = 0;
17736
17737 app.handle_action(Action::PageDown);
17738 assert_eq!(app.column_selector_index, 10);
17739
17740 app.handle_action(Action::PageDown);
17741 assert_eq!(app.column_selector_index, 20);
17742 }
17743
17744 #[test]
17745 fn test_column_selector_page_up_ec2() {
17746 let mut app = test_app();
17747 app.current_service = Service::Ec2Instances;
17748 app.service_selected = true;
17749 app.mode = Mode::ColumnSelector;
17750 app.column_selector_index = 30;
17751
17752 app.handle_action(Action::PageUp);
17753 assert_eq!(app.column_selector_index, 20);
17754
17755 app.handle_action(Action::PageUp);
17756 assert_eq!(app.column_selector_index, 10);
17757 }
17758
17759 #[test]
17760 fn test_ec2_state_filter_dropdown_focus() {
17761 let mut app = test_app();
17762 app.current_service = Service::Ec2Instances;
17763 app.service_selected = true;
17764 app.mode = Mode::FilterInput;
17765
17766 app.handle_action(Action::NextFilterFocus);
17768 assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
17769
17770 app.handle_action(Action::ToggleFilterCheckbox);
17773 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17774 }
17775
17776 #[test]
17777 fn test_column_selector_ctrl_d_scrolling() {
17778 let mut app = test_app();
17779 app.current_service = Service::LambdaFunctions;
17780 app.mode = Mode::ColumnSelector;
17781 app.column_selector_index = 0;
17782
17783 app.handle_action(Action::PageDown);
17785 assert_eq!(app.column_selector_index, 11);
17786
17787 let max = app.get_column_selector_max();
17789 app.handle_action(Action::PageDown);
17790 assert_eq!(app.column_selector_index, max);
17791 }
17792
17793 #[test]
17794 fn test_column_selector_ctrl_u_scrolling() {
17795 let mut app = test_app();
17796 app.current_service = Service::CloudFormationStacks;
17797 app.mode = Mode::ColumnSelector;
17798 app.column_selector_index = 25;
17799
17800 app.handle_action(Action::PageUp);
17801 assert_eq!(app.column_selector_index, 15);
17802
17803 app.handle_action(Action::PageUp);
17804 assert_eq!(app.column_selector_index, 5);
17805 }
17806
17807 #[test]
17808 fn test_prev_preferences_lambda() {
17809 let mut app = test_app();
17810 app.current_service = Service::LambdaFunctions;
17811 app.mode = Mode::ColumnSelector;
17812 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
17813 app.column_selector_index = page_size_idx;
17814
17815 app.handle_action(Action::PrevPreferences);
17816 assert_eq!(app.column_selector_index, 0);
17817
17818 app.handle_action(Action::PrevPreferences);
17819 assert_eq!(app.column_selector_index, page_size_idx);
17820 }
17821
17822 #[test]
17823 fn test_prev_preferences_cloudformation() {
17824 let mut app = test_app();
17825 app.current_service = Service::CloudFormationStacks;
17826 app.mode = Mode::ColumnSelector;
17827 let page_size_idx = app.cfn_column_ids.len() + 2;
17828 app.column_selector_index = page_size_idx;
17829
17830 app.handle_action(Action::PrevPreferences);
17831 assert_eq!(app.column_selector_index, 0);
17832
17833 app.handle_action(Action::PrevPreferences);
17834 assert_eq!(app.column_selector_index, page_size_idx);
17835 }
17836
17837 #[test]
17838 fn test_prev_preferences_alarms() {
17839 let mut app = test_app();
17840 app.current_service = Service::CloudWatchAlarms;
17841 app.mode = Mode::ColumnSelector;
17842 app.column_selector_index = 28; app.handle_action(Action::PrevPreferences);
17845 assert_eq!(app.column_selector_index, 22); app.handle_action(Action::PrevPreferences);
17848 assert_eq!(app.column_selector_index, 18); app.handle_action(Action::PrevPreferences);
17851 assert_eq!(app.column_selector_index, 0); app.handle_action(Action::PrevPreferences);
17854 assert_eq!(app.column_selector_index, 28); }
17856
17857 #[test]
17858 fn test_ec2_page_size_in_preferences() {
17859 let mut app = test_app();
17860 app.current_service = Service::Ec2Instances;
17861 app.mode = Mode::ColumnSelector;
17862 app.ec2_state.table.page_size = PageSize::Fifty;
17863
17864 let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
17867 app.handle_action(Action::ToggleColumn);
17868
17869 assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
17870 }
17871
17872 #[test]
17873 fn test_ec2_next_preferences_with_page_size() {
17874 let mut app = test_app();
17875 app.current_service = Service::Ec2Instances;
17876 app.mode = Mode::ColumnSelector;
17877 app.column_selector_index = 0;
17878
17879 let page_size_idx = app.ec2_column_ids.len() + 2;
17880 app.handle_action(Action::NextPreferences);
17881 assert_eq!(app.column_selector_index, page_size_idx);
17882
17883 app.handle_action(Action::NextPreferences);
17884 assert_eq!(app.column_selector_index, 0);
17885 }
17886
17887 #[test]
17888 fn test_ec2_dropdown_next_item() {
17889 let mut app = test_app();
17890 app.current_service = Service::Ec2Instances;
17891 app.mode = Mode::FilterInput;
17892 app.ec2_state.input_focus = EC2_STATE_FILTER;
17893 app.ec2_state.state_filter = Ec2StateFilter::AllStates;
17894
17895 app.handle_action(Action::NextItem);
17896 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17897
17898 app.handle_action(Action::NextItem);
17899 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
17900 }
17901
17902 #[test]
17903 fn test_ec2_dropdown_prev_item() {
17904 let mut app = test_app();
17905 app.current_service = Service::Ec2Instances;
17906 app.mode = Mode::FilterInput;
17907 app.ec2_state.input_focus = EC2_STATE_FILTER;
17908 app.ec2_state.state_filter = Ec2StateFilter::Stopped;
17909
17910 app.handle_action(Action::PrevItem);
17911 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
17912
17913 app.handle_action(Action::PrevItem);
17914 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
17915 }
17916
17917 #[test]
17918 fn test_ec2_dropdown_cycles_with_arrows() {
17919 let mut app = test_app();
17920 app.current_service = Service::Ec2Instances;
17921 app.mode = Mode::FilterInput;
17922 app.ec2_state.input_focus = EC2_STATE_FILTER;
17923 app.ec2_state.state_filter = Ec2StateFilter::Stopping;
17924
17925 app.handle_action(Action::NextItem);
17927 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
17928
17929 app.handle_action(Action::PrevItem);
17931 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
17932 }
17933
17934 #[test]
17935 fn test_collapse_row_ec2_instances() {
17936 let mut app = test_app();
17937 app.current_service = Service::Ec2Instances;
17938 app.ec2_state.table.expanded_item = Some(0);
17939
17940 app.handle_action(Action::CollapseRow);
17941 assert_eq!(app.ec2_state.table.expanded_item, None);
17942 }
17943
17944 #[test]
17945 fn test_collapse_row_ec2_tags() {
17946 let mut app = test_app();
17947 app.current_service = Service::Ec2Instances;
17948 app.ec2_state.current_instance = Some("i-123".to_string());
17949 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
17950 app.ec2_state.tags.expanded_item = Some(1);
17951
17952 app.handle_action(Action::CollapseRow);
17953 assert_eq!(app.ec2_state.tags.expanded_item, None);
17954 }
17955
17956 #[test]
17957 fn test_collapse_row_cloudwatch_log_groups() {
17958 let mut app = test_app();
17959 app.current_service = Service::CloudWatchLogGroups;
17960 app.log_groups_state.log_groups.expanded_item = Some(2);
17961
17962 app.handle_action(Action::CollapseRow);
17963 assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
17964 }
17965
17966 #[test]
17967 fn test_collapse_row_cloudwatch_alarms() {
17968 let mut app = test_app();
17969 app.current_service = Service::CloudWatchAlarms;
17970 app.alarms_state.table.expanded_item = Some(0);
17971
17972 app.handle_action(Action::CollapseRow);
17973 assert_eq!(app.alarms_state.table.expanded_item, None);
17974 }
17975
17976 #[test]
17977 fn test_collapse_row_lambda_functions() {
17978 let mut app = test_app();
17979 app.current_service = Service::LambdaFunctions;
17980 app.lambda_state.table.expanded_item = Some(1);
17981
17982 app.handle_action(Action::CollapseRow);
17983 assert_eq!(app.lambda_state.table.expanded_item, None);
17984 }
17985
17986 #[test]
17987 fn test_collapse_row_cfn_stacks() {
17988 let mut app = test_app();
17989 app.current_service = Service::CloudFormationStacks;
17990 app.cfn_state.table.expanded_item = Some(0);
17991
17992 app.handle_action(Action::CollapseRow);
17993 assert_eq!(app.cfn_state.table.expanded_item, None);
17994 }
17995
17996 #[test]
17997 fn test_collapse_row_cfn_resources() {
17998 let mut app = test_app();
17999 app.current_service = Service::CloudFormationStacks;
18000 app.cfn_state.current_stack = Some("test-stack".to_string());
18001 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
18002 app.cfn_state.resources.expanded_item = Some(2);
18003
18004 app.handle_action(Action::CollapseRow);
18005 assert_eq!(app.cfn_state.resources.expanded_item, None);
18006 }
18007
18008 #[test]
18009 fn test_collapse_row_iam_users() {
18010 let mut app = test_app();
18011 app.current_service = Service::IamUsers;
18012 app.iam_state.users.expanded_item = Some(1);
18013
18014 app.handle_action(Action::CollapseRow);
18015 assert_eq!(app.iam_state.users.expanded_item, None);
18016 }
18017
18018 #[test]
18019 fn test_collapse_row_does_nothing_when_not_expanded() {
18020 let mut app = test_app();
18021 app.current_service = Service::Ec2Instances;
18022 app.ec2_state.table.expanded_item = None;
18023
18024 app.handle_action(Action::CollapseRow);
18025 assert_eq!(app.ec2_state.table.expanded_item, None);
18026 }
18027
18028 #[test]
18029 fn test_s3_collapse_expanded_folder_moves_to_parent() {
18030 let mut app = test_app();
18031 app.current_service = Service::S3Buckets;
18032 app.service_selected = true;
18033 app.mode = Mode::Normal;
18034
18035 app.s3_state.buckets.items = vec![S3Bucket {
18037 name: "bucket1".to_string(),
18038 region: "us-east-1".to_string(),
18039 creation_date: "2024-01-01T00:00:00Z".to_string(),
18040 }];
18041
18042 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18044 app.s3_state.bucket_preview.insert(
18045 "bucket1".to_string(),
18046 vec![S3Object {
18047 key: "folder1/".to_string(),
18048 size: 0,
18049 last_modified: "2024-01-01T00:00:00Z".to_string(),
18050 is_prefix: true,
18051 storage_class: String::new(),
18052 }],
18053 );
18054
18055 app.s3_state
18057 .expanded_prefixes
18058 .insert("folder1/".to_string());
18059 app.s3_state.prefix_preview.insert(
18060 "folder1/".to_string(),
18061 vec![S3Object {
18062 key: "folder1/file.txt".to_string(),
18063 size: 0,
18064 last_modified: "2024-01-01T00:00:00Z".to_string(),
18065 is_prefix: false,
18066 storage_class: String::new(),
18067 }],
18068 );
18069
18070 app.s3_state.selected_row = 1;
18072
18073 app.handle_action(Action::PrevPane);
18075
18076 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
18078 assert_eq!(app.s3_state.selected_row, 0);
18080 }
18081
18082 #[test]
18083 fn test_s3_collapse_hierarchy_level_by_level() {
18084 let mut app = test_app();
18085 app.current_service = Service::S3Buckets;
18086 app.service_selected = true;
18087 app.mode = Mode::Normal;
18088
18089 app.s3_state.buckets.items = vec![S3Bucket {
18091 name: "bucket1".to_string(),
18092 region: "us-east-1".to_string(),
18093 creation_date: "2024-01-01T00:00:00Z".to_string(),
18094 }];
18095
18096 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18098 app.s3_state.bucket_preview.insert(
18099 "bucket1".to_string(),
18100 vec![S3Object {
18101 key: "level1/".to_string(),
18102 size: 0,
18103 last_modified: "2024-01-01T00:00:00Z".to_string(),
18104 is_prefix: true,
18105 storage_class: String::new(),
18106 }],
18107 );
18108
18109 app.s3_state.expanded_prefixes.insert("level1/".to_string());
18111 app.s3_state.prefix_preview.insert(
18112 "level1/".to_string(),
18113 vec![S3Object {
18114 key: "level1/level2/".to_string(),
18115 size: 0,
18116 last_modified: "2024-01-01T00:00:00Z".to_string(),
18117 is_prefix: true,
18118 storage_class: String::new(),
18119 }],
18120 );
18121
18122 app.s3_state
18124 .expanded_prefixes
18125 .insert("level1/level2/".to_string());
18126 app.s3_state.prefix_preview.insert(
18127 "level1/level2/".to_string(),
18128 vec![S3Object {
18129 key: "level1/level2/file.txt".to_string(),
18130 size: 100,
18131 last_modified: "2024-01-01T00:00:00Z".to_string(),
18132 is_prefix: false,
18133 storage_class: String::new(),
18134 }],
18135 );
18136
18137 app.s3_state.selected_row = 3;
18139
18140 app.handle_action(Action::PrevPane);
18142 assert_eq!(app.s3_state.selected_row, 2);
18143
18144 app.handle_action(Action::PrevPane);
18146 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
18147 assert_eq!(app.s3_state.selected_row, 1);
18148
18149 app.handle_action(Action::PrevPane);
18151 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
18152 assert_eq!(app.s3_state.selected_row, 0);
18153
18154 app.handle_action(Action::PrevPane);
18156 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18157 assert_eq!(app.s3_state.selected_row, 0);
18158 }
18159
18160 #[test]
18161 fn test_ec2_instance_detail_tabs_no_preferences() {
18162 let mut app = test_app();
18163 app.current_service = Service::Ec2Instances;
18164 app.service_selected = true;
18165 app.ec2_state.table.expanded_item = Some(0);
18166 app.mode = Mode::Normal;
18167
18168 app.ec2_state.detail_tab = Ec2DetailTab::Details;
18170 app.handle_action(Action::OpenColumnSelector);
18171 assert_eq!(app.mode, Mode::Normal);
18172
18173 app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
18175 app.handle_action(Action::OpenColumnSelector);
18176 assert_eq!(app.mode, Mode::Normal);
18177
18178 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
18180 app.handle_action(Action::OpenColumnSelector);
18181 assert_eq!(app.mode, Mode::Normal);
18182
18183 app.ec2_state.detail_tab = Ec2DetailTab::Security;
18185 app.handle_action(Action::OpenColumnSelector);
18186 assert_eq!(app.mode, Mode::Normal);
18187
18188 app.ec2_state.detail_tab = Ec2DetailTab::Networking;
18190 app.handle_action(Action::OpenColumnSelector);
18191 assert_eq!(app.mode, Mode::Normal);
18192
18193 app.ec2_state.detail_tab = Ec2DetailTab::Storage;
18195 app.handle_action(Action::OpenColumnSelector);
18196 assert_eq!(app.mode, Mode::Normal);
18197
18198 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
18200 app.handle_action(Action::OpenColumnSelector);
18201 assert_eq!(app.mode, Mode::ColumnSelector);
18202 }
18203
18204 #[test]
18205 fn test_ec2_monitoring_ctrl_u_scrolls_up() {
18206 let mut app = test_app();
18210 app.current_service = Service::Ec2Instances;
18211 app.service_selected = true;
18212 app.mode = Mode::Normal;
18213 app.ec2_state.current_instance = Some("i-abc".to_string());
18214 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
18215
18216 app.ec2_state.set_monitoring_scroll(3);
18218 assert_eq!(app.ec2_state.monitoring_scroll(), 3);
18219
18220 app.handle_action(Action::PageUp);
18222 assert_eq!(
18223 app.ec2_state.monitoring_scroll(),
18224 2,
18225 "Ctrl+U must scroll monitoring up by 1"
18226 );
18227
18228 app.handle_action(Action::PageDown);
18230 assert_eq!(
18231 app.ec2_state.monitoring_scroll(),
18232 3,
18233 "Ctrl+D must scroll monitoring down by 1"
18234 );
18235 }
18236
18237 #[test]
18238 fn test_ec2_instance_always_shows_when_state_filter_is_all_states() {
18239 use crate::app::Ec2Instance;
18242 use crate::ui::ec2::{filtered_ec2_instances, StateFilter};
18243 let mut app = test_app();
18244 app.current_service = Service::Ec2Instances;
18245 app.service_selected = true;
18246 app.ec2_state.table.filter.clear();
18247 app.ec2_state.state_filter = StateFilter::AllStates;
18248
18249 app.ec2_state.table.items = vec![Ec2Instance {
18251 instance_id: "i-05f181d4aaabadf5a".to_string(),
18252 state: "running".to_string(),
18253 ..Default::default()
18254 }];
18255
18256 let filtered = filtered_ec2_instances(&app);
18257 assert_eq!(
18258 filtered.len(),
18259 1,
18260 "Instance must appear with AllStates filter and no text filter"
18261 );
18262 assert_eq!(filtered[0].instance_id, "i-05f181d4aaabadf5a");
18263 }
18264
18265 #[test]
18266 fn test_ec2_instance_private_ip_populated_from_load() {
18267 use crate::app::Ec2Instance;
18271 let instance = Ec2Instance {
18272 instance_id: "i-abc".to_string(),
18273 name: "my-server".to_string(),
18274 state: "running".to_string(),
18275 private_dns_name: "ip-10-0-1-5.ec2.internal".to_string(),
18276 private_ip_address: "10.0.1.5".to_string(),
18277 security_group_ids: "sg-abc123".to_string(),
18278 ..Default::default()
18279 };
18280 assert!(
18282 !instance.private_ip_address.is_empty(),
18283 "private_ip_address must be populated"
18284 );
18285 assert!(
18286 !instance.private_dns_name.is_empty(),
18287 "private_dns_name must be populated"
18288 );
18289 }
18290
18291 #[test]
18292 fn test_log_streams_filter_only_updates_when_focused() {
18293 let mut app = test_app();
18294 app.current_service = Service::CloudWatchLogGroups;
18295 app.service_selected = true;
18296 app.view_mode = ViewMode::Detail;
18297 app.mode = Mode::FilterInput;
18298 app.log_groups_state.stream_filter = "test".to_string();
18299
18300 app.log_groups_state.input_focus = InputFocus::Filter;
18302 app.handle_action(Action::FilterInput('x'));
18303 assert_eq!(app.log_groups_state.stream_filter, "testx");
18304
18305 app.log_groups_state.input_focus = InputFocus::Pagination;
18307 app.handle_action(Action::FilterInput('y'));
18308 assert_eq!(app.log_groups_state.stream_filter, "testx"); }
18310
18311 #[test]
18312 fn test_log_streams_backspace_only_updates_when_focused() {
18313 let mut app = test_app();
18314 app.current_service = Service::CloudWatchLogGroups;
18315 app.service_selected = true;
18316 app.view_mode = ViewMode::Detail;
18317 app.mode = Mode::FilterInput;
18318 app.log_groups_state.stream_filter = "test".to_string();
18319
18320 app.log_groups_state.input_focus = InputFocus::Filter;
18322 app.handle_action(Action::FilterBackspace);
18323 assert_eq!(app.log_groups_state.stream_filter, "tes");
18324
18325 app.log_groups_state.input_focus = InputFocus::Pagination;
18327 app.handle_action(Action::FilterBackspace);
18328 assert_eq!(app.log_groups_state.stream_filter, "tes"); }
18330
18331 #[test]
18332 fn test_log_groups_filter_only_updates_when_focused() {
18333 let mut app = test_app();
18334 app.current_service = Service::CloudWatchLogGroups;
18335 app.service_selected = true;
18336 app.view_mode = ViewMode::List;
18337 app.mode = Mode::FilterInput;
18338 app.log_groups_state.log_groups.filter = "test".to_string();
18339
18340 app.log_groups_state.input_focus = InputFocus::Filter;
18342 app.handle_action(Action::FilterInput('x'));
18343 assert_eq!(app.log_groups_state.log_groups.filter, "testx");
18344
18345 app.log_groups_state.input_focus = InputFocus::Pagination;
18347 app.handle_action(Action::FilterInput('y'));
18348 assert_eq!(app.log_groups_state.log_groups.filter, "testx"); }
18350
18351 #[test]
18352 fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
18353 use S3Bucket;
18354 use S3Object;
18355
18356 let mut app = test_app();
18357 app.current_service = Service::S3Buckets;
18358 app.service_selected = true;
18359
18360 app.s3_state.buckets.items = vec![S3Bucket {
18362 name: "test-bucket".to_string(),
18363 region: "us-east-1".to_string(),
18364 creation_date: String::new(),
18365 }];
18366
18367 app.s3_state
18369 .expanded_prefixes
18370 .insert("test-bucket".to_string());
18371 app.s3_state.bucket_preview.insert(
18372 "test-bucket".to_string(),
18373 vec![S3Object {
18374 key: "folder1/".to_string(),
18375 is_prefix: true,
18376 size: 0,
18377 last_modified: String::new(),
18378 storage_class: String::new(),
18379 }],
18380 );
18381
18382 app.s3_state
18384 .expanded_prefixes
18385 .insert("folder1/".to_string());
18386 app.s3_state.prefix_preview.insert(
18387 "folder1/".to_string(),
18388 vec![S3Object {
18389 key: "folder1/folder2/".to_string(),
18390 is_prefix: true,
18391 size: 0,
18392 last_modified: String::new(),
18393 storage_class: String::new(),
18394 }],
18395 );
18396
18397 app.s3_state.selected_row = 2;
18399
18400 app.handle_action(Action::CollapseRow);
18402
18403 assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
18405 assert_eq!(app.s3_state.selected_row, 1);
18407 }
18408
18409 #[test]
18410 fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
18411 use S3Bucket;
18412 use S3Object;
18413
18414 let mut app = test_app();
18415 app.current_service = Service::S3Buckets;
18416 app.service_selected = true;
18417
18418 app.s3_state.buckets.items = vec![S3Bucket {
18420 name: "test-bucket".to_string(),
18421 region: "us-east-1".to_string(),
18422 creation_date: String::new(),
18423 }];
18424
18425 app.s3_state
18427 .expanded_prefixes
18428 .insert("test-bucket".to_string());
18429 app.s3_state.bucket_preview.insert(
18430 "test-bucket".to_string(),
18431 vec![S3Object {
18432 key: "folder1/".to_string(),
18433 is_prefix: true,
18434 size: 0,
18435 last_modified: String::new(),
18436 storage_class: String::new(),
18437 }],
18438 );
18439
18440 app.s3_state
18442 .expanded_prefixes
18443 .insert("folder1/".to_string());
18444 app.s3_state.prefix_preview.insert(
18445 "folder1/".to_string(),
18446 vec![S3Object {
18447 key: "folder1/file.txt".to_string(),
18448 is_prefix: false,
18449 size: 100,
18450 last_modified: String::new(),
18451 storage_class: String::new(),
18452 }],
18453 );
18454
18455 app.s3_state.selected_row = 1;
18457
18458 app.handle_action(Action::CollapseRow);
18460
18461 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
18463 assert_eq!(app.s3_state.selected_row, 0);
18465 }
18466
18467 #[test]
18468 fn test_log_streams_pagination_limits_table_content() {
18469 let mut app = test_app();
18470 app.current_service = Service::CloudWatchLogGroups;
18471 app.service_selected = true;
18472 app.view_mode = ViewMode::Detail;
18473
18474 app.log_groups_state.log_streams = (0..50)
18476 .map(|i| rusticity_core::LogStream {
18477 name: format!("stream-{}", i),
18478 creation_time: None,
18479 last_event_time: None,
18480 })
18481 .collect();
18482
18483 app.log_groups_state.stream_page_size = 10;
18485 app.log_groups_state.stream_current_page = 0;
18486
18487 assert_eq!(app.log_groups_state.stream_page_size, 10);
18490 assert_eq!(app.log_groups_state.stream_current_page, 0);
18491
18492 app.log_groups_state.stream_current_page = 1;
18494 assert_eq!(app.log_groups_state.stream_current_page, 1);
18495 }
18496
18497 #[test]
18498 fn test_log_streams_page_size_change_resets_page() {
18499 let mut app = test_app();
18500 app.current_service = Service::CloudWatchLogGroups;
18501 app.service_selected = true;
18502 app.view_mode = ViewMode::Detail;
18503 app.mode = Mode::ColumnSelector;
18504
18505 app.log_groups_state.stream_page_size = 10;
18506 app.log_groups_state.stream_current_page = 3;
18507
18508 app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; app.handle_action(Action::ToggleColumn);
18511
18512 assert_eq!(app.log_groups_state.stream_page_size, 25);
18513 assert_eq!(app.log_groups_state.stream_current_page, 0);
18514 }
18515
18516 #[test]
18517 fn test_s3_objects_expanded_rows_stay_visible() {
18518 use S3Object;
18519
18520 let mut app = test_app();
18521 app.current_service = Service::S3Buckets;
18522 app.service_selected = true;
18523 app.mode = Mode::Normal;
18524 app.s3_state.current_bucket = Some("test-bucket".to_string());
18525
18526 app.s3_state.objects = vec![S3Object {
18528 key: "folder1/".to_string(),
18529 is_prefix: true,
18530 size: 0,
18531 last_modified: String::new(),
18532 storage_class: String::new(),
18533 }];
18534
18535 app.s3_state
18537 .expanded_prefixes
18538 .insert("folder1/".to_string());
18539 app.s3_state.prefix_preview.insert(
18540 "folder1/".to_string(),
18541 (0..20)
18542 .map(|i| S3Object {
18543 key: format!("folder1/file{}.txt", i),
18544 is_prefix: false,
18545 size: 100,
18546 last_modified: String::new(),
18547 storage_class: String::new(),
18548 })
18549 .collect(),
18550 );
18551
18552 app.s3_state.object_visible_rows.set(10);
18554 app.s3_state.object_scroll_offset = 0;
18555 app.s3_state.selected_object = 0; for i in 1..=20 {
18559 app.handle_action(Action::NextItem);
18560 assert_eq!(app.s3_state.selected_object, i);
18561
18562 let visible_start = app.s3_state.object_scroll_offset;
18564 let visible_end = visible_start + app.s3_state.object_visible_rows.get();
18565 assert!(
18566 app.s3_state.selected_object >= visible_start
18567 && app.s3_state.selected_object < visible_end,
18568 "Selection {} should be visible in range [{}, {})",
18569 app.s3_state.selected_object,
18570 visible_start,
18571 visible_end
18572 );
18573 }
18574 }
18575
18576 #[test]
18577 fn test_s3_bucket_error_rows_counted_in_total() {
18578 use S3Bucket;
18579
18580 let mut app = test_app();
18581 app.current_service = Service::S3Buckets;
18582 app.service_selected = true;
18583
18584 app.s3_state.buckets.items = vec![
18586 S3Bucket {
18587 name: "bucket1".to_string(),
18588 region: "us-east-1".to_string(),
18589 creation_date: String::new(),
18590 },
18591 S3Bucket {
18592 name: "bucket2".to_string(),
18593 region: "us-east-1".to_string(),
18594 creation_date: String::new(),
18595 },
18596 ];
18597
18598 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18600 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();
18601 app.s3_state
18602 .bucket_errors
18603 .insert("bucket1".to_string(), long_error.clone());
18604
18605 let total = app.calculate_total_bucket_rows();
18607
18608 let error_rows = long_error.len().div_ceil(120);
18610 assert_eq!(total, 2 + error_rows);
18611 }
18612
18613 #[test]
18614 fn test_s3_bucket_with_error_can_be_collapsed() {
18615 use S3Bucket;
18616
18617 let mut app = test_app();
18618 app.current_service = Service::S3Buckets;
18619 app.service_selected = true;
18620 app.mode = Mode::Normal;
18621
18622 app.s3_state.buckets.items = vec![S3Bucket {
18624 name: "bucket1".to_string(),
18625 region: "us-east-1".to_string(),
18626 creation_date: String::new(),
18627 }];
18628
18629 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18631 let error = "service error: PermanentRedirect".to_string();
18632 app.s3_state
18633 .bucket_errors
18634 .insert("bucket1".to_string(), error);
18635
18636 app.s3_state.selected_row = 0;
18638
18639 app.handle_action(Action::CollapseRow);
18641
18642 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18644 assert_eq!(app.s3_state.selected_row, 0);
18646 }
18647
18648 #[test]
18649 fn test_s3_bucket_collapse_on_bucket_row() {
18650 use S3Bucket;
18651
18652 let mut app = test_app();
18653 app.current_service = Service::S3Buckets;
18654 app.service_selected = true;
18655 app.mode = Mode::Normal;
18656
18657 app.s3_state.buckets.items = vec![S3Bucket {
18659 name: "bucket1".to_string(),
18660 region: "us-east-1".to_string(),
18661 creation_date: String::new(),
18662 }];
18663
18664 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18666 let error = "service error: PermanentRedirect".to_string();
18667 app.s3_state
18668 .bucket_errors
18669 .insert("bucket1".to_string(), error);
18670
18671 app.s3_state.selected_row = 0;
18673
18674 app.handle_action(Action::CollapseRow);
18676
18677 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18679 assert_eq!(app.s3_state.selected_row, 0);
18681 }
18682
18683 #[test]
18684 fn test_s3_bucket_collapse_adjusts_scroll_offset() {
18685 use S3Bucket;
18686
18687 let mut app = test_app();
18688 app.current_service = Service::S3Buckets;
18689 app.service_selected = true;
18690 app.mode = Mode::Normal;
18691
18692 app.s3_state.buckets.items = (0..20)
18694 .map(|i| S3Bucket {
18695 name: format!("bucket{}", i),
18696 region: "us-east-1".to_string(),
18697 creation_date: String::new(),
18698 })
18699 .collect();
18700
18701 app.s3_state
18703 .expanded_prefixes
18704 .insert("bucket10".to_string());
18705 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();
18706 app.s3_state
18707 .bucket_errors
18708 .insert("bucket10".to_string(), long_error.clone());
18709
18710 app.s3_state.bucket_visible_rows.set(10);
18712 app.s3_state.bucket_scroll_offset = 10; app.s3_state.selected_row = 10;
18716
18717 app.handle_action(Action::CollapseRow);
18719
18720 assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
18722 assert_eq!(app.s3_state.selected_row, 10);
18724 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
18726 assert!(
18727 app.s3_state.selected_row
18728 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
18729 );
18730 }
18731
18732 #[test]
18733 fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
18734 use S3Bucket;
18735
18736 let mut app = test_app();
18737 app.current_service = Service::S3Buckets;
18738 app.service_selected = true;
18739 app.mode = Mode::Normal;
18740
18741 app.s3_state.buckets.items = vec![
18743 S3Bucket {
18744 name: "bucket1".to_string(),
18745 region: "us-east-1".to_string(),
18746 creation_date: String::new(),
18747 },
18748 S3Bucket {
18749 name: "bucket2".to_string(),
18750 region: "us-east-1".to_string(),
18751 creation_date: String::new(),
18752 },
18753 S3Bucket {
18754 name: "bucket3".to_string(),
18755 region: "us-east-1".to_string(),
18756 creation_date: String::new(),
18757 },
18758 ];
18759
18760 app.s3_state.expanded_prefixes.insert("bucket2".to_string());
18762 app.s3_state.bucket_preview.insert(
18763 "bucket2".to_string(),
18764 vec![
18765 S3Object {
18766 key: "folder1/".to_string(),
18767 is_prefix: true,
18768 size: 0,
18769 last_modified: String::new(),
18770 storage_class: String::new(),
18771 },
18772 S3Object {
18773 key: "file1.txt".to_string(),
18774 is_prefix: false,
18775 size: 100,
18776 last_modified: String::new(),
18777 storage_class: String::new(),
18778 },
18779 ],
18780 );
18781
18782 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
18784 let error = "service error: PermanentRedirect".to_string();
18785 app.s3_state
18786 .bucket_errors
18787 .insert("bucket3".to_string(), error);
18788
18789 app.s3_state.bucket_visible_rows.set(10);
18791 app.s3_state.bucket_scroll_offset = 0;
18792
18793 app.s3_state.selected_row = 3;
18795
18796 app.handle_action(Action::CollapseRow);
18798
18799 assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
18801 assert_eq!(app.s3_state.selected_row, 1);
18803 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
18805 assert!(
18806 app.s3_state.selected_row
18807 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
18808 );
18809 }
18810
18811 #[test]
18812 fn test_s3_collapse_bucket_with_error() {
18813 use S3Bucket;
18814
18815 let mut app = test_app();
18816 app.current_service = Service::S3Buckets;
18817 app.service_selected = true;
18818 app.mode = Mode::Normal;
18819
18820 app.s3_state.buckets.items = vec![
18821 S3Bucket {
18822 name: "bucket1".to_string(),
18823 region: "us-east-1".to_string(),
18824 creation_date: String::new(),
18825 },
18826 S3Bucket {
18827 name: "bucket2".to_string(),
18828 region: "us-east-1".to_string(),
18829 creation_date: String::new(),
18830 },
18831 ];
18832
18833 let error = "service error: unhandled error (PermanentRedirect)".to_string();
18834 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18835 app.s3_state
18836 .bucket_errors
18837 .insert("bucket1".to_string(), error);
18838
18839 app.s3_state.bucket_visible_rows.set(10);
18840 app.s3_state.bucket_scroll_offset = 0;
18841
18842 app.s3_state.selected_row = 0;
18844
18845 app.handle_action(Action::CollapseRow);
18847
18848 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18850 assert_eq!(app.s3_state.selected_row, 0);
18851 }
18852
18853 #[test]
18854 fn test_s3_collapse_row_with_multiple_error_buckets() {
18855 use S3Bucket;
18856
18857 let mut app = test_app();
18858 app.current_service = Service::S3Buckets;
18859 app.service_selected = true;
18860 app.mode = Mode::Normal;
18861
18862 app.s3_state.buckets.items = vec![
18863 S3Bucket {
18864 name: "bucket1".to_string(),
18865 region: "us-east-1".to_string(),
18866 creation_date: String::new(),
18867 },
18868 S3Bucket {
18869 name: "bucket2".to_string(),
18870 region: "us-east-1".to_string(),
18871 creation_date: String::new(),
18872 },
18873 S3Bucket {
18874 name: "bucket3".to_string(),
18875 region: "us-east-1".to_string(),
18876 creation_date: String::new(),
18877 },
18878 ];
18879
18880 let error = "service error: unhandled error (PermanentRedirect)".to_string();
18881
18882 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18884 app.s3_state
18885 .bucket_errors
18886 .insert("bucket1".to_string(), error.clone());
18887
18888 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
18890 app.s3_state
18891 .bucket_errors
18892 .insert("bucket3".to_string(), error.clone());
18893
18894 app.s3_state.bucket_visible_rows.set(30);
18895 app.s3_state.bucket_scroll_offset = 0;
18896
18897 app.s3_state.selected_row = 2;
18902
18903 app.handle_action(Action::CollapseRow);
18904
18905 assert!(
18907 !app.s3_state.expanded_prefixes.contains("bucket3"),
18908 "bucket3 should be collapsed"
18909 );
18910 assert!(
18911 app.s3_state.expanded_prefixes.contains("bucket1"),
18912 "bucket1 should still be expanded"
18913 );
18914 assert_eq!(app.s3_state.selected_row, 2);
18915 }
18916
18917 #[test]
18918 fn test_s3_collapse_row_nested_only_collapses_one_level() {
18919 use S3Bucket;
18920
18921 let mut app = test_app();
18922 app.current_service = Service::S3Buckets;
18923 app.service_selected = true;
18924 app.mode = Mode::Normal;
18925
18926 app.s3_state.buckets.items = vec![S3Bucket {
18927 name: "bucket1".to_string(),
18928 region: "us-east-1".to_string(),
18929 creation_date: String::new(),
18930 }];
18931
18932 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18934 app.s3_state.bucket_preview.insert(
18935 "bucket1".to_string(),
18936 vec![S3Object {
18937 key: "level1/".to_string(),
18938 size: 0,
18939 last_modified: "2024-01-01T00:00:00Z".to_string(),
18940 is_prefix: true,
18941 storage_class: String::new(),
18942 }],
18943 );
18944
18945 app.s3_state.expanded_prefixes.insert("level1/".to_string());
18947 app.s3_state.prefix_preview.insert(
18948 "level1/".to_string(),
18949 vec![S3Object {
18950 key: "level1/level2/".to_string(),
18951 size: 0,
18952 last_modified: "2024-01-01T00:00:00Z".to_string(),
18953 is_prefix: true,
18954 storage_class: String::new(),
18955 }],
18956 );
18957
18958 app.s3_state
18960 .expanded_prefixes
18961 .insert("level1/level2/".to_string());
18962 app.s3_state.prefix_preview.insert(
18963 "level1/level2/".to_string(),
18964 vec![S3Object {
18965 key: "level1/level2/file.txt".to_string(),
18966 size: 100,
18967 last_modified: "2024-01-01T00:00:00Z".to_string(),
18968 is_prefix: false,
18969 storage_class: String::new(),
18970 }],
18971 );
18972
18973 app.s3_state.bucket_visible_rows.set(10);
18974
18975 app.s3_state.selected_row = 2;
18977
18978 app.handle_action(Action::CollapseRow);
18980
18981 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
18983 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
18985 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
18987 assert_eq!(app.s3_state.selected_row, 1);
18989 }
18990
18991 #[test]
18992 fn test_s3_collapse_row_deeply_nested_file() {
18993 use S3Bucket;
18994
18995 let mut app = test_app();
18996 app.current_service = Service::S3Buckets;
18997 app.service_selected = true;
18998 app.mode = Mode::Normal;
18999
19000 app.s3_state.buckets.items = vec![S3Bucket {
19001 name: "bucket1".to_string(),
19002 region: "us-east-1".to_string(),
19003 creation_date: String::new(),
19004 }];
19005
19006 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19008 app.s3_state.bucket_preview.insert(
19009 "bucket1".to_string(),
19010 vec![S3Object {
19011 key: "level1/".to_string(),
19012 size: 0,
19013 last_modified: "2024-01-01T00:00:00Z".to_string(),
19014 is_prefix: true,
19015 storage_class: String::new(),
19016 }],
19017 );
19018
19019 app.s3_state.expanded_prefixes.insert("level1/".to_string());
19021 app.s3_state.prefix_preview.insert(
19022 "level1/".to_string(),
19023 vec![S3Object {
19024 key: "level1/level2/".to_string(),
19025 size: 0,
19026 last_modified: "2024-01-01T00:00:00Z".to_string(),
19027 is_prefix: true,
19028 storage_class: String::new(),
19029 }],
19030 );
19031
19032 app.s3_state
19034 .expanded_prefixes
19035 .insert("level1/level2/".to_string());
19036 app.s3_state.prefix_preview.insert(
19037 "level1/level2/".to_string(),
19038 vec![S3Object {
19039 key: "level1/level2/file.txt".to_string(),
19040 size: 100,
19041 last_modified: "2024-01-01T00:00:00Z".to_string(),
19042 is_prefix: false,
19043 storage_class: String::new(),
19044 }],
19045 );
19046
19047 app.s3_state.bucket_visible_rows.set(10);
19048
19049 app.s3_state.selected_row = 3;
19051
19052 app.handle_action(Action::CollapseRow);
19054
19055 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
19057 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
19058 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
19059 assert_eq!(app.s3_state.selected_row, 2);
19061 }
19062
19063 #[test]
19064 fn test_s3_bucket_pagination_adjusts_scroll() {
19065 use S3Bucket;
19066
19067 let mut app = test_app();
19068 app.current_service = Service::S3Buckets;
19069 app.service_selected = true;
19070 app.mode = Mode::Normal;
19071
19072 app.s3_state.buckets.items = (0..150)
19074 .map(|i| S3Bucket {
19075 name: format!("bucket{:03}", i),
19076 region: "us-east-1".to_string(),
19077 creation_date: String::new(),
19078 })
19079 .collect();
19080
19081 app.s3_state.bucket_visible_rows.set(20);
19082 app.s3_state.selected_row = 0;
19083 app.s3_state.bucket_scroll_offset = 0;
19084
19085 app.go_to_page(2);
19087
19088 assert_eq!(app.s3_state.selected_row, 50);
19089 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
19091
19092 app.go_to_page(3);
19094
19095 assert_eq!(app.s3_state.selected_row, 100);
19096 assert_eq!(app.s3_state.bucket_scroll_offset, 100);
19097
19098 app.go_to_page(1);
19100
19101 assert_eq!(app.s3_state.selected_row, 0);
19102 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
19103 }
19104
19105 #[test]
19106 fn test_s3_bucket_pagination_uses_page_size() {
19107 use S3Bucket;
19108
19109 let mut app = test_app();
19110 app.current_service = Service::S3Buckets;
19111 app.service_selected = true;
19112 app.mode = Mode::Normal;
19113
19114 app.s3_state.buckets.items = (0..100)
19116 .map(|i| S3Bucket {
19117 name: format!("bucket{:03}", i),
19118 region: "us-east-1".to_string(),
19119 creation_date: String::new(),
19120 })
19121 .collect();
19122
19123 app.s3_state.bucket_visible_rows.set(20);
19124 app.s3_state.selected_row = 0;
19125
19126 assert_eq!(app.s3_state.buckets.page_size.value(), 50);
19128
19129 app.go_to_page(2);
19131 assert_eq!(app.s3_state.selected_row, 50);
19132 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
19133
19134 app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
19136 assert_eq!(app.s3_state.buckets.page_size.value(), 25);
19137
19138 app.go_to_page(2);
19140 assert_eq!(app.s3_state.selected_row, 25);
19141 assert_eq!(app.s3_state.bucket_scroll_offset, 25);
19142 }
19143
19144 #[test]
19145 fn test_s3_bucket_page_size_limits_visible_rows() {
19146 use S3Bucket;
19147
19148 let mut app = test_app();
19149 app.current_service = Service::S3Buckets;
19150 app.service_selected = true;
19151 app.mode = Mode::Normal;
19152
19153 app.s3_state.buckets.items = (0..100)
19155 .map(|i| S3Bucket {
19156 name: format!("bucket{:03}", i),
19157 region: "us-east-1".to_string(),
19158 creation_date: String::new(),
19159 })
19160 .collect();
19161
19162 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19164 assert_eq!(app.s3_state.buckets.page_size.value(), 10);
19165
19166 let total_rows = app.calculate_total_bucket_rows();
19168 assert!(total_rows >= 10, "Should have at least 10 rows");
19172 }
19173
19174 #[test]
19175 fn test_s3_bucket_tab_cycling_in_filter() {
19176 use crate::common::InputFocus;
19177
19178 let mut app = test_app();
19179 app.current_service = Service::S3Buckets;
19180 app.mode = Mode::FilterInput;
19181
19182 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
19184
19185 app.handle_action(Action::NextFilterFocus);
19187 assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
19188
19189 app.handle_action(Action::NextFilterFocus);
19191 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
19192 }
19193
19194 #[test]
19195 fn test_s3_bucket_pagination_navigation_with_arrows() {
19196 use S3Bucket;
19197
19198 let mut app = test_app();
19199 app.current_service = Service::S3Buckets;
19200 app.mode = Mode::FilterInput;
19201
19202 app.s3_state.buckets.items = (0..100)
19204 .map(|i| S3Bucket {
19205 name: format!("bucket{:03}", i),
19206 region: "us-east-1".to_string(),
19207 creation_date: String::new(),
19208 })
19209 .collect();
19210
19211 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19212 app.s3_state.selected_row = 0;
19213
19214 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
19216
19217 app.handle_action(Action::NextItem);
19219 assert_eq!(app.s3_state.selected_row, 10);
19220
19221 app.handle_action(Action::NextItem);
19223 assert_eq!(app.s3_state.selected_row, 20);
19224
19225 app.handle_action(Action::PrevItem);
19227 assert_eq!(app.s3_state.selected_row, 10);
19228 }
19229
19230 #[test]
19231 fn test_s3_bucket_go_to_page_shows_correct_buckets() {
19232 use S3Bucket;
19233
19234 let mut app = test_app();
19235 app.current_service = Service::S3Buckets;
19236 app.service_selected = true;
19237 app.mode = Mode::Normal;
19238
19239 app.s3_state.buckets.items = (0..100)
19241 .map(|i| S3Bucket {
19242 name: format!("bucket{:03}", i),
19243 region: "us-east-1".to_string(),
19244 creation_date: String::new(),
19245 })
19246 .collect();
19247
19248 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19249
19250 app.go_to_page(2);
19252 assert_eq!(app.s3_state.selected_row, 10);
19253
19254 app.go_to_page(5);
19256 assert_eq!(app.s3_state.selected_row, 40);
19257 }
19258
19259 #[test]
19260 fn test_s3_bucket_left_right_arrows_change_pages() {
19261 use S3Bucket;
19262
19263 let mut app = test_app();
19264 app.current_service = Service::S3Buckets;
19265 app.mode = Mode::FilterInput;
19266
19267 app.s3_state.buckets.items = (0..100)
19269 .map(|i| S3Bucket {
19270 name: format!("bucket{:03}", i),
19271 region: "us-east-1".to_string(),
19272 creation_date: String::new(),
19273 })
19274 .collect();
19275
19276 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19277 app.s3_state.selected_row = 0;
19278 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
19279
19280 app.handle_action(Action::PageDown);
19282 assert_eq!(app.s3_state.selected_row, 10);
19283
19284 app.handle_action(Action::PageDown);
19286 assert_eq!(app.s3_state.selected_row, 20);
19287
19288 app.handle_action(Action::PageUp);
19290 assert_eq!(app.s3_state.selected_row, 10);
19291 }
19292
19293 #[test]
19294 fn test_s3_bucket_preview_uses_bucket_region_not_config_region() {
19295 use S3Bucket;
19303 let mut app = test_app();
19304 app.config.region = "ap-southeast-3".to_string(); app.s3_state.buckets.items = vec![S3Bucket {
19308 name: "my-bucket".to_string(),
19309 region: "us-east-1".to_string(), creation_date: String::new(),
19311 }];
19312
19313 let bucket = app
19315 .s3_state
19316 .buckets
19317 .items
19318 .iter()
19319 .find(|b| b.name == "my-bucket")
19320 .unwrap();
19321 let effective_region = if bucket.region.is_empty() {
19322 app.config.region.as_str()
19323 } else {
19324 bucket.region.as_str()
19325 };
19326 assert_eq!(
19327 effective_region, "us-east-1",
19328 "load_bucket_preview must use bucket's region, not app config region"
19329 );
19330 assert_ne!(
19331 effective_region, "ap-southeast-3",
19332 "Must NOT fall back to configured region for cross-region buckets"
19333 );
19334 }
19335
19336 #[test]
19337 fn test_s3_scroll_follows_selection_after_bucket_expand() {
19338 use S3Bucket;
19339 use S3Object;
19340 let mut app = test_app();
19343 app.current_service = Service::S3Buckets;
19344 app.service_selected = true;
19345 app.mode = Mode::Normal;
19346
19347 app.s3_state.buckets.items = (0..5)
19349 .map(|i| S3Bucket {
19350 name: format!("bucket{}", i),
19351 region: "us-east-1".to_string(),
19352 creation_date: String::new(),
19353 })
19354 .collect();
19355
19356 app.s3_state.bucket_visible_rows = std::cell::Cell::new(3);
19357
19358 app.s3_state.selected_row = 2;
19360 app.s3_state.bucket_scroll_offset = 0;
19361
19362 app.s3_state.bucket_preview.insert(
19364 "bucket2".to_string(),
19365 vec![
19366 S3Object {
19367 key: "file1.txt".to_string(),
19368 is_prefix: false,
19369 size: 100,
19370 last_modified: String::new(),
19371 storage_class: String::new(),
19372 },
19373 S3Object {
19374 key: "file2.txt".to_string(),
19375 is_prefix: false,
19376 size: 200,
19377 last_modified: String::new(),
19378 storage_class: String::new(),
19379 },
19380 ],
19381 );
19382
19383 app.handle_action(Action::CollapseRow); app.handle_action(Action::NextPane); let visible = app.s3_state.bucket_visible_rows.get();
19391 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
19392 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
19393 assert!(
19394 in_view,
19395 "selected_row {} must be visible in [{}, {})",
19396 app.s3_state.selected_row,
19397 app.s3_state.bucket_scroll_offset,
19398 app.s3_state.bucket_scroll_offset + visible
19399 );
19400 }
19401
19402 #[test]
19403 fn test_s3_expand_without_preview_does_not_move_selection() {
19404 use S3Bucket;
19408 let mut app = test_app();
19409 app.current_service = Service::S3Buckets;
19410 app.service_selected = true;
19411 app.mode = Mode::Normal;
19412
19413 app.s3_state.buckets.items = vec![
19414 S3Bucket {
19415 name: "bucket0".to_string(),
19416 region: "us-east-1".to_string(),
19417 creation_date: String::new(),
19418 },
19419 S3Bucket {
19420 name: "bucket1".to_string(),
19421 region: "us-east-1".to_string(),
19422 creation_date: String::new(),
19423 },
19424 ];
19425 app.s3_state.selected_row = 0; app.handle_action(Action::NextPane); assert_eq!(
19432 app.s3_state.selected_row, 0,
19433 "Selection must not move before preview is loaded"
19434 );
19435 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
19437 assert!(app.s3_state.buckets.loading);
19438 }
19439
19440 #[test]
19441 fn test_s3_expand_with_preview_moves_to_first_child() {
19442 use crate::s3::Object as S3Object;
19444 use S3Bucket;
19445 let mut app = test_app();
19446 app.current_service = Service::S3Buckets;
19447 app.service_selected = true;
19448 app.mode = Mode::Normal;
19449
19450 app.s3_state.buckets.items = vec![
19451 S3Bucket {
19452 name: "bucket0".to_string(),
19453 region: "us-east-1".to_string(),
19454 creation_date: String::new(),
19455 },
19456 S3Bucket {
19457 name: "bucket1".to_string(),
19458 region: "us-east-1".to_string(),
19459 creation_date: String::new(),
19460 },
19461 ];
19462 app.s3_state.selected_row = 0;
19463 app.s3_state.bucket_preview.insert(
19464 "bucket0".to_string(),
19465 vec![S3Object {
19466 key: "file.txt".to_string(),
19467 is_prefix: false,
19468 size: 0,
19469 last_modified: String::new(),
19470 storage_class: String::new(),
19471 }],
19472 );
19473 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19474
19475 app.handle_action(Action::NextPane);
19477 assert_eq!(app.s3_state.selected_row, 1, "Should move to first child");
19478 }
19479
19480 #[test]
19481 fn test_s3_expand_then_load_then_right_goes_to_first_child_not_sibling() {
19482 use crate::s3::Object as S3Object;
19486 use S3Bucket;
19487 let mut app = test_app();
19488 app.current_service = Service::S3Buckets;
19489 app.service_selected = true;
19490 app.mode = Mode::Normal;
19491 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19492
19493 app.s3_state.buckets.items = vec![
19494 S3Bucket {
19495 name: "bucket0".to_string(),
19496 region: "us-east-1".to_string(),
19497 creation_date: String::new(),
19498 },
19499 S3Bucket {
19500 name: "bucket1".to_string(),
19501 region: "us-east-1".to_string(),
19502 creation_date: String::new(),
19503 },
19504 ];
19505 app.s3_state.selected_row = 0;
19506
19507 app.handle_action(Action::NextPane);
19509 assert_eq!(
19510 app.s3_state.selected_row, 0,
19511 "Must stay on bucket0 while loading"
19512 );
19513 assert!(app.s3_state.buckets.loading);
19514
19515 app.s3_state.bucket_preview.insert(
19517 "bucket0".to_string(),
19518 vec![S3Object {
19519 key: "file.txt".to_string(),
19520 is_prefix: false,
19521 size: 0,
19522 last_modified: String::new(),
19523 storage_class: String::new(),
19524 }],
19525 );
19526 app.s3_state.buckets.loading = false;
19527
19528 app.handle_action(Action::NextPane);
19530 assert_eq!(
19531 app.s3_state.selected_row, 1,
19532 "After preview loads, right arrow must select first child (row 1), not sibling (row 2)"
19533 );
19534 }
19535
19536 #[test]
19537 fn test_s3_first_expand_never_moves_selection_before_children_visible() {
19538 use S3Bucket;
19542 let mut app = test_app();
19543 app.current_service = Service::S3Buckets;
19544 app.service_selected = true;
19545 app.mode = Mode::Normal;
19546 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19547
19548 app.s3_state.buckets.items = vec![
19549 S3Bucket {
19550 name: "bucket0".to_string(),
19551 region: "us-east-1".to_string(),
19552 creation_date: String::new(),
19553 },
19554 S3Bucket {
19555 name: "bucket1".to_string(),
19556 region: "us-east-1".to_string(),
19557 creation_date: String::new(),
19558 },
19559 ];
19560 app.s3_state.selected_row = 0;
19562
19563 app.handle_action(Action::ExpandRow);
19565 assert_eq!(
19566 app.s3_state.selected_row, 0,
19567 "Selection must stay on bucket0 until children are visible"
19568 );
19569 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
19570 assert!(app.s3_state.buckets.loading);
19571 }
19572
19573 #[test]
19574 fn test_s3_after_preview_loads_selection_moves_to_first_child() {
19575 use crate::s3::Object as S3Object;
19578 use S3Bucket;
19579 let mut app = test_app();
19580 app.current_service = Service::S3Buckets;
19581 app.service_selected = true;
19582 app.mode = Mode::Normal;
19583 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19584 app.s3_state.buckets.items = vec![
19585 S3Bucket {
19586 name: "b0".to_string(),
19587 region: "us-east-1".to_string(),
19588 creation_date: String::new(),
19589 },
19590 S3Bucket {
19591 name: "b1".to_string(),
19592 region: "us-east-1".to_string(),
19593 creation_date: String::new(),
19594 },
19595 ];
19596 app.s3_state.selected_row = 0;
19597
19598 app.handle_action(Action::ExpandRow);
19600 assert_eq!(
19601 app.s3_state.selected_row, 0,
19602 "Must stay on bucket while loading"
19603 );
19604
19605 app.s3_state.bucket_preview.insert(
19607 "b0".to_string(),
19608 vec![S3Object {
19609 key: "f.txt".to_string(),
19610 is_prefix: false,
19611 size: 0,
19612 last_modified: String::new(),
19613 storage_class: String::new(),
19614 }],
19615 );
19616 app.after_bucket_preview_loaded("b0");
19618 assert_eq!(
19619 app.s3_state.selected_row, 1,
19620 "After preview loads, selection must advance to first child (row 1)"
19621 );
19622 }
19623
19624 #[test]
19625 fn test_s3_right_arrow_on_expanded_loaded_bucket_enters_first_child() {
19626 use crate::s3::Object as S3Object;
19629 use S3Bucket;
19630 let mut app = test_app();
19631 app.current_service = Service::S3Buckets;
19632 app.service_selected = true;
19633 app.mode = Mode::Normal;
19634 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19635 app.s3_state.buckets.items = vec![
19636 S3Bucket {
19637 name: "b0".to_string(),
19638 region: "us-east-1".to_string(),
19639 creation_date: String::new(),
19640 },
19641 S3Bucket {
19642 name: "b1".to_string(),
19643 region: "us-east-1".to_string(),
19644 creation_date: String::new(),
19645 },
19646 ];
19647 app.s3_state.expanded_prefixes.insert("b0".to_string());
19648 app.s3_state.bucket_preview.insert(
19649 "b0".to_string(),
19650 vec![S3Object {
19651 key: "f.txt".to_string(),
19652 is_prefix: false,
19653 size: 0,
19654 last_modified: String::new(),
19655 storage_class: String::new(),
19656 }],
19657 );
19658 app.s3_state.selected_row = 0;
19659
19660 app.handle_action(Action::ExpandRow);
19661
19662 assert!(
19664 app.s3_state.expanded_prefixes.contains("b0"),
19665 "Must stay expanded"
19666 );
19667 assert_eq!(app.s3_state.selected_row, 1, "Must move to first child");
19668 }
19669
19670 #[test]
19671 fn test_s3_second_expand_press_collapses_when_already_expanded_with_children() {
19672 use crate::s3::Object as S3Object;
19675 use S3Bucket;
19676 let mut app = test_app();
19677 app.current_service = Service::S3Buckets;
19678 app.service_selected = true;
19679 app.mode = Mode::Normal;
19680 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19681 app.s3_state.buckets.items = vec![S3Bucket {
19682 name: "bucket0".to_string(),
19683 region: "us-east-1".to_string(),
19684 creation_date: String::new(),
19685 }];
19686 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
19687 app.s3_state.bucket_preview.insert(
19688 "bucket0".to_string(),
19689 vec![S3Object {
19690 key: "file.txt".to_string(),
19691 is_prefix: false,
19692 size: 0,
19693 last_modified: String::new(),
19694 storage_class: String::new(),
19695 }],
19696 );
19697 app.s3_state.selected_row = 0;
19698
19699 app.handle_action(Action::CollapseRow);
19701 assert!(
19702 !app.s3_state.expanded_prefixes.contains("bucket0"),
19703 "Left arrow must collapse the bucket"
19704 );
19705 assert_eq!(app.s3_state.selected_row, 0);
19706 }
19707
19708 #[test]
19709 fn test_s3_expand_row_index_correct_when_prior_bucket_has_error() {
19710 use S3Bucket;
19716 let mut app = test_app();
19717 app.current_service = Service::S3Buckets;
19718 app.service_selected = true;
19719 app.mode = Mode::Normal;
19720 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19721
19722 app.s3_state.buckets.items = vec![
19723 S3Bucket {
19724 name: "bucket0".to_string(),
19725 region: "us-east-1".to_string(),
19726 creation_date: String::new(),
19727 },
19728 S3Bucket {
19729 name: "bucket1".to_string(),
19730 region: "us-east-1".to_string(),
19731 creation_date: String::new(),
19732 },
19733 S3Bucket {
19734 name: "bucket2".to_string(),
19735 region: "us-east-1".to_string(),
19736 creation_date: String::new(),
19737 },
19738 ];
19739
19740 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
19742 app.s3_state
19743 .bucket_errors
19744 .insert("bucket0".to_string(), "AccessDenied".to_string());
19745
19746 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
19749 assert_eq!(
19750 total, 4,
19751 "Should be 4 rows: bucket0 + error + bucket1 + bucket2"
19752 );
19753
19754 app.s3_state.selected_row = 3;
19756
19757 use crate::s3::Object as S3Object;
19759 app.s3_state.bucket_preview.insert(
19760 "bucket2".to_string(),
19761 vec![S3Object {
19762 key: "file.txt".to_string(),
19763 is_prefix: false,
19764 size: 0,
19765 last_modified: String::new(),
19766 storage_class: String::new(),
19767 }],
19768 );
19769
19770 app.handle_action(Action::NextPane);
19771
19772 assert!(
19774 app.s3_state.expanded_prefixes.contains("bucket2"),
19775 "Must expand bucket2, not bucket1"
19776 );
19777 assert_eq!(
19778 app.s3_state.selected_row, 4,
19779 "First child of bucket2 must be row 4"
19780 );
19781 }
19782
19783 #[test]
19784 fn test_s3_next_pane_row_count_matches_calculate_filtered_bucket_rows() {
19785 use crate::s3::Object as S3Object;
19789 use S3Bucket;
19790 let mut app = test_app();
19791 app.current_service = Service::S3Buckets;
19792 app.service_selected = true;
19793 app.mode = Mode::Normal;
19794 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19795
19796 app.s3_state.buckets.items = vec![
19797 S3Bucket {
19798 name: "bucket0".to_string(),
19799 region: "us-east-1".to_string(),
19800 creation_date: String::new(),
19801 },
19802 S3Bucket {
19803 name: "bucket1".to_string(),
19804 region: "us-east-1".to_string(),
19805 creation_date: String::new(),
19806 },
19807 S3Bucket {
19808 name: "bucket2".to_string(),
19809 region: "us-east-1".to_string(),
19810 creation_date: String::new(),
19811 },
19812 ];
19813
19814 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
19816 app.s3_state.bucket_preview.insert(
19817 "bucket0".to_string(),
19818 vec![
19819 S3Object {
19820 key: "a.txt".to_string(),
19821 is_prefix: false,
19822 size: 0,
19823 last_modified: String::new(),
19824 storage_class: String::new(),
19825 },
19826 S3Object {
19827 key: "b.txt".to_string(),
19828 is_prefix: false,
19829 size: 0,
19830 last_modified: String::new(),
19831 storage_class: String::new(),
19832 },
19833 S3Object {
19834 key: "c.txt".to_string(),
19835 is_prefix: false,
19836 size: 0,
19837 last_modified: String::new(),
19838 storage_class: String::new(),
19839 },
19840 ],
19841 );
19842
19843 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19845
19846 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
19848 assert_eq!(
19849 total, 6,
19850 "Total rows: bucket0(1)+3children+bucket1(1)+bucket2(1)=6"
19851 );
19852
19853 app.s3_state.selected_row = 5;
19856
19857 app.s3_state.bucket_preview.insert(
19859 "bucket2".to_string(),
19860 vec![S3Object {
19861 key: "x.txt".to_string(),
19862 is_prefix: false,
19863 size: 0,
19864 last_modified: String::new(),
19865 storage_class: String::new(),
19866 }],
19867 );
19868
19869 app.handle_action(Action::NextPane);
19870
19871 assert!(
19872 app.s3_state.expanded_prefixes.contains("bucket2"),
19873 "Must expand bucket2, not bucket1"
19874 );
19875 assert_eq!(
19876 app.s3_state.selected_row, 6,
19877 "First child of bucket2 must be row 6"
19878 );
19879 }
19880
19881 #[test]
19882 fn test_s3_expand_row_index_correct_when_prior_bucket_has_loaded_children() {
19883 use crate::s3::Object as S3Object;
19888 use S3Bucket;
19889 let mut app = test_app();
19890 app.current_service = Service::S3Buckets;
19891 app.service_selected = true;
19892 app.mode = Mode::Normal;
19893 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19894
19895 app.s3_state.buckets.items = vec![
19896 S3Bucket {
19897 name: "bucket0".to_string(),
19898 region: "us-east-1".to_string(),
19899 creation_date: String::new(),
19900 },
19901 S3Bucket {
19902 name: "bucket1".to_string(),
19903 region: "us-east-1".to_string(),
19904 creation_date: String::new(),
19905 },
19906 S3Bucket {
19907 name: "bucket2".to_string(),
19908 region: "us-east-1".to_string(),
19909 creation_date: String::new(),
19910 },
19911 ];
19912
19913 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
19915 app.s3_state.bucket_preview.insert(
19916 "bucket0".to_string(),
19917 vec![
19918 S3Object {
19919 key: "a.txt".to_string(),
19920 is_prefix: false,
19921 size: 0,
19922 last_modified: String::new(),
19923 storage_class: String::new(),
19924 },
19925 S3Object {
19926 key: "b.txt".to_string(),
19927 is_prefix: false,
19928 size: 0,
19929 last_modified: String::new(),
19930 storage_class: String::new(),
19931 },
19932 ],
19933 );
19934
19935 app.s3_state.selected_row = 4;
19938
19939 app.s3_state.bucket_preview.insert(
19941 "bucket2".to_string(),
19942 vec![S3Object {
19943 key: "c.txt".to_string(),
19944 is_prefix: false,
19945 size: 0,
19946 last_modified: String::new(),
19947 storage_class: String::new(),
19948 }],
19949 );
19950
19951 app.handle_action(Action::NextPane);
19952
19953 assert!(
19955 app.s3_state.expanded_prefixes.contains("bucket2"),
19956 "bucket2 must be expanded"
19957 );
19958 assert_eq!(
19959 app.s3_state.selected_row, 5,
19960 "Must select first child of bucket2 (row 5), not some wrong row"
19961 );
19962 assert!(
19963 !app.s3_state.expanded_prefixes.contains("bucket1"),
19964 "bucket1 must NOT be expanded"
19965 );
19966 }
19967
19968 #[test]
19969 fn test_s3_expand_with_empty_preview_stays_on_bucket() {
19970 use S3Bucket;
19973 let mut app = test_app();
19974 app.current_service = Service::S3Buckets;
19975 app.service_selected = true;
19976 app.mode = Mode::Normal;
19977 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19978
19979 app.s3_state.buckets.items = vec![
19980 S3Bucket {
19981 name: "bucket0".to_string(),
19982 region: "us-east-1".to_string(),
19983 creation_date: String::new(),
19984 },
19985 S3Bucket {
19986 name: "bucket1".to_string(),
19987 region: "us-east-1".to_string(),
19988 creation_date: String::new(),
19989 },
19990 S3Bucket {
19991 name: "bucket2".to_string(),
19992 region: "us-east-1".to_string(),
19993 creation_date: String::new(),
19994 },
19995 ];
19996 app.s3_state
19998 .bucket_preview
19999 .insert("bucket1".to_string(), vec![]);
20000 app.s3_state.selected_row = 1; app.handle_action(Action::NextPane);
20003
20004 assert_eq!(
20006 app.s3_state.selected_row, 1,
20007 "Empty bucket: selection must stay on bucket row, not advance to next sibling"
20008 );
20009 assert!(
20010 app.s3_state.expanded_prefixes.contains("bucket1"),
20011 "Bucket must be expanded (showing ▼)"
20012 );
20013 }
20014
20015 #[test]
20016 fn test_s3_expand_with_preceding_loading_buckets_on_prior_page() {
20017 use crate::s3::Object as S3Object;
20022 use S3Bucket;
20023 let mut app = test_app();
20024 app.current_service = Service::S3Buckets;
20025 app.service_selected = true;
20026 app.mode = Mode::Normal;
20027 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20028
20029 app.s3_state.buckets.items = (0..60)
20031 .map(|i| S3Bucket {
20032 name: format!("bucket{:02}", i),
20033 region: "us-east-1".to_string(),
20034 creation_date: String::new(),
20035 })
20036 .collect();
20037
20038 for i in 0..3usize {
20040 app.s3_state
20041 .expanded_prefixes
20042 .insert(format!("bucket{:02}", i));
20043 }
20044
20045 app.s3_state.bucket_scroll_offset = 50;
20046
20047 app.s3_state.bucket_preview.insert(
20049 "bucket55".to_string(),
20050 vec![S3Object {
20051 key: "file.txt".to_string(),
20052 is_prefix: false,
20053 size: 0,
20054 last_modified: String::new(),
20055 storage_class: String::new(),
20056 }],
20057 );
20058
20059 app.s3_state.selected_row = 55;
20061
20062 app.handle_action(Action::NextPane);
20063
20064 assert!(
20066 app.s3_state.expanded_prefixes.contains("bucket55"),
20067 "bucket55 must be expanded"
20068 );
20069 assert_eq!(
20070 app.s3_state.selected_row, 56,
20071 "Must select first child of bucket55 (row 56)"
20072 );
20073 }
20074
20075 #[test]
20076 fn test_s3_expand_with_multiple_preceding_loading_buckets() {
20077 use crate::s3::Object as S3Object;
20080 use S3Bucket;
20081 let mut app = test_app();
20082 app.current_service = Service::S3Buckets;
20083 app.service_selected = true;
20084 app.mode = Mode::Normal;
20085 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20086
20087 app.s3_state.buckets.items = (0..5)
20088 .map(|i| S3Bucket {
20089 name: format!("bucket{}", i),
20090 region: "us-east-1".to_string(),
20091 creation_date: String::new(),
20092 })
20093 .collect();
20094
20095 for i in 0..3 {
20097 app.s3_state
20098 .expanded_prefixes
20099 .insert(format!("bucket{}", i));
20100 }
20101
20102 app.s3_state.bucket_preview.insert(
20104 "bucket3".to_string(),
20105 vec![S3Object {
20106 key: "file.txt".to_string(),
20107 is_prefix: false,
20108 size: 0,
20109 last_modified: String::new(),
20110 storage_class: String::new(),
20111 }],
20112 );
20113
20114 app.s3_state.selected_row = 3; app.handle_action(Action::NextPane);
20119
20120 assert!(
20122 app.s3_state.expanded_prefixes.contains("bucket3"),
20123 "bucket3 must be expanded"
20124 );
20125 assert_eq!(
20126 app.s3_state.selected_row, 4,
20127 "Must select first child of bucket3 (row 4)"
20128 );
20129 }
20130
20131 #[test]
20132 fn test_s3_expand_with_preceding_loading_bucket_selects_correct_row() {
20133 use crate::s3::Object as S3Object;
20138 use S3Bucket;
20139 let mut app = test_app();
20140 app.current_service = Service::S3Buckets;
20141 app.service_selected = true;
20142 app.mode = Mode::Normal;
20143 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20144
20145 app.s3_state.buckets.items = vec![
20146 S3Bucket {
20147 name: "bucket0".to_string(),
20148 region: "us-east-1".to_string(),
20149 creation_date: String::new(),
20150 },
20151 S3Bucket {
20152 name: "bucket1".to_string(),
20153 region: "us-east-1".to_string(),
20154 creation_date: String::new(),
20155 },
20156 S3Bucket {
20157 name: "bucket2".to_string(),
20158 region: "us-east-1".to_string(),
20159 creation_date: String::new(),
20160 },
20161 ];
20162
20163 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20165 app.s3_state.buckets.loading = true;
20166
20167 app.s3_state.bucket_preview.insert(
20169 "bucket1".to_string(),
20170 vec![S3Object {
20171 key: "file.txt".to_string(),
20172 is_prefix: false,
20173 size: 0,
20174 last_modified: String::new(),
20175 storage_class: String::new(),
20176 }],
20177 );
20178
20179 app.s3_state.selected_row = 1;
20181
20182 app.handle_action(Action::NextPane);
20184
20185 assert_eq!(
20187 app.s3_state.selected_row, 2,
20188 "Must expand bucket1's first child (row 2), not bucket2 (row 3)"
20189 );
20190 assert!(
20191 app.s3_state.expanded_prefixes.contains("bucket1"),
20192 "bucket1 must be in expanded_prefixes"
20193 );
20194 }
20195
20196 #[test]
20197 fn test_s3_expand_last_row_on_page_shows_first_child_not_next_page() {
20198 use crate::s3::Object as S3Object;
20203 use S3Bucket;
20204 let mut app = test_app();
20205 app.current_service = Service::S3Buckets;
20206 app.service_selected = true;
20207 app.mode = Mode::Normal;
20208
20209 app.s3_state.buckets.items = vec![
20211 S3Bucket {
20212 name: "bucket0".to_string(),
20213 region: "us-east-1".to_string(),
20214 creation_date: String::new(),
20215 },
20216 S3Bucket {
20217 name: "bucket1".to_string(),
20218 region: "us-east-1".to_string(),
20219 creation_date: String::new(),
20220 },
20221 S3Bucket {
20222 name: "bucket2".to_string(),
20223 region: "us-east-1".to_string(),
20224 creation_date: String::new(),
20225 },
20226 ];
20227 app.s3_state.bucket_visible_rows = std::cell::Cell::new(2);
20228 app.s3_state.selected_row = 1;
20230 app.s3_state.bucket_scroll_offset = 0;
20231
20232 app.s3_state.bucket_preview.insert(
20234 "bucket1".to_string(),
20235 vec![S3Object {
20236 key: "file.txt".to_string(),
20237 is_prefix: false,
20238 size: 0,
20239 last_modified: String::new(),
20240 storage_class: String::new(),
20241 }],
20242 );
20243
20244 app.handle_action(Action::NextPane); assert_eq!(app.s3_state.selected_row, 2, "Must select first child");
20248
20249 let visible = app.s3_state.bucket_visible_rows.get();
20251 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
20252 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
20253 assert!(
20254 in_view,
20255 "First child row {} must be visible in [{}, {})",
20256 app.s3_state.selected_row,
20257 app.s3_state.bucket_scroll_offset,
20258 app.s3_state.bucket_scroll_offset + visible
20259 );
20260 }
20261
20262 #[test]
20263 fn test_apig_detail_tab_navigation() {
20264 use crate::apig::api::RestApi;
20265 use crate::ui::apig::ApiDetailTab;
20266
20267 let mut app = test_app();
20268 app.current_service = Service::ApiGatewayApis;
20269 app.apig_state.current_api = Some(RestApi {
20270 id: "test123".to_string(),
20271 name: "Test API".to_string(),
20272 description: "Test".to_string(),
20273 created_date: "2024-01-01".to_string(),
20274 api_key_source: "HEADER".to_string(),
20275 endpoint_configuration: "REGIONAL".to_string(),
20276 protocol_type: "REST".to_string(),
20277 disable_execute_api_endpoint: false,
20278 status: "AVAILABLE".to_string(),
20279 });
20280
20281 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20283
20284 app.handle_action(Action::NextDetailTab);
20286 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20287
20288 app.handle_action(Action::PrevDetailTab);
20290 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20291
20292 app.handle_action(Action::NextDetailTab);
20294 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20295
20296 app.handle_action(Action::PrevDetailTab);
20298 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20299
20300 app.handle_action(Action::NextDetailTab);
20302 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20303 }
20304
20305 #[test]
20306 fn test_apig_routes_expand_collapse() {
20307 use crate::apig::api::RestApi;
20308 use crate::apig::route::Route;
20309 use crate::ui::apig::ApiDetailTab;
20310
20311 let mut app = test_app();
20312 app.current_service = Service::ApiGatewayApis;
20313 app.apig_state.current_api = Some(RestApi {
20314 id: "test123".to_string(),
20315 name: "Test API".to_string(),
20316 description: "Test".to_string(),
20317 created_date: "2024-01-01".to_string(),
20318 api_key_source: "HEADER".to_string(),
20319 endpoint_configuration: "REGIONAL".to_string(),
20320 protocol_type: "HTTP".to_string(),
20321 disable_execute_api_endpoint: false,
20322 status: "AVAILABLE".to_string(),
20323 });
20324 app.apig_state.detail_tab = ApiDetailTab::Routes;
20325
20326 let virtual_parent = Route {
20328 route_id: "virtual_/api".to_string(),
20329 route_key: "/api".to_string(),
20330 target: String::new(), authorization_type: String::new(),
20332 api_key_required: false,
20333 display_name: String::new(),
20334 arn: String::new(),
20335 };
20336 let child_route = Route {
20337 route_id: "1".to_string(),
20338 route_key: "/api/users".to_string(),
20339 target: "integration1".to_string(),
20340 authorization_type: "NONE".to_string(),
20341 api_key_required: false,
20342 display_name: String::new(),
20343 arn: String::new(),
20344 };
20345
20346 app.apig_state.routes.items = vec![virtual_parent];
20347 app.apig_state
20348 .route_children
20349 .insert("/api".to_string(), vec![child_route]);
20350
20351 assert!(app.apig_state.expanded_routes.is_empty());
20353
20354 app.apig_state.routes.selected = 0;
20356 app.handle_action(Action::ExpandRow);
20357 assert!(app.apig_state.expanded_routes.contains("/api"));
20358
20359 app.handle_action(Action::CollapseRow);
20361 assert!(!app.apig_state.expanded_routes.contains("/api"));
20362
20363 app.handle_action(Action::ExpandRow);
20365 assert!(app.apig_state.expanded_routes.contains("/api"));
20366 }
20367
20368 #[test]
20369 fn test_apig_routes_navigation() {
20370 use crate::apig::api::RestApi;
20371 use crate::apig::route::Route;
20372 use crate::ui::apig::ApiDetailTab;
20373
20374 let mut app = test_app();
20375 app.mode = Mode::Normal;
20376 app.service_selected = true;
20377 app.current_service = Service::ApiGatewayApis;
20378 app.apig_state.current_api = Some(RestApi {
20379 id: "test123".to_string(),
20380 name: "Test API".to_string(),
20381 description: "Test".to_string(),
20382 created_date: "2024-01-01".to_string(),
20383 api_key_source: "HEADER".to_string(),
20384 endpoint_configuration: "REGIONAL".to_string(),
20385 protocol_type: "HTTP".to_string(),
20386 disable_execute_api_endpoint: false,
20387 status: "AVAILABLE".to_string(),
20388 });
20389 app.apig_state.detail_tab = ApiDetailTab::Routes;
20390 app.apig_state.routes.items = vec![
20391 Route {
20392 route_id: "1".to_string(),
20393 route_key: "/api/users".to_string(),
20394 target: "integration1".to_string(),
20395 authorization_type: "NONE".to_string(),
20396 api_key_required: false,
20397 display_name: String::new(),
20398 arn: String::new(),
20399 },
20400 Route {
20401 route_id: "2".to_string(),
20402 route_key: "/health".to_string(),
20403 target: "integration2".to_string(),
20404 authorization_type: "NONE".to_string(),
20405 api_key_required: false,
20406 display_name: String::new(),
20407 arn: String::new(),
20408 },
20409 ];
20410
20411 assert_eq!(app.apig_state.routes.selected, 0);
20413
20414 app.handle_action(Action::NextItem);
20416 assert_eq!(app.apig_state.routes.selected, 1);
20417
20418 app.handle_action(Action::NextItem);
20420 assert_eq!(app.apig_state.routes.selected, 1);
20421
20422 app.handle_action(Action::PrevItem);
20424 assert_eq!(app.apig_state.routes.selected, 0);
20425
20426 app.handle_action(Action::PrevItem);
20428 assert_eq!(app.apig_state.routes.selected, 0);
20429 }
20430
20431 #[test]
20432 fn test_apig_routes_expand_jumps_to_child() {
20433 use crate::apig::api::RestApi;
20434 use crate::apig::route::Route;
20435 use crate::ui::apig::ApiDetailTab;
20436 use std::collections::HashMap;
20437
20438 let mut app = test_app();
20439 app.mode = Mode::Normal;
20440 app.service_selected = true;
20441 app.current_service = Service::ApiGatewayApis;
20442 app.apig_state.current_api = Some(RestApi {
20443 id: "test123".to_string(),
20444 name: "Test API".to_string(),
20445 description: "Test".to_string(),
20446 created_date: "2024-01-01".to_string(),
20447 api_key_source: "HEADER".to_string(),
20448 endpoint_configuration: "REGIONAL".to_string(),
20449 protocol_type: "HTTP".to_string(),
20450 disable_execute_api_endpoint: false,
20451 status: "AVAILABLE".to_string(),
20452 });
20453 app.apig_state.detail_tab = ApiDetailTab::Routes;
20454
20455 app.apig_state.routes.items = vec![Route {
20457 route_id: "virtual_/api".to_string(),
20458 route_key: "/api".to_string(),
20459 target: String::new(),
20460 authorization_type: String::new(),
20461 api_key_required: false,
20462 display_name: String::new(),
20463 arn: String::new(),
20464 }];
20465
20466 let mut children = HashMap::new();
20467 children.insert(
20468 "/api".to_string(),
20469 vec![Route {
20470 route_id: "1".to_string(),
20471 route_key: "/api/users".to_string(),
20472 target: "integration1".to_string(),
20473 authorization_type: "NONE".to_string(),
20474 api_key_required: false,
20475 display_name: String::new(),
20476 arn: String::new(),
20477 }],
20478 );
20479 app.apig_state.route_children = children;
20480
20481 assert_eq!(app.apig_state.routes.selected, 0);
20483 assert!(!app.apig_state.expanded_routes.contains("/api"));
20484
20485 app.handle_action(Action::ExpandRow);
20487 assert!(app.apig_state.expanded_routes.contains("/api"));
20488 assert_eq!(app.apig_state.routes.selected, 0);
20489
20490 app.handle_action(Action::ExpandRow);
20492 assert_eq!(app.apig_state.routes.selected, 1);
20493 }
20494
20495 #[test]
20496 fn test_apig_filter_only_when_focused() {
20497 use crate::apig::api::RestApi;
20498 use crate::ui::apig::ApiDetailTab;
20499
20500 let mut app = test_app();
20501 app.current_service = Service::ApiGatewayApis;
20502 app.apig_state.current_api = Some(RestApi {
20503 id: "test".to_string(),
20504 name: "Test API".to_string(),
20505 description: "Test".to_string(),
20506 created_date: "2024-01-01".to_string(),
20507 api_key_source: "HEADER".to_string(),
20508 endpoint_configuration: "REGIONAL".to_string(),
20509 protocol_type: "HTTP".to_string(),
20510 disable_execute_api_endpoint: false,
20511 status: "AVAILABLE".to_string(),
20512 });
20513 app.apig_state.detail_tab = ApiDetailTab::Routes;
20514 app.mode = Mode::FilterInput;
20515
20516 app.apig_state.input_focus = InputFocus::Pagination;
20518 app.handle_action(Action::FilterInput('x'));
20519 assert_eq!(app.apig_state.route_filter, "");
20520
20521 app.apig_state.input_focus = InputFocus::Filter;
20523 app.handle_action(Action::FilterInput('x'));
20524 assert_eq!(app.apig_state.route_filter, "x");
20525 }
20526
20527 #[test]
20528 fn test_apig_routes_and_resources_use_same_render_function() {
20529 let source = include_str!("ui/apig.rs");
20531 let render_calls: Vec<_> = source
20532 .match_indices("crate::ui::table::render_tree_table")
20533 .collect();
20534
20535 assert_eq!(
20537 render_calls.len(),
20538 2,
20539 "Both routes and resources must use render_tree_table"
20540 );
20541 }
20542
20543 #[test]
20544 fn test_s3_uses_same_render_function() {
20545 let source = include_str!("ui/s3.rs");
20547 let render_calls: Vec<_> = source
20548 .match_indices("crate::ui::table::render_tree_table")
20549 .collect();
20550
20551 assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
20553 }
20554
20555 #[test]
20556 fn test_search_icon_has_proper_border_spacing() {
20557 use crate::ui::SEARCH_ICON;
20560
20561 assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
20562 assert!(
20563 SEARCH_ICON.ends_with("─"),
20564 "Should end with dash for proper border spacing"
20565 );
20566 assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
20567 }
20568
20569 #[test]
20570 fn test_apig_expand_with_filter() {
20571 use crate::apig::api::RestApi;
20572 use crate::apig::route::Route;
20573 use crate::ui::apig::ApiDetailTab;
20574 use std::collections::HashMap;
20575
20576 let mut app = test_app();
20577 app.current_service = Service::ApiGatewayApis;
20578 app.apig_state.current_api = Some(RestApi {
20579 id: "test123".to_string(),
20580 name: "Test API".to_string(),
20581 description: "Test".to_string(),
20582 created_date: "2024-01-01".to_string(),
20583 api_key_source: "HEADER".to_string(),
20584 endpoint_configuration: "REGIONAL".to_string(),
20585 protocol_type: "HTTP".to_string(),
20586 disable_execute_api_endpoint: false,
20587 status: "AVAILABLE".to_string(),
20588 });
20589 app.apig_state.detail_tab = ApiDetailTab::Routes;
20590
20591 app.apig_state.routes.items = vec![Route {
20593 route_id: "0".to_string(),
20594 route_key: "/api".to_string(),
20595 target: "".to_string(),
20596 authorization_type: "NONE".to_string(),
20597 api_key_required: false,
20598 display_name: "/api".to_string(),
20599 arn: String::new(),
20600 }];
20601
20602 let mut children = HashMap::new();
20603 children.insert(
20604 "/api".to_string(),
20605 vec![Route {
20606 route_id: "1".to_string(),
20607 route_key: "GET".to_string(),
20608 target: "integration1".to_string(),
20609 authorization_type: "NONE".to_string(),
20610 api_key_required: false,
20611 display_name: "GET".to_string(),
20612 arn: String::new(),
20613 }],
20614 );
20615 app.apig_state.route_children = children;
20616
20617 app.apig_state.route_filter = "GET".to_string();
20619
20620 assert_eq!(app.apig_state.routes.selected, 0);
20623 assert!(!app.apig_state.expanded_routes.contains("/api"));
20624
20625 app.handle_action(Action::ExpandRow);
20626
20627 assert!(app.apig_state.expanded_routes.contains("/api"));
20629 }
20630
20631 #[test]
20632 fn test_apig_console_url_routes() {
20633 use crate::apig::api::RestApi;
20634 use crate::apig::route::Route;
20635 use crate::ui::apig::ApiDetailTab;
20636
20637 let mut app = test_app();
20638 app.current_service = Service::ApiGatewayApis;
20639 app.config.region = "us-east-1".to_string();
20640
20641 let url = app.get_console_url();
20643 assert!(url.contains("apigateway/main/apis"));
20644 assert!(url.contains("region=us-east-1"));
20645
20646 app.apig_state.current_api = Some(RestApi {
20648 id: "2todvod3n0".to_string(),
20649 name: "Test API".to_string(),
20650 description: "Test".to_string(),
20651 created_date: "2024-01-01".to_string(),
20652 api_key_source: "HEADER".to_string(),
20653 endpoint_configuration: "REGIONAL".to_string(),
20654 protocol_type: "HTTP".to_string(),
20655 disable_execute_api_endpoint: false,
20656 status: "AVAILABLE".to_string(),
20657 });
20658 app.apig_state.detail_tab = ApiDetailTab::Routes;
20659 app.apig_state.routes.items = vec![Route {
20660 route_id: "eizmisr".to_string(),
20661 route_key: "GET /test".to_string(),
20662 target: "integration1".to_string(),
20663 authorization_type: "NONE".to_string(),
20664 api_key_required: false,
20665 display_name: "GET /test".to_string(),
20666 arn: String::new(),
20667 }];
20668 app.apig_state.routes.selected = 0;
20669
20670 let url = app.get_console_url();
20671 assert!(url.contains("apigateway/main/develop/routes"));
20672 assert!(url.contains("api=2todvod3n0"));
20673 assert!(url.contains("routes=eizmisr"));
20674 assert!(url.contains("region=us-east-1"));
20675 }
20676
20677 #[test]
20678 fn test_apig_console_url_resources() {
20679 use crate::apig::api::RestApi;
20680 use crate::apig::resource::Resource;
20681 use crate::ui::apig::ApiDetailTab;
20682
20683 let mut app = test_app();
20684 app.current_service = Service::ApiGatewayApis;
20685 app.config.region = "us-east-1".to_string();
20686
20687 app.apig_state.current_api = Some(RestApi {
20689 id: "2j9j50ze47".to_string(),
20690 name: "Test API".to_string(),
20691 description: "Test".to_string(),
20692 created_date: "2024-01-01".to_string(),
20693 api_key_source: "HEADER".to_string(),
20694 endpoint_configuration: "REGIONAL".to_string(),
20695 protocol_type: "REST".to_string(),
20696 disable_execute_api_endpoint: false,
20697 status: "AVAILABLE".to_string(),
20698 });
20699 app.apig_state.detail_tab = ApiDetailTab::Routes;
20700 app.apig_state.resources.items = vec![Resource {
20701 id: "abc123".to_string(),
20702 path: "/test".to_string(),
20703 parent_id: None,
20704 methods: vec![],
20705 display_name: "/test".to_string(),
20706 arn: String::new(),
20707 }];
20708 app.apig_state.resources.selected = 0;
20709
20710 let url = app.get_console_url();
20711 assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
20712 assert!(url.contains("api=2j9j50ze47"));
20713 assert!(url.contains("#abc123"));
20714 assert!(url.contains("region=us-east-1"));
20715 }
20716
20717 #[test]
20718 fn test_apig_console_url_routes_parent_vs_leaf() {
20719 use crate::apig::api::RestApi;
20720 use crate::apig::route::Route;
20721 use crate::ui::apig::ApiDetailTab;
20722 use std::collections::HashMap;
20723
20724 let mut app = test_app();
20725 app.current_service = Service::ApiGatewayApis;
20726 app.config.region = "us-east-1".to_string();
20727
20728 app.apig_state.current_api = Some(RestApi {
20729 id: "2todvod3n0".to_string(),
20730 name: "Test API".to_string(),
20731 description: "Test".to_string(),
20732 created_date: "2024-01-01".to_string(),
20733 api_key_source: "HEADER".to_string(),
20734 endpoint_configuration: "REGIONAL".to_string(),
20735 protocol_type: "HTTP".to_string(),
20736 disable_execute_api_endpoint: false,
20737 status: "AVAILABLE".to_string(),
20738 });
20739 app.apig_state.detail_tab = ApiDetailTab::Routes;
20740
20741 app.apig_state.routes.items = vec![Route {
20743 route_id: "parent".to_string(),
20744 route_key: "/v1/get/jobs".to_string(),
20745 target: "".to_string(), authorization_type: "NONE".to_string(),
20747 api_key_required: false,
20748 display_name: "/v1/get/jobs".to_string(),
20749 arn: String::new(),
20750 }];
20751
20752 let mut children = HashMap::new();
20754 children.insert(
20755 "/v1/get/jobs".to_string(),
20756 vec![Route {
20757 route_id: "1iz9vtl".to_string(),
20758 route_key: "GET".to_string(),
20759 target: "integration1".to_string(), authorization_type: "NONE".to_string(),
20761 api_key_required: false,
20762 display_name: "GET".to_string(),
20763 arn: String::new(),
20764 }],
20765 );
20766 app.apig_state.route_children = children;
20767
20768 app.apig_state.routes.selected = 0;
20770 let url = app.get_console_url();
20771 assert!(url.contains("apigateway/main/develop/routes"));
20772 assert!(url.contains("api=2todvod3n0"));
20773 assert!(
20774 !url.contains("routes="),
20775 "Parent node should not include routes parameter"
20776 );
20777
20778 app.apig_state
20780 .expanded_routes
20781 .insert("/v1/get/jobs".to_string());
20782 app.apig_state.routes.selected = 1;
20783 let url = app.get_console_url();
20784 assert!(
20785 url.contains("routes=1iz9vtl"),
20786 "Leaf node should include routes parameter: {}",
20787 url
20788 );
20789 }
20790
20791 #[test]
20792 fn test_apig_preferences_context() {
20793 use crate::apig::api::RestApi;
20794 use crate::ui::apig::ApiDetailTab;
20795
20796 let mut app = test_app();
20797 app.current_service = Service::ApiGatewayApis;
20798
20799 assert!(
20801 app.apig_state.current_api.is_none(),
20802 "Should be in list view"
20803 );
20804
20805 app.apig_state.current_api = Some(RestApi {
20807 id: "test123".to_string(),
20808 name: "Test API".to_string(),
20809 description: "Test".to_string(),
20810 created_date: "2024-01-01".to_string(),
20811 api_key_source: "HEADER".to_string(),
20812 endpoint_configuration: "REGIONAL".to_string(),
20813 protocol_type: "HTTP".to_string(),
20814 disable_execute_api_endpoint: false,
20815 status: "AVAILABLE".to_string(),
20816 });
20817 app.apig_state.detail_tab = ApiDetailTab::Routes;
20818
20819 assert!(
20820 app.apig_state.current_api.is_some(),
20821 "Should be in detail view"
20822 );
20823 }
20825
20826 #[test]
20827 fn test_apig_route_columns() {
20828 use crate::apig::route::Column as RouteColumn;
20829
20830 let cols = RouteColumn::all();
20832 assert_eq!(cols.len(), 5);
20833
20834 assert_eq!(RouteColumn::RouteKey.id(), "route_key");
20836 assert_eq!(RouteColumn::RouteId.id(), "route_id");
20837 assert_eq!(RouteColumn::Arn.id(), "arn");
20838 assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
20839 assert_eq!(RouteColumn::Target.id(), "target");
20840
20841 assert_eq!(
20843 RouteColumn::from_id("route_key"),
20844 Some(RouteColumn::RouteKey)
20845 );
20846 assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
20847 assert_eq!(RouteColumn::from_id("invalid"), None);
20848 }
20849
20850 #[test]
20851 fn test_apig_yank_copies_route_arn() {
20852 use crate::apig::api::RestApi;
20853 use crate::ui::apig::ApiDetailTab;
20854
20855 let mut app = test_app();
20856 app.current_service = Service::ApiGatewayApis;
20857 app.apig_state.current_api = Some(RestApi {
20858 id: "test123".to_string(),
20859 name: "Test API".to_string(),
20860 description: "Test".to_string(),
20861 created_date: "2024-01-01".to_string(),
20862 api_key_source: "HEADER".to_string(),
20863 endpoint_configuration: "REGIONAL".to_string(),
20864 protocol_type: "HTTP".to_string(),
20865 disable_execute_api_endpoint: false,
20866 status: "AVAILABLE".to_string(),
20867 });
20868 app.apig_state.detail_tab = ApiDetailTab::Routes;
20869
20870 app.apig_state.routes.items = vec![
20872 Route {
20873 route_id: "route1".to_string(),
20874 route_key: "GET /users".to_string(),
20875 target: "integrations/abc".to_string(),
20876 authorization_type: "NONE".to_string(),
20877 api_key_required: false,
20878 display_name: "GET /users".to_string(),
20879 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
20880 },
20881 Route {
20882 route_id: "route2".to_string(),
20883 route_key: "POST /users".to_string(),
20884 target: "integrations/def".to_string(),
20885 authorization_type: "AWS_IAM".to_string(),
20886 api_key_required: true,
20887 display_name: "POST /users".to_string(),
20888 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
20889 },
20890 ];
20891
20892 app.apig_state.routes.selected = 0;
20894
20895 assert_eq!(
20897 app.apig_state.routes.items[0].arn,
20898 "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
20899 );
20900 }
20901
20902 #[test]
20903 fn test_apig_yank_ignores_empty_arn() {
20904 use crate::apig::api::RestApi;
20905 use crate::ui::apig::ApiDetailTab;
20906
20907 let mut app = test_app();
20908 app.current_service = Service::ApiGatewayApis;
20909 app.apig_state.current_api = Some(RestApi {
20910 id: "test123".to_string(),
20911 name: "Test API".to_string(),
20912 description: "Test".to_string(),
20913 created_date: "2024-01-01".to_string(),
20914 api_key_source: "HEADER".to_string(),
20915 endpoint_configuration: "REGIONAL".to_string(),
20916 protocol_type: "HTTP".to_string(),
20917 disable_execute_api_endpoint: false,
20918 status: "AVAILABLE".to_string(),
20919 });
20920 app.apig_state.detail_tab = ApiDetailTab::Routes;
20921
20922 app.apig_state.routes.items = vec![Route {
20924 route_id: String::new(),
20925 route_key: "/users".to_string(),
20926 target: String::new(), authorization_type: String::new(),
20928 api_key_required: false,
20929 display_name: "/users".to_string(),
20930 arn: String::new(), }];
20932
20933 app.apig_state.routes.selected = 0;
20934
20935 assert!(
20937 app.apig_state.routes.items[0].arn.is_empty(),
20938 "Virtual parent should have empty ARN"
20939 );
20940 }
20941
20942 #[test]
20943 fn test_apig_route_column_toggle() {
20944 use crate::apig::api::RestApi;
20945 use crate::apig::route::Column as RouteColumn;
20946 use crate::keymap::Action;
20947 use crate::ui::apig::ApiDetailTab;
20948
20949 let mut app = test_app();
20950 app.current_service = Service::ApiGatewayApis;
20951 app.mode = Mode::ColumnSelector;
20952 app.apig_state.current_api = Some(RestApi {
20953 id: "test123".to_string(),
20954 name: "Test API".to_string(),
20955 description: "Test".to_string(),
20956 created_date: "2024-01-01".to_string(),
20957 api_key_source: "HEADER".to_string(),
20958 endpoint_configuration: "REGIONAL".to_string(),
20959 protocol_type: "HTTP".to_string(),
20960 disable_execute_api_endpoint: false,
20961 status: "AVAILABLE".to_string(),
20962 });
20963 app.apig_state.detail_tab = ApiDetailTab::Routes;
20964
20965 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
20967
20968 app.column_selector_index = 1;
20970 app.handle_action(Action::ToggleColumn);
20971 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
20972 assert!(app
20973 .apig_route_visible_column_ids
20974 .contains(&RouteColumn::RouteKey.id()));
20975
20976 app.column_selector_index = 3;
20978 app.handle_action(Action::ToggleColumn);
20979
20980 assert_eq!(app.apig_route_visible_column_ids.len(), 4);
20982 assert!(!app
20983 .apig_route_visible_column_ids
20984 .contains(&RouteColumn::Arn.id()));
20985
20986 app.handle_action(Action::ToggleColumn);
20988 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
20989 assert!(app
20990 .apig_route_visible_column_ids
20991 .contains(&RouteColumn::Arn.id()));
20992 }
20993
20994 #[test]
20995 fn test_apig_resource_column_toggle() {
20996 use crate::apig::api::RestApi;
20997 use crate::apig::resource::Column as ResourceColumn;
20998 use crate::keymap::Action;
20999 use crate::ui::apig::ApiDetailTab;
21000
21001 let mut app = test_app();
21002 app.current_service = Service::ApiGatewayApis;
21003 app.mode = Mode::ColumnSelector;
21004 app.apig_state.current_api = Some(RestApi {
21005 id: "test123".to_string(),
21006 name: "Test API".to_string(),
21007 description: "Test".to_string(),
21008 created_date: "2024-01-01".to_string(),
21009 api_key_source: "HEADER".to_string(),
21010 endpoint_configuration: "REGIONAL".to_string(),
21011 protocol_type: "REST".to_string(), disable_execute_api_endpoint: false,
21013 status: "AVAILABLE".to_string(),
21014 });
21015 app.apig_state.detail_tab = ApiDetailTab::Routes; assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21019
21020 app.column_selector_index = 1;
21022 app.handle_action(Action::ToggleColumn);
21023 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21024 assert!(app
21025 .apig_resource_visible_column_ids
21026 .contains(&ResourceColumn::Path.id()));
21027
21028 app.column_selector_index = 3;
21030 app.handle_action(Action::ToggleColumn);
21031
21032 assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
21034 assert!(!app
21035 .apig_resource_visible_column_ids
21036 .contains(&ResourceColumn::Arn.id()));
21037
21038 app.handle_action(Action::ToggleColumn);
21040 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21041 assert!(app
21042 .apig_resource_visible_column_ids
21043 .contains(&ResourceColumn::Arn.id()));
21044 }
21045
21046 #[test]
21047 fn test_cloudtrail_filter_input() {
21048 let mut app = App::new_without_client("default".to_string(), None);
21049 app.service_selected = true;
21050 app.current_service = Service::CloudTrailEvents;
21051
21052 app.handle_action(Action::StartFilter);
21053 assert_eq!(app.mode, Mode::FilterInput);
21054
21055 app.cloudtrail_state.table.filter = "test".to_string();
21056
21057 assert_eq!(app.cloudtrail_state.table.filter, "test");
21058 }
21059
21060 #[test]
21061 fn test_cloudtrail_row_expansion() {
21062 let mut app = App::new_without_client("default".to_string(), None);
21063 app.service_selected = true;
21064 app.current_service = Service::CloudTrailEvents;
21065 app.cloudtrail_state.table.items = vec![CloudTrailEvent {
21066 event_name: "Event1".to_string(),
21067 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21068 username: "user1".to_string(),
21069 event_source: "s3.amazonaws.com".to_string(),
21070 resource_type: "Bucket".to_string(),
21071 resource_name: "my-bucket".to_string(),
21072 read_only: "false".to_string(),
21073 aws_region: "us-east-1".to_string(),
21074 event_id: "abc123".to_string(),
21075 access_key_id: "AKIA...".to_string(),
21076 source_ip_address: "1.2.3.4".to_string(),
21077 error_code: "".to_string(),
21078 request_id: "req-123".to_string(),
21079 event_type: "AwsApiCall".to_string(),
21080 cloud_trail_event_json: "{}".to_string(),
21081 }];
21082
21083 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21084
21085 app.expand_row();
21086 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
21087
21088 app.collapse_row();
21089 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21090 }
21091
21092 #[test]
21093 fn test_cloudtrail_service_initialization() {
21094 let app = App::new_without_client("default".to_string(), None);
21095 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
21096 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21097 }
21098
21099 #[test]
21100 fn test_cloudtrail_service_name() {
21101 assert_eq!(
21102 Service::CloudTrailEvents.name(),
21103 "CloudTrail › Event History"
21104 );
21105 }
21106
21107 #[test]
21108 fn test_cloudtrail_in_service_picker() {
21109 let app = App::new_without_client("default".to_string(), None);
21110 assert!(app
21111 .service_picker
21112 .services
21113 .contains(&"CloudTrail › Event History"));
21114 }
21115
21116 #[test]
21117 fn test_cloudtrail_service_selection() {
21118 let mut app = App::new_without_client("default".to_string(), None);
21119 app.current_service = Service::CloudTrailEvents;
21120 app.service_selected = true;
21121
21122 assert_eq!(app.current_service, Service::CloudTrailEvents);
21123 assert!(app.service_selected);
21124 }
21125
21126 #[test]
21127 fn test_cloudtrail_filter_resets_selection() {
21128 let mut app = App::new_without_client("default".to_string(), None);
21129 app.service_selected = true;
21130 app.current_service = Service::CloudTrailEvents;
21131 app.cloudtrail_state.table.selected = 5;
21132 app.cloudtrail_state.table.expanded_item = Some(3);
21133
21134 app.handle_action(Action::StartFilter);
21135 app.apply_filter_operation(|_| {});
21136
21137 assert_eq!(app.cloudtrail_state.table.selected, 0);
21138 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21139 }
21140
21141 #[test]
21142 fn test_cloudtrail_column_toggle() {
21143 let mut app = App::new_without_client("default".to_string(), None);
21144 app.service_selected = true;
21145 app.current_service = Service::CloudTrailEvents;
21146 app.mode = Mode::ColumnSelector;
21147
21148 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21150
21151 app.column_selector_index = 7;
21153 app.handle_action(Action::ToggleColumn);
21154
21155 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
21157 }
21158
21159 #[test]
21160 fn test_cloudtrail_tab_cycles_filter_focus() {
21161 let mut app = App::new_without_client("default".to_string(), None);
21162 app.service_selected = true;
21163 app.current_service = Service::CloudTrailEvents;
21164 app.mode = Mode::FilterInput;
21165 app.cloudtrail_state.input_focus = InputFocus::Filter;
21166
21167 app.handle_action(Action::NextFilterFocus);
21169 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21170
21171 app.handle_action(Action::NextFilterFocus);
21173 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21174 }
21175
21176 #[test]
21177 fn test_cloudtrail_shift_tab_cycles_filter_focus() {
21178 let mut app = App::new_without_client("default".to_string(), None);
21179 app.service_selected = true;
21180 app.current_service = Service::CloudTrailEvents;
21181 app.mode = Mode::FilterInput;
21182 app.cloudtrail_state.input_focus = InputFocus::Filter;
21183
21184 app.handle_action(Action::PrevFilterFocus);
21186 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21187
21188 app.handle_action(Action::PrevFilterFocus);
21190 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21191 }
21192
21193 #[test]
21194 fn test_cloudtrail_detail_view_tab_cycles_focus() {
21195 let mut app = App::new_without_client("default".to_string(), None);
21196 app.service_selected = true;
21197 app.current_service = Service::CloudTrailEvents;
21198 app.mode = Mode::Normal;
21199 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21200 event_name: "PutObject".to_string(),
21201 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21202 username: "user".to_string(),
21203 event_source: "s3.amazonaws.com".to_string(),
21204 resource_type: "AWS::S3::Bucket".to_string(),
21205 resource_name: "my-bucket".to_string(),
21206 read_only: "false".to_string(),
21207 aws_region: "us-east-1".to_string(),
21208 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21209 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21210 source_ip_address: "192.0.2.1".to_string(),
21211 error_code: "".to_string(),
21212 request_id: "req-123".to_string(),
21213 event_type: "AwsApiCall".to_string(),
21214 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21215 });
21216
21217 assert_eq!(
21219 app.cloudtrail_state.detail_focus,
21220 CloudTrailDetailFocus::Resources
21221 );
21222
21223 app.handle_action(Action::NextDetailTab);
21225 assert_eq!(
21226 app.cloudtrail_state.detail_focus,
21227 CloudTrailDetailFocus::EventRecord
21228 );
21229
21230 app.handle_action(Action::NextDetailTab);
21232 assert_eq!(
21233 app.cloudtrail_state.detail_focus,
21234 CloudTrailDetailFocus::Resources
21235 );
21236 }
21237
21238 #[test]
21239 fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
21240 let mut app = App::new_without_client("default".to_string(), None);
21241 app.service_selected = true;
21242 app.current_service = Service::CloudTrailEvents;
21243 app.mode = Mode::Normal;
21244 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21245 event_name: "PutObject".to_string(),
21246 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21247 username: "user".to_string(),
21248 event_source: "s3.amazonaws.com".to_string(),
21249 resource_type: "AWS::S3::Bucket".to_string(),
21250 resource_name: "my-bucket".to_string(),
21251 read_only: "false".to_string(),
21252 aws_region: "us-east-1".to_string(),
21253 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21254 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21255 source_ip_address: "192.0.2.1".to_string(),
21256 error_code: "".to_string(),
21257 request_id: "req-123".to_string(),
21258 event_type: "AwsApiCall".to_string(),
21259 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21260 });
21261
21262 assert_eq!(
21264 app.cloudtrail_state.detail_focus,
21265 CloudTrailDetailFocus::Resources
21266 );
21267
21268 app.handle_action(Action::PrevDetailTab);
21270 assert_eq!(
21271 app.cloudtrail_state.detail_focus,
21272 CloudTrailDetailFocus::EventRecord
21273 );
21274
21275 app.handle_action(Action::PrevDetailTab);
21277 assert_eq!(
21278 app.cloudtrail_state.detail_focus,
21279 CloudTrailDetailFocus::Resources
21280 );
21281 }
21282
21283 #[test]
21284 fn test_cloudtrail_json_scroll_with_arrow_keys() {
21285 let mut app = App::new_without_client("default".to_string(), None);
21286 app.service_selected = true;
21287 app.current_service = Service::CloudTrailEvents;
21288 app.mode = Mode::Normal;
21289 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21290 event_name: "PutObject".to_string(),
21291 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21292 username: "user".to_string(),
21293 event_source: "s3.amazonaws.com".to_string(),
21294 resource_type: "AWS::S3::Bucket".to_string(),
21295 resource_name: "my-bucket".to_string(),
21296 read_only: "false".to_string(),
21297 aws_region: "us-east-1".to_string(),
21298 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21299 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21300 source_ip_address: "192.0.2.1".to_string(),
21301 error_code: "".to_string(),
21302 request_id: "req-123".to_string(),
21303 event_type: "AwsApiCall".to_string(),
21304 cloud_trail_event_json: (0..50)
21305 .map(|i| format!("line {}", i))
21306 .collect::<Vec<_>>()
21307 .join("\n"),
21308 });
21309 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21310 app.cloudtrail_state.event_json_scroll = 0;
21311
21312 app.handle_action(Action::NextItem);
21314 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21315
21316 app.handle_action(Action::NextItem);
21317 assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
21318
21319 app.handle_action(Action::PrevItem);
21321 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21322
21323 app.handle_action(Action::PrevItem);
21324 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21325
21326 app.handle_action(Action::PrevItem);
21328 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21329 }
21330
21331 #[test]
21332 fn test_cloudtrail_tab_works_with_no_resources() {
21333 let mut app = App::new_without_client("default".to_string(), None);
21334 app.service_selected = true;
21335 app.current_service = Service::CloudTrailEvents;
21336 app.mode = Mode::Normal;
21337 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21338 event_name: "PutObject".to_string(),
21339 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21340 username: "user".to_string(),
21341 event_source: "s3.amazonaws.com".to_string(),
21342 resource_type: "".to_string(), resource_name: "".to_string(), read_only: "false".to_string(),
21345 aws_region: "us-east-1".to_string(),
21346 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21347 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21348 source_ip_address: "192.0.2.1".to_string(),
21349 error_code: "".to_string(),
21350 request_id: "req-123".to_string(),
21351 event_type: "AwsApiCall".to_string(),
21352 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21353 });
21354 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21355
21356 app.handle_action(Action::NextDetailTab);
21358 assert_eq!(
21359 app.cloudtrail_state.detail_focus,
21360 CloudTrailDetailFocus::Resources
21361 );
21362
21363 app.handle_action(Action::NextDetailTab);
21365 assert_eq!(
21366 app.cloudtrail_state.detail_focus,
21367 CloudTrailDetailFocus::EventRecord
21368 );
21369 }
21370
21371 #[test]
21372 fn test_cloudtrail_resources_expand_collapse() {
21373 let mut app = App::new_without_client("default".to_string(), None);
21374 app.service_selected = true;
21375 app.current_service = Service::CloudTrailEvents;
21376 app.mode = Mode::Normal;
21377 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21378 event_name: "PutObject".to_string(),
21379 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21380 username: "user".to_string(),
21381 event_source: "s3.amazonaws.com".to_string(),
21382 resource_type: "AWS::S3::Bucket".to_string(),
21383 resource_name: "my-bucket".to_string(),
21384 read_only: "false".to_string(),
21385 aws_region: "us-east-1".to_string(),
21386 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21387 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21388 source_ip_address: "192.0.2.1".to_string(),
21389 error_code: "".to_string(),
21390 request_id: "req-123".to_string(),
21391 event_type: "AwsApiCall".to_string(),
21392 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21393 });
21394 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21395 app.cloudtrail_state.resources_expanded_index = None;
21396
21397 app.handle_action(Action::ExpandRow);
21399 assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
21400
21401 app.handle_action(Action::CollapseRow);
21403 assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
21404 }
21405
21406 #[test]
21407 fn test_cloudtrail_event_json_no_column_selector() {
21408 let mut app = App::new_without_client("default".to_string(), None);
21409 app.service_selected = true;
21410 app.current_service = Service::CloudTrailEvents;
21411 app.mode = Mode::Normal;
21412 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21413 event_name: "PutObject".to_string(),
21414 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21415 username: "user".to_string(),
21416 event_source: "s3.amazonaws.com".to_string(),
21417 resource_type: "AWS::S3::Bucket".to_string(),
21418 resource_name: "my-bucket".to_string(),
21419 read_only: "false".to_string(),
21420 aws_region: "us-east-1".to_string(),
21421 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21422 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21423 source_ip_address: "192.0.2.1".to_string(),
21424 error_code: "".to_string(),
21425 request_id: "req-123".to_string(),
21426 event_type: "AwsApiCall".to_string(),
21427 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21428 });
21429 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21430
21431 app.handle_action(Action::OpenColumnSelector);
21433 assert_eq!(app.mode, Mode::Normal);
21434
21435 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21437 app.handle_action(Action::OpenColumnSelector);
21438 assert_eq!(app.mode, Mode::ColumnSelector);
21439 }
21440
21441 #[test]
21442 fn test_cloudtrail_resources_preferences_show_only_3_columns() {
21443 let mut app = App::new_without_client("default".to_string(), None);
21444 app.service_selected = true;
21445 app.current_service = Service::CloudTrailEvents;
21446 app.mode = Mode::ColumnSelector;
21447 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21448 event_name: "PutObject".to_string(),
21449 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21450 username: "user".to_string(),
21451 event_source: "s3.amazonaws.com".to_string(),
21452 resource_type: "AWS::S3::Bucket".to_string(),
21453 resource_name: "my-bucket".to_string(),
21454 read_only: "false".to_string(),
21455 aws_region: "us-east-1".to_string(),
21456 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21457 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21458 source_ip_address: "192.0.2.1".to_string(),
21459 error_code: "".to_string(),
21460 request_id: "req-123".to_string(),
21461 event_type: "AwsApiCall".to_string(),
21462 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21463 });
21464 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21465
21466 assert_eq!(app.get_column_count(), 3);
21468 assert_eq!(app.get_column_selector_max(), 3);
21469
21470 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
21472
21473 app.column_selector_index = 1;
21475 app.handle_action(Action::ToggleColumn);
21476 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21477
21478 app.column_selector_index = 2;
21480 app.handle_action(Action::ToggleColumn);
21481 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21482
21483 app.column_selector_index = 3;
21485 app.handle_action(Action::ToggleColumn);
21486 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21487
21488 app.column_selector_index = 1;
21490 app.handle_action(Action::ToggleColumn);
21491 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21492 }
21493
21494 #[test]
21495 fn test_cloudtrail_resources_preferences_tab_cycles() {
21496 let mut app = App::new_without_client("default".to_string(), None);
21497 app.service_selected = true;
21498 app.current_service = Service::CloudTrailEvents;
21499 app.mode = Mode::ColumnSelector;
21500 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21501 event_name: "PutObject".to_string(),
21502 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21503 username: "user".to_string(),
21504 event_source: "s3.amazonaws.com".to_string(),
21505 resource_type: "AWS::S3::Bucket".to_string(),
21506 resource_name: "my-bucket".to_string(),
21507 read_only: "false".to_string(),
21508 aws_region: "us-east-1".to_string(),
21509 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21510 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21511 source_ip_address: "192.0.2.1".to_string(),
21512 error_code: "".to_string(),
21513 request_id: "req-123".to_string(),
21514 event_type: "AwsApiCall".to_string(),
21515 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21516 });
21517 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21518 app.column_selector_index = 1;
21519
21520 app.handle_action(Action::NextPreferences);
21522 assert_eq!(app.column_selector_index, 0);
21523
21524 app.column_selector_index = 1;
21526 app.handle_action(Action::PrevPreferences);
21527 assert_eq!(app.column_selector_index, 0);
21528 }
21529
21530 #[test]
21531 fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
21532 let visible_cols_3 = 3;
21540 let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
21541 assert_eq!(height_3, 7);
21542
21543 let visible_cols_2 = 2;
21545 let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
21546 assert_eq!(height_2, 6);
21547
21548 let visible_cols_1 = 1;
21550 let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
21551 assert_eq!(height_1, 5);
21552 }
21553
21554 #[test]
21555 fn test_cloudtrail_default_focus_is_resources() {
21556 let app = App::new_without_client("default".to_string(), None);
21557 assert_eq!(
21558 app.cloudtrail_state.detail_focus,
21559 CloudTrailDetailFocus::Resources
21560 );
21561 }
21562
21563 fn setup_cloudtrail_pagination_test() -> App {
21564 let mut app = App::new_without_client("default".to_string(), None);
21565 app.service_selected = true;
21566 app.current_service = Service::CloudTrailEvents;
21567 app.mode = Mode::FilterInput;
21568 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21569 app.cloudtrail_state.table.page_size = PageSize::Ten;
21570 app.cloudtrail_state.table.items = (0..25)
21571 .map(|i| CloudTrailEvent {
21572 event_name: format!("Event{}", i),
21573 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21574 username: "user".to_string(),
21575 event_source: "s3.amazonaws.com".to_string(),
21576 resource_type: "Bucket".to_string(),
21577 resource_name: "bucket".to_string(),
21578 read_only: "false".to_string(),
21579 aws_region: "us-east-1".to_string(),
21580 event_id: "id".to_string(),
21581 access_key_id: "key".to_string(),
21582 source_ip_address: "1.2.3.4".to_string(),
21583 error_code: "".to_string(),
21584 request_id: "req".to_string(),
21585 event_type: "AwsApiCall".to_string(),
21586 cloud_trail_event_json: "{}".to_string(),
21587 })
21588 .collect();
21589 app
21590 }
21591
21592 #[test]
21593 fn test_cloudtrail_pagination_navigation() {
21594 let mut app = setup_cloudtrail_pagination_test();
21596 assert_eq!(app.cloudtrail_state.table.selected, 0);
21597 app.handle_action(Action::NextItem);
21598 assert_eq!(app.cloudtrail_state.table.selected, 10);
21599 app.handle_action(Action::NextItem);
21600 assert_eq!(app.cloudtrail_state.table.selected, 20);
21601 app.handle_action(Action::NextItem);
21602 assert_eq!(app.cloudtrail_state.table.selected, 20);
21603
21604 app.handle_action(Action::PrevItem);
21606 assert_eq!(app.cloudtrail_state.table.selected, 10);
21607 app.handle_action(Action::PrevItem);
21608 assert_eq!(app.cloudtrail_state.table.selected, 0);
21609 app.handle_action(Action::PrevItem);
21610 assert_eq!(app.cloudtrail_state.table.selected, 0);
21611 }
21612
21613 #[test]
21614 fn test_cloudtrail_pagination_navigation_right_arrow() {
21615 let mut app = setup_cloudtrail_pagination_test();
21616 app.current_service = Service::CloudTrailEvents;
21617 app.mode = Mode::FilterInput;
21618 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21619 app.cloudtrail_state.table.page_size = PageSize::Ten;
21620
21621 app.cloudtrail_state.table.items = (0..25)
21623 .map(|i| CloudTrailEvent {
21624 event_name: format!("Event{}", i),
21625 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21626 username: "user".to_string(),
21627 event_source: "s3.amazonaws.com".to_string(),
21628 resource_type: "Bucket".to_string(),
21629 resource_name: "bucket".to_string(),
21630 read_only: "false".to_string(),
21631 aws_region: "us-east-1".to_string(),
21632 event_id: "id".to_string(),
21633 access_key_id: "key".to_string(),
21634 source_ip_address: "1.2.3.4".to_string(),
21635 error_code: "".to_string(),
21636 request_id: "req".to_string(),
21637 event_type: "AwsApiCall".to_string(),
21638 cloud_trail_event_json: "{}".to_string(),
21639 })
21640 .collect();
21641
21642 assert_eq!(app.cloudtrail_state.table.selected, 0);
21644
21645 app.handle_action(Action::NextItem);
21647 assert_eq!(app.cloudtrail_state.table.selected, 10);
21648
21649 app.handle_action(Action::NextItem);
21651 assert_eq!(app.cloudtrail_state.table.selected, 20);
21652
21653 app.handle_action(Action::NextItem);
21655 assert_eq!(app.cloudtrail_state.table.selected, 20);
21656 }
21657
21658 #[test]
21659 fn test_cloudtrail_pagination_navigation_left_arrow() {
21660 let mut app = App::new_without_client("default".to_string(), None);
21661 app.service_selected = true;
21662 app.current_service = Service::CloudTrailEvents;
21663 app.mode = Mode::FilterInput;
21664 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21665 app.cloudtrail_state.table.page_size = PageSize::Ten;
21666
21667 app.cloudtrail_state.table.items = (0..25)
21669 .map(|i| CloudTrailEvent {
21670 event_name: format!("Event{}", i),
21671 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21672 username: "user".to_string(),
21673 event_source: "s3.amazonaws.com".to_string(),
21674 resource_type: "Bucket".to_string(),
21675 resource_name: "bucket".to_string(),
21676 read_only: "false".to_string(),
21677 aws_region: "us-east-1".to_string(),
21678 event_id: "id".to_string(),
21679 access_key_id: "key".to_string(),
21680 source_ip_address: "1.2.3.4".to_string(),
21681 error_code: "".to_string(),
21682 request_id: "req".to_string(),
21683 event_type: "AwsApiCall".to_string(),
21684 cloud_trail_event_json: "{}".to_string(),
21685 })
21686 .collect();
21687
21688 app.cloudtrail_state.table.selected = 20;
21690
21691 app.handle_action(Action::PrevItem);
21693 assert_eq!(app.cloudtrail_state.table.selected, 10);
21694
21695 app.handle_action(Action::PrevItem);
21697 assert_eq!(app.cloudtrail_state.table.selected, 0);
21698
21699 app.handle_action(Action::PrevItem);
21701 assert_eq!(app.cloudtrail_state.table.selected, 0);
21702 }
21703
21704 #[test]
21705 fn test_cloudtrail_arrow_navigation() {
21706 let mut app = App::new_without_client("default".to_string(), None);
21707 app.service_selected = true;
21708 app.current_service = Service::CloudTrailEvents;
21709 app.mode = Mode::Normal;
21710
21711 app.cloudtrail_state.table.items = (0..5)
21713 .map(|i| CloudTrailEvent {
21714 event_name: format!("Event{}", i),
21715 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21716 username: "user".to_string(),
21717 event_source: "s3.amazonaws.com".to_string(),
21718 resource_type: "Bucket".to_string(),
21719 resource_name: "bucket".to_string(),
21720 read_only: "false".to_string(),
21721 aws_region: "us-east-1".to_string(),
21722 event_id: "id".to_string(),
21723 access_key_id: "key".to_string(),
21724 source_ip_address: "1.2.3.4".to_string(),
21725 error_code: "".to_string(),
21726 request_id: "req".to_string(),
21727 event_type: "AwsApiCall".to_string(),
21728 cloud_trail_event_json: "{}".to_string(),
21729 })
21730 .collect();
21731
21732 app.handle_action(Action::NextItem);
21734 assert_eq!(app.cloudtrail_state.table.selected, 1);
21735
21736 app.handle_action(Action::NextItem);
21737 assert_eq!(app.cloudtrail_state.table.selected, 2);
21738
21739 app.handle_action(Action::PrevItem);
21741 assert_eq!(app.cloudtrail_state.table.selected, 1);
21742 }
21743
21744 #[test]
21745 fn test_cloudtrail_page_down_navigation() {
21746 let mut app = App::new_without_client("default".to_string(), None);
21747 app.service_selected = true;
21748 app.current_service = Service::CloudTrailEvents;
21749 app.mode = Mode::Normal;
21750 app.cloudtrail_state.table.page_size = PageSize::Ten;
21751
21752 app.cloudtrail_state.table.items = (0..25)
21754 .map(|i| CloudTrailEvent {
21755 event_name: format!("Event{}", i),
21756 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21757 username: "user".to_string(),
21758 event_source: "s3.amazonaws.com".to_string(),
21759 resource_type: "Bucket".to_string(),
21760 resource_name: "bucket".to_string(),
21761 read_only: "false".to_string(),
21762 aws_region: "us-east-1".to_string(),
21763 event_id: "id".to_string(),
21764 access_key_id: "key".to_string(),
21765 source_ip_address: "1.2.3.4".to_string(),
21766 error_code: "".to_string(),
21767 request_id: "req".to_string(),
21768 event_type: "AwsApiCall".to_string(),
21769 cloud_trail_event_json: "{}".to_string(),
21770 })
21771 .collect();
21772
21773 app.handle_action(Action::PageDown);
21775 assert_eq!(app.cloudtrail_state.table.selected, 10);
21776
21777 app.handle_action(Action::PageDown);
21778 assert_eq!(app.cloudtrail_state.table.selected, 20);
21779 }
21780
21781 #[test]
21782 fn test_cloudtrail_page_up_navigation() {
21783 let mut app = App::new_without_client("default".to_string(), None);
21784 app.service_selected = true;
21785 app.current_service = Service::CloudTrailEvents;
21786 app.mode = Mode::Normal;
21787 app.cloudtrail_state.table.page_size = PageSize::Ten;
21788
21789 app.cloudtrail_state.table.items = (0..25)
21791 .map(|i| CloudTrailEvent {
21792 event_name: format!("Event{}", i),
21793 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21794 username: "user".to_string(),
21795 event_source: "s3.amazonaws.com".to_string(),
21796 resource_type: "Bucket".to_string(),
21797 resource_name: "bucket".to_string(),
21798 read_only: "false".to_string(),
21799 aws_region: "us-east-1".to_string(),
21800 event_id: "id".to_string(),
21801 access_key_id: "key".to_string(),
21802 source_ip_address: "1.2.3.4".to_string(),
21803 error_code: "".to_string(),
21804 request_id: "req".to_string(),
21805 event_type: "AwsApiCall".to_string(),
21806 cloud_trail_event_json: "{}".to_string(),
21807 })
21808 .collect();
21809
21810 app.cloudtrail_state.table.selected = 20;
21812
21813 app.handle_action(Action::PageUp);
21815 assert_eq!(app.cloudtrail_state.table.selected, 10);
21816
21817 app.handle_action(Action::PageUp);
21818 assert_eq!(app.cloudtrail_state.table.selected, 0);
21819 }
21820
21821 #[test]
21822 fn test_cloudtrail_page_size_change_updates_display() {
21823 let mut app = App::new_without_client("default".to_string(), None);
21824 app.service_selected = true;
21825 app.current_service = Service::CloudTrailEvents;
21826 app.mode = Mode::ColumnSelector;
21827
21828 app.cloudtrail_state.table.items = (0..50)
21830 .map(|i| CloudTrailEvent {
21831 event_name: format!("Event{}", i),
21832 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21833 username: "user".to_string(),
21834 event_source: "s3.amazonaws.com".to_string(),
21835 resource_type: "Bucket".to_string(),
21836 resource_name: "bucket".to_string(),
21837 read_only: "false".to_string(),
21838 aws_region: "us-east-1".to_string(),
21839 event_id: "id".to_string(),
21840 access_key_id: "key".to_string(),
21841 source_ip_address: "1.2.3.4".to_string(),
21842 error_code: "".to_string(),
21843 request_id: "req".to_string(),
21844 event_type: "AwsApiCall".to_string(),
21845 cloud_trail_event_json: "{}".to_string(),
21846 })
21847 .collect();
21848
21849 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
21851
21852 app.column_selector_index = 17;
21854 app.handle_action(Action::ToggleColumn);
21855
21856 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
21857 }
21859
21860 #[test]
21861 fn test_cloudtrail_all_columns_toggleable() {
21862 let mut app = App::new_without_client("default".to_string(), None);
21863 app.service_selected = true;
21864 app.current_service = Service::CloudTrailEvents;
21865 app.mode = Mode::ColumnSelector;
21866
21867 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
21869
21870 for idx in 1..=14 {
21873 app.column_selector_index = idx;
21874 let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
21875 let initial_count = initial_visible.len();
21876
21877 let col = app.cloudtrail_event_column_ids.get(idx - 1).unwrap();
21879 let is_visible = initial_visible.contains(col);
21880
21881 app.handle_action(Action::ToggleColumn);
21882
21883 if is_visible && initial_count == 1 {
21885 assert_eq!(
21886 app.cloudtrail_event_visible_column_ids, initial_visible,
21887 "Last visible column at index {} should not be toggleable",
21888 idx
21889 );
21890 } else {
21891 assert_ne!(
21893 app.cloudtrail_event_visible_column_ids, initial_visible,
21894 "Column at index {} should be toggleable when not the last one",
21895 idx
21896 );
21897 }
21898 }
21899 }
21900
21901 #[test]
21902 fn test_cloudtrail_readonly_column_toggleable() {
21903 use crate::cloudtrail::events::CloudTrailEventColumn;
21904
21905 let mut app = App::new_without_client("default".to_string(), None);
21906 app.service_selected = true;
21907 app.current_service = Service::CloudTrailEvents;
21908 app.mode = Mode::ColumnSelector;
21909
21910 let readonly_id = CloudTrailEventColumn::ReadOnly.id();
21913
21914 assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
21916
21917 assert!(!app
21919 .cloudtrail_event_visible_column_ids
21920 .contains(&readonly_id));
21921
21922 app.column_selector_index = 7;
21924 app.handle_action(Action::ToggleColumn);
21925
21926 assert!(
21928 app.cloudtrail_event_visible_column_ids
21929 .contains(&readonly_id),
21930 "ReadOnly column should be toggleable at index 7"
21931 );
21932
21933 app.handle_action(Action::ToggleColumn);
21935
21936 assert!(!app
21938 .cloudtrail_event_visible_column_ids
21939 .contains(&readonly_id));
21940 }
21941
21942 #[test]
21943 fn test_cloudtrail_pagination_limits_displayed_items() {
21944 let mut app = App::new_without_client("default".to_string(), None);
21945 app.service_selected = true;
21946 app.current_service = Service::CloudTrailEvents;
21947
21948 app.cloudtrail_state.table.items = (0..50)
21950 .map(|i| CloudTrailEvent {
21951 event_name: format!("Event{}", i),
21952 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21953 username: "user".to_string(),
21954 event_source: "s3.amazonaws.com".to_string(),
21955 resource_type: "Bucket".to_string(),
21956 resource_name: "bucket".to_string(),
21957 read_only: "false".to_string(),
21958 aws_region: "us-east-1".to_string(),
21959 event_id: "id".to_string(),
21960 access_key_id: "key".to_string(),
21961 source_ip_address: "1.2.3.4".to_string(),
21962 error_code: "".to_string(),
21963 request_id: "req".to_string(),
21964 event_type: "AwsApiCall".to_string(),
21965 cloud_trail_event_json: "{}".to_string(),
21966 })
21967 .collect();
21968
21969 app.cloudtrail_state.table.page_size = PageSize::Ten;
21971
21972 use ratatui::backend::TestBackend;
21974 use ratatui::Terminal;
21975
21976 let backend = TestBackend::new(100, 30);
21977 let mut terminal = Terminal::new(backend).unwrap();
21978
21979 terminal
21980 .draw(|frame| {
21981 let area = frame.area();
21982 crate::ui::cloudtrail::render_events(frame, &app, area);
21983 })
21984 .unwrap();
21985
21986 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
21989 }
21990
21991 #[test]
21992 fn test_cloudtrail_readonly_column_selectable_in_preferences() {
21993 let mut app = App::new_without_client("default".to_string(), None);
21994 app.service_selected = true;
21995 app.current_service = Service::CloudTrailEvents;
21996 app.mode = Mode::ColumnSelector;
21997
21998 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22000
22001 app.column_selector_index = 7;
22003
22004 let max_index = app.get_column_selector_max();
22006 assert!(
22007 app.column_selector_index <= max_index,
22008 "Read-only column index {} should be <= max {}",
22009 app.column_selector_index,
22010 max_index
22011 );
22012
22013 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22015 let col = CloudTrailEventColumn::from_id(col_id);
22016 assert!(col.is_some(), "Column should exist at index 7");
22017 assert_eq!(
22018 col.unwrap().default_name(),
22019 "Read-only",
22020 "Column at index 7 should be Read-only"
22021 );
22022 }
22023
22024 #[test]
22025 fn test_cloudtrail_navigate_to_readonly_column() {
22026 let mut app = App::new_without_client("default".to_string(), None);
22027 app.service_selected = true;
22028 app.current_service = Service::CloudTrailEvents;
22029 app.mode = Mode::ColumnSelector;
22030 app.column_selector_index = 1; println!(
22034 "cloudtrail_event_column_ids.len() = {}",
22035 app.cloudtrail_event_column_ids.len()
22036 );
22037 assert_eq!(
22038 app.cloudtrail_event_column_ids.len(),
22039 14,
22040 "Should have 14 columns"
22041 );
22042
22043 let column_count = app.get_column_count();
22045 println!("Column count from get_column_count(): {}", column_count);
22046 assert_eq!(column_count, 14, "Column count should be 14");
22047
22048 assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
22050 assert!(app.is_blank_row_index(15), "Index 15 should be blank");
22051
22052 for _ in 0..6 {
22054 app.handle_action(Action::NextItem);
22055 }
22056
22057 assert_eq!(
22058 app.column_selector_index, 7,
22059 "Should navigate to Read-only column at index 7"
22060 );
22061
22062 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22064 let col = CloudTrailEventColumn::from_id(col_id).unwrap();
22065 assert_eq!(col.default_name(), "Read-only");
22066 }
22067
22068 #[test]
22069 fn test_cloudtrail_navigate_beyond_loaded_items() {
22070 let mut app = App::new_without_client("default".to_string(), None);
22071 app.service_selected = true;
22072 app.current_service = Service::CloudTrailEvents;
22073
22074 app.cloudtrail_state.table.items = (0..50)
22076 .map(|i| CloudTrailEvent {
22077 event_name: format!("Event{}", i),
22078 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22079 username: "user".to_string(),
22080 event_source: "s3.amazonaws.com".to_string(),
22081 resource_type: "Bucket".to_string(),
22082 resource_name: "bucket".to_string(),
22083 read_only: "false".to_string(),
22084 aws_region: "us-east-1".to_string(),
22085 event_id: "id".to_string(),
22086 access_key_id: "key".to_string(),
22087 source_ip_address: "1.2.3.4".to_string(),
22088 error_code: "".to_string(),
22089 request_id: "req".to_string(),
22090 event_type: "AwsApiCall".to_string(),
22091 cloud_trail_event_json: "{}".to_string(),
22092 })
22093 .collect();
22094
22095 let initial_selected = app.cloudtrail_state.table.selected;
22098 app.page_input = "10".to_string();
22099 app.go_to_page(10);
22100
22101 assert_eq!(
22103 app.cloudtrail_state.table.selected, initial_selected,
22104 "Should ignore navigation to page 10 when only 1 page is loaded"
22105 );
22106
22107 app.go_to_page(2);
22109 let page_size = app.cloudtrail_state.table.page_size.value();
22110 assert_eq!(
22111 app.cloudtrail_state.table.selected, page_size,
22112 "Should allow navigation to page 2 (loaded + 1)"
22113 );
22114 }
22115
22116 #[test]
22117 fn test_cloudtrail_page_change_resets_expansion() {
22118 let mut app = App::new_without_client("default".to_string(), None);
22119 app.service_selected = true;
22120 app.current_service = Service::CloudTrailEvents;
22121
22122 app.cloudtrail_state.table.items = (0..100)
22124 .map(|i| CloudTrailEvent {
22125 event_name: format!("Event{}", i),
22126 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22127 username: "user".to_string(),
22128 event_source: "s3.amazonaws.com".to_string(),
22129 resource_type: "Bucket".to_string(),
22130 resource_name: "bucket".to_string(),
22131 read_only: "false".to_string(),
22132 aws_region: "us-east-1".to_string(),
22133 event_id: "id".to_string(),
22134 access_key_id: "key".to_string(),
22135 source_ip_address: "1.2.3.4".to_string(),
22136 error_code: "".to_string(),
22137 request_id: "req".to_string(),
22138 event_type: "AwsApiCall".to_string(),
22139 cloud_trail_event_json: "{}".to_string(),
22140 })
22141 .collect();
22142
22143 app.cloudtrail_state.table.expanded_item = Some(5);
22145 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
22146
22147 app.go_to_page(2);
22149
22150 assert_eq!(
22152 app.cloudtrail_state.table.expanded_item, None,
22153 "Page change should reset expanded_item"
22154 );
22155 }
22156
22157 #[test]
22158 fn test_lambda_application_resources_have_columns() {
22159 let app = test_app();
22160
22161 assert_eq!(app.lambda_resource_visible_column_ids.len(), 4);
22163 assert_eq!(app.lambda_resource_column_ids.len(), 4);
22164
22165 assert!(app
22167 .lambda_resource_visible_column_ids
22168 .contains(&"column.lambda.resource.logical_id"));
22169 assert!(app
22170 .lambda_resource_column_ids
22171 .contains(&"column.lambda.resource.logical_id"));
22172 }
22173
22174 #[test]
22175 fn test_lambda_functions_list_right_expands_left_collapses() {
22176 use crate::lambda::Function;
22177 let mut app = test_app();
22178 app.mode = Mode::Normal;
22179 app.current_service = Service::LambdaFunctions;
22180 app.service_selected = true;
22181 app.lambda_state.table.items = vec![Function {
22182 name: "fn1".to_string(),
22183 arn: "arn::fn1".to_string(),
22184 application: None,
22185 description: String::new(),
22186 package_type: String::new(),
22187 runtime: "python3.12".to_string(),
22188 architecture: String::new(),
22189 code_size: 0,
22190 code_sha256: String::new(),
22191 memory_mb: 128,
22192 timeout_seconds: 3,
22193 last_modified: String::new(),
22194 layers: vec![],
22195 }];
22196 app.lambda_state.table.selected = 0;
22197
22198 app.handle_action(Action::NextPane);
22200 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
22201
22202 app.handle_action(Action::NextPane);
22204 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
22205
22206 app.handle_action(Action::CollapseRow);
22208 assert_eq!(app.lambda_state.table.expanded_item, None);
22209 }
22210
22211 #[test]
22212 fn test_lambda_applications_list_right_expands_left_collapses() {
22213 use crate::lambda::Application;
22214 let mut app = test_app();
22215 app.mode = Mode::Normal;
22216 app.current_service = Service::LambdaApplications;
22217 app.service_selected = true;
22218 app.lambda_application_state.table.items = vec![Application {
22219 name: "my-app".to_string(),
22220 arn: String::new(),
22221 description: String::new(),
22222 status: "ACTIVE".to_string(),
22223 last_modified: String::new(),
22224 }];
22225 app.lambda_application_state.table.selected = 0;
22226
22227 app.handle_action(Action::NextPane);
22229 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
22230
22231 app.handle_action(Action::NextPane);
22233 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
22234
22235 app.handle_action(Action::CollapseRow);
22237 assert_eq!(app.lambda_application_state.table.expanded_item, None);
22238 }
22239
22240 #[test]
22241 fn test_lambda_application_enter_on_deployments_tab_does_not_switch_to_overview() {
22242 use crate::lambda::Application;
22243 let mut app = test_app();
22244 app.mode = Mode::Normal;
22245 app.current_service = Service::LambdaApplications;
22246 app.service_selected = true;
22247 app.lambda_application_state.table.items = vec![Application {
22248 name: "my-app".to_string(),
22249 arn: String::new(),
22250 description: String::new(),
22251 status: "ACTIVE".to_string(),
22252 last_modified: String::new(),
22253 }];
22254
22255 app.handle_action(Action::Select);
22257 assert!(app.lambda_application_state.current_application.is_some());
22258
22259 use crate::ui::lambda::ApplicationDetailTab as LambdaApplicationDetailTab;
22261 app.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Deployments;
22262 assert_eq!(
22263 app.lambda_application_state.detail_tab,
22264 LambdaApplicationDetailTab::Deployments
22265 );
22266
22267 app.handle_action(Action::Select);
22269 assert_eq!(
22270 app.lambda_application_state.detail_tab,
22271 LambdaApplicationDetailTab::Deployments
22272 );
22273 }
22274
22275 #[test]
22276 fn test_lambda_applications_pagination_focus_left_right_jump_pages() {
22277 use crate::lambda::Application;
22278 let mut app = test_app();
22279 app.current_service = Service::LambdaApplications;
22280 app.service_selected = true;
22281 app.mode = Mode::FilterInput;
22282 app.lambda_application_state.input_focus = InputFocus::Pagination;
22283
22284 app.lambda_application_state.table.items = (0..100)
22286 .map(|i| Application {
22287 name: format!("app{:03}", i),
22288 arn: String::new(),
22289 description: String::new(),
22290 status: "ACTIVE".to_string(),
22291 last_modified: String::new(),
22292 })
22293 .collect();
22294 app.lambda_application_state.table.selected = 0;
22295
22296 app.handle_action(Action::PageDown);
22298 assert_eq!(
22299 app.lambda_application_state.table.selected, 50,
22300 "Right arrow with pagination focus should jump to page 2"
22301 );
22302
22303 app.handle_action(Action::PageUp);
22305 assert_eq!(
22306 app.lambda_application_state.table.selected, 0,
22307 "Left arrow with pagination focus should jump back to page 1"
22308 );
22309
22310 app.handle_action(Action::NextItem);
22312 assert_eq!(
22313 app.lambda_application_state.table.selected, 0,
22314 "Down arrow must not move table when pagination is focused"
22315 );
22316 app.lambda_application_state.table.selected = 5;
22317 app.handle_action(Action::PrevItem);
22318 assert_eq!(
22319 app.lambda_application_state.table.selected, 5,
22320 "Up arrow must not move table when pagination is focused"
22321 );
22322 }
22323
22324 #[test]
22325 fn test_cloudwatch_alarms_enter_drills_down() {
22326 let mut app = test_app();
22327 app.mode = Mode::Normal;
22328 app.current_service = Service::CloudWatchAlarms;
22329 app.service_selected = true;
22330 app.view_mode = ViewMode::List;
22331
22332 app.alarms_state.table.items = vec![Alarm {
22334 name: "test-alarm".to_string(),
22335 state: "ALARM".to_string(),
22336 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22337 description: "Test alarm".to_string(),
22338 metric_name: "CPUUtilization".to_string(),
22339 namespace: "AWS/EC2".to_string(),
22340 statistic: "Average".to_string(),
22341 period: 300,
22342 comparison_operator: "GreaterThanThreshold".to_string(),
22343 threshold: 80.0,
22344 actions_enabled: true,
22345 state_reason: "Threshold crossed".to_string(),
22346 resource: "".to_string(),
22347 dimensions: "".to_string(),
22348 expression: "".to_string(),
22349 alarm_type: "MetricAlarm".to_string(),
22350 cross_account: "".to_string(),
22351 }];
22352
22353 assert!(app.alarms_state.current_alarm.is_none());
22354 assert_eq!(app.view_mode, ViewMode::List);
22355 assert!(!app.alarms_state.metrics_loading);
22356
22357 app.handle_action(Action::Select);
22359
22360 assert_eq!(
22361 app.alarms_state.current_alarm,
22362 Some("test-alarm".to_string())
22363 );
22364 assert_eq!(app.view_mode, ViewMode::Detail);
22365 assert!(
22366 app.alarms_state.metrics_loading,
22367 "Should trigger metrics loading"
22368 );
22369 }
22370
22371 #[test]
22372 fn test_cloudwatch_alarms_metric_data_renders() {
22373 let mut app = test_app();
22374 app.mode = Mode::Normal;
22375 app.current_service = Service::CloudWatchAlarms;
22376 app.service_selected = true;
22377 app.view_mode = ViewMode::Detail;
22378
22379 app.alarms_state.table.items = vec![Alarm {
22381 name: "test-alarm".to_string(),
22382 state: "ALARM".to_string(),
22383 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22384 description: "Test alarm".to_string(),
22385 metric_name: "CPUUtilization".to_string(),
22386 namespace: "AWS/EC2".to_string(),
22387 statistic: "Average".to_string(),
22388 period: 300,
22389 comparison_operator: "GreaterThanThreshold".to_string(),
22390 threshold: 80.0,
22391 actions_enabled: true,
22392 state_reason: "Threshold crossed".to_string(),
22393 resource: "".to_string(),
22394 dimensions: "".to_string(),
22395 expression: "".to_string(),
22396 alarm_type: "MetricAlarm".to_string(),
22397 cross_account: "".to_string(),
22398 }];
22399
22400 app.alarms_state.current_alarm = Some("test-alarm".to_string());
22401
22402 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0), (3000, 70.0)];
22404 app.alarms_state.metrics_loading = false;
22405
22406 assert!(!app.alarms_state.metric_data.is_empty());
22408 assert_eq!(app.alarms_state.metric_data.len(), 3);
22409 assert!(!app.alarms_state.metrics_loading);
22410 }
22411
22412 #[test]
22413 fn test_cloudwatch_alarms_back_clears_metrics() {
22414 let mut app = test_app();
22415 app.mode = Mode::Normal;
22416 app.current_service = Service::CloudWatchAlarms;
22417 app.service_selected = true;
22418 app.view_mode = ViewMode::Detail;
22419
22420 app.alarms_state.current_alarm = Some("test-alarm".to_string());
22422 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
22423
22424 assert!(!app.alarms_state.metric_data.is_empty());
22425
22426 app.handle_action(Action::GoBack);
22428
22429 assert!(app.alarms_state.current_alarm.is_none());
22431 assert!(app.alarms_state.metric_data.is_empty());
22432 assert_eq!(app.view_mode, ViewMode::List);
22433 }
22434
22435 #[test]
22436 fn test_cloudwatch_alarms_refresh_reloads_metrics() {
22437 let mut app = test_app();
22438 app.mode = Mode::Normal;
22439 app.current_service = Service::CloudWatchAlarms;
22440 app.service_selected = true;
22441 app.view_mode = ViewMode::Detail;
22442
22443 app.alarms_state.current_alarm = Some("test-alarm".to_string());
22445 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
22446 app.alarms_state.metrics_loading = false;
22447
22448 assert!(!app.alarms_state.metric_data.is_empty());
22449 assert!(!app.alarms_state.metrics_loading);
22450
22451 app.handle_action(Action::Refresh);
22453
22454 assert!(!app.alarms_state.metric_data.is_empty());
22456 assert!(app.alarms_state.metrics_loading);
22457 }
22458
22459 #[test]
22460 fn test_cloudwatch_alarms_right_arrow_expands() {
22461 let mut app = test_app();
22462 app.mode = Mode::Normal;
22463 app.current_service = Service::CloudWatchAlarms;
22464 app.service_selected = true;
22465 app.view_mode = ViewMode::List;
22466
22467 app.alarms_state.table.items = vec![Alarm {
22469 name: "test-alarm".to_string(),
22470 state: "ALARM".to_string(),
22471 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22472 description: "Test alarm".to_string(),
22473 metric_name: "CPUUtilization".to_string(),
22474 namespace: "AWS/EC2".to_string(),
22475 statistic: "Average".to_string(),
22476 period: 300,
22477 comparison_operator: "GreaterThanThreshold".to_string(),
22478 threshold: 80.0,
22479 actions_enabled: true,
22480 state_reason: "Threshold crossed".to_string(),
22481 resource: "".to_string(),
22482 dimensions: "".to_string(),
22483 expression: "".to_string(),
22484 alarm_type: "MetricAlarm".to_string(),
22485 cross_account: "".to_string(),
22486 }];
22487
22488 assert!(!app.alarms_state.table.is_expanded());
22489
22490 app.handle_action(Action::NextPane);
22492 assert!(app.alarms_state.table.is_expanded());
22493
22494 app.handle_action(Action::PrevPane);
22496 assert!(!app.alarms_state.table.is_expanded());
22497 }
22498
22499 #[test]
22500 fn test_cloudwatch_alarms_tab_switches_tabs() {
22501 let mut app = test_app();
22502 app.mode = Mode::Normal;
22503 app.current_service = Service::CloudWatchAlarms;
22504 app.service_selected = true;
22505
22506 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
22508
22509 app.handle_action(Action::NextDetailTab);
22511 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
22512
22513 app.handle_action(Action::NextDetailTab);
22515 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
22516
22517 app.handle_action(Action::PrevDetailTab);
22519 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
22520 }
22521
22522 #[test]
22523 fn test_column_toggle_prevents_hiding_last_column() {
22524 let mut app = test_app();
22525 app.mode = Mode::Normal;
22526 app.current_service = Service::CloudWatchAlarms;
22527 app.service_selected = true;
22528
22529 app.cw_alarm_visible_column_ids = vec!["column.cw.alarm.name"];
22531
22532 app.handle_action(Action::OpenColumnSelector);
22534 assert_eq!(app.mode, Mode::ColumnSelector);
22535
22536 app.column_selector_index = 1;
22538
22539 app.handle_action(Action::ToggleColumn);
22541
22542 assert_eq!(app.cw_alarm_visible_column_ids.len(), 1);
22544 assert_eq!(app.cw_alarm_visible_column_ids[0], "column.cw.alarm.name");
22545 }
22546}