1use crate::apig::api::Column as ApigColumn;
2use crate::apig::resource::Column as ResourceColumn;
3use crate::apig::resource::Resource as ApigResource;
4use crate::apig::route::Column as RouteColumn;
5use crate::apig::route::Route;
6pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
7use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
8use crate::cloudtrail::{CloudTrailEvent, CloudTrailEventColumn, EventResourceColumn};
9use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
10pub use crate::cw::insights::InsightsFocus;
11use crate::cw::insights::InsightsState;
12pub use crate::cw::{Alarm, AlarmColumn};
13pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
14use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
15use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
16use crate::iam::{
17 self, GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag,
18 UserColumn, UserTag as IamUserTag,
19};
20#[cfg(test)]
21use crate::iam::{IamRole, IamUser, LastAccessedService};
22use crate::keymap::{Action, Mode};
23pub use crate::lambda::{
24 Alias as LambdaAlias, Application as LambdaApplication,
25 ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
26 Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
27 ResourceColumn as LambdaResourceColumn, Version as LambdaVersion,
28};
29pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
30use crate::session::{Session, SessionTab};
31pub use crate::sqs::queue::Column as SqsColumn;
32pub use crate::sqs::trigger::Column as SqsTriggerColumn;
33use crate::sqs::{console_url_queue_detail, console_url_queues};
34#[cfg(test)]
35use crate::sqs::{
36 EventBridgePipe, LambdaTrigger, Queue as SqsQueue, QueueTag as SqsQueueTag, SnsSubscription,
37};
38use crate::table::TableState;
39pub use crate::ui::apig::{
40 filtered_apis, State as ApigState, FILTER_CONTROLS as APIG_FILTER_CONTROLS,
41};
42use crate::ui::cfn::State as CfnStateConstants;
43pub use crate::ui::cfn::{
44 filtered_cloudformation_stacks, filtered_outputs, filtered_parameters, filtered_resources,
45 output_column_ids, parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
46 State as CfnState, StatusFilter as CfnStatusFilter,
47};
48pub use crate::ui::cw::alarms::{
49 AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
50};
51pub use crate::ui::cw::logs::{
52 filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
53 DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
54};
55use crate::ui::ec2;
56use crate::ui::ec2::filtered_ec2_instances;
57pub use crate::ui::ec2::{
58 DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
59 STATE_FILTER as EC2_STATE_FILTER,
60};
61pub use crate::ui::ecr::{
62 filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
63 FILTER_CONTROLS as ECR_FILTER_CONTROLS,
64};
65use crate::ui::iam::{
66 filtered_iam_policies, filtered_iam_roles, filtered_iam_users, filtered_last_accessed,
67 filtered_tags as filtered_iam_tags, filtered_user_tags, GroupTab, RoleTab, State as IamState,
68 UserTab,
69};
70pub use crate::ui::lambda::{
71 filtered_lambda_applications, filtered_lambda_functions,
72 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
73 DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
74};
75use crate::ui::monitoring::MonitoringState;
76pub use crate::ui::s3::{
77 calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
78 ObjectTab as S3ObjectTab, State as S3State,
79};
80pub use crate::ui::sqs::{
81 extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
82 filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
83 State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
84 SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
85};
86use crate::ui::tree::TreeItem;
87pub use crate::ui::{
88 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
89 StreamColumn, StreamSort, TimeUnit,
90};
91#[cfg(test)]
92use rusticity_core::LogStream;
93use rusticity_core::{
94 AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
95 CloudWatchClient, Ec2Client, EcrClient, IamClient, LambdaClient, S3Client, SqsClient,
96};
97use std::collections::HashMap;
98
99#[derive(Clone)]
100pub struct Tab {
101 pub service: Service,
102 pub title: String,
103 pub breadcrumb: String,
104}
105
106pub struct App {
107 pub running: bool,
108 pub mode: Mode,
109 pub config: AwsConfig,
110 pub cloudwatch_client: CloudWatchClient,
111 pub cloudtrail_client: CloudTrailClient,
112 pub s3_client: S3Client,
113 pub sqs_client: SqsClient,
114 pub alarms_client: AlarmsClient,
115 pub ec2_client: Ec2Client,
116 pub ecr_client: EcrClient,
117 pub apig_client: ApiGatewayClient,
118 pub iam_client: IamClient,
119 pub lambda_client: LambdaClient,
120 pub cloudformation_client: CloudFormationClient,
121 pub current_service: Service,
122 pub tabs: Vec<Tab>,
123 pub current_tab: usize,
124 pub tab_picker_selected: usize,
125 pub tab_filter: String,
126 pub pending_key: Option<char>,
127 pub log_groups_state: CloudWatchLogGroupsState,
128 pub insights_state: CloudWatchInsightsState,
129 pub alarms_state: CloudWatchAlarmsState,
130 pub cloudtrail_state: CloudTrailState,
131 pub s3_state: S3State,
132 pub sqs_state: SqsState,
133 pub ec2_state: Ec2State,
134 pub ecr_state: EcrState,
135 pub apig_state: ApigState,
136 pub lambda_state: LambdaState,
137 pub lambda_application_state: LambdaApplicationState,
138 pub cfn_state: CfnState,
139 pub iam_state: IamState,
140 pub service_picker: ServicePickerState,
141 pub service_selected: bool,
142 pub profile: String,
143 pub region: String,
144 pub region_selector_index: usize,
145 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
146 pub cw_log_group_column_ids: Vec<ColumnId>,
147 pub column_selector_index: usize,
148 pub preference_section: Preferences,
149 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
150 pub cw_log_stream_column_ids: Vec<ColumnId>,
151 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
152 pub cw_log_event_column_ids: Vec<ColumnId>,
153 pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
154 pub cw_log_tag_column_ids: Vec<ColumnId>,
155 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
156 pub cw_alarm_column_ids: Vec<ColumnId>,
157 pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
158 pub cloudtrail_event_column_ids: Vec<ColumnId>,
159 pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
160 pub cloudtrail_resource_column_ids: Vec<ColumnId>,
161 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
162 pub s3_bucket_column_ids: Vec<ColumnId>,
163 pub sqs_visible_column_ids: Vec<ColumnId>,
164 pub sqs_column_ids: Vec<ColumnId>,
165 pub ec2_visible_column_ids: Vec<ColumnId>,
166 pub ec2_column_ids: Vec<ColumnId>,
167 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
168 pub ecr_repo_column_ids: Vec<ColumnId>,
169 pub ecr_image_visible_column_ids: Vec<ColumnId>,
170 pub ecr_image_column_ids: Vec<ColumnId>,
171 pub apig_api_visible_column_ids: Vec<ColumnId>,
172 pub apig_api_column_ids: Vec<ColumnId>,
173 pub apig_route_visible_column_ids: Vec<ColumnId>,
174 pub apig_route_column_ids: Vec<ColumnId>,
175 pub apig_resource_visible_column_ids: Vec<ColumnId>,
176 pub apig_resource_column_ids: Vec<ColumnId>,
177 pub lambda_application_visible_column_ids: Vec<ColumnId>,
178 pub lambda_application_column_ids: Vec<ColumnId>,
179 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
180 pub lambda_deployment_column_ids: Vec<ColumnId>,
181 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
182 pub lambda_resource_column_ids: Vec<ColumnId>,
183 pub cfn_visible_column_ids: Vec<ColumnId>,
184 pub cfn_column_ids: Vec<ColumnId>,
185 pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
186 pub cfn_parameter_column_ids: Vec<ColumnId>,
187 pub cfn_output_visible_column_ids: Vec<ColumnId>,
188 pub cfn_output_column_ids: Vec<ColumnId>,
189 pub cfn_resource_visible_column_ids: Vec<ColumnId>,
190 pub cfn_resource_column_ids: Vec<ColumnId>,
191 pub iam_user_visible_column_ids: Vec<ColumnId>,
192 pub iam_user_column_ids: Vec<ColumnId>,
193 pub iam_role_visible_column_ids: Vec<ColumnId>,
194 pub iam_role_column_ids: Vec<ColumnId>,
195 pub iam_group_visible_column_ids: Vec<String>,
196 pub iam_group_column_ids: Vec<String>,
197 pub iam_policy_visible_column_ids: Vec<String>,
198 pub iam_policy_column_ids: Vec<String>,
199 pub view_mode: ViewMode,
200 pub error_message: Option<String>,
201 pub error_scroll: usize,
202 pub page_input: String,
203 pub calendar_date: Option<time::Date>,
204 pub calendar_selecting: CalendarField,
205 pub cursor_pos: usize,
206 pub current_session: Option<Session>,
207 pub sessions: Vec<Session>,
208 pub session_picker_selected: usize,
209 pub session_filter: String,
210 pub session_filter_active: bool,
211 pub region_filter: String,
212 pub region_picker_selected: usize,
213 pub region_filter_active: bool,
214 pub region_latencies: std::collections::HashMap<String, u64>,
215 pub profile_filter: String,
216 pub profile_picker_selected: usize,
217 pub profile_filter_active: bool,
218 pub available_profiles: Vec<AwsProfile>,
219 pub snapshot_requested: bool,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq)]
223pub enum CalendarField {
224 StartDate,
225 EndDate,
226}
227
228pub struct CloudWatchInsightsState {
229 pub insights: InsightsState,
230 pub loading: bool,
231}
232
233pub struct CloudWatchAlarmsState {
234 pub table: TableState<Alarm>,
235 pub current_alarm: Option<String>,
236 pub alarm_tab: AlarmTab,
237 pub view_as: AlarmViewMode,
238 pub wrap_lines: bool,
239 pub sort_column: String,
240 pub sort_direction: SortDirection,
241 pub input_focus: InputFocus,
242 pub metric_data: Vec<(i64, f64)>,
243 pub metrics_loading: bool,
244}
245
246#[derive(Debug, Clone)]
247pub struct CloudTrailState {
248 pub table: TableState<CloudTrailEvent>,
249 pub input_focus: InputFocus,
250 pub current_event: Option<CloudTrailEvent>,
251 pub event_json_scroll: usize,
252 pub detail_focus: CloudTrailDetailFocus,
253 pub resources_expanded_index: Option<usize>,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq)]
257pub enum CloudTrailDetailFocus {
258 Resources,
259 EventRecord,
260}
261
262impl CyclicEnum for CloudTrailDetailFocus {
263 const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
264}
265
266impl PageSize {
267 pub fn value(&self) -> usize {
268 match self {
269 PageSize::Ten => 10,
270 PageSize::TwentyFive => 25,
271 PageSize::Fifty => 50,
272 PageSize::OneHundred => 100,
273 }
274 }
275
276 pub fn next(&self) -> Self {
277 match self {
278 PageSize::Ten => PageSize::TwentyFive,
279 PageSize::TwentyFive => PageSize::Fifty,
280 PageSize::Fifty => PageSize::OneHundred,
281 PageSize::OneHundred => PageSize::Ten,
282 }
283 }
284}
285
286pub struct ServicePickerState {
287 pub filter: String,
288 pub filter_active: bool,
289 pub selected: usize,
290 pub services: Vec<&'static str>,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq)]
294pub enum ViewMode {
295 List,
296 Detail,
297 Events,
298 InsightsResults,
299 PolicyView,
300}
301
302#[derive(Debug, Clone, Copy, PartialEq)]
303pub enum Service {
304 ApiGatewayApis,
305 CloudWatchLogGroups,
306 CloudWatchInsights,
307 CloudWatchAlarms,
308 CloudTrailEvents,
309 S3Buckets,
310 SqsQueues,
311 Ec2Instances,
312 EcrRepositories,
313 LambdaFunctions,
314 LambdaApplications,
315 CloudFormationStacks,
316 IamUsers,
317 IamRoles,
318 IamUserGroups,
319}
320
321impl Service {
322 pub fn name(&self) -> &str {
323 match self {
324 Service::ApiGatewayApis => "API Gateway › APIs",
325 Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
326 Service::CloudWatchInsights => "CloudWatch › Logs Insights",
327 Service::CloudWatchAlarms => "CloudWatch › Alarms",
328 Service::CloudTrailEvents => "CloudTrail › Event History",
329 Service::S3Buckets => "S3 › Buckets",
330 Service::SqsQueues => "SQS › Queues",
331 Service::Ec2Instances => "EC2 › Instances",
332 Service::EcrRepositories => "ECR › Repositories",
333 Service::LambdaFunctions => "Lambda › Functions",
334 Service::LambdaApplications => "Lambda › Applications",
335 Service::CloudFormationStacks => "CloudFormation › Stacks",
336 Service::IamUsers => "IAM › Users",
337 Service::IamRoles => "IAM › Roles",
338 Service::IamUserGroups => "IAM › User Groups",
339 }
340 }
341}
342
343pub(crate) fn copy_to_clipboard(text: &str) {
344 use std::io::Write;
345 use std::process::{Command, Stdio};
346 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
347 if let Some(mut stdin) = child.stdin.take() {
348 let _ = stdin.write_all(text.as_bytes());
349 }
350 let _ = child.wait();
351 }
352}
353
354fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
355 if max > 0 {
356 *selected = (*selected + page_size).min(max - 1);
357 }
358}
359
360fn toggle_iam_preference(
361 idx: usize,
362 column_ids: &[String],
363 visible_column_ids: &mut Vec<String>,
364 page_size: &mut PageSize,
365) {
366 if idx > 0 && idx <= column_ids.len() {
367 if let Some(col) = column_ids.get(idx - 1) {
368 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
369 if visible_column_ids.len() > 1 {
371 visible_column_ids.remove(pos);
372 }
373 } else {
374 visible_column_ids.push(col.clone());
375 }
376 }
377 } else if idx == column_ids.len() + 3 {
378 *page_size = PageSize::Ten;
379 } else if idx == column_ids.len() + 4 {
380 *page_size = PageSize::TwentyFive;
381 } else if idx == column_ids.len() + 5 {
382 *page_size = PageSize::Fifty;
383 }
384}
385
386fn toggle_iam_preference_static(
387 idx: usize,
388 column_ids: &[ColumnId],
389 visible_column_ids: &mut Vec<ColumnId>,
390 page_size: &mut PageSize,
391) {
392 if idx > 0 && idx <= column_ids.len() {
393 if let Some(col) = column_ids.get(idx - 1) {
394 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
395 if visible_column_ids.len() > 1 {
397 visible_column_ids.remove(pos);
398 }
399 } else {
400 visible_column_ids.push(*col);
401 }
402 }
403 } else if idx == column_ids.len() + 3 {
404 *page_size = PageSize::Ten;
405 } else if idx == column_ids.len() + 4 {
406 *page_size = PageSize::TwentyFive;
407 } else if idx == column_ids.len() + 5 {
408 *page_size = PageSize::Fifty;
409 }
410}
411
412fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
413 if idx == base_idx {
414 *page_size = PageSize::Ten;
415 } else if idx == base_idx + 1 {
416 *page_size = PageSize::TwentyFive;
417 } else if idx == base_idx + 2 {
418 *page_size = PageSize::Fifty;
419 }
420}
421
422fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
424 let page_size_idx = num_columns + 2;
425 if *current_idx < page_size_idx {
426 *current_idx = page_size_idx;
427 } else {
428 *current_idx = 0;
429 }
430}
431
432fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
434 let page_size_idx = num_columns + 2;
435 if *current_idx >= page_size_idx {
436 *current_idx = 0;
437 } else {
438 *current_idx = page_size_idx;
439 }
440}
441
442impl App {
443 pub fn get_input_focus(&self) -> InputFocus {
444 InputFocus::Filter
445 }
446
447 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
448 if self.current_service == Service::ApiGatewayApis {
449 if self.apig_state.current_api.is_some()
450 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
451 {
452 Some(&mut self.apig_state.route_filter)
453 } else {
454 Some(&mut self.apig_state.apis.filter)
455 }
456 } else if self.current_service == Service::CloudWatchAlarms {
457 Some(&mut self.alarms_state.table.filter)
458 } else if self.current_service == Service::CloudTrailEvents {
459 Some(&mut self.cloudtrail_state.table.filter)
460 } else if self.current_service == Service::Ec2Instances {
461 if self.ec2_state.current_instance.is_some()
462 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
463 {
464 Some(&mut self.ec2_state.tags.filter)
465 } else {
466 Some(&mut self.ec2_state.table.filter)
467 }
468 } else if self.current_service == Service::S3Buckets {
469 if self.s3_state.current_bucket.is_some() {
470 Some(&mut self.s3_state.object_filter)
471 } else {
472 Some(&mut self.s3_state.buckets.filter)
473 }
474 } else if self.current_service == Service::EcrRepositories {
475 if self.ecr_state.current_repository.is_some() {
476 Some(&mut self.ecr_state.images.filter)
477 } else {
478 Some(&mut self.ecr_state.repositories.filter)
479 }
480 } else if self.current_service == Service::SqsQueues {
481 if self.sqs_state.current_queue.is_some()
482 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
483 {
484 Some(&mut self.sqs_state.triggers.filter)
485 } else if self.sqs_state.current_queue.is_some()
486 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
487 {
488 Some(&mut self.sqs_state.pipes.filter)
489 } else if self.sqs_state.current_queue.is_some()
490 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
491 {
492 Some(&mut self.sqs_state.tags.filter)
493 } else if self.sqs_state.current_queue.is_some()
494 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
495 {
496 Some(&mut self.sqs_state.subscriptions.filter)
497 } else {
498 Some(&mut self.sqs_state.queues.filter)
499 }
500 } else if self.current_service == Service::LambdaFunctions {
501 if self.lambda_state.current_version.is_some()
502 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
503 {
504 Some(&mut self.lambda_state.alias_table.filter)
505 } else if self.lambda_state.current_function.is_some()
506 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
507 {
508 Some(&mut self.lambda_state.version_table.filter)
509 } else if self.lambda_state.current_function.is_some()
510 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
511 {
512 Some(&mut self.lambda_state.alias_table.filter)
513 } else {
514 Some(&mut self.lambda_state.table.filter)
515 }
516 } else if self.current_service == Service::LambdaApplications {
517 if self.lambda_application_state.current_application.is_some() {
518 if self.lambda_application_state.detail_tab
519 == LambdaApplicationDetailTab::Deployments
520 {
521 Some(&mut self.lambda_application_state.deployments.filter)
522 } else {
523 Some(&mut self.lambda_application_state.resources.filter)
524 }
525 } else {
526 Some(&mut self.lambda_application_state.table.filter)
527 }
528 } else if self.current_service == Service::CloudFormationStacks {
529 if self.cfn_state.current_stack.is_some()
530 && self.cfn_state.detail_tab == CfnDetailTab::Resources
531 {
532 Some(&mut self.cfn_state.resources.filter)
533 } else {
534 Some(&mut self.cfn_state.table.filter)
535 }
536 } else if self.current_service == Service::IamUsers {
537 if self.iam_state.current_user.is_some() {
538 if self.iam_state.user_tab == UserTab::Tags {
539 Some(&mut self.iam_state.user_tags.filter)
540 } else {
541 Some(&mut self.iam_state.policies.filter)
542 }
543 } else {
544 Some(&mut self.iam_state.users.filter)
545 }
546 } else if self.current_service == Service::IamRoles {
547 if self.iam_state.current_role.is_some() {
548 if self.iam_state.role_tab == RoleTab::Tags {
549 Some(&mut self.iam_state.tags.filter)
550 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
551 Some(&mut self.iam_state.last_accessed_filter)
552 } else {
553 Some(&mut self.iam_state.policies.filter)
554 }
555 } else {
556 Some(&mut self.iam_state.roles.filter)
557 }
558 } else if self.current_service == Service::IamUserGroups {
559 if self.iam_state.current_group.is_some() {
560 if self.iam_state.group_tab == GroupTab::Permissions {
561 Some(&mut self.iam_state.policies.filter)
562 } else if self.iam_state.group_tab == GroupTab::Users {
563 Some(&mut self.iam_state.group_users.filter)
564 } else {
565 None
566 }
567 } else {
568 Some(&mut self.iam_state.groups.filter)
569 }
570 } else if self.view_mode == ViewMode::List {
571 Some(&mut self.log_groups_state.log_groups.filter)
572 } else if self.view_mode == ViewMode::Detail
573 && self.log_groups_state.detail_tab == DetailTab::LogStreams
574 {
575 Some(&mut self.log_groups_state.stream_filter)
576 } else {
577 None
578 }
579 }
580
581 fn apply_filter_operation<F>(&mut self, op: F)
582 where
583 F: FnOnce(&mut String),
584 {
585 if let Some(filter) = self.get_active_filter_mut() {
586 op(filter);
587 if self.current_service == Service::CloudWatchAlarms {
589 self.alarms_state.table.reset();
590 } else if self.current_service == Service::CloudTrailEvents {
591 self.cloudtrail_state.table.reset();
592 } else if self.current_service == Service::Ec2Instances {
593 self.ec2_state.table.reset();
594 } else if self.current_service == Service::S3Buckets {
595 crate::s3::actions::apply_filter_reset(self);
596 } else if self.current_service == Service::EcrRepositories {
597 crate::ecr::actions::apply_filter_reset(self);
598 } else if self.current_service == Service::SqsQueues {
599 self.sqs_state.queues.reset();
600 } else if self.current_service == Service::LambdaFunctions {
601 crate::lambda::functions::apply_filter_reset(self);
602 } else if self.current_service == Service::LambdaApplications {
603 crate::lambda::applications::apply_filter_reset(self);
604 } else if self.current_service == Service::CloudFormationStacks {
605 self.cfn_state.table.reset();
606 } else if self.current_service == Service::IamUsers {
607 if self.iam_state.current_user.is_some() {
608 self.iam_state.user_tags.reset();
609 self.iam_state.policies.reset();
610 } else {
611 self.iam_state.users.reset();
612 }
613 } else if self.current_service == Service::IamRoles {
614 if self.iam_state.current_role.is_some() {
615 self.iam_state.tags.reset();
616 self.iam_state.policies.reset();
617 } else {
618 self.iam_state.roles.reset();
619 }
620 } else if self.current_service == Service::IamUserGroups {
621 if self.iam_state.current_group.is_some() {
622 self.iam_state.policies.reset();
623 self.iam_state.group_users.reset();
624 } else {
625 self.iam_state.groups.reset();
626 }
627 } else if self.current_service == Service::CloudWatchLogGroups {
628 if self.view_mode == ViewMode::List {
629 self.log_groups_state.log_groups.reset();
630 } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
631 self.log_groups_state.selected_stream = 0;
632 }
633 } else if self.current_service == Service::ApiGatewayApis {
634 self.apig_state.apis.reset();
635 }
636 }
637 }
638
639 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
640 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
641 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
642
643 std::env::set_var("AWS_PROFILE", &profile_name);
644
645 let config = AwsConfig::new(region).await?;
646 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
647 let cloudtrail_client = CloudTrailClient::new(config.clone());
648 let s3_client = S3Client::new(config.clone());
649 let sqs_client = SqsClient::new(config.clone());
650 let alarms_client = AlarmsClient::new(config.clone());
651 let ec2_client = Ec2Client::new(config.clone());
652 let ecr_client = EcrClient::new(config.clone());
653 let apig_client = ApiGatewayClient::new(config.clone());
654 let iam_client = IamClient::new(config.clone());
655 let lambda_client = LambdaClient::new(config.clone());
656 let cloudformation_client = CloudFormationClient::new(config.clone());
657 let region_name = config.region.clone();
658
659 Ok(Self {
660 running: true,
661 mode: Mode::ServicePicker,
662 config,
663 cloudwatch_client,
664 cloudtrail_client,
665 s3_client,
666 sqs_client,
667 alarms_client,
668 ec2_client,
669 ecr_client,
670 apig_client,
671 iam_client,
672 lambda_client,
673 cloudformation_client,
674 current_service: Service::CloudWatchLogGroups,
675 tabs: Vec::new(),
676 current_tab: 0,
677 tab_picker_selected: 0,
678 tab_filter: String::new(),
679 pending_key: None,
680 log_groups_state: CloudWatchLogGroupsState::new(),
681 insights_state: CloudWatchInsightsState::new(),
682 alarms_state: CloudWatchAlarmsState::new(),
683 cloudtrail_state: CloudTrailState {
684 table: TableState::new(),
685 input_focus: InputFocus::Filter,
686 current_event: None,
687 event_json_scroll: 0,
688 detail_focus: CloudTrailDetailFocus::Resources,
689 resources_expanded_index: None,
690 },
691 s3_state: S3State::new(),
692 sqs_state: SqsState::new(),
693 ec2_state: Ec2State::default(),
694 ecr_state: EcrState::new(),
695 apig_state: ApigState::new(),
696 lambda_state: LambdaState::new(),
697 lambda_application_state: LambdaApplicationState::new(),
698 cfn_state: CfnState::new(),
699 iam_state: IamState::new(),
700 service_picker: ServicePickerState::new(),
701 service_selected: false,
702 profile: profile_name,
703 region: region_name,
704 region_selector_index: 0,
705 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
706 cw_log_group_column_ids: LogGroupColumn::ids(),
707 column_selector_index: 0,
708 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
709 cw_log_stream_column_ids: StreamColumn::ids(),
710 cw_log_event_visible_column_ids: EventColumn::default_visible(),
711 cw_log_event_column_ids: EventColumn::ids(),
712 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
713 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
714 cw_alarm_visible_column_ids: [
715 AlarmColumn::Name,
716 AlarmColumn::State,
717 AlarmColumn::LastStateUpdate,
718 AlarmColumn::Conditions,
719 AlarmColumn::Actions,
720 ]
721 .iter()
722 .map(|c| c.id())
723 .collect(),
724 cw_alarm_column_ids: AlarmColumn::ids(),
725 cloudtrail_event_visible_column_ids: [
726 CloudTrailEventColumn::EventName,
727 CloudTrailEventColumn::EventTime,
728 CloudTrailEventColumn::Username,
729 CloudTrailEventColumn::EventSource,
730 CloudTrailEventColumn::ResourceType,
731 CloudTrailEventColumn::ResourceName,
732 ]
733 .iter()
734 .map(|c| c.id())
735 .collect(),
736 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
737 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
738 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
739 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
740 s3_bucket_column_ids: S3BucketColumn::ids(),
741 sqs_visible_column_ids: [
742 SqsColumn::Name,
743 SqsColumn::Type,
744 SqsColumn::Created,
745 SqsColumn::MessagesAvailable,
746 SqsColumn::MessagesInFlight,
747 SqsColumn::Encryption,
748 SqsColumn::ContentBasedDeduplication,
749 ]
750 .iter()
751 .map(|c| c.id())
752 .collect(),
753 sqs_column_ids: SqsColumn::ids(),
754 ec2_visible_column_ids: [
755 Ec2Column::Name,
756 Ec2Column::InstanceId,
757 Ec2Column::InstanceState,
758 Ec2Column::InstanceType,
759 Ec2Column::StatusCheck,
760 Ec2Column::AlarmStatus,
761 Ec2Column::AvailabilityZone,
762 Ec2Column::PublicIpv4Dns,
763 Ec2Column::PublicIpv4Address,
764 Ec2Column::ElasticIp,
765 Ec2Column::Ipv6Ips,
766 Ec2Column::Monitoring,
767 Ec2Column::SecurityGroupName,
768 Ec2Column::KeyName,
769 Ec2Column::LaunchTime,
770 Ec2Column::PlatformDetails,
771 ]
772 .iter()
773 .map(|c| c.id())
774 .collect(),
775 ec2_column_ids: Ec2Column::ids(),
776 ecr_repo_visible_column_ids: EcrColumn::ids(),
777 ecr_repo_column_ids: EcrColumn::ids(),
778 ecr_image_visible_column_ids: EcrImageColumn::ids(),
779 ecr_image_column_ids: EcrImageColumn::ids(),
780 apig_api_visible_column_ids: ApigColumn::ids(),
781 apig_api_column_ids: ApigColumn::ids(),
782 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
783 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
784 apig_resource_visible_column_ids: ResourceColumn::all()
785 .iter()
786 .map(|c| c.id())
787 .collect(),
788 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
789 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
790 lambda_application_column_ids: LambdaApplicationColumn::ids(),
791 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
792 lambda_deployment_column_ids: DeploymentColumn::ids(),
793 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
794 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
795 cfn_visible_column_ids: [
796 CfnColumn::Name,
797 CfnColumn::Status,
798 CfnColumn::CreatedTime,
799 CfnColumn::Description,
800 ]
801 .iter()
802 .map(|c| c.id())
803 .collect(),
804 cfn_column_ids: CfnColumn::ids(),
805 cfn_parameter_visible_column_ids: parameter_column_ids(),
806 cfn_parameter_column_ids: parameter_column_ids(),
807 cfn_output_visible_column_ids: output_column_ids(),
808 cfn_output_column_ids: output_column_ids(),
809 cfn_resource_visible_column_ids: resource_column_ids(),
810 cfn_resource_column_ids: resource_column_ids(),
811 iam_user_visible_column_ids: UserColumn::visible(),
812 iam_user_column_ids: UserColumn::ids(),
813 iam_role_visible_column_ids: RoleColumn::visible(),
814 iam_role_column_ids: RoleColumn::ids(),
815 iam_group_visible_column_ids: vec![
816 "Group name".to_string(),
817 "Users".to_string(),
818 "Permissions".to_string(),
819 "Creation time".to_string(),
820 ],
821 iam_group_column_ids: vec![
822 "Group name".to_string(),
823 "Path".to_string(),
824 "Users".to_string(),
825 "Permissions".to_string(),
826 "Creation time".to_string(),
827 ],
828 iam_policy_visible_column_ids: vec![
829 "Policy name".to_string(),
830 "Type".to_string(),
831 "Attached via".to_string(),
832 ],
833 iam_policy_column_ids: vec![
834 "Policy name".to_string(),
835 "Type".to_string(),
836 "Attached via".to_string(),
837 "Attached entities".to_string(),
838 "Description".to_string(),
839 "Creation time".to_string(),
840 "Edited time".to_string(),
841 ],
842 preference_section: Preferences::Columns,
843 view_mode: ViewMode::List,
844 error_message: None,
845 error_scroll: 0,
846 page_input: String::new(),
847 calendar_date: None,
848 calendar_selecting: CalendarField::StartDate,
849 cursor_pos: 0,
850 current_session: None,
851 sessions: Vec::new(),
852 session_picker_selected: 0,
853 session_filter: String::new(),
854 session_filter_active: false,
855 region_filter: String::new(),
856 region_filter_active: false,
857 region_picker_selected: 0,
858 region_latencies: std::collections::HashMap::new(),
859 profile_filter: String::new(),
860 profile_filter_active: false,
861 profile_picker_selected: 0,
862 available_profiles: Vec::new(),
863 snapshot_requested: false,
864 })
865 }
866
867 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
868 let config = AwsConfig::dummy(region.clone());
869 Self {
870 running: true,
871 mode: Mode::ServicePicker,
872 config: config.clone(),
873 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
874 cloudtrail_client: CloudTrailClient::new(config.clone()),
875 s3_client: S3Client::new(config.clone()),
876 sqs_client: SqsClient::new(config.clone()),
877 alarms_client: AlarmsClient::new(config.clone()),
878 ec2_client: Ec2Client::new(config.clone()),
879 ecr_client: EcrClient::new(config.clone()),
880 apig_client: ApiGatewayClient::new(config.clone()),
881 iam_client: IamClient::new(config.clone()),
882 lambda_client: LambdaClient::new(config.clone()),
883 cloudformation_client: CloudFormationClient::new(config.clone()),
884 current_service: Service::CloudWatchLogGroups,
885 tabs: Vec::new(),
886 current_tab: 0,
887 tab_picker_selected: 0,
888 tab_filter: String::new(),
889 pending_key: None,
890 log_groups_state: CloudWatchLogGroupsState::new(),
891 insights_state: CloudWatchInsightsState::new(),
892 alarms_state: CloudWatchAlarmsState::new(),
893 s3_state: S3State::new(),
894 cloudtrail_state: CloudTrailState {
895 table: TableState::new(),
896 input_focus: InputFocus::Filter,
897 current_event: None,
898 event_json_scroll: 0,
899 detail_focus: CloudTrailDetailFocus::Resources,
900 resources_expanded_index: None,
901 },
902 sqs_state: SqsState::new(),
903 ec2_state: Ec2State::default(),
904 ecr_state: EcrState::new(),
905 apig_state: ApigState::new(),
906 lambda_state: LambdaState::new(),
907 lambda_application_state: LambdaApplicationState::new(),
908 cfn_state: CfnState::new(),
909 iam_state: IamState::new(),
910 service_picker: ServicePickerState::new(),
911 service_selected: false,
912 profile,
913 region: region.unwrap_or_default(),
914 region_selector_index: 0,
915 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
916 cw_log_group_column_ids: LogGroupColumn::ids(),
917 column_selector_index: 0,
918 preference_section: Preferences::Columns,
919 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
920 cw_log_stream_column_ids: StreamColumn::ids(),
921 cw_log_event_visible_column_ids: EventColumn::default_visible(),
922 cw_log_event_column_ids: EventColumn::ids(),
923 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
924 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
925 cw_alarm_visible_column_ids: [
926 AlarmColumn::Name,
927 AlarmColumn::State,
928 AlarmColumn::LastStateUpdate,
929 AlarmColumn::Conditions,
930 AlarmColumn::Actions,
931 ]
932 .iter()
933 .map(|c| c.id())
934 .collect(),
935 cw_alarm_column_ids: AlarmColumn::ids(),
936 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
937 cloudtrail_event_visible_column_ids: [
938 CloudTrailEventColumn::EventName,
939 CloudTrailEventColumn::EventTime,
940 CloudTrailEventColumn::Username,
941 CloudTrailEventColumn::EventSource,
942 CloudTrailEventColumn::ResourceType,
943 CloudTrailEventColumn::ResourceName,
944 ]
945 .iter()
946 .map(|c| c.id())
947 .collect(),
948 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
949 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
950 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
951 s3_bucket_column_ids: S3BucketColumn::ids(),
952 sqs_visible_column_ids: [
953 SqsColumn::Name,
954 SqsColumn::Type,
955 SqsColumn::Created,
956 SqsColumn::MessagesAvailable,
957 SqsColumn::MessagesInFlight,
958 SqsColumn::Encryption,
959 SqsColumn::ContentBasedDeduplication,
960 ]
961 .iter()
962 .map(|c| c.id())
963 .collect(),
964 sqs_column_ids: SqsColumn::ids(),
965 ec2_visible_column_ids: [
966 Ec2Column::Name,
967 Ec2Column::InstanceId,
968 Ec2Column::InstanceState,
969 Ec2Column::InstanceType,
970 Ec2Column::StatusCheck,
971 Ec2Column::AlarmStatus,
972 Ec2Column::AvailabilityZone,
973 Ec2Column::PublicIpv4Dns,
974 Ec2Column::PublicIpv4Address,
975 Ec2Column::ElasticIp,
976 Ec2Column::Ipv6Ips,
977 Ec2Column::Monitoring,
978 Ec2Column::SecurityGroupName,
979 Ec2Column::KeyName,
980 Ec2Column::LaunchTime,
981 Ec2Column::PlatformDetails,
982 ]
983 .iter()
984 .map(|c| c.id())
985 .collect(),
986 ec2_column_ids: Ec2Column::ids(),
987 ecr_repo_visible_column_ids: EcrColumn::ids(),
988 ecr_repo_column_ids: EcrColumn::ids(),
989 ecr_image_visible_column_ids: EcrImageColumn::ids(),
990 ecr_image_column_ids: EcrImageColumn::ids(),
991 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
992 lambda_application_column_ids: LambdaApplicationColumn::ids(),
993 apig_api_visible_column_ids: ApigColumn::ids(),
994 apig_api_column_ids: ApigColumn::ids(),
995 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
996 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
997 apig_resource_visible_column_ids: ResourceColumn::all()
998 .iter()
999 .map(|c| c.id())
1000 .collect(),
1001 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
1002 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
1003 lambda_deployment_column_ids: DeploymentColumn::ids(),
1004 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
1005 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
1006 cfn_visible_column_ids: [
1007 CfnColumn::Name,
1008 CfnColumn::Status,
1009 CfnColumn::CreatedTime,
1010 CfnColumn::Description,
1011 ]
1012 .iter()
1013 .map(|c| c.id())
1014 .collect(),
1015 cfn_column_ids: CfnColumn::ids(),
1016 iam_user_visible_column_ids: UserColumn::visible(),
1017 cfn_parameter_visible_column_ids: parameter_column_ids(),
1018 cfn_parameter_column_ids: parameter_column_ids(),
1019 cfn_output_visible_column_ids: output_column_ids(),
1020 cfn_output_column_ids: output_column_ids(),
1021 cfn_resource_visible_column_ids: resource_column_ids(),
1022 cfn_resource_column_ids: resource_column_ids(),
1023 iam_user_column_ids: UserColumn::ids(),
1024 iam_role_visible_column_ids: RoleColumn::visible(),
1025 iam_role_column_ids: RoleColumn::ids(),
1026 iam_group_visible_column_ids: vec![
1027 "Group name".to_string(),
1028 "Users".to_string(),
1029 "Permissions".to_string(),
1030 "Creation time".to_string(),
1031 ],
1032 iam_group_column_ids: vec![
1033 "Group name".to_string(),
1034 "Path".to_string(),
1035 "Users".to_string(),
1036 "Permissions".to_string(),
1037 "Creation time".to_string(),
1038 ],
1039 iam_policy_visible_column_ids: vec![
1040 "Policy name".to_string(),
1041 "Type".to_string(),
1042 "Attached via".to_string(),
1043 ],
1044 iam_policy_column_ids: vec![
1045 "Policy name".to_string(),
1046 "Type".to_string(),
1047 "Attached via".to_string(),
1048 "Attached entities".to_string(),
1049 "Description".to_string(),
1050 "Creation time".to_string(),
1051 "Edited time".to_string(),
1052 ],
1053 view_mode: ViewMode::List,
1054 error_message: None,
1055 error_scroll: 0,
1056 page_input: String::new(),
1057 calendar_date: None,
1058 calendar_selecting: CalendarField::StartDate,
1059 cursor_pos: 0,
1060 current_session: None,
1061 sessions: Vec::new(),
1062 session_picker_selected: 0,
1063 session_filter: String::new(),
1064 session_filter_active: false,
1065 region_filter: String::new(),
1066 region_filter_active: false,
1067 region_picker_selected: 0,
1068 region_latencies: std::collections::HashMap::new(),
1069 profile_filter: String::new(),
1070 profile_filter_active: false,
1071 profile_picker_selected: 0,
1072 available_profiles: Vec::new(),
1073 snapshot_requested: false,
1074 }
1075 }
1076
1077 pub fn handle_action(&mut self, action: Action) {
1078 match action {
1079 Action::Noop => {}
1080 Action::EnterFilterMode => match self.mode {
1081 Mode::ServicePicker => self.service_picker.filter_active = true,
1082 Mode::RegionPicker => self.region_filter_active = true,
1083 Mode::SessionPicker => self.session_filter_active = true,
1084 Mode::ProfilePicker => self.profile_filter_active = true,
1085 _ => {}
1086 },
1087 Action::ExitFilterMode => match self.mode {
1088 Mode::ServicePicker => {
1089 if self.service_picker.filter_active {
1090 self.service_picker.filter_active = false;
1091 } else {
1092 self.mode = Mode::Normal;
1093 }
1094 }
1095 Mode::RegionPicker => {
1096 if self.region_filter_active {
1097 self.region_filter_active = false;
1098 } else {
1099 self.mode = Mode::Normal;
1100 }
1101 }
1102 Mode::SessionPicker => {
1103 if self.session_filter_active {
1104 self.session_filter_active = false;
1105 } else {
1106 self.mode = Mode::Normal;
1107 }
1108 }
1109 Mode::ProfilePicker => {
1110 if self.profile_filter_active {
1111 self.profile_filter_active = false;
1112 } else {
1113 self.mode = Mode::Normal;
1114 }
1115 }
1116 _ => {}
1117 },
1118 Action::Quit => {
1119 self.save_current_session();
1120 self.running = false;
1121 }
1122 Action::CloseService => {
1123 if !self.tabs.is_empty() {
1124 self.tabs.remove(self.current_tab);
1126
1127 if self.tabs.is_empty() {
1128 self.service_selected = false;
1130 self.current_tab = 0;
1131 self.mode = Mode::ServicePicker;
1132 } else {
1133 if self.current_tab >= self.tabs.len() {
1135 self.current_tab = self.tabs.len() - 1;
1136 }
1137 self.current_service = self.tabs[self.current_tab].service;
1138 self.service_selected = true;
1139 self.mode = Mode::Normal;
1140 }
1141 } else {
1142 self.service_selected = false;
1144 self.mode = Mode::Normal;
1145 }
1146 self.service_picker.filter.clear();
1147 self.service_picker.selected = 0;
1148 }
1149 Action::NextItem => {
1150 let should_navigate = match self.mode {
1152 Mode::ServicePicker => !self.service_picker.filter_active,
1153 Mode::RegionPicker => !self.region_filter_active,
1154 Mode::SessionPicker => !self.session_filter_active,
1155 Mode::ProfilePicker => !self.profile_filter_active,
1156 _ => true,
1157 };
1158 if should_navigate {
1159 self.next_item();
1160 }
1161 }
1162 Action::PrevItem => {
1163 let should_navigate = match self.mode {
1165 Mode::ServicePicker => !self.service_picker.filter_active,
1166 Mode::RegionPicker => !self.region_filter_active,
1167 Mode::SessionPicker => !self.session_filter_active,
1168 Mode::ProfilePicker => !self.profile_filter_active,
1169 _ => true,
1170 };
1171 if should_navigate {
1172 self.prev_item();
1173 }
1174 }
1175 Action::PageUp => self.page_up(),
1176 Action::PageDown => self.page_down(),
1177 Action::NextPane => self.next_pane(),
1178 Action::PrevPane => self.prev_pane(),
1179 Action::CollapseRow => self.collapse_row(),
1180 Action::ExpandRow => self.expand_row(),
1181 Action::Select => self.select_item(),
1182 Action::OpenSpaceMenu => {
1183 self.mode = Mode::SpaceMenu;
1184 self.service_picker.filter.clear();
1185 self.service_picker.filter_active = false;
1186 self.service_picker.selected = 0;
1187 }
1188 Action::CloseMenu => {
1189 self.mode = Mode::Normal;
1190 self.service_picker.filter.clear();
1191 match self.current_service {
1193 Service::S3Buckets => {
1194 crate::s3::actions::reset_on_service_select(self);
1195 }
1196 Service::CloudFormationStacks => {
1197 if self.cfn_state.current_stack.is_some()
1198 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1199 {
1200 self.cfn_state.parameters.reset();
1201 } else if self.cfn_state.current_stack.is_some()
1202 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1203 {
1204 self.cfn_state.outputs.reset();
1205 } else {
1206 self.cfn_state.table.reset();
1207 }
1208 }
1209 Service::LambdaFunctions => {
1210 crate::lambda::functions::apply_filter_reset(self);
1211 }
1212 Service::SqsQueues => {
1213 self.sqs_state.queues.reset();
1214 }
1215 Service::IamRoles => {
1216 self.iam_state.roles.reset();
1217 }
1218 Service::IamUsers => {
1219 self.iam_state.users.reset();
1220 }
1221 Service::IamUserGroups => {
1222 self.iam_state.groups.reset();
1223 }
1224 Service::CloudWatchAlarms => {
1225 self.alarms_state.table.reset();
1226 }
1227 Service::Ec2Instances => {
1228 self.ec2_state.table.reset();
1229 }
1230 Service::EcrRepositories => {
1231 crate::ecr::actions::apply_filter_reset(self);
1232 }
1233 Service::ApiGatewayApis => {
1234 self.apig_state.apis.reset();
1235 }
1236 Service::LambdaApplications => {
1237 crate::lambda::applications::apply_filter_reset(self);
1238 }
1239 _ => {}
1240 }
1241 }
1242 Action::NextTab => {
1243 if !self.tabs.is_empty() {
1244 self.current_tab = (self.current_tab + 1) % self.tabs.len();
1245 self.current_service = self.tabs[self.current_tab].service;
1246 }
1247 }
1248 Action::PrevTab => {
1249 if !self.tabs.is_empty() {
1250 self.current_tab = if self.current_tab == 0 {
1251 self.tabs.len() - 1
1252 } else {
1253 self.current_tab - 1
1254 };
1255 self.current_service = self.tabs[self.current_tab].service;
1256 }
1257 }
1258 Action::CloseTab => {
1259 if !self.tabs.is_empty() {
1260 self.tabs.remove(self.current_tab);
1261 if self.tabs.is_empty() {
1262 self.service_selected = false;
1264 self.current_tab = 0;
1265 self.service_picker.filter.clear();
1266 self.service_picker.selected = 0;
1267 self.mode = Mode::ServicePicker;
1268 } else {
1269 if self.current_tab >= self.tabs.len() {
1272 self.current_tab = self.tabs.len() - 1;
1273 }
1274 self.current_service = self.tabs[self.current_tab].service;
1275 self.service_selected = true;
1276 self.mode = Mode::Normal;
1277 }
1278 }
1279 }
1280 Action::OpenTabPicker => {
1281 if !self.tabs.is_empty() {
1282 self.tab_picker_selected = self.current_tab;
1283 self.mode = Mode::TabPicker;
1284 } else {
1285 self.mode = Mode::Normal;
1286 }
1287 }
1288 Action::OpenSessionPicker => {
1289 self.save_current_session();
1290 self.sessions = Session::list_all().unwrap_or_default();
1291 self.session_picker_selected = 0;
1292 self.mode = Mode::SessionPicker;
1293 }
1294 Action::LoadSession => {
1295 let filtered_sessions = self.get_filtered_sessions();
1296 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1297 let session = session.clone();
1298 self.profile = session.profile.clone();
1300 self.region = session.region.clone();
1301 self.config.account_id = session.account_id.clone();
1302 self.config.role_arn = session.role_arn.clone();
1303
1304 self.tabs.clear();
1306 for session_tab in &session.tabs {
1307 let service = match session_tab.service.as_str() {
1309 "ApiGatewayApis" => Service::ApiGatewayApis,
1310 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1311 "CloudWatchInsights" => Service::CloudWatchInsights,
1312 "CloudWatchAlarms" => Service::CloudWatchAlarms,
1313 "CloudTrailEvents" => Service::CloudTrailEvents,
1314 "S3Buckets" => Service::S3Buckets,
1315 "SqsQueues" => Service::SqsQueues,
1316 "Ec2Instances" => Service::Ec2Instances,
1317 "EcrRepositories" => Service::EcrRepositories,
1318 "LambdaFunctions" => Service::LambdaFunctions,
1319 "LambdaApplications" => Service::LambdaApplications,
1320 "CloudFormationStacks" => Service::CloudFormationStacks,
1321 "IamUsers" => Service::IamUsers,
1322 "IamRoles" => Service::IamRoles,
1323 "IamUserGroups" => Service::IamUserGroups,
1324 _ => continue,
1325 };
1326
1327 self.tabs.push(Tab {
1328 service,
1329 title: session_tab.title.clone(),
1330 breadcrumb: session_tab.breadcrumb.clone(),
1331 });
1332
1333 if let Some(filter) = &session_tab.filter {
1335 if service == Service::CloudWatchLogGroups {
1336 self.log_groups_state.log_groups.filter = filter.clone();
1337 }
1338 }
1339 }
1340
1341 if !self.tabs.is_empty() {
1342 self.current_tab = 0;
1343 self.current_service = self.tabs[0].service;
1344 self.service_selected = true;
1345 self.current_session = Some(session.clone());
1346 }
1347 }
1348 self.mode = Mode::Normal;
1349 }
1350 Action::SaveSession => {
1351 }
1353 Action::OpenServicePicker => {
1354 if self.mode == Mode::ServicePicker {
1355 self.tabs.push(Tab {
1356 service: Service::S3Buckets,
1357 title: "S3 › Buckets".to_string(),
1358 breadcrumb: "S3 › Buckets".to_string(),
1359 });
1360 self.current_tab = self.tabs.len() - 1;
1361 self.current_service = Service::S3Buckets;
1362 self.view_mode = ViewMode::List;
1363 self.service_selected = true;
1364 self.mode = Mode::Normal;
1365 } else {
1366 self.mode = Mode::ServicePicker;
1367 self.service_picker.filter.clear();
1368 self.service_picker.selected = 0;
1369 }
1370 }
1371 Action::OpenCloudWatch => {
1372 self.current_service = Service::CloudWatchLogGroups;
1373 self.view_mode = ViewMode::List;
1374 self.service_selected = true;
1375 self.mode = Mode::Normal;
1376 }
1377 Action::OpenCloudWatchSplit => {
1378 self.current_service = Service::CloudWatchInsights;
1379 self.view_mode = ViewMode::InsightsResults;
1380 self.service_selected = true;
1381 self.mode = Mode::Normal;
1382 }
1383 Action::OpenCloudWatchAlarms => {
1384 self.current_service = Service::CloudWatchAlarms;
1385 self.view_mode = ViewMode::List;
1386 self.service_selected = true;
1387 self.mode = Mode::Normal;
1388 }
1389 Action::FilterInput(c) => {
1390 if self.mode == Mode::TabPicker {
1391 self.tab_filter.push(c);
1392 self.tab_picker_selected = 0;
1393 } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1394 self.service_picker.filter.push(c);
1395 self.service_picker.selected = 0;
1396 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1397 self.region_filter.push(c);
1398 self.region_picker_selected = 0;
1399 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1400 self.profile_filter.push(c);
1401 self.profile_picker_selected = 0;
1402 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1403 self.session_filter.push(c);
1404 self.session_picker_selected = 0;
1405 } else if self.mode == Mode::InsightsInput {
1406 match self.insights_state.insights.insights_focus {
1407 InsightsFocus::Query => {
1408 self.insights_state.insights.query_text.push(c);
1409 }
1410 InsightsFocus::LogGroupSearch => {
1411 self.insights_state.insights.log_group_search.push(c);
1412 if !self.insights_state.insights.log_group_search.is_empty() {
1414 self.insights_state.insights.log_group_matches = self
1415 .log_groups_state
1416 .log_groups
1417 .items
1418 .iter()
1419 .filter(|g| {
1420 g.name.to_lowercase().contains(
1421 &self
1422 .insights_state
1423 .insights
1424 .log_group_search
1425 .to_lowercase(),
1426 )
1427 })
1428 .take(50)
1429 .map(|g| g.name.clone())
1430 .collect();
1431 self.insights_state.insights.show_dropdown = true;
1432 } else {
1433 self.insights_state.insights.log_group_matches.clear();
1434 self.insights_state.insights.show_dropdown = false;
1435 }
1436 }
1437 _ => {}
1438 }
1439 } else if self.mode == Mode::FilterInput {
1440 let is_pagination_focused = if self.current_service
1442 == Service::LambdaApplications
1443 {
1444 crate::lambda::applications::is_pagination_focused(self)
1445 } else if self.current_service == Service::CloudFormationStacks {
1446 self.cfn_state.input_focus == InputFocus::Pagination
1447 } else if self.current_service == Service::IamRoles
1448 && self.iam_state.current_role.is_none()
1449 {
1450 self.iam_state.role_input_focus == InputFocus::Pagination
1451 } else if self.view_mode == ViewMode::PolicyView {
1452 self.iam_state.policy_input_focus == InputFocus::Pagination
1453 } else if self.current_service == Service::CloudWatchAlarms {
1454 self.alarms_state.input_focus == InputFocus::Pagination
1455 } else if self.current_service == Service::CloudTrailEvents {
1456 self.cloudtrail_state.input_focus == InputFocus::Pagination
1457 } else if self.current_service == Service::Ec2Instances {
1458 self.ec2_state.input_focus == InputFocus::Pagination
1459 } else if self.current_service == Service::CloudWatchLogGroups {
1460 self.log_groups_state.input_focus == InputFocus::Pagination
1461 } else if self.current_service == Service::ApiGatewayApis {
1462 self.apig_state.input_focus == InputFocus::Pagination
1463 } else if self.current_service == Service::EcrRepositories
1464 && self.ecr_state.current_repository.is_none()
1465 {
1466 crate::ecr::actions::is_pagination_focused(self)
1467 } else if self.current_service == Service::LambdaFunctions {
1468 crate::lambda::functions::is_pagination_focused(self)
1469 } else if self.current_service == Service::SqsQueues {
1470 if self.sqs_state.current_queue.is_some()
1471 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1472 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1473 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1474 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1475 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1476 {
1477 self.sqs_state.input_focus == InputFocus::Pagination
1478 } else {
1479 false
1480 }
1481 } else {
1482 false
1483 };
1484
1485 if is_pagination_focused && c.is_ascii_digit() {
1486 self.page_input.push(c);
1487 } else if self.current_service == Service::LambdaApplications {
1488 if crate::lambda::applications::is_filter_focused(self) {
1489 self.apply_filter_operation(|f| f.push(c));
1490 }
1491 } else if self.current_service == Service::CloudFormationStacks {
1492 if self.cfn_state.current_stack.is_some()
1493 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1494 {
1495 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1496 self.cfn_state.parameters.filter.push(c);
1497 self.cfn_state.parameters.selected = 0;
1498 }
1499 } else if self.cfn_state.current_stack.is_some()
1500 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1501 {
1502 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1503 self.cfn_state.outputs.filter.push(c);
1504 self.cfn_state.outputs.selected = 0;
1505 }
1506 } else if self.cfn_state.current_stack.is_some()
1507 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1508 {
1509 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1510 self.cfn_state.resources.filter.push(c);
1511 self.cfn_state.resources.selected = 0;
1512 }
1513 } else if self.cfn_state.input_focus == InputFocus::Filter {
1514 self.apply_filter_operation(|f| f.push(c));
1515 }
1516 } else if self.current_service == Service::EcrRepositories
1517 && self.ecr_state.current_repository.is_none()
1518 {
1519 if self.ecr_state.input_focus == InputFocus::Filter {
1520 self.apply_filter_operation(|f| f.push(c));
1521 }
1522 } else if self.current_service == Service::IamRoles
1523 && self.iam_state.current_role.is_none()
1524 {
1525 if self.iam_state.role_input_focus == InputFocus::Filter {
1526 self.apply_filter_operation(|f| f.push(c));
1527 }
1528 } else if self.view_mode == ViewMode::PolicyView {
1529 if self.iam_state.policy_input_focus == InputFocus::Filter {
1530 self.apply_filter_operation(|f| f.push(c));
1531 }
1532 } else if self.current_service == Service::ApiGatewayApis
1533 && self.apig_state.current_api.is_some()
1534 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1535 {
1536 if self.apig_state.input_focus == InputFocus::Filter {
1537 self.apply_filter_operation(|f| f.push(c));
1538 }
1539 } else if self.current_service == Service::LambdaFunctions {
1540 if crate::lambda::functions::filter_char_allowed(self) {
1541 self.apply_filter_operation(|f| f.push(c));
1542 }
1543 } else if self.current_service == Service::SqsQueues
1544 && self.sqs_state.current_queue.is_some()
1545 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1546 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1547 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1548 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1549 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1550 {
1551 if self.sqs_state.input_focus == InputFocus::Filter {
1552 self.apply_filter_operation(|f| f.push(c));
1553 }
1554 } else if self.current_service == Service::Ec2Instances
1555 && self.ec2_state.current_instance.is_some()
1556 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1557 {
1558 if self.ec2_state.input_focus == InputFocus::Filter {
1559 self.ec2_state.tags.filter.push(c);
1560 self.ec2_state.tags.selected = 0;
1561 }
1562 } else if self.current_service == Service::CloudWatchLogGroups {
1563 if self.log_groups_state.input_focus == InputFocus::Filter {
1564 self.apply_filter_operation(|f| f.push(c));
1565 }
1566 } else {
1567 self.apply_filter_operation(|f| f.push(c));
1568 }
1569 } else if self.mode == Mode::EventFilterInput {
1570 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1571 self.log_groups_state.event_filter.push(c);
1572 } else if c.is_ascii_digit() {
1573 self.log_groups_state.relative_amount.push(c);
1574 }
1575 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1576 self.page_input.push(c);
1577 }
1578 }
1579 Action::FilterBackspace => {
1580 if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1581 self.service_picker.filter.pop();
1582 self.service_picker.selected = 0;
1583 } else if self.mode == Mode::TabPicker {
1584 self.tab_filter.pop();
1585 self.tab_picker_selected = 0;
1586 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1587 self.region_filter.pop();
1588 self.region_picker_selected = 0;
1589 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1590 self.profile_filter.pop();
1591 self.profile_picker_selected = 0;
1592 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1593 self.session_filter.pop();
1594 self.session_picker_selected = 0;
1595 } else if self.mode == Mode::InsightsInput {
1596 match self.insights_state.insights.insights_focus {
1597 InsightsFocus::Query => {
1598 self.insights_state.insights.query_text.pop();
1599 }
1600 InsightsFocus::LogGroupSearch => {
1601 self.insights_state.insights.log_group_search.pop();
1602 if !self.insights_state.insights.log_group_search.is_empty() {
1604 self.insights_state.insights.log_group_matches = self
1605 .log_groups_state
1606 .log_groups
1607 .items
1608 .iter()
1609 .filter(|g| {
1610 g.name.to_lowercase().contains(
1611 &self
1612 .insights_state
1613 .insights
1614 .log_group_search
1615 .to_lowercase(),
1616 )
1617 })
1618 .take(50)
1619 .map(|g| g.name.clone())
1620 .collect();
1621 self.insights_state.insights.show_dropdown = true;
1622 } else {
1623 self.insights_state.insights.log_group_matches.clear();
1624 self.insights_state.insights.show_dropdown = false;
1625 }
1626 }
1627 _ => {}
1628 }
1629 } else if self.mode == Mode::FilterInput {
1630 if self.current_service == Service::CloudFormationStacks {
1632 if self.cfn_state.current_stack.is_some()
1633 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1634 {
1635 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1636 self.cfn_state.parameters.filter.pop();
1637 self.cfn_state.parameters.selected = 0;
1638 }
1639 } else if self.cfn_state.current_stack.is_some()
1640 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1641 {
1642 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1643 self.cfn_state.outputs.filter.pop();
1644 self.cfn_state.outputs.selected = 0;
1645 }
1646 } else if self.cfn_state.current_stack.is_some()
1647 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1648 {
1649 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1650 self.cfn_state.resources.filter.pop();
1651 self.cfn_state.resources.selected = 0;
1652 }
1653 } else if self.cfn_state.input_focus == InputFocus::Filter {
1654 self.apply_filter_operation(|f| {
1655 f.pop();
1656 });
1657 }
1658 } else if self.current_service == Service::Ec2Instances
1659 && self.ec2_state.current_instance.is_some()
1660 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1661 {
1662 if self.ec2_state.input_focus == InputFocus::Filter {
1663 self.ec2_state.tags.filter.pop();
1664 self.ec2_state.tags.selected = 0;
1665 }
1666 } else if self.current_service == Service::CloudWatchLogGroups {
1667 if self.log_groups_state.input_focus == InputFocus::Filter {
1668 self.apply_filter_operation(|f| {
1669 f.pop();
1670 });
1671 }
1672 } else {
1673 self.apply_filter_operation(|f| {
1674 f.pop();
1675 });
1676 }
1677 } else if self.mode == Mode::EventFilterInput {
1678 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1679 self.log_groups_state.event_filter.pop();
1680 } else {
1681 self.log_groups_state.relative_amount.pop();
1682 }
1683 }
1684 }
1685 Action::DeleteWord => {
1686 let text = if self.mode == Mode::ServicePicker {
1687 &mut self.service_picker.filter
1688 } else if self.mode == Mode::InsightsInput {
1689 use crate::app::InsightsFocus;
1690 match self.insights_state.insights.insights_focus {
1691 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1692 InsightsFocus::LogGroupSearch => {
1693 &mut self.insights_state.insights.log_group_search
1694 }
1695 _ => return,
1696 }
1697 } else if self.mode == Mode::FilterInput {
1698 if let Some(filter) = self.get_active_filter_mut() {
1699 filter
1700 } else {
1701 return;
1702 }
1703 } else if self.mode == Mode::EventFilterInput {
1704 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1705 &mut self.log_groups_state.event_filter
1706 } else {
1707 &mut self.log_groups_state.relative_amount
1708 }
1709 } else {
1710 return;
1711 };
1712
1713 if text.is_empty() {
1714 return;
1715 }
1716
1717 let mut chars: Vec<char> = text.chars().collect();
1718 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1719 chars.pop();
1720 }
1721 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1722 chars.pop();
1723 }
1724 *text = chars.into_iter().collect();
1725 }
1726 Action::WordLeft => {
1727 }
1729 Action::WordRight => {
1730 }
1732 Action::OpenColumnSelector => {
1733 if self.current_service == Service::CloudFormationStacks
1735 && self.cfn_state.current_stack.is_some()
1736 && (self.cfn_state.detail_tab == CfnDetailTab::Template
1737 || self.cfn_state.detail_tab == CfnDetailTab::GitSync)
1738 {
1739 return;
1740 }
1741
1742 if self.current_service == Service::IamUsers
1744 && self.iam_state.current_user.is_some()
1745 && self.iam_state.user_tab == UserTab::SecurityCredentials
1746 {
1747 return;
1748 }
1749
1750 if self.current_service == Service::IamRoles
1752 && self.iam_state.current_role.is_some()
1753 && (self.iam_state.role_tab == RoleTab::TrustRelationships
1754 || self.iam_state.role_tab == RoleTab::RevokeSessions)
1755 {
1756 return;
1757 }
1758
1759 if self.current_service == Service::SqsQueues
1761 && self.sqs_state.current_queue.is_some()
1762 && matches!(
1763 self.sqs_state.detail_tab,
1764 SqsQueueDetailTab::QueuePolicies
1765 | SqsQueueDetailTab::Monitoring
1766 | SqsQueueDetailTab::DeadLetterQueue
1767 | SqsQueueDetailTab::Encryption
1768 | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1769 )
1770 {
1771 return;
1772 }
1773
1774 if self.current_service == Service::Ec2Instances
1776 && self.ec2_state.table.expanded_item.is_some()
1777 && self.ec2_state.detail_tab != Ec2DetailTab::Tags
1778 {
1779 return;
1780 }
1781
1782 if self.current_service == Service::CloudTrailEvents
1784 && self.cloudtrail_state.current_event.is_some()
1785 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1786 {
1787 return;
1788 }
1789
1790 if !self.page_input.is_empty() {
1792 if let Ok(page) = self.page_input.parse::<usize>() {
1793 self.go_to_page(page);
1794 }
1795 self.page_input.clear();
1796 } else {
1797 self.mode = Mode::ColumnSelector;
1798 self.column_selector_index = 0;
1799 }
1800 }
1801 Action::ToggleColumn => {
1802 if self.current_service == Service::S3Buckets
1803 && self.s3_state.current_bucket.is_none()
1804 {
1805 crate::s3::actions::toggle_column(self);
1806 } else if self.current_service == Service::CloudWatchAlarms {
1807 let idx = self.column_selector_index;
1811 if (1..=16).contains(&idx) {
1812 if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1814 Self::toggle_column_visibility(
1815 &mut self.cw_alarm_visible_column_ids,
1816 &self.cw_alarm_column_ids,
1817 *col,
1818 );
1819 }
1820 } else if idx == 19 {
1821 self.alarms_state.view_as = AlarmViewMode::Table;
1822 } else if idx == 20 {
1823 self.alarms_state.view_as = AlarmViewMode::Cards;
1824 } else if idx == 23 {
1825 self.alarms_state.table.page_size = PageSize::Ten;
1826 } else if idx == 24 {
1827 self.alarms_state.table.page_size = PageSize::TwentyFive;
1828 } else if idx == 25 {
1829 self.alarms_state.table.page_size = PageSize::Fifty;
1830 } else if idx == 26 {
1831 self.alarms_state.table.page_size = PageSize::OneHundred;
1832 } else if idx == 29 {
1833 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1834 }
1835 } else if self.current_service == Service::CloudTrailEvents {
1836 if self.cloudtrail_state.current_event.is_some()
1837 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
1838 {
1839 let idx = self.column_selector_index;
1841 if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
1842 if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
1843 Self::toggle_column_visibility(
1844 &mut self.cloudtrail_resource_visible_column_ids,
1845 &self.cloudtrail_resource_column_ids,
1846 *col,
1847 );
1848 }
1849 }
1850 } else {
1851 let idx = self.column_selector_index;
1853 if (1..=14).contains(&idx) {
1854 if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
1855 Self::toggle_column_visibility(
1856 &mut self.cloudtrail_event_visible_column_ids,
1857 &self.cloudtrail_event_column_ids,
1858 *col,
1859 );
1860 }
1861 } else if idx == 17 {
1862 self.cloudtrail_state.table.page_size = PageSize::Ten;
1863 self.cloudtrail_state.table.snap_to_page();
1864 } else if idx == 18 {
1865 self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
1866 self.cloudtrail_state.table.snap_to_page();
1867 } else if idx == 19 {
1868 self.cloudtrail_state.table.page_size = PageSize::Fifty;
1869 self.cloudtrail_state.table.snap_to_page();
1870 } else if idx == 20 {
1871 self.cloudtrail_state.table.page_size = PageSize::OneHundred;
1872 self.cloudtrail_state.table.snap_to_page();
1873 }
1874 }
1875 } else if self.current_service == Service::ApiGatewayApis {
1876 if let Some(api) = &self.apig_state.current_api {
1877 use crate::ui::apig::ApiDetailTab;
1878 if self.apig_state.detail_tab == ApiDetailTab::Routes {
1879 if api.protocol_type.to_uppercase() == "REST" {
1881 let idx = self.column_selector_index;
1883 if idx > 1 && idx <= self.apig_resource_column_ids.len() {
1884 if let Some(col) = self.apig_resource_column_ids.get(idx - 1) {
1885 if let Some(pos) = self
1886 .apig_resource_visible_column_ids
1887 .iter()
1888 .position(|c| c == col)
1889 {
1890 self.apig_resource_visible_column_ids.remove(pos);
1891 } else {
1892 self.apig_resource_visible_column_ids.push(*col);
1893 }
1894 }
1895 }
1896 } else {
1897 let idx = self.column_selector_index;
1899 if idx > 1 && idx <= self.apig_route_column_ids.len() {
1900 if let Some(col) = self.apig_route_column_ids.get(idx - 1) {
1901 if let Some(pos) = self
1902 .apig_route_visible_column_ids
1903 .iter()
1904 .position(|c| c == col)
1905 {
1906 self.apig_route_visible_column_ids.remove(pos);
1907 } else {
1908 self.apig_route_visible_column_ids.push(*col);
1909 }
1910 }
1911 }
1912 }
1913 }
1915 } else {
1916 let idx = self.column_selector_index;
1918 if idx > 0 && idx <= self.apig_api_column_ids.len() {
1919 if let Some(col) = self.apig_api_column_ids.get(idx - 1) {
1920 if let Some(pos) = self
1921 .apig_api_visible_column_ids
1922 .iter()
1923 .position(|c| c == col)
1924 {
1925 self.apig_api_visible_column_ids.remove(pos);
1926 } else {
1927 self.apig_api_visible_column_ids.push(*col);
1928 }
1929 }
1930 } else if idx == self.apig_api_column_ids.len() + 3 {
1931 self.apig_state.apis.page_size = PageSize::Ten;
1932 } else if idx == self.apig_api_column_ids.len() + 4 {
1933 self.apig_state.apis.page_size = PageSize::TwentyFive;
1934 } else if idx == self.apig_api_column_ids.len() + 5 {
1935 self.apig_state.apis.page_size = PageSize::Fifty;
1936 } else if idx == self.apig_api_column_ids.len() + 6 {
1937 self.apig_state.apis.page_size = PageSize::OneHundred;
1938 }
1939 }
1940 } else if self.current_service == Service::EcrRepositories {
1941 crate::ecr::actions::toggle_column(self);
1942 } else if self.current_service == Service::Ec2Instances {
1943 if self.ec2_state.current_instance.is_some()
1944 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1945 {
1946 let idx = self.column_selector_index;
1947 if idx > 0 && idx <= self.ec2_state.tag_column_ids.len() {
1948 if let Some(col) = self.ec2_state.tag_column_ids.get(idx - 1) {
1949 if let Some(pos) = self
1950 .ec2_state
1951 .tag_visible_column_ids
1952 .iter()
1953 .position(|c| c == col)
1954 {
1955 self.ec2_state.tag_visible_column_ids.remove(pos);
1956 } else {
1957 self.ec2_state.tag_visible_column_ids.push(col.clone());
1958 }
1959 }
1960 } else if idx == self.ec2_state.tag_column_ids.len() + 3 {
1961 self.ec2_state.tags.page_size = PageSize::Ten;
1962 } else if idx == self.ec2_state.tag_column_ids.len() + 4 {
1963 self.ec2_state.tags.page_size = PageSize::TwentyFive;
1964 } else if idx == self.ec2_state.tag_column_ids.len() + 5 {
1965 self.ec2_state.tags.page_size = PageSize::Fifty;
1966 } else if idx == self.ec2_state.tag_column_ids.len() + 6 {
1967 self.ec2_state.tags.page_size = PageSize::OneHundred;
1968 }
1969 } else {
1970 let idx = self.column_selector_index;
1971 if idx > 0 && idx <= self.ec2_column_ids.len() {
1972 if let Some(col) = self.ec2_column_ids.get(idx - 1) {
1973 if let Some(pos) =
1974 self.ec2_visible_column_ids.iter().position(|c| c == col)
1975 {
1976 self.ec2_visible_column_ids.remove(pos);
1977 } else {
1978 self.ec2_visible_column_ids.push(*col);
1979 }
1980 }
1981 } else if idx == self.ec2_column_ids.len() + 3 {
1982 self.ec2_state.table.page_size = PageSize::Ten;
1983 } else if idx == self.ec2_column_ids.len() + 4 {
1984 self.ec2_state.table.page_size = PageSize::TwentyFive;
1985 } else if idx == self.ec2_column_ids.len() + 5 {
1986 self.ec2_state.table.page_size = PageSize::Fifty;
1987 } else if idx == self.ec2_column_ids.len() + 6 {
1988 self.ec2_state.table.page_size = PageSize::OneHundred;
1989 }
1990 }
1991 } else if self.current_service == Service::SqsQueues {
1992 if self.sqs_state.current_queue.is_some()
1993 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1994 {
1995 let idx = self.column_selector_index;
1997 if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
1998 if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
1999 if let Some(pos) = self
2000 .sqs_state
2001 .trigger_visible_column_ids
2002 .iter()
2003 .position(|c| c == col)
2004 {
2005 self.sqs_state.trigger_visible_column_ids.remove(pos);
2006 } else {
2007 self.sqs_state.trigger_visible_column_ids.push(col.clone());
2008 }
2009 }
2010 } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
2011 self.sqs_state.triggers.page_size = PageSize::Ten;
2012 } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
2013 self.sqs_state.triggers.page_size = PageSize::TwentyFive;
2014 } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
2015 self.sqs_state.triggers.page_size = PageSize::Fifty;
2016 } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
2017 self.sqs_state.triggers.page_size = PageSize::OneHundred;
2018 }
2019 } else if self.sqs_state.current_queue.is_some()
2020 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2021 {
2022 let idx = self.column_selector_index;
2024 if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
2025 if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
2026 if let Some(pos) = self
2027 .sqs_state
2028 .pipe_visible_column_ids
2029 .iter()
2030 .position(|c| c == col)
2031 {
2032 self.sqs_state.pipe_visible_column_ids.remove(pos);
2033 } else {
2034 self.sqs_state.pipe_visible_column_ids.push(col.clone());
2035 }
2036 }
2037 } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
2038 self.sqs_state.pipes.page_size = PageSize::Ten;
2039 } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
2040 self.sqs_state.pipes.page_size = PageSize::TwentyFive;
2041 } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
2042 self.sqs_state.pipes.page_size = PageSize::Fifty;
2043 } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
2044 self.sqs_state.pipes.page_size = PageSize::OneHundred;
2045 }
2046 } else if self.sqs_state.current_queue.is_some()
2047 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2048 {
2049 let idx = self.column_selector_index;
2051 if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
2052 if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
2053 if let Some(pos) = self
2054 .sqs_state
2055 .tag_visible_column_ids
2056 .iter()
2057 .position(|c| c == col)
2058 {
2059 self.sqs_state.tag_visible_column_ids.remove(pos);
2060 } else {
2061 self.sqs_state.tag_visible_column_ids.push(col.clone());
2062 }
2063 }
2064 } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
2065 self.sqs_state.tags.page_size = PageSize::Ten;
2066 } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
2067 self.sqs_state.tags.page_size = PageSize::TwentyFive;
2068 } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
2069 self.sqs_state.tags.page_size = PageSize::Fifty;
2070 } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
2071 self.sqs_state.tags.page_size = PageSize::OneHundred;
2072 }
2073 } else if self.sqs_state.current_queue.is_some()
2074 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2075 {
2076 let idx = self.column_selector_index;
2078 if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
2079 if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
2080 if let Some(pos) = self
2081 .sqs_state
2082 .subscription_visible_column_ids
2083 .iter()
2084 .position(|c| c == col)
2085 {
2086 self.sqs_state.subscription_visible_column_ids.remove(pos);
2087 } else {
2088 self.sqs_state
2089 .subscription_visible_column_ids
2090 .push(col.clone());
2091 }
2092 }
2093 } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
2094 self.sqs_state.subscriptions.page_size = PageSize::Ten;
2095 } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
2096 self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
2097 } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
2098 self.sqs_state.subscriptions.page_size = PageSize::Fifty;
2099 } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
2100 self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
2101 }
2102 } else {
2103 let idx = self.column_selector_index;
2105 if let Some(col) = self.sqs_column_ids.get(idx) {
2106 if let Some(pos) =
2107 self.sqs_visible_column_ids.iter().position(|c| c == col)
2108 {
2109 self.sqs_visible_column_ids.remove(pos);
2110 } else {
2111 self.sqs_visible_column_ids.push(*col);
2112 }
2113 } else if idx == self.sqs_column_ids.len() + 2 {
2114 self.sqs_state.queues.page_size = PageSize::Ten;
2115 } else if idx == self.sqs_column_ids.len() + 3 {
2116 self.sqs_state.queues.page_size = PageSize::TwentyFive;
2117 } else if idx == self.sqs_column_ids.len() + 4 {
2118 self.sqs_state.queues.page_size = PageSize::Fifty;
2119 } else if idx == self.sqs_column_ids.len() + 5 {
2120 self.sqs_state.queues.page_size = PageSize::OneHundred;
2121 }
2122 }
2123 } else if self.current_service == Service::LambdaFunctions {
2124 crate::lambda::functions::toggle_column(self);
2125 } else if self.current_service == Service::LambdaApplications {
2126 crate::lambda::applications::toggle_column(self);
2127 } else if self.view_mode == ViewMode::Events {
2128 if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
2129 {
2130 if let Some(pos) = self
2131 .cw_log_event_visible_column_ids
2132 .iter()
2133 .position(|c| c == col)
2134 {
2135 self.cw_log_event_visible_column_ids.remove(pos);
2136 } else {
2137 self.cw_log_event_visible_column_ids.push(*col);
2138 }
2139 }
2140 } else if self.view_mode == ViewMode::Detail {
2141 let idx = self.column_selector_index;
2142 if self.log_groups_state.detail_tab == DetailTab::Tags {
2143 if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
2145 if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
2146 if let Some(pos) = self
2147 .cw_log_tag_visible_column_ids
2148 .iter()
2149 .position(|c| c == col)
2150 {
2151 self.cw_log_tag_visible_column_ids.remove(pos);
2152 } else {
2153 self.cw_log_tag_visible_column_ids.push(*col);
2154 }
2155 }
2156 } else if idx == self.cw_log_tag_column_ids.len() + 3 {
2157 self.log_groups_state.tags.page_size = PageSize::Ten;
2158 } else if idx == self.cw_log_tag_column_ids.len() + 4 {
2159 self.log_groups_state.tags.page_size = PageSize::TwentyFive;
2160 } else if idx == self.cw_log_tag_column_ids.len() + 5 {
2161 self.log_groups_state.tags.page_size = PageSize::Fifty;
2162 } else if idx == self.cw_log_tag_column_ids.len() + 6 {
2163 self.log_groups_state.tags.page_size = PageSize::OneHundred;
2164 }
2165 } else {
2166 if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
2168 if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
2169 if let Some(pos) = self
2170 .cw_log_stream_visible_column_ids
2171 .iter()
2172 .position(|c| c == col)
2173 {
2174 self.cw_log_stream_visible_column_ids.remove(pos);
2175 } else {
2176 self.cw_log_stream_visible_column_ids.push(*col);
2177 }
2178 }
2179 } else if idx == self.cw_log_stream_column_ids.len() + 3 {
2180 self.log_groups_state.stream_page_size = 10;
2181 self.log_groups_state.stream_current_page = 0;
2182 } else if idx == self.cw_log_stream_column_ids.len() + 4 {
2183 self.log_groups_state.stream_page_size = 25;
2184 self.log_groups_state.stream_current_page = 0;
2185 } else if idx == self.cw_log_stream_column_ids.len() + 5 {
2186 self.log_groups_state.stream_page_size = 50;
2187 self.log_groups_state.stream_current_page = 0;
2188 } else if idx == self.cw_log_stream_column_ids.len() + 6 {
2189 self.log_groups_state.stream_page_size = 100;
2190 self.log_groups_state.stream_current_page = 0;
2191 }
2192 }
2193 } else if self.current_service == Service::CloudFormationStacks {
2194 let idx = self.column_selector_index;
2195 if self.cfn_state.current_stack.is_some()
2197 && self.cfn_state.detail_tab == CfnDetailTab::StackInfo
2198 {
2199 if idx == 4 {
2201 self.cfn_state.tags.page_size = PageSize::Ten;
2202 } else if idx == 5 {
2203 self.cfn_state.tags.page_size = PageSize::TwentyFive;
2204 } else if idx == 6 {
2205 self.cfn_state.tags.page_size = PageSize::Fifty;
2206 } else if idx == 7 {
2207 self.cfn_state.tags.page_size = PageSize::OneHundred;
2208 }
2209 } else if self.cfn_state.current_stack.is_some()
2210 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2211 {
2212 if idx > 0 && idx <= self.cfn_parameter_column_ids.len() {
2213 if let Some(col) = self.cfn_parameter_column_ids.get(idx - 1) {
2214 if let Some(pos) = self
2215 .cfn_parameter_visible_column_ids
2216 .iter()
2217 .position(|c| c == col)
2218 {
2219 self.cfn_parameter_visible_column_ids.remove(pos);
2220 } else {
2221 self.cfn_parameter_visible_column_ids.push(col);
2222 }
2223 }
2224 } else if idx == self.cfn_parameter_column_ids.len() + 3 {
2225 self.cfn_state.parameters.page_size = PageSize::Ten;
2226 } else if idx == self.cfn_parameter_column_ids.len() + 4 {
2227 self.cfn_state.parameters.page_size = PageSize::TwentyFive;
2228 } else if idx == self.cfn_parameter_column_ids.len() + 5 {
2229 self.cfn_state.parameters.page_size = PageSize::Fifty;
2230 } else if idx == self.cfn_parameter_column_ids.len() + 6 {
2231 self.cfn_state.parameters.page_size = PageSize::OneHundred;
2232 }
2233 } else if self.cfn_state.current_stack.is_some()
2234 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2235 {
2236 if idx > 0 && idx <= self.cfn_output_column_ids.len() {
2237 if let Some(col) = self.cfn_output_column_ids.get(idx - 1) {
2238 if let Some(pos) = self
2239 .cfn_output_visible_column_ids
2240 .iter()
2241 .position(|c| c == col)
2242 {
2243 self.cfn_output_visible_column_ids.remove(pos);
2244 } else {
2245 self.cfn_output_visible_column_ids.push(col);
2246 }
2247 }
2248 } else if idx == self.cfn_output_column_ids.len() + 3 {
2249 self.cfn_state.outputs.page_size = PageSize::Ten;
2250 } else if idx == self.cfn_output_column_ids.len() + 4 {
2251 self.cfn_state.outputs.page_size = PageSize::TwentyFive;
2252 } else if idx == self.cfn_output_column_ids.len() + 5 {
2253 self.cfn_state.outputs.page_size = PageSize::Fifty;
2254 } else if idx == self.cfn_output_column_ids.len() + 6 {
2255 self.cfn_state.outputs.page_size = PageSize::OneHundred;
2256 }
2257 } else if self.cfn_state.current_stack.is_some()
2258 && self.cfn_state.detail_tab == CfnDetailTab::Resources
2259 {
2260 if idx > 0 && idx <= self.cfn_resource_column_ids.len() {
2261 if let Some(col) = self.cfn_resource_column_ids.get(idx - 1) {
2262 if let Some(pos) = self
2263 .cfn_resource_visible_column_ids
2264 .iter()
2265 .position(|c| c == col)
2266 {
2267 self.cfn_resource_visible_column_ids.remove(pos);
2268 } else {
2269 self.cfn_resource_visible_column_ids.push(col);
2270 }
2271 }
2272 } else if idx == self.cfn_resource_column_ids.len() + 3 {
2273 self.cfn_state.resources.page_size = PageSize::Ten;
2274 } else if idx == self.cfn_resource_column_ids.len() + 4 {
2275 self.cfn_state.resources.page_size = PageSize::TwentyFive;
2276 } else if idx == self.cfn_resource_column_ids.len() + 5 {
2277 self.cfn_state.resources.page_size = PageSize::Fifty;
2278 } else if idx == self.cfn_resource_column_ids.len() + 6 {
2279 self.cfn_state.resources.page_size = PageSize::OneHundred;
2280 }
2281 } else if self.cfn_state.current_stack.is_none() {
2282 if idx > 0 && idx <= self.cfn_column_ids.len() {
2284 if let Some(col) = self.cfn_column_ids.get(idx - 1) {
2285 if let Some(pos) =
2286 self.cfn_visible_column_ids.iter().position(|c| c == col)
2287 {
2288 self.cfn_visible_column_ids.remove(pos);
2289 } else {
2290 self.cfn_visible_column_ids.push(*col);
2291 }
2292 }
2293 } else if idx == self.cfn_column_ids.len() + 3 {
2294 self.cfn_state.table.page_size = PageSize::Ten;
2295 } else if idx == self.cfn_column_ids.len() + 4 {
2296 self.cfn_state.table.page_size = PageSize::TwentyFive;
2297 } else if idx == self.cfn_column_ids.len() + 5 {
2298 self.cfn_state.table.page_size = PageSize::Fifty;
2299 } else if idx == self.cfn_column_ids.len() + 6 {
2300 self.cfn_state.table.page_size = PageSize::OneHundred;
2301 }
2302 }
2303 } else if self.current_service == Service::IamUsers {
2305 let idx = self.column_selector_index;
2306 if self.iam_state.current_user.is_some() {
2307 match self.iam_state.user_tab {
2308 UserTab::Permissions => {
2309 if idx > 0 && idx <= self.iam_policy_column_ids.len() {
2311 if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
2312 if let Some(pos) = self
2313 .iam_policy_visible_column_ids
2314 .iter()
2315 .position(|c| c == col)
2316 {
2317 self.iam_policy_visible_column_ids.remove(pos);
2318 } else {
2319 self.iam_policy_visible_column_ids.push(col.clone());
2320 }
2321 }
2322 } else if idx == self.iam_policy_column_ids.len() + 3 {
2323 self.iam_state.policies.page_size = PageSize::Ten;
2324 } else if idx == self.iam_policy_column_ids.len() + 4 {
2325 self.iam_state.policies.page_size = PageSize::TwentyFive;
2326 } else if idx == self.iam_policy_column_ids.len() + 5 {
2327 self.iam_state.policies.page_size = PageSize::Fifty;
2328 }
2329 }
2330 UserTab::Groups => {
2331 toggle_iam_page_size_only(
2332 idx,
2333 5,
2334 &mut self.iam_state.user_group_memberships.page_size,
2335 );
2336 }
2337 UserTab::Tags => {
2338 toggle_iam_page_size_only(
2339 idx,
2340 5,
2341 &mut self.iam_state.user_tags.page_size,
2342 );
2343 }
2344 UserTab::LastAccessed => {
2345 toggle_iam_page_size_only(
2346 idx,
2347 6,
2348 &mut self.iam_state.last_accessed_services.page_size,
2349 );
2350 }
2351 _ => {}
2352 }
2353 } else {
2354 toggle_iam_preference_static(
2356 idx,
2357 &self.iam_user_column_ids,
2358 &mut self.iam_user_visible_column_ids,
2359 &mut self.iam_state.users.page_size,
2360 );
2361 }
2362 } else if self.current_service == Service::IamRoles {
2363 let idx = self.column_selector_index;
2364 if self.iam_state.current_role.is_some() {
2365 match self.iam_state.role_tab {
2366 RoleTab::Permissions => {
2367 toggle_iam_preference(
2369 idx,
2370 &self.iam_policy_column_ids,
2371 &mut self.iam_policy_visible_column_ids,
2372 &mut self.iam_state.policies.page_size,
2373 );
2374 }
2375 RoleTab::LastAccessed => {
2376 toggle_iam_page_size_only(
2378 idx,
2379 6,
2380 &mut self.iam_state.last_accessed_services.page_size,
2381 );
2382 }
2383 _ => {}
2384 }
2385 } else {
2386 toggle_iam_preference_static(
2388 idx,
2389 &self.iam_role_column_ids,
2390 &mut self.iam_role_visible_column_ids,
2391 &mut self.iam_state.roles.page_size,
2392 );
2393 }
2394 } else if self.current_service == Service::IamUserGroups {
2395 toggle_iam_preference(
2396 self.column_selector_index,
2397 &self.iam_group_column_ids,
2398 &mut self.iam_group_visible_column_ids,
2399 &mut self.iam_state.groups.page_size,
2400 );
2401 } else {
2402 let idx = self.column_selector_index;
2403 if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
2404 if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
2405 if let Some(pos) = self
2406 .cw_log_group_visible_column_ids
2407 .iter()
2408 .position(|c| c == col)
2409 {
2410 self.cw_log_group_visible_column_ids.remove(pos);
2411 } else {
2412 self.cw_log_group_visible_column_ids.push(*col);
2413 }
2414 }
2415 } else if idx == self.cw_log_group_column_ids.len() + 3 {
2416 self.log_groups_state.log_groups.page_size = PageSize::Ten;
2417 } else if idx == self.cw_log_group_column_ids.len() + 4 {
2418 self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
2419 } else if idx == self.cw_log_group_column_ids.len() + 5 {
2420 self.log_groups_state.log_groups.page_size = PageSize::Fifty;
2421 } else if idx == self.cw_log_group_column_ids.len() + 6 {
2422 self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2423 }
2424 }
2425 }
2426 Action::NextPreferences => {
2427 if self.current_service == Service::ApiGatewayApis {
2428 cycle_preference_next(
2429 &mut self.column_selector_index,
2430 self.apig_api_column_ids.len(),
2431 );
2432 } else if self.current_service == Service::CloudWatchAlarms {
2433 if self.column_selector_index < 18 {
2435 self.column_selector_index = 18; } else if self.column_selector_index < 22 {
2437 self.column_selector_index = 22; } else if self.column_selector_index < 28 {
2439 self.column_selector_index = 28; } else {
2441 self.column_selector_index = 0; }
2443 } else if self.current_service == Service::EcrRepositories
2444 && self.ecr_state.current_repository.is_some()
2445 {
2446 crate::ecr::actions::next_preferences(self);
2447 } else if self.current_service == Service::LambdaFunctions {
2448 crate::lambda::functions::next_preferences(self);
2449 } else if self.current_service == Service::LambdaApplications {
2450 crate::lambda::applications::next_preferences(self);
2451 } else if self.current_service == Service::CloudFormationStacks {
2452 let page_size_idx = self.cfn_column_ids.len() + 2;
2454 if self.column_selector_index < page_size_idx {
2455 self.column_selector_index = page_size_idx;
2456 } else {
2457 self.column_selector_index = 0;
2458 }
2459 } else if self.current_service == Service::CloudTrailEvents {
2460 if self.cloudtrail_state.current_event.is_some()
2461 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2462 {
2463 self.column_selector_index = 0;
2465 } else {
2466 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2467 if self.column_selector_index < page_size_idx {
2468 self.column_selector_index = page_size_idx;
2469 } else {
2470 self.column_selector_index = 0;
2471 }
2472 }
2473 } else if self.current_service == Service::Ec2Instances {
2474 let page_size_idx = self.ec2_column_ids.len() + 2;
2475 if self.column_selector_index < page_size_idx {
2476 self.column_selector_index = page_size_idx;
2477 } else {
2478 self.column_selector_index = 0;
2479 }
2480 } else if self.current_service == Service::IamUsers {
2481 if self.iam_state.current_user.is_some() {
2482 match self.iam_state.user_tab {
2483 UserTab::Permissions => {
2484 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2486 if self.column_selector_index < page_size_idx {
2487 self.column_selector_index = page_size_idx;
2488 } else {
2489 self.column_selector_index = 0;
2490 }
2491 }
2492 UserTab::Groups | UserTab::Tags => {
2493 if self.column_selector_index < 4 {
2495 self.column_selector_index = 4;
2496 } else {
2497 self.column_selector_index = 0;
2498 }
2499 }
2500 UserTab::LastAccessed => {
2501 if self.column_selector_index < 5 {
2503 self.column_selector_index = 5;
2504 } else {
2505 self.column_selector_index = 0;
2506 }
2507 }
2508 _ => {}
2509 }
2510 } else {
2511 let page_size_idx = self.iam_user_column_ids.len() + 2;
2513 if self.column_selector_index < page_size_idx {
2514 self.column_selector_index = page_size_idx;
2515 } else {
2516 self.column_selector_index = 0;
2517 }
2518 }
2519 } else if self.current_service == Service::IamRoles {
2520 if self.iam_state.current_role.is_some() {
2521 match self.iam_state.role_tab {
2522 RoleTab::Permissions => {
2523 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2525 if self.column_selector_index < page_size_idx {
2526 self.column_selector_index = page_size_idx;
2527 } else {
2528 self.column_selector_index = 0;
2529 }
2530 }
2531 RoleTab::Tags => {
2532 if self.column_selector_index < 4 {
2534 self.column_selector_index = 4;
2535 } else {
2536 self.column_selector_index = 0;
2537 }
2538 }
2539 RoleTab::LastAccessed => {
2540 if self.column_selector_index < 5 {
2542 self.column_selector_index = 5;
2543 } else {
2544 self.column_selector_index = 0;
2545 }
2546 }
2547 _ => {}
2548 }
2549 } else {
2550 let page_size_idx = self.iam_role_column_ids.len() + 2;
2552 if self.column_selector_index < page_size_idx {
2553 self.column_selector_index = page_size_idx;
2554 } else {
2555 self.column_selector_index = 0;
2556 }
2557 }
2558 } else if self.current_service == Service::IamUserGroups {
2559 let page_size_idx = self.iam_group_column_ids.len() + 2;
2561 if self.column_selector_index < page_size_idx {
2562 self.column_selector_index = page_size_idx;
2563 } else {
2564 self.column_selector_index = 0;
2565 }
2566 } else if self.current_service == Service::SqsQueues
2567 && self.sqs_state.current_queue.is_some()
2568 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2569 {
2570 let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2572 if self.column_selector_index < page_size_idx {
2573 self.column_selector_index = page_size_idx;
2574 } else {
2575 self.column_selector_index = 0;
2576 }
2577 } else if self.current_service == Service::SqsQueues
2578 && self.sqs_state.current_queue.is_some()
2579 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2580 {
2581 let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2583 if self.column_selector_index < page_size_idx {
2584 self.column_selector_index = page_size_idx;
2585 } else {
2586 self.column_selector_index = 0;
2587 }
2588 } else if self.current_service == Service::SqsQueues
2589 && self.sqs_state.current_queue.is_some()
2590 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2591 {
2592 let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2594 if self.column_selector_index < page_size_idx {
2595 self.column_selector_index = page_size_idx;
2596 } else {
2597 self.column_selector_index = 0;
2598 }
2599 } else if self.current_service == Service::SqsQueues
2600 && self.sqs_state.current_queue.is_some()
2601 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2602 {
2603 let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2605 if self.column_selector_index < page_size_idx {
2606 self.column_selector_index = page_size_idx;
2607 } else {
2608 self.column_selector_index = 0;
2609 }
2610 } else if self.current_service == Service::S3Buckets
2611 && self.s3_state.current_bucket.is_none()
2612 {
2613 crate::s3::actions::next_preferences(self);
2614 } else if self.current_service == Service::CloudWatchLogGroups {
2615 if self.view_mode == ViewMode::Events {
2616 } else if self.view_mode == ViewMode::Detail {
2618 if self.log_groups_state.detail_tab == DetailTab::Tags {
2619 cycle_preference_next(
2621 &mut self.column_selector_index,
2622 self.cw_log_tag_column_ids.len(),
2623 );
2624 } else {
2625 cycle_preference_next(
2627 &mut self.column_selector_index,
2628 self.cw_log_stream_column_ids.len(),
2629 );
2630 }
2631 } else {
2632 cycle_preference_next(
2634 &mut self.column_selector_index,
2635 self.cw_log_group_column_ids.len(),
2636 );
2637 }
2638 }
2639 }
2640 Action::PrevPreferences => {
2641 if self.current_service == Service::ApiGatewayApis {
2642 cycle_preference_prev(
2643 &mut self.column_selector_index,
2644 self.apig_api_column_ids.len(),
2645 );
2646 } else if self.current_service == Service::CloudWatchAlarms {
2647 if self.column_selector_index >= 28 {
2649 self.column_selector_index = 22;
2650 } else if self.column_selector_index >= 22 {
2651 self.column_selector_index = 18;
2652 } else if self.column_selector_index >= 18 {
2653 self.column_selector_index = 0;
2654 } else {
2655 self.column_selector_index = 28;
2656 }
2657 } else if self.current_service == Service::EcrRepositories
2658 && self.ecr_state.current_repository.is_some()
2659 {
2660 crate::ecr::actions::prev_preferences(self);
2661 } else if self.current_service == Service::LambdaFunctions {
2662 crate::lambda::functions::prev_preferences(self);
2663 } else if self.current_service == Service::LambdaApplications {
2664 crate::lambda::applications::prev_preferences(self);
2665 } else if self.current_service == Service::CloudFormationStacks {
2666 let page_size_idx = self.cfn_column_ids.len() + 2;
2667 if self.column_selector_index >= page_size_idx {
2668 self.column_selector_index = 0;
2669 } else {
2670 self.column_selector_index = page_size_idx;
2671 }
2672 } else if self.current_service == Service::CloudTrailEvents {
2673 if self.cloudtrail_state.current_event.is_some()
2674 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2675 {
2676 self.column_selector_index = 0;
2678 } else {
2679 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2680 if self.column_selector_index >= page_size_idx {
2681 self.column_selector_index = 0;
2682 } else {
2683 self.column_selector_index = page_size_idx;
2684 }
2685 }
2686 } else if self.current_service == Service::Ec2Instances {
2687 let page_size_idx = self.ec2_column_ids.len() + 2;
2688 if self.column_selector_index >= page_size_idx {
2689 self.column_selector_index = 0;
2690 } else {
2691 self.column_selector_index = page_size_idx;
2692 }
2693 } else if self.current_service == Service::IamUsers {
2694 if self.iam_state.current_user.is_some() {
2695 match self.iam_state.user_tab {
2696 UserTab::Permissions => {
2697 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2698 if self.column_selector_index >= page_size_idx {
2699 self.column_selector_index = 0;
2700 } else {
2701 self.column_selector_index = page_size_idx;
2702 }
2703 }
2704 UserTab::Groups | UserTab::Tags => {
2705 if self.column_selector_index >= 4 {
2706 self.column_selector_index = 0;
2707 } else {
2708 self.column_selector_index = 4;
2709 }
2710 }
2711 UserTab::LastAccessed => {
2712 if self.column_selector_index >= 5 {
2713 self.column_selector_index = 0;
2714 } else {
2715 self.column_selector_index = 5;
2716 }
2717 }
2718 _ => {}
2719 }
2720 } else {
2721 let page_size_idx = self.iam_user_column_ids.len() + 2;
2722 if self.column_selector_index >= page_size_idx {
2723 self.column_selector_index = 0;
2724 } else {
2725 self.column_selector_index = page_size_idx;
2726 }
2727 }
2728 } else if self.current_service == Service::IamRoles {
2729 if self.iam_state.current_role.is_some() {
2730 match self.iam_state.role_tab {
2731 RoleTab::Permissions => {
2732 let page_size_idx = self.iam_policy_column_ids.len() + 2;
2733 if self.column_selector_index >= page_size_idx {
2734 self.column_selector_index = 0;
2735 } else {
2736 self.column_selector_index = page_size_idx;
2737 }
2738 }
2739 RoleTab::Tags => {
2740 if self.column_selector_index >= 4 {
2741 self.column_selector_index = 0;
2742 } else {
2743 self.column_selector_index = 4;
2744 }
2745 }
2746 RoleTab::LastAccessed => {
2747 if self.column_selector_index >= 5 {
2748 self.column_selector_index = 0;
2749 } else {
2750 self.column_selector_index = 5;
2751 }
2752 }
2753 _ => {}
2754 }
2755 } else {
2756 let page_size_idx = self.iam_role_column_ids.len() + 2;
2757 if self.column_selector_index >= page_size_idx {
2758 self.column_selector_index = 0;
2759 } else {
2760 self.column_selector_index = page_size_idx;
2761 }
2762 }
2763 } else if self.current_service == Service::IamUserGroups {
2764 let page_size_idx = self.iam_group_column_ids.len() + 2;
2765 if self.column_selector_index >= page_size_idx {
2766 self.column_selector_index = 0;
2767 } else {
2768 self.column_selector_index = page_size_idx;
2769 }
2770 } else if self.current_service == Service::SqsQueues
2771 && self.sqs_state.current_queue.is_some()
2772 {
2773 let page_size_idx = match self.sqs_state.detail_tab {
2774 SqsQueueDetailTab::LambdaTriggers => {
2775 self.sqs_state.trigger_column_ids.len() + 2
2776 }
2777 SqsQueueDetailTab::EventBridgePipes => {
2778 self.sqs_state.pipe_column_ids.len() + 2
2779 }
2780 SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
2781 SqsQueueDetailTab::SnsSubscriptions => {
2782 self.sqs_state.subscription_column_ids.len() + 2
2783 }
2784 _ => 0,
2785 };
2786 if page_size_idx > 0 {
2787 if self.column_selector_index >= page_size_idx {
2788 self.column_selector_index = 0;
2789 } else {
2790 self.column_selector_index = page_size_idx;
2791 }
2792 }
2793 } else if self.current_service == Service::S3Buckets
2794 && self.s3_state.current_bucket.is_none()
2795 {
2796 crate::s3::actions::prev_preferences(self);
2797 } else if self.current_service == Service::CloudWatchLogGroups {
2798 if self.view_mode == ViewMode::Events {
2799 } else if self.view_mode == ViewMode::Detail {
2801 if self.log_groups_state.detail_tab == DetailTab::Tags {
2802 cycle_preference_prev(
2804 &mut self.column_selector_index,
2805 self.cw_log_tag_column_ids.len(),
2806 );
2807 } else {
2808 cycle_preference_prev(
2810 &mut self.column_selector_index,
2811 self.cw_log_stream_column_ids.len(),
2812 );
2813 }
2814 } else {
2815 cycle_preference_prev(
2817 &mut self.column_selector_index,
2818 self.cw_log_group_column_ids.len(),
2819 );
2820 }
2821 }
2822 }
2823 Action::CloseColumnSelector => {
2824 self.mode = Mode::Normal;
2825 self.preference_section = Preferences::Columns;
2826 }
2827 Action::NextDetailTab => {
2828 if self.current_service == Service::CloudTrailEvents
2829 && self.cloudtrail_state.current_event.is_some()
2830 {
2831 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
2832 } else if self.current_service == Service::ApiGatewayApis
2833 && self.apig_state.current_api.is_some()
2834 {
2835 self.apig_state.detail_tab = self.apig_state.detail_tab.next();
2836 } else if self.current_service == Service::SqsQueues
2837 && self.sqs_state.current_queue.is_some()
2838 {
2839 self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
2840 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2841 self.sqs_state.set_metrics_loading(true);
2842 self.sqs_state.set_monitoring_scroll(0);
2843 self.sqs_state.clear_metrics();
2844 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
2845 self.sqs_state.triggers.loading = true;
2846 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
2847 self.sqs_state.pipes.loading = true;
2848 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
2849 self.sqs_state.tags.loading = true;
2850 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
2851 self.sqs_state.subscriptions.loading = true;
2852 }
2853 } else if self.current_service == Service::Ec2Instances
2854 && self.ec2_state.current_instance.is_some()
2855 {
2856 self.ec2_state.detail_tab = self.ec2_state.detail_tab.next();
2857 if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
2858 self.ec2_state.tags.loading = true;
2859 } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
2860 self.ec2_state.set_metrics_loading(true);
2861 self.ec2_state.set_monitoring_scroll(0);
2862 self.ec2_state.clear_metrics();
2863 }
2864 } else if self.current_service == Service::LambdaApplications
2865 && self.lambda_application_state.current_application.is_some()
2866 {
2867 crate::lambda::applications::next_detail_tab(self);
2868 } else if self.current_service == Service::IamRoles
2869 && self.iam_state.current_role.is_some()
2870 {
2871 self.iam_state.role_tab = self.iam_state.role_tab.next();
2872 if self.iam_state.role_tab == RoleTab::Tags {
2873 self.iam_state.tags.loading = true;
2874 }
2875 } else if self.current_service == Service::IamUsers
2876 && self.iam_state.current_user.is_some()
2877 {
2878 self.iam_state.user_tab = self.iam_state.user_tab.next();
2879 if self.iam_state.user_tab == UserTab::Tags {
2880 self.iam_state.user_tags.loading = true;
2881 }
2882 } else if self.current_service == Service::IamUserGroups
2883 && self.iam_state.current_group.is_some()
2884 {
2885 self.iam_state.group_tab = self.iam_state.group_tab.next();
2886 } else if self.view_mode == ViewMode::Detail {
2887 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
2888 if self.log_groups_state.detail_tab == DetailTab::Tags {
2889 self.log_groups_state.tags.loading = true;
2890 }
2891 } else if self.current_service == Service::S3Buckets {
2892 crate::s3::actions::next_detail_tab(self);
2893 } else if self.current_service == Service::CloudWatchAlarms {
2894 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2895 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2896 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2897 };
2898 self.alarms_state.table.reset();
2899 } else if self.current_service == Service::EcrRepositories
2900 && self.ecr_state.current_repository.is_none()
2901 {
2902 crate::ecr::actions::next_detail_tab(self);
2903 } else if self.current_service == Service::LambdaFunctions
2904 && self.lambda_state.current_function.is_some()
2905 {
2906 crate::lambda::functions::next_detail_tab(self);
2907 } else if self.current_service == Service::CloudFormationStacks
2908 && self.cfn_state.current_stack.is_some()
2909 {
2910 self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
2911 }
2912 }
2913 Action::PrevDetailTab => {
2914 if self.current_service == Service::CloudTrailEvents
2915 && self.cloudtrail_state.current_event.is_some()
2916 {
2917 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
2918 } else if self.current_service == Service::ApiGatewayApis
2919 && self.apig_state.current_api.is_some()
2920 {
2921 self.apig_state.detail_tab = self.apig_state.detail_tab.prev();
2922 } else if self.current_service == Service::SqsQueues
2923 && self.sqs_state.current_queue.is_some()
2924 {
2925 self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
2926 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2927 self.sqs_state.set_metrics_loading(true);
2928 self.sqs_state.set_monitoring_scroll(0);
2929 self.sqs_state.clear_metrics();
2930 }
2931 } else if self.current_service == Service::Ec2Instances
2932 && self.ec2_state.current_instance.is_some()
2933 {
2934 self.ec2_state.detail_tab = self.ec2_state.detail_tab.prev();
2935 if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
2936 self.ec2_state.tags.loading = true;
2937 } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
2938 self.ec2_state.set_metrics_loading(true);
2939 self.ec2_state.set_monitoring_scroll(0);
2940 self.ec2_state.clear_metrics();
2941 }
2942 } else if self.current_service == Service::LambdaApplications
2943 && self.lambda_application_state.current_application.is_some()
2944 {
2945 crate::lambda::applications::prev_detail_tab(self);
2946 } else if self.current_service == Service::IamRoles
2947 && self.iam_state.current_role.is_some()
2948 {
2949 self.iam_state.role_tab = self.iam_state.role_tab.prev();
2950 } else if self.current_service == Service::IamUsers
2951 && self.iam_state.current_user.is_some()
2952 {
2953 self.iam_state.user_tab = self.iam_state.user_tab.prev();
2954 } else if self.current_service == Service::IamUserGroups
2955 && self.iam_state.current_group.is_some()
2956 {
2957 self.iam_state.group_tab = self.iam_state.group_tab.prev();
2958 } else if self.view_mode == ViewMode::Detail {
2959 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
2960 } else if self.current_service == Service::S3Buckets {
2961 crate::s3::actions::prev_detail_tab(self);
2962 } else if self.current_service == Service::CloudWatchAlarms {
2963 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2964 AlarmTab::AllAlarms => AlarmTab::InAlarm,
2965 AlarmTab::InAlarm => AlarmTab::AllAlarms,
2966 };
2967 } else if self.current_service == Service::EcrRepositories
2968 && self.ecr_state.current_repository.is_none()
2969 {
2970 crate::ecr::actions::prev_detail_tab(self);
2971 } else if self.current_service == Service::LambdaFunctions
2972 && self.lambda_state.current_function.is_some()
2973 {
2974 crate::lambda::functions::prev_detail_tab(self);
2975 } else if self.current_service == Service::CloudFormationStacks
2976 && self.cfn_state.current_stack.is_some()
2977 {
2978 self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
2979 }
2980 }
2981 Action::StartFilter => {
2982 if !self.service_selected && self.tabs.is_empty() {
2984 return;
2985 }
2986
2987 if self.current_service == Service::CloudWatchInsights {
2988 self.mode = Mode::InsightsInput;
2989 } else if self.current_service == Service::CloudWatchAlarms {
2990 self.mode = Mode::FilterInput;
2991 } else if self.current_service == Service::CloudTrailEvents {
2992 self.mode = Mode::FilterInput;
2993 self.cloudtrail_state.input_focus = InputFocus::Filter;
2994 } else if self.current_service == Service::S3Buckets {
2995 self.mode = Mode::FilterInput;
2996 crate::s3::actions::start_filter(self);
2997 } else if self.current_service == Service::ApiGatewayApis
2998 || self.current_service == Service::EcrRepositories
2999 || self.current_service == Service::IamUsers
3000 || self.current_service == Service::IamUserGroups
3001 {
3002 self.mode = Mode::FilterInput;
3003 if self.current_service == Service::ApiGatewayApis {
3004 self.apig_state.input_focus = InputFocus::Filter;
3005 } else if self.current_service == Service::EcrRepositories
3006 && self.ecr_state.current_repository.is_none()
3007 {
3008 self.ecr_state.input_focus = InputFocus::Filter;
3009 }
3010 } else if self.current_service == Service::LambdaFunctions {
3011 self.mode = Mode::FilterInput;
3012 crate::lambda::functions::start_filter(self);
3013 } else if self.current_service == Service::LambdaApplications {
3014 self.mode = Mode::FilterInput;
3015 crate::lambda::applications::start_filter(self);
3016 } else if self.current_service == Service::IamRoles {
3017 self.mode = Mode::FilterInput;
3018 } else if self.current_service == Service::CloudFormationStacks {
3019 self.mode = Mode::FilterInput;
3020 if self.cfn_state.current_stack.is_some()
3021 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3022 {
3023 self.cfn_state.parameters_input_focus = InputFocus::Filter;
3024 } else if self.cfn_state.current_stack.is_some()
3025 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3026 {
3027 self.cfn_state.outputs_input_focus = InputFocus::Filter;
3028 } else {
3029 self.cfn_state.input_focus = InputFocus::Filter;
3030 }
3031 } else if self.current_service == Service::SqsQueues {
3032 self.mode = Mode::FilterInput;
3033 self.sqs_state.input_focus = InputFocus::Filter;
3034 } else if self.view_mode == ViewMode::List
3035 || (self.view_mode == ViewMode::Detail
3036 && (self.log_groups_state.detail_tab == DetailTab::LogStreams
3037 || self.log_groups_state.detail_tab == DetailTab::Tags))
3038 {
3039 self.mode = Mode::FilterInput;
3040 self.log_groups_state.filter_mode = true;
3041 self.log_groups_state.input_focus = InputFocus::Filter;
3042 } else if self.view_mode == ViewMode::Events {
3043 self.mode = Mode::EventFilterInput;
3044 }
3045 }
3046 Action::StartEventFilter => {
3047 if self.current_service == Service::CloudWatchInsights {
3048 self.mode = Mode::InsightsInput;
3049 } else if self.view_mode == ViewMode::List {
3050 self.mode = Mode::FilterInput;
3051 self.log_groups_state.filter_mode = true;
3052 self.log_groups_state.input_focus = InputFocus::Filter;
3053 } else if self.view_mode == ViewMode::Events {
3054 self.mode = Mode::EventFilterInput;
3055 } else if self.view_mode == ViewMode::Detail
3056 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3057 {
3058 self.mode = Mode::FilterInput;
3059 self.log_groups_state.filter_mode = true;
3060 self.log_groups_state.input_focus = InputFocus::Filter;
3061 }
3062 }
3063 Action::NextFilterFocus => {
3064 if self.current_service == Service::CloudTrailEvents
3065 && self.cloudtrail_state.current_event.is_some()
3066 {
3067 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
3068 } else if self.mode == Mode::FilterInput
3069 && self.current_service == Service::S3Buckets
3070 {
3071 crate::s3::actions::next_filter_focus(self);
3072 } else if self.mode == Mode::FilterInput
3073 && self.current_service == Service::Ec2Instances
3074 {
3075 self.ec2_state.input_focus =
3076 self.ec2_state.input_focus.next(&ec2::FILTER_CONTROLS);
3077 } else if self.mode == Mode::FilterInput
3078 && self.current_service == Service::LambdaApplications
3079 {
3080 crate::lambda::applications::next_filter_focus(self);
3081 } else if self.mode == Mode::FilterInput
3082 && self.current_service == Service::IamRoles
3083 && self.iam_state.current_role.is_some()
3084 {
3085 use crate::ui::iam::POLICY_FILTER_CONTROLS;
3086 self.iam_state.policy_input_focus = self
3087 .iam_state
3088 .policy_input_focus
3089 .next(&POLICY_FILTER_CONTROLS);
3090 } else if self.mode == Mode::FilterInput
3091 && self.current_service == Service::IamRoles
3092 && self.iam_state.current_role.is_none()
3093 {
3094 use crate::ui::iam::ROLE_FILTER_CONTROLS;
3095 self.iam_state.role_input_focus =
3096 self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
3097 } else if self.mode == Mode::FilterInput
3098 && self.current_service == Service::IamUsers
3099 && self.iam_state.current_user.is_some()
3100 {
3101 use crate::ui::iam::{
3102 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3103 USER_SIMPLE_FILTER_CONTROLS,
3104 };
3105 if self.iam_state.user_tab == UserTab::Permissions {
3106 self.iam_state.policy_input_focus = self
3107 .iam_state
3108 .policy_input_focus
3109 .next(&POLICY_FILTER_CONTROLS);
3110 } else if self.iam_state.user_tab == UserTab::LastAccessed {
3111 self.iam_state.last_accessed_input_focus = self
3112 .iam_state
3113 .last_accessed_input_focus
3114 .next(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3115 } else {
3116 self.iam_state.user_input_focus = self
3117 .iam_state
3118 .user_input_focus
3119 .next(&USER_SIMPLE_FILTER_CONTROLS);
3120 }
3121 } else if self.mode == Mode::FilterInput
3122 && self.current_service == Service::IamUserGroups
3123 {
3124 use crate::ui::iam::GROUP_FILTER_CONTROLS;
3125 self.iam_state.group_input_focus = self
3126 .iam_state
3127 .group_input_focus
3128 .next(&GROUP_FILTER_CONTROLS);
3129 } else if self.mode == Mode::InsightsInput {
3130 use crate::app::InsightsFocus;
3131 self.insights_state.insights.insights_focus =
3132 match self.insights_state.insights.insights_focus {
3133 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
3134 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
3135 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
3136 InsightsFocus::Query => InsightsFocus::QueryLanguage,
3137 };
3138 } else if self.mode == Mode::FilterInput
3139 && self.current_service == Service::CloudFormationStacks
3140 {
3141 if self.cfn_state.current_stack.is_some()
3142 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3143 {
3144 self.cfn_state.parameters_input_focus = self
3145 .cfn_state
3146 .parameters_input_focus
3147 .next(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3148 } else if self.cfn_state.current_stack.is_some()
3149 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3150 {
3151 self.cfn_state.outputs_input_focus = self
3152 .cfn_state
3153 .outputs_input_focus
3154 .next(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3155 } else if self.cfn_state.current_stack.is_some()
3156 && self.cfn_state.detail_tab == CfnDetailTab::Resources
3157 {
3158 self.cfn_state.resources_input_focus = self
3159 .cfn_state
3160 .resources_input_focus
3161 .next(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3162 } else {
3163 self.cfn_state.input_focus = self
3164 .cfn_state
3165 .input_focus
3166 .next(&CfnStateConstants::FILTER_CONTROLS);
3167 }
3168 } else if self.mode == Mode::FilterInput
3169 && self.current_service == Service::SqsQueues
3170 {
3171 if self.sqs_state.current_queue.is_some()
3172 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3173 {
3174 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3175 self.sqs_state.input_focus = self
3176 .sqs_state
3177 .input_focus
3178 .next(SUBSCRIPTION_FILTER_CONTROLS);
3179 } else {
3180 use crate::ui::sqs::FILTER_CONTROLS;
3181 self.sqs_state.input_focus =
3182 self.sqs_state.input_focus.next(FILTER_CONTROLS);
3183 }
3184 } else if self.mode == Mode::FilterInput
3185 && self.current_service == Service::CloudWatchLogGroups
3186 {
3187 use crate::ui::cw::logs::FILTER_CONTROLS;
3188 self.log_groups_state.input_focus =
3189 self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
3190 } else if self.mode == Mode::EventFilterInput {
3191 self.log_groups_state.event_input_focus =
3192 self.log_groups_state.event_input_focus.next();
3193 } else if self.mode == Mode::FilterInput
3194 && self.current_service == Service::CloudWatchAlarms
3195 {
3196 use crate::ui::cw::alarms::FILTER_CONTROLS;
3197 self.alarms_state.input_focus =
3198 self.alarms_state.input_focus.next(&FILTER_CONTROLS);
3199 } else if self.mode == Mode::FilterInput
3200 && self.current_service == Service::CloudTrailEvents
3201 {
3202 const FILTER_CONTROLS: [InputFocus; 2] =
3203 [InputFocus::Filter, InputFocus::Pagination];
3204 self.cloudtrail_state.input_focus =
3205 self.cloudtrail_state.input_focus.next(&FILTER_CONTROLS);
3206 } else if self.mode == Mode::FilterInput
3207 && self.current_service == Service::ApiGatewayApis
3208 {
3209 use crate::ui::apig::FILTER_CONTROLS;
3210 self.apig_state.input_focus =
3211 self.apig_state.input_focus.next(&FILTER_CONTROLS);
3212 } else if self.mode == Mode::FilterInput
3213 && self.current_service == Service::EcrRepositories
3214 && self.ecr_state.current_repository.is_none()
3215 {
3216 crate::ecr::actions::next_filter_focus(self);
3217 } else if self.mode == Mode::FilterInput
3218 && self.current_service == Service::LambdaFunctions
3219 {
3220 crate::lambda::functions::next_filter_focus(self);
3221 }
3222 }
3223 Action::PrevFilterFocus => {
3224 if self.current_service == Service::CloudTrailEvents
3225 && self.cloudtrail_state.current_event.is_some()
3226 {
3227 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
3228 } else if self.mode == Mode::FilterInput
3229 && self.current_service == Service::ApiGatewayApis
3230 {
3231 use crate::ui::apig::FILTER_CONTROLS;
3232 self.apig_state.input_focus =
3233 self.apig_state.input_focus.prev(&FILTER_CONTROLS);
3234 } else if self.mode == Mode::FilterInput
3235 && self.current_service == Service::S3Buckets
3236 {
3237 crate::s3::actions::prev_filter_focus(self);
3238 } else if self.mode == Mode::FilterInput
3239 && self.current_service == Service::Ec2Instances
3240 {
3241 self.ec2_state.input_focus =
3242 self.ec2_state.input_focus.prev(&ec2::FILTER_CONTROLS);
3243 } else if self.mode == Mode::FilterInput
3244 && self.current_service == Service::LambdaApplications
3245 {
3246 crate::lambda::applications::prev_filter_focus(self);
3247 } else if self.mode == Mode::FilterInput
3248 && self.current_service == Service::CloudFormationStacks
3249 {
3250 if self.cfn_state.current_stack.is_some()
3251 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3252 {
3253 self.cfn_state.parameters_input_focus = self
3254 .cfn_state
3255 .parameters_input_focus
3256 .prev(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3257 } else if self.cfn_state.current_stack.is_some()
3258 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3259 {
3260 self.cfn_state.outputs_input_focus = self
3261 .cfn_state
3262 .outputs_input_focus
3263 .prev(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3264 } else if self.cfn_state.current_stack.is_some()
3265 && self.cfn_state.detail_tab == CfnDetailTab::Resources
3266 {
3267 self.cfn_state.resources_input_focus = self
3268 .cfn_state
3269 .resources_input_focus
3270 .prev(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3271 } else {
3272 self.cfn_state.input_focus = self
3273 .cfn_state
3274 .input_focus
3275 .prev(&CfnStateConstants::FILTER_CONTROLS);
3276 }
3277 } else if self.mode == Mode::FilterInput
3278 && self.current_service == Service::SqsQueues
3279 {
3280 if self.sqs_state.current_queue.is_some()
3281 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3282 {
3283 use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3284 self.sqs_state.input_focus = self
3285 .sqs_state
3286 .input_focus
3287 .prev(SUBSCRIPTION_FILTER_CONTROLS);
3288 } else {
3289 use crate::ui::sqs::FILTER_CONTROLS;
3290 self.sqs_state.input_focus =
3291 self.sqs_state.input_focus.prev(FILTER_CONTROLS);
3292 }
3293 } else if self.mode == Mode::FilterInput
3294 && self.current_service == Service::IamRoles
3295 && self.iam_state.current_role.is_none()
3296 {
3297 use crate::ui::iam::ROLE_FILTER_CONTROLS;
3298 self.iam_state.role_input_focus =
3299 self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
3300 } else if self.mode == Mode::FilterInput
3301 && self.current_service == Service::IamUsers
3302 && self.iam_state.current_user.is_some()
3303 {
3304 use crate::ui::iam::{
3305 POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3306 USER_SIMPLE_FILTER_CONTROLS,
3307 };
3308 if self.iam_state.user_tab == UserTab::Permissions {
3309 self.iam_state.policy_input_focus = self
3310 .iam_state
3311 .policy_input_focus
3312 .prev(&POLICY_FILTER_CONTROLS);
3313 } else if self.iam_state.user_tab == UserTab::LastAccessed {
3314 self.iam_state.last_accessed_input_focus = self
3315 .iam_state
3316 .last_accessed_input_focus
3317 .prev(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3318 } else {
3319 self.iam_state.user_input_focus = self
3320 .iam_state
3321 .user_input_focus
3322 .prev(&USER_SIMPLE_FILTER_CONTROLS);
3323 }
3324 } else if self.mode == Mode::FilterInput
3325 && self.current_service == Service::IamUserGroups
3326 {
3327 use crate::ui::iam::GROUP_FILTER_CONTROLS;
3328 self.iam_state.group_input_focus = self
3329 .iam_state
3330 .group_input_focus
3331 .prev(&GROUP_FILTER_CONTROLS);
3332 } else if self.mode == Mode::FilterInput
3333 && self.current_service == Service::CloudWatchLogGroups
3334 {
3335 use crate::ui::cw::logs::FILTER_CONTROLS;
3336 self.log_groups_state.input_focus =
3337 self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
3338 } else if self.mode == Mode::EventFilterInput {
3339 self.log_groups_state.event_input_focus =
3340 self.log_groups_state.event_input_focus.prev();
3341 } else if self.mode == Mode::FilterInput
3342 && self.current_service == Service::IamRoles
3343 && self.iam_state.current_role.is_some()
3344 {
3345 use crate::ui::iam::POLICY_FILTER_CONTROLS;
3346 self.iam_state.policy_input_focus = self
3347 .iam_state
3348 .policy_input_focus
3349 .prev(&POLICY_FILTER_CONTROLS);
3350 } else if self.mode == Mode::FilterInput
3351 && self.current_service == Service::CloudWatchAlarms
3352 {
3353 use crate::ui::cw::alarms::FILTER_CONTROLS;
3354 self.alarms_state.input_focus =
3355 self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
3356 } else if self.mode == Mode::FilterInput
3357 && self.current_service == Service::CloudTrailEvents
3358 {
3359 const FILTER_CONTROLS: [InputFocus; 2] =
3360 [InputFocus::Filter, InputFocus::Pagination];
3361 self.cloudtrail_state.input_focus =
3362 self.cloudtrail_state.input_focus.prev(&FILTER_CONTROLS);
3363 } else if self.mode == Mode::FilterInput
3364 && self.current_service == Service::EcrRepositories
3365 && self.ecr_state.current_repository.is_none()
3366 {
3367 crate::ecr::actions::prev_filter_focus(self);
3368 } else if self.mode == Mode::FilterInput
3369 && self.current_service == Service::LambdaFunctions
3370 {
3371 crate::lambda::functions::prev_filter_focus(self);
3372 }
3373 }
3374 Action::ToggleFilterCheckbox => {
3375 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
3376 if self.ec2_state.input_focus == EC2_STATE_FILTER {
3377 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
3378 self.ec2_state.table.reset();
3379 }
3380 } else if self.mode == Mode::InsightsInput {
3381 use crate::app::InsightsFocus;
3382 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3383 && self.insights_state.insights.show_dropdown
3384 && !self.insights_state.insights.log_group_matches.is_empty()
3385 {
3386 let selected_idx = self.insights_state.insights.dropdown_selected;
3387 if let Some(group_name) = self
3388 .insights_state
3389 .insights
3390 .log_group_matches
3391 .get(selected_idx)
3392 {
3393 let group_name = group_name.clone();
3394 if let Some(pos) = self
3395 .insights_state
3396 .insights
3397 .selected_log_groups
3398 .iter()
3399 .position(|g| g == &group_name)
3400 {
3401 self.insights_state.insights.selected_log_groups.remove(pos);
3402 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
3403 self.insights_state
3404 .insights
3405 .selected_log_groups
3406 .push(group_name);
3407 }
3408 }
3409 }
3410 } else if self.mode == Mode::FilterInput
3411 && self.current_service == Service::CloudFormationStacks
3412 {
3413 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
3414 match self.cfn_state.input_focus {
3415 STATUS_FILTER => {
3416 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3417 self.cfn_state.table.reset();
3418 }
3419 VIEW_NESTED => {
3420 self.cfn_state.view_nested = !self.cfn_state.view_nested;
3421 self.cfn_state.table.reset();
3422 }
3423 _ => {}
3424 }
3425 } else if self.mode == Mode::FilterInput
3426 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3427 {
3428 match self.log_groups_state.input_focus {
3429 InputFocus::Checkbox("ExactMatch") => {
3430 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
3431 }
3432 InputFocus::Checkbox("ShowExpired") => {
3433 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
3434 }
3435 _ => {}
3436 }
3437 } else if self.mode == Mode::EventFilterInput
3438 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
3439 {
3440 self.log_groups_state.relative_unit =
3441 self.log_groups_state.relative_unit.next();
3442 }
3443 }
3444 Action::CycleSortColumn => {
3445 if self.view_mode == ViewMode::Detail
3446 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3447 {
3448 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
3449 StreamSort::Name => StreamSort::CreationTime,
3450 StreamSort::CreationTime => StreamSort::LastEventTime,
3451 StreamSort::LastEventTime => StreamSort::Name,
3452 };
3453 }
3454 }
3455 Action::ToggleSortDirection => {
3456 if self.view_mode == ViewMode::Detail
3457 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3458 {
3459 self.log_groups_state.stream_sort_desc =
3460 !self.log_groups_state.stream_sort_desc;
3461 }
3462 }
3463 Action::ScrollUp => {
3464 if self.mode == Mode::ErrorModal {
3465 self.error_scroll = self.error_scroll.saturating_sub(1);
3466 } else if self.current_service == Service::CloudTrailEvents
3467 && self.cloudtrail_state.current_event.is_some()
3468 {
3469 self.cloudtrail_state.event_json_scroll =
3470 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
3471 } else if self.current_service == Service::LambdaFunctions
3472 && self.lambda_state.current_function.is_some()
3473 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3474 && !self.lambda_state.is_metrics_loading()
3475 {
3476 self.lambda_state.set_monitoring_scroll(
3477 self.lambda_state.monitoring_scroll().saturating_sub(1),
3478 );
3479 } else if self.current_service == Service::Ec2Instances
3480 && self.ec2_state.current_instance.is_some()
3481 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
3482 && !self.ec2_state.is_metrics_loading()
3483 {
3484 self.ec2_state.set_monitoring_scroll(
3485 self.ec2_state.monitoring_scroll().saturating_sub(1),
3486 );
3487 } else if self.current_service == Service::SqsQueues
3488 && self.sqs_state.current_queue.is_some()
3489 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3490 && !self.sqs_state.is_metrics_loading()
3491 {
3492 self.sqs_state.set_monitoring_scroll(
3493 self.sqs_state.monitoring_scroll().saturating_sub(1),
3494 );
3495 } else if self.view_mode == ViewMode::PolicyView {
3496 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
3497 } else if self.current_service == Service::IamRoles
3498 && self.iam_state.current_role.is_some()
3499 && self.iam_state.role_tab == RoleTab::TrustRelationships
3500 {
3501 self.iam_state.trust_policy_scroll =
3502 self.iam_state.trust_policy_scroll.saturating_sub(10);
3503 } else if self.view_mode == ViewMode::Events {
3504 if self.log_groups_state.event_scroll_offset == 0
3505 && self.log_groups_state.has_older_events
3506 {
3507 self.log_groups_state.loading = true;
3508 } else {
3509 self.log_groups_state.event_scroll_offset =
3510 self.log_groups_state.event_scroll_offset.saturating_sub(1);
3511 }
3512 } else if self.view_mode == ViewMode::InsightsResults {
3513 self.insights_state.insights.results_selected = self
3514 .insights_state
3515 .insights
3516 .results_selected
3517 .saturating_sub(1);
3518 } else if self.view_mode == ViewMode::Detail {
3519 self.log_groups_state.selected_stream =
3520 self.log_groups_state.selected_stream.saturating_sub(1);
3521 self.log_groups_state.expanded_stream = None;
3522 } else if self.view_mode == ViewMode::List
3523 && self.current_service == Service::CloudWatchLogGroups
3524 {
3525 self.log_groups_state.log_groups.selected =
3526 self.log_groups_state.log_groups.selected.saturating_sub(1);
3527 self.log_groups_state.log_groups.snap_to_page();
3528 } else if self.current_service == Service::EcrRepositories {
3529 crate::ecr::actions::scroll_up(self);
3530 }
3531 }
3532 Action::ScrollDown => {
3533 if self.mode == Mode::ErrorModal {
3534 if let Some(error_msg) = &self.error_message {
3535 let lines = error_msg.lines().count();
3536 let max_scroll = lines.saturating_sub(1);
3537 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
3538 }
3539 } else if self.current_service == Service::CloudTrailEvents
3540 && self.cloudtrail_state.current_event.is_some()
3541 {
3542 if let Some(event) = &self.cloudtrail_state.current_event {
3543 let lines = event.cloud_trail_event_json.lines().count();
3544 let max_scroll = lines.saturating_sub(1);
3545 self.cloudtrail_state.event_json_scroll =
3546 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
3547 }
3548 } else if self.current_service == Service::SqsQueues
3549 && self.sqs_state.current_queue.is_some()
3550 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3551 {
3552 self.sqs_state
3553 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
3554 } else if self.view_mode == ViewMode::PolicyView {
3555 let lines = self.iam_state.policy_document.lines().count();
3556 let max_scroll = lines.saturating_sub(1);
3557 self.iam_state.policy_scroll =
3558 (self.iam_state.policy_scroll + 10).min(max_scroll);
3559 } else if self.current_service == Service::IamRoles
3560 && self.iam_state.current_role.is_some()
3561 && self.iam_state.role_tab == RoleTab::TrustRelationships
3562 {
3563 let lines = self.iam_state.trust_policy_document.lines().count();
3564 let max_scroll = lines.saturating_sub(1);
3565 self.iam_state.trust_policy_scroll =
3566 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
3567 } else if self.view_mode == ViewMode::Events {
3568 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3569 if self.log_groups_state.event_scroll_offset >= max_scroll {
3570 } else {
3572 self.log_groups_state.event_scroll_offset =
3573 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3574 }
3575 } else if self.view_mode == ViewMode::InsightsResults {
3576 let max = self
3577 .insights_state
3578 .insights
3579 .query_results
3580 .len()
3581 .saturating_sub(1);
3582 self.insights_state.insights.results_selected =
3583 (self.insights_state.insights.results_selected + 1).min(max);
3584 } else if self.view_mode == ViewMode::Detail {
3585 let filtered_streams = filtered_log_streams(self);
3586 let max = filtered_streams.len().saturating_sub(1);
3587 self.log_groups_state.selected_stream =
3588 (self.log_groups_state.selected_stream + 1).min(max);
3589 } else if self.view_mode == ViewMode::List
3590 && self.current_service == Service::CloudWatchLogGroups
3591 {
3592 let filtered_groups = filtered_log_groups(self);
3593 self.log_groups_state
3594 .log_groups
3595 .next_item(filtered_groups.len());
3596 } else if self.current_service == Service::EcrRepositories {
3597 crate::ecr::actions::scroll_down(self);
3598 }
3599 }
3600
3601 Action::Refresh => {
3602 if self.mode == Mode::ProfilePicker {
3603 self.log_groups_state.loading = true;
3604 self.log_groups_state.loading_message = "Refreshing...".to_string();
3605 } else if self.mode == Mode::RegionPicker {
3606 self.measure_region_latencies();
3607 } else if self.mode == Mode::SessionPicker {
3608 self.sessions = Session::list_all().unwrap_or_default();
3609 } else if self.current_service == Service::CloudWatchAlarms
3610 && self.alarms_state.current_alarm.is_some()
3611 {
3612 self.alarms_state.metrics_loading = true;
3614 } else if self.current_service == Service::CloudWatchInsights
3615 && !self.insights_state.insights.selected_log_groups.is_empty()
3616 {
3617 self.log_groups_state.loading = true;
3618 self.insights_state.insights.query_completed = true;
3619 } else if self.current_service == Service::LambdaFunctions {
3620 crate::lambda::functions::refresh(self);
3621 } else if self.current_service == Service::LambdaApplications {
3622 crate::lambda::applications::refresh(self);
3623 } else if matches!(
3624 self.view_mode,
3625 ViewMode::Events | ViewMode::Detail | ViewMode::List
3626 ) {
3627 self.log_groups_state.loading = true;
3628 }
3629 }
3630 Action::Yank => {
3631 if self.mode == Mode::ErrorModal {
3632 if let Some(error) = &self.error_message {
3634 copy_to_clipboard(error);
3635 }
3636 } else if self.view_mode == ViewMode::Events {
3637 if let Some(event) = self
3638 .log_groups_state
3639 .log_events
3640 .get(self.log_groups_state.event_scroll_offset)
3641 {
3642 copy_to_clipboard(&event.message);
3643 }
3644 } else if self.current_service == Service::EcrRepositories {
3645 crate::ecr::actions::yank(self);
3646 } else if self.current_service == Service::LambdaFunctions {
3647 crate::lambda::functions::yank(self);
3648 } else if self.current_service == Service::CloudFormationStacks {
3649 if let Some(stack_name) = &self.cfn_state.current_stack {
3650 if let Some(stack) = self
3652 .cfn_state
3653 .table
3654 .items
3655 .iter()
3656 .find(|s| &s.name == stack_name)
3657 {
3658 copy_to_clipboard(&stack.stack_id);
3659 }
3660 } else {
3661 let filtered_stacks = filtered_cloudformation_stacks(self);
3663 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
3664 copy_to_clipboard(&stack.stack_id);
3665 }
3666 }
3667 } else if self.current_service == Service::IamUsers {
3668 if self.iam_state.current_user.is_some() {
3669 if let Some(user_name) = &self.iam_state.current_user {
3670 if let Some(user) = self
3671 .iam_state
3672 .users
3673 .items
3674 .iter()
3675 .find(|u| u.user_name == *user_name)
3676 {
3677 copy_to_clipboard(&user.arn);
3678 }
3679 }
3680 } else {
3681 let filtered_users = filtered_iam_users(self);
3682 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
3683 copy_to_clipboard(&user.arn);
3684 }
3685 }
3686 } else if self.current_service == Service::IamRoles {
3687 if self.iam_state.current_role.is_some() {
3688 if let Some(role_name) = &self.iam_state.current_role {
3689 if let Some(role) = self
3690 .iam_state
3691 .roles
3692 .items
3693 .iter()
3694 .find(|r| r.role_name == *role_name)
3695 {
3696 copy_to_clipboard(&role.arn);
3697 }
3698 }
3699 } else {
3700 let filtered_roles = filtered_iam_roles(self);
3701 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
3702 copy_to_clipboard(&role.arn);
3703 }
3704 }
3705 } else if self.current_service == Service::IamUserGroups {
3706 if self.iam_state.current_group.is_some() {
3707 if let Some(group_name) = &self.iam_state.current_group {
3708 let arn = iam::format_arn(&self.config.account_id, "group", group_name);
3709 copy_to_clipboard(&arn);
3710 }
3711 } else {
3712 let filtered_groups: Vec<_> = self
3713 .iam_state
3714 .groups
3715 .items
3716 .iter()
3717 .filter(|g| {
3718 if self.iam_state.groups.filter.is_empty() {
3719 true
3720 } else {
3721 g.group_name
3722 .to_lowercase()
3723 .contains(&self.iam_state.groups.filter.to_lowercase())
3724 }
3725 })
3726 .collect();
3727 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
3728 let arn = iam::format_arn(
3729 &self.config.account_id,
3730 "group",
3731 &group.group_name,
3732 );
3733 copy_to_clipboard(&arn);
3734 }
3735 }
3736 } else if self.current_service == Service::SqsQueues {
3737 if self.sqs_state.current_queue.is_some() {
3738 if let Some(queue) = self
3740 .sqs_state
3741 .queues
3742 .items
3743 .iter()
3744 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
3745 {
3746 let arn = format!(
3747 "arn:aws:sqs:{}:{}:{}",
3748 extract_region(&queue.url),
3749 extract_account_id(&queue.url),
3750 queue.name
3751 );
3752 copy_to_clipboard(&arn);
3753 }
3754 } else {
3755 let filtered_queues = filtered_queues(
3757 &self.sqs_state.queues.items,
3758 &self.sqs_state.queues.filter,
3759 );
3760 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
3761 let arn = format!(
3762 "arn:aws:sqs:{}:{}:{}",
3763 extract_region(&queue.url),
3764 extract_account_id(&queue.url),
3765 queue.name
3766 );
3767 copy_to_clipboard(&arn);
3768 }
3769 }
3770 } else if self.current_service == Service::ApiGatewayApis {
3771 if let Some(_api) = &self.apig_state.current_api {
3772 use crate::ui::apig::ApiDetailTab;
3773 if self.apig_state.detail_tab == ApiDetailTab::Routes {
3774 let (filtered_routes, _) = crate::ui::apig::filter_tree_items(
3776 &self.apig_state.routes.items,
3777 &self.apig_state.route_children,
3778 &self.apig_state.route_filter,
3779 );
3780 let filtered_refs: Vec<&Route> = filtered_routes.iter().collect();
3781 if let Some(route) = self.apig_state.routes.get_selected(&filtered_refs)
3782 {
3783 if !route.arn.is_empty() {
3784 copy_to_clipboard(&route.arn);
3785 }
3786 }
3787 }
3788 }
3789 }
3790 }
3791 Action::CopyToClipboard => {
3792 self.snapshot_requested = true;
3794 }
3795 Action::RetryLoad => {
3796 self.error_message = None;
3797 self.mode = Mode::Normal;
3798 self.log_groups_state.loading = true;
3799 }
3800 Action::ApplyFilter => {
3801 if self.mode == Mode::FilterInput
3802 && self.current_service == Service::SqsQueues
3803 && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
3804 {
3805 let regions = AwsRegion::all();
3806 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
3807 self.sqs_state.subscription_region_filter = region.code.to_string();
3808 }
3809 self.mode = Mode::Normal;
3810 } else if self.mode == Mode::InsightsInput {
3811 use crate::app::InsightsFocus;
3812 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3813 && self.insights_state.insights.show_dropdown
3814 {
3815 self.insights_state.insights.show_dropdown = false;
3817 self.mode = Mode::Normal;
3818 if !self.insights_state.insights.selected_log_groups.is_empty() {
3819 self.log_groups_state.loading = true;
3820 self.insights_state.insights.query_completed = true;
3821 }
3822 }
3823 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
3824 if let Ok(page) = self.page_input.parse::<usize>() {
3825 self.go_to_page(page);
3826 }
3827 self.page_input.clear();
3828 } else {
3829 self.mode = Mode::Normal;
3830 self.log_groups_state.filter_mode = false;
3831 }
3832 }
3833 Action::ToggleExactMatch => {
3834 if self.view_mode == ViewMode::Detail
3835 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3836 {
3837 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
3838 }
3839 }
3840 Action::ToggleShowExpired => {
3841 if self.view_mode == ViewMode::Detail
3842 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3843 {
3844 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
3845 }
3846 }
3847 Action::GoBack => {
3848 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
3850 self.mode = Mode::Normal;
3851 self.service_picker.filter.clear();
3852 }
3853 else if self.current_service == Service::ApiGatewayApis
3855 && self.apig_state.current_api.is_some()
3856 {
3857 self.apig_state.current_api = None;
3858 self.apig_state.routes.items.clear();
3859 self.apig_state.detail_tab = crate::ui::apig::ApiDetailTab::Routes;
3860 self.update_current_tab_breadcrumb();
3861 }
3862 else if self.current_service == Service::S3Buckets
3864 && self.s3_state.current_bucket.is_some()
3865 {
3866 crate::s3::actions::go_back(self);
3867 }
3868 else if self.current_service == Service::EcrRepositories
3870 && self.ecr_state.current_repository.is_some()
3871 {
3872 crate::ecr::actions::go_back(self);
3873 }
3874 else if self.current_service == Service::Ec2Instances
3876 && self.ec2_state.current_instance.is_some()
3877 {
3878 self.ec2_state.current_instance = None;
3879 self.view_mode = ViewMode::List;
3880 self.update_current_tab_breadcrumb();
3881 }
3882 else if self.current_service == Service::SqsQueues
3884 && self.sqs_state.current_queue.is_some()
3885 {
3886 self.sqs_state.current_queue = None;
3887 }
3888 else if self.current_service == Service::IamUsers
3890 && self.iam_state.current_user.is_some()
3891 {
3892 self.iam_state.current_user = None;
3893 self.iam_state.policies.items.clear();
3894 self.iam_state.policies.reset();
3895 self.update_current_tab_breadcrumb();
3896 }
3897 else if self.current_service == Service::IamUserGroups
3899 && self.iam_state.current_group.is_some()
3900 {
3901 self.iam_state.current_group = None;
3902 self.update_current_tab_breadcrumb();
3903 }
3904 else if self.current_service == Service::IamRoles {
3906 if self.view_mode == ViewMode::PolicyView {
3907 self.view_mode = ViewMode::Detail;
3909 self.iam_state.current_policy = None;
3910 self.iam_state.policy_document.clear();
3911 self.iam_state.policy_scroll = 0;
3912 self.update_current_tab_breadcrumb();
3913 } else if self.iam_state.current_role.is_some() {
3914 self.iam_state.current_role = None;
3915 self.iam_state.policies.items.clear();
3916 self.iam_state.policies.reset();
3917 self.update_current_tab_breadcrumb();
3918 }
3919 }
3920 else if self.current_service == Service::LambdaFunctions
3922 && (self.lambda_state.current_version.is_some()
3923 || self.lambda_state.current_alias.is_some()
3924 || self.lambda_state.current_function.is_some())
3925 {
3926 crate::lambda::functions::go_back(self);
3927 }
3928 else if self.current_service == Service::CloudWatchAlarms
3930 && self.alarms_state.current_alarm.is_some()
3931 {
3932 self.alarms_state.current_alarm = None;
3933 self.alarms_state.metric_data.clear();
3934 self.view_mode = ViewMode::List;
3935 self.update_current_tab_breadcrumb();
3936 }
3937 else if self.current_service == Service::LambdaApplications
3939 && self.lambda_application_state.current_application.is_some()
3940 {
3941 crate::lambda::applications::go_back(self);
3942 }
3943 else if self.current_service == Service::CloudFormationStacks
3945 && self.cfn_state.current_stack.is_some()
3946 {
3947 self.cfn_state.current_stack = None;
3948 self.update_current_tab_breadcrumb();
3949 }
3950 else if self.current_service == Service::CloudTrailEvents
3952 && self.cloudtrail_state.current_event.is_some()
3953 {
3954 self.cloudtrail_state.current_event = None;
3955 self.update_current_tab_breadcrumb();
3956 }
3957 else if self.view_mode == ViewMode::InsightsResults {
3959 if self.insights_state.insights.expanded_result.is_some() {
3960 self.insights_state.insights.expanded_result = None;
3961 }
3962 }
3963 else if self.current_service == Service::CloudWatchAlarms {
3965 if self.alarms_state.table.has_expanded_item() {
3966 self.alarms_state.table.collapse();
3967 }
3968 }
3969 else if self.current_service == Service::Ec2Instances {
3971 if self.ec2_state.current_instance.is_some()
3972 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
3973 {
3974 self.ec2_state.tags.collapse();
3975 } else {
3976 self.ec2_state.table.collapse();
3977 }
3978 }
3979 else if self.view_mode == ViewMode::Events {
3981 if self.log_groups_state.expanded_event.is_some() {
3982 self.log_groups_state.expanded_event = None;
3983 } else {
3984 self.view_mode = ViewMode::Detail;
3985 self.log_groups_state.event_filter.clear();
3986 }
3987 }
3988 else if self.view_mode == ViewMode::Detail {
3990 self.view_mode = ViewMode::List;
3991 self.log_groups_state.stream_filter.clear();
3992 self.log_groups_state.exact_match = false;
3993 self.log_groups_state.show_expired = false;
3994 }
3995 }
3996 Action::OpenInConsole | Action::OpenInBrowser => {
3997 let url = self.get_console_url();
3998 let _ = webbrowser::open(&url);
3999 }
4000 Action::ShowHelp => {
4001 self.mode = Mode::HelpModal;
4002 }
4003 Action::OpenRegionPicker => {
4004 self.region_filter.clear();
4005 self.region_picker_selected = 0;
4006 self.mode = Mode::RegionPicker;
4007 }
4008 Action::OpenProfilePicker => {
4009 self.profile_filter.clear();
4010 self.profile_picker_selected = 0;
4011 self.available_profiles = Self::load_aws_profiles();
4012 self.mode = Mode::ProfilePicker;
4013 }
4014 Action::OpenCalendar => {
4015 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
4016 self.calendar_selecting = CalendarField::StartDate;
4017 self.mode = Mode::CalendarPicker;
4018 }
4019 Action::CloseCalendar => {
4020 self.mode = Mode::Normal;
4021 self.calendar_date = None;
4022 }
4023 Action::CalendarPrevDay => {
4024 if let Some(date) = self.calendar_date {
4025 self.calendar_date = date.checked_sub(time::Duration::days(1));
4026 }
4027 }
4028 Action::CalendarNextDay => {
4029 if let Some(date) = self.calendar_date {
4030 self.calendar_date = date.checked_add(time::Duration::days(1));
4031 }
4032 }
4033 Action::CalendarPrevWeek => {
4034 if let Some(date) = self.calendar_date {
4035 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
4036 }
4037 }
4038 Action::CalendarNextWeek => {
4039 if let Some(date) = self.calendar_date {
4040 self.calendar_date = date.checked_add(time::Duration::weeks(1));
4041 }
4042 }
4043 Action::CalendarPrevMonth => {
4044 if let Some(date) = self.calendar_date {
4045 self.calendar_date = Some(if date.month() == time::Month::January {
4046 date.replace_month(time::Month::December)
4047 .unwrap()
4048 .replace_year(date.year() - 1)
4049 .unwrap()
4050 } else {
4051 date.replace_month(date.month().previous()).unwrap()
4052 });
4053 }
4054 }
4055 Action::CalendarNextMonth => {
4056 if let Some(date) = self.calendar_date {
4057 self.calendar_date = Some(if date.month() == time::Month::December {
4058 date.replace_month(time::Month::January)
4059 .unwrap()
4060 .replace_year(date.year() + 1)
4061 .unwrap()
4062 } else {
4063 date.replace_month(date.month().next()).unwrap()
4064 });
4065 }
4066 }
4067 Action::CalendarSelect => {
4068 if let Some(date) = self.calendar_date {
4069 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
4070 .unix_timestamp()
4071 * 1000;
4072 match self.calendar_selecting {
4073 CalendarField::StartDate => {
4074 self.log_groups_state.start_time = Some(timestamp);
4075 self.calendar_selecting = CalendarField::EndDate;
4076 }
4077 CalendarField::EndDate => {
4078 self.log_groups_state.end_time = Some(timestamp);
4079 self.mode = Mode::Normal;
4080 self.calendar_date = None;
4081 }
4082 }
4083 }
4084 }
4085 }
4086 }
4087
4088 pub fn filtered_services(&self) -> Vec<&'static str> {
4089 let mut services = if self.service_picker.filter.is_empty() {
4090 self.service_picker.services.clone()
4091 } else {
4092 self.service_picker
4093 .services
4094 .iter()
4095 .filter(|s| {
4096 s.to_lowercase()
4097 .contains(&self.service_picker.filter.to_lowercase())
4098 })
4099 .copied()
4100 .collect()
4101 };
4102 services.sort();
4103 services
4104 }
4105
4106 pub fn breadcrumbs(&self) -> String {
4107 if !self.service_selected {
4108 return String::new();
4109 }
4110
4111 let mut parts = vec![];
4112
4113 match self.current_service {
4114 Service::CloudWatchLogGroups => {
4115 parts.push("CloudWatch".to_string());
4116 parts.push("Log groups".to_string());
4117
4118 if self.view_mode != ViewMode::List {
4119 if let Some(group) = selected_log_group(self) {
4120 parts.push(group.name.clone());
4121 }
4122 }
4123
4124 if self.view_mode == ViewMode::Events {
4125 if let Some(stream) = self
4126 .log_groups_state
4127 .log_streams
4128 .get(self.log_groups_state.selected_stream)
4129 {
4130 parts.push(stream.name.clone());
4131 }
4132 }
4133 }
4134 Service::CloudWatchInsights => {
4135 parts.push("CloudWatch".to_string());
4136 parts.push("Insights".to_string());
4137 }
4138 Service::CloudWatchAlarms => {
4139 parts.push("CloudWatch".to_string());
4140 parts.push("Alarms".to_string());
4141 }
4142 Service::CloudTrailEvents => {
4143 parts.push("CloudTrail".to_string());
4144 parts.push("Event History".to_string());
4145 }
4146 Service::S3Buckets => {
4147 parts.extend(crate::s3::actions::breadcrumb(self));
4148 }
4149 Service::SqsQueues => {
4150 parts.push("SQS".to_string());
4151 parts.push("Queues".to_string());
4152 }
4153 Service::EcrRepositories => {
4154 parts.extend(crate::ecr::actions::breadcrumb(self));
4155 }
4156 Service::LambdaFunctions => {
4157 parts.extend(crate::lambda::functions::breadcrumb(self));
4158 }
4159 Service::LambdaApplications => {
4160 parts.extend(crate::lambda::applications::breadcrumb());
4161 }
4162 Service::CloudFormationStacks => {
4163 parts.push("CloudFormation".to_string());
4164 if let Some(stack_name) = &self.cfn_state.current_stack {
4165 parts.push(stack_name.clone());
4166 } else {
4167 parts.push("Stacks".to_string());
4168 }
4169 }
4170 Service::IamUsers => {
4171 parts.push("IAM".to_string());
4172 parts.push("Users".to_string());
4173 }
4174 Service::IamRoles => {
4175 parts.push("IAM".to_string());
4176 parts.push("Roles".to_string());
4177 if let Some(role_name) = &self.iam_state.current_role {
4178 parts.push(role_name.clone());
4179 if let Some(policy_name) = &self.iam_state.current_policy {
4180 parts.push(policy_name.clone());
4181 }
4182 }
4183 }
4184 Service::IamUserGroups => {
4185 parts.push("IAM".to_string());
4186 parts.push("User Groups".to_string());
4187 if let Some(group_name) = &self.iam_state.current_group {
4188 parts.push(group_name.clone());
4189 }
4190 }
4191 Service::Ec2Instances => {
4192 parts.push("EC2".to_string());
4193 parts.push("Instances".to_string());
4194 }
4195 Service::ApiGatewayApis => {
4196 parts.push("API Gateway".to_string());
4197 if let Some(api) = &self.apig_state.current_api {
4198 parts.push(api.name.clone());
4199 } else {
4200 parts.push("APIs".to_string());
4201 }
4202 }
4203 }
4204
4205 parts.join(" > ")
4206 }
4207
4208 pub fn update_current_tab_breadcrumb(&mut self) {
4209 if !self.tabs.is_empty() {
4210 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
4211 }
4212 }
4213
4214 pub fn get_console_url(&self) -> String {
4215 use crate::{cfn, cw, iam};
4216
4217 match self.current_service {
4218 Service::CloudWatchLogGroups => {
4219 if self.view_mode == ViewMode::Events {
4220 if let Some(group) = selected_log_group(self) {
4221 if let Some(stream) = self
4222 .log_groups_state
4223 .log_streams
4224 .get(self.log_groups_state.selected_stream)
4225 {
4226 return cw::logs::console_url_stream(
4227 &self.config.region,
4228 &group.name,
4229 &stream.name,
4230 );
4231 }
4232 }
4233 } else if self.view_mode == ViewMode::Detail {
4234 if let Some(group) = selected_log_group(self) {
4235 return cw::logs::console_url_detail(&self.config.region, &group.name);
4236 }
4237 }
4238 cw::logs::console_url_list(&self.config.region)
4239 }
4240 Service::CloudWatchInsights => cw::insights::console_url(
4241 &self.config.region,
4242 &self.config.account_id,
4243 &self.insights_state.insights.query_text,
4244 &self.insights_state.insights.selected_log_groups,
4245 ),
4246 Service::CloudWatchAlarms => {
4247 let view_type = match self.alarms_state.view_as {
4248 AlarmViewMode::Table | AlarmViewMode::Detail => "table",
4249 AlarmViewMode::Cards => "card",
4250 };
4251 cw::alarms::console_url(
4252 &self.config.region,
4253 view_type,
4254 self.alarms_state.table.page_size.value(),
4255 &self.alarms_state.sort_column,
4256 self.alarms_state.sort_direction.as_str(),
4257 )
4258 }
4259 Service::CloudTrailEvents => {
4260 if let Some(event) = &self.cloudtrail_state.current_event {
4261 format!(
4262 "https://{}.console.aws.amazon.com/cloudtrailv2/home?region={}#/events/{}",
4263 self.config.region, self.config.region, event.event_id
4264 )
4265 } else {
4266 format!(
4267 "https://{}.console.aws.amazon.com/cloudtrail/home?region={}#/events",
4268 self.config.region, self.config.region
4269 )
4270 }
4271 }
4272 Service::S3Buckets => crate::s3::actions::console_url(self),
4273 Service::SqsQueues => {
4274 if let Some(queue_url) = &self.sqs_state.current_queue {
4275 console_url_queue_detail(&self.config.region, queue_url)
4276 } else {
4277 console_url_queues(&self.config.region)
4278 }
4279 }
4280 Service::EcrRepositories => crate::ecr::actions::console_url(self),
4281 Service::LambdaFunctions => crate::lambda::functions::console_url(self),
4282 Service::LambdaApplications => crate::lambda::applications::console_url(self),
4283 Service::CloudFormationStacks => {
4284 if let Some(stack_name) = &self.cfn_state.current_stack {
4285 if let Some(stack) = self
4286 .cfn_state
4287 .table
4288 .items
4289 .iter()
4290 .find(|s| &s.name == stack_name)
4291 {
4292 return cfn::console_url_stack_detail_with_tab(
4293 &self.config.region,
4294 &stack.stack_id,
4295 &self.cfn_state.detail_tab,
4296 );
4297 }
4298 }
4299 cfn::console_url_stacks(&self.config.region)
4300 }
4301 Service::IamUsers => {
4302 if let Some(user_name) = &self.iam_state.current_user {
4303 let section = match self.iam_state.user_tab {
4304 UserTab::Permissions => "permissions",
4305 UserTab::Groups => "groups",
4306 UserTab::Tags => "tags",
4307 UserTab::SecurityCredentials => "security_credentials",
4308 UserTab::LastAccessed => "access_advisor",
4309 };
4310 iam::console_url_user_detail(&self.config.region, user_name, section)
4311 } else {
4312 iam::console_url_users(&self.config.region)
4313 }
4314 }
4315 Service::IamRoles => {
4316 if let Some(policy_name) = &self.iam_state.current_policy {
4317 if let Some(role_name) = &self.iam_state.current_role {
4318 return iam::console_url_role_policy(
4319 &self.config.region,
4320 role_name,
4321 policy_name,
4322 );
4323 }
4324 }
4325 if let Some(role_name) = &self.iam_state.current_role {
4326 let section = match self.iam_state.role_tab {
4327 RoleTab::Permissions => "permissions",
4328 RoleTab::TrustRelationships => "trust_relationships",
4329 RoleTab::Tags => "tags",
4330 RoleTab::LastAccessed => "access_advisor",
4331 RoleTab::RevokeSessions => "revoke_sessions",
4332 };
4333 iam::console_url_role_detail(&self.config.region, role_name, section)
4334 } else {
4335 iam::console_url_roles(&self.config.region)
4336 }
4337 }
4338 Service::IamUserGroups => iam::console_url_groups(&self.config.region),
4339 Service::Ec2Instances => {
4340 if let Some(instance_id) = &self.ec2_state.current_instance {
4341 format!(
4342 "https://{}.console.aws.amazon.com/ec2/home?region={}#InstanceDetails:instanceId={}",
4343 self.config.region, self.config.region, instance_id
4344 )
4345 } else {
4346 format!(
4347 "https://{}.console.aws.amazon.com/ec2/home?region={}#Instances:",
4348 self.config.region, self.config.region
4349 )
4350 }
4351 }
4352 Service::ApiGatewayApis => {
4353 use crate::apig;
4354 if let Some(api) = &self.apig_state.current_api {
4355 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4356 let protocol = api.protocol_type.to_uppercase();
4357 if protocol == "REST" {
4358 let (filtered_items, _) = crate::ui::apig::filter_tree_items(
4360 &self.apig_state.resources.items,
4361 &self.apig_state.resource_children,
4362 &self.apig_state.route_filter,
4363 );
4364
4365 let resource_id =
4366 if self.apig_state.resources.selected < filtered_items.len() {
4367 let mut current_row = 0;
4369 self.find_resource_at_row(
4370 &filtered_items,
4371 self.apig_state.resources.selected,
4372 &mut current_row,
4373 )
4374 } else {
4375 None
4376 };
4377 apig::console_url_resources(
4378 &self.config.region,
4379 &api.id,
4380 resource_id.as_deref(),
4381 )
4382 } else {
4383 let (filtered_items, filtered_children) =
4385 crate::ui::apig::filter_tree_items(
4386 &self.apig_state.routes.items,
4387 &self.apig_state.route_children,
4388 &self.apig_state.route_filter,
4389 );
4390
4391 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4392 &filtered_items,
4393 &self.apig_state.expanded_routes,
4394 &filtered_children,
4395 );
4396
4397 let route_id = if self.apig_state.routes.selected < total_rows {
4398 let mut current_row = 0;
4400 self.find_route_id_at_row_with_children(
4401 &filtered_items,
4402 &filtered_children,
4403 self.apig_state.routes.selected,
4404 &mut current_row,
4405 )
4406 } else {
4407 None
4408 };
4409 apig::console_url_routes(
4410 &self.config.region,
4411 &api.id,
4412 route_id.as_deref(),
4413 )
4414 }
4415 } else {
4416 apig::console_url_api(&self.config.region, &api.id)
4417 }
4418 } else {
4419 apig::console_url_apis(&self.config.region)
4420 }
4421 }
4422 }
4423 }
4424
4425 pub fn calculate_total_bucket_rows(&self) -> usize {
4426 calculate_total_bucket_rows(self)
4427 }
4428
4429 fn calculate_total_object_rows(&self) -> usize {
4430 calculate_total_object_rows(self)
4431 }
4432
4433 fn get_column_selector_max(&self) -> usize {
4434 if self.current_service == Service::ApiGatewayApis {
4435 self.apig_api_column_ids.len() + 6
4436 } else if self.current_service == Service::S3Buckets
4437 && self.s3_state.current_bucket.is_none()
4438 {
4439 crate::s3::actions::column_selector_max(self)
4440 } else if self.view_mode == ViewMode::Events {
4441 self.cw_log_event_column_ids.len() - 1
4442 } else if self.view_mode == ViewMode::Detail {
4443 self.cw_log_stream_column_ids.len() + 6
4444 } else if self.current_service == Service::CloudWatchAlarms {
4445 29
4446 } else if self.current_service == Service::CloudTrailEvents {
4447 if self.cloudtrail_state.current_event.is_some()
4448 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4449 {
4450 self.cloudtrail_resource_column_ids.len()
4451 } else {
4452 self.cloudtrail_event_column_ids.len() + 6
4453 }
4454 } else if self.current_service == Service::Ec2Instances {
4455 if self.ec2_state.current_instance.is_some()
4456 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4457 {
4458 self.ec2_state.tag_column_ids.len() + 6
4459 } else {
4460 self.ec2_column_ids.len() + 6
4461 }
4462 } else if self.current_service == Service::EcrRepositories {
4463 crate::ecr::actions::column_selector_max(self)
4464 } else if self.current_service == Service::SqsQueues {
4465 self.sqs_column_ids.len() - 1
4466 } else if self.current_service == Service::LambdaFunctions {
4467 crate::lambda::functions::column_selector_max(self)
4468 } else if self.current_service == Service::LambdaApplications {
4469 crate::lambda::applications::column_selector_max(self)
4470 } else if self.current_service == Service::CloudFormationStacks {
4471 self.cfn_column_ids.len() + 6
4472 } else if self.current_service == Service::IamUsers {
4473 if self.iam_state.current_user.is_some() {
4474 self.iam_policy_column_ids.len() + 5
4475 } else {
4476 self.iam_user_column_ids.len() + 5
4477 }
4478 } else if self.current_service == Service::IamRoles {
4479 if self.iam_state.current_role.is_some() {
4480 self.iam_policy_column_ids.len() + 5
4481 } else {
4482 self.iam_role_column_ids.len() + 5
4483 }
4484 } else {
4485 self.cw_log_group_column_ids.len() + 6
4486 }
4487 }
4488
4489 fn get_column_count(&self) -> usize {
4490 if self.current_service == Service::ApiGatewayApis {
4491 self.apig_api_column_ids.len()
4492 } else if self.current_service == Service::S3Buckets
4493 && self.s3_state.current_bucket.is_none()
4494 {
4495 crate::s3::actions::column_count(self)
4496 } else if self.view_mode == ViewMode::Events {
4497 self.cw_log_event_column_ids.len()
4498 } else if self.view_mode == ViewMode::Detail {
4499 self.cw_log_stream_column_ids.len()
4500 } else if self.current_service == Service::CloudWatchAlarms {
4501 14
4502 } else if self.current_service == Service::CloudTrailEvents {
4503 if self.cloudtrail_state.current_event.is_some()
4504 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4505 {
4506 self.cloudtrail_resource_column_ids.len()
4507 } else {
4508 self.cloudtrail_event_column_ids.len()
4509 }
4510 } else if self.current_service == Service::Ec2Instances {
4511 if self.ec2_state.current_instance.is_some()
4512 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4513 {
4514 self.ec2_state.tag_column_ids.len()
4515 } else {
4516 self.ec2_column_ids.len()
4517 }
4518 } else if self.current_service == Service::EcrRepositories {
4519 crate::ecr::actions::column_count(self)
4520 } else if self.current_service == Service::SqsQueues {
4521 self.sqs_column_ids.len()
4522 } else if self.current_service == Service::LambdaFunctions {
4523 crate::lambda::functions::column_count(self)
4524 } else if self.current_service == Service::LambdaApplications {
4525 crate::lambda::applications::column_count(self)
4526 } else if self.current_service == Service::CloudFormationStacks {
4527 self.cfn_column_ids.len()
4528 } else if self.current_service == Service::IamUsers {
4529 if self.iam_state.current_user.is_some() {
4530 self.iam_policy_column_ids.len()
4531 } else {
4532 self.iam_user_column_ids.len()
4533 }
4534 } else if self.current_service == Service::IamRoles {
4535 if self.iam_state.current_role.is_some() {
4536 self.iam_policy_column_ids.len()
4537 } else {
4538 self.iam_role_column_ids.len()
4539 }
4540 } else {
4541 self.cw_log_group_column_ids.len()
4542 }
4543 }
4544
4545 fn is_blank_row_index(&self, idx: usize) -> bool {
4546 let column_count = self.get_column_count();
4547 idx == column_count + 1
4549 }
4550
4551 fn next_item(&mut self) {
4552 match self.mode {
4553 Mode::FilterInput => {
4554 if self.current_service == Service::S3Buckets
4555 && crate::s3::actions::is_pagination_focused(self)
4556 {
4557 crate::s3::actions::page_down_filter_input(self);
4558 } else if self.current_service == Service::CloudTrailEvents
4559 && self.cloudtrail_state.input_focus == InputFocus::Pagination
4560 {
4561 let page_size = self.cloudtrail_state.table.page_size.value();
4563 let total_items = self.cloudtrail_state.table.items.len();
4564 let current_page = self.cloudtrail_state.table.selected / page_size;
4565 let total_pages = total_items.div_ceil(page_size);
4566 if current_page + 1 < total_pages {
4567 self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
4568 }
4569 } else if self.current_service == Service::CloudFormationStacks {
4570 use crate::ui::cfn::STATUS_FILTER;
4571 if self.cfn_state.input_focus == STATUS_FILTER {
4572 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
4573 self.cfn_state.table.reset();
4574 }
4575 } else if self.current_service == Service::IamUsers
4576 && self.iam_state.current_user.is_some()
4577 {
4578 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
4579 if self.iam_state.user_tab == UserTab::Permissions
4580 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
4581 {
4582 self.cycle_policy_type_next();
4583 } else if self.iam_state.user_tab == UserTab::LastAccessed
4584 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
4585 {
4586 self.iam_state.last_accessed_history_filter =
4587 self.iam_state.last_accessed_history_filter.next();
4588 self.iam_state.last_accessed_services.reset();
4589 }
4590 } else if self.current_service == Service::IamRoles
4591 && self.iam_state.current_role.is_some()
4592 && self.iam_state.role_tab == RoleTab::Permissions
4593 {
4594 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
4595 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
4596 self.cycle_policy_type_next();
4597 }
4598 } else if self.current_service == Service::Ec2Instances {
4599 if self.ec2_state.input_focus == EC2_STATE_FILTER {
4600 self.ec2_state.state_filter = self.ec2_state.state_filter.next();
4601 self.ec2_state.table.reset();
4602 }
4603 } else if self.current_service == Service::SqsQueues {
4604 use crate::ui::sqs::SUBSCRIPTION_REGION;
4605 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
4606 let regions = AwsRegion::all();
4607 self.sqs_state.subscription_region_selected =
4608 (self.sqs_state.subscription_region_selected + 1)
4609 .min(regions.len() - 1);
4610 self.sqs_state.subscriptions.reset();
4611 }
4612 }
4613 }
4614 Mode::RegionPicker => {
4615 let filtered = self.get_filtered_regions();
4616 if !filtered.is_empty() {
4617 self.region_picker_selected =
4618 (self.region_picker_selected + 1).min(filtered.len() - 1);
4619 }
4620 }
4621 Mode::ProfilePicker => {
4622 let filtered = self.get_filtered_profiles();
4623 if !filtered.is_empty() {
4624 self.profile_picker_selected =
4625 (self.profile_picker_selected + 1).min(filtered.len() - 1);
4626 }
4627 }
4628 Mode::SessionPicker => {
4629 let filtered = self.get_filtered_sessions();
4630 if !filtered.is_empty() {
4631 self.session_picker_selected =
4632 (self.session_picker_selected + 1).min(filtered.len() - 1);
4633 }
4634 }
4635 Mode::InsightsInput => {
4636 use crate::app::InsightsFocus;
4637 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4638 && self.insights_state.insights.show_dropdown
4639 && !self.insights_state.insights.log_group_matches.is_empty()
4640 {
4641 let max = self.insights_state.insights.log_group_matches.len() - 1;
4642 self.insights_state.insights.dropdown_selected =
4643 (self.insights_state.insights.dropdown_selected + 1).min(max);
4644 }
4645 }
4646 Mode::ColumnSelector => {
4647 let max = self.get_column_selector_max();
4648 let mut next_idx = (self.column_selector_index + 1).min(max);
4649 if self.is_blank_row_index(next_idx) {
4651 next_idx = (next_idx + 1).min(max);
4652 }
4653 self.column_selector_index = next_idx;
4654 }
4655 Mode::ServicePicker => {
4656 let filtered = self.filtered_services();
4657 if !filtered.is_empty() {
4658 self.service_picker.selected =
4659 (self.service_picker.selected + 1).min(filtered.len() - 1);
4660 }
4661 }
4662 Mode::TabPicker => {
4663 let filtered = self.get_filtered_tabs();
4664 if !filtered.is_empty() {
4665 self.tab_picker_selected =
4666 (self.tab_picker_selected + 1).min(filtered.len() - 1);
4667 }
4668 }
4669 Mode::Normal => {
4670 if !self.service_selected {
4671 let filtered = self.filtered_services();
4672 if !filtered.is_empty() {
4673 self.service_picker.selected =
4674 (self.service_picker.selected + 1).min(filtered.len() - 1);
4675 }
4676 } else if self.current_service == Service::S3Buckets {
4677 if self.s3_state.current_bucket.is_some() {
4678 if self.s3_state.object_tab == S3ObjectTab::Properties {
4679 self.s3_state.properties_scroll =
4681 self.s3_state.properties_scroll.saturating_add(1);
4682 } else {
4683 let total_rows = self.calculate_total_object_rows();
4685 let max = total_rows.saturating_sub(1);
4686 self.s3_state.selected_object =
4687 (self.s3_state.selected_object + 1).min(max);
4688
4689 let visible_rows = self.s3_state.object_visible_rows.get();
4691 if self.s3_state.selected_object
4692 >= self.s3_state.object_scroll_offset + visible_rows
4693 {
4694 self.s3_state.object_scroll_offset =
4695 self.s3_state.selected_object - visible_rows + 1;
4696 }
4697 }
4698 } else {
4699 let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
4701 if total_rows > 0 {
4702 self.s3_state.selected_row =
4703 (self.s3_state.selected_row + 1).min(total_rows - 1);
4704
4705 let visible_rows = self.s3_state.bucket_visible_rows.get();
4707 if self.s3_state.selected_row
4708 >= self.s3_state.bucket_scroll_offset + visible_rows
4709 {
4710 self.s3_state.bucket_scroll_offset =
4711 self.s3_state.selected_row - visible_rows + 1;
4712 }
4713 }
4714 }
4715 } else if self.view_mode == ViewMode::InsightsResults {
4716 let max = self
4717 .insights_state
4718 .insights
4719 .query_results
4720 .len()
4721 .saturating_sub(1);
4722 if self.insights_state.insights.results_selected < max {
4723 self.insights_state.insights.results_selected += 1;
4724 }
4725 } else if self.current_service == Service::ApiGatewayApis {
4726 if let Some(api) = &self.apig_state.current_api {
4727 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4728 let protocol = api.protocol_type.to_uppercase();
4729 if protocol == "REST" {
4730 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4732 &self.apig_state.resources.items,
4733 &self.apig_state.expanded_resources,
4734 &self.apig_state.resource_children,
4735 );
4736 if total_rows > 0 {
4737 self.apig_state.resources.selected =
4738 (self.apig_state.resources.selected + 1)
4739 .min(total_rows - 1);
4740 }
4741 } else {
4742 let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4744 &self.apig_state.routes.items,
4745 &self.apig_state.expanded_routes,
4746 &self.apig_state.route_children,
4747 );
4748 if total_rows > 0 {
4749 self.apig_state.routes.selected =
4750 (self.apig_state.routes.selected + 1).min(total_rows - 1);
4751 }
4752 }
4753 }
4754 } else {
4755 let filtered = crate::ui::apig::filtered_apis(self);
4756 if !filtered.is_empty() {
4757 self.apig_state.apis.next_item(filtered.len());
4758 }
4759 }
4760 } else if self.view_mode == ViewMode::PolicyView {
4761 let lines = self.iam_state.policy_document.lines().count();
4762 let max_scroll = lines.saturating_sub(1);
4763 self.iam_state.policy_scroll =
4764 (self.iam_state.policy_scroll + 1).min(max_scroll);
4765 } else if self.current_service == Service::CloudFormationStacks
4766 && self.cfn_state.current_stack.is_some()
4767 && self.cfn_state.detail_tab == CfnDetailTab::Template
4768 {
4769 let lines = self.cfn_state.template_body.lines().count();
4770 let max_scroll = lines.saturating_sub(1);
4771 self.cfn_state.template_scroll =
4772 (self.cfn_state.template_scroll + 1).min(max_scroll);
4773 } else if self.current_service == Service::SqsQueues
4774 && self.sqs_state.current_queue.is_some()
4775 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
4776 {
4777 let lines = self.sqs_state.policy_document.lines().count();
4778 let max_scroll = lines.saturating_sub(1);
4779 self.sqs_state.policy_scroll =
4780 (self.sqs_state.policy_scroll + 1).min(max_scroll);
4781 } else if self.current_service == Service::LambdaFunctions
4782 && self.lambda_state.current_function.is_some()
4783 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4784 && !self.lambda_state.is_metrics_loading()
4785 {
4786 self.lambda_state
4787 .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
4788 } else if self.current_service == Service::Ec2Instances
4789 && self.ec2_state.current_instance.is_some()
4790 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
4791 && !self.ec2_state.is_metrics_loading()
4792 {
4793 self.ec2_state
4794 .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
4795 } else if self.current_service == Service::SqsQueues
4796 && self.sqs_state.current_queue.is_some()
4797 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4798 && !self.sqs_state.is_metrics_loading()
4799 {
4800 self.sqs_state
4801 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
4802 } else if self.view_mode == ViewMode::Events {
4803 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
4804 if self.log_groups_state.event_scroll_offset >= max_scroll {
4805 } else {
4807 self.log_groups_state.event_scroll_offset =
4808 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
4809 }
4810 } else if self.current_service == Service::CloudWatchLogGroups {
4811 if self.view_mode == ViewMode::List {
4812 let filtered_groups = filtered_log_groups(self);
4813 self.log_groups_state
4814 .log_groups
4815 .next_item(filtered_groups.len());
4816 } else if self.view_mode == ViewMode::Detail {
4817 let filtered_streams = filtered_log_streams(self);
4818 if !filtered_streams.is_empty() {
4819 let max = filtered_streams.len() - 1;
4820 if self.log_groups_state.selected_stream >= max {
4821 } else {
4823 self.log_groups_state.selected_stream =
4824 (self.log_groups_state.selected_stream + 1).min(max);
4825 self.log_groups_state.expanded_stream = None;
4826 }
4827 }
4828 }
4829 } else if self.current_service == Service::CloudWatchAlarms {
4830 let filtered_alarms = match self.alarms_state.alarm_tab {
4831 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4832 AlarmTab::InAlarm => self
4833 .alarms_state
4834 .table
4835 .items
4836 .iter()
4837 .filter(|a| a.state.to_uppercase() == "ALARM")
4838 .count(),
4839 };
4840 if filtered_alarms > 0 {
4841 self.alarms_state.table.next_item(filtered_alarms);
4842 }
4843 } else if self.current_service == Service::CloudTrailEvents {
4844 if self.cloudtrail_state.current_event.is_some()
4845 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
4846 {
4847 if let Some(event) = &self.cloudtrail_state.current_event {
4849 let line_count = event.cloud_trail_event_json.lines().count();
4850 let max_scroll = line_count.saturating_sub(1);
4851 self.cloudtrail_state.event_json_scroll =
4852 (self.cloudtrail_state.event_json_scroll + 1).min(max_scroll);
4853 }
4854 } else {
4855 let page_size = self.cloudtrail_state.table.page_size.value();
4857 let current_page = self.cloudtrail_state.table.selected / page_size;
4858 let page_start = current_page * page_size;
4859 let page_end = page_start + page_size;
4860 let filtered_count = self.cloudtrail_state.table.items.len();
4861
4862 if self.cloudtrail_state.table.selected < filtered_count.saturating_sub(1) {
4864 self.cloudtrail_state.table.selected =
4865 (self.cloudtrail_state.table.selected + 1)
4866 .min(page_end - 1)
4867 .min(filtered_count - 1);
4868 }
4869 }
4870 } else if self.current_service == Service::Ec2Instances {
4871 if self.ec2_state.current_instance.is_some()
4872 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4873 {
4874 let filtered = crate::ui::ec2::filtered_tags(self);
4875 if !filtered.is_empty() {
4876 self.ec2_state.tags.next_item(filtered.len());
4877 }
4878 } else {
4879 let filtered: Vec<_> = self
4880 .ec2_state
4881 .table
4882 .items
4883 .iter()
4884 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
4885 .filter(|i| {
4886 if self.ec2_state.table.filter.is_empty() {
4887 return true;
4888 }
4889 i.name.contains(&self.ec2_state.table.filter)
4890 || i.instance_id.contains(&self.ec2_state.table.filter)
4891 || i.state.contains(&self.ec2_state.table.filter)
4892 || i.instance_type.contains(&self.ec2_state.table.filter)
4893 || i.availability_zone.contains(&self.ec2_state.table.filter)
4894 || i.security_groups.contains(&self.ec2_state.table.filter)
4895 || i.key_name.contains(&self.ec2_state.table.filter)
4896 })
4897 .collect();
4898 if !filtered.is_empty() {
4899 self.ec2_state.table.next_item(filtered.len());
4900 }
4901 }
4902 } else if self.current_service == Service::EcrRepositories {
4903 crate::ecr::actions::next_item(self);
4904 } else if self.current_service == Service::SqsQueues {
4905 if self.sqs_state.current_queue.is_some()
4906 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4907 {
4908 let filtered = filtered_lambda_triggers(self);
4909 if !filtered.is_empty() {
4910 self.sqs_state.triggers.next_item(filtered.len());
4911 }
4912 } else if self.sqs_state.current_queue.is_some()
4913 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4914 {
4915 let filtered = filtered_eventbridge_pipes(self);
4916 if !filtered.is_empty() {
4917 self.sqs_state.pipes.next_item(filtered.len());
4918 }
4919 } else if self.sqs_state.current_queue.is_some()
4920 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4921 {
4922 let filtered = filtered_tags(self);
4923 if !filtered.is_empty() {
4924 self.sqs_state.tags.next_item(filtered.len());
4925 }
4926 } else if self.sqs_state.current_queue.is_some()
4927 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4928 {
4929 let filtered = filtered_subscriptions(self);
4930 if !filtered.is_empty() {
4931 self.sqs_state.subscriptions.next_item(filtered.len());
4932 }
4933 } else {
4934 let filtered_queues = filtered_queues(
4935 &self.sqs_state.queues.items,
4936 &self.sqs_state.queues.filter,
4937 );
4938 if !filtered_queues.is_empty() {
4939 self.sqs_state.queues.next_item(filtered_queues.len());
4940 }
4941 }
4942 } else if self.current_service == Service::LambdaFunctions {
4943 crate::lambda::functions::next_item(self);
4944 } else if self.current_service == Service::LambdaApplications {
4945 crate::lambda::applications::next_item(self);
4946 } else if self.current_service == Service::CloudFormationStacks {
4947 if self.cfn_state.current_stack.is_some()
4948 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4949 {
4950 let filtered = filtered_parameters(self);
4951 self.cfn_state.parameters.next_item(filtered.len());
4952 } else if self.cfn_state.current_stack.is_some()
4953 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4954 {
4955 let filtered = filtered_outputs(self);
4956 self.cfn_state.outputs.next_item(filtered.len());
4957 } else if self.cfn_state.current_stack.is_some()
4958 && self.cfn_state.detail_tab == CfnDetailTab::Resources
4959 {
4960 let filtered = filtered_resources(self);
4961 self.cfn_state.resources.next_item(filtered.len());
4962 } else {
4963 let filtered = filtered_cloudformation_stacks(self);
4964 self.cfn_state.table.next_item(filtered.len());
4965 }
4966 } else if self.current_service == Service::IamUsers {
4967 if self.iam_state.current_user.is_some() {
4968 if self.iam_state.user_tab == UserTab::Tags {
4969 let filtered = filtered_user_tags(self);
4970 if !filtered.is_empty() {
4971 self.iam_state.user_tags.next_item(filtered.len());
4972 }
4973 } else {
4974 let filtered = filtered_iam_policies(self);
4975 if !filtered.is_empty() {
4976 self.iam_state.policies.next_item(filtered.len());
4977 }
4978 }
4979 } else {
4980 let filtered = filtered_iam_users(self);
4981 if !filtered.is_empty() {
4982 self.iam_state.users.next_item(filtered.len());
4983 }
4984 }
4985 } else if self.current_service == Service::IamRoles {
4986 if self.iam_state.current_role.is_some() {
4987 if self.iam_state.role_tab == RoleTab::TrustRelationships {
4988 let lines = self.iam_state.trust_policy_document.lines().count();
4989 let max_scroll = lines.saturating_sub(1);
4990 self.iam_state.trust_policy_scroll =
4991 (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
4992 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4993 self.iam_state.revoke_sessions_scroll =
4994 (self.iam_state.revoke_sessions_scroll + 1).min(19);
4995 } else if self.iam_state.role_tab == RoleTab::Tags {
4996 let filtered = filtered_iam_tags(self);
4997 if !filtered.is_empty() {
4998 self.iam_state.tags.next_item(filtered.len());
4999 }
5000 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5001 let filtered = filtered_last_accessed(self);
5002 if !filtered.is_empty() {
5003 self.iam_state
5004 .last_accessed_services
5005 .next_item(filtered.len());
5006 }
5007 } else {
5008 let filtered = filtered_iam_policies(self);
5009 if !filtered.is_empty() {
5010 self.iam_state.policies.next_item(filtered.len());
5011 }
5012 }
5013 } else {
5014 let filtered = filtered_iam_roles(self);
5015 if !filtered.is_empty() {
5016 self.iam_state.roles.next_item(filtered.len());
5017 }
5018 }
5019 } else if self.current_service == Service::IamUserGroups {
5020 if self.iam_state.current_group.is_some() {
5021 if self.iam_state.group_tab == GroupTab::Users {
5022 let filtered: Vec<_> = self
5023 .iam_state
5024 .group_users
5025 .items
5026 .iter()
5027 .filter(|u| {
5028 if self.iam_state.group_users.filter.is_empty() {
5029 true
5030 } else {
5031 u.user_name.to_lowercase().contains(
5032 &self.iam_state.group_users.filter.to_lowercase(),
5033 )
5034 }
5035 })
5036 .collect();
5037 if !filtered.is_empty() {
5038 self.iam_state.group_users.next_item(filtered.len());
5039 }
5040 } else if self.iam_state.group_tab == GroupTab::Permissions {
5041 let filtered = filtered_iam_policies(self);
5042 if !filtered.is_empty() {
5043 self.iam_state.policies.next_item(filtered.len());
5044 }
5045 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5046 let filtered = filtered_last_accessed(self);
5047 if !filtered.is_empty() {
5048 self.iam_state
5049 .last_accessed_services
5050 .next_item(filtered.len());
5051 }
5052 }
5053 } else {
5054 let filtered: Vec<_> = self
5055 .iam_state
5056 .groups
5057 .items
5058 .iter()
5059 .filter(|g| {
5060 if self.iam_state.groups.filter.is_empty() {
5061 true
5062 } else {
5063 g.group_name
5064 .to_lowercase()
5065 .contains(&self.iam_state.groups.filter.to_lowercase())
5066 }
5067 })
5068 .collect();
5069 if !filtered.is_empty() {
5070 self.iam_state.groups.next_item(filtered.len());
5071 }
5072 }
5073 }
5074 }
5075 _ => {}
5076 }
5077 }
5078
5079 fn prev_item(&mut self) {
5080 match self.mode {
5081 Mode::FilterInput => {
5082 if self.current_service == Service::S3Buckets
5083 && crate::s3::actions::is_pagination_focused(self)
5084 {
5085 crate::s3::actions::page_up_filter_input(self);
5086 } else if self.current_service == Service::CloudTrailEvents
5087 && self.cloudtrail_state.input_focus == InputFocus::Pagination
5088 {
5089 let page_size = self.cloudtrail_state.table.page_size.value();
5091 let current_page = self.cloudtrail_state.table.selected / page_size;
5092 if current_page > 0 {
5093 self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
5094 }
5095 } else if self.current_service == Service::CloudFormationStacks {
5096 use crate::ui::cfn::STATUS_FILTER;
5097 if self.cfn_state.input_focus == STATUS_FILTER {
5098 self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
5099 self.cfn_state.table.reset();
5100 }
5101 } else if self.current_service == Service::IamUsers
5102 && self.iam_state.current_user.is_some()
5103 {
5104 use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
5105 if self.iam_state.user_tab == UserTab::Permissions
5106 && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
5107 {
5108 self.cycle_policy_type_prev();
5109 } else if self.iam_state.user_tab == UserTab::LastAccessed
5110 && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
5111 {
5112 self.iam_state.last_accessed_history_filter =
5113 self.iam_state.last_accessed_history_filter.prev();
5114 self.iam_state.last_accessed_services.reset();
5115 }
5116 } else if self.current_service == Service::IamRoles
5117 && self.iam_state.current_role.is_some()
5118 && self.iam_state.role_tab == RoleTab::Permissions
5119 {
5120 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
5121 if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
5122 self.cycle_policy_type_prev();
5123 }
5124 } else if self.current_service == Service::Ec2Instances {
5125 if self.ec2_state.input_focus == EC2_STATE_FILTER {
5126 self.ec2_state.state_filter = self.ec2_state.state_filter.prev();
5127 self.ec2_state.table.reset();
5128 }
5129 } else if self.current_service == Service::SqsQueues {
5130 use crate::ui::sqs::SUBSCRIPTION_REGION;
5131 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
5132 self.sqs_state.subscription_region_selected = self
5133 .sqs_state
5134 .subscription_region_selected
5135 .saturating_sub(1);
5136 self.sqs_state.subscriptions.reset();
5137 }
5138 }
5139 }
5140 Mode::RegionPicker => {
5141 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
5142 }
5143 Mode::ProfilePicker => {
5144 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
5145 }
5146 Mode::SessionPicker => {
5147 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
5148 }
5149 Mode::InsightsInput => {
5150 use crate::app::InsightsFocus;
5151 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
5152 && self.insights_state.insights.show_dropdown
5153 && !self.insights_state.insights.log_group_matches.is_empty()
5154 {
5155 self.insights_state.insights.dropdown_selected = self
5156 .insights_state
5157 .insights
5158 .dropdown_selected
5159 .saturating_sub(1);
5160 }
5161 }
5162 Mode::ColumnSelector => {
5163 let mut prev_idx = self.column_selector_index.saturating_sub(1);
5164 if self.is_blank_row_index(prev_idx) {
5166 prev_idx = prev_idx.saturating_sub(1);
5167 }
5168 self.column_selector_index = prev_idx;
5169 }
5170 Mode::ServicePicker => {
5171 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5172 }
5173 Mode::TabPicker => {
5174 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
5175 }
5176 Mode::Normal => {
5177 if !self.service_selected {
5178 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5179 } else if self.current_service == Service::S3Buckets {
5180 if self.s3_state.current_bucket.is_some() {
5181 if self.s3_state.object_tab == S3ObjectTab::Properties {
5182 self.s3_state.properties_scroll =
5183 self.s3_state.properties_scroll.saturating_sub(1);
5184 } else {
5185 self.s3_state.selected_object =
5186 self.s3_state.selected_object.saturating_sub(1);
5187
5188 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5190 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5191 }
5192 }
5193 } else {
5194 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
5195
5196 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5198 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5199 }
5200 }
5201 } else if self.current_service == Service::ApiGatewayApis {
5202 if let Some(api) = &self.apig_state.current_api {
5203 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
5204 let protocol = api.protocol_type.to_uppercase();
5205 if protocol == "REST" {
5206 if self.apig_state.resources.selected > 0 {
5208 self.apig_state.resources.selected -= 1;
5209 }
5210 } else {
5211 if self.apig_state.routes.selected > 0 {
5213 self.apig_state.routes.selected -= 1;
5214 }
5215 }
5216 }
5217 } else {
5218 self.apig_state.apis.prev_item();
5219 }
5220 } else if self.view_mode == ViewMode::InsightsResults {
5221 if self.insights_state.insights.results_selected > 0 {
5222 self.insights_state.insights.results_selected -= 1;
5223 }
5224 } else if self.view_mode == ViewMode::PolicyView {
5225 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
5226 } else if self.current_service == Service::CloudFormationStacks
5227 && self.cfn_state.current_stack.is_some()
5228 && self.cfn_state.detail_tab == CfnDetailTab::Template
5229 {
5230 self.cfn_state.template_scroll =
5231 self.cfn_state.template_scroll.saturating_sub(1);
5232 } else if self.current_service == Service::SqsQueues
5233 && self.sqs_state.current_queue.is_some()
5234 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
5235 {
5236 } else if self.current_service == Service::LambdaFunctions
5237 && self.lambda_state.current_function.is_some()
5238 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5239 && !self.lambda_state.is_metrics_loading()
5240 {
5241 crate::lambda::functions::scroll_up(self);
5242 } else if self.current_service == Service::Ec2Instances
5243 && self.ec2_state.current_instance.is_some()
5244 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5245 && !self.ec2_state.is_metrics_loading()
5246 {
5247 self.ec2_state.set_monitoring_scroll(
5248 self.ec2_state.monitoring_scroll().saturating_sub(1),
5249 );
5250 } else if self.current_service == Service::SqsQueues
5251 && self.sqs_state.current_queue.is_some()
5252 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
5253 && !self.sqs_state.is_metrics_loading()
5254 {
5255 self.sqs_state.set_monitoring_scroll(
5256 self.sqs_state.monitoring_scroll().saturating_sub(1),
5257 );
5258 } else if self.view_mode == ViewMode::Events {
5259 if self.log_groups_state.event_scroll_offset == 0 {
5260 if self.log_groups_state.has_older_events {
5261 self.log_groups_state.loading = true;
5262 }
5263 } else {
5265 self.log_groups_state.event_scroll_offset =
5266 self.log_groups_state.event_scroll_offset.saturating_sub(1);
5267 }
5268 } else if self.current_service == Service::CloudWatchLogGroups {
5269 if self.view_mode == ViewMode::List {
5270 self.log_groups_state.log_groups.prev_item();
5271 } else if self.view_mode == ViewMode::Detail
5272 && self.log_groups_state.selected_stream > 0
5273 {
5274 self.log_groups_state.selected_stream =
5275 self.log_groups_state.selected_stream.saturating_sub(1);
5276 self.log_groups_state.expanded_stream = None;
5277 }
5278 } else if self.current_service == Service::CloudWatchAlarms {
5279 self.alarms_state.table.prev_item();
5280 } else if self.current_service == Service::CloudTrailEvents {
5281 if self.cloudtrail_state.current_event.is_some()
5282 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
5283 {
5284 self.cloudtrail_state.event_json_scroll =
5286 self.cloudtrail_state.event_json_scroll.saturating_sub(1);
5287 } else {
5288 let page_size = self.cloudtrail_state.table.page_size.value();
5290 let current_page = self.cloudtrail_state.table.selected / page_size;
5291 let page_start = current_page * page_size;
5292
5293 if self.cloudtrail_state.table.selected > page_start {
5295 self.cloudtrail_state.table.selected -= 1;
5296 }
5297 }
5298 } else if self.current_service == Service::Ec2Instances {
5299 if self.ec2_state.current_instance.is_some()
5300 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5301 {
5302 self.ec2_state.tags.prev_item();
5303 } else {
5304 self.ec2_state.table.prev_item();
5305 }
5306 } else if self.current_service == Service::EcrRepositories {
5307 crate::ecr::actions::prev_item(self);
5308 } else if self.current_service == Service::SqsQueues {
5309 if self.sqs_state.current_queue.is_some()
5310 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5311 {
5312 self.sqs_state.triggers.prev_item();
5313 } else if self.sqs_state.current_queue.is_some()
5314 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5315 {
5316 self.sqs_state.pipes.prev_item();
5317 } else if self.sqs_state.current_queue.is_some()
5318 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5319 {
5320 self.sqs_state.tags.prev_item();
5321 } else if self.sqs_state.current_queue.is_some()
5322 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5323 {
5324 self.sqs_state.subscriptions.prev_item();
5325 } else {
5326 self.sqs_state.queues.prev_item();
5327 }
5328 } else if self.current_service == Service::LambdaFunctions {
5329 crate::lambda::functions::prev_item(self);
5330 } else if self.current_service == Service::LambdaApplications {
5331 crate::lambda::applications::prev_item(self);
5332 } else if self.current_service == Service::CloudFormationStacks {
5333 if self.cfn_state.current_stack.is_some()
5334 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5335 {
5336 self.cfn_state.parameters.prev_item();
5337 } else if self.cfn_state.current_stack.is_some()
5338 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5339 {
5340 self.cfn_state.outputs.prev_item();
5341 } else if self.cfn_state.current_stack.is_some()
5342 && self.cfn_state.detail_tab == CfnDetailTab::Resources
5343 {
5344 self.cfn_state.resources.prev_item();
5345 } else {
5346 self.cfn_state.table.prev_item();
5347 }
5348 } else if self.current_service == Service::IamUsers {
5349 self.iam_state.users.prev_item();
5350 } else if self.current_service == Service::IamRoles {
5351 if self.iam_state.current_role.is_some() {
5352 if self.iam_state.role_tab == RoleTab::TrustRelationships {
5353 self.iam_state.trust_policy_scroll =
5354 self.iam_state.trust_policy_scroll.saturating_sub(1);
5355 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
5356 self.iam_state.revoke_sessions_scroll =
5357 self.iam_state.revoke_sessions_scroll.saturating_sub(1);
5358 } else if self.iam_state.role_tab == RoleTab::Tags {
5359 self.iam_state.tags.prev_item();
5360 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5361 self.iam_state.last_accessed_services.prev_item();
5362 } else {
5363 self.iam_state.policies.prev_item();
5364 }
5365 } else {
5366 self.iam_state.roles.prev_item();
5367 }
5368 } else if self.current_service == Service::IamUserGroups {
5369 if self.iam_state.current_group.is_some() {
5370 if self.iam_state.group_tab == GroupTab::Users {
5371 self.iam_state.group_users.prev_item();
5372 } else if self.iam_state.group_tab == GroupTab::Permissions {
5373 self.iam_state.policies.prev_item();
5374 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5375 self.iam_state.last_accessed_services.prev_item();
5376 }
5377 } else {
5378 self.iam_state.groups.prev_item();
5379 }
5380 }
5381 }
5382 _ => {}
5383 }
5384 }
5385
5386 fn page_down(&mut self) {
5387 if self.current_service == Service::CloudTrailEvents
5388 && self.cloudtrail_state.current_event.is_some()
5389 {
5390 if let Some(event) = &self.cloudtrail_state.current_event {
5391 let lines = event.cloud_trail_event_json.lines().count();
5392 let max_scroll = lines.saturating_sub(1);
5393 self.cloudtrail_state.event_json_scroll =
5394 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
5395 }
5396 } else if self.mode == Mode::ColumnSelector {
5397 let max = self.get_column_selector_max();
5398 let mut next_idx = (self.column_selector_index + 10).min(max);
5399 if self.is_blank_row_index(next_idx) {
5401 next_idx = (next_idx + 1).min(max);
5402 }
5403 self.column_selector_index = next_idx;
5404 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
5405 crate::s3::actions::page_down_filter_input(self);
5406 } else if self.mode == Mode::FilterInput
5407 && self.current_service == Service::CloudFormationStacks
5408 {
5409 if self.cfn_state.current_stack.is_some()
5410 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5411 {
5412 let page_size = self.cfn_state.parameters.page_size.value();
5413 let filtered_count = filtered_parameters(self).len();
5414 self.cfn_state.parameters_input_focus.handle_page_down(
5415 &mut self.cfn_state.parameters.selected,
5416 &mut self.cfn_state.parameters.scroll_offset,
5417 page_size,
5418 filtered_count,
5419 );
5420 } else if self.cfn_state.current_stack.is_some()
5421 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5422 {
5423 let page_size = self.cfn_state.outputs.page_size.value();
5424 let filtered_count = filtered_outputs(self).len();
5425 self.cfn_state.outputs_input_focus.handle_page_down(
5426 &mut self.cfn_state.outputs.selected,
5427 &mut self.cfn_state.outputs.scroll_offset,
5428 page_size,
5429 filtered_count,
5430 );
5431 } else {
5432 use crate::ui::cfn::filtered_cloudformation_stacks;
5433 let page_size = self.cfn_state.table.page_size.value();
5434 let filtered_count = filtered_cloudformation_stacks(self).len();
5435 self.cfn_state.input_focus.handle_page_down(
5436 &mut self.cfn_state.table.selected,
5437 &mut self.cfn_state.table.scroll_offset,
5438 page_size,
5439 filtered_count,
5440 );
5441 }
5442 } else if self.mode == Mode::FilterInput
5443 && self.current_service == Service::IamRoles
5444 && self.iam_state.current_role.is_none()
5445 {
5446 let page_size = self.iam_state.roles.page_size.value();
5447 let filtered_count = filtered_iam_roles(self).len();
5448 self.iam_state.role_input_focus.handle_page_down(
5449 &mut self.iam_state.roles.selected,
5450 &mut self.iam_state.roles.scroll_offset,
5451 page_size,
5452 filtered_count,
5453 );
5454 } else if self.mode == Mode::FilterInput
5455 && self.current_service == Service::CloudWatchAlarms
5456 {
5457 let page_size = self.alarms_state.table.page_size.value();
5458 let filtered_count = self.alarms_state.table.items.len();
5459 self.alarms_state.input_focus.handle_page_down(
5460 &mut self.alarms_state.table.selected,
5461 &mut self.alarms_state.table.scroll_offset,
5462 page_size,
5463 filtered_count,
5464 );
5465 } else if self.mode == Mode::FilterInput
5466 && self.current_service == Service::CloudTrailEvents
5467 {
5468 let page_size = self.cloudtrail_state.table.page_size.value();
5469 let filtered_count = self.cloudtrail_state.table.items.len();
5470 self.cloudtrail_state.input_focus.handle_page_down(
5471 &mut self.cloudtrail_state.table.selected,
5472 &mut self.cloudtrail_state.table.scroll_offset,
5473 page_size,
5474 filtered_count,
5475 );
5476 } else if self.mode == Mode::FilterInput
5477 && self.current_service == Service::CloudWatchLogGroups
5478 {
5479 if self.view_mode == ViewMode::List {
5480 let filtered = filtered_log_groups(self);
5482 let page_size = self.log_groups_state.log_groups.page_size.value();
5483 let filtered_count = filtered.len();
5484 self.log_groups_state.input_focus.handle_page_down(
5485 &mut self.log_groups_state.log_groups.selected,
5486 &mut self.log_groups_state.log_groups.scroll_offset,
5487 page_size,
5488 filtered_count,
5489 );
5490 } else {
5491 let filtered = filtered_log_streams(self);
5493 let page_size = self.log_groups_state.stream_page_size;
5494 let filtered_count = filtered.len();
5495 self.log_groups_state.input_focus.handle_page_down(
5496 &mut self.log_groups_state.selected_stream,
5497 &mut self.log_groups_state.stream_current_page,
5498 page_size,
5499 filtered_count,
5500 );
5501 self.log_groups_state.expanded_stream = None;
5502 }
5503 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5504 {
5505 crate::lambda::functions::page_down_filter_input(self);
5506 } else if self.mode == Mode::FilterInput
5507 && self.current_service == Service::LambdaApplications
5508 {
5509 crate::lambda::applications::page_down_filter_input(self);
5510 } else if self.mode == Mode::FilterInput
5511 && self.current_service == Service::EcrRepositories
5512 && self.ecr_state.current_repository.is_none()
5513 {
5514 crate::ecr::actions::page_down_filter_input(self);
5515 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5516 let page_size = self.iam_state.policies.page_size.value();
5517 let filtered_count = filtered_iam_policies(self).len();
5518 self.iam_state.policy_input_focus.handle_page_down(
5519 &mut self.iam_state.policies.selected,
5520 &mut self.iam_state.policies.scroll_offset,
5521 page_size,
5522 filtered_count,
5523 );
5524 } else if self.view_mode == ViewMode::PolicyView {
5525 let lines = self.iam_state.policy_document.lines().count();
5526 let max_scroll = lines.saturating_sub(1);
5527 self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
5528 } else if self.current_service == Service::CloudFormationStacks
5529 && self.cfn_state.current_stack.is_some()
5530 && self.cfn_state.detail_tab == CfnDetailTab::Template
5531 {
5532 let lines = self.cfn_state.template_body.lines().count();
5533 let max_scroll = lines.saturating_sub(1);
5534 self.cfn_state.template_scroll = (self.cfn_state.template_scroll + 10).min(max_scroll);
5535 } else if self.current_service == Service::LambdaFunctions
5536 && self.lambda_state.current_function.is_some()
5537 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5538 && !self.lambda_state.is_metrics_loading()
5539 {
5540 crate::lambda::functions::scroll_down(self);
5541 } else if self.current_service == Service::Ec2Instances
5542 && self.ec2_state.current_instance.is_some()
5543 && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5544 && !self.ec2_state.is_metrics_loading()
5545 {
5546 self.ec2_state
5547 .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
5548 } else if self.current_service == Service::SqsQueues
5549 && self.sqs_state.current_queue.is_some()
5550 {
5551 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5552 self.sqs_state
5553 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
5554 } else {
5555 let lines = self.sqs_state.policy_document.lines().count();
5556 let max_scroll = lines.saturating_sub(1);
5557 self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
5558 }
5559 } else if self.current_service == Service::IamRoles
5560 && self.iam_state.current_role.is_some()
5561 && self.iam_state.role_tab == RoleTab::TrustRelationships
5562 {
5563 let lines = self.iam_state.trust_policy_document.lines().count();
5564 let max_scroll = lines.saturating_sub(1);
5565 self.iam_state.trust_policy_scroll =
5566 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
5567 } else if self.current_service == Service::IamRoles
5568 && self.iam_state.current_role.is_some()
5569 && self.iam_state.role_tab == RoleTab::RevokeSessions
5570 {
5571 self.iam_state.revoke_sessions_scroll =
5572 (self.iam_state.revoke_sessions_scroll + 10).min(19);
5573 } else if self.mode == Mode::Normal {
5574 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5575 {
5576 let total_rows = self.calculate_total_bucket_rows();
5577 self.s3_state.selected_row = self
5578 .s3_state
5579 .selected_row
5580 .saturating_add(10)
5581 .min(total_rows.saturating_sub(1));
5582
5583 let visible_rows = self.s3_state.bucket_visible_rows.get();
5585 if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
5586 self.s3_state.bucket_scroll_offset =
5587 self.s3_state.selected_row - visible_rows + 1;
5588 }
5589 } else if self.current_service == Service::S3Buckets
5590 && self.s3_state.current_bucket.is_some()
5591 {
5592 let total_rows = self.calculate_total_object_rows();
5593 self.s3_state.selected_object = self
5594 .s3_state
5595 .selected_object
5596 .saturating_add(10)
5597 .min(total_rows.saturating_sub(1));
5598
5599 let visible_rows = self.s3_state.object_visible_rows.get();
5601 if self.s3_state.selected_object
5602 >= self.s3_state.object_scroll_offset + visible_rows
5603 {
5604 self.s3_state.object_scroll_offset =
5605 self.s3_state.selected_object - visible_rows + 1;
5606 }
5607 } else if self.current_service == Service::CloudWatchLogGroups
5608 && self.view_mode == ViewMode::List
5609 {
5610 let filtered = filtered_log_groups(self);
5611 self.log_groups_state.log_groups.page_down(filtered.len());
5612 } else if self.current_service == Service::CloudWatchLogGroups
5613 && self.view_mode == ViewMode::Detail
5614 {
5615 let len = filtered_log_streams(self).len();
5616 nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
5617 } else if self.view_mode == ViewMode::Events {
5618 let max = self.log_groups_state.log_events.len();
5619 nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
5620 } else if self.view_mode == ViewMode::InsightsResults {
5621 let max = self.insights_state.insights.query_results.len();
5622 nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
5623 } else if self.current_service == Service::CloudWatchAlarms {
5624 let filtered = match self.alarms_state.alarm_tab {
5625 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5626 AlarmTab::InAlarm => self
5627 .alarms_state
5628 .table
5629 .items
5630 .iter()
5631 .filter(|a| a.state.to_uppercase() == "ALARM")
5632 .count(),
5633 };
5634 if filtered > 0 {
5635 self.alarms_state.table.page_down(filtered);
5636 }
5637 } else if self.current_service == Service::CloudTrailEvents {
5638 let filtered_count = self.cloudtrail_state.table.items.len();
5639 if filtered_count > 0 {
5640 self.cloudtrail_state.table.page_down(filtered_count);
5641 }
5642 } else if self.current_service == Service::Ec2Instances {
5643 let filtered: Vec<_> = self
5644 .ec2_state
5645 .table
5646 .items
5647 .iter()
5648 .filter(|i| self.ec2_state.state_filter.matches(&i.state))
5649 .filter(|i| {
5650 if self.ec2_state.table.filter.is_empty() {
5651 return true;
5652 }
5653 i.name.contains(&self.ec2_state.table.filter)
5654 || i.instance_id.contains(&self.ec2_state.table.filter)
5655 || i.state.contains(&self.ec2_state.table.filter)
5656 || i.instance_type.contains(&self.ec2_state.table.filter)
5657 || i.availability_zone.contains(&self.ec2_state.table.filter)
5658 || i.security_groups.contains(&self.ec2_state.table.filter)
5659 || i.key_name.contains(&self.ec2_state.table.filter)
5660 })
5661 .collect();
5662 if !filtered.is_empty() {
5663 self.ec2_state.table.page_down(filtered.len());
5664 }
5665 } else if self.current_service == Service::EcrRepositories {
5666 crate::ecr::actions::page_down_normal(self);
5667 } else if self.current_service == Service::SqsQueues {
5668 let filtered =
5669 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter);
5670 self.sqs_state.queues.page_down(filtered.len());
5671 } else if self.current_service == Service::LambdaFunctions {
5672 crate::lambda::functions::page_down_normal(self);
5673 } else if self.current_service == Service::LambdaApplications {
5674 crate::lambda::applications::page_down_normal(self);
5675 } else if self.current_service == Service::CloudFormationStacks {
5676 if self.cfn_state.current_stack.is_some()
5677 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5678 {
5679 let filtered = filtered_parameters(self);
5680 self.cfn_state.parameters.page_down(filtered.len());
5681 } else if self.cfn_state.current_stack.is_some()
5682 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5683 {
5684 let filtered = filtered_outputs(self);
5685 self.cfn_state.outputs.page_down(filtered.len());
5686 } else {
5687 let filtered = filtered_cloudformation_stacks(self);
5688 self.cfn_state.table.page_down(filtered.len());
5689 }
5690 } else if self.current_service == Service::IamUsers {
5691 let len = filtered_iam_users(self).len();
5692 nav_page_down(&mut self.iam_state.users.selected, len, 10);
5693 } else if self.current_service == Service::IamRoles {
5694 if self.iam_state.current_role.is_some() {
5695 let filtered = filtered_iam_policies(self);
5696 if !filtered.is_empty() {
5697 self.iam_state.policies.page_down(filtered.len());
5698 }
5699 } else {
5700 let filtered = filtered_iam_roles(self);
5701 self.iam_state.roles.page_down(filtered.len());
5702 }
5703 } else if self.current_service == Service::IamUserGroups {
5704 if self.iam_state.current_group.is_some() {
5705 if self.iam_state.group_tab == GroupTab::Users {
5706 let filtered: Vec<_> = self
5707 .iam_state
5708 .group_users
5709 .items
5710 .iter()
5711 .filter(|u| {
5712 if self.iam_state.group_users.filter.is_empty() {
5713 true
5714 } else {
5715 u.user_name
5716 .to_lowercase()
5717 .contains(&self.iam_state.group_users.filter.to_lowercase())
5718 }
5719 })
5720 .collect();
5721 if !filtered.is_empty() {
5722 self.iam_state.group_users.page_down(filtered.len());
5723 }
5724 } else if self.iam_state.group_tab == GroupTab::Permissions {
5725 let filtered = filtered_iam_policies(self);
5726 if !filtered.is_empty() {
5727 self.iam_state.policies.page_down(filtered.len());
5728 }
5729 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5730 let filtered = filtered_last_accessed(self);
5731 if !filtered.is_empty() {
5732 self.iam_state
5733 .last_accessed_services
5734 .page_down(filtered.len());
5735 }
5736 }
5737 } else {
5738 let filtered: Vec<_> = self
5739 .iam_state
5740 .groups
5741 .items
5742 .iter()
5743 .filter(|g| {
5744 if self.iam_state.groups.filter.is_empty() {
5745 true
5746 } else {
5747 g.group_name
5748 .to_lowercase()
5749 .contains(&self.iam_state.groups.filter.to_lowercase())
5750 }
5751 })
5752 .collect();
5753 if !filtered.is_empty() {
5754 self.iam_state.groups.page_down(filtered.len());
5755 }
5756 }
5757 }
5758 }
5759 }
5760
5761 fn cycle_policy_type_next(&mut self) {
5762 let types = ["All types", "AWS managed", "Customer managed"];
5763 let current_idx = types
5764 .iter()
5765 .position(|&t| t == self.iam_state.policy_type_filter)
5766 .unwrap_or(0);
5767 let next_idx = (current_idx + 1) % types.len();
5768 self.iam_state.policy_type_filter = types[next_idx].to_string();
5769 self.iam_state.policies.reset();
5770 }
5771
5772 fn cycle_policy_type_prev(&mut self) {
5773 let types = ["All types", "AWS managed", "Customer managed"];
5774 let current_idx = types
5775 .iter()
5776 .position(|&t| t == self.iam_state.policy_type_filter)
5777 .unwrap_or(0);
5778 let prev_idx = if current_idx == 0 {
5779 types.len() - 1
5780 } else {
5781 current_idx - 1
5782 };
5783 self.iam_state.policy_type_filter = types[prev_idx].to_string();
5784 self.iam_state.policies.reset();
5785 }
5786
5787 fn page_up(&mut self) {
5788 if self.current_service == Service::CloudTrailEvents
5789 && self.cloudtrail_state.current_event.is_some()
5790 {
5791 self.cloudtrail_state.event_json_scroll =
5792 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
5793 } else if self.mode == Mode::ColumnSelector {
5794 let mut prev_idx = self.column_selector_index.saturating_sub(10);
5795 if self.is_blank_row_index(prev_idx) {
5797 prev_idx = prev_idx.saturating_sub(1);
5798 }
5799 self.column_selector_index = prev_idx;
5800 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
5801 crate::s3::actions::page_up_filter_input(self);
5802 } else if self.mode == Mode::FilterInput
5803 && self.current_service == Service::CloudFormationStacks
5804 {
5805 if self.cfn_state.current_stack.is_some()
5806 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5807 {
5808 let page_size = self.cfn_state.parameters.page_size.value();
5809 self.cfn_state.parameters_input_focus.handle_page_up(
5810 &mut self.cfn_state.parameters.selected,
5811 &mut self.cfn_state.parameters.scroll_offset,
5812 page_size,
5813 );
5814 } else if self.cfn_state.current_stack.is_some()
5815 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5816 {
5817 let page_size = self.cfn_state.outputs.page_size.value();
5818 self.cfn_state.outputs_input_focus.handle_page_up(
5819 &mut self.cfn_state.outputs.selected,
5820 &mut self.cfn_state.outputs.scroll_offset,
5821 page_size,
5822 );
5823 } else {
5824 let page_size = self.cfn_state.table.page_size.value();
5825 self.cfn_state.input_focus.handle_page_up(
5826 &mut self.cfn_state.table.selected,
5827 &mut self.cfn_state.table.scroll_offset,
5828 page_size,
5829 );
5830 }
5831 } else if self.mode == Mode::FilterInput
5832 && self.current_service == Service::IamRoles
5833 && self.iam_state.current_role.is_none()
5834 {
5835 let page_size = self.iam_state.roles.page_size.value();
5836 self.iam_state.role_input_focus.handle_page_up(
5837 &mut self.iam_state.roles.selected,
5838 &mut self.iam_state.roles.scroll_offset,
5839 page_size,
5840 );
5841 } else if self.mode == Mode::FilterInput
5842 && self.current_service == Service::CloudWatchAlarms
5843 {
5844 let page_size = self.alarms_state.table.page_size.value();
5845 self.alarms_state.input_focus.handle_page_up(
5846 &mut self.alarms_state.table.selected,
5847 &mut self.alarms_state.table.scroll_offset,
5848 page_size,
5849 );
5850 } else if self.mode == Mode::FilterInput
5851 && self.current_service == Service::CloudTrailEvents
5852 {
5853 let page_size = self.cloudtrail_state.table.page_size.value();
5854 self.cloudtrail_state.input_focus.handle_page_up(
5855 &mut self.cloudtrail_state.table.selected,
5856 &mut self.cloudtrail_state.table.scroll_offset,
5857 page_size,
5858 );
5859 } else if self.mode == Mode::FilterInput
5860 && self.current_service == Service::CloudWatchLogGroups
5861 {
5862 if self.view_mode == ViewMode::List {
5863 let page_size = self.log_groups_state.log_groups.page_size.value();
5865 self.log_groups_state.input_focus.handle_page_up(
5866 &mut self.log_groups_state.log_groups.selected,
5867 &mut self.log_groups_state.log_groups.scroll_offset,
5868 page_size,
5869 );
5870 } else {
5871 let page_size = self.log_groups_state.stream_page_size;
5873 self.log_groups_state.input_focus.handle_page_up(
5874 &mut self.log_groups_state.selected_stream,
5875 &mut self.log_groups_state.stream_current_page,
5876 page_size,
5877 );
5878 self.log_groups_state.expanded_stream = None;
5879 }
5880 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5881 {
5882 crate::lambda::functions::page_up_filter_input(self);
5883 } else if self.mode == Mode::FilterInput
5884 && self.current_service == Service::LambdaApplications
5885 {
5886 crate::lambda::applications::page_up_filter_input(self);
5887 } else if self.mode == Mode::FilterInput
5888 && self.current_service == Service::EcrRepositories
5889 && self.ecr_state.current_repository.is_none()
5890 {
5891 crate::ecr::actions::page_up_filter_input(self);
5892 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5893 let page_size = self.iam_state.policies.page_size.value();
5894 self.iam_state.policy_input_focus.handle_page_up(
5895 &mut self.iam_state.policies.selected,
5896 &mut self.iam_state.policies.scroll_offset,
5897 page_size,
5898 );
5899 } else if self.view_mode == ViewMode::PolicyView {
5900 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
5901 } else if self.current_service == Service::CloudFormationStacks
5902 && self.cfn_state.current_stack.is_some()
5903 && self.cfn_state.detail_tab == CfnDetailTab::Template
5904 {
5905 self.cfn_state.template_scroll = self.cfn_state.template_scroll.saturating_sub(10);
5906 } else if self.current_service == Service::SqsQueues
5907 && self.sqs_state.current_queue.is_some()
5908 {
5909 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5910 self.sqs_state
5911 .set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
5912 } else {
5913 self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
5914 }
5915 } else if self.current_service == Service::IamRoles
5916 && self.iam_state.current_role.is_some()
5917 && self.iam_state.role_tab == RoleTab::TrustRelationships
5918 {
5919 self.iam_state.trust_policy_scroll =
5920 self.iam_state.trust_policy_scroll.saturating_sub(10);
5921 } else if self.current_service == Service::IamRoles
5922 && self.iam_state.current_role.is_some()
5923 && self.iam_state.role_tab == RoleTab::RevokeSessions
5924 {
5925 self.iam_state.revoke_sessions_scroll =
5926 self.iam_state.revoke_sessions_scroll.saturating_sub(10);
5927 } else if self.mode == Mode::Normal {
5928 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5929 {
5930 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
5931
5932 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5934 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5935 }
5936 } else if self.current_service == Service::S3Buckets
5937 && self.s3_state.current_bucket.is_some()
5938 {
5939 self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
5940
5941 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5943 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5944 }
5945 } else if self.current_service == Service::CloudWatchLogGroups
5946 && self.view_mode == ViewMode::List
5947 {
5948 self.log_groups_state.log_groups.page_up();
5949 } else if self.current_service == Service::CloudWatchLogGroups
5950 && self.view_mode == ViewMode::Detail
5951 {
5952 self.log_groups_state.selected_stream =
5953 self.log_groups_state.selected_stream.saturating_sub(10);
5954 } else if self.view_mode == ViewMode::Events {
5955 if self.log_groups_state.event_scroll_offset < 10
5956 && self.log_groups_state.has_older_events
5957 {
5958 self.log_groups_state.loading = true;
5959 }
5960 self.log_groups_state.event_scroll_offset =
5961 self.log_groups_state.event_scroll_offset.saturating_sub(10);
5962 } else if self.view_mode == ViewMode::InsightsResults {
5963 self.insights_state.insights.results_selected = self
5964 .insights_state
5965 .insights
5966 .results_selected
5967 .saturating_sub(10);
5968 } else if self.current_service == Service::CloudWatchAlarms {
5969 self.alarms_state.table.page_up();
5970 } else if self.current_service == Service::CloudTrailEvents {
5971 self.cloudtrail_state.table.page_up();
5972 } else if self.current_service == Service::Ec2Instances {
5973 self.ec2_state.table.page_up();
5974 } else if self.current_service == Service::EcrRepositories {
5975 crate::ecr::actions::page_up_normal(self);
5976 } else if self.current_service == Service::SqsQueues {
5977 self.sqs_state.queues.page_up();
5978 } else if self.current_service == Service::LambdaFunctions {
5979 crate::lambda::functions::page_up_normal(self);
5980 } else if self.current_service == Service::LambdaApplications {
5981 crate::lambda::applications::page_up_normal(self);
5982 } else if self.current_service == Service::CloudFormationStacks {
5983 if self.cfn_state.current_stack.is_some()
5984 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5985 {
5986 self.cfn_state.parameters.page_up();
5987 } else if self.cfn_state.current_stack.is_some()
5988 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5989 {
5990 self.cfn_state.outputs.page_up();
5991 } else {
5992 self.cfn_state.table.page_up();
5993 }
5994 } else if self.current_service == Service::IamUsers {
5995 self.iam_state.users.page_up();
5996 } else if self.current_service == Service::IamRoles {
5997 if self.iam_state.current_role.is_some() {
5998 self.iam_state.policies.page_up();
5999 } else {
6000 self.iam_state.roles.page_up();
6001 }
6002 }
6003 }
6004 }
6005
6006 fn next_pane(&mut self) {
6007 if self.current_service == Service::S3Buckets {
6008 if self.s3_state.current_bucket.is_some() {
6009 let mut visual_idx = 0;
6012 let mut found_obj: Option<S3Object> = None;
6013
6014 fn check_nested(
6016 obj: &S3Object,
6017 visual_idx: &mut usize,
6018 target_idx: usize,
6019 expanded_prefixes: &std::collections::HashSet<String>,
6020 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6021 found_obj: &mut Option<S3Object>,
6022 ) {
6023 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6024 if let Some(preview) = prefix_preview.get(&obj.key) {
6025 for nested_obj in preview {
6026 if *visual_idx == target_idx {
6027 *found_obj = Some(nested_obj.clone());
6028 return;
6029 }
6030 *visual_idx += 1;
6031
6032 check_nested(
6034 nested_obj,
6035 visual_idx,
6036 target_idx,
6037 expanded_prefixes,
6038 prefix_preview,
6039 found_obj,
6040 );
6041 if found_obj.is_some() {
6042 return;
6043 }
6044 }
6045 } else {
6046 *visual_idx += 1;
6048 }
6049 }
6050 }
6051
6052 for obj in &self.s3_state.objects {
6053 if visual_idx == self.s3_state.selected_object {
6054 found_obj = Some(obj.clone());
6055 break;
6056 }
6057 visual_idx += 1;
6058
6059 check_nested(
6061 obj,
6062 &mut visual_idx,
6063 self.s3_state.selected_object,
6064 &self.s3_state.expanded_prefixes,
6065 &self.s3_state.prefix_preview,
6066 &mut found_obj,
6067 );
6068 if found_obj.is_some() {
6069 break;
6070 }
6071 }
6072
6073 if let Some(obj) = found_obj {
6074 if obj.is_prefix {
6075 if !self.s3_state.expanded_prefixes.contains(&obj.key) {
6076 self.s3_state.expanded_prefixes.insert(obj.key.clone());
6077 if !self.s3_state.prefix_preview.contains_key(&obj.key) {
6079 self.s3_state.buckets.loading = true;
6080 }
6081 }
6082 if self.s3_state.expanded_prefixes.contains(&obj.key) {
6084 if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
6085 if !preview.is_empty() {
6086 self.s3_state.selected_object += 1;
6087 }
6088 }
6089 }
6090 }
6091 }
6092 } else {
6093 let mut row_idx = 0;
6095 let mut found = false;
6096 for bucket in &self.s3_state.buckets.items {
6097 if row_idx == self.s3_state.selected_row {
6098 if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
6100 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
6101 if !self.s3_state.bucket_preview.contains_key(&bucket.name)
6102 && !self.s3_state.bucket_errors.contains_key(&bucket.name)
6103 {
6104 self.s3_state.buckets.loading = true;
6105 }
6106 }
6107 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6109 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6110 if !preview.is_empty() {
6111 self.s3_state.selected_row = row_idx + 1;
6112 let visible = self.s3_state.bucket_visible_rows.get();
6114 if self.s3_state.selected_row
6115 >= self.s3_state.bucket_scroll_offset + visible
6116 {
6117 self.s3_state.bucket_scroll_offset =
6118 self.s3_state.selected_row.saturating_sub(visible - 1);
6119 }
6120 }
6121 }
6122 }
6123 break;
6124 }
6125 row_idx += 1;
6126
6127 if self.s3_state.bucket_errors.contains_key(&bucket.name)
6129 && self.s3_state.expanded_prefixes.contains(&bucket.name)
6130 {
6131 if let Some(err) = self.s3_state.bucket_errors.get(&bucket.name) {
6132 let max_width = 120;
6133 let error_rows = if err.len() > max_width {
6134 err.len().div_ceil(max_width)
6135 } else {
6136 1
6137 };
6138 row_idx += error_rows;
6139 }
6140 continue;
6141 }
6142
6143 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6144 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6145 #[allow(clippy::too_many_arguments)]
6147 fn check_nested_expansion(
6148 objects: &[S3Object],
6149 row_idx: &mut usize,
6150 target_row: usize,
6151 expanded_prefixes: &mut std::collections::HashSet<String>,
6152 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6153 found: &mut bool,
6154 loading: &mut bool,
6155 selected_row: &mut usize,
6156 ) {
6157 for obj in objects {
6158 if *row_idx == target_row {
6159 if obj.is_prefix {
6161 if !expanded_prefixes.contains(&obj.key) {
6162 expanded_prefixes.insert(obj.key.clone());
6163 if !prefix_preview.contains_key(&obj.key) {
6164 *loading = true;
6165 }
6166 }
6167 if expanded_prefixes.contains(&obj.key) {
6169 if let Some(preview) = prefix_preview.get(&obj.key)
6170 {
6171 if !preview.is_empty() {
6172 *selected_row = *row_idx + 1;
6173 }
6174 }
6175 }
6176 }
6177 *found = true;
6178 return;
6179 }
6180 *row_idx += 1;
6181
6182 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6184 if let Some(nested) = prefix_preview.get(&obj.key) {
6185 check_nested_expansion(
6186 nested,
6187 row_idx,
6188 target_row,
6189 expanded_prefixes,
6190 prefix_preview,
6191 found,
6192 loading,
6193 selected_row,
6194 );
6195 if *found {
6196 return;
6197 }
6198 } else {
6199 *row_idx += 1; }
6201 }
6202 }
6203 }
6204
6205 check_nested_expansion(
6206 preview,
6207 &mut row_idx,
6208 self.s3_state.selected_row,
6209 &mut self.s3_state.expanded_prefixes,
6210 &self.s3_state.prefix_preview,
6211 &mut found,
6212 &mut self.s3_state.buckets.loading,
6213 &mut self.s3_state.selected_row,
6214 );
6215 if found || row_idx > self.s3_state.selected_row {
6216 break;
6217 }
6218 } else {
6219 if row_idx > self.s3_state.selected_row {
6222 break;
6223 }
6224 }
6225 }
6226 if found {
6227 break;
6228 }
6229 }
6230 }
6231 } else if self.view_mode == ViewMode::InsightsResults {
6232 let max_cols = self
6234 .insights_state
6235 .insights
6236 .query_results
6237 .first()
6238 .map(|r| r.len())
6239 .unwrap_or(0);
6240 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
6241 self.insights_state.insights.results_horizontal_scroll += 1;
6242 }
6243 } else if self.current_service == Service::CloudWatchLogGroups
6244 && self.view_mode == ViewMode::List
6245 {
6246 if self.log_groups_state.log_groups.expanded_item
6248 != Some(self.log_groups_state.log_groups.selected)
6249 {
6250 self.log_groups_state.log_groups.expanded_item =
6251 Some(self.log_groups_state.log_groups.selected);
6252 }
6253 } else if self.current_service == Service::CloudWatchLogGroups
6254 && self.view_mode == ViewMode::Detail
6255 {
6256 if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
6258 {
6259 self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
6260 }
6261 } else if self.view_mode == ViewMode::Events {
6262 if self.log_groups_state.expanded_event
6265 != Some(self.log_groups_state.event_scroll_offset)
6266 {
6267 self.log_groups_state.expanded_event =
6268 Some(self.log_groups_state.event_scroll_offset);
6269 }
6270 } else if self.current_service == Service::CloudWatchAlarms {
6271 if !self.alarms_state.table.is_expanded() {
6273 self.alarms_state.table.toggle_expand();
6274 }
6275 } else if self.current_service == Service::Ec2Instances {
6276 if self.ec2_state.current_instance.is_some()
6277 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
6278 {
6279 self.ec2_state.tags.toggle_expand();
6280 } else if !self.ec2_state.table.is_expanded() {
6281 self.ec2_state.table.toggle_expand();
6282 }
6283 } else if self.current_service == Service::EcrRepositories {
6284 crate::ecr::actions::next_pane(self);
6285 } else if self.current_service == Service::SqsQueues {
6286 if self.sqs_state.current_queue.is_some()
6287 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6288 {
6289 self.sqs_state.triggers.toggle_expand();
6290 } else if self.sqs_state.current_queue.is_some()
6291 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6292 {
6293 self.sqs_state.pipes.toggle_expand();
6294 } else if self.sqs_state.current_queue.is_some()
6295 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6296 {
6297 self.sqs_state.tags.toggle_expand();
6298 } else if self.sqs_state.current_queue.is_some()
6299 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6300 {
6301 self.sqs_state.subscriptions.toggle_expand();
6302 } else {
6303 self.sqs_state.queues.expand();
6304 }
6305 } else if self.current_service == Service::LambdaFunctions {
6306 crate::lambda::functions::expand_row(self);
6307 } else if self.current_service == Service::LambdaApplications {
6308 crate::lambda::applications::expand_row(self);
6309 } else if self.current_service == Service::CloudFormationStacks
6310 && self.cfn_state.current_stack.is_none()
6311 {
6312 self.cfn_state.table.toggle_expand();
6313 } else if self.current_service == Service::CloudFormationStacks
6314 && self.cfn_state.current_stack.is_some()
6315 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6316 {
6317 self.cfn_state.parameters.toggle_expand();
6318 } else if self.current_service == Service::CloudFormationStacks
6319 && self.cfn_state.current_stack.is_some()
6320 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6321 {
6322 self.cfn_state.outputs.toggle_expand();
6323 } else if self.current_service == Service::CloudFormationStacks
6324 && self.cfn_state.current_stack.is_some()
6325 && self.cfn_state.detail_tab == CfnDetailTab::Resources
6326 {
6327 self.cfn_state.resources.toggle_expand();
6328 } else if self.current_service == Service::IamUsers {
6329 if self.iam_state.current_user.is_some() {
6330 if self.iam_state.user_tab == UserTab::Tags {
6331 if self.iam_state.user_tags.expanded_item
6332 != Some(self.iam_state.user_tags.selected)
6333 {
6334 self.iam_state.user_tags.expanded_item =
6335 Some(self.iam_state.user_tags.selected);
6336 }
6337 } else if self.iam_state.policies.expanded_item
6338 != Some(self.iam_state.policies.selected)
6339 {
6340 self.iam_state.policies.toggle_expand();
6341 }
6342 } else if !self.iam_state.users.is_expanded() {
6343 self.iam_state.users.toggle_expand();
6344 }
6345 } else if self.current_service == Service::IamRoles {
6346 if self.iam_state.current_role.is_some() {
6347 if self.iam_state.role_tab == RoleTab::Tags {
6349 if !self.iam_state.tags.is_expanded() {
6350 self.iam_state.tags.expand();
6351 }
6352 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
6353 if !self.iam_state.last_accessed_services.is_expanded() {
6354 self.iam_state.last_accessed_services.expand();
6355 }
6356 } else if !self.iam_state.policies.is_expanded() {
6357 self.iam_state.policies.expand();
6358 }
6359 } else if !self.iam_state.roles.is_expanded() {
6360 self.iam_state.roles.expand();
6361 }
6362 } else if self.current_service == Service::IamUserGroups {
6363 if self.iam_state.current_group.is_some() {
6364 if self.iam_state.group_tab == GroupTab::Users {
6365 if !self.iam_state.group_users.is_expanded() {
6366 self.iam_state.group_users.expand();
6367 }
6368 } else if self.iam_state.group_tab == GroupTab::Permissions {
6369 if !self.iam_state.policies.is_expanded() {
6370 self.iam_state.policies.expand();
6371 }
6372 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6373 && !self.iam_state.last_accessed_services.is_expanded()
6374 {
6375 self.iam_state.last_accessed_services.expand();
6376 }
6377 } else if !self.iam_state.groups.is_expanded() {
6378 self.iam_state.groups.expand();
6379 }
6380 }
6381 }
6382
6383 fn go_to_page(&mut self, page: usize) {
6384 if page == 0 {
6385 return;
6386 }
6387
6388 match self.current_service {
6389 Service::CloudWatchAlarms => {
6390 let alarm_page_size = self.alarms_state.table.page_size.value();
6391 let target = (page - 1) * alarm_page_size;
6392 let filtered_count = match self.alarms_state.alarm_tab {
6393 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
6394 AlarmTab::InAlarm => self
6395 .alarms_state
6396 .table
6397 .items
6398 .iter()
6399 .filter(|a| a.state.to_uppercase() == "ALARM")
6400 .count(),
6401 };
6402 let max_offset = filtered_count.saturating_sub(alarm_page_size);
6403 self.alarms_state.table.scroll_offset = target.min(max_offset);
6404 self.alarms_state.table.selected = self
6405 .alarms_state
6406 .table
6407 .scroll_offset
6408 .min(filtered_count.saturating_sub(1));
6409 }
6410 Service::CloudTrailEvents => {
6411 let page_size = self.cloudtrail_state.table.page_size.value();
6412 let filtered_count = self.cloudtrail_state.table.items.len();
6413 let max_page = (filtered_count / page_size) + 1; if page <= max_page {
6417 let target = (page - 1) * page_size;
6418 self.cloudtrail_state.table.scroll_offset = target;
6419 self.cloudtrail_state.table.selected = target;
6420 self.cloudtrail_state.table.expanded_item = None; }
6422 }
6424 Service::CloudWatchLogGroups => match self.view_mode {
6425 ViewMode::Events => {
6426 let page_size = 20;
6427 let target = (page - 1) * page_size;
6428 let max = self.log_groups_state.log_events.len().saturating_sub(1);
6429 self.log_groups_state.event_scroll_offset = target.min(max);
6430 }
6431 ViewMode::Detail => {
6432 let page_size = self.log_groups_state.stream_page_size;
6433 self.log_groups_state.stream_current_page = (page - 1).min(
6434 self.log_groups_state
6435 .log_streams
6436 .len()
6437 .div_ceil(page_size)
6438 .saturating_sub(1),
6439 );
6440 self.log_groups_state.selected_stream = 0;
6441 }
6442 ViewMode::List => {
6443 let total = self.log_groups_state.log_groups.items.len();
6444 self.log_groups_state.log_groups.goto_page(page, total);
6445 }
6446 _ => {}
6447 },
6448 Service::EcrRepositories => {
6449 crate::ecr::actions::go_to_page(self, page);
6450 }
6451 Service::SqsQueues => {
6452 let filtered_count =
6453 filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter)
6454 .len();
6455 self.sqs_state.queues.goto_page(page, filtered_count);
6456 }
6457 Service::S3Buckets => {
6458 if self.s3_state.current_bucket.is_some() {
6459 let page_size = 50; let target = (page - 1) * page_size;
6461 let total_rows = self.calculate_total_object_rows();
6462 let max = total_rows.saturating_sub(1);
6463 self.s3_state.selected_object = target.min(max);
6464 } else {
6465 let page_size = self.s3_state.buckets.page_size.value();
6466 let target = (page - 1) * page_size;
6467 let total_rows = self.calculate_total_bucket_rows();
6468 let max = total_rows.saturating_sub(1);
6469 self.s3_state.selected_row = target.min(max);
6470 self.s3_state.bucket_scroll_offset =
6472 target.min(total_rows.saturating_sub(page_size));
6473 }
6474 }
6475 Service::LambdaFunctions => {
6476 crate::lambda::functions::go_to_page(self, page);
6477 }
6478 Service::LambdaApplications => {
6479 crate::lambda::applications::go_to_page(self, page);
6480 }
6481 Service::CloudFormationStacks => {
6482 let filtered_count = filtered_cloudformation_stacks(self).len();
6483 self.cfn_state.table.goto_page(page, filtered_count);
6484 }
6485 Service::IamUsers => {
6486 let filtered_count = filtered_iam_users(self).len();
6487 self.iam_state.users.goto_page(page, filtered_count);
6488 }
6489 Service::IamRoles => {
6490 let filtered_count = filtered_iam_roles(self).len();
6491 self.iam_state.roles.goto_page(page, filtered_count);
6492 }
6493 _ => {}
6494 }
6495 }
6496
6497 fn prev_pane(&mut self) {
6498 if self.current_service == Service::S3Buckets {
6499 if self.s3_state.current_bucket.is_some() {
6500 let mut visual_idx = 0;
6503 let mut found_obj: Option<S3Object> = None;
6504 let mut parent_idx: Option<usize> = None;
6505
6506 #[allow(clippy::too_many_arguments)]
6508 fn find_with_parent(
6509 objects: &[S3Object],
6510 visual_idx: &mut usize,
6511 target_idx: usize,
6512 expanded_prefixes: &std::collections::HashSet<String>,
6513 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6514 found_obj: &mut Option<S3Object>,
6515 parent_idx: &mut Option<usize>,
6516 current_parent: Option<usize>,
6517 ) {
6518 for obj in objects {
6519 if *visual_idx == target_idx {
6520 *found_obj = Some(obj.clone());
6521 *parent_idx = current_parent;
6522 return;
6523 }
6524 let obj_idx = *visual_idx;
6525 *visual_idx += 1;
6526
6527 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6529 if let Some(preview) = prefix_preview.get(&obj.key) {
6530 find_with_parent(
6531 preview,
6532 visual_idx,
6533 target_idx,
6534 expanded_prefixes,
6535 prefix_preview,
6536 found_obj,
6537 parent_idx,
6538 Some(obj_idx),
6539 );
6540 if found_obj.is_some() {
6541 return;
6542 }
6543 }
6544 }
6545 }
6546 }
6547
6548 find_with_parent(
6549 &self.s3_state.objects,
6550 &mut visual_idx,
6551 self.s3_state.selected_object,
6552 &self.s3_state.expanded_prefixes,
6553 &self.s3_state.prefix_preview,
6554 &mut found_obj,
6555 &mut parent_idx,
6556 None,
6557 );
6558
6559 if let Some(obj) = found_obj {
6560 if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
6561 self.s3_state.expanded_prefixes.remove(&obj.key);
6563 if let Some(parent) = parent_idx {
6564 self.s3_state.selected_object = parent;
6565 }
6566 } else if let Some(parent) = parent_idx {
6567 self.s3_state.selected_object = parent;
6569 }
6570 }
6571
6572 let visible_rows = self.s3_state.object_visible_rows.get();
6574 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
6575 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
6576 } else if self.s3_state.selected_object
6577 >= self.s3_state.object_scroll_offset + visible_rows
6578 {
6579 self.s3_state.object_scroll_offset = self
6580 .s3_state
6581 .selected_object
6582 .saturating_sub(visible_rows - 1);
6583 }
6584 } else {
6585 let mut row_idx = 0;
6587 for bucket in &self.s3_state.buckets.items {
6588 if row_idx == self.s3_state.selected_row {
6589 self.s3_state.expanded_prefixes.remove(&bucket.name);
6591 break;
6592 }
6593 row_idx += 1;
6594 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6595 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6596 #[allow(clippy::too_many_arguments)]
6598 fn check_nested_collapse(
6599 objects: &[S3Object],
6600 row_idx: &mut usize,
6601 target_row: usize,
6602 expanded_prefixes: &mut std::collections::HashSet<String>,
6603 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6604 found: &mut bool,
6605 selected_row: &mut usize,
6606 parent_row: usize,
6607 ) {
6608 for obj in objects {
6609 let current_row = *row_idx;
6610 if *row_idx == target_row {
6611 if obj.is_prefix {
6613 if expanded_prefixes.contains(&obj.key) {
6614 expanded_prefixes.remove(&obj.key);
6616 *selected_row = parent_row;
6617 } else {
6618 *selected_row = parent_row;
6620 }
6621 } else {
6622 *selected_row = parent_row;
6624 }
6625 *found = true;
6626 return;
6627 }
6628 *row_idx += 1;
6629
6630 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6632 if let Some(nested) = prefix_preview.get(&obj.key) {
6633 check_nested_collapse(
6634 nested,
6635 row_idx,
6636 target_row,
6637 expanded_prefixes,
6638 prefix_preview,
6639 found,
6640 selected_row,
6641 current_row,
6642 );
6643 if *found {
6644 return;
6645 }
6646 } else {
6647 *row_idx += 1; }
6649 }
6650 }
6651 }
6652
6653 let mut found = false;
6654 let parent_row = row_idx - 1; check_nested_collapse(
6656 preview,
6657 &mut row_idx,
6658 self.s3_state.selected_row,
6659 &mut self.s3_state.expanded_prefixes,
6660 &self.s3_state.prefix_preview,
6661 &mut found,
6662 &mut self.s3_state.selected_row,
6663 parent_row,
6664 );
6665 if found {
6666 let visible_rows = self.s3_state.bucket_visible_rows.get();
6668 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6669 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6670 } else if self.s3_state.selected_row
6671 >= self.s3_state.bucket_scroll_offset + visible_rows
6672 {
6673 self.s3_state.bucket_scroll_offset =
6674 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6675 }
6676 return;
6677 }
6678 } else {
6679 row_idx += 1;
6680 }
6681 }
6682 }
6683
6684 let visible_rows = self.s3_state.bucket_visible_rows.get();
6686 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6687 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6688 } else if self.s3_state.selected_row
6689 >= self.s3_state.bucket_scroll_offset + visible_rows
6690 {
6691 self.s3_state.bucket_scroll_offset =
6692 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6693 }
6694 }
6695 } else if self.view_mode == ViewMode::InsightsResults {
6696 self.insights_state.insights.results_horizontal_scroll = self
6698 .insights_state
6699 .insights
6700 .results_horizontal_scroll
6701 .saturating_sub(1);
6702 } else if self.current_service == Service::CloudWatchLogGroups
6703 && self.view_mode == ViewMode::List
6704 {
6705 if self.log_groups_state.log_groups.has_expanded_item() {
6707 self.log_groups_state.log_groups.collapse();
6708 }
6709 } else if self.current_service == Service::CloudWatchLogGroups
6710 && self.view_mode == ViewMode::Detail
6711 {
6712 if self.log_groups_state.expanded_stream.is_some() {
6714 self.log_groups_state.expanded_stream = None;
6715 }
6716 } else if self.view_mode == ViewMode::Events {
6717 if self.log_groups_state.expanded_event.is_some() {
6719 self.log_groups_state.expanded_event = None;
6720 }
6721 } else if self.current_service == Service::CloudWatchAlarms {
6722 self.alarms_state.table.collapse();
6724 } else if self.current_service == Service::Ec2Instances {
6725 self.ec2_state.table.collapse();
6726 } else if self.current_service == Service::ApiGatewayApis {
6727 self.apig_state.apis.collapse();
6728 } else if self.current_service == Service::EcrRepositories {
6729 crate::ecr::actions::prev_pane(self);
6730 } else if self.current_service == Service::SqsQueues {
6731 if self.sqs_state.current_queue.is_some()
6732 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6733 {
6734 self.sqs_state.triggers.collapse();
6735 } else if self.sqs_state.current_queue.is_some()
6736 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6737 {
6738 self.sqs_state.pipes.collapse();
6739 } else if self.sqs_state.current_queue.is_some()
6740 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6741 {
6742 self.sqs_state.tags.collapse();
6743 } else if self.sqs_state.current_queue.is_some()
6744 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6745 {
6746 self.sqs_state.subscriptions.collapse();
6747 } else {
6748 self.sqs_state.queues.collapse();
6749 }
6750 } else if self.current_service == Service::LambdaFunctions {
6751 crate::lambda::functions::prev_pane(self);
6752 } else if self.current_service == Service::LambdaApplications {
6753 crate::lambda::applications::prev_pane(self);
6754 } else if self.current_service == Service::CloudFormationStacks
6755 && self.cfn_state.current_stack.is_none()
6756 {
6757 self.cfn_state.table.collapse();
6758 } else if self.current_service == Service::CloudFormationStacks
6759 && self.cfn_state.current_stack.is_some()
6760 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6761 {
6762 self.cfn_state.parameters.collapse();
6763 } else if self.current_service == Service::CloudFormationStacks
6764 && self.cfn_state.current_stack.is_some()
6765 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6766 {
6767 self.cfn_state.outputs.collapse();
6768 } else if self.current_service == Service::CloudFormationStacks
6769 && self.cfn_state.current_stack.is_some()
6770 && self.cfn_state.detail_tab == CfnDetailTab::Resources
6771 {
6772 self.cfn_state.resources.collapse();
6773 } else if self.current_service == Service::IamUsers {
6774 if self.iam_state.users.has_expanded_item() {
6775 self.iam_state.users.collapse();
6776 }
6777 } else if self.current_service == Service::IamRoles {
6778 if self.view_mode == ViewMode::PolicyView {
6779 self.view_mode = ViewMode::Detail;
6781 self.iam_state.current_policy = None;
6782 self.iam_state.policy_document.clear();
6783 self.iam_state.policy_scroll = 0;
6784 } else if self.iam_state.current_role.is_some() {
6785 if self.iam_state.role_tab == RoleTab::Tags
6786 && self.iam_state.tags.has_expanded_item()
6787 {
6788 self.iam_state.tags.collapse();
6789 } else if self.iam_state.role_tab == RoleTab::LastAccessed
6790 && self
6791 .iam_state
6792 .last_accessed_services
6793 .expanded_item
6794 .is_some()
6795 {
6796 self.iam_state.last_accessed_services.collapse();
6797 } else if self.iam_state.policies.has_expanded_item() {
6798 self.iam_state.policies.collapse();
6799 }
6800 } else if self.iam_state.roles.has_expanded_item() {
6801 self.iam_state.roles.collapse();
6802 }
6803 } else if self.current_service == Service::IamUserGroups {
6804 if self.iam_state.current_group.is_some() {
6805 if self.iam_state.group_tab == GroupTab::Users
6806 && self.iam_state.group_users.has_expanded_item()
6807 {
6808 self.iam_state.group_users.collapse();
6809 } else if self.iam_state.group_tab == GroupTab::Permissions
6810 && self.iam_state.policies.has_expanded_item()
6811 {
6812 self.iam_state.policies.collapse();
6813 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6814 && self
6815 .iam_state
6816 .last_accessed_services
6817 .expanded_item
6818 .is_some()
6819 {
6820 self.iam_state.last_accessed_services.collapse();
6821 }
6822 } else if self.iam_state.groups.has_expanded_item() {
6823 self.iam_state.groups.collapse();
6824 }
6825 }
6826 }
6827
6828 fn collapse_row(&mut self) {
6829 match self.current_service {
6830 Service::S3Buckets => {
6831 if self.s3_state.current_bucket.is_none() {
6832 let filtered_buckets: Vec<_> = self
6834 .s3_state
6835 .buckets
6836 .items
6837 .iter()
6838 .filter(|b| {
6839 if self.s3_state.buckets.filter.is_empty() {
6840 true
6841 } else {
6842 b.name
6843 .to_lowercase()
6844 .contains(&self.s3_state.buckets.filter.to_lowercase())
6845 }
6846 })
6847 .collect();
6848
6849 let mut row_idx = 0;
6851
6852 for bucket in filtered_buckets {
6853 if row_idx == self.s3_state.selected_row {
6854 self.s3_state.expanded_prefixes.remove(&bucket.name);
6856 break;
6858 }
6859 row_idx += 1;
6860 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6861 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
6863 continue;
6865 }
6866 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6867 #[allow(clippy::too_many_arguments)]
6869 fn check_nested_collapse(
6870 objects: &[S3Object],
6871 row_idx: &mut usize,
6872 target_row: usize,
6873 expanded_prefixes: &mut std::collections::HashSet<String>,
6874 prefix_preview: &std::collections::HashMap<
6875 String,
6876 Vec<S3Object>,
6877 >,
6878 found: &mut bool,
6879 selected_row: &mut usize,
6880 parent_row: usize,
6881 ) {
6882 for obj in objects {
6883 let current_row = *row_idx;
6884 if *row_idx == target_row {
6885 if obj.is_prefix {
6887 if expanded_prefixes.contains(&obj.key) {
6888 expanded_prefixes.remove(&obj.key);
6890 *selected_row = parent_row;
6891 } else {
6892 *selected_row = parent_row;
6894 }
6895 } else {
6896 *selected_row = parent_row;
6898 }
6899 *found = true;
6900 return;
6901 }
6902 *row_idx += 1;
6903
6904 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6906 if let Some(nested) = prefix_preview.get(&obj.key) {
6907 check_nested_collapse(
6908 nested,
6909 row_idx,
6910 target_row,
6911 expanded_prefixes,
6912 prefix_preview,
6913 found,
6914 selected_row,
6915 current_row,
6916 );
6917 if *found {
6918 return;
6919 }
6920 } else {
6921 *row_idx += 1; }
6923 }
6924 }
6925 }
6926
6927 let mut found = false;
6928 let parent_row = row_idx - 1; check_nested_collapse(
6930 preview,
6931 &mut row_idx,
6932 self.s3_state.selected_row,
6933 &mut self.s3_state.expanded_prefixes,
6934 &self.s3_state.prefix_preview,
6935 &mut found,
6936 &mut self.s3_state.selected_row,
6937 parent_row,
6938 );
6939 if found {
6940 let visible_rows = self.s3_state.bucket_visible_rows.get();
6942 if self.s3_state.selected_row
6943 < self.s3_state.bucket_scroll_offset
6944 {
6945 self.s3_state.bucket_scroll_offset =
6946 self.s3_state.selected_row;
6947 } else if self.s3_state.selected_row
6948 >= self.s3_state.bucket_scroll_offset + visible_rows
6949 {
6950 self.s3_state.bucket_scroll_offset = self
6951 .s3_state
6952 .selected_row
6953 .saturating_sub(visible_rows - 1);
6954 }
6955 return;
6956 }
6957 } else {
6958 row_idx += 1;
6959 }
6960 }
6961 }
6962
6963 let visible_rows = self.s3_state.bucket_visible_rows.get();
6965 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6966 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6967 } else if self.s3_state.selected_row
6968 >= self.s3_state.bucket_scroll_offset + visible_rows
6969 {
6970 self.s3_state.bucket_scroll_offset =
6971 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6972 }
6973 }
6974 }
6975 Service::CloudWatchLogGroups => {
6976 if self.view_mode == ViewMode::Events {
6977 if let Some(idx) = self.log_groups_state.expanded_event {
6978 self.log_groups_state.expanded_event = None;
6979 self.log_groups_state.selected_event = idx;
6980 }
6981 } else if self.view_mode == ViewMode::Detail {
6982 if let Some(idx) = self.log_groups_state.expanded_stream {
6983 self.log_groups_state.expanded_stream = None;
6984 self.log_groups_state.selected_stream = idx;
6985 }
6986 } else {
6987 self.log_groups_state.log_groups.collapse();
6988 }
6989 }
6990 Service::CloudWatchAlarms => self.alarms_state.table.collapse(),
6991 Service::Ec2Instances => {
6992 if self.ec2_state.current_instance.is_some()
6993 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
6994 {
6995 self.ec2_state.tags.collapse();
6996 } else {
6997 self.ec2_state.table.collapse();
6998 }
6999 }
7000 Service::EcrRepositories => {
7001 crate::ecr::actions::collapse_row(self);
7002 }
7003 Service::LambdaFunctions => crate::lambda::functions::collapse_row(self),
7004 Service::LambdaApplications => crate::lambda::applications::prev_pane(self),
7005 Service::SqsQueues => self.sqs_state.queues.collapse(),
7006 Service::CloudFormationStacks => {
7007 if self.cfn_state.current_stack.is_some() {
7008 match self.cfn_state.detail_tab {
7009 crate::ui::cfn::DetailTab::Resources => {
7010 self.cfn_state.resources.collapse();
7011 }
7012 crate::ui::cfn::DetailTab::Parameters => {
7013 self.cfn_state.parameters.collapse();
7014 }
7015 crate::ui::cfn::DetailTab::Outputs => {
7016 self.cfn_state.outputs.collapse();
7017 }
7018 _ => {}
7019 }
7020 } else {
7021 self.cfn_state.table.collapse();
7022 }
7023 }
7024 Service::IamUsers => {
7025 if self.iam_state.current_user.is_some() {
7026 match self.iam_state.user_tab {
7027 crate::ui::iam::UserTab::Permissions => {
7028 self.iam_state.policies.collapse();
7029 }
7030 crate::ui::iam::UserTab::Groups => {
7031 self.iam_state.user_group_memberships.collapse();
7032 }
7033 crate::ui::iam::UserTab::Tags => {
7034 self.iam_state.user_tags.collapse();
7035 }
7036 _ => {}
7037 }
7038 } else {
7039 self.iam_state.users.collapse();
7040 }
7041 }
7042 Service::IamRoles => {
7043 if self.iam_state.current_role.is_some() {
7044 match self.iam_state.role_tab {
7045 crate::ui::iam::RoleTab::Permissions => {
7046 self.iam_state.policies.collapse();
7047 }
7048 crate::ui::iam::RoleTab::Tags => {
7049 self.iam_state.tags.collapse();
7050 }
7051 _ => {}
7052 }
7053 } else {
7054 self.iam_state.roles.collapse();
7055 }
7056 }
7057 Service::IamUserGroups => self.iam_state.groups.collapse(),
7058 Service::ApiGatewayApis => {
7059 if let Some(api) = &self.apig_state.current_api {
7060 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
7061 let protocol = api.protocol_type.to_uppercase();
7062 if protocol == "REST" {
7063 let (filtered_items, _filtered_children) =
7065 crate::ui::apig::filter_tree_items(
7066 &self.apig_state.resources.items,
7067 &self.apig_state.resource_children,
7068 &self.apig_state.route_filter,
7069 );
7070
7071 let selected_row = self.apig_state.resources.selected;
7072 let mut current_row = 0;
7073 if let Some(resource_id) = self.find_resource_at_row(
7074 &filtered_items,
7075 selected_row,
7076 &mut current_row,
7077 ) {
7078 if self.apig_state.expanded_resources.contains(&resource_id) {
7079 self.apig_state.expanded_resources.remove(&resource_id);
7080 } else if let Some(parent_row) =
7081 self.find_resource_parent_row(&filtered_items, &resource_id)
7082 {
7083 self.apig_state.resources.selected = parent_row;
7084 }
7085 }
7086 } else {
7087 let (filtered_items, _filtered_children) =
7089 crate::ui::apig::filter_tree_items(
7090 &self.apig_state.routes.items,
7091 &self.apig_state.route_children,
7092 &self.apig_state.route_filter,
7093 );
7094
7095 let selected_row = self.apig_state.routes.selected;
7096 let mut current_row = 0;
7097 if let Some(route_key) = self.find_route_at_row(
7098 &filtered_items,
7099 selected_row,
7100 &mut current_row,
7101 ) {
7102 if self.apig_state.expanded_routes.contains(&route_key) {
7103 self.apig_state.expanded_routes.remove(&route_key);
7104 } else if let Some(parent_row) =
7105 self.find_parent_row(&filtered_items, &route_key)
7106 {
7107 self.apig_state.routes.selected = parent_row;
7108 }
7109 }
7110 }
7111 }
7112 } else {
7113 self.apig_state.apis.collapse();
7114 }
7115 }
7116 Service::CloudTrailEvents => {
7117 if self.cloudtrail_state.current_event.is_some()
7118 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
7119 {
7120 self.cloudtrail_state.resources_expanded_index = None;
7122 } else {
7123 self.cloudtrail_state.table.collapse();
7124 }
7125 }
7126 _ => {}
7127 }
7128 }
7129
7130 fn expand_row(&mut self) {
7131 match self.current_service {
7132 Service::S3Buckets => {
7133 if self.s3_state.current_bucket.is_none() {
7134 let filtered_buckets: Vec<_> = self
7136 .s3_state
7137 .buckets
7138 .items
7139 .iter()
7140 .filter(|b| {
7141 if self.s3_state.buckets.filter.is_empty() {
7142 true
7143 } else {
7144 b.name
7145 .to_lowercase()
7146 .contains(&self.s3_state.buckets.filter.to_lowercase())
7147 }
7148 })
7149 .collect();
7150
7151 fn check_nested_expand(
7153 objects: &[S3Object],
7154 row_idx: &mut usize,
7155 target_row: usize,
7156 expanded_prefixes: &mut std::collections::HashSet<String>,
7157 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7158 ) -> Option<(bool, usize)> {
7159 for obj in objects {
7160 if *row_idx == target_row {
7161 if obj.is_prefix {
7162 if expanded_prefixes.contains(&obj.key) {
7164 expanded_prefixes.remove(&obj.key);
7165 return Some((true, *row_idx));
7166 } else {
7167 expanded_prefixes.insert(obj.key.clone());
7168 return Some((true, *row_idx + 1)); }
7170 }
7171 return Some((false, *row_idx));
7172 }
7173 *row_idx += 1;
7174
7175 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7177 if let Some(nested) = prefix_preview.get(&obj.key) {
7178 if let Some(result) = check_nested_expand(
7179 nested,
7180 row_idx,
7181 target_row,
7182 expanded_prefixes,
7183 prefix_preview,
7184 ) {
7185 return Some(result);
7186 }
7187 }
7188 }
7189 }
7190 None
7191 }
7192
7193 let mut row_idx = 0;
7194 for bucket in filtered_buckets {
7195 if row_idx == self.s3_state.selected_row {
7196 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7197 if let Some(preview) =
7199 self.s3_state.bucket_preview.get(&bucket.name)
7200 {
7201 if !preview.is_empty() {
7202 self.s3_state.selected_row = row_idx + 1;
7203 let visible = self.s3_state.bucket_visible_rows.get();
7204 if self.s3_state.selected_row
7205 >= self.s3_state.bucket_scroll_offset + visible
7206 {
7207 self.s3_state.bucket_scroll_offset = self
7208 .s3_state
7209 .selected_row
7210 .saturating_sub(visible - 1);
7211 }
7212 }
7213 } else {
7215 }
7217 } else {
7218 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
7219 self.s3_state.buckets.loading = true;
7220 if let Some(preview) =
7222 self.s3_state.bucket_preview.get(&bucket.name)
7223 {
7224 if !preview.is_empty() {
7225 self.s3_state.selected_row = row_idx + 1;
7226 let visible = self.s3_state.bucket_visible_rows.get();
7227 if self.s3_state.selected_row
7228 >= self.s3_state.bucket_scroll_offset + visible
7229 {
7230 self.s3_state.bucket_scroll_offset = self
7231 .s3_state
7232 .selected_row
7233 .saturating_sub(visible - 1);
7234 }
7235 }
7236 }
7237 }
7238 return;
7239 }
7240 row_idx += 1;
7241
7242 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7243 if self.s3_state.bucket_errors.contains_key(&bucket.name) {
7244 continue;
7245 }
7246 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7247 if let Some((loading, new_row)) = check_nested_expand(
7248 preview,
7249 &mut row_idx,
7250 self.s3_state.selected_row,
7251 &mut self.s3_state.expanded_prefixes,
7252 &self.s3_state.prefix_preview,
7253 ) {
7254 self.s3_state.selected_row = new_row;
7255 if loading {
7256 self.s3_state.buckets.loading = true;
7257 }
7258 let visible = self.s3_state.bucket_visible_rows.get();
7260 if self.s3_state.selected_row
7261 >= self.s3_state.bucket_scroll_offset + visible
7262 {
7263 self.s3_state.bucket_scroll_offset =
7264 self.s3_state.selected_row.saturating_sub(visible - 1);
7265 } else if self.s3_state.selected_row
7266 < self.s3_state.bucket_scroll_offset
7267 {
7268 self.s3_state.bucket_scroll_offset =
7269 self.s3_state.selected_row;
7270 }
7271 return;
7272 }
7273 }
7274 }
7275 }
7276 } else {
7277 fn check_object_expand(
7279 objects: &[S3Object],
7280 row_idx: &mut usize,
7281 target_row: usize,
7282 expanded_prefixes: &mut std::collections::HashSet<String>,
7283 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7284 ) -> Option<(bool, usize)> {
7285 for obj in objects {
7286 if *row_idx == target_row {
7287 if obj.is_prefix {
7288 if expanded_prefixes.contains(&obj.key) {
7289 expanded_prefixes.remove(&obj.key);
7290 return Some((true, *row_idx));
7291 } else {
7292 expanded_prefixes.insert(obj.key.clone());
7293 return Some((true, *row_idx + 1));
7294 }
7295 }
7296 return Some((false, *row_idx));
7297 }
7298 *row_idx += 1;
7299
7300 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7301 if let Some(nested) = prefix_preview.get(&obj.key) {
7302 if let Some(result) = check_object_expand(
7303 nested,
7304 row_idx,
7305 target_row,
7306 expanded_prefixes,
7307 prefix_preview,
7308 ) {
7309 return Some(result);
7310 }
7311 }
7312 }
7313 }
7314 None
7315 }
7316
7317 let mut row_idx = 0;
7318 if let Some((loading, new_row)) = check_object_expand(
7319 &self.s3_state.objects,
7320 &mut row_idx,
7321 self.s3_state.selected_object,
7322 &mut self.s3_state.expanded_prefixes,
7323 &self.s3_state.prefix_preview,
7324 ) {
7325 self.s3_state.selected_object = new_row;
7326 if loading {
7327 self.s3_state.buckets.loading = true;
7328 }
7329 }
7330 }
7331 }
7332 Service::ApiGatewayApis => {
7333 if let Some(api) = &self.apig_state.current_api {
7334 if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
7335 let protocol = api.protocol_type.to_uppercase();
7336 if protocol == "REST" {
7337 let (filtered_items, filtered_children) =
7339 crate::ui::apig::filter_tree_items(
7340 &self.apig_state.resources.items,
7341 &self.apig_state.resource_children,
7342 &self.apig_state.route_filter,
7343 );
7344
7345 let selected_row = self.apig_state.resources.selected;
7346 let mut current_row = 0;
7347 if let Some(resource_id) = self.find_resource_at_row(
7348 &filtered_items,
7349 selected_row,
7350 &mut current_row,
7351 ) {
7352 if self.apig_state.expanded_resources.contains(&resource_id) {
7353 let total_rows =
7354 crate::ui::tree::TreeRenderer::count_visible_rows(
7355 &filtered_items,
7356 &self.apig_state.expanded_resources,
7357 &filtered_children,
7358 );
7359 if selected_row + 1 < total_rows {
7360 self.apig_state.resources.selected = selected_row + 1;
7361 }
7362 } else {
7363 self.apig_state.expanded_resources.insert(resource_id);
7364 }
7365 }
7366 } else {
7367 let (filtered_items, filtered_children) =
7369 crate::ui::apig::filter_tree_items(
7370 &self.apig_state.routes.items,
7371 &self.apig_state.route_children,
7372 &self.apig_state.route_filter,
7373 );
7374
7375 let selected_row = self.apig_state.routes.selected;
7376 let mut current_row = 0;
7377 if let Some(route_key) = self.find_route_at_row(
7378 &filtered_items,
7379 selected_row,
7380 &mut current_row,
7381 ) {
7382 if self.apig_state.expanded_routes.contains(&route_key) {
7383 let total_rows =
7384 crate::ui::tree::TreeRenderer::count_visible_rows(
7385 &filtered_items,
7386 &self.apig_state.expanded_routes,
7387 &filtered_children,
7388 );
7389 if selected_row + 1 < total_rows {
7390 self.apig_state.routes.selected = selected_row + 1;
7391 }
7392 } else {
7393 self.apig_state.expanded_routes.insert(route_key);
7394 }
7395 }
7396 }
7397 }
7398 } else {
7399 self.apig_state.apis.expand();
7400 }
7401 }
7402 Service::CloudTrailEvents => {
7403 if self.cloudtrail_state.current_event.is_some()
7404 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
7405 {
7406 self.cloudtrail_state.resources_expanded_index = Some(0);
7408 } else {
7409 self.cloudtrail_state.table.expand();
7410 }
7411 }
7412 _ => {
7413 self.next_pane();
7415 }
7416 }
7417 }
7418
7419 fn find_route_at_row(
7420 &self,
7421 routes: &[Route],
7422 target_row: usize,
7423 current_row: &mut usize,
7424 ) -> Option<String> {
7425 for route in routes {
7426 if *current_row == target_row {
7427 return Some(route.route_key.clone());
7428 }
7429 *current_row += 1;
7430
7431 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
7433 if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
7434 if let Some(key) = self.find_route_at_row(children, target_row, current_row) {
7435 return Some(key);
7436 }
7437 }
7438 }
7439 }
7440 None
7441 }
7442
7443 fn find_route_id_at_row_with_children(
7444 &self,
7445 routes: &[Route],
7446 children_map: &HashMap<String, Vec<Route>>,
7447 target_row: usize,
7448 current_row: &mut usize,
7449 ) -> Option<String> {
7450 for route in routes {
7451 if *current_row == target_row {
7452 if !route.target.is_empty() {
7454 return Some(route.route_id.clone());
7455 } else {
7456 return None;
7457 }
7458 }
7459 *current_row += 1;
7460
7461 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
7463 if let Some(children) = children_map.get(&route.route_key) {
7464 if let Some(id) = self.find_route_id_at_row_with_children(
7465 children,
7466 children_map,
7467 target_row,
7468 current_row,
7469 ) {
7470 return Some(id);
7471 }
7472 }
7473 }
7474 }
7475 None
7476 }
7477
7478 fn find_parent_row(&self, routes: &[Route], child_key: &str) -> Option<usize> {
7479 let mut current_row = 0;
7480 self.find_parent_row_recursive(routes, child_key, &mut current_row)
7481 }
7482
7483 fn find_parent_row_recursive(
7484 &self,
7485 routes: &[Route],
7486 child_key: &str,
7487 current_row: &mut usize,
7488 ) -> Option<usize> {
7489 for route in routes {
7490 let parent_row = *current_row;
7491 *current_row += 1;
7492
7493 if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
7495 if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
7496 for child in children {
7498 if child.route_key == child_key {
7499 return Some(parent_row);
7500 }
7501 }
7502
7503 if let Some(row) =
7505 self.find_parent_row_recursive(children, child_key, current_row)
7506 {
7507 return Some(row);
7508 }
7509 }
7510 }
7511 }
7512 None
7513 }
7514
7515 fn find_resource_at_row(
7516 &self,
7517 resources: &[ApigResource],
7518 target_row: usize,
7519 current_row: &mut usize,
7520 ) -> Option<String> {
7521 for resource in resources {
7522 if *current_row == target_row {
7523 return Some(resource.id.clone());
7524 }
7525 *current_row += 1;
7526
7527 if self.apig_state.expanded_resources.contains(&resource.id) {
7528 if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
7529 if let Some(id) = self.find_resource_at_row(children, target_row, current_row) {
7530 return Some(id);
7531 }
7532 }
7533 }
7534 }
7535 None
7536 }
7537
7538 fn find_resource_parent_row(
7539 &self,
7540 resources: &[ApigResource],
7541 child_id: &str,
7542 ) -> Option<usize> {
7543 let mut current_row = 0;
7544 self.find_resource_parent_row_recursive(resources, child_id, &mut current_row)
7545 }
7546
7547 fn find_resource_parent_row_recursive(
7548 &self,
7549 resources: &[ApigResource],
7550 child_id: &str,
7551 current_row: &mut usize,
7552 ) -> Option<usize> {
7553 for resource in resources {
7554 let parent_row = *current_row;
7555 *current_row += 1;
7556
7557 if self.apig_state.expanded_resources.contains(&resource.id) {
7558 if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
7559 for child in children {
7560 if child.id == child_id {
7561 return Some(parent_row);
7562 }
7563 }
7564
7565 if let Some(row) =
7566 self.find_resource_parent_row_recursive(children, child_id, current_row)
7567 {
7568 return Some(row);
7569 }
7570 }
7571 }
7572 }
7573 None
7574 }
7575
7576 fn select_item(&mut self) {
7577 if self.mode == Mode::RegionPicker {
7578 let filtered = self.get_filtered_regions();
7579 if let Some(region) = filtered.get(self.region_picker_selected) {
7580 if !self.tabs.is_empty() {
7582 let mut session = Session::new(
7583 self.profile.clone(),
7584 self.region.clone(),
7585 self.config.account_id.clone(),
7586 self.config.role_arn.clone(),
7587 );
7588
7589 for tab in &self.tabs {
7590 session.tabs.push(SessionTab {
7591 service: format!("{:?}", tab.service),
7592 title: tab.title.clone(),
7593 breadcrumb: tab.breadcrumb.clone(),
7594 filter: None,
7595 selected_item: None,
7596 });
7597 }
7598
7599 let _ = session.save();
7600 }
7601
7602 self.region = region.code.to_string();
7603 self.config.region = region.code.to_string();
7604
7605 self.tabs.clear();
7607 self.current_tab = 0;
7608 self.service_selected = false;
7609
7610 self.mode = Mode::Normal;
7611 }
7612 } else if self.mode == Mode::ProfilePicker {
7613 let filtered = self.get_filtered_profiles();
7614 if let Some(profile) = filtered.get(self.profile_picker_selected) {
7615 let profile_name = profile.name.clone();
7616 let profile_region = profile.region.clone();
7617
7618 self.profile = profile_name.clone();
7619 std::env::set_var("AWS_PROFILE", &profile_name);
7620
7621 if let Some(region) = profile_region {
7623 self.region = region;
7624 }
7625
7626 self.mode = Mode::Normal;
7627 }
7629 } else if self.mode == Mode::ServicePicker {
7630 if self.service_picker.filter_active {
7632 self.service_picker.filter_active = false;
7633 return;
7634 }
7635
7636 let filtered = self.filtered_services();
7637 if let Some(&service) = filtered.get(self.service_picker.selected) {
7638 let new_service = match service {
7639 "API Gateway › APIs" => Service::ApiGatewayApis,
7640 "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
7641 "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
7642 "CloudWatch › Alarms" => Service::CloudWatchAlarms,
7643 "CloudTrail › Event History" => Service::CloudTrailEvents,
7644 "CloudFormation › Stacks" => Service::CloudFormationStacks,
7645 "EC2 › Instances" => Service::Ec2Instances,
7646 "ECR › Repositories" => Service::EcrRepositories,
7647 "IAM › Users" => Service::IamUsers,
7648 "IAM › Roles" => Service::IamRoles,
7649 "IAM › User Groups" => Service::IamUserGroups,
7650 "Lambda › Functions" => Service::LambdaFunctions,
7651 "Lambda › Applications" => Service::LambdaApplications,
7652 "S3 › Buckets" => Service::S3Buckets,
7653 "SQS › Queues" => Service::SqsQueues,
7654 _ => return,
7655 };
7656
7657 self.tabs.push(Tab {
7659 service: new_service,
7660 title: service.to_string(),
7661 breadcrumb: service.to_string(),
7662 });
7663 self.current_tab = self.tabs.len() - 1;
7664 self.current_service = new_service;
7665 self.view_mode = ViewMode::List;
7666 self.service_selected = true;
7667 self.mode = Mode::Normal;
7668 }
7669 } else if self.mode == Mode::TabPicker {
7670 let filtered = self.get_filtered_tabs();
7671 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
7672 self.current_tab = idx;
7673 self.current_service = self.tabs[idx].service;
7674 self.mode = Mode::Normal;
7675 self.tab_filter.clear();
7676 }
7677 } else if self.mode == Mode::SessionPicker {
7678 let filtered = self.get_filtered_sessions();
7679 if let Some(&session) = filtered.get(self.session_picker_selected) {
7680 let session = session.clone();
7681
7682 self.current_session = Some(session.clone());
7684 self.profile = session.profile.clone();
7685 self.region = session.region.clone();
7686 self.config.region = session.region.clone();
7687 self.config.account_id = session.account_id.clone();
7688 self.config.role_arn = session.role_arn.clone();
7689
7690 self.tabs = session
7692 .tabs
7693 .iter()
7694 .map(|st| Tab {
7695 service: match st.service.as_str() {
7696 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
7697 "CloudWatchInsights" => Service::CloudWatchInsights,
7698 "CloudWatchAlarms" => Service::CloudWatchAlarms,
7699 "S3Buckets" => Service::S3Buckets,
7700 "CloudTrailEvents" => Service::CloudTrailEvents,
7701 "SqsQueues" => Service::SqsQueues,
7702 _ => Service::CloudWatchLogGroups,
7703 },
7704 title: st.title.clone(),
7705 breadcrumb: st.breadcrumb.clone(),
7706 })
7707 .collect();
7708
7709 if !self.tabs.is_empty() {
7710 self.current_tab = 0;
7711 self.current_service = self.tabs[0].service;
7712 self.service_selected = true;
7713 }
7714
7715 self.mode = Mode::Normal;
7716 }
7717 } else if self.mode == Mode::InsightsInput {
7718 use crate::app::InsightsFocus;
7720 match self.insights_state.insights.insights_focus {
7721 InsightsFocus::Query => {
7722 self.insights_state.insights.query_text.push('\n');
7724 self.insights_state.insights.query_cursor_line += 1;
7725 self.insights_state.insights.query_cursor_col = 0;
7726 }
7727 InsightsFocus::LogGroupSearch => {
7728 self.insights_state.insights.show_dropdown =
7730 !self.insights_state.insights.show_dropdown;
7731 }
7732 _ => {}
7733 }
7734 } else if self.mode == Mode::Normal {
7735 if !self.service_selected {
7737 let filtered = self.filtered_services();
7738 if let Some(&service) = filtered.get(self.service_picker.selected) {
7739 match service {
7740 "CloudWatch › Log Groups" => {
7741 self.current_service = Service::CloudWatchLogGroups;
7742 self.view_mode = ViewMode::List;
7743 self.service_selected = true;
7744 }
7745 "CloudWatch › Logs Insights" => {
7746 self.current_service = Service::CloudWatchInsights;
7747 self.view_mode = ViewMode::InsightsResults;
7748 self.service_selected = true;
7749 }
7750 "CloudWatch › Alarms" => {
7751 self.current_service = Service::CloudWatchAlarms;
7752 self.view_mode = ViewMode::List;
7753 self.service_selected = true;
7754 }
7755 "S3 › Buckets" => {
7756 self.current_service = Service::S3Buckets;
7757 self.view_mode = ViewMode::List;
7758 self.service_selected = true;
7759 }
7760 "EC2 › Instances" => {
7761 self.current_service = Service::Ec2Instances;
7762 self.view_mode = ViewMode::List;
7763 self.service_selected = true;
7764 }
7765 "ECR › Repositories" => {
7766 self.current_service = Service::EcrRepositories;
7767 self.view_mode = ViewMode::List;
7768 self.service_selected = true;
7769 }
7770 "Lambda › Functions" => {
7771 self.current_service = Service::LambdaFunctions;
7772 self.view_mode = ViewMode::List;
7773 self.service_selected = true;
7774 }
7775 "Lambda › Applications" => {
7776 self.current_service = Service::LambdaApplications;
7777 self.view_mode = ViewMode::List;
7778 self.service_selected = true;
7779 }
7780 _ => {}
7781 }
7782 }
7783 return;
7784 }
7785
7786 if self.view_mode == ViewMode::InsightsResults {
7788 if self.insights_state.insights.expanded_result
7790 == Some(self.insights_state.insights.results_selected)
7791 {
7792 self.insights_state.insights.expanded_result = None;
7793 } else {
7794 self.insights_state.insights.expanded_result =
7795 Some(self.insights_state.insights.results_selected);
7796 }
7797 } else if self.current_service == Service::S3Buckets {
7798 if self.s3_state.current_bucket.is_none() {
7799 let filtered_buckets: Vec<_> = self
7801 .s3_state
7802 .buckets
7803 .items
7804 .iter()
7805 .filter(|b| {
7806 if self.s3_state.buckets.filter.is_empty() {
7807 true
7808 } else {
7809 b.name
7810 .to_lowercase()
7811 .contains(&self.s3_state.buckets.filter.to_lowercase())
7812 }
7813 })
7814 .collect();
7815
7816 let mut row_idx = 0;
7818 for bucket in filtered_buckets {
7819 if row_idx == self.s3_state.selected_row {
7820 self.s3_state.current_bucket = Some(bucket.name.clone());
7822 self.s3_state.prefix_stack.clear();
7823 self.s3_state.buckets.loading = true;
7824 return;
7825 }
7826 row_idx += 1;
7827
7828 if self.s3_state.bucket_errors.contains_key(&bucket.name)
7830 && self.s3_state.expanded_prefixes.contains(&bucket.name)
7831 {
7832 continue;
7833 }
7834
7835 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7836 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7837 for obj in preview {
7838 if row_idx == self.s3_state.selected_row {
7839 if obj.is_prefix {
7841 self.s3_state.current_bucket =
7842 Some(bucket.name.clone());
7843 self.s3_state.prefix_stack = vec![obj.key.clone()];
7844 self.s3_state.buckets.loading = true;
7845 }
7846 return;
7847 }
7848 row_idx += 1;
7849
7850 if obj.is_prefix
7852 && self.s3_state.expanded_prefixes.contains(&obj.key)
7853 {
7854 if let Some(nested) =
7855 self.s3_state.prefix_preview.get(&obj.key)
7856 {
7857 for nested_obj in nested {
7858 if row_idx == self.s3_state.selected_row {
7859 if nested_obj.is_prefix {
7861 self.s3_state.current_bucket =
7862 Some(bucket.name.clone());
7863 self.s3_state.prefix_stack = vec![
7865 obj.key.clone(),
7866 nested_obj.key.clone(),
7867 ];
7868 self.s3_state.buckets.loading = true;
7869 }
7870 return;
7871 }
7872 row_idx += 1;
7873 }
7874 } else {
7875 row_idx += 1;
7876 }
7877 }
7878 }
7879 } else {
7880 row_idx += 1;
7881 }
7882 }
7883 }
7884 } else {
7885 let mut visual_idx = 0;
7887 let mut found_obj: Option<S3Object> = None;
7888
7889 fn check_nested_select(
7891 obj: &S3Object,
7892 visual_idx: &mut usize,
7893 target_idx: usize,
7894 expanded_prefixes: &std::collections::HashSet<String>,
7895 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7896 found_obj: &mut Option<S3Object>,
7897 ) {
7898 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7899 if let Some(preview) = prefix_preview.get(&obj.key) {
7900 for nested_obj in preview {
7901 if *visual_idx == target_idx {
7902 *found_obj = Some(nested_obj.clone());
7903 return;
7904 }
7905 *visual_idx += 1;
7906
7907 check_nested_select(
7909 nested_obj,
7910 visual_idx,
7911 target_idx,
7912 expanded_prefixes,
7913 prefix_preview,
7914 found_obj,
7915 );
7916 if found_obj.is_some() {
7917 return;
7918 }
7919 }
7920 } else {
7921 *visual_idx += 1;
7923 }
7924 }
7925 }
7926
7927 for obj in &self.s3_state.objects {
7928 if visual_idx == self.s3_state.selected_object {
7929 found_obj = Some(obj.clone());
7930 break;
7931 }
7932 visual_idx += 1;
7933
7934 check_nested_select(
7936 obj,
7937 &mut visual_idx,
7938 self.s3_state.selected_object,
7939 &self.s3_state.expanded_prefixes,
7940 &self.s3_state.prefix_preview,
7941 &mut found_obj,
7942 );
7943 if found_obj.is_some() {
7944 break;
7945 }
7946 }
7947
7948 if let Some(obj) = found_obj {
7949 if obj.is_prefix {
7950 self.s3_state.prefix_stack.push(obj.key.clone());
7952 self.s3_state.buckets.loading = true;
7953 }
7954 }
7955 }
7956 } else if self.current_service == Service::ApiGatewayApis {
7957 if self.apig_state.current_api.is_none() {
7958 let filtered_apis = crate::ui::apig::filtered_apis(self);
7960 if let Some(api) = self.apig_state.apis.get_selected(&filtered_apis) {
7961 let protocol = api.protocol_type.to_uppercase();
7962 self.apig_state.current_api = Some((*api).clone());
7963 if protocol == "REST" {
7964 self.apig_state.resources.loading = true;
7965 } else {
7966 self.apig_state.routes.loading = true;
7967 }
7968 self.update_current_tab_breadcrumb();
7969 }
7970 }
7971 } else if self.current_service == Service::CloudFormationStacks {
7972 if self.cfn_state.current_stack.is_none() {
7973 let filtered_stacks = filtered_cloudformation_stacks(self);
7975 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
7976 let stack_name = stack.name.clone();
7977 let mut tags = stack.tags.clone();
7978 tags.sort_by(|a, b| a.0.cmp(&b.0));
7979
7980 self.cfn_state.current_stack = Some(stack_name);
7981 self.cfn_state.tags.items = tags;
7982 self.cfn_state.tags.reset();
7983 self.cfn_state.table.loading = true;
7984 self.update_current_tab_breadcrumb();
7985 }
7986 }
7987 } else if self.current_service == Service::CloudWatchAlarms {
7988 if self.alarms_state.current_alarm.is_none() {
7989 let filtered_alarms: Vec<_> = self.alarms_state.table.items.iter().collect();
7990 if let Some(alarm) = self.alarms_state.table.get_selected(&filtered_alarms) {
7991 self.alarms_state.current_alarm = Some(alarm.name.clone());
7992 self.alarms_state.metrics_loading = true;
7993 self.view_mode = ViewMode::Detail;
7994 self.update_current_tab_breadcrumb();
7995 }
7996 }
7997 } else if self.current_service == Service::CloudTrailEvents {
7998 if self.cloudtrail_state.current_event.is_none() {
7999 let filtered_events: Vec<_> =
8000 self.cloudtrail_state.table.items.iter().collect();
8001 if let Some(event) = self.cloudtrail_state.table.get_selected(&filtered_events)
8002 {
8003 self.cloudtrail_state.current_event = Some((*event).clone());
8004 self.cloudtrail_state.event_json_scroll = 0;
8005 self.update_current_tab_breadcrumb();
8006 }
8007 }
8008 } else if self.current_service == Service::EcrRepositories {
8009 crate::ecr::actions::select_item(self);
8010 } else if self.current_service == Service::Ec2Instances {
8011 if self.ec2_state.current_instance.is_none() {
8012 let filtered_instances = filtered_ec2_instances(self);
8013 if let Some(instance) = self.ec2_state.table.get_selected(&filtered_instances) {
8014 self.ec2_state.current_instance = Some(instance.instance_id.clone());
8015 self.view_mode = ViewMode::Detail;
8016 self.update_current_tab_breadcrumb();
8017 }
8018 }
8019 } else if self.current_service == Service::SqsQueues {
8020 if self.sqs_state.current_queue.is_none() {
8021 let filtered_queues = filtered_queues(
8022 &self.sqs_state.queues.items,
8023 &self.sqs_state.queues.filter,
8024 );
8025 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
8026 self.sqs_state.current_queue = Some(queue.url.clone());
8027
8028 if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
8029 self.sqs_state.metrics_loading = true;
8030 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
8031 self.sqs_state.triggers.loading = true;
8032 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
8033 self.sqs_state.pipes.loading = true;
8034 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
8035 self.sqs_state.tags.loading = true;
8036 } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
8037 self.sqs_state.subscriptions.loading = true;
8038 }
8039 }
8040 }
8041 } else if self.current_service == Service::IamUsers {
8042 if self.iam_state.current_user.is_some() {
8043 if self.iam_state.user_tab == UserTab::Permissions {
8045 let filtered = filtered_iam_policies(self);
8046 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
8047 self.iam_state.current_policy = Some(policy.policy_name.clone());
8048 self.iam_state.policy_scroll = 0;
8049 self.view_mode = ViewMode::PolicyView;
8050 self.iam_state.policies.loading = true;
8051 self.update_current_tab_breadcrumb();
8052 }
8053 }
8054 } else if self.iam_state.current_user.is_none() {
8055 let filtered_users = filtered_iam_users(self);
8056 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
8057 self.iam_state.current_user = Some(user.user_name.clone());
8058 self.iam_state.user_tab = UserTab::Permissions;
8059 self.iam_state.policies.reset();
8060 self.update_current_tab_breadcrumb();
8061 }
8062 }
8063 } else if self.current_service == Service::IamRoles {
8064 if self.iam_state.current_role.is_some() {
8065 if self.iam_state.role_tab == RoleTab::Permissions {
8067 let filtered = filtered_iam_policies(self);
8068 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
8069 self.iam_state.current_policy = Some(policy.policy_name.clone());
8070 self.iam_state.policy_scroll = 0;
8071 self.view_mode = ViewMode::PolicyView;
8072 self.iam_state.policies.loading = true;
8073 self.update_current_tab_breadcrumb();
8074 }
8075 }
8076 } else if self.iam_state.current_role.is_none() {
8077 let filtered_roles = filtered_iam_roles(self);
8078 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
8079 self.iam_state.current_role = Some(role.role_name.clone());
8080 self.iam_state.role_tab = RoleTab::Permissions;
8081 self.iam_state.policies.reset();
8082 self.update_current_tab_breadcrumb();
8083 }
8084 }
8085 } else if self.current_service == Service::IamUserGroups {
8086 if self.iam_state.current_group.is_none() {
8087 let filtered_groups: Vec<_> = self
8088 .iam_state
8089 .groups
8090 .items
8091 .iter()
8092 .filter(|g| {
8093 if self.iam_state.groups.filter.is_empty() {
8094 true
8095 } else {
8096 g.group_name
8097 .to_lowercase()
8098 .contains(&self.iam_state.groups.filter.to_lowercase())
8099 }
8100 })
8101 .collect();
8102 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
8103 self.iam_state.current_group = Some(group.group_name.clone());
8104 self.update_current_tab_breadcrumb();
8105 }
8106 }
8107 } else if self.current_service == Service::LambdaFunctions {
8108 crate::lambda::functions::select_item(self);
8109 } else if self.current_service == Service::LambdaApplications {
8110 crate::lambda::applications::select_item(self);
8111 } else if self.current_service == Service::CloudWatchLogGroups {
8112 if self.view_mode == ViewMode::List {
8113 let filtered_groups = filtered_log_groups(self);
8115 if let Some(selected_group) =
8116 filtered_groups.get(self.log_groups_state.log_groups.selected)
8117 {
8118 if let Some(actual_idx) = self
8119 .log_groups_state
8120 .log_groups
8121 .items
8122 .iter()
8123 .position(|g| g.name == selected_group.name)
8124 {
8125 self.log_groups_state.log_groups.selected = actual_idx;
8126 }
8127 }
8128 self.view_mode = ViewMode::Detail;
8129 self.log_groups_state.log_streams.clear();
8130 self.log_groups_state.tags.items.clear();
8131 self.log_groups_state.tags.reset();
8132 self.log_groups_state.selected_stream = 0;
8133 self.log_groups_state.loading = true;
8134 self.column_selector_index = 0;
8135 self.update_current_tab_breadcrumb();
8136 } else if self.view_mode == ViewMode::Detail {
8137 let filtered_streams = filtered_log_streams(self);
8139 if let Some(selected_stream) =
8140 filtered_streams.get(self.log_groups_state.selected_stream)
8141 {
8142 if let Some(actual_idx) = self
8143 .log_groups_state
8144 .log_streams
8145 .iter()
8146 .position(|s| s.name == selected_stream.name)
8147 {
8148 self.log_groups_state.selected_stream = actual_idx;
8149 }
8150 }
8151 self.view_mode = ViewMode::Events;
8152 self.update_current_tab_breadcrumb();
8153 self.log_groups_state.log_events.clear();
8154 self.log_groups_state.event_scroll_offset = 0;
8155 self.log_groups_state.next_backward_token = None;
8156 self.log_groups_state.loading = true;
8157 } else if self.view_mode == ViewMode::Events {
8158 if self.log_groups_state.expanded_event
8160 == Some(self.log_groups_state.event_scroll_offset)
8161 {
8162 self.log_groups_state.expanded_event = None;
8163 } else {
8164 self.log_groups_state.expanded_event =
8165 Some(self.log_groups_state.event_scroll_offset);
8166 }
8167 }
8168 } else if self.current_service == Service::CloudWatchAlarms
8169 && self.view_mode != ViewMode::Detail
8170 {
8171 self.alarms_state.table.toggle_expand();
8173 } else if self.current_service == Service::CloudWatchInsights {
8174 if !self.insights_state.insights.selected_log_groups.is_empty() {
8176 self.log_groups_state.loading = true;
8177 self.insights_state.insights.query_completed = true;
8178 }
8179 }
8180 }
8181 }
8182
8183 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
8184 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
8185 Ok(())
8186 }
8187
8188 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
8189 let alarms = self.alarms_client.list_alarms().await?;
8190 self.alarms_state.table.items = alarms
8191 .into_iter()
8192 .map(
8193 |(
8194 name,
8195 state,
8196 state_updated,
8197 description,
8198 metric_name,
8199 namespace,
8200 statistic,
8201 period,
8202 comparison,
8203 threshold,
8204 actions_enabled,
8205 state_reason,
8206 resource,
8207 dimensions,
8208 expression,
8209 alarm_type,
8210 cross_account,
8211 )| Alarm {
8212 name,
8213 state,
8214 state_updated_timestamp: state_updated,
8215 description,
8216 metric_name,
8217 namespace,
8218 statistic,
8219 period,
8220 comparison_operator: comparison,
8221 threshold,
8222 actions_enabled,
8223 state_reason,
8224 resource,
8225 dimensions,
8226 expression,
8227 alarm_type,
8228 cross_account,
8229 },
8230 )
8231 .collect();
8232 Ok(())
8233 }
8234
8235 pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
8236 let (events, next_token) = self.cloudtrail_client.lookup_events(None, None).await?;
8237 self.cloudtrail_state.table.items = events
8238 .into_iter()
8239 .map(
8240 |(
8241 event_name,
8242 event_time,
8243 username,
8244 event_source,
8245 resource_type,
8246 resource_name,
8247 read_only,
8248 aws_region,
8249 event_id,
8250 access_key_id,
8251 source_ip_address,
8252 error_code,
8253 request_id,
8254 event_type,
8255 cloud_trail_event_json,
8256 )| CloudTrailEvent {
8257 event_name,
8258 event_time,
8259 username,
8260 event_source,
8261 resource_type,
8262 resource_name,
8263 read_only,
8264 aws_region,
8265 event_id,
8266 access_key_id,
8267 source_ip_address,
8268 error_code,
8269 request_id,
8270 event_type,
8271 cloud_trail_event_json,
8272 },
8273 )
8274 .collect();
8275 self.cloudtrail_state.table.next_token = next_token;
8276 Ok(())
8277 }
8278
8279 pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
8280 if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
8281 let (events, next_token) = self
8283 .cloudtrail_client
8284 .lookup_events(None, Some(token))
8285 .await?;
8286 self.cloudtrail_state
8287 .table
8288 .items
8289 .extend(events.into_iter().map(
8290 |(
8291 event_name,
8292 event_time,
8293 username,
8294 event_source,
8295 resource_type,
8296 resource_name,
8297 read_only,
8298 aws_region,
8299 event_id,
8300 access_key_id,
8301 source_ip_address,
8302 error_code,
8303 request_id,
8304 event_type,
8305 cloud_trail_event_json,
8306 )| CloudTrailEvent {
8307 event_name,
8308 event_time,
8309 username,
8310 event_source,
8311 resource_type,
8312 resource_name,
8313 read_only,
8314 aws_region,
8315 event_id,
8316 access_key_id,
8317 source_ip_address,
8318 error_code,
8319 request_id,
8320 event_type,
8321 cloud_trail_event_json,
8322 },
8323 ));
8324 self.cloudtrail_state.table.next_token = next_token;
8325 }
8326 Ok(())
8327 }
8328
8329 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
8330 if let Some(bucket_name) = &self.s3_state.current_bucket {
8331 let bucket_region = if let Some(bucket) = self
8333 .s3_state
8334 .buckets
8335 .items
8336 .iter_mut()
8337 .find(|b| &b.name == bucket_name)
8338 {
8339 if bucket.region.is_empty() {
8340 let region = self.s3_client.get_bucket_location(bucket_name).await?;
8342 bucket.region = region.clone();
8343 region
8344 } else {
8345 bucket.region.clone()
8346 }
8347 } else {
8348 self.config.region.clone()
8349 };
8350
8351 let prefix = self
8352 .s3_state
8353 .prefix_stack
8354 .last()
8355 .cloned()
8356 .unwrap_or_default();
8357 let objects = self
8358 .s3_client
8359 .list_objects(bucket_name, &bucket_region, &prefix)
8360 .await?;
8361 self.s3_state.objects = objects
8362 .into_iter()
8363 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8364 key,
8365 size,
8366 last_modified: modified,
8367 is_prefix,
8368 storage_class,
8369 })
8370 .collect();
8371 self.s3_state.selected_object = 0;
8372 }
8373 Ok(())
8374 }
8375
8376 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
8377 let stored_region = self
8378 .s3_state
8379 .buckets
8380 .items
8381 .iter()
8382 .find(|b| b.name == bucket_name)
8383 .and_then(|b| {
8384 if b.region.is_empty() {
8385 None
8386 } else {
8387 Some(b.region.clone())
8388 }
8389 });
8390
8391 let bucket_region = match stored_region {
8392 Some(r) => r,
8393 None => {
8394 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
8395 if let Some(b) = self
8397 .s3_state
8398 .buckets
8399 .items
8400 .iter_mut()
8401 .find(|b| b.name == bucket_name)
8402 {
8403 b.region = r.clone();
8404 }
8405 r
8406 }
8407 };
8408 let objects = self
8409 .s3_client
8410 .list_objects(&bucket_name, &bucket_region, "")
8411 .await?;
8412 let preview: Vec<S3Object> = objects
8413 .into_iter()
8414 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8415 key,
8416 size,
8417 last_modified: modified,
8418 is_prefix,
8419 storage_class,
8420 })
8421 .collect();
8422 self.s3_state
8423 .bucket_preview
8424 .insert(bucket_name.clone(), preview);
8425 self.after_bucket_preview_loaded(&bucket_name);
8426 Ok(())
8427 }
8428
8429 pub fn after_bucket_preview_loaded(&mut self, bucket_name: &str) {
8433 if self.s3_state.current_bucket.is_some() {
8434 return; }
8436 let mut row_idx = 0usize;
8438 for b in &self.s3_state.buckets.items {
8439 if b.name == bucket_name {
8440 if self.s3_state.selected_row == row_idx {
8442 if let Some(preview) = self.s3_state.bucket_preview.get(bucket_name) {
8443 if !preview.is_empty() {
8444 self.s3_state.selected_row = row_idx + 1;
8445 let visible = self.s3_state.bucket_visible_rows.get();
8446 if self.s3_state.selected_row
8447 >= self.s3_state.bucket_scroll_offset + visible
8448 {
8449 self.s3_state.bucket_scroll_offset =
8450 self.s3_state.selected_row.saturating_sub(visible - 1);
8451 }
8452 }
8453 }
8454 }
8455 return;
8456 }
8457 row_idx += 1;
8458 if self.s3_state.expanded_prefixes.contains(&b.name) {
8460 if let Some(p) = self.s3_state.bucket_preview.get(&b.name) {
8461 row_idx += p.len();
8462 }
8463 }
8464 }
8465 }
8466
8467 pub async fn load_prefix_preview(
8468 &mut self,
8469 bucket_name: String,
8470 prefix: String,
8471 ) -> anyhow::Result<()> {
8472 let stored_region = self
8473 .s3_state
8474 .buckets
8475 .items
8476 .iter()
8477 .find(|b| b.name == bucket_name)
8478 .and_then(|b| {
8479 if b.region.is_empty() {
8480 None
8481 } else {
8482 Some(b.region.clone())
8483 }
8484 });
8485
8486 let bucket_region = match stored_region {
8487 Some(r) => r,
8488 None => {
8489 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
8490 if let Some(b) = self
8491 .s3_state
8492 .buckets
8493 .items
8494 .iter_mut()
8495 .find(|b| b.name == bucket_name)
8496 {
8497 b.region = r.clone();
8498 }
8499 r
8500 }
8501 };
8502 let objects = self
8503 .s3_client
8504 .list_objects(&bucket_name, &bucket_region, &prefix)
8505 .await?;
8506 let preview: Vec<S3Object> = objects
8507 .into_iter()
8508 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8509 key,
8510 size,
8511 last_modified: modified,
8512 is_prefix,
8513 storage_class,
8514 })
8515 .collect();
8516 self.s3_state.prefix_preview.insert(prefix, preview);
8517 Ok(())
8518 }
8519
8520 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
8521 let repos = match self.ecr_state.tab {
8522 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
8523 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
8524 };
8525
8526 self.ecr_state.repositories.items = repos
8527 .into_iter()
8528 .map(|r| EcrRepository {
8529 name: r.name,
8530 uri: r.uri,
8531 created_at: r.created_at,
8532 tag_immutability: r.tag_immutability,
8533 encryption_type: r.encryption_type,
8534 })
8535 .collect();
8536
8537 self.ecr_state
8538 .repositories
8539 .items
8540 .sort_by(|a, b| a.name.cmp(&b.name));
8541 Ok(())
8542 }
8543
8544 pub async fn load_apis(&mut self) -> anyhow::Result<()> {
8545 let apis = self.apig_client.list_rest_apis().await?;
8546
8547 self.apig_state.apis.items = apis
8548 .into_iter()
8549 .map(|a| crate::apig::api::RestApi {
8550 id: a.id,
8551 name: a.name,
8552 description: a.description,
8553 created_date: a.created_date,
8554 api_key_source: a.api_key_source,
8555 endpoint_configuration: a.endpoint_configuration,
8556 protocol_type: a.protocol_type,
8557 disable_execute_api_endpoint: a.disable_execute_api_endpoint,
8558 status: a.status,
8559 })
8560 .collect();
8561
8562 self.apig_state
8563 .apis
8564 .items
8565 .sort_by(|a, b| a.name.cmp(&b.name));
8566 Ok(())
8567 }
8568
8569 pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
8570 let instances = self.ec2_client.list_instances().await?;
8571
8572 self.ec2_state.table.items = instances
8573 .into_iter()
8574 .map(|i| Ec2Instance {
8575 instance_id: i.instance_id,
8576 name: i.name,
8577 state: i.state,
8578 instance_type: i.instance_type,
8579 availability_zone: i.availability_zone,
8580 public_ipv4_dns: i.public_ipv4_dns,
8581 public_ipv4_address: i.public_ipv4_address,
8582 elastic_ip: i.elastic_ip,
8583 ipv6_ips: i.ipv6_ips,
8584 monitoring: i.monitoring,
8585 security_groups: i.security_groups,
8586 key_name: i.key_name,
8587 launch_time: i.launch_time,
8588 platform_details: i.platform_details,
8589 status_checks: i.status_checks,
8590 alarm_status: i.alarm_status,
8591 private_dns_name: String::new(),
8592 private_ip_address: String::new(),
8593 security_group_ids: String::new(),
8594 owner_id: String::new(),
8595 volume_id: String::new(),
8596 root_device_name: String::new(),
8597 root_device_type: String::new(),
8598 ebs_optimized: String::new(),
8599 image_id: String::new(),
8600 kernel_id: String::new(),
8601 ramdisk_id: String::new(),
8602 ami_launch_index: String::new(),
8603 reservation_id: String::new(),
8604 vpc_id: String::new(),
8605 subnet_ids: String::new(),
8606 instance_lifecycle: String::new(),
8607 architecture: String::new(),
8608 virtualization_type: String::new(),
8609 platform: String::new(),
8610 iam_instance_profile_arn: String::new(),
8611 tenancy: String::new(),
8612 affinity: String::new(),
8613 host_id: String::new(),
8614 placement_group: String::new(),
8615 partition_number: String::new(),
8616 capacity_reservation_id: String::new(),
8617 state_transition_reason_code: String::new(),
8618 state_transition_reason_message: String::new(),
8619 stop_hibernation_behavior: String::new(),
8620 outpost_arn: String::new(),
8621 product_codes: String::new(),
8622 availability_zone_id: String::new(),
8623 imdsv2: String::new(),
8624 usage_operation: String::new(),
8625 managed: String::new(),
8626 operator: String::new(),
8627 })
8628 .collect();
8629
8630 self.ec2_state
8632 .table
8633 .items
8634 .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
8635 Ok(())
8636 }
8637
8638 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
8639 if let Some(repo_name) = &self.ecr_state.current_repository {
8640 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
8641 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
8642
8643 self.ecr_state.images.items = images
8644 .into_iter()
8645 .map(|i| EcrImage {
8646 tag: i.tag,
8647 artifact_type: i.artifact_type,
8648 pushed_at: i.pushed_at,
8649 size_bytes: i.size_bytes,
8650 uri: i.uri,
8651 digest: i.digest,
8652 last_pull_time: i.last_pull_time,
8653 })
8654 .collect();
8655
8656 self.ecr_state
8657 .images
8658 .items
8659 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
8660 }
8661 }
8662 Ok(())
8663 }
8664
8665 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
8666 let stacks = self
8667 .cloudformation_client
8668 .list_stacks(self.cfn_state.view_nested)
8669 .await?;
8670
8671 let mut stacks: Vec<CfnStack> = stacks
8672 .into_iter()
8673 .map(|s| CfnStack {
8674 name: s.name,
8675 stack_id: s.stack_id,
8676 status: s.status,
8677 created_time: s.created_time,
8678 updated_time: s.updated_time,
8679 deleted_time: s.deleted_time,
8680 drift_status: s.drift_status,
8681 last_drift_check_time: s.last_drift_check_time,
8682 status_reason: s.status_reason,
8683 description: s.description,
8684 detailed_status: String::new(),
8685 root_stack: String::new(),
8686 parent_stack: String::new(),
8687 termination_protection: false,
8688 iam_role: String::new(),
8689 tags: Vec::new(),
8690 stack_policy: String::new(),
8691 rollback_monitoring_time: String::new(),
8692 rollback_alarms: Vec::new(),
8693 notification_arns: Vec::new(),
8694 })
8695 .collect();
8696
8697 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
8699
8700 self.cfn_state.table.items = stacks;
8701
8702 Ok(())
8703 }
8704
8705 pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
8706 let template = self.cloudformation_client.get_template(stack_name).await?;
8707 self.cfn_state.template_body = template;
8708 self.cfn_state.template_scroll = 0;
8709 Ok(())
8710 }
8711
8712 pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
8713 let mut parameters = self
8714 .cloudformation_client
8715 .get_stack_parameters(stack_name)
8716 .await?;
8717 parameters.sort_by(|a, b| a.key.cmp(&b.key));
8718 self.cfn_state.parameters.items = parameters;
8719 self.cfn_state.parameters.reset();
8720 Ok(())
8721 }
8722
8723 pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
8724 let outputs = self
8725 .cloudformation_client
8726 .get_stack_outputs(stack_name)
8727 .await?;
8728 self.cfn_state.outputs.items = outputs;
8729 self.cfn_state.outputs.reset();
8730 Ok(())
8731 }
8732
8733 pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
8734 let resources = self
8735 .cloudformation_client
8736 .get_stack_resources(stack_name)
8737 .await?;
8738 self.cfn_state.resources.items = resources;
8739 self.cfn_state.resources.reset();
8740 Ok(())
8741 }
8742
8743 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
8744 let attached_policies = self
8746 .iam_client
8747 .list_attached_role_policies(role_name)
8748 .await
8749 .map_err(|e| anyhow::anyhow!(e))?;
8750
8751 let mut policies: Vec<IamPolicy> = attached_policies
8752 .into_iter()
8753 .map(|p| IamPolicy {
8754 policy_name: p.policy_name().unwrap_or("").to_string(),
8755 policy_type: "Managed".to_string(),
8756 attached_via: "Direct".to_string(),
8757 attached_entities: "-".to_string(),
8758 description: "-".to_string(),
8759 creation_time: "-".to_string(),
8760 edited_time: "-".to_string(),
8761 policy_arn: p.policy_arn().map(|s| s.to_string()),
8762 })
8763 .collect();
8764
8765 let inline_policy_names = self
8767 .iam_client
8768 .list_role_policies(role_name)
8769 .await
8770 .map_err(|e| anyhow::anyhow!(e))?;
8771
8772 for policy_name in inline_policy_names {
8773 policies.push(IamPolicy {
8774 policy_name,
8775 policy_type: "Inline".to_string(),
8776 attached_via: "Direct".to_string(),
8777 attached_entities: "-".to_string(),
8778 description: "-".to_string(),
8779 creation_time: "-".to_string(),
8780 edited_time: "-".to_string(),
8781 policy_arn: None,
8782 });
8783 }
8784
8785 self.iam_state.policies.items = policies;
8786
8787 Ok(())
8788 }
8789
8790 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
8791 let attached_policies = self
8792 .iam_client
8793 .list_attached_group_policies(group_name)
8794 .await
8795 .map_err(|e| anyhow::anyhow!(e))?;
8796
8797 let mut policies: Vec<IamPolicy> = attached_policies
8798 .into_iter()
8799 .map(|p| IamPolicy {
8800 policy_name: p.policy_name().unwrap_or("").to_string(),
8801 policy_type: "AWS managed".to_string(),
8802 attached_via: "Direct".to_string(),
8803 attached_entities: "-".to_string(),
8804 description: "-".to_string(),
8805 creation_time: "-".to_string(),
8806 edited_time: "-".to_string(),
8807 policy_arn: p.policy_arn().map(|s| s.to_string()),
8808 })
8809 .collect();
8810
8811 let inline_policy_names = self
8812 .iam_client
8813 .list_group_policies(group_name)
8814 .await
8815 .map_err(|e| anyhow::anyhow!(e))?;
8816
8817 for policy_name in inline_policy_names {
8818 policies.push(IamPolicy {
8819 policy_name,
8820 policy_type: "Inline".to_string(),
8821 attached_via: "Direct".to_string(),
8822 attached_entities: "-".to_string(),
8823 description: "-".to_string(),
8824 creation_time: "-".to_string(),
8825 edited_time: "-".to_string(),
8826 policy_arn: None,
8827 });
8828 }
8829
8830 self.iam_state.policies.items = policies;
8831
8832 Ok(())
8833 }
8834
8835 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
8836 let users = self
8837 .iam_client
8838 .get_group_users(group_name)
8839 .await
8840 .map_err(|e| anyhow::anyhow!(e))?;
8841
8842 let group_users: Vec<IamGroupUser> = users
8843 .into_iter()
8844 .map(|u| {
8845 let creation_time = {
8846 let dt = u.create_date();
8847 let timestamp = dt.secs();
8848 let datetime =
8849 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
8850 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
8851 };
8852
8853 IamGroupUser {
8854 user_name: u.user_name().to_string(),
8855 groups: String::new(),
8856 last_activity: String::new(),
8857 creation_time,
8858 }
8859 })
8860 .collect();
8861
8862 self.iam_state.group_users.items = group_users;
8863
8864 Ok(())
8865 }
8866
8867 pub async fn load_policy_document(
8868 &mut self,
8869 role_name: &str,
8870 policy_name: &str,
8871 ) -> anyhow::Result<()> {
8872 let policy = self
8874 .iam_state
8875 .policies
8876 .items
8877 .iter()
8878 .find(|p| p.policy_name == policy_name)
8879 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
8880
8881 let document = if let Some(policy_arn) = &policy.policy_arn {
8882 self.iam_client
8884 .get_policy_version(policy_arn)
8885 .await
8886 .map_err(|e| anyhow::anyhow!(e))?
8887 } else {
8888 self.iam_client
8890 .get_role_policy(role_name, policy_name)
8891 .await
8892 .map_err(|e| anyhow::anyhow!(e))?
8893 };
8894
8895 self.iam_state.policy_document = document;
8896
8897 Ok(())
8898 }
8899
8900 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
8901 let document = self
8902 .iam_client
8903 .get_role(role_name)
8904 .await
8905 .map_err(|e| anyhow::anyhow!(e))?;
8906
8907 self.iam_state.trust_policy_document = document;
8908
8909 Ok(())
8910 }
8911
8912 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
8913 self.iam_state.last_accessed_services.items = vec![];
8915 self.iam_state.last_accessed_services.selected = 0;
8916
8917 Ok(())
8918 }
8919
8920 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
8921 let tags = self
8922 .iam_client
8923 .list_role_tags(role_name)
8924 .await
8925 .map_err(|e| anyhow::anyhow!(e))?;
8926 self.iam_state.tags.items = tags
8927 .into_iter()
8928 .map(|(k, v)| IamRoleTag { key: k, value: v })
8929 .collect();
8930 self.iam_state.tags.reset();
8931 Ok(())
8932 }
8933
8934 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
8935 let tags = self
8936 .iam_client
8937 .list_user_tags(user_name)
8938 .await
8939 .map_err(|e| anyhow::anyhow!(e))?;
8940 self.iam_state.user_tags.items = tags
8941 .into_iter()
8942 .map(|(k, v)| IamUserTag { key: k, value: v })
8943 .collect();
8944 self.iam_state.user_tags.reset();
8945 Ok(())
8946 }
8947
8948 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
8949 if let Some(group) = self
8950 .log_groups_state
8951 .log_groups
8952 .items
8953 .get(self.log_groups_state.log_groups.selected)
8954 {
8955 self.log_groups_state.log_streams =
8956 self.cloudwatch_client.list_log_streams(&group.name).await?;
8957 self.log_groups_state.selected_stream = 0;
8958 }
8959 Ok(())
8960 }
8961
8962 pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
8963 if let Some(group) = self
8964 .log_groups_state
8965 .log_groups
8966 .items
8967 .get(self.log_groups_state.log_groups.selected)
8968 {
8969 let arn = if let Some(arn) = &group.log_group_arn {
8971 arn.clone()
8972 } else if let Some(arn) = &group.arn {
8973 arn.clone()
8974 } else {
8975 let account_id = if self.config.account_id.is_empty() {
8977 "*"
8978 } else {
8979 &self.config.account_id
8980 };
8981 format!(
8982 "arn:aws:logs:{}:{}:log-group:{}",
8983 self.config.region, account_id, group.name
8984 )
8985 };
8986
8987 let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
8988 self.log_groups_state.tags.items = tags;
8989 self.log_groups_state.tags.selected = 0;
8990 self.log_groups_state.tags.scroll_offset = 0;
8991 }
8992 Ok(())
8993 }
8994
8995 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
8996 if let Some(group) = self
8997 .log_groups_state
8998 .log_groups
8999 .items
9000 .get(self.log_groups_state.log_groups.selected)
9001 {
9002 if let Some(stream) = self
9003 .log_groups_state
9004 .log_streams
9005 .get(self.log_groups_state.selected_stream)
9006 {
9007 let (start_time, end_time) =
9009 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
9010 let now = chrono::Utc::now().timestamp_millis();
9011 let duration_ms = match self.log_groups_state.relative_unit {
9012 TimeUnit::Minutes => amount * 60 * 1000,
9013 TimeUnit::Hours => amount * 60 * 60 * 1000,
9014 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
9015 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
9016 };
9017 (Some(now - duration_ms), Some(now))
9018 } else {
9019 (None, None)
9020 };
9021
9022 let (mut events, has_more, token) = self
9023 .cloudwatch_client
9024 .get_log_events(
9025 &group.name,
9026 &stream.name,
9027 self.log_groups_state.next_backward_token.clone(),
9028 start_time,
9029 end_time,
9030 )
9031 .await?;
9032
9033 if self.log_groups_state.next_backward_token.is_some() {
9034 events.append(&mut self.log_groups_state.log_events);
9036 self.log_groups_state.event_scroll_offset = 0;
9037 } else {
9038 self.log_groups_state.event_scroll_offset = 0;
9040 }
9041
9042 self.log_groups_state.log_events = events;
9043 self.log_groups_state.has_older_events =
9044 has_more && self.log_groups_state.log_events.len() >= 25;
9045 self.log_groups_state.next_backward_token = token;
9046 self.log_groups_state.selected_event = 0;
9047 }
9048 }
9049 Ok(())
9050 }
9051
9052 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
9053 if self.insights_state.insights.selected_log_groups.is_empty() {
9054 return Err(anyhow::anyhow!(
9055 "No log groups selected. Please select at least one log group."
9056 ));
9057 }
9058
9059 let now = chrono::Utc::now().timestamp_millis();
9060 let amount = self
9061 .insights_state
9062 .insights
9063 .insights_relative_amount
9064 .parse::<i64>()
9065 .unwrap_or(1);
9066 let duration_ms = match self.insights_state.insights.insights_relative_unit {
9067 TimeUnit::Minutes => amount * 60 * 1000,
9068 TimeUnit::Hours => amount * 60 * 60 * 1000,
9069 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
9070 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
9071 };
9072 let start_time = now - duration_ms;
9073
9074 let query_id = self
9075 .cloudwatch_client
9076 .start_query(
9077 self.insights_state.insights.selected_log_groups.clone(),
9078 self.insights_state.insights.query_text.trim().to_string(),
9079 start_time,
9080 now,
9081 )
9082 .await?;
9083
9084 for _ in 0..60 {
9086 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
9087 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
9088
9089 if status == "Complete" {
9090 self.insights_state.insights.query_results = results;
9091 self.insights_state.insights.query_completed = true;
9092 self.insights_state.insights.results_selected = 0;
9093 self.insights_state.insights.expanded_result = None;
9094 self.view_mode = ViewMode::InsightsResults;
9095 return Ok(());
9096 } else if status == "Failed" || status == "Cancelled" {
9097 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
9098 }
9099 }
9100
9101 Err(anyhow::anyhow!("Query timeout"))
9102 }
9103
9104 pub(crate) fn toggle_column_visibility<T: PartialEq + Copy>(
9106 visible_columns: &mut Vec<T>,
9107 _all_columns: &[T],
9108 column_to_toggle: T,
9109 ) {
9110 if let Some(pos) = visible_columns.iter().position(|c| c == &column_to_toggle) {
9111 if visible_columns.len() > 1 {
9113 visible_columns.remove(pos);
9114 }
9115 } else {
9116 visible_columns.push(column_to_toggle);
9117 }
9118 }
9119}
9120
9121impl CloudWatchInsightsState {
9122 fn new() -> Self {
9123 Self {
9124 insights: InsightsState::default(),
9125 loading: false,
9126 }
9127 }
9128}
9129
9130impl CloudWatchAlarmsState {
9131 fn new() -> Self {
9132 Self {
9133 table: TableState::new(),
9134 current_alarm: None,
9135 alarm_tab: AlarmTab::AllAlarms,
9136 view_as: AlarmViewMode::Table,
9137 wrap_lines: false,
9138 sort_column: "Last state update".to_string(),
9139 sort_direction: SortDirection::Asc,
9140 input_focus: InputFocus::Filter,
9141 metric_data: Vec::new(),
9142 metrics_loading: false,
9143 }
9144 }
9145}
9146
9147impl ServicePickerState {
9148 fn new() -> Self {
9149 Self {
9150 filter: String::new(),
9151 filter_active: false,
9152 selected: 0,
9153 services: vec![
9154 "API Gateway › APIs",
9155 "CloudWatch › Log Groups",
9156 "CloudWatch › Logs Insights",
9157 "CloudWatch › Alarms",
9158 "CloudTrail › Event History",
9159 "CloudFormation › Stacks",
9160 "EC2 › Instances",
9161 "ECR › Repositories",
9162 "IAM › Users",
9163 "IAM › Roles",
9164 "IAM › User Groups",
9165 "Lambda › Functions",
9166 "Lambda › Applications",
9167 "S3 › Buckets",
9168 "SQS › Queues",
9169 ],
9170 }
9171 }
9172}
9173
9174#[cfg(test)]
9175mod test_helpers {
9176 use super::*;
9177
9178 pub fn test_app() -> App {
9180 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
9181 }
9182
9183 pub fn test_app_no_region() -> App {
9184 App::new_without_client("test".to_string(), None)
9185 }
9186}
9187
9188#[cfg(test)]
9189mod tests {
9190 use super::*;
9191 use crate::keymap::Action;
9192 use test_helpers::*;
9193
9194 #[test]
9195 fn test_next_tab_cycles_forward() {
9196 let mut app = test_app();
9197 app.tabs = vec![
9198 Tab {
9199 service: Service::CloudWatchLogGroups,
9200 title: "CloudWatch › Log Groups".to_string(),
9201 breadcrumb: "CloudWatch › Log Groups".to_string(),
9202 },
9203 Tab {
9204 service: Service::CloudWatchInsights,
9205 title: "CloudWatch › Logs Insights".to_string(),
9206 breadcrumb: "CloudWatch › Logs Insights".to_string(),
9207 },
9208 Tab {
9209 service: Service::CloudWatchAlarms,
9210 title: "CloudWatch › Alarms".to_string(),
9211 breadcrumb: "CloudWatch › Alarms".to_string(),
9212 },
9213 ];
9214 app.current_tab = 0;
9215
9216 app.handle_action(Action::NextTab);
9217 assert_eq!(app.current_tab, 1);
9218 assert_eq!(app.current_service, Service::CloudWatchInsights);
9219
9220 app.handle_action(Action::NextTab);
9221 assert_eq!(app.current_tab, 2);
9222 assert_eq!(app.current_service, Service::CloudWatchAlarms);
9223
9224 app.handle_action(Action::NextTab);
9226 assert_eq!(app.current_tab, 0);
9227 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
9228 }
9229
9230 #[test]
9231 fn test_prev_tab_cycles_backward() {
9232 let mut app = test_app();
9233 app.tabs = vec![
9234 Tab {
9235 service: Service::CloudWatchLogGroups,
9236 title: "CloudWatch › Log Groups".to_string(),
9237 breadcrumb: "CloudWatch › Log Groups".to_string(),
9238 },
9239 Tab {
9240 service: Service::CloudWatchInsights,
9241 title: "CloudWatch › Logs Insights".to_string(),
9242 breadcrumb: "CloudWatch › Logs Insights".to_string(),
9243 },
9244 Tab {
9245 service: Service::CloudWatchAlarms,
9246 title: "CloudWatch › Alarms".to_string(),
9247 breadcrumb: "CloudWatch › Alarms".to_string(),
9248 },
9249 ];
9250 app.current_tab = 2;
9251
9252 app.handle_action(Action::PrevTab);
9253 assert_eq!(app.current_tab, 1);
9254 assert_eq!(app.current_service, Service::CloudWatchInsights);
9255
9256 app.handle_action(Action::PrevTab);
9257 assert_eq!(app.current_tab, 0);
9258 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
9259
9260 app.handle_action(Action::PrevTab);
9262 assert_eq!(app.current_tab, 2);
9263 assert_eq!(app.current_service, Service::CloudWatchAlarms);
9264 }
9265
9266 #[test]
9267 fn test_close_tab_removes_current() {
9268 let mut app = test_app();
9269 app.tabs = vec![
9270 Tab {
9271 service: Service::CloudWatchLogGroups,
9272 title: "CloudWatch › Log Groups".to_string(),
9273 breadcrumb: "CloudWatch › Log Groups".to_string(),
9274 },
9275 Tab {
9276 service: Service::CloudWatchInsights,
9277 title: "CloudWatch › Logs Insights".to_string(),
9278 breadcrumb: "CloudWatch › Logs Insights".to_string(),
9279 },
9280 Tab {
9281 service: Service::CloudWatchAlarms,
9282 title: "CloudWatch › Alarms".to_string(),
9283 breadcrumb: "CloudWatch › Alarms".to_string(),
9284 },
9285 ];
9286 app.current_tab = 1;
9287 app.service_selected = true;
9288
9289 app.handle_action(Action::CloseTab);
9290 assert_eq!(app.tabs.len(), 2);
9291 assert_eq!(app.current_tab, 1);
9292 assert_eq!(app.current_service, Service::CloudWatchAlarms);
9293 }
9294
9295 #[test]
9296 fn test_close_last_tab_exits_service() {
9297 let mut app = test_app();
9298 app.tabs = vec![Tab {
9299 service: Service::CloudWatchLogGroups,
9300 title: "CloudWatch › Log Groups".to_string(),
9301 breadcrumb: "CloudWatch › Log Groups".to_string(),
9302 }];
9303 app.current_tab = 0;
9304 app.service_selected = true;
9305
9306 app.handle_action(Action::CloseTab);
9307 assert_eq!(app.tabs.len(), 0);
9308 assert!(!app.service_selected);
9309 assert_eq!(app.current_tab, 0);
9310 }
9311
9312 #[test]
9313 fn test_close_service_removes_current_tab() {
9314 let mut app = test_app();
9315 app.tabs = vec![
9316 Tab {
9317 service: Service::CloudWatchLogGroups,
9318 title: "CloudWatch › Log Groups".to_string(),
9319 breadcrumb: "CloudWatch › Log Groups".to_string(),
9320 },
9321 Tab {
9322 service: Service::CloudWatchInsights,
9323 title: "CloudWatch › Logs Insights".to_string(),
9324 breadcrumb: "CloudWatch › Logs Insights".to_string(),
9325 },
9326 Tab {
9327 service: Service::CloudWatchAlarms,
9328 title: "CloudWatch › Alarms".to_string(),
9329 breadcrumb: "CloudWatch › Alarms".to_string(),
9330 },
9331 ];
9332 app.current_tab = 1;
9333 app.service_selected = true;
9334
9335 app.handle_action(Action::CloseService);
9336
9337 assert_eq!(app.tabs.len(), 2);
9339 assert_eq!(app.current_tab, 1);
9341 assert_eq!(app.current_service, Service::CloudWatchAlarms);
9342 assert!(app.service_selected);
9344 assert_eq!(app.mode, Mode::Normal);
9345 }
9346
9347 #[test]
9348 fn test_close_service_last_tab_shows_picker() {
9349 let mut app = test_app();
9350 app.tabs = vec![Tab {
9351 service: Service::CloudWatchLogGroups,
9352 title: "CloudWatch › Log Groups".to_string(),
9353 breadcrumb: "CloudWatch › Log Groups".to_string(),
9354 }];
9355 app.current_tab = 0;
9356 app.service_selected = true;
9357
9358 app.handle_action(Action::CloseService);
9359
9360 assert_eq!(app.tabs.len(), 0);
9362 assert!(!app.service_selected);
9364 assert_eq!(app.mode, Mode::ServicePicker);
9365 }
9366
9367 #[test]
9368 fn test_open_tab_picker_with_tabs() {
9369 let mut app = test_app();
9370 app.tabs = vec![
9371 Tab {
9372 service: Service::CloudWatchLogGroups,
9373 title: "CloudWatch › Log Groups".to_string(),
9374 breadcrumb: "CloudWatch › Log Groups".to_string(),
9375 },
9376 Tab {
9377 service: Service::CloudWatchInsights,
9378 title: "CloudWatch › Logs Insights".to_string(),
9379 breadcrumb: "CloudWatch › Logs Insights".to_string(),
9380 },
9381 ];
9382 app.current_tab = 1;
9383
9384 app.handle_action(Action::OpenTabPicker);
9385 assert_eq!(app.mode, Mode::TabPicker);
9386 assert_eq!(app.tab_picker_selected, 1);
9387 }
9388
9389 #[test]
9390 fn test_open_tab_picker_without_tabs() {
9391 let mut app = test_app();
9392 app.tabs = vec![];
9393
9394 app.handle_action(Action::OpenTabPicker);
9395 assert_eq!(app.mode, Mode::Normal);
9396 }
9397
9398 #[test]
9399 fn test_pending_key_state() {
9400 let mut app = test_app();
9401 assert_eq!(app.pending_key, None);
9402
9403 app.pending_key = Some('g');
9404 assert_eq!(app.pending_key, Some('g'));
9405 }
9406
9407 #[test]
9408 fn test_tab_breadcrumb_updates() {
9409 let mut app = test_app();
9410 app.tabs = vec![Tab {
9411 service: Service::CloudWatchLogGroups,
9412 title: "CloudWatch › Log Groups".to_string(),
9413 breadcrumb: "CloudWatch > Log groups".to_string(),
9414 }];
9415 app.current_tab = 0;
9416 app.service_selected = true;
9417 app.current_service = Service::CloudWatchLogGroups;
9418
9419 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
9421
9422 app.log_groups_state
9424 .log_groups
9425 .items
9426 .push(rusticity_core::LogGroup {
9427 name: "/aws/lambda/test".to_string(),
9428 creation_time: None,
9429 stored_bytes: Some(1024),
9430 retention_days: None,
9431 log_class: None,
9432 arn: None,
9433 log_group_arn: None,
9434 deletion_protection_enabled: None,
9435 });
9436 app.log_groups_state.log_groups.reset();
9437 app.view_mode = ViewMode::Detail;
9438 app.update_current_tab_breadcrumb();
9439
9440 assert_eq!(
9442 app.tabs[0].breadcrumb,
9443 "CloudWatch > Log groups > /aws/lambda/test"
9444 );
9445 }
9446
9447 #[test]
9448 fn test_s3_bucket_column_selector_navigation() {
9449 let mut app = test_app();
9450 app.current_service = Service::S3Buckets;
9451 app.mode = Mode::ColumnSelector;
9452 app.column_selector_index = 0;
9453
9454 app.handle_action(Action::NextItem);
9456 assert_eq!(app.column_selector_index, 1);
9457
9458 app.handle_action(Action::NextItem);
9459 assert_eq!(app.column_selector_index, 2);
9460
9461 app.handle_action(Action::NextItem);
9462 assert_eq!(app.column_selector_index, 3);
9463
9464 for _ in 0..10 {
9466 app.handle_action(Action::NextItem);
9467 }
9468 assert_eq!(app.column_selector_index, 9);
9469
9470 app.handle_action(Action::PrevItem);
9472 assert_eq!(app.column_selector_index, 8);
9473
9474 app.handle_action(Action::PrevItem);
9475 assert_eq!(app.column_selector_index, 7);
9476
9477 for _ in 0..10 {
9479 app.handle_action(Action::PrevItem);
9480 }
9481 assert_eq!(app.column_selector_index, 0);
9482 }
9483
9484 #[test]
9485 fn test_cloudwatch_alarms_state_initialized() {
9486 let app = test_app();
9487
9488 assert_eq!(app.alarms_state.table.items.len(), 0);
9490 assert_eq!(app.alarms_state.table.selected, 0);
9491 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
9492 assert!(!app.alarms_state.table.loading);
9493 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
9494 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
9495 }
9496
9497 #[test]
9498 fn test_cloudwatch_alarms_service_selection() {
9499 let mut app = test_app();
9500
9501 app.current_service = Service::CloudWatchAlarms;
9503 app.service_selected = true;
9504
9505 assert_eq!(app.current_service, Service::CloudWatchAlarms);
9506 assert!(app.service_selected);
9507 }
9508
9509 #[test]
9510 fn test_cloudwatch_alarms_column_preferences() {
9511 let app = test_app();
9512
9513 assert!(!app.cw_alarm_column_ids.is_empty());
9515 assert!(!app.cw_alarm_visible_column_ids.is_empty());
9516
9517 assert!(app
9519 .cw_alarm_visible_column_ids
9520 .contains(&AlarmColumn::Name.id()));
9521 assert!(app
9522 .cw_alarm_visible_column_ids
9523 .contains(&AlarmColumn::State.id()));
9524 }
9525
9526 #[test]
9527 fn test_s3_bucket_navigation_without_expansion() {
9528 let mut app = test_app();
9529 app.current_service = Service::S3Buckets;
9530 app.service_selected = true;
9531 app.mode = Mode::Normal;
9532
9533 app.s3_state.buckets.items = vec![
9535 S3Bucket {
9536 name: "bucket1".to_string(),
9537 region: "us-east-1".to_string(),
9538 creation_date: "2024-01-01T00:00:00Z".to_string(),
9539 },
9540 S3Bucket {
9541 name: "bucket2".to_string(),
9542 region: "us-east-1".to_string(),
9543 creation_date: "2024-01-02T00:00:00Z".to_string(),
9544 },
9545 S3Bucket {
9546 name: "bucket3".to_string(),
9547 region: "us-east-1".to_string(),
9548 creation_date: "2024-01-03T00:00:00Z".to_string(),
9549 },
9550 ];
9551 app.s3_state.selected_row = 0;
9552
9553 app.handle_action(Action::NextItem);
9555 assert_eq!(app.s3_state.selected_row, 1);
9556
9557 app.handle_action(Action::NextItem);
9558 assert_eq!(app.s3_state.selected_row, 2);
9559
9560 app.handle_action(Action::NextItem);
9562 assert_eq!(app.s3_state.selected_row, 2);
9563
9564 app.handle_action(Action::PrevItem);
9566 assert_eq!(app.s3_state.selected_row, 1);
9567
9568 app.handle_action(Action::PrevItem);
9569 assert_eq!(app.s3_state.selected_row, 0);
9570
9571 app.handle_action(Action::PrevItem);
9573 assert_eq!(app.s3_state.selected_row, 0);
9574 }
9575
9576 #[test]
9577 fn test_s3_bucket_navigation_with_expansion() {
9578 let mut app = test_app();
9579 app.current_service = Service::S3Buckets;
9580 app.service_selected = true;
9581 app.mode = Mode::Normal;
9582
9583 app.s3_state.buckets.items = vec![
9585 S3Bucket {
9586 name: "bucket1".to_string(),
9587 region: "us-east-1".to_string(),
9588 creation_date: "2024-01-01T00:00:00Z".to_string(),
9589 },
9590 S3Bucket {
9591 name: "bucket2".to_string(),
9592 region: "us-east-1".to_string(),
9593 creation_date: "2024-01-02T00:00:00Z".to_string(),
9594 },
9595 ];
9596
9597 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9599 app.s3_state.bucket_preview.insert(
9600 "bucket1".to_string(),
9601 vec![
9602 S3Object {
9603 key: "file1.txt".to_string(),
9604 size: 100,
9605 last_modified: "2024-01-01T00:00:00Z".to_string(),
9606 is_prefix: false,
9607 storage_class: "STANDARD".to_string(),
9608 },
9609 S3Object {
9610 key: "folder/".to_string(),
9611 size: 0,
9612 last_modified: "2024-01-01T00:00:00Z".to_string(),
9613 is_prefix: true,
9614 storage_class: String::new(),
9615 },
9616 ],
9617 );
9618
9619 app.s3_state.selected_row = 0;
9620
9621 app.handle_action(Action::NextItem);
9624 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
9627 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
9630 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
9634 assert_eq!(app.s3_state.selected_row, 3);
9635 }
9636
9637 #[test]
9638 fn test_s3_bucket_navigation_with_nested_expansion() {
9639 let mut app = test_app();
9640 app.current_service = Service::S3Buckets;
9641 app.service_selected = true;
9642 app.mode = Mode::Normal;
9643
9644 app.s3_state.buckets.items = vec![S3Bucket {
9646 name: "bucket1".to_string(),
9647 region: "us-east-1".to_string(),
9648 creation_date: "2024-01-01T00:00:00Z".to_string(),
9649 }];
9650
9651 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9653 app.s3_state.bucket_preview.insert(
9654 "bucket1".to_string(),
9655 vec![S3Object {
9656 key: "folder/".to_string(),
9657 size: 0,
9658 last_modified: "2024-01-01T00:00:00Z".to_string(),
9659 is_prefix: true,
9660 storage_class: String::new(),
9661 }],
9662 );
9663
9664 app.s3_state.expanded_prefixes.insert("folder/".to_string());
9666 app.s3_state.prefix_preview.insert(
9667 "folder/".to_string(),
9668 vec![
9669 S3Object {
9670 key: "folder/file1.txt".to_string(),
9671 size: 100,
9672 last_modified: "2024-01-01T00:00:00Z".to_string(),
9673 is_prefix: false,
9674 storage_class: "STANDARD".to_string(),
9675 },
9676 S3Object {
9677 key: "folder/file2.txt".to_string(),
9678 size: 200,
9679 last_modified: "2024-01-01T00:00:00Z".to_string(),
9680 is_prefix: false,
9681 storage_class: "STANDARD".to_string(),
9682 },
9683 ],
9684 );
9685
9686 app.s3_state.selected_row = 0;
9687
9688 app.handle_action(Action::NextItem);
9690 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
9693 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
9696 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
9700 assert_eq!(app.s3_state.selected_row, 3);
9701 }
9702
9703 #[test]
9704 fn test_calculate_total_bucket_rows() {
9705 let mut app = test_app();
9706
9707 assert_eq!(app.calculate_total_bucket_rows(), 0);
9709
9710 app.s3_state.buckets.items = vec![
9712 S3Bucket {
9713 name: "bucket1".to_string(),
9714 region: "us-east-1".to_string(),
9715 creation_date: "2024-01-01T00:00:00Z".to_string(),
9716 },
9717 S3Bucket {
9718 name: "bucket2".to_string(),
9719 region: "us-east-1".to_string(),
9720 creation_date: "2024-01-02T00:00:00Z".to_string(),
9721 },
9722 ];
9723 assert_eq!(app.calculate_total_bucket_rows(), 2);
9724
9725 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9727 app.s3_state.bucket_preview.insert(
9728 "bucket1".to_string(),
9729 vec![
9730 S3Object {
9731 key: "file1.txt".to_string(),
9732 size: 100,
9733 last_modified: "2024-01-01T00:00:00Z".to_string(),
9734 is_prefix: false,
9735 storage_class: "STANDARD".to_string(),
9736 },
9737 S3Object {
9738 key: "file2.txt".to_string(),
9739 size: 200,
9740 last_modified: "2024-01-01T00:00:00Z".to_string(),
9741 is_prefix: false,
9742 storage_class: "STANDARD".to_string(),
9743 },
9744 S3Object {
9745 key: "folder/".to_string(),
9746 size: 0,
9747 last_modified: "2024-01-01T00:00:00Z".to_string(),
9748 is_prefix: true,
9749 storage_class: String::new(),
9750 },
9751 ],
9752 );
9753 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
9757 app.s3_state.prefix_preview.insert(
9758 "folder/".to_string(),
9759 vec![
9760 S3Object {
9761 key: "folder/nested1.txt".to_string(),
9762 size: 50,
9763 last_modified: "2024-01-01T00:00:00Z".to_string(),
9764 is_prefix: false,
9765 storage_class: "STANDARD".to_string(),
9766 },
9767 S3Object {
9768 key: "folder/nested2.txt".to_string(),
9769 size: 75,
9770 last_modified: "2024-01-01T00:00:00Z".to_string(),
9771 is_prefix: false,
9772 storage_class: "STANDARD".to_string(),
9773 },
9774 ],
9775 );
9776 assert_eq!(app.calculate_total_bucket_rows(), 7); }
9778
9779 #[test]
9780 fn test_calculate_total_object_rows() {
9781 let mut app = test_app();
9782 app.s3_state.current_bucket = Some("test-bucket".to_string());
9783
9784 assert_eq!(app.calculate_total_object_rows(), 0);
9786
9787 app.s3_state.objects = vec![
9789 S3Object {
9790 key: "file1.txt".to_string(),
9791 size: 100,
9792 last_modified: "2024-01-01T00:00:00Z".to_string(),
9793 is_prefix: false,
9794 storage_class: "STANDARD".to_string(),
9795 },
9796 S3Object {
9797 key: "folder/".to_string(),
9798 size: 0,
9799 last_modified: "2024-01-01T00:00:00Z".to_string(),
9800 is_prefix: true,
9801 storage_class: String::new(),
9802 },
9803 ];
9804 assert_eq!(app.calculate_total_object_rows(), 2);
9805
9806 app.s3_state.expanded_prefixes.insert("folder/".to_string());
9808 app.s3_state.prefix_preview.insert(
9809 "folder/".to_string(),
9810 vec![
9811 S3Object {
9812 key: "folder/file2.txt".to_string(),
9813 size: 200,
9814 last_modified: "2024-01-01T00:00:00Z".to_string(),
9815 is_prefix: false,
9816 storage_class: "STANDARD".to_string(),
9817 },
9818 S3Object {
9819 key: "folder/subfolder/".to_string(),
9820 size: 0,
9821 last_modified: "2024-01-01T00:00:00Z".to_string(),
9822 is_prefix: true,
9823 storage_class: String::new(),
9824 },
9825 ],
9826 );
9827 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
9831 .expanded_prefixes
9832 .insert("folder/subfolder/".to_string());
9833 app.s3_state.prefix_preview.insert(
9834 "folder/subfolder/".to_string(),
9835 vec![S3Object {
9836 key: "folder/subfolder/deep.txt".to_string(),
9837 size: 50,
9838 last_modified: "2024-01-01T00:00:00Z".to_string(),
9839 is_prefix: false,
9840 storage_class: "STANDARD".to_string(),
9841 }],
9842 );
9843 assert_eq!(app.calculate_total_object_rows(), 5); }
9845
9846 #[test]
9847 fn test_s3_object_navigation_with_deep_nesting() {
9848 let mut app = test_app();
9849 app.current_service = Service::S3Buckets;
9850 app.service_selected = true;
9851 app.mode = Mode::Normal;
9852 app.s3_state.current_bucket = Some("test-bucket".to_string());
9853
9854 app.s3_state.objects = vec![S3Object {
9856 key: "folder1/".to_string(),
9857 size: 0,
9858 last_modified: "2024-01-01T00:00:00Z".to_string(),
9859 is_prefix: true,
9860 storage_class: String::new(),
9861 }];
9862
9863 app.s3_state
9865 .expanded_prefixes
9866 .insert("folder1/".to_string());
9867 app.s3_state.prefix_preview.insert(
9868 "folder1/".to_string(),
9869 vec![S3Object {
9870 key: "folder1/folder2/".to_string(),
9871 size: 0,
9872 last_modified: "2024-01-01T00:00:00Z".to_string(),
9873 is_prefix: true,
9874 storage_class: String::new(),
9875 }],
9876 );
9877
9878 app.s3_state
9880 .expanded_prefixes
9881 .insert("folder1/folder2/".to_string());
9882 app.s3_state.prefix_preview.insert(
9883 "folder1/folder2/".to_string(),
9884 vec![S3Object {
9885 key: "folder1/folder2/file.txt".to_string(),
9886 size: 100,
9887 last_modified: "2024-01-01T00:00:00Z".to_string(),
9888 is_prefix: false,
9889 storage_class: "STANDARD".to_string(),
9890 }],
9891 );
9892
9893 app.s3_state.selected_object = 0;
9894
9895 app.handle_action(Action::NextItem);
9897 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
9900 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
9904 assert_eq!(app.s3_state.selected_object, 2);
9905 }
9906
9907 #[test]
9908 fn test_s3_expand_nested_folder_in_objects_view() {
9909 let mut app = test_app();
9910 app.current_service = Service::S3Buckets;
9911 app.service_selected = true;
9912 app.mode = Mode::Normal;
9913 app.s3_state.current_bucket = Some("test-bucket".to_string());
9914
9915 app.s3_state.objects = vec![S3Object {
9917 key: "parent/".to_string(),
9918 size: 0,
9919 last_modified: "2024-01-01T00:00:00Z".to_string(),
9920 is_prefix: true,
9921 storage_class: String::new(),
9922 }];
9923
9924 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9926 app.s3_state.prefix_preview.insert(
9927 "parent/".to_string(),
9928 vec![S3Object {
9929 key: "parent/child/".to_string(),
9930 size: 0,
9931 last_modified: "2024-01-01T00:00:00Z".to_string(),
9932 is_prefix: true,
9933 storage_class: String::new(),
9934 }],
9935 );
9936
9937 app.s3_state.selected_object = 1;
9939
9940 app.handle_action(Action::NextPane);
9942
9943 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
9945 assert!(app.s3_state.buckets.loading); }
9947
9948 #[test]
9949 fn test_s3_drill_into_nested_folder() {
9950 let mut app = test_app();
9951 app.current_service = Service::S3Buckets;
9952 app.service_selected = true;
9953 app.mode = Mode::Normal;
9954 app.s3_state.current_bucket = Some("test-bucket".to_string());
9955
9956 app.s3_state.objects = vec![S3Object {
9958 key: "parent/".to_string(),
9959 size: 0,
9960 last_modified: "2024-01-01T00:00:00Z".to_string(),
9961 is_prefix: true,
9962 storage_class: String::new(),
9963 }];
9964
9965 app.s3_state.expanded_prefixes.insert("parent/".to_string());
9967 app.s3_state.prefix_preview.insert(
9968 "parent/".to_string(),
9969 vec![S3Object {
9970 key: "parent/child/".to_string(),
9971 size: 0,
9972 last_modified: "2024-01-01T00:00:00Z".to_string(),
9973 is_prefix: true,
9974 storage_class: String::new(),
9975 }],
9976 );
9977
9978 app.s3_state.selected_object = 1;
9980
9981 app.handle_action(Action::Select);
9983
9984 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
9986 assert!(app.s3_state.buckets.loading); }
9988
9989 #[test]
9990 fn test_s3_esc_pops_navigation_stack() {
9991 let mut app = test_app();
9992 app.current_service = Service::S3Buckets;
9993 app.s3_state.current_bucket = Some("test-bucket".to_string());
9994 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
9995
9996 app.handle_action(Action::GoBack);
9998 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
9999 assert!(app.s3_state.buckets.loading);
10000
10001 app.s3_state.buckets.loading = false;
10003 app.handle_action(Action::GoBack);
10004 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
10005 assert!(app.s3_state.buckets.loading);
10006
10007 app.s3_state.buckets.loading = false;
10009 app.handle_action(Action::GoBack);
10010 assert_eq!(app.s3_state.current_bucket, None);
10011 }
10012
10013 #[test]
10014 fn test_s3_esc_from_bucket_root_exits() {
10015 let mut app = test_app();
10016 app.current_service = Service::S3Buckets;
10017 app.s3_state.current_bucket = Some("test-bucket".to_string());
10018 app.s3_state.prefix_stack = vec![];
10019
10020 app.handle_action(Action::GoBack);
10022 assert_eq!(app.s3_state.current_bucket, None);
10023 assert_eq!(app.s3_state.objects.len(), 0);
10024 }
10025
10026 #[test]
10027 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
10028 let mut app = test_app();
10029 app.current_service = Service::S3Buckets;
10030 app.service_selected = true;
10031 app.mode = Mode::Normal;
10032
10033 app.s3_state.buckets.items = vec![S3Bucket {
10035 name: "test-bucket".to_string(),
10036 region: "us-east-1".to_string(),
10037 creation_date: "2024-01-01".to_string(),
10038 }];
10039
10040 app.s3_state
10042 .expanded_prefixes
10043 .insert("test-bucket".to_string());
10044 app.s3_state.bucket_preview.insert(
10045 "test-bucket".to_string(),
10046 vec![S3Object {
10047 key: "parent/".to_string(),
10048 size: 0,
10049 last_modified: "2024-01-01".to_string(),
10050 is_prefix: true,
10051 storage_class: String::new(),
10052 }],
10053 );
10054
10055 app.s3_state.expanded_prefixes.insert("parent/".to_string());
10057 app.s3_state.prefix_preview.insert(
10058 "parent/".to_string(),
10059 vec![S3Object {
10060 key: "parent/child/".to_string(),
10061 size: 0,
10062 last_modified: "2024-01-01".to_string(),
10063 is_prefix: true,
10064 storage_class: String::new(),
10065 }],
10066 );
10067
10068 app.s3_state.selected_row = 2;
10070
10071 app.handle_action(Action::Select);
10073
10074 assert_eq!(
10076 app.s3_state.prefix_stack,
10077 vec!["parent/".to_string(), "parent/child/".to_string()]
10078 );
10079 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
10080 assert!(app.s3_state.buckets.loading);
10081
10082 app.s3_state.buckets.loading = false;
10084 app.handle_action(Action::GoBack);
10085 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
10086 assert!(app.s3_state.buckets.loading);
10087
10088 app.s3_state.buckets.loading = false;
10090 app.handle_action(Action::GoBack);
10091 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
10092 assert!(app.s3_state.buckets.loading);
10093
10094 app.s3_state.buckets.loading = false;
10096 app.handle_action(Action::GoBack);
10097 assert_eq!(app.s3_state.current_bucket, None);
10098 }
10099
10100 #[test]
10101 fn test_region_picker_fuzzy_filter() {
10102 let mut app = test_app();
10103 app.region_latencies.insert("us-east-1".to_string(), 10);
10104 app.region_filter = "vir".to_string();
10105 let filtered = app.get_filtered_regions();
10106 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
10107 }
10108
10109 #[test]
10110 fn test_profile_picker_loads_profiles() {
10111 let profiles = App::load_aws_profiles();
10112 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
10114 }
10115
10116 #[test]
10117 fn test_profile_with_region_uses_it() {
10118 let mut app = test_app_no_region();
10119 app.available_profiles = vec![AwsProfile {
10120 name: "test-profile".to_string(),
10121 region: Some("eu-west-1".to_string()),
10122 account: Some("123456789".to_string()),
10123 role_arn: None,
10124 source_profile: None,
10125 }];
10126 app.profile_picker_selected = 0;
10127 app.mode = Mode::ProfilePicker;
10128
10129 let filtered = app.get_filtered_profiles();
10131 if let Some(profile) = filtered.first() {
10132 let profile_name = profile.name.clone();
10133 let profile_region = profile.region.clone();
10134
10135 app.profile = profile_name;
10136 if let Some(region) = profile_region {
10137 app.region = region;
10138 }
10139 }
10140
10141 assert_eq!(app.profile, "test-profile");
10142 assert_eq!(app.region, "eu-west-1");
10143 }
10144
10145 #[test]
10146 fn test_profile_without_region_keeps_unknown() {
10147 let mut app = test_app_no_region();
10148 let initial_region = app.region.clone();
10149
10150 app.available_profiles = vec![AwsProfile {
10151 name: "test-profile".to_string(),
10152 region: None,
10153 account: None,
10154 role_arn: None,
10155 source_profile: None,
10156 }];
10157 app.profile_picker_selected = 0;
10158 app.mode = Mode::ProfilePicker;
10159
10160 let filtered = app.get_filtered_profiles();
10161 if let Some(profile) = filtered.first() {
10162 let profile_name = profile.name.clone();
10163 let profile_region = profile.region.clone();
10164
10165 app.profile = profile_name;
10166 if let Some(region) = profile_region {
10167 app.region = region;
10168 }
10169 }
10170
10171 assert_eq!(app.profile, "test-profile");
10172 assert_eq!(app.region, initial_region); }
10174
10175 #[test]
10176 fn test_region_selection_closes_all_tabs() {
10177 let mut app = test_app();
10178
10179 app.tabs.push(Tab {
10181 service: Service::CloudWatchLogGroups,
10182 title: "CloudWatch".to_string(),
10183 breadcrumb: "CloudWatch".to_string(),
10184 });
10185 app.tabs.push(Tab {
10186 service: Service::S3Buckets,
10187 title: "S3".to_string(),
10188 breadcrumb: "S3".to_string(),
10189 });
10190 app.service_selected = true;
10191 app.current_tab = 1;
10192
10193 app.region_latencies.insert("eu-west-1".to_string(), 50);
10195
10196 app.mode = Mode::RegionPicker;
10198 app.region_picker_selected = 0;
10199
10200 let filtered = app.get_filtered_regions();
10201 if let Some(region) = filtered.first() {
10202 app.region = region.code.to_string();
10203 app.tabs.clear();
10204 app.current_tab = 0;
10205 app.service_selected = false;
10206 app.mode = Mode::Normal;
10207 }
10208
10209 assert_eq!(app.tabs.len(), 0);
10210 assert_eq!(app.current_tab, 0);
10211 assert!(!app.service_selected);
10212 assert_eq!(app.region, "eu-west-1");
10213 }
10214
10215 #[test]
10216 fn test_region_picker_can_be_closed_without_selection() {
10217 let mut app = test_app();
10218 let initial_region = app.region.clone();
10219
10220 app.mode = Mode::RegionPicker;
10221
10222 app.mode = Mode::Normal;
10224
10225 assert_eq!(app.region, initial_region);
10227 }
10228
10229 #[test]
10230 fn test_session_filter_works() {
10231 let mut app = test_app();
10232
10233 app.sessions = vec![
10234 Session {
10235 id: "1".to_string(),
10236 timestamp: "2024-01-01".to_string(),
10237 profile: "prod-profile".to_string(),
10238 region: "us-east-1".to_string(),
10239 account_id: "123456789".to_string(),
10240 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
10241 tabs: vec![],
10242 },
10243 Session {
10244 id: "2".to_string(),
10245 timestamp: "2024-01-02".to_string(),
10246 profile: "dev-profile".to_string(),
10247 region: "eu-west-1".to_string(),
10248 account_id: "987654321".to_string(),
10249 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
10250 tabs: vec![],
10251 },
10252 ];
10253
10254 app.session_filter = "prod".to_string();
10256 let filtered = app.get_filtered_sessions();
10257 assert_eq!(filtered.len(), 1);
10258 assert_eq!(filtered[0].profile, "prod-profile");
10259
10260 app.session_filter = "eu".to_string();
10262 let filtered = app.get_filtered_sessions();
10263 assert_eq!(filtered.len(), 1);
10264 assert_eq!(filtered[0].region, "eu-west-1");
10265
10266 app.session_filter.clear();
10268 let filtered = app.get_filtered_sessions();
10269 assert_eq!(filtered.len(), 2);
10270 }
10271
10272 #[test]
10273 fn test_profile_picker_shows_account() {
10274 let mut app = test_app_no_region();
10275 app.available_profiles = vec![AwsProfile {
10276 name: "test-profile".to_string(),
10277 region: Some("us-east-1".to_string()),
10278 account: Some("123456789".to_string()),
10279 role_arn: None,
10280 source_profile: None,
10281 }];
10282
10283 let filtered = app.get_filtered_profiles();
10284 assert_eq!(filtered.len(), 1);
10285 assert_eq!(filtered[0].account, Some("123456789".to_string()));
10286 }
10287
10288 #[test]
10289 fn test_profile_without_account() {
10290 let mut app = test_app_no_region();
10291 app.available_profiles = vec![AwsProfile {
10292 name: "test-profile".to_string(),
10293 region: Some("us-east-1".to_string()),
10294 account: None,
10295 role_arn: None,
10296 source_profile: None,
10297 }];
10298
10299 let filtered = app.get_filtered_profiles();
10300 assert_eq!(filtered.len(), 1);
10301 assert_eq!(filtered[0].account, None);
10302 }
10303
10304 #[test]
10305 fn test_profile_with_all_fields() {
10306 let mut app = test_app_no_region();
10307 app.available_profiles = vec![AwsProfile {
10308 name: "prod-profile".to_string(),
10309 region: Some("us-west-2".to_string()),
10310 account: Some("123456789".to_string()),
10311 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
10312 source_profile: Some("base-profile".to_string()),
10313 }];
10314
10315 let filtered = app.get_filtered_profiles();
10316 assert_eq!(filtered.len(), 1);
10317 assert_eq!(filtered[0].name, "prod-profile");
10318 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
10319 assert_eq!(filtered[0].account, Some("123456789".to_string()));
10320 assert_eq!(
10321 filtered[0].role_arn,
10322 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
10323 );
10324 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
10325 }
10326
10327 #[test]
10328 fn test_profile_filter_by_source_profile() {
10329 let mut app = test_app_no_region();
10330 app.available_profiles = vec![
10331 AwsProfile {
10332 name: "profile1".to_string(),
10333 region: None,
10334 account: None,
10335 role_arn: None,
10336 source_profile: Some("base".to_string()),
10337 },
10338 AwsProfile {
10339 name: "profile2".to_string(),
10340 region: None,
10341 account: None,
10342 role_arn: None,
10343 source_profile: Some("other".to_string()),
10344 },
10345 ];
10346
10347 app.profile_filter = "base".to_string();
10348 let filtered = app.get_filtered_profiles();
10349 assert_eq!(filtered.len(), 1);
10350 assert_eq!(filtered[0].name, "profile1");
10351 }
10352
10353 #[test]
10354 fn test_profile_filter_by_role() {
10355 let mut app = test_app_no_region();
10356 app.available_profiles = vec![
10357 AwsProfile {
10358 name: "admin-profile".to_string(),
10359 region: None,
10360 account: None,
10361 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
10362 source_profile: None,
10363 },
10364 AwsProfile {
10365 name: "dev-profile".to_string(),
10366 region: None,
10367 account: None,
10368 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
10369 source_profile: None,
10370 },
10371 ];
10372
10373 app.profile_filter = "Admin".to_string();
10374 let filtered = app.get_filtered_profiles();
10375 assert_eq!(filtered.len(), 1);
10376 assert_eq!(filtered[0].name, "admin-profile");
10377 }
10378
10379 #[test]
10380 fn test_profiles_sorted_by_name() {
10381 let mut app = test_app_no_region();
10382 app.available_profiles = vec![
10383 AwsProfile {
10384 name: "zebra-profile".to_string(),
10385 region: None,
10386 account: None,
10387 role_arn: None,
10388 source_profile: None,
10389 },
10390 AwsProfile {
10391 name: "alpha-profile".to_string(),
10392 region: None,
10393 account: None,
10394 role_arn: None,
10395 source_profile: None,
10396 },
10397 AwsProfile {
10398 name: "beta-profile".to_string(),
10399 region: None,
10400 account: None,
10401 role_arn: None,
10402 source_profile: None,
10403 },
10404 ];
10405
10406 let filtered = app.get_filtered_profiles();
10407 assert_eq!(filtered.len(), 3);
10408 assert_eq!(filtered[0].name, "alpha-profile");
10409 assert_eq!(filtered[1].name, "beta-profile");
10410 assert_eq!(filtered[2].name, "zebra-profile");
10411 }
10412
10413 #[test]
10414 fn test_profile_with_role_arn() {
10415 let mut app = test_app_no_region();
10416 app.available_profiles = vec![AwsProfile {
10417 name: "role-profile".to_string(),
10418 region: Some("us-east-1".to_string()),
10419 account: Some("123456789".to_string()),
10420 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
10421 source_profile: None,
10422 }];
10423
10424 let filtered = app.get_filtered_profiles();
10425 assert_eq!(filtered.len(), 1);
10426 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
10427 }
10428
10429 #[test]
10430 fn test_profile_with_user_arn() {
10431 let mut app = test_app_no_region();
10432 app.available_profiles = vec![AwsProfile {
10433 name: "user-profile".to_string(),
10434 region: Some("us-east-1".to_string()),
10435 account: Some("123456789".to_string()),
10436 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
10437 source_profile: None,
10438 }];
10439
10440 let filtered = app.get_filtered_profiles();
10441 assert_eq!(filtered.len(), 1);
10442 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
10443 }
10444
10445 #[test]
10446 fn test_filtered_profiles_also_sorted() {
10447 let mut app = test_app_no_region();
10448 app.available_profiles = vec![
10449 AwsProfile {
10450 name: "prod-zebra".to_string(),
10451 region: Some("us-east-1".to_string()),
10452 account: None,
10453 role_arn: None,
10454 source_profile: None,
10455 },
10456 AwsProfile {
10457 name: "prod-alpha".to_string(),
10458 region: Some("us-east-1".to_string()),
10459 account: None,
10460 role_arn: None,
10461 source_profile: None,
10462 },
10463 AwsProfile {
10464 name: "dev-profile".to_string(),
10465 region: Some("us-west-2".to_string()),
10466 account: None,
10467 role_arn: None,
10468 source_profile: None,
10469 },
10470 ];
10471
10472 app.profile_filter = "prod".to_string();
10473 let filtered = app.get_filtered_profiles();
10474 assert_eq!(filtered.len(), 2);
10475 assert_eq!(filtered[0].name, "prod-alpha");
10476 assert_eq!(filtered[1].name, "prod-zebra");
10477 }
10478
10479 #[test]
10480 fn test_profile_picker_has_all_columns() {
10481 let mut app = test_app_no_region();
10482 app.available_profiles = vec![AwsProfile {
10483 name: "test".to_string(),
10484 region: Some("us-east-1".to_string()),
10485 account: Some("123456789".to_string()),
10486 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
10487 source_profile: Some("base".to_string()),
10488 }];
10489
10490 let filtered = app.get_filtered_profiles();
10491 assert_eq!(filtered.len(), 1);
10492 assert!(filtered[0].name == "test");
10493 assert!(filtered[0].region.is_some());
10494 assert!(filtered[0].account.is_some());
10495 assert!(filtered[0].role_arn.is_some());
10496 assert!(filtered[0].source_profile.is_some());
10497 }
10498
10499 #[test]
10500 fn test_session_picker_shows_tab_count() {
10501 let mut app = test_app_no_region();
10502 app.sessions = vec![Session {
10503 id: "1".to_string(),
10504 timestamp: "2024-01-01".to_string(),
10505 profile: "test".to_string(),
10506 region: "us-east-1".to_string(),
10507 account_id: "123".to_string(),
10508 role_arn: String::new(),
10509 tabs: vec![
10510 SessionTab {
10511 service: "CloudWatch".to_string(),
10512 title: "Logs".to_string(),
10513 breadcrumb: String::new(),
10514 filter: None,
10515 selected_item: None,
10516 },
10517 SessionTab {
10518 service: "S3".to_string(),
10519 title: "Buckets".to_string(),
10520 breadcrumb: String::new(),
10521 filter: None,
10522 selected_item: None,
10523 },
10524 ],
10525 }];
10526
10527 let filtered = app.get_filtered_sessions();
10528 assert_eq!(filtered.len(), 1);
10529 assert_eq!(filtered[0].tabs.len(), 2);
10530 }
10531
10532 #[test]
10533 fn test_start_background_data_fetch_loads_profiles() {
10534 let mut app = test_app_no_region();
10535 assert!(app.available_profiles.is_empty());
10536
10537 app.available_profiles = App::load_aws_profiles();
10539
10540 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10542 }
10543
10544 #[test]
10545 fn test_refresh_in_profile_picker() {
10546 let mut app = test_app_no_region();
10547 app.mode = Mode::ProfilePicker;
10548 app.available_profiles = vec![AwsProfile {
10549 name: "test".to_string(),
10550 region: None,
10551 account: None,
10552 role_arn: None,
10553 source_profile: None,
10554 }];
10555
10556 app.handle_action(Action::Refresh);
10557
10558 assert!(app.log_groups_state.loading);
10560 assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
10561 }
10562
10563 #[test]
10564 fn test_refresh_sets_loading_for_profile_picker() {
10565 let mut app = test_app_no_region();
10566 app.mode = Mode::ProfilePicker;
10567
10568 assert!(!app.log_groups_state.loading);
10569
10570 app.handle_action(Action::Refresh);
10571
10572 assert!(app.log_groups_state.loading);
10573 }
10574
10575 #[test]
10576 fn test_profiles_loaded_on_demand() {
10577 let mut app = test_app_no_region();
10578
10579 assert!(app.available_profiles.is_empty());
10581
10582 app.available_profiles = App::load_aws_profiles();
10584
10585 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10587 }
10588
10589 #[test]
10590 fn test_profile_accounts_not_fetched_automatically() {
10591 let mut app = test_app_no_region();
10592 app.available_profiles = App::load_aws_profiles();
10593
10594 for profile in &app.available_profiles {
10596 assert!(profile.account.is_none() || profile.account.is_some());
10599 }
10600 }
10601
10602 #[test]
10603 fn test_ctrl_r_triggers_account_fetch() {
10604 let mut app = test_app_no_region();
10605 app.mode = Mode::ProfilePicker;
10606 app.available_profiles = vec![AwsProfile {
10607 name: "test".to_string(),
10608 region: Some("us-east-1".to_string()),
10609 account: None,
10610 role_arn: None,
10611 source_profile: None,
10612 }];
10613
10614 assert!(app.available_profiles[0].account.is_none());
10616
10617 app.handle_action(Action::Refresh);
10619
10620 assert!(app.log_groups_state.loading);
10622 }
10623
10624 #[test]
10625 fn test_refresh_in_region_picker() {
10626 let mut app = test_app_no_region();
10627 app.mode = Mode::RegionPicker;
10628
10629 let initial_latencies = app.region_latencies.len();
10630 app.handle_action(Action::Refresh);
10631
10632 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
10634 }
10635
10636 #[test]
10637 fn test_refresh_in_session_picker() {
10638 let mut app = test_app_no_region();
10639 app.mode = Mode::SessionPicker;
10640 app.sessions = vec![];
10641
10642 app.handle_action(Action::Refresh);
10643
10644 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
10646 }
10647
10648 #[test]
10649 fn test_session_picker_selection() {
10650 let mut app = test_app();
10651
10652 app.sessions = vec![Session {
10653 id: "1".to_string(),
10654 timestamp: "2024-01-01".to_string(),
10655 profile: "prod-profile".to_string(),
10656 region: "us-west-2".to_string(),
10657 account_id: "123456789".to_string(),
10658 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
10659 tabs: vec![SessionTab {
10660 service: "CloudWatchLogGroups".to_string(),
10661 title: "Log Groups".to_string(),
10662 breadcrumb: "CloudWatch › Log Groups".to_string(),
10663 filter: Some("test".to_string()),
10664 selected_item: None,
10665 }],
10666 }];
10667
10668 app.mode = Mode::SessionPicker;
10669 app.session_picker_selected = 0;
10670
10671 app.handle_action(Action::Select);
10673
10674 assert_eq!(app.mode, Mode::Normal);
10675 assert_eq!(app.profile, "prod-profile");
10676 assert_eq!(app.region, "us-west-2");
10677 assert_eq!(app.config.account_id, "123456789");
10678 assert_eq!(app.tabs.len(), 1);
10679 assert_eq!(app.tabs[0].title, "Log Groups");
10680 }
10681
10682 #[test]
10683 fn test_save_session_creates_session() {
10684 let mut app =
10685 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10686 app.config.account_id = "123456789".to_string();
10687 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10688
10689 app.tabs.push(Tab {
10690 service: Service::CloudWatchLogGroups,
10691 title: "Log Groups".to_string(),
10692 breadcrumb: "CloudWatch › Log Groups".to_string(),
10693 });
10694
10695 app.save_current_session();
10696
10697 assert!(app.current_session.is_some());
10698 let session = app.current_session.clone().unwrap();
10699 assert_eq!(session.profile, "test-profile");
10700 assert_eq!(session.region, "us-east-1");
10701 assert_eq!(session.account_id, "123456789");
10702 assert_eq!(session.tabs.len(), 1);
10703
10704 let _ = session.delete();
10706 }
10707
10708 #[test]
10709 fn test_save_session_updates_existing() {
10710 let mut app =
10711 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10712 app.config.account_id = "123456789".to_string();
10713 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10714
10715 app.current_session = Some(Session {
10716 id: "existing".to_string(),
10717 timestamp: "2024-01-01".to_string(),
10718 profile: "test-profile".to_string(),
10719 region: "us-east-1".to_string(),
10720 account_id: "123456789".to_string(),
10721 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10722 tabs: vec![],
10723 });
10724
10725 app.tabs.push(Tab {
10726 service: Service::CloudWatchLogGroups,
10727 title: "Log Groups".to_string(),
10728 breadcrumb: "CloudWatch › Log Groups".to_string(),
10729 });
10730
10731 app.save_current_session();
10732
10733 let session = app.current_session.clone().unwrap();
10734 assert_eq!(session.id, "existing");
10735 assert_eq!(session.tabs.len(), 1);
10736
10737 let _ = session.delete();
10739 }
10740
10741 #[test]
10742 fn test_save_session_skips_empty_tabs() {
10743 let mut app =
10744 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10745 app.config.account_id = "123456789".to_string();
10746
10747 app.save_current_session();
10748
10749 assert!(app.current_session.is_none());
10750 }
10751
10752 #[test]
10753 fn test_save_session_deletes_when_tabs_closed() {
10754 let mut app =
10755 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10756 app.config.account_id = "123456789".to_string();
10757 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10758
10759 app.current_session = Some(Session {
10761 id: "test_delete".to_string(),
10762 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
10763 profile: "test-profile".to_string(),
10764 region: "us-east-1".to_string(),
10765 account_id: "123456789".to_string(),
10766 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10767 tabs: vec![],
10768 });
10769
10770 app.save_current_session();
10772
10773 assert!(app.current_session.is_none());
10774 }
10775
10776 #[test]
10777 fn test_closing_all_tabs_deletes_session() {
10778 let mut app =
10779 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10780 app.config.account_id = "123456789".to_string();
10781 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10782
10783 app.tabs.push(Tab {
10785 service: Service::CloudWatchLogGroups,
10786 title: "Log Groups".to_string(),
10787 breadcrumb: "CloudWatch › Log Groups".to_string(),
10788 });
10789
10790 app.save_current_session();
10792 assert!(app.current_session.is_some());
10793 let session_id = app.current_session.as_ref().unwrap().id.clone();
10794
10795 app.tabs.clear();
10797
10798 app.save_current_session();
10800 assert!(app.current_session.is_none());
10801
10802 let _ = Session::load(&session_id).map(|s| s.delete());
10804 }
10805
10806 #[test]
10807 fn test_credential_error_opens_profile_picker() {
10808 let mut app = App::new_without_client("default".to_string(), None);
10810 let error_str = "Unable to load credentials from any source";
10811
10812 if error_str.contains("credentials") {
10813 app.available_profiles = App::load_aws_profiles();
10814 app.mode = Mode::ProfilePicker;
10815 }
10816
10817 assert_eq!(app.mode, Mode::ProfilePicker);
10818 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10820 }
10821
10822 #[test]
10823 fn test_non_credential_error_shows_error_modal() {
10824 let mut app = App::new_without_client("default".to_string(), None);
10825 let error_str = "Network timeout";
10826
10827 if !error_str.contains("credentials") {
10828 app.error_message = Some(error_str.to_string());
10829 app.mode = Mode::ErrorModal;
10830 }
10831
10832 assert_eq!(app.mode, Mode::ErrorModal);
10833 assert!(app.error_message.is_some());
10834 }
10835
10836 #[tokio::test]
10837 async fn test_profile_selection_loads_credentials() {
10838 std::env::set_var("AWS_PROFILE", "default");
10840
10841 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
10843
10844 if let Ok(app) = result {
10845 assert!(!app.config.account_id.is_empty());
10847 assert!(!app.config.role_arn.is_empty());
10848 assert_eq!(app.profile, "default");
10849 assert_eq!(app.config.region, "us-east-1");
10850 }
10851 }
10853
10854 #[test]
10855 fn test_new_app_shows_service_picker_with_no_tabs() {
10856 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
10857
10858 assert!(!app.service_selected);
10860 assert_eq!(app.mode, Mode::ServicePicker);
10862 assert!(app.tabs.is_empty());
10864 }
10865
10866 #[tokio::test]
10867 async fn test_aws_profile_env_var_read_before_config_load() {
10868 std::env::set_var("AWS_PROFILE", "test-profile");
10870
10871 let profile_name = None
10873 .or_else(|| std::env::var("AWS_PROFILE").ok())
10874 .unwrap_or_else(|| "default".to_string());
10875
10876 assert_eq!(profile_name, "test-profile");
10878
10879 std::env::set_var("AWS_PROFILE", &profile_name);
10881
10882 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
10884
10885 std::env::remove_var("AWS_PROFILE");
10886 }
10887
10888 #[test]
10889 fn test_next_preferences_cloudformation() {
10890 let mut app = test_app();
10891 app.current_service = Service::CloudFormationStacks;
10892 app.mode = Mode::ColumnSelector;
10893 app.column_selector_index = 0;
10894
10895 let page_size_idx = app.cfn_column_ids.len() + 2;
10897 app.handle_action(Action::NextPreferences);
10898 assert_eq!(app.column_selector_index, page_size_idx);
10899
10900 app.handle_action(Action::NextPreferences);
10902 assert_eq!(app.column_selector_index, 0);
10903 }
10904
10905 #[test]
10906 fn test_s3_preferences_tab_cycling() {
10907 let mut app = test_app();
10908 app.current_service = Service::S3Buckets;
10909 app.mode = Mode::ColumnSelector;
10910 app.column_selector_index = 0;
10911
10912 let page_size_idx = app.s3_bucket_column_ids.len() + 2;
10913
10914 app.handle_action(Action::NextPreferences);
10916 assert_eq!(app.column_selector_index, page_size_idx);
10917
10918 app.handle_action(Action::NextPreferences);
10920 assert_eq!(app.column_selector_index, 0);
10921
10922 app.handle_action(Action::PrevPreferences);
10924 assert_eq!(app.column_selector_index, page_size_idx);
10925
10926 app.handle_action(Action::PrevPreferences);
10928 assert_eq!(app.column_selector_index, 0);
10929 }
10930
10931 #[test]
10932 fn test_s3_filter_resets_selection() {
10933 let mut app = test_app();
10934 app.current_service = Service::S3Buckets;
10935 app.service_selected = true;
10936
10937 app.s3_state.buckets.items = vec![
10939 S3Bucket {
10940 name: "bucket-1".to_string(),
10941 region: "us-east-1".to_string(),
10942 creation_date: "2023-01-01".to_string(),
10943 },
10944 S3Bucket {
10945 name: "bucket-2".to_string(),
10946 region: "us-east-1".to_string(),
10947 creation_date: "2023-01-02".to_string(),
10948 },
10949 S3Bucket {
10950 name: "other-bucket".to_string(),
10951 region: "us-east-1".to_string(),
10952 creation_date: "2023-01-03".to_string(),
10953 },
10954 ];
10955
10956 app.s3_state.selected_row = 1;
10958 app.s3_state.bucket_scroll_offset = 1;
10959
10960 app.mode = Mode::FilterInput;
10962 app.apply_filter_operation(|f| f.push_str("bucket-"));
10963
10964 assert_eq!(app.s3_state.selected_row, 0);
10966 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
10967 assert_eq!(app.s3_state.buckets.filter, "bucket-");
10968 }
10969
10970 #[test]
10971 fn test_s3_navigation_respects_filter() {
10972 let mut app = test_app();
10973 app.current_service = Service::S3Buckets;
10974 app.service_selected = true;
10975 app.mode = Mode::Normal;
10976
10977 app.s3_state.buckets.items = vec![
10979 S3Bucket {
10980 name: "prod-bucket".to_string(),
10981 region: "us-east-1".to_string(),
10982 creation_date: "2023-01-01".to_string(),
10983 },
10984 S3Bucket {
10985 name: "dev-bucket".to_string(),
10986 region: "us-east-1".to_string(),
10987 creation_date: "2023-01-02".to_string(),
10988 },
10989 S3Bucket {
10990 name: "prod-logs".to_string(),
10991 region: "us-east-1".to_string(),
10992 creation_date: "2023-01-03".to_string(),
10993 },
10994 ];
10995
10996 app.s3_state.buckets.filter = "prod".to_string();
10998
10999 assert_eq!(app.s3_state.selected_row, 0);
11001
11002 app.handle_action(Action::NextItem);
11004 assert_eq!(app.s3_state.selected_row, 1);
11005
11006 app.handle_action(Action::NextItem);
11008 assert_eq!(app.s3_state.selected_row, 1);
11009
11010 app.handle_action(Action::PrevItem);
11012 assert_eq!(app.s3_state.selected_row, 0);
11013 }
11014
11015 #[test]
11016 fn test_next_preferences_lambda_functions() {
11017 let mut app = test_app();
11018 app.current_service = Service::LambdaFunctions;
11019 app.mode = Mode::ColumnSelector;
11020 app.column_selector_index = 0;
11021
11022 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
11023 app.handle_action(Action::NextPreferences);
11024 assert_eq!(app.column_selector_index, page_size_idx);
11025
11026 app.handle_action(Action::NextPreferences);
11027 assert_eq!(app.column_selector_index, 0);
11028 }
11029
11030 #[test]
11031 fn test_next_preferences_lambda_applications() {
11032 let mut app = test_app();
11033 app.current_service = Service::LambdaApplications;
11034 app.mode = Mode::ColumnSelector;
11035 app.column_selector_index = 0;
11036
11037 let page_size_idx = app.lambda_application_column_ids.len() + 2;
11038 app.handle_action(Action::NextPreferences);
11039 assert_eq!(app.column_selector_index, page_size_idx);
11040
11041 app.handle_action(Action::NextPreferences);
11042 assert_eq!(app.column_selector_index, 0);
11043 }
11044
11045 #[test]
11046 fn test_next_preferences_ecr_images() {
11047 let mut app = test_app();
11048 app.current_service = Service::EcrRepositories;
11049 app.ecr_state.current_repository = Some("test-repo".to_string());
11050 app.mode = Mode::ColumnSelector;
11051 app.column_selector_index = 0;
11052
11053 let page_size_idx = app.ecr_image_column_ids.len() + 2;
11054 app.handle_action(Action::NextPreferences);
11055 assert_eq!(app.column_selector_index, page_size_idx);
11056
11057 app.handle_action(Action::NextPreferences);
11058 assert_eq!(app.column_selector_index, 0);
11059 }
11060
11061 #[test]
11062 fn test_next_preferences_cloudwatch_log_groups() {
11063 let mut app = test_app();
11064 app.current_service = Service::CloudWatchLogGroups;
11065 app.view_mode = ViewMode::List;
11066 app.mode = Mode::ColumnSelector;
11067 app.column_selector_index = 0;
11068
11069 let page_size_idx = app.cw_log_group_column_ids.len() + 2;
11071 app.handle_action(Action::NextPreferences);
11072 assert_eq!(app.column_selector_index, page_size_idx);
11073
11074 app.handle_action(Action::NextPreferences);
11076 assert_eq!(app.column_selector_index, 0);
11077
11078 app.handle_action(Action::PrevPreferences);
11080 assert_eq!(app.column_selector_index, page_size_idx);
11081
11082 app.handle_action(Action::PrevPreferences);
11084 assert_eq!(app.column_selector_index, 0);
11085 }
11086
11087 #[test]
11088 fn test_next_preferences_cloudwatch_log_streams() {
11089 let mut app = test_app();
11090 app.current_service = Service::CloudWatchLogGroups;
11091 app.view_mode = ViewMode::Detail;
11092 app.mode = Mode::ColumnSelector;
11093 app.column_selector_index = 0;
11094
11095 let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
11097 app.handle_action(Action::NextPreferences);
11098 assert_eq!(app.column_selector_index, page_size_idx);
11099
11100 app.handle_action(Action::NextPreferences);
11102 assert_eq!(app.column_selector_index, 0);
11103 }
11104
11105 #[test]
11106 fn test_cloudformation_next_item() {
11107 let mut app = test_app();
11108 app.current_service = Service::CloudFormationStacks;
11109 app.service_selected = true;
11110 app.mode = Mode::Normal;
11111 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11112 app.cfn_state.table.items = vec![
11113 CfnStack {
11114 name: "stack1".to_string(),
11115 stack_id: "id1".to_string(),
11116 status: "CREATE_COMPLETE".to_string(),
11117 created_time: "2024-01-01".to_string(),
11118 updated_time: String::new(),
11119 deleted_time: String::new(),
11120 drift_status: String::new(),
11121 last_drift_check_time: String::new(),
11122 status_reason: String::new(),
11123 description: String::new(),
11124 detailed_status: String::new(),
11125 root_stack: String::new(),
11126 parent_stack: String::new(),
11127 termination_protection: false,
11128 iam_role: String::new(),
11129 tags: Vec::new(),
11130 stack_policy: String::new(),
11131 rollback_monitoring_time: String::new(),
11132 rollback_alarms: Vec::new(),
11133 notification_arns: Vec::new(),
11134 },
11135 CfnStack {
11136 name: "stack2".to_string(),
11137 stack_id: "id2".to_string(),
11138 status: "UPDATE_COMPLETE".to_string(),
11139 created_time: "2024-01-02".to_string(),
11140 updated_time: String::new(),
11141 deleted_time: String::new(),
11142 drift_status: String::new(),
11143 last_drift_check_time: String::new(),
11144 status_reason: String::new(),
11145 description: String::new(),
11146 detailed_status: String::new(),
11147 root_stack: String::new(),
11148 parent_stack: String::new(),
11149 termination_protection: false,
11150 iam_role: String::new(),
11151 tags: Vec::new(),
11152 stack_policy: String::new(),
11153 rollback_monitoring_time: String::new(),
11154 rollback_alarms: Vec::new(),
11155 notification_arns: Vec::new(),
11156 },
11157 ];
11158 app.cfn_state.table.reset();
11159
11160 app.handle_action(Action::NextItem);
11161 assert_eq!(app.cfn_state.table.selected, 1);
11162
11163 app.handle_action(Action::NextItem);
11164 assert_eq!(app.cfn_state.table.selected, 1); }
11166
11167 #[test]
11168 fn test_cloudformation_prev_item() {
11169 let mut app = test_app();
11170 app.current_service = Service::CloudFormationStacks;
11171 app.service_selected = true;
11172 app.mode = Mode::Normal;
11173 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11174 app.cfn_state.table.items = vec![
11175 CfnStack {
11176 name: "stack1".to_string(),
11177 stack_id: "id1".to_string(),
11178 status: "CREATE_COMPLETE".to_string(),
11179 created_time: "2024-01-01".to_string(),
11180 updated_time: String::new(),
11181 deleted_time: String::new(),
11182 drift_status: String::new(),
11183 last_drift_check_time: String::new(),
11184 status_reason: String::new(),
11185 description: String::new(),
11186 detailed_status: String::new(),
11187 root_stack: String::new(),
11188 parent_stack: String::new(),
11189 termination_protection: false,
11190 iam_role: String::new(),
11191 tags: Vec::new(),
11192 stack_policy: String::new(),
11193 rollback_monitoring_time: String::new(),
11194 rollback_alarms: Vec::new(),
11195 notification_arns: Vec::new(),
11196 },
11197 CfnStack {
11198 name: "stack2".to_string(),
11199 stack_id: "id2".to_string(),
11200 status: "UPDATE_COMPLETE".to_string(),
11201 created_time: "2024-01-02".to_string(),
11202 updated_time: String::new(),
11203 deleted_time: String::new(),
11204 drift_status: String::new(),
11205 last_drift_check_time: String::new(),
11206 status_reason: String::new(),
11207 description: String::new(),
11208 detailed_status: String::new(),
11209 root_stack: String::new(),
11210 parent_stack: String::new(),
11211 termination_protection: false,
11212 iam_role: String::new(),
11213 tags: Vec::new(),
11214 stack_policy: String::new(),
11215 rollback_monitoring_time: String::new(),
11216 rollback_alarms: Vec::new(),
11217 notification_arns: Vec::new(),
11218 },
11219 ];
11220 app.cfn_state.table.selected = 1;
11221
11222 app.handle_action(Action::PrevItem);
11223 assert_eq!(app.cfn_state.table.selected, 0);
11224
11225 app.handle_action(Action::PrevItem);
11226 assert_eq!(app.cfn_state.table.selected, 0); }
11228
11229 #[test]
11230 fn test_cloudformation_page_down() {
11231 let mut app = test_app();
11232 app.current_service = Service::CloudFormationStacks;
11233 app.service_selected = true;
11234 app.mode = Mode::Normal;
11235 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11236
11237 for i in 0..20 {
11239 app.cfn_state.table.items.push(CfnStack {
11240 name: format!("stack{}", i),
11241 stack_id: format!("id{}", i),
11242 status: "CREATE_COMPLETE".to_string(),
11243 created_time: format!("2024-01-{:02}", i + 1),
11244 updated_time: String::new(),
11245 deleted_time: String::new(),
11246 drift_status: String::new(),
11247 last_drift_check_time: String::new(),
11248 status_reason: String::new(),
11249 description: String::new(),
11250 detailed_status: String::new(),
11251 root_stack: String::new(),
11252 parent_stack: String::new(),
11253 termination_protection: false,
11254 iam_role: String::new(),
11255 tags: Vec::new(),
11256 stack_policy: String::new(),
11257 rollback_monitoring_time: String::new(),
11258 rollback_alarms: Vec::new(),
11259 notification_arns: Vec::new(),
11260 });
11261 }
11262 app.cfn_state.table.reset();
11263
11264 app.handle_action(Action::PageDown);
11265 assert_eq!(app.cfn_state.table.selected, 10);
11266
11267 app.handle_action(Action::PageDown);
11268 assert_eq!(app.cfn_state.table.selected, 19); }
11270
11271 #[test]
11272 fn test_cloudformation_page_up() {
11273 let mut app = test_app();
11274 app.current_service = Service::CloudFormationStacks;
11275 app.service_selected = true;
11276 app.mode = Mode::Normal;
11277 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11278
11279 for i in 0..20 {
11281 app.cfn_state.table.items.push(CfnStack {
11282 name: format!("stack{}", i),
11283 stack_id: format!("id{}", i),
11284 status: "CREATE_COMPLETE".to_string(),
11285 created_time: format!("2024-01-{:02}", i + 1),
11286 updated_time: String::new(),
11287 deleted_time: String::new(),
11288 drift_status: String::new(),
11289 last_drift_check_time: String::new(),
11290 status_reason: String::new(),
11291 description: String::new(),
11292 detailed_status: String::new(),
11293 root_stack: String::new(),
11294 parent_stack: String::new(),
11295 termination_protection: false,
11296 iam_role: String::new(),
11297 tags: Vec::new(),
11298 stack_policy: String::new(),
11299 rollback_monitoring_time: String::new(),
11300 rollback_alarms: Vec::new(),
11301 notification_arns: Vec::new(),
11302 });
11303 }
11304 app.cfn_state.table.selected = 15;
11305
11306 app.handle_action(Action::PageUp);
11307 assert_eq!(app.cfn_state.table.selected, 5);
11308
11309 app.handle_action(Action::PageUp);
11310 assert_eq!(app.cfn_state.table.selected, 0); }
11312
11313 #[test]
11314 fn test_cloudformation_filter_input() {
11315 let mut app = test_app();
11316 app.current_service = Service::CloudFormationStacks;
11317 app.service_selected = true;
11318 app.mode = Mode::Normal;
11319
11320 app.handle_action(Action::StartFilter);
11321 assert_eq!(app.mode, Mode::FilterInput);
11322
11323 app.cfn_state.table.filter = "test".to_string();
11325 assert_eq!(app.cfn_state.table.filter, "test");
11326 }
11327
11328 #[test]
11329 fn test_cloudformation_filter_applies() {
11330 let mut app = test_app();
11331 app.current_service = Service::CloudFormationStacks;
11332 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11333 app.cfn_state.table.items = vec![
11334 CfnStack {
11335 name: "prod-stack".to_string(),
11336 stack_id: "id1".to_string(),
11337 status: "CREATE_COMPLETE".to_string(),
11338 created_time: "2024-01-01".to_string(),
11339 updated_time: String::new(),
11340 deleted_time: String::new(),
11341 drift_status: String::new(),
11342 last_drift_check_time: String::new(),
11343 status_reason: String::new(),
11344 description: "Production stack".to_string(),
11345 detailed_status: String::new(),
11346 root_stack: String::new(),
11347 parent_stack: String::new(),
11348 termination_protection: false,
11349 iam_role: String::new(),
11350 tags: Vec::new(),
11351 stack_policy: String::new(),
11352 rollback_monitoring_time: String::new(),
11353 rollback_alarms: Vec::new(),
11354 notification_arns: Vec::new(),
11355 },
11356 CfnStack {
11357 name: "dev-stack".to_string(),
11358 stack_id: "id2".to_string(),
11359 status: "UPDATE_COMPLETE".to_string(),
11360 created_time: "2024-01-02".to_string(),
11361 updated_time: String::new(),
11362 deleted_time: String::new(),
11363 drift_status: String::new(),
11364 last_drift_check_time: String::new(),
11365 status_reason: String::new(),
11366 description: "Development stack".to_string(),
11367 detailed_status: String::new(),
11368 root_stack: String::new(),
11369 parent_stack: String::new(),
11370 termination_protection: false,
11371 iam_role: String::new(),
11372 tags: Vec::new(),
11373 stack_policy: String::new(),
11374 rollback_monitoring_time: String::new(),
11375 rollback_alarms: Vec::new(),
11376 notification_arns: Vec::new(),
11377 },
11378 ];
11379 app.cfn_state.table.filter = "prod".to_string();
11380
11381 let filtered = filtered_cloudformation_stacks(&app);
11382 assert_eq!(filtered.len(), 1);
11383 assert_eq!(filtered[0].name, "prod-stack");
11384 }
11385
11386 #[test]
11387 fn test_cloudformation_right_arrow_expands() {
11388 let mut app = test_app();
11389 app.current_service = Service::CloudFormationStacks;
11390 app.service_selected = true;
11391 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11392 app.cfn_state.table.items = vec![CfnStack {
11393 name: "test-stack".to_string(),
11394 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11395 .to_string(),
11396 status: "CREATE_COMPLETE".to_string(),
11397 created_time: "2024-01-01".to_string(),
11398 updated_time: String::new(),
11399 deleted_time: String::new(),
11400 drift_status: String::new(),
11401 last_drift_check_time: String::new(),
11402 status_reason: String::new(),
11403 description: "Test stack".to_string(),
11404 detailed_status: String::new(),
11405 root_stack: String::new(),
11406 parent_stack: String::new(),
11407 termination_protection: false,
11408 iam_role: String::new(),
11409 tags: Vec::new(),
11410 stack_policy: String::new(),
11411 rollback_monitoring_time: String::new(),
11412 rollback_alarms: Vec::new(),
11413 notification_arns: Vec::new(),
11414 }];
11415 app.cfn_state.table.reset();
11416
11417 assert_eq!(app.cfn_state.table.expanded_item, None);
11418
11419 app.handle_action(Action::NextPane);
11420 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
11421 }
11422
11423 #[test]
11424 fn test_cloudformation_left_arrow_collapses() {
11425 let mut app = test_app();
11426 app.current_service = Service::CloudFormationStacks;
11427 app.service_selected = true;
11428 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11429 app.cfn_state.table.items = vec![CfnStack {
11430 name: "test-stack".to_string(),
11431 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11432 .to_string(),
11433 status: "CREATE_COMPLETE".to_string(),
11434 created_time: "2024-01-01".to_string(),
11435 updated_time: String::new(),
11436 deleted_time: String::new(),
11437 drift_status: String::new(),
11438 last_drift_check_time: String::new(),
11439 status_reason: String::new(),
11440 description: "Test stack".to_string(),
11441 detailed_status: String::new(),
11442 root_stack: String::new(),
11443 parent_stack: String::new(),
11444 termination_protection: false,
11445 iam_role: String::new(),
11446 tags: Vec::new(),
11447 stack_policy: String::new(),
11448 rollback_monitoring_time: String::new(),
11449 rollback_alarms: Vec::new(),
11450 notification_arns: Vec::new(),
11451 }];
11452 app.cfn_state.table.reset();
11453 app.cfn_state.table.expanded_item = Some(0);
11454
11455 app.handle_action(Action::PrevPane);
11456 assert_eq!(app.cfn_state.table.expanded_item, None);
11457 }
11458
11459 #[test]
11460 fn test_cloudformation_enter_drills_into_stack() {
11461 let mut app = test_app();
11462 app.current_service = Service::CloudFormationStacks;
11463 app.service_selected = true;
11464 app.mode = Mode::Normal;
11465 app.tabs = vec![Tab {
11466 service: Service::CloudFormationStacks,
11467 title: "CloudFormation › Stacks".to_string(),
11468 breadcrumb: "CloudFormation › Stacks".to_string(),
11469 }];
11470 app.current_tab = 0;
11471 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11472 app.cfn_state.table.items = vec![CfnStack {
11473 name: "test-stack".to_string(),
11474 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11475 .to_string(),
11476 status: "CREATE_COMPLETE".to_string(),
11477 created_time: "2024-01-01".to_string(),
11478 updated_time: String::new(),
11479 deleted_time: String::new(),
11480 drift_status: String::new(),
11481 last_drift_check_time: String::new(),
11482 status_reason: String::new(),
11483 description: "Test stack".to_string(),
11484 detailed_status: String::new(),
11485 root_stack: String::new(),
11486 parent_stack: String::new(),
11487 termination_protection: false,
11488 iam_role: String::new(),
11489 tags: Vec::new(),
11490 stack_policy: String::new(),
11491 rollback_monitoring_time: String::new(),
11492 rollback_alarms: Vec::new(),
11493 notification_arns: Vec::new(),
11494 }];
11495 app.cfn_state.table.reset();
11496
11497 let filtered = filtered_cloudformation_stacks(&app);
11499 assert_eq!(filtered.len(), 1);
11500 assert_eq!(filtered[0].name, "test-stack");
11501
11502 assert_eq!(app.cfn_state.current_stack, None);
11503
11504 app.handle_action(Action::Select);
11506 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
11507 }
11508
11509 #[test]
11510 fn test_cloudformation_copy_to_clipboard() {
11511 let mut app = test_app();
11512 app.current_service = Service::CloudFormationStacks;
11513 app.service_selected = true;
11514 app.mode = Mode::Normal;
11515 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11516 app.cfn_state.table.items = vec![
11517 CfnStack {
11518 name: "stack1".to_string(),
11519 stack_id: "id1".to_string(),
11520 status: "CREATE_COMPLETE".to_string(),
11521 created_time: "2024-01-01".to_string(),
11522 updated_time: String::new(),
11523 deleted_time: String::new(),
11524 drift_status: String::new(),
11525 last_drift_check_time: String::new(),
11526 status_reason: String::new(),
11527 description: String::new(),
11528 detailed_status: String::new(),
11529 root_stack: String::new(),
11530 parent_stack: String::new(),
11531 termination_protection: false,
11532 iam_role: String::new(),
11533 tags: Vec::new(),
11534 stack_policy: String::new(),
11535 rollback_monitoring_time: String::new(),
11536 rollback_alarms: Vec::new(),
11537 notification_arns: Vec::new(),
11538 },
11539 CfnStack {
11540 name: "stack2".to_string(),
11541 stack_id: "id2".to_string(),
11542 status: "UPDATE_COMPLETE".to_string(),
11543 created_time: "2024-01-02".to_string(),
11544 updated_time: String::new(),
11545 deleted_time: String::new(),
11546 drift_status: String::new(),
11547 last_drift_check_time: String::new(),
11548 status_reason: String::new(),
11549 description: String::new(),
11550 detailed_status: String::new(),
11551 root_stack: String::new(),
11552 parent_stack: String::new(),
11553 termination_protection: false,
11554 iam_role: String::new(),
11555 tags: Vec::new(),
11556 stack_policy: String::new(),
11557 rollback_monitoring_time: String::new(),
11558 rollback_alarms: Vec::new(),
11559 notification_arns: Vec::new(),
11560 },
11561 ];
11562
11563 assert!(!app.snapshot_requested);
11564 app.handle_action(Action::CopyToClipboard);
11565
11566 assert!(app.snapshot_requested);
11568 }
11569
11570 #[test]
11571 fn test_cloudformation_expansion_shows_all_visible_columns() {
11572 let mut app = test_app();
11573 app.current_service = Service::CloudFormationStacks;
11574 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11575 app.cfn_state.table.items = vec![CfnStack {
11576 name: "test-stack".to_string(),
11577 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11578 .to_string(),
11579 status: "CREATE_COMPLETE".to_string(),
11580 created_time: "2024-01-01".to_string(),
11581 updated_time: "2024-01-02".to_string(),
11582 deleted_time: String::new(),
11583 drift_status: "IN_SYNC".to_string(),
11584 last_drift_check_time: "2024-01-03".to_string(),
11585 status_reason: String::new(),
11586 description: "Test description".to_string(),
11587 detailed_status: String::new(),
11588 root_stack: String::new(),
11589 parent_stack: String::new(),
11590 termination_protection: false,
11591 iam_role: String::new(),
11592 tags: Vec::new(),
11593 stack_policy: String::new(),
11594 rollback_monitoring_time: String::new(),
11595 rollback_alarms: Vec::new(),
11596 notification_arns: Vec::new(),
11597 }];
11598
11599 app.cfn_visible_column_ids = [
11601 CfnColumn::Name,
11602 CfnColumn::Status,
11603 CfnColumn::CreatedTime,
11604 CfnColumn::Description,
11605 ]
11606 .iter()
11607 .map(|c| c.id())
11608 .collect();
11609
11610 app.cfn_state.table.expanded_item = Some(0);
11611
11612 assert_eq!(app.cfn_visible_column_ids.len(), 4);
11615 assert!(app.cfn_state.table.has_expanded_item());
11616 }
11617
11618 #[test]
11619 fn test_cloudformation_empty_list_shows_page_1() {
11620 let mut app = test_app();
11621 app.current_service = Service::CloudFormationStacks;
11622 app.cfn_state.table.items = vec![];
11623
11624 let filtered = filtered_cloudformation_stacks(&app);
11625 assert_eq!(filtered.len(), 0);
11626
11627 let page_size = app.cfn_state.table.page_size.value();
11629 let total_pages = filtered.len().div_ceil(page_size);
11630 assert_eq!(total_pages, 0);
11631
11632 }
11635}
11636
11637impl App {
11638 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
11639 let mut all = AwsRegion::all();
11640
11641 for region in &mut all {
11643 region.latency_ms = self.region_latencies.get(region.code).copied();
11644 }
11645
11646 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
11648 all
11649 } else {
11650 let filter_lower = self.region_filter.to_lowercase();
11651 all.into_iter()
11652 .filter(|r| {
11653 r.name.to_lowercase().contains(&filter_lower)
11654 || r.code.to_lowercase().contains(&filter_lower)
11655 || r.group.to_lowercase().contains(&filter_lower)
11656 })
11657 .collect()
11658 };
11659
11660 let mut sorted = filtered;
11662 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
11663 sorted
11664 }
11665
11666 pub fn measure_region_latencies(&mut self) {
11667 use std::time::Instant;
11668 self.region_latencies.clear();
11669
11670 let regions = AwsRegion::all();
11671 let start_all = Instant::now();
11672 tracing::info!("Starting latency measurement for {} regions", regions.len());
11673
11674 let handles: Vec<_> = regions
11675 .iter()
11676 .map(|region| {
11677 let code = region.code.to_string();
11678 std::thread::spawn(move || {
11679 let endpoint = format!("https://sts.{}.amazonaws.com", code);
11681 let start = Instant::now();
11682
11683 match ureq::get(&endpoint)
11684 .timeout(std::time::Duration::from_secs(2))
11685 .call()
11686 {
11687 Ok(_) => {
11688 let latency = start.elapsed().as_millis() as u64;
11689 Some((code, latency))
11690 }
11691 Err(e) => {
11692 tracing::debug!("Failed to measure {}: {}", code, e);
11693 Some((code, 9999))
11694 }
11695 }
11696 })
11697 })
11698 .collect();
11699
11700 for handle in handles {
11701 if let Ok(Some((code, latency))) = handle.join() {
11702 self.region_latencies.insert(code, latency);
11703 }
11704 }
11705
11706 tracing::info!(
11707 "Measured {} regions in {:?}",
11708 self.region_latencies.len(),
11709 start_all.elapsed()
11710 );
11711 }
11712
11713 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
11714 filter_profiles(&self.available_profiles, &self.profile_filter)
11715 }
11716
11717 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
11718 if self.session_filter.is_empty() {
11719 return self.sessions.iter().collect();
11720 }
11721 let filter_lower = self.session_filter.to_lowercase();
11722 self.sessions
11723 .iter()
11724 .filter(|s| {
11725 s.profile.to_lowercase().contains(&filter_lower)
11726 || s.region.to_lowercase().contains(&filter_lower)
11727 || s.account_id.to_lowercase().contains(&filter_lower)
11728 || s.role_arn.to_lowercase().contains(&filter_lower)
11729 })
11730 .collect()
11731 }
11732
11733 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
11734 if self.tab_filter.is_empty() {
11735 return self.tabs.iter().enumerate().collect();
11736 }
11737 let filter_lower = self.tab_filter.to_lowercase();
11738 self.tabs
11739 .iter()
11740 .enumerate()
11741 .filter(|(_, tab)| {
11742 tab.title.to_lowercase().contains(&filter_lower)
11743 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
11744 })
11745 .collect()
11746 }
11747
11748 pub fn load_aws_profiles() -> Vec<AwsProfile> {
11749 AwsProfile::load_all()
11750 }
11751
11752 pub async fn fetch_profile_accounts(&mut self) {
11753 for profile in &mut self.available_profiles {
11754 if profile.account.is_none() {
11755 let region = profile
11756 .region
11757 .clone()
11758 .unwrap_or_else(|| "us-east-1".to_string());
11759 if let Ok(account) =
11760 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
11761 {
11762 profile.account = Some(account);
11763 }
11764 }
11765 }
11766 }
11767
11768 fn save_current_session(&mut self) {
11769 if self.tabs.is_empty() {
11771 if let Some(ref session) = self.current_session {
11772 let _ = session.delete();
11773 self.current_session = None;
11774 }
11775 return;
11776 }
11777
11778 let session = if let Some(ref mut current) = self.current_session {
11779 current.tabs = self
11781 .tabs
11782 .iter()
11783 .map(|t| SessionTab {
11784 service: format!("{:?}", t.service),
11785 title: t.title.clone(),
11786 breadcrumb: t.breadcrumb.clone(),
11787 filter: match t.service {
11788 Service::CloudWatchLogGroups => {
11789 Some(self.log_groups_state.log_groups.filter.clone())
11790 }
11791 _ => None,
11792 },
11793 selected_item: None,
11794 })
11795 .collect();
11796 current.clone()
11797 } else {
11798 let mut session = Session::new(
11800 self.profile.clone(),
11801 self.region.clone(),
11802 self.config.account_id.clone(),
11803 self.config.role_arn.clone(),
11804 );
11805 session.tabs = self
11806 .tabs
11807 .iter()
11808 .map(|t| SessionTab {
11809 service: format!("{:?}", t.service),
11810 title: t.title.clone(),
11811 breadcrumb: t.breadcrumb.clone(),
11812 filter: match t.service {
11813 Service::CloudWatchLogGroups => {
11814 Some(self.log_groups_state.log_groups.filter.clone())
11815 }
11816 _ => None,
11817 },
11818 selected_item: None,
11819 })
11820 .collect();
11821 self.current_session = Some(session.clone());
11822 session
11823 };
11824
11825 let _ = session.save();
11826 }
11827}
11828
11829#[cfg(test)]
11830mod iam_policy_view_tests {
11831 use super::*;
11832 use test_helpers::*;
11833
11834 #[test]
11835 fn test_enter_opens_policy_view() {
11836 let mut app = test_app();
11837 app.current_service = Service::IamRoles;
11838 app.service_selected = true;
11839 app.mode = Mode::Normal;
11840 app.view_mode = ViewMode::Detail;
11841 app.iam_state.current_role = Some("TestRole".to_string());
11842 app.iam_state.policies.items = vec![IamPolicy {
11843 policy_name: "TestPolicy".to_string(),
11844 policy_type: "Inline".to_string(),
11845 attached_via: "Direct".to_string(),
11846 attached_entities: "1".to_string(),
11847 description: "Test".to_string(),
11848 creation_time: "2023-01-01".to_string(),
11849 edited_time: "2023-01-01".to_string(),
11850 policy_arn: None,
11851 }];
11852 app.iam_state.policies.reset();
11853
11854 app.handle_action(Action::Select);
11855
11856 assert_eq!(app.view_mode, ViewMode::PolicyView);
11857 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
11858 assert_eq!(app.iam_state.policy_scroll, 0);
11859 assert!(app.iam_state.policies.loading);
11860 }
11861
11862 #[test]
11863 fn test_escape_closes_policy_view() {
11864 let mut app = test_app();
11865 app.current_service = Service::IamRoles;
11866 app.service_selected = true;
11867 app.mode = Mode::Normal;
11868 app.view_mode = ViewMode::PolicyView;
11869 app.iam_state.current_role = Some("TestRole".to_string());
11870 app.iam_state.current_policy = Some("TestPolicy".to_string());
11871 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
11872 app.iam_state.policy_scroll = 5;
11873
11874 app.handle_action(Action::PrevPane);
11875
11876 assert_eq!(app.view_mode, ViewMode::Detail);
11877 assert_eq!(app.iam_state.current_policy, None);
11878 assert_eq!(app.iam_state.policy_document, "");
11879 assert_eq!(app.iam_state.policy_scroll, 0);
11880 }
11881
11882 #[test]
11883 fn test_ctrl_d_scrolls_down_in_policy_view() {
11884 let mut app = test_app();
11885 app.current_service = Service::IamRoles;
11886 app.service_selected = true;
11887 app.mode = Mode::Normal;
11888 app.view_mode = ViewMode::PolicyView;
11889 app.iam_state.current_role = Some("TestRole".to_string());
11890 app.iam_state.current_policy = Some("TestPolicy".to_string());
11891 app.iam_state.policy_document = (0..100)
11892 .map(|i| format!("line {}", i))
11893 .collect::<Vec<_>>()
11894 .join("\n");
11895 app.iam_state.policy_scroll = 0;
11896
11897 app.handle_action(Action::ScrollDown);
11898
11899 assert_eq!(app.iam_state.policy_scroll, 10);
11900
11901 app.handle_action(Action::ScrollDown);
11902
11903 assert_eq!(app.iam_state.policy_scroll, 20);
11904 }
11905
11906 #[test]
11907 fn test_ctrl_u_scrolls_up_in_policy_view() {
11908 let mut app = test_app();
11909 app.current_service = Service::IamRoles;
11910 app.service_selected = true;
11911 app.mode = Mode::Normal;
11912 app.view_mode = ViewMode::PolicyView;
11913 app.iam_state.current_role = Some("TestRole".to_string());
11914 app.iam_state.current_policy = Some("TestPolicy".to_string());
11915 app.iam_state.policy_document = (0..100)
11916 .map(|i| format!("line {}", i))
11917 .collect::<Vec<_>>()
11918 .join("\n");
11919 app.iam_state.policy_scroll = 30;
11920
11921 app.handle_action(Action::ScrollUp);
11922
11923 assert_eq!(app.iam_state.policy_scroll, 20);
11924
11925 app.handle_action(Action::ScrollUp);
11926
11927 assert_eq!(app.iam_state.policy_scroll, 10);
11928 }
11929
11930 #[test]
11931 fn test_scroll_does_not_go_negative() {
11932 let mut app = test_app();
11933 app.current_service = Service::IamRoles;
11934 app.service_selected = true;
11935 app.mode = Mode::Normal;
11936 app.view_mode = ViewMode::PolicyView;
11937 app.iam_state.current_role = Some("TestRole".to_string());
11938 app.iam_state.current_policy = Some("TestPolicy".to_string());
11939 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11940 app.iam_state.policy_scroll = 0;
11941
11942 app.handle_action(Action::ScrollUp);
11943
11944 assert_eq!(app.iam_state.policy_scroll, 0);
11945 }
11946
11947 #[test]
11948 fn test_scroll_does_not_exceed_max() {
11949 let mut app = test_app();
11950 app.current_service = Service::IamRoles;
11951 app.service_selected = true;
11952 app.mode = Mode::Normal;
11953 app.view_mode = ViewMode::PolicyView;
11954 app.iam_state.current_role = Some("TestRole".to_string());
11955 app.iam_state.current_policy = Some("TestPolicy".to_string());
11956 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11957 app.iam_state.policy_scroll = 0;
11958
11959 app.handle_action(Action::ScrollDown);
11960
11961 assert_eq!(app.iam_state.policy_scroll, 2); }
11963
11964 #[test]
11965 fn test_policy_view_console_url() {
11966 let mut app = test_app();
11967 app.current_service = Service::IamRoles;
11968 app.service_selected = true;
11969 app.view_mode = ViewMode::PolicyView;
11970 app.iam_state.current_role = Some("TestRole".to_string());
11971 app.iam_state.current_policy = Some("TestPolicy".to_string());
11972
11973 let url = app.get_console_url();
11974
11975 assert!(url.contains("us-east-1.console.aws.amazon.com"));
11976 assert!(url.contains("/roles/details/TestRole"));
11977 assert!(url.contains("/editPolicy/TestPolicy"));
11978 assert!(url.contains("step=addPermissions"));
11979 }
11980
11981 #[test]
11982 fn test_esc_from_policy_view_goes_to_role_detail() {
11983 let mut app = test_app();
11984 app.current_service = Service::IamRoles;
11985 app.service_selected = true;
11986 app.mode = Mode::Normal;
11987 app.view_mode = ViewMode::PolicyView;
11988 app.iam_state.current_role = Some("TestRole".to_string());
11989 app.iam_state.current_policy = Some("TestPolicy".to_string());
11990 app.iam_state.policy_document = "test".to_string();
11991 app.iam_state.policy_scroll = 5;
11992
11993 app.handle_action(Action::GoBack);
11994
11995 assert_eq!(app.view_mode, ViewMode::Detail);
11996 assert_eq!(app.iam_state.current_policy, None);
11997 assert_eq!(app.iam_state.policy_document, "");
11998 assert_eq!(app.iam_state.policy_scroll, 0);
11999 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
12000 }
12001
12002 #[test]
12003 fn test_esc_from_role_detail_goes_to_role_list() {
12004 let mut app = test_app();
12005 app.current_service = Service::IamRoles;
12006 app.service_selected = true;
12007 app.mode = Mode::Normal;
12008 app.view_mode = ViewMode::Detail;
12009 app.iam_state.current_role = Some("TestRole".to_string());
12010
12011 app.handle_action(Action::GoBack);
12012
12013 assert_eq!(app.iam_state.current_role, None);
12014 }
12015
12016 #[test]
12017 fn test_right_arrow_expands_policy_row() {
12018 let mut app = test_app();
12019 app.current_service = Service::IamRoles;
12020 app.service_selected = true;
12021 app.mode = Mode::Normal;
12022 app.view_mode = ViewMode::Detail;
12023 app.iam_state.current_role = Some("TestRole".to_string());
12024 app.iam_state.policies.items = vec![IamPolicy {
12025 policy_name: "TestPolicy".to_string(),
12026 policy_type: "Inline".to_string(),
12027 attached_via: "Direct".to_string(),
12028 attached_entities: "1".to_string(),
12029 description: "Test".to_string(),
12030 creation_time: "2023-01-01".to_string(),
12031 edited_time: "2023-01-01".to_string(),
12032 policy_arn: None,
12033 }];
12034 app.iam_state.policies.reset();
12035
12036 app.handle_action(Action::NextPane);
12037
12038 assert_eq!(app.view_mode, ViewMode::Detail);
12040 assert_eq!(app.iam_state.current_policy, None);
12041 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
12042 }
12043}
12044
12045#[cfg(test)]
12046mod tab_filter_tests {
12047 use super::*;
12048 use test_helpers::*;
12049
12050 #[test]
12051 fn test_space_t_opens_tab_picker() {
12052 let mut app = test_app();
12053 app.tabs = vec![
12054 Tab {
12055 service: Service::CloudWatchLogGroups,
12056 title: "Tab 1".to_string(),
12057 breadcrumb: "CloudWatch > Log groups".to_string(),
12058 },
12059 Tab {
12060 service: Service::S3Buckets,
12061 title: "Tab 2".to_string(),
12062 breadcrumb: "S3 › Buckets".to_string(),
12063 },
12064 ];
12065 app.current_tab = 0;
12066
12067 app.handle_action(Action::OpenTabPicker);
12068
12069 assert_eq!(app.mode, Mode::TabPicker);
12070 assert_eq!(app.tab_picker_selected, 0);
12071 }
12072
12073 #[test]
12074 fn test_tab_filter_works() {
12075 let mut app = test_app();
12076 app.tabs = vec![
12077 Tab {
12078 service: Service::CloudWatchLogGroups,
12079 title: "CloudWatch Logs".to_string(),
12080 breadcrumb: "CloudWatch > Log groups".to_string(),
12081 },
12082 Tab {
12083 service: Service::S3Buckets,
12084 title: "S3 Buckets".to_string(),
12085 breadcrumb: "S3 › Buckets".to_string(),
12086 },
12087 Tab {
12088 service: Service::CloudWatchAlarms,
12089 title: "CloudWatch Alarms".to_string(),
12090 breadcrumb: "CloudWatch › Alarms".to_string(),
12091 },
12092 ];
12093 app.mode = Mode::TabPicker;
12094
12095 app.handle_action(Action::FilterInput('s'));
12097 app.handle_action(Action::FilterInput('3'));
12098
12099 let filtered = app.get_filtered_tabs();
12100 assert_eq!(filtered.len(), 1);
12101 assert_eq!(filtered[0].1.title, "S3 Buckets");
12102 }
12103
12104 #[test]
12105 fn test_tab_filter_by_breadcrumb() {
12106 let mut app = test_app();
12107 app.tabs = vec![
12108 Tab {
12109 service: Service::CloudWatchLogGroups,
12110 title: "Tab 1".to_string(),
12111 breadcrumb: "CloudWatch > Log groups".to_string(),
12112 },
12113 Tab {
12114 service: Service::S3Buckets,
12115 title: "Tab 2".to_string(),
12116 breadcrumb: "S3 › Buckets".to_string(),
12117 },
12118 ];
12119 app.mode = Mode::TabPicker;
12120
12121 app.handle_action(Action::FilterInput('c'));
12123 app.handle_action(Action::FilterInput('l'));
12124 app.handle_action(Action::FilterInput('o'));
12125 app.handle_action(Action::FilterInput('u'));
12126 app.handle_action(Action::FilterInput('d'));
12127
12128 let filtered = app.get_filtered_tabs();
12129 assert_eq!(filtered.len(), 1);
12130 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
12131 }
12132
12133 #[test]
12134 fn test_tab_filter_backspace() {
12135 let mut app = test_app();
12136 app.tabs = vec![
12137 Tab {
12138 service: Service::CloudWatchLogGroups,
12139 title: "CloudWatch Logs".to_string(),
12140 breadcrumb: "CloudWatch > Log groups".to_string(),
12141 },
12142 Tab {
12143 service: Service::S3Buckets,
12144 title: "S3 Buckets".to_string(),
12145 breadcrumb: "S3 › Buckets".to_string(),
12146 },
12147 ];
12148 app.mode = Mode::TabPicker;
12149
12150 app.handle_action(Action::FilterInput('s'));
12151 app.handle_action(Action::FilterInput('3'));
12152 assert_eq!(app.tab_filter, "s3");
12153
12154 app.handle_action(Action::FilterBackspace);
12155 assert_eq!(app.tab_filter, "s");
12156
12157 let filtered = app.get_filtered_tabs();
12158 assert_eq!(filtered.len(), 2); }
12160
12161 #[test]
12162 fn test_tab_selection_with_filter() {
12163 let mut app = test_app();
12164 app.tabs = vec![
12165 Tab {
12166 service: Service::CloudWatchLogGroups,
12167 title: "CloudWatch Logs".to_string(),
12168 breadcrumb: "CloudWatch > Log groups".to_string(),
12169 },
12170 Tab {
12171 service: Service::S3Buckets,
12172 title: "S3 Buckets".to_string(),
12173 breadcrumb: "S3 › Buckets".to_string(),
12174 },
12175 ];
12176 app.mode = Mode::TabPicker;
12177 app.current_tab = 0;
12178
12179 app.handle_action(Action::FilterInput('s'));
12181 app.handle_action(Action::FilterInput('3'));
12182
12183 app.handle_action(Action::Select);
12185
12186 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
12188 assert_eq!(app.tab_filter, ""); }
12190}
12191
12192#[cfg(test)]
12193mod region_latency_tests {
12194 use super::*;
12195 use test_helpers::*;
12196
12197 #[test]
12198 fn test_regions_sorted_by_latency() {
12199 let mut app = test_app();
12200
12201 app.region_latencies.insert("us-west-2".to_string(), 50);
12203 app.region_latencies.insert("us-east-1".to_string(), 10);
12204 app.region_latencies.insert("eu-west-1".to_string(), 100);
12205
12206 let filtered = app.get_filtered_regions();
12207
12208 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
12210
12211 assert!(with_latency.len() >= 3);
12212 assert_eq!(with_latency[0].code, "us-east-1");
12213 assert_eq!(with_latency[0].latency_ms, Some(10));
12214 assert_eq!(with_latency[1].code, "us-west-2");
12215 assert_eq!(with_latency[1].latency_ms, Some(50));
12216 assert_eq!(with_latency[2].code, "eu-west-1");
12217 assert_eq!(with_latency[2].latency_ms, Some(100));
12218 }
12219
12220 #[test]
12221 fn test_regions_with_latency_before_without() {
12222 let mut app = test_app();
12223
12224 app.region_latencies.insert("eu-west-1".to_string(), 100);
12226
12227 let filtered = app.get_filtered_regions();
12228
12229 assert_eq!(filtered[0].code, "eu-west-1");
12231 assert_eq!(filtered[0].latency_ms, Some(100));
12232
12233 for region in &filtered[1..] {
12235 assert!(region.latency_ms.is_none());
12236 }
12237 }
12238
12239 #[test]
12240 fn test_region_filter_with_latency() {
12241 let mut app = test_app();
12242
12243 app.region_latencies.insert("us-east-1".to_string(), 10);
12244 app.region_latencies.insert("us-west-2".to_string(), 50);
12245 app.region_filter = "us".to_string();
12246
12247 let filtered = app.get_filtered_regions();
12248
12249 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
12251 assert_eq!(filtered[0].code, "us-east-1");
12252 assert_eq!(filtered[1].code, "us-west-2");
12253 }
12254
12255 #[test]
12256 fn test_latency_persists_across_filters() {
12257 let mut app = test_app();
12258
12259 app.region_latencies.insert("us-east-1".to_string(), 10);
12260
12261 app.region_filter = "eu".to_string();
12263 let filtered = app.get_filtered_regions();
12264 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
12265
12266 app.region_filter.clear();
12268 let all = app.get_filtered_regions();
12269
12270 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
12272 assert_eq!(us_east.latency_ms, Some(10));
12273 }
12274
12275 #[test]
12276 fn test_measure_region_latencies_clears_previous() {
12277 let mut app = test_app();
12278
12279 app.region_latencies.insert("us-east-1".to_string(), 100);
12281 app.region_latencies.insert("eu-west-1".to_string(), 200);
12282
12283 app.measure_region_latencies();
12285
12286 assert!(
12288 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
12289 );
12290 }
12291
12292 #[test]
12293 fn test_regions_with_latency_sorted_first() {
12294 let mut app = test_app();
12295
12296 app.region_latencies.insert("us-east-1".to_string(), 50);
12298 app.region_latencies.insert("eu-west-1".to_string(), 500);
12299
12300 let filtered = app.get_filtered_regions();
12301
12302 assert!(filtered.len() > 2);
12304
12305 assert_eq!(filtered[0].code, "us-east-1");
12307 assert_eq!(filtered[0].latency_ms, Some(50));
12308 assert_eq!(filtered[1].code, "eu-west-1");
12309 assert_eq!(filtered[1].latency_ms, Some(500));
12310
12311 for region in &filtered[2..] {
12313 assert!(region.latency_ms.is_none());
12314 }
12315 }
12316
12317 #[test]
12318 fn test_regions_without_latency_sorted_as_1000ms() {
12319 let mut app = test_app();
12320
12321 app.region_latencies
12323 .insert("ap-southeast-2".to_string(), 1500);
12324 app.region_latencies.insert("us-east-1".to_string(), 50);
12326
12327 let filtered = app.get_filtered_regions();
12328
12329 assert_eq!(filtered[0].code, "us-east-1");
12331 assert_eq!(filtered[0].latency_ms, Some(50));
12332
12333 let slow_region_idx = filtered
12335 .iter()
12336 .position(|r| r.code == "ap-southeast-2")
12337 .unwrap();
12338 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
12342 assert!(region.latency_ms.is_none());
12343 }
12344 }
12345
12346 #[test]
12347 fn test_region_picker_opens_with_latencies() {
12348 let mut app = test_app();
12349
12350 app.region_filter.clear();
12352 app.region_picker_selected = 0;
12353 app.measure_region_latencies();
12354
12355 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
12358 }
12359
12360 #[test]
12361 fn test_ecr_tab_next() {
12362 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
12363 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
12364 }
12365
12366 #[test]
12367 fn test_ecr_tab_switching() {
12368 let mut app = test_app();
12369 app.current_service = Service::EcrRepositories;
12370 app.service_selected = true;
12371 app.ecr_state.tab = EcrTab::Private;
12372
12373 app.handle_action(Action::NextDetailTab);
12374 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12375 assert_eq!(app.ecr_state.repositories.selected, 0);
12376
12377 app.handle_action(Action::NextDetailTab);
12378 assert_eq!(app.ecr_state.tab, EcrTab::Private);
12379 }
12380
12381 #[test]
12382 fn test_ecr_navigation() {
12383 let mut app = test_app();
12384 app.current_service = Service::EcrRepositories;
12385 app.service_selected = true;
12386 app.mode = Mode::Normal;
12387 app.ecr_state.repositories.items = vec![
12388 EcrRepository {
12389 name: "repo1".to_string(),
12390 uri: "uri1".to_string(),
12391 created_at: "2023-01-01".to_string(),
12392 tag_immutability: "MUTABLE".to_string(),
12393 encryption_type: "AES256".to_string(),
12394 },
12395 EcrRepository {
12396 name: "repo2".to_string(),
12397 uri: "uri2".to_string(),
12398 created_at: "2023-01-02".to_string(),
12399 tag_immutability: "IMMUTABLE".to_string(),
12400 encryption_type: "KMS".to_string(),
12401 },
12402 ];
12403
12404 app.handle_action(Action::NextItem);
12405 assert_eq!(app.ecr_state.repositories.selected, 1);
12406
12407 app.handle_action(Action::PrevItem);
12408 assert_eq!(app.ecr_state.repositories.selected, 0);
12409 }
12410
12411 #[test]
12412 fn test_ecr_filter() {
12413 let mut app = test_app();
12414 app.current_service = Service::EcrRepositories;
12415 app.service_selected = true;
12416 app.ecr_state.repositories.items = vec![
12417 EcrRepository {
12418 name: "my-app".to_string(),
12419 uri: "uri1".to_string(),
12420 created_at: "2023-01-01".to_string(),
12421 tag_immutability: "MUTABLE".to_string(),
12422 encryption_type: "AES256".to_string(),
12423 },
12424 EcrRepository {
12425 name: "other-service".to_string(),
12426 uri: "uri2".to_string(),
12427 created_at: "2023-01-02".to_string(),
12428 tag_immutability: "IMMUTABLE".to_string(),
12429 encryption_type: "KMS".to_string(),
12430 },
12431 ];
12432
12433 app.ecr_state.repositories.filter = "app".to_string();
12434 let filtered = filtered_ecr_repositories(&app);
12435 assert_eq!(filtered.len(), 1);
12436 assert_eq!(filtered[0].name, "my-app");
12437 }
12438
12439 #[test]
12440 fn test_ecr_filter_input() {
12441 let mut app = test_app();
12442 app.current_service = Service::EcrRepositories;
12443 app.service_selected = true;
12444 app.mode = Mode::FilterInput;
12445
12446 app.handle_action(Action::FilterInput('t'));
12447 app.handle_action(Action::FilterInput('e'));
12448 app.handle_action(Action::FilterInput('s'));
12449 app.handle_action(Action::FilterInput('t'));
12450 assert_eq!(app.ecr_state.repositories.filter, "test");
12451
12452 app.handle_action(Action::FilterBackspace);
12453 assert_eq!(app.ecr_state.repositories.filter, "tes");
12454 }
12455
12456 #[test]
12457 fn test_ecr_filter_resets_selection() {
12458 let mut app = test_app();
12459 app.current_service = Service::EcrRepositories;
12460 app.service_selected = true;
12461 app.mode = Mode::FilterInput;
12462 app.ecr_state.repositories.items = vec![
12463 EcrRepository {
12464 name: "repo1".to_string(),
12465 uri: "uri1".to_string(),
12466 created_at: "2023-01-01".to_string(),
12467 tag_immutability: "MUTABLE".to_string(),
12468 encryption_type: "AES256".to_string(),
12469 },
12470 EcrRepository {
12471 name: "repo2".to_string(),
12472 uri: "uri2".to_string(),
12473 created_at: "2023-01-02".to_string(),
12474 tag_immutability: "IMMUTABLE".to_string(),
12475 encryption_type: "KMS".to_string(),
12476 },
12477 EcrRepository {
12478 name: "repo3".to_string(),
12479 uri: "uri3".to_string(),
12480 created_at: "2023-01-03".to_string(),
12481 tag_immutability: "MUTABLE".to_string(),
12482 encryption_type: "AES256".to_string(),
12483 },
12484 ];
12485
12486 app.ecr_state.repositories.selected = 2;
12488 assert_eq!(app.ecr_state.repositories.selected, 2);
12489
12490 app.handle_action(Action::FilterInput('t'));
12492 assert_eq!(app.ecr_state.repositories.filter, "t");
12493 assert_eq!(app.ecr_state.repositories.selected, 0);
12494
12495 app.ecr_state.repositories.selected = 1;
12497
12498 app.handle_action(Action::FilterBackspace);
12500 assert_eq!(app.ecr_state.repositories.filter, "");
12501 assert_eq!(app.ecr_state.repositories.selected, 0);
12502 }
12503
12504 #[test]
12505 fn test_ecr_images_filter_resets_selection() {
12506 let mut app = test_app();
12507 app.current_service = Service::EcrRepositories;
12508 app.service_selected = true;
12509 app.mode = Mode::FilterInput;
12510 app.ecr_state.current_repository = Some("test-repo".to_string());
12511 app.ecr_state.images.items = vec![
12512 EcrImage {
12513 tag: "v1.0.0".to_string(),
12514 artifact_type: "container".to_string(),
12515 digest: "sha256:abc123".to_string(),
12516 pushed_at: "2023-01-01".to_string(),
12517 size_bytes: 1000,
12518 uri: "uri1".to_string(),
12519 last_pull_time: "".to_string(),
12520 },
12521 EcrImage {
12522 tag: "v2.0.0".to_string(),
12523 artifact_type: "container".to_string(),
12524 digest: "sha256:def456".to_string(),
12525 pushed_at: "2023-01-02".to_string(),
12526 size_bytes: 2000,
12527 uri: "uri2".to_string(),
12528 last_pull_time: "".to_string(),
12529 },
12530 ];
12531
12532 app.ecr_state.images.selected = 1;
12534 assert_eq!(app.ecr_state.images.selected, 1);
12535
12536 app.handle_action(Action::FilterInput('v'));
12538 assert_eq!(app.ecr_state.images.filter, "v");
12539 assert_eq!(app.ecr_state.images.selected, 0);
12540 }
12541
12542 #[test]
12543 fn test_iam_users_filter_input() {
12544 let mut app = test_app();
12545 app.current_service = Service::IamUsers;
12546 app.service_selected = true;
12547 app.mode = Mode::FilterInput;
12548
12549 app.handle_action(Action::FilterInput('a'));
12550 app.handle_action(Action::FilterInput('d'));
12551 app.handle_action(Action::FilterInput('m'));
12552 app.handle_action(Action::FilterInput('i'));
12553 app.handle_action(Action::FilterInput('n'));
12554 assert_eq!(app.iam_state.users.filter, "admin");
12555
12556 app.handle_action(Action::FilterBackspace);
12557 assert_eq!(app.iam_state.users.filter, "admi");
12558 }
12559
12560 #[test]
12561 fn test_iam_policies_filter_input() {
12562 let mut app = test_app();
12563 app.current_service = Service::IamUsers;
12564 app.service_selected = true;
12565 app.iam_state.current_user = Some("testuser".to_string());
12566 app.mode = Mode::FilterInput;
12567
12568 app.handle_action(Action::FilterInput('r'));
12569 app.handle_action(Action::FilterInput('e'));
12570 app.handle_action(Action::FilterInput('a'));
12571 app.handle_action(Action::FilterInput('d'));
12572 assert_eq!(app.iam_state.policies.filter, "read");
12573
12574 app.handle_action(Action::FilterBackspace);
12575 assert_eq!(app.iam_state.policies.filter, "rea");
12576 }
12577
12578 #[test]
12579 fn test_iam_start_filter() {
12580 let mut app = test_app();
12581 app.current_service = Service::IamUsers;
12582 app.service_selected = true;
12583 app.mode = Mode::Normal;
12584
12585 app.handle_action(Action::StartFilter);
12586 assert_eq!(app.mode, Mode::FilterInput);
12587 }
12588
12589 #[test]
12590 fn test_iam_roles_filter_input() {
12591 let mut app = test_app();
12592 app.current_service = Service::IamRoles;
12593 app.service_selected = true;
12594 app.mode = Mode::FilterInput;
12595
12596 app.handle_action(Action::FilterInput('a'));
12597 app.handle_action(Action::FilterInput('d'));
12598 app.handle_action(Action::FilterInput('m'));
12599 app.handle_action(Action::FilterInput('i'));
12600 app.handle_action(Action::FilterInput('n'));
12601 assert_eq!(app.iam_state.roles.filter, "admin");
12602
12603 app.handle_action(Action::FilterBackspace);
12604 assert_eq!(app.iam_state.roles.filter, "admi");
12605 }
12606
12607 #[test]
12608 fn test_iam_roles_start_filter() {
12609 let mut app = test_app();
12610 app.current_service = Service::IamRoles;
12611 app.service_selected = true;
12612 app.mode = Mode::Normal;
12613
12614 app.handle_action(Action::StartFilter);
12615 assert_eq!(app.mode, Mode::FilterInput);
12616 }
12617
12618 #[test]
12619 fn test_iam_roles_navigation() {
12620 let mut app = test_app();
12621 app.current_service = Service::IamRoles;
12622 app.service_selected = true;
12623 app.mode = Mode::Normal;
12624 app.iam_state.roles.items = (0..10)
12625 .map(|i| IamRole {
12626 role_name: format!("role{}", i),
12627 path: "/".to_string(),
12628 trusted_entities: String::new(),
12629 last_activity: String::new(),
12630 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12631 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12632 description: String::new(),
12633 max_session_duration: Some(3600),
12634 })
12635 .collect();
12636
12637 assert_eq!(app.iam_state.roles.selected, 0);
12638
12639 app.handle_action(Action::NextItem);
12640 assert_eq!(app.iam_state.roles.selected, 1);
12641
12642 app.handle_action(Action::NextItem);
12643 assert_eq!(app.iam_state.roles.selected, 2);
12644
12645 app.handle_action(Action::PrevItem);
12646 assert_eq!(app.iam_state.roles.selected, 1);
12647 }
12648
12649 #[test]
12650 fn test_iam_roles_page_hotkey() {
12651 let mut app = test_app();
12652 app.current_service = Service::IamRoles;
12653 app.service_selected = true;
12654 app.mode = Mode::Normal;
12655 app.iam_state.roles.page_size = PageSize::Ten;
12656 app.iam_state.roles.items = (0..100)
12657 .map(|i| IamRole {
12658 role_name: format!("role{}", i),
12659 path: "/".to_string(),
12660 trusted_entities: String::new(),
12661 last_activity: String::new(),
12662 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12663 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12664 description: String::new(),
12665 max_session_duration: Some(3600),
12666 })
12667 .collect();
12668
12669 app.handle_action(Action::FilterInput('2'));
12670 app.handle_action(Action::OpenColumnSelector);
12671 assert_eq!(app.iam_state.roles.selected, 10); }
12673
12674 #[test]
12675 fn test_iam_users_page_hotkey() {
12676 let mut app = test_app();
12677 app.current_service = Service::IamUsers;
12678 app.service_selected = true;
12679 app.mode = Mode::Normal;
12680 app.iam_state.users.page_size = PageSize::Ten;
12681 app.iam_state.users.items = (0..100)
12682 .map(|i| IamUser {
12683 user_name: format!("user{}", i),
12684 path: "/".to_string(),
12685 groups: String::new(),
12686 last_activity: String::new(),
12687 mfa: String::new(),
12688 password_age: String::new(),
12689 console_last_sign_in: String::new(),
12690 access_key_id: String::new(),
12691 active_key_age: String::new(),
12692 access_key_last_used: String::new(),
12693 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
12694 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12695 console_access: String::new(),
12696 signing_certs: String::new(),
12697 })
12698 .collect();
12699
12700 app.handle_action(Action::FilterInput('3'));
12701 app.handle_action(Action::OpenColumnSelector);
12702 assert_eq!(app.iam_state.users.selected, 20); }
12704
12705 #[test]
12706 fn test_ecr_scroll_navigation() {
12707 let mut app = test_app();
12708 app.current_service = Service::EcrRepositories;
12709 app.service_selected = true;
12710 app.ecr_state.repositories.items = (0..20)
12711 .map(|i| EcrRepository {
12712 name: format!("repo{}", i),
12713 uri: format!("uri{}", i),
12714 created_at: "2023-01-01".to_string(),
12715 tag_immutability: "MUTABLE".to_string(),
12716 encryption_type: "AES256".to_string(),
12717 })
12718 .collect();
12719
12720 app.handle_action(Action::ScrollDown);
12721 assert_eq!(app.ecr_state.repositories.selected, 10);
12722
12723 app.handle_action(Action::ScrollUp);
12724 assert_eq!(app.ecr_state.repositories.selected, 0);
12725 }
12726
12727 #[test]
12728 fn test_ecr_tab_switching_triggers_reload() {
12729 let mut app = test_app();
12730 app.current_service = Service::EcrRepositories;
12731 app.service_selected = true;
12732 app.ecr_state.tab = EcrTab::Private;
12733 app.ecr_state.repositories.loading = false;
12734 app.ecr_state.repositories.items = vec![EcrRepository {
12735 name: "private-repo".to_string(),
12736 uri: "uri".to_string(),
12737 created_at: "2023-01-01".to_string(),
12738 tag_immutability: "MUTABLE".to_string(),
12739 encryption_type: "AES256".to_string(),
12740 }];
12741
12742 app.handle_action(Action::NextDetailTab);
12743 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12744 assert!(app.ecr_state.repositories.loading);
12745 assert_eq!(app.ecr_state.repositories.selected, 0);
12746 }
12747
12748 #[test]
12749 fn test_ecr_tab_cycles_between_private_and_public() {
12750 let mut app = test_app();
12751 app.current_service = Service::EcrRepositories;
12752 app.service_selected = true;
12753 app.ecr_state.tab = EcrTab::Private;
12754
12755 app.handle_action(Action::NextDetailTab);
12756 assert_eq!(app.ecr_state.tab, EcrTab::Public);
12757
12758 app.handle_action(Action::NextDetailTab);
12759 assert_eq!(app.ecr_state.tab, EcrTab::Private);
12760 }
12761
12762 #[test]
12763 fn test_page_size_values() {
12764 assert_eq!(PageSize::Ten.value(), 10);
12765 assert_eq!(PageSize::TwentyFive.value(), 25);
12766 assert_eq!(PageSize::Fifty.value(), 50);
12767 assert_eq!(PageSize::OneHundred.value(), 100);
12768 }
12769
12770 #[test]
12771 fn test_page_size_next() {
12772 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
12773 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
12774 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
12775 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
12776 }
12777
12778 #[test]
12779 fn test_ecr_enter_drills_into_repository() {
12780 let mut app = test_app();
12781 app.current_service = Service::EcrRepositories;
12782 app.service_selected = true;
12783 app.mode = Mode::Normal;
12784 app.ecr_state.repositories.items = vec![EcrRepository {
12785 name: "my-repo".to_string(),
12786 uri: "uri".to_string(),
12787 created_at: "2023-01-01".to_string(),
12788 tag_immutability: "MUTABLE".to_string(),
12789 encryption_type: "AES256".to_string(),
12790 }];
12791
12792 app.handle_action(Action::Select);
12793 assert_eq!(
12794 app.ecr_state.current_repository,
12795 Some("my-repo".to_string())
12796 );
12797 assert!(app.ecr_state.repositories.loading);
12798 }
12799
12800 #[test]
12801 fn test_ecr_repository_expansion() {
12802 let mut app = test_app();
12803 app.current_service = Service::EcrRepositories;
12804 app.service_selected = true;
12805 app.ecr_state.repositories.items = vec![EcrRepository {
12806 name: "my-repo".to_string(),
12807 uri: "uri".to_string(),
12808 created_at: "2023-01-01".to_string(),
12809 tag_immutability: "MUTABLE".to_string(),
12810 encryption_type: "AES256".to_string(),
12811 }];
12812 app.ecr_state.repositories.selected = 0;
12813
12814 assert_eq!(app.ecr_state.repositories.expanded_item, None);
12815
12816 app.handle_action(Action::NextPane);
12817 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
12818
12819 app.handle_action(Action::PrevPane);
12820 assert_eq!(app.ecr_state.repositories.expanded_item, None);
12821 }
12822
12823 #[test]
12824 fn test_ecr_ctrl_d_scrolls_down() {
12825 let mut app = test_app();
12826 app.current_service = Service::EcrRepositories;
12827 app.service_selected = true;
12828 app.mode = Mode::Normal;
12829 app.ecr_state.repositories.items = (0..30)
12830 .map(|i| EcrRepository {
12831 name: format!("repo{}", i),
12832 uri: format!("uri{}", i),
12833 created_at: "2023-01-01".to_string(),
12834 tag_immutability: "MUTABLE".to_string(),
12835 encryption_type: "AES256".to_string(),
12836 })
12837 .collect();
12838 app.ecr_state.repositories.selected = 0;
12839
12840 app.handle_action(Action::PageDown);
12841 assert_eq!(app.ecr_state.repositories.selected, 10);
12842 }
12843
12844 #[test]
12845 fn test_ecr_ctrl_u_scrolls_up() {
12846 let mut app = test_app();
12847 app.current_service = Service::EcrRepositories;
12848 app.service_selected = true;
12849 app.mode = Mode::Normal;
12850 app.ecr_state.repositories.items = (0..30)
12851 .map(|i| EcrRepository {
12852 name: format!("repo{}", i),
12853 uri: format!("uri{}", i),
12854 created_at: "2023-01-01".to_string(),
12855 tag_immutability: "MUTABLE".to_string(),
12856 encryption_type: "AES256".to_string(),
12857 })
12858 .collect();
12859 app.ecr_state.repositories.selected = 15;
12860
12861 app.handle_action(Action::PageUp);
12862 assert_eq!(app.ecr_state.repositories.selected, 5);
12863 }
12864
12865 #[test]
12866 fn test_ecr_images_ctrl_d_scrolls_down() {
12867 let mut app = test_app();
12868 app.current_service = Service::EcrRepositories;
12869 app.service_selected = true;
12870 app.mode = Mode::Normal;
12871 app.ecr_state.current_repository = Some("repo".to_string());
12872 app.ecr_state.images.items = (0..30)
12873 .map(|i| EcrImage {
12874 tag: format!("tag{}", i),
12875 artifact_type: "container".to_string(),
12876 pushed_at: "2023-01-01T12:00:00Z".to_string(),
12877 size_bytes: 104857600,
12878 uri: format!("uri{}", i),
12879 digest: format!("sha256:{}", i),
12880 last_pull_time: String::new(),
12881 })
12882 .collect();
12883 app.ecr_state.images.selected = 0;
12884
12885 app.handle_action(Action::PageDown);
12886 assert_eq!(app.ecr_state.images.selected, 10);
12887 }
12888
12889 #[test]
12890 fn test_ecr_esc_goes_back_from_images_to_repos() {
12891 let mut app = test_app();
12892 app.current_service = Service::EcrRepositories;
12893 app.service_selected = true;
12894 app.mode = Mode::Normal;
12895 app.ecr_state.current_repository = Some("my-repo".to_string());
12896 app.ecr_state.images.items = vec![EcrImage {
12897 tag: "latest".to_string(),
12898 artifact_type: "container".to_string(),
12899 pushed_at: "2023-01-01T12:00:00Z".to_string(),
12900 size_bytes: 104857600,
12901 uri: "uri".to_string(),
12902 digest: "sha256:abc".to_string(),
12903 last_pull_time: String::new(),
12904 }];
12905
12906 app.handle_action(Action::GoBack);
12907 assert_eq!(app.ecr_state.current_repository, None);
12908 assert!(app.ecr_state.images.items.is_empty());
12909 }
12910
12911 #[test]
12912 fn test_ecr_esc_collapses_expanded_image_first() {
12913 let mut app = test_app();
12914 app.current_service = Service::EcrRepositories;
12915 app.service_selected = true;
12916 app.mode = Mode::Normal;
12917 app.ecr_state.current_repository = Some("my-repo".to_string());
12918 app.ecr_state.images.expanded_item = Some(0);
12919
12920 app.handle_action(Action::GoBack);
12921 assert_eq!(app.ecr_state.images.expanded_item, None);
12922 assert_eq!(
12923 app.ecr_state.current_repository,
12924 Some("my-repo".to_string())
12925 );
12926 }
12927
12928 #[test]
12929 fn test_pagination_with_lowercase_p() {
12930 let mut app = test_app();
12931 app.current_service = Service::EcrRepositories;
12932 app.service_selected = true;
12933 app.mode = Mode::Normal;
12934 app.ecr_state.repositories.items = (0..100)
12935 .map(|i| EcrRepository {
12936 name: format!("repo{}", i),
12937 uri: format!("uri{}", i),
12938 created_at: "2023-01-01".to_string(),
12939 tag_immutability: "MUTABLE".to_string(),
12940 encryption_type: "AES256".to_string(),
12941 })
12942 .collect();
12943
12944 app.handle_action(Action::FilterInput('2'));
12946 assert_eq!(app.page_input, "2");
12947
12948 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
12952
12953 #[test]
12954 fn test_lowercase_p_without_number_opens_preferences() {
12955 let mut app = test_app();
12956 app.current_service = Service::EcrRepositories;
12957 app.service_selected = true;
12958 app.mode = Mode::Normal;
12959
12960 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
12962 }
12963
12964 #[test]
12965 fn test_ctrl_o_generates_correct_console_url() {
12966 let mut app = test_app();
12967 app.current_service = Service::EcrRepositories;
12968 app.service_selected = true;
12969 app.mode = Mode::Normal;
12970 app.config.account_id = "123456789012".to_string();
12971
12972 let url = app.get_console_url();
12974 assert!(url.contains("ecr/private-registry/repositories"));
12975 assert!(url.contains("region=us-east-1"));
12976
12977 app.ecr_state.current_repository = Some("my-repo".to_string());
12979 let url = app.get_console_url();
12980 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
12981 assert!(url.contains("region=us-east-1"));
12982 }
12983
12984 #[test]
12985 fn test_page_input_display_and_reset() {
12986 let mut app = test_app();
12987 app.current_service = Service::EcrRepositories;
12988 app.service_selected = true;
12989 app.mode = Mode::Normal;
12990 app.ecr_state.repositories.items = (0..100)
12991 .map(|i| EcrRepository {
12992 name: format!("repo{}", i),
12993 uri: format!("uri{}", i),
12994 created_at: "2023-01-01".to_string(),
12995 tag_immutability: "MUTABLE".to_string(),
12996 encryption_type: "AES256".to_string(),
12997 })
12998 .collect();
12999
13000 app.handle_action(Action::FilterInput('2'));
13002 assert_eq!(app.page_input, "2");
13003
13004 app.handle_action(Action::OpenColumnSelector);
13006 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
13009
13010 #[test]
13011 fn test_page_navigation_updates_scroll_offset_for_cfn() {
13012 let mut app = test_app();
13013 app.current_service = Service::CloudFormationStacks;
13014 app.service_selected = true;
13015 app.mode = Mode::Normal;
13016 app.cfn_state.table.items = (0..100)
13017 .map(|i| CfnStack {
13018 name: format!("stack-{}", i),
13019 stack_id: format!(
13020 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
13021 i
13022 ),
13023 status: "CREATE_COMPLETE".to_string(),
13024 created_time: "2023-01-01T00:00:00Z".to_string(),
13025 updated_time: "2023-01-01T00:00:00Z".to_string(),
13026 deleted_time: String::new(),
13027 drift_status: "IN_SYNC".to_string(),
13028 last_drift_check_time: String::new(),
13029 status_reason: String::new(),
13030 description: String::new(),
13031 detailed_status: String::new(),
13032 root_stack: String::new(),
13033 parent_stack: String::new(),
13034 termination_protection: false,
13035 iam_role: String::new(),
13036 tags: vec![],
13037 stack_policy: String::new(),
13038 rollback_monitoring_time: String::new(),
13039 rollback_alarms: vec![],
13040 notification_arns: vec![],
13041 })
13042 .collect();
13043
13044 app.handle_action(Action::FilterInput('2'));
13046 assert_eq!(app.page_input, "2");
13047
13048 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
13053 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
13055 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
13056
13057 let current_page = app.cfn_state.table.scroll_offset / page_size;
13059 assert_eq!(
13060 current_page, 1,
13061 "2p should go to page 2 (0-indexed as 1), not page 3"
13062 ); }
13064
13065 #[test]
13066 fn test_3p_goes_to_page_3_not_page_5() {
13067 let mut app = test_app();
13068 app.current_service = Service::CloudFormationStacks;
13069 app.service_selected = true;
13070 app.mode = Mode::Normal;
13071 app.cfn_state.table.items = (0..200)
13072 .map(|i| CfnStack {
13073 name: format!("stack-{}", i),
13074 stack_id: format!(
13075 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
13076 i
13077 ),
13078 status: "CREATE_COMPLETE".to_string(),
13079 created_time: "2023-01-01T00:00:00Z".to_string(),
13080 updated_time: "2023-01-01T00:00:00Z".to_string(),
13081 deleted_time: String::new(),
13082 drift_status: "IN_SYNC".to_string(),
13083 last_drift_check_time: String::new(),
13084 status_reason: String::new(),
13085 description: String::new(),
13086 detailed_status: String::new(),
13087 root_stack: String::new(),
13088 parent_stack: String::new(),
13089 termination_protection: false,
13090 iam_role: String::new(),
13091 tags: vec![],
13092 stack_policy: String::new(),
13093 rollback_monitoring_time: String::new(),
13094 rollback_alarms: vec![],
13095 notification_arns: vec![],
13096 })
13097 .collect();
13098
13099 app.handle_action(Action::FilterInput('3'));
13101 app.handle_action(Action::OpenColumnSelector);
13102
13103 let page_size = app.cfn_state.table.page_size.value();
13104 let current_page = app.cfn_state.table.scroll_offset / page_size;
13105 assert_eq!(
13106 current_page, 2,
13107 "3p should go to page 3 (0-indexed as 2), not page 5"
13108 );
13109 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
13110 }
13111
13112 #[test]
13113 fn test_log_streams_page_navigation_uses_correct_page_size() {
13114 let mut app = test_app();
13115 app.current_service = Service::CloudWatchLogGroups;
13116 app.view_mode = ViewMode::Detail;
13117 app.service_selected = true;
13118 app.mode = Mode::Normal;
13119 app.log_groups_state.log_streams = (0..100)
13120 .map(|i| LogStream {
13121 name: format!("stream-{}", i),
13122 creation_time: None,
13123 last_event_time: None,
13124 })
13125 .collect();
13126
13127 app.handle_action(Action::FilterInput('2'));
13129 app.handle_action(Action::OpenColumnSelector);
13130
13131 assert_eq!(app.log_groups_state.stream_current_page, 1);
13133 assert_eq!(app.log_groups_state.selected_stream, 0);
13134
13135 assert_eq!(
13137 app.log_groups_state.stream_current_page, 1,
13138 "2p should go to page 2 (0-indexed as 1), not page 3"
13139 );
13140 }
13141
13142 #[test]
13143 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
13144 let mut app = test_app();
13145 app.current_service = Service::EcrRepositories;
13146 app.service_selected = true;
13147 app.mode = Mode::Normal;
13148 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
13150 .map(|i| EcrRepository {
13151 name: format!("repo{}", i),
13152 uri: format!("uri{}", i),
13153 created_at: "2023-01-01".to_string(),
13154 tag_immutability: "MUTABLE".to_string(),
13155 encryption_type: "AES256".to_string(),
13156 })
13157 .collect();
13158
13159 app.handle_action(Action::FilterInput('3'));
13161 app.handle_action(Action::OpenColumnSelector);
13162
13163 assert_eq!(app.ecr_state.repositories.selected, 50);
13165
13166 let page_size = app.ecr_state.repositories.page_size.value();
13167 let current_page = app.ecr_state.repositories.selected / page_size;
13168 assert_eq!(
13169 current_page, 2,
13170 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
13171 );
13172 }
13173
13174 #[test]
13175 fn test_page_navigation_updates_scroll_offset_for_alarms() {
13176 let mut app = test_app();
13177 app.current_service = Service::CloudWatchAlarms;
13178 app.service_selected = true;
13179 app.mode = Mode::Normal;
13180 app.alarms_state.table.items = (0..100)
13181 .map(|i| Alarm {
13182 name: format!("alarm-{}", i),
13183 state: "OK".to_string(),
13184 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
13185 description: String::new(),
13186 metric_name: "CPUUtilization".to_string(),
13187 namespace: "AWS/EC2".to_string(),
13188 statistic: "Average".to_string(),
13189 period: 300,
13190 comparison_operator: "GreaterThanThreshold".to_string(),
13191 threshold: 80.0,
13192 actions_enabled: true,
13193 state_reason: String::new(),
13194 resource: String::new(),
13195 dimensions: String::new(),
13196 expression: String::new(),
13197 alarm_type: "MetricAlarm".to_string(),
13198 cross_account: String::new(),
13199 })
13200 .collect();
13201
13202 app.handle_action(Action::FilterInput('2'));
13204 app.handle_action(Action::OpenColumnSelector);
13205
13206 let page_size = app.alarms_state.table.page_size.value();
13208 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
13210 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
13211 }
13212
13213 #[test]
13214 fn test_ecr_pagination_with_65_repos() {
13215 let mut app = test_app();
13216 app.current_service = Service::EcrRepositories;
13217 app.service_selected = true;
13218 app.mode = Mode::Normal;
13219 app.ecr_state.repositories.items = (0..65)
13220 .map(|i| EcrRepository {
13221 name: format!("repo{:02}", i),
13222 uri: format!("uri{}", i),
13223 created_at: "2023-01-01".to_string(),
13224 tag_immutability: "MUTABLE".to_string(),
13225 encryption_type: "AES256".to_string(),
13226 })
13227 .collect();
13228
13229 assert_eq!(app.ecr_state.repositories.selected, 0);
13231 let page_size = 50;
13232 let current_page = app.ecr_state.repositories.selected / page_size;
13233 assert_eq!(current_page, 0);
13234
13235 app.handle_action(Action::FilterInput('2'));
13237 app.handle_action(Action::OpenColumnSelector);
13238 assert_eq!(app.ecr_state.repositories.selected, 50);
13239
13240 let current_page = app.ecr_state.repositories.selected / page_size;
13242 assert_eq!(current_page, 1);
13243 }
13244
13245 #[test]
13246 fn test_ecr_repos_input_focus_tab_cycling() {
13247 let mut app = test_app();
13248 app.current_service = Service::EcrRepositories;
13249 app.service_selected = true;
13250 app.mode = Mode::FilterInput;
13251 app.ecr_state.input_focus = InputFocus::Filter;
13252
13253 app.handle_action(Action::NextFilterFocus);
13255 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13256
13257 app.handle_action(Action::NextFilterFocus);
13259 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13260
13261 app.handle_action(Action::PrevFilterFocus);
13263 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13264
13265 app.handle_action(Action::PrevFilterFocus);
13267 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13268 }
13269
13270 #[test]
13271 fn test_ecr_images_column_toggle_not_off_by_one() {
13272 use crate::ecr::image::Column as ImageColumn;
13273 let mut app = test_app();
13274 app.current_service = Service::EcrRepositories;
13275 app.service_selected = true;
13276 app.mode = Mode::ColumnSelector;
13277 app.ecr_state.current_repository = Some("test-repo".to_string());
13278
13279 app.ecr_image_visible_column_ids = ImageColumn::ids();
13281 let initial_count = app.ecr_image_visible_column_ids.len();
13282
13283 app.column_selector_index = 0;
13285 app.handle_action(Action::ToggleColumn);
13286
13287 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
13289 assert!(!app
13290 .ecr_image_visible_column_ids
13291 .contains(&ImageColumn::Tag.id()));
13292
13293 app.handle_action(Action::ToggleColumn);
13295 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
13296 assert!(app
13297 .ecr_image_visible_column_ids
13298 .contains(&ImageColumn::Tag.id()));
13299 }
13300
13301 #[test]
13302 fn test_ecr_repos_column_toggle_works() {
13303 let mut app = test_app();
13304 app.current_service = Service::EcrRepositories;
13305 app.service_selected = true;
13306 app.mode = Mode::ColumnSelector;
13307 app.ecr_state.current_repository = None;
13308
13309 app.ecr_repo_visible_column_ids = EcrColumn::ids();
13311 let initial_count = app.ecr_repo_visible_column_ids.len();
13312
13313 app.column_selector_index = 1;
13315 app.handle_action(Action::ToggleColumn);
13316
13317 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
13319 assert!(!app
13320 .ecr_repo_visible_column_ids
13321 .contains(&EcrColumn::Name.id()));
13322
13323 app.handle_action(Action::ToggleColumn);
13325 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
13326 assert!(app
13327 .ecr_repo_visible_column_ids
13328 .contains(&EcrColumn::Name.id()));
13329 }
13330
13331 #[test]
13332 fn test_ecr_repos_pagination_left_right_navigation() {
13333 use crate::ecr::repo::Repository as EcrRepository;
13334 let mut app = test_app();
13335 app.current_service = Service::EcrRepositories;
13336 app.service_selected = true;
13337 app.mode = Mode::FilterInput;
13338 app.ecr_state.input_focus = InputFocus::Pagination;
13339
13340 app.ecr_state.repositories.items = (0..150)
13342 .map(|i| EcrRepository {
13343 name: format!("repo{:03}", i),
13344 uri: format!("uri{}", i),
13345 created_at: "2023-01-01".to_string(),
13346 tag_immutability: "MUTABLE".to_string(),
13347 encryption_type: "AES256".to_string(),
13348 })
13349 .collect();
13350
13351 app.ecr_state.repositories.selected = 0;
13353 eprintln!(
13354 "Initial: selected={}, focus={:?}, mode={:?}",
13355 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
13356 );
13357
13358 app.handle_action(Action::PageDown);
13360 eprintln!(
13361 "After PageDown: selected={}",
13362 app.ecr_state.repositories.selected
13363 );
13364 assert_eq!(app.ecr_state.repositories.selected, 50);
13365
13366 app.handle_action(Action::PageDown);
13368 eprintln!(
13369 "After 2nd PageDown: selected={}",
13370 app.ecr_state.repositories.selected
13371 );
13372 assert_eq!(app.ecr_state.repositories.selected, 100);
13373
13374 app.handle_action(Action::PageDown);
13376 eprintln!(
13377 "After 3rd PageDown: selected={}",
13378 app.ecr_state.repositories.selected
13379 );
13380 assert_eq!(app.ecr_state.repositories.selected, 100);
13381
13382 app.handle_action(Action::PageUp);
13384 eprintln!(
13385 "After PageUp: selected={}",
13386 app.ecr_state.repositories.selected
13387 );
13388 assert_eq!(app.ecr_state.repositories.selected, 50);
13389
13390 app.handle_action(Action::PageUp);
13392 eprintln!(
13393 "After 2nd PageUp: selected={}",
13394 app.ecr_state.repositories.selected
13395 );
13396 assert_eq!(app.ecr_state.repositories.selected, 0);
13397
13398 app.handle_action(Action::PageUp);
13400 eprintln!(
13401 "After 3rd PageUp: selected={}",
13402 app.ecr_state.repositories.selected
13403 );
13404 assert_eq!(app.ecr_state.repositories.selected, 0);
13405 }
13406
13407 #[test]
13408 fn test_ecr_repos_filter_input_when_input_focused() {
13409 use crate::ecr::repo::Repository as EcrRepository;
13410 let mut app = test_app();
13411 app.current_service = Service::EcrRepositories;
13412 app.service_selected = true;
13413 app.mode = Mode::FilterInput;
13414 app.ecr_state.input_focus = InputFocus::Filter;
13415
13416 app.ecr_state.repositories.items = vec![
13418 EcrRepository {
13419 name: "test-repo".to_string(),
13420 uri: "uri1".to_string(),
13421 created_at: "2023-01-01".to_string(),
13422 tag_immutability: "MUTABLE".to_string(),
13423 encryption_type: "AES256".to_string(),
13424 },
13425 EcrRepository {
13426 name: "prod-repo".to_string(),
13427 uri: "uri2".to_string(),
13428 created_at: "2023-01-01".to_string(),
13429 tag_immutability: "MUTABLE".to_string(),
13430 encryption_type: "AES256".to_string(),
13431 },
13432 ];
13433
13434 assert_eq!(app.ecr_state.repositories.filter, "");
13436 app.handle_action(Action::FilterInput('t'));
13437 assert_eq!(app.ecr_state.repositories.filter, "t");
13438 app.handle_action(Action::FilterInput('e'));
13439 assert_eq!(app.ecr_state.repositories.filter, "te");
13440 app.handle_action(Action::FilterInput('s'));
13441 assert_eq!(app.ecr_state.repositories.filter, "tes");
13442 app.handle_action(Action::FilterInput('t'));
13443 assert_eq!(app.ecr_state.repositories.filter, "test");
13444 }
13445
13446 #[test]
13447 fn test_ecr_repos_digit_input_when_pagination_focused() {
13448 use crate::ecr::repo::Repository as EcrRepository;
13449 let mut app = test_app();
13450 app.current_service = Service::EcrRepositories;
13451 app.service_selected = true;
13452 app.mode = Mode::FilterInput;
13453 app.ecr_state.input_focus = InputFocus::Pagination;
13454
13455 app.ecr_state.repositories.items = vec![EcrRepository {
13457 name: "test-repo".to_string(),
13458 uri: "uri1".to_string(),
13459 created_at: "2023-01-01".to_string(),
13460 tag_immutability: "MUTABLE".to_string(),
13461 encryption_type: "AES256".to_string(),
13462 }];
13463
13464 assert_eq!(app.ecr_state.repositories.filter, "");
13466 assert_eq!(app.page_input, "");
13467 app.handle_action(Action::FilterInput('2'));
13468 assert_eq!(app.ecr_state.repositories.filter, "");
13469 assert_eq!(app.page_input, "2");
13470
13471 app.handle_action(Action::FilterInput('a'));
13473 assert_eq!(app.ecr_state.repositories.filter, "");
13474 assert_eq!(app.page_input, "2");
13475 }
13476
13477 #[test]
13478 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
13479 use crate::ecr::repo::Repository as EcrRepository;
13480 let mut app = test_app();
13481 app.current_service = Service::EcrRepositories;
13482 app.service_selected = true;
13483 app.mode = Mode::FilterInput;
13484 app.ecr_state.input_focus = InputFocus::Filter;
13485
13486 app.ecr_state.repositories.items = (0..150)
13488 .map(|i| EcrRepository {
13489 name: format!("repo{:03}", i),
13490 uri: format!("uri{}", i),
13491 created_at: "2023-01-01".to_string(),
13492 tag_immutability: "MUTABLE".to_string(),
13493 encryption_type: "AES256".to_string(),
13494 })
13495 .collect();
13496
13497 app.ecr_state.repositories.selected = 0;
13499
13500 app.handle_action(Action::PageDown);
13502 assert_eq!(
13503 app.ecr_state.repositories.selected, 10,
13504 "Should scroll down by 10"
13505 );
13506
13507 app.handle_action(Action::PageUp);
13508 assert_eq!(
13509 app.ecr_state.repositories.selected, 0,
13510 "Should scroll back up"
13511 );
13512 }
13513
13514 #[test]
13515 fn test_ecr_repos_pagination_control_actually_works() {
13516 use crate::ecr::repo::Repository as EcrRepository;
13517
13518 let mut app = test_app();
13520 app.current_service = Service::EcrRepositories;
13521 app.service_selected = true;
13522 app.mode = Mode::FilterInput;
13523 app.ecr_state.current_repository = None;
13524 app.ecr_state.input_focus = InputFocus::Pagination;
13525
13526 app.ecr_state.repositories.items = (0..100)
13528 .map(|i| EcrRepository {
13529 name: format!("repo{:03}", i),
13530 uri: format!("uri{}", i),
13531 created_at: "2023-01-01".to_string(),
13532 tag_immutability: "MUTABLE".to_string(),
13533 encryption_type: "AES256".to_string(),
13534 })
13535 .collect();
13536
13537 app.ecr_state.repositories.selected = 0;
13538
13539 assert_eq!(app.mode, Mode::FilterInput);
13541 assert_eq!(app.current_service, Service::EcrRepositories);
13542 assert_eq!(app.ecr_state.current_repository, None);
13543 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13544
13545 app.handle_action(Action::PageDown);
13547 assert_eq!(
13548 app.ecr_state.repositories.selected, 50,
13549 "PageDown should move to page 2"
13550 );
13551
13552 app.handle_action(Action::PageUp);
13553 assert_eq!(
13554 app.ecr_state.repositories.selected, 0,
13555 "PageUp should move back to page 1"
13556 );
13557 }
13558
13559 #[test]
13560 fn test_ecr_repos_can_reach_last_page_when_not_multiple_of_page_size() {
13561 use crate::ecr::repo::Repository as EcrRepository;
13562 let mut app = test_app();
13563 app.current_service = Service::EcrRepositories;
13564 app.service_selected = true;
13565 app.mode = Mode::FilterInput;
13566 app.ecr_state.input_focus = InputFocus::Pagination;
13567
13568 app.ecr_state.repositories.items = (0..754)
13570 .map(|i| EcrRepository {
13571 name: format!("repo{:03}", i),
13572 uri: format!("uri{}", i),
13573 created_at: "2023-01-01".to_string(),
13574 tag_immutability: "MUTABLE".to_string(),
13575 encryption_type: "AES256".to_string(),
13576 })
13577 .collect();
13578
13579 app.ecr_state.repositories.selected = 700;
13581
13582 app.handle_action(Action::PageDown);
13584 assert_eq!(
13585 app.ecr_state.repositories.selected, 750,
13586 "Should reach last page (750) but got {}",
13587 app.ecr_state.repositories.selected
13588 );
13589
13590 app.handle_action(Action::PageDown);
13592 assert_eq!(app.ecr_state.repositories.selected, 750);
13593 }
13594
13595 #[test]
13596 fn test_ecr_repos_start_filter_resets_focus_to_input() {
13597 let mut app = test_app();
13598 app.current_service = Service::EcrRepositories;
13599 app.service_selected = true;
13600 app.mode = Mode::Normal;
13601 app.ecr_state.current_repository = None;
13602
13603 app.ecr_state.input_focus = InputFocus::Pagination;
13605
13606 app.handle_action(Action::StartFilter);
13608
13609 assert_eq!(app.mode, Mode::FilterInput);
13611 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13612 }
13613
13614 #[test]
13615 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
13616 use crate::ecr::repo::Repository as EcrRepository;
13617
13618 let mut app = test_app();
13619 app.current_service = Service::EcrRepositories;
13620 app.service_selected = true;
13621 app.mode = Mode::Normal;
13622 app.ecr_state.current_repository = None;
13623
13624 app.ecr_state.repositories.items = (0..100)
13626 .map(|i| EcrRepository {
13627 name: format!("repo{:03}", i),
13628 uri: format!("uri{}", i),
13629 created_at: "2023-01-01".to_string(),
13630 tag_immutability: "MUTABLE".to_string(),
13631 encryption_type: "AES256".to_string(),
13632 })
13633 .collect();
13634
13635 app.ecr_state.repositories.selected = 0;
13636
13637 app.handle_action(Action::StartFilter);
13639 assert_eq!(app.mode, Mode::FilterInput);
13640 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13641
13642 app.handle_action(Action::NextFilterFocus);
13644 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13645
13646 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
13648 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
13649 app.handle_action(Action::PageDown);
13650 eprintln!(
13651 "After PageDown: selected={}",
13652 app.ecr_state.repositories.selected
13653 );
13654
13655 assert_eq!(
13657 app.ecr_state.repositories.selected, 50,
13658 "Right arrow should move to page 2"
13659 );
13660
13661 app.handle_action(Action::PageUp);
13663 assert_eq!(
13664 app.ecr_state.repositories.selected, 0,
13665 "Left arrow should move back to page 1"
13666 );
13667 }
13668
13669 #[test]
13670 fn test_apig_filter_input() {
13671 let mut app = test_app();
13672 app.current_service = Service::ApiGatewayApis;
13673 app.service_selected = true;
13674 app.mode = Mode::FilterInput;
13675
13676 app.handle_action(Action::FilterInput('t'));
13677 app.handle_action(Action::FilterInput('e'));
13678 app.handle_action(Action::FilterInput('s'));
13679 app.handle_action(Action::FilterInput('t'));
13680 assert_eq!(app.apig_state.apis.filter, "test");
13681
13682 app.handle_action(Action::FilterBackspace);
13683 assert_eq!(app.apig_state.apis.filter, "tes");
13684 }
13685
13686 #[test]
13687 fn test_apig_start_filter_enters_filter_mode() {
13688 let mut app = test_app();
13689 app.current_service = Service::ApiGatewayApis;
13690 app.service_selected = true;
13691 app.mode = Mode::Normal;
13692
13693 app.handle_action(Action::StartFilter);
13694 assert_eq!(app.mode, Mode::FilterInput);
13695 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13696 }
13697
13698 #[test]
13699 fn test_apig_input_focus_cycles_with_tab() {
13700 let mut app = test_app();
13701 app.current_service = Service::ApiGatewayApis;
13702 app.service_selected = true;
13703 app.mode = Mode::FilterInput;
13704 app.apig_state.input_focus = InputFocus::Filter;
13705
13706 app.handle_action(Action::NextFilterFocus);
13708 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13709
13710 app.handle_action(Action::NextFilterFocus);
13712 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13713 }
13714
13715 #[test]
13716 fn test_apig_input_focus_cycles_with_shift_tab() {
13717 let mut app = test_app();
13718 app.current_service = Service::ApiGatewayApis;
13719 app.service_selected = true;
13720 app.mode = Mode::FilterInput;
13721 app.apig_state.input_focus = InputFocus::Filter;
13722
13723 app.handle_action(Action::PrevFilterFocus);
13725 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13726
13727 app.handle_action(Action::PrevFilterFocus);
13729 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13730 }
13731
13732 #[test]
13733 fn test_apig_exact_user_flow_i_tab_filter() {
13734 let mut app = test_app();
13735 app.current_service = Service::ApiGatewayApis;
13736 app.service_selected = true;
13737 app.mode = Mode::Normal;
13738
13739 app.apig_state.apis.items = vec![
13741 crate::apig::api::RestApi {
13742 id: "api1".to_string(),
13743 name: "test-api".to_string(),
13744 description: "Test API".to_string(),
13745 created_date: "2023-01-01".to_string(),
13746 api_key_source: "HEADER".to_string(),
13747 endpoint_configuration: "REGIONAL".to_string(),
13748 protocol_type: "REST".to_string(),
13749 disable_execute_api_endpoint: false,
13750 status: "AVAILABLE".to_string(),
13751 },
13752 crate::apig::api::RestApi {
13753 id: "api2".to_string(),
13754 name: "prod-api".to_string(),
13755 description: "Production API".to_string(),
13756 created_date: "2023-01-02".to_string(),
13757 api_key_source: "HEADER".to_string(),
13758 endpoint_configuration: "REGIONAL".to_string(),
13759 protocol_type: "REST".to_string(),
13760 disable_execute_api_endpoint: false,
13761 status: "AVAILABLE".to_string(),
13762 },
13763 ];
13764
13765 app.handle_action(Action::StartFilter);
13767 assert_eq!(app.mode, Mode::FilterInput);
13768 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13769
13770 app.handle_action(Action::FilterInput('t'));
13772 app.handle_action(Action::FilterInput('e'));
13773 app.handle_action(Action::FilterInput('s'));
13774 app.handle_action(Action::FilterInput('t'));
13775 assert_eq!(app.apig_state.apis.filter, "test");
13776
13777 app.handle_action(Action::NextFilterFocus);
13779 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13780
13781 app.handle_action(Action::NextFilterFocus);
13783 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13784 }
13785
13786 #[test]
13787 fn test_apig_row_expansion() {
13788 let mut app = test_app();
13789 app.current_service = Service::ApiGatewayApis;
13790 app.service_selected = true;
13791 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
13792 id: "api1".to_string(),
13793 name: "test-api".to_string(),
13794 description: "Test API".to_string(),
13795 created_date: "2023-01-01".to_string(),
13796 api_key_source: "HEADER".to_string(),
13797 endpoint_configuration: "REGIONAL".to_string(),
13798 protocol_type: "REST".to_string(),
13799 disable_execute_api_endpoint: false,
13800 status: "AVAILABLE".to_string(),
13801 }];
13802 app.apig_state.apis.selected = 0;
13803
13804 assert_eq!(app.apig_state.apis.expanded_item, None);
13805
13806 app.handle_action(Action::NextPane);
13808 assert_eq!(app.apig_state.apis.expanded_item, None);
13809
13810 app.handle_action(Action::ExpandRow);
13812 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
13813
13814 app.handle_action(Action::NextPane);
13816 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
13817
13818 app.handle_action(Action::PrevPane);
13820 assert_eq!(app.apig_state.apis.expanded_item, None);
13821 }
13822
13823 #[test]
13824 fn test_apig_column_preferences() {
13825 let mut app = test_app();
13826 app.current_service = Service::ApiGatewayApis;
13827 app.service_selected = true;
13828
13829 let initial_count = app.apig_api_visible_column_ids.len();
13831 assert_eq!(initial_count, app.apig_api_column_ids.len());
13832
13833 app.handle_action(Action::OpenColumnSelector);
13835 assert_eq!(app.mode, Mode::ColumnSelector);
13836
13837 app.column_selector_index = 1;
13839 let first_col = app.apig_api_column_ids[0];
13840 assert!(app.apig_api_visible_column_ids.contains(&first_col));
13841
13842 app.handle_action(Action::ToggleColumn);
13843
13844 assert!(!app.apig_api_visible_column_ids.contains(&first_col));
13846 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
13847
13848 app.handle_action(Action::ToggleColumn);
13850 assert!(app.apig_api_visible_column_ids.contains(&first_col));
13851 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
13852 }
13853
13854 #[test]
13855 fn test_apig_page_size_preferences() {
13856 use crate::common::PageSize;
13857
13858 let mut app = test_app();
13859 app.current_service = Service::ApiGatewayApis;
13860 app.service_selected = true;
13861
13862 assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
13864
13865 app.handle_action(Action::OpenColumnSelector);
13867
13868 app.column_selector_index = app.apig_api_column_ids.len() + 3;
13873 app.handle_action(Action::ToggleColumn);
13874 assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
13875
13876 app.column_selector_index = app.apig_api_column_ids.len() + 6;
13878 app.handle_action(Action::ToggleColumn);
13879 assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
13880 }
13881
13882 #[test]
13883 fn test_apig_preferences_skip_blank_row() {
13884 let mut app = test_app();
13885 app.current_service = Service::ApiGatewayApis;
13886 app.service_selected = true;
13887 app.mode = Mode::ColumnSelector;
13888
13889 app.column_selector_index = 8;
13891
13892 app.handle_action(Action::NextItem);
13894 assert_eq!(app.column_selector_index, 10);
13895
13896 app.handle_action(Action::PrevItem);
13898 assert_eq!(app.column_selector_index, 8);
13899 }
13900
13901 #[test]
13902 fn test_apig_preferences_ctrl_d_skip_blank_row() {
13903 let mut app = test_app();
13904 app.current_service = Service::ApiGatewayApis;
13905 app.service_selected = true;
13906 app.mode = Mode::ColumnSelector;
13907
13908 app.column_selector_index = 0;
13910
13911 app.handle_action(Action::PageDown);
13913 assert_eq!(app.column_selector_index, 10);
13914
13915 app.handle_action(Action::PageUp);
13917 assert_eq!(app.column_selector_index, 0);
13918 }
13919
13920 #[test]
13921 fn test_apig_preferences_ctrl_u_skip_blank_row() {
13922 let mut app = test_app();
13923 app.current_service = Service::ApiGatewayApis;
13924 app.service_selected = true;
13925 app.mode = Mode::ColumnSelector;
13926
13927 app.column_selector_index = 10;
13929
13930 app.handle_action(Action::PageUp);
13932 assert_eq!(app.column_selector_index, 0);
13933
13934 app.column_selector_index = 14; app.handle_action(Action::PageUp);
13939 assert_eq!(app.column_selector_index, 4);
13940 }
13941
13942 #[test]
13943 fn test_apig_preferences_tab_cycles_sections() {
13944 let mut app = test_app();
13945 app.current_service = Service::ApiGatewayApis;
13946 app.service_selected = true;
13947 app.mode = Mode::ColumnSelector;
13948
13949 app.column_selector_index = 0;
13951
13952 app.handle_action(Action::NextPreferences);
13954 assert_eq!(app.column_selector_index, 10);
13955
13956 app.handle_action(Action::NextPreferences);
13958 assert_eq!(app.column_selector_index, 0);
13959 }
13960
13961 #[test]
13962 fn test_apig_preferences_shift_tab_cycles_sections() {
13963 let mut app = test_app();
13964 app.current_service = Service::ApiGatewayApis;
13965 app.service_selected = true;
13966 app.mode = Mode::ColumnSelector;
13967
13968 app.column_selector_index = 0;
13970
13971 app.handle_action(Action::PrevPreferences);
13973 assert_eq!(app.column_selector_index, 10);
13974
13975 app.handle_action(Action::PrevPreferences);
13977 assert_eq!(app.column_selector_index, 0);
13978 }
13979
13980 #[test]
13981 fn test_apig_arrow_navigation() {
13982 let mut app = test_app();
13983 app.current_service = Service::ApiGatewayApis;
13984 app.service_selected = true;
13985 app.mode = Mode::Normal; app.apig_state.apis.filter = String::new(); app.apig_state.apis.items = vec![
13988 crate::apig::api::RestApi {
13989 id: "api1".to_string(),
13990 name: "test-api-1".to_string(),
13991 description: "Test API 1".to_string(),
13992 created_date: "2023-01-01".to_string(),
13993 api_key_source: "HEADER".to_string(),
13994 endpoint_configuration: "REGIONAL".to_string(),
13995 protocol_type: "REST".to_string(),
13996 disable_execute_api_endpoint: false,
13997 status: "AVAILABLE".to_string(),
13998 },
13999 crate::apig::api::RestApi {
14000 id: "api2".to_string(),
14001 name: "test-api-2".to_string(),
14002 description: "Test API 2".to_string(),
14003 created_date: "2023-01-02".to_string(),
14004 api_key_source: "HEADER".to_string(),
14005 endpoint_configuration: "REGIONAL".to_string(),
14006 protocol_type: "HTTP".to_string(),
14007 disable_execute_api_endpoint: false,
14008 status: "AVAILABLE".to_string(),
14009 },
14010 ];
14011 app.apig_state.apis.selected = 0;
14012
14013 app.handle_action(Action::NextItem);
14015 assert_eq!(app.apig_state.apis.selected, 1);
14016
14017 app.handle_action(Action::PrevItem);
14019 assert_eq!(app.apig_state.apis.selected, 0);
14020 }
14021
14022 #[test]
14023 fn test_apig_collapse_row() {
14024 let mut app = test_app();
14025 app.current_service = Service::ApiGatewayApis;
14026 app.service_selected = true;
14027 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
14028 id: "api1".to_string(),
14029 name: "test-api".to_string(),
14030 description: "Test API".to_string(),
14031 created_date: "2023-01-01".to_string(),
14032 api_key_source: "HEADER".to_string(),
14033 endpoint_configuration: "REGIONAL".to_string(),
14034 protocol_type: "REST".to_string(),
14035 disable_execute_api_endpoint: false,
14036 status: "AVAILABLE".to_string(),
14037 }];
14038 app.apig_state.apis.selected = 0;
14039 app.apig_state.apis.expanded_item = Some(0);
14040
14041 app.handle_action(Action::CollapseRow);
14043 assert_eq!(app.apig_state.apis.expanded_item, None);
14044 }
14045
14046 #[test]
14047 fn test_apig_filter_resets_selection() {
14048 let mut app = test_app();
14049 app.current_service = Service::ApiGatewayApis;
14050 app.service_selected = true;
14051 app.mode = Mode::FilterInput;
14052 app.apig_state.input_focus = InputFocus::Filter;
14053 app.apig_state.apis.items = vec![
14054 crate::apig::api::RestApi {
14055 id: "api1".to_string(),
14056 name: "alpha-api".to_string(),
14057 description: "Alpha API".to_string(),
14058 created_date: "2023-01-01".to_string(),
14059 api_key_source: "HEADER".to_string(),
14060 endpoint_configuration: "REGIONAL".to_string(),
14061 protocol_type: "REST".to_string(),
14062 disable_execute_api_endpoint: false,
14063 status: "AVAILABLE".to_string(),
14064 },
14065 crate::apig::api::RestApi {
14066 id: "api2".to_string(),
14067 name: "beta-api".to_string(),
14068 description: "Beta API".to_string(),
14069 created_date: "2023-01-02".to_string(),
14070 api_key_source: "HEADER".to_string(),
14071 endpoint_configuration: "REGIONAL".to_string(),
14072 protocol_type: "HTTP".to_string(),
14073 disable_execute_api_endpoint: false,
14074 status: "AVAILABLE".to_string(),
14075 },
14076 ];
14077
14078 app.apig_state.apis.selected = 1;
14080 app.apig_state.apis.expanded_item = Some(1);
14081
14082 app.handle_action(Action::FilterInput('a'));
14084
14085 assert_eq!(app.apig_state.apis.selected, 0);
14087 assert_eq!(app.apig_state.apis.expanded_item, None);
14088 assert_eq!(app.apig_state.apis.filter, "a");
14089 }
14090
14091 #[test]
14092 fn test_service_picker_starts_in_normal_mode() {
14093 let app = test_app();
14094 assert_eq!(app.mode, Mode::ServicePicker);
14095 assert!(!app.service_picker.filter_active);
14096 }
14097
14098 #[test]
14099 fn test_service_picker_i_key_activates_filter() {
14100 let mut app = test_app();
14101
14102 assert_eq!(app.mode, Mode::ServicePicker);
14104 assert!(!app.service_picker.filter_active);
14105 assert!(app.service_picker.filter.is_empty());
14106
14107 app.handle_action(Action::EnterFilterMode);
14109
14110 assert_eq!(app.mode, Mode::ServicePicker);
14112 assert!(app.service_picker.filter_active);
14113 assert!(app.service_picker.filter.is_empty());
14114
14115 app.handle_action(Action::FilterInput('s'));
14117 assert_eq!(app.service_picker.filter, "s");
14118 }
14119
14120 #[test]
14121 fn test_service_picker_esc_exits_filter_mode() {
14122 let mut app = test_app();
14123 assert_eq!(app.mode, Mode::ServicePicker);
14124 assert!(!app.service_picker.filter_active);
14125
14126 app.handle_action(Action::EnterFilterMode);
14128 assert!(app.service_picker.filter_active);
14129 assert_eq!(app.mode, Mode::ServicePicker);
14130
14131 app.handle_action(Action::FilterInput('s'));
14133 assert_eq!(app.service_picker.filter, "s");
14134
14135 app.handle_action(Action::ExitFilterMode);
14137 assert!(!app.service_picker.filter_active);
14138 assert_eq!(app.mode, Mode::ServicePicker); app.handle_action(Action::ExitFilterMode);
14142 assert_eq!(app.mode, Mode::Normal);
14143 }
14144
14145 #[test]
14146 fn test_service_picker_navigation_only_works_in_normal_mode() {
14147 let mut app = test_app();
14148 app.service_picker.selected = 0;
14149
14150 assert!(!app.service_picker.filter_active);
14152 app.handle_action(Action::NextItem);
14153 assert_eq!(app.service_picker.selected, 1);
14154
14155 app.handle_action(Action::EnterFilterMode);
14157 assert!(app.service_picker.filter_active);
14158
14159 let prev_selected = app.service_picker.selected;
14161 app.handle_action(Action::NextItem);
14162 assert_eq!(app.service_picker.selected, prev_selected); app.handle_action(Action::ExitFilterMode);
14166 assert!(!app.service_picker.filter_active);
14167
14168 app.handle_action(Action::NextItem);
14170 assert_eq!(app.service_picker.selected, prev_selected + 1);
14171 }
14172
14173 #[test]
14174 fn test_service_picker_typing_filters_services() {
14175 let mut app = test_app();
14176
14177 assert_eq!(app.mode, Mode::ServicePicker);
14179 assert!(!app.service_picker.filter_active);
14180
14181 app.handle_action(Action::EnterFilterMode);
14183 assert!(app.service_picker.filter_active);
14184
14185 app.handle_action(Action::FilterInput('s'));
14187 app.handle_action(Action::FilterInput('3'));
14188
14189 assert_eq!(app.service_picker.filter, "s3");
14190 assert_eq!(app.mode, Mode::ServicePicker);
14191 }
14192
14193 #[test]
14194 fn test_service_picker_enter_exits_insert_mode() {
14195 let mut app = test_app();
14196 assert_eq!(app.mode, Mode::ServicePicker);
14197 assert!(!app.service_selected);
14198
14199 app.handle_action(Action::EnterFilterMode);
14201 assert!(app.service_picker.filter_active);
14202
14203 app.handle_action(Action::FilterInput('s'));
14205 app.handle_action(Action::FilterInput('3'));
14206
14207 app.handle_action(Action::Select);
14209 assert!(!app.service_selected, "Should not select service");
14210 assert!(!app.service_picker.filter_active, "Should exit INSERT mode");
14211 assert_eq!(app.mode, Mode::ServicePicker);
14212
14213 app.handle_action(Action::Select);
14215 assert!(app.service_selected, "Should select service in NORMAL mode");
14216 assert_eq!(app.mode, Mode::Normal);
14217 }
14218
14219 #[test]
14220 fn test_service_picker_resets_on_open() {
14221 let mut app = test_app();
14222
14223 app.service_selected = true;
14225 app.mode = Mode::Normal;
14226
14227 app.service_picker.filter = "previous".to_string();
14229 app.service_picker.filter_active = true;
14230 app.service_picker.selected = 5;
14231
14232 app.handle_action(Action::OpenSpaceMenu);
14234
14235 assert_eq!(app.mode, Mode::SpaceMenu);
14237 assert!(app.service_picker.filter.is_empty());
14238 assert!(!app.service_picker.filter_active);
14239 assert_eq!(app.service_picker.selected, 0);
14240 }
14241
14242 #[test]
14243 fn test_no_pii_in_test_data() {
14244 let test_repo = EcrRepository {
14246 name: "test-repo".to_string(),
14247 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
14248 created_at: "2024-01-01".to_string(),
14249 tag_immutability: "MUTABLE".to_string(),
14250 encryption_type: "AES256".to_string(),
14251 };
14252
14253 assert!(test_repo.uri.starts_with("123456789012"));
14255 assert!(!test_repo.uri.contains("123456789013")); }
14257
14258 #[test]
14259 fn test_lambda_versions_tab_triggers_loading() {
14260 let mut app = test_app();
14261 app.current_service = Service::LambdaFunctions;
14262 app.service_selected = true;
14263
14264 app.lambda_state.current_function = Some("test-function".to_string());
14266 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14267
14268 assert!(app.lambda_state.version_table.items.is_empty());
14270
14271 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14273
14274 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14277 assert!(app.lambda_state.current_function.is_some());
14278 }
14279
14280 #[test]
14281 fn test_lambda_versions_navigation() {
14282 let mut app = test_app();
14283 app.current_service = Service::LambdaFunctions;
14284 app.service_selected = true;
14285 app.lambda_state.current_function = Some("test-function".to_string());
14286 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14287
14288 app.lambda_state.version_table.items = vec![
14290 LambdaVersion {
14291 version: "3".to_string(),
14292 aliases: "prod".to_string(),
14293 description: "".to_string(),
14294 last_modified: "".to_string(),
14295 architecture: "X86_64".to_string(),
14296 },
14297 LambdaVersion {
14298 version: "2".to_string(),
14299 aliases: "".to_string(),
14300 description: "".to_string(),
14301 last_modified: "".to_string(),
14302 architecture: "X86_64".to_string(),
14303 },
14304 LambdaVersion {
14305 version: "1".to_string(),
14306 aliases: "".to_string(),
14307 description: "".to_string(),
14308 last_modified: "".to_string(),
14309 architecture: "X86_64".to_string(),
14310 },
14311 ];
14312
14313 assert_eq!(app.lambda_state.version_table.items.len(), 3);
14315 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
14316 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
14317
14318 app.lambda_state.version_table.selected = 1;
14320 assert_eq!(app.lambda_state.version_table.selected, 1);
14321 }
14322
14323 #[test]
14324 fn test_lambda_versions_with_aliases() {
14325 let version = LambdaVersion {
14326 version: "35".to_string(),
14327 aliases: "prod, staging".to_string(),
14328 description: "Production version".to_string(),
14329 last_modified: "2024-01-01".to_string(),
14330 architecture: "X86_64".to_string(),
14331 };
14332
14333 assert_eq!(version.aliases, "prod, staging");
14334 assert!(!version.aliases.is_empty());
14335 }
14336
14337 #[test]
14338 fn test_lambda_versions_expansion() {
14339 let mut app = test_app();
14340 app.current_service = Service::LambdaFunctions;
14341 app.service_selected = true;
14342 app.lambda_state.current_function = Some("test-function".to_string());
14343 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14344
14345 app.lambda_state.version_table.items = vec![
14347 LambdaVersion {
14348 version: "2".to_string(),
14349 aliases: "prod".to_string(),
14350 description: "Production".to_string(),
14351 last_modified: "2024-01-01".to_string(),
14352 architecture: "X86_64".to_string(),
14353 },
14354 LambdaVersion {
14355 version: "1".to_string(),
14356 aliases: "".to_string(),
14357 description: "".to_string(),
14358 last_modified: "2024-01-01".to_string(),
14359 architecture: "Arm64".to_string(),
14360 },
14361 ];
14362
14363 app.lambda_state.version_table.selected = 0;
14364
14365 app.lambda_state.version_table.expanded_item = Some(0);
14367 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
14368
14369 app.lambda_state.version_table.selected = 1;
14371 app.lambda_state.version_table.expanded_item = Some(1);
14372 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
14373 }
14374
14375 #[test]
14376 fn test_lambda_versions_page_navigation() {
14377 let mut app = test_app();
14378 app.current_service = Service::LambdaFunctions;
14379 app.service_selected = true;
14380 app.lambda_state.current_function = Some("test-function".to_string());
14381 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14382
14383 app.lambda_state.version_table.items = (1..=30)
14385 .map(|i| LambdaVersion {
14386 version: i.to_string(),
14387 aliases: "".to_string(),
14388 description: "".to_string(),
14389 last_modified: "".to_string(),
14390 architecture: "X86_64".to_string(),
14391 })
14392 .collect();
14393
14394 app.lambda_state.version_table.page_size = PageSize::Ten;
14395 app.lambda_state.version_table.selected = 0;
14396
14397 app.page_input = "2".to_string();
14399 app.handle_action(Action::OpenColumnSelector);
14400
14401 assert_eq!(app.lambda_state.version_table.selected, 10);
14403 }
14404
14405 #[test]
14406 fn test_lambda_versions_pagination_arrow_keys() {
14407 let mut app = test_app();
14408 app.current_service = Service::LambdaFunctions;
14409 app.service_selected = true;
14410 app.lambda_state.current_function = Some("test-function".to_string());
14411 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14412 app.mode = Mode::FilterInput;
14413 app.lambda_state.version_input_focus = InputFocus::Pagination;
14414
14415 app.lambda_state.version_table.items = (1..=30)
14417 .map(|i| LambdaVersion {
14418 version: i.to_string(),
14419 aliases: "".to_string(),
14420 description: "".to_string(),
14421 last_modified: "".to_string(),
14422 architecture: "X86_64".to_string(),
14423 })
14424 .collect();
14425
14426 app.lambda_state.version_table.page_size = PageSize::Ten;
14427 app.lambda_state.version_table.selected = 0;
14428
14429 app.handle_action(Action::PageDown);
14431 assert_eq!(app.lambda_state.version_table.selected, 10);
14432
14433 app.handle_action(Action::PageUp);
14435 assert_eq!(app.lambda_state.version_table.selected, 0);
14436 }
14437
14438 #[test]
14439 fn test_lambda_versions_page_input_in_filter_mode() {
14440 let mut app = test_app();
14441 app.current_service = Service::LambdaFunctions;
14442 app.service_selected = true;
14443 app.lambda_state.current_function = Some("test-function".to_string());
14444 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14445 app.mode = Mode::FilterInput;
14446 app.lambda_state.version_input_focus = InputFocus::Pagination;
14447
14448 app.lambda_state.version_table.items = (1..=30)
14450 .map(|i| LambdaVersion {
14451 version: i.to_string(),
14452 aliases: "".to_string(),
14453 description: "".to_string(),
14454 last_modified: "".to_string(),
14455 architecture: "X86_64".to_string(),
14456 })
14457 .collect();
14458
14459 app.lambda_state.version_table.page_size = PageSize::Ten;
14460 app.lambda_state.version_table.selected = 0;
14461
14462 app.handle_action(Action::FilterInput('2'));
14464 assert_eq!(app.page_input, "2");
14465 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
14469 assert_eq!(app.lambda_state.version_table.selected, 10);
14470 assert_eq!(app.page_input, ""); }
14472
14473 #[test]
14474 fn test_lambda_versions_filter_input() {
14475 let mut app = test_app();
14476 app.current_service = Service::LambdaFunctions;
14477 app.service_selected = true;
14478 app.lambda_state.current_function = Some("test-function".to_string());
14479 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14480 app.mode = Mode::FilterInput;
14481 app.lambda_state.version_input_focus = InputFocus::Filter;
14482
14483 app.lambda_state.version_table.items = vec![
14485 LambdaVersion {
14486 version: "1".to_string(),
14487 aliases: "prod".to_string(),
14488 description: "Production".to_string(),
14489 last_modified: "".to_string(),
14490 architecture: "X86_64".to_string(),
14491 },
14492 LambdaVersion {
14493 version: "2".to_string(),
14494 aliases: "staging".to_string(),
14495 description: "Staging".to_string(),
14496 last_modified: "".to_string(),
14497 architecture: "X86_64".to_string(),
14498 },
14499 ];
14500
14501 app.handle_action(Action::FilterInput('p'));
14503 app.handle_action(Action::FilterInput('r'));
14504 app.handle_action(Action::FilterInput('o'));
14505 app.handle_action(Action::FilterInput('d'));
14506 assert_eq!(app.lambda_state.version_table.filter, "prod");
14507
14508 app.handle_action(Action::FilterBackspace);
14510 assert_eq!(app.lambda_state.version_table.filter, "pro");
14511 }
14512
14513 #[test]
14514 fn test_lambda_aliases_table_expansion() {
14515 use crate::lambda::Alias;
14516
14517 let mut app = test_app();
14518 app.current_service = Service::LambdaFunctions;
14519 app.service_selected = true;
14520 app.lambda_state.current_function = Some("test-function".to_string());
14521 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
14522 app.mode = Mode::Normal;
14523
14524 app.lambda_state.alias_table.items = vec![
14525 Alias {
14526 name: "prod".to_string(),
14527 versions: "1".to_string(),
14528 description: "Production alias".to_string(),
14529 },
14530 Alias {
14531 name: "staging".to_string(),
14532 versions: "2".to_string(),
14533 description: "Staging alias".to_string(),
14534 },
14535 ];
14536
14537 app.lambda_state.alias_table.selected = 0;
14538
14539 app.handle_action(Action::Select);
14541 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
14542 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14543
14544 app.handle_action(Action::GoBack);
14546 assert_eq!(app.lambda_state.current_alias, None);
14547 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14548
14549 app.lambda_state.alias_table.selected = 1;
14551 app.handle_action(Action::Select);
14552 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
14553 }
14554
14555 #[test]
14556 fn test_lambda_versions_arrow_key_expansion() {
14557 let mut app = test_app();
14558 app.current_service = Service::LambdaFunctions;
14559 app.service_selected = true;
14560 app.lambda_state.current_function = Some("test-function".to_string());
14561 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14562 app.mode = Mode::Normal;
14563
14564 app.lambda_state.version_table.items = vec![LambdaVersion {
14565 version: "1".to_string(),
14566 aliases: "prod".to_string(),
14567 description: "Production".to_string(),
14568 last_modified: "2024-01-01".to_string(),
14569 architecture: "X86_64".to_string(),
14570 }];
14571
14572 app.lambda_state.version_table.selected = 0;
14573
14574 app.handle_action(Action::NextPane);
14576 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
14577
14578 app.handle_action(Action::PrevPane);
14580 assert_eq!(app.lambda_state.version_table.expanded_item, None);
14581 }
14582
14583 #[test]
14584 fn test_lambda_version_detail_view() {
14585 use crate::lambda::Function;
14586
14587 let mut app = test_app();
14588 app.current_service = Service::LambdaFunctions;
14589 app.service_selected = true;
14590 app.lambda_state.current_function = Some("test-function".to_string());
14591 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14592 app.mode = Mode::Normal;
14593
14594 app.lambda_state.table.items = vec![Function {
14595 name: "test-function".to_string(),
14596 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14597 application: None,
14598 description: "Test".to_string(),
14599 package_type: "Zip".to_string(),
14600 runtime: "python3.12".to_string(),
14601 architecture: "X86_64".to_string(),
14602 code_size: 1024,
14603 code_sha256: "hash".to_string(),
14604 memory_mb: 128,
14605 timeout_seconds: 30,
14606 last_modified: "2024-01-01".to_string(),
14607 layers: vec![],
14608 }];
14609
14610 app.lambda_state.version_table.items = vec![LambdaVersion {
14611 version: "1".to_string(),
14612 aliases: "prod".to_string(),
14613 description: "Production".to_string(),
14614 last_modified: "2024-01-01".to_string(),
14615 architecture: "X86_64".to_string(),
14616 }];
14617
14618 app.lambda_state.version_table.selected = 0;
14619
14620 app.handle_action(Action::Select);
14622 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
14623 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14624
14625 app.handle_action(Action::GoBack);
14627 assert_eq!(app.lambda_state.current_version, None);
14628 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14629 }
14630
14631 #[test]
14632 fn test_lambda_version_detail_tabs() {
14633 use crate::lambda::Function;
14634
14635 let mut app = test_app();
14636 app.current_service = Service::LambdaFunctions;
14637 app.service_selected = true;
14638 app.lambda_state.current_function = Some("test-function".to_string());
14639 app.lambda_state.current_version = Some("1".to_string());
14640 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14641 app.mode = Mode::Normal;
14642
14643 app.lambda_state.table.items = vec![Function {
14644 name: "test-function".to_string(),
14645 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14646 application: None,
14647 description: "Test".to_string(),
14648 package_type: "Zip".to_string(),
14649 runtime: "python3.12".to_string(),
14650 architecture: "X86_64".to_string(),
14651 code_size: 1024,
14652 code_sha256: "hash".to_string(),
14653 memory_mb: 128,
14654 timeout_seconds: 30,
14655 last_modified: "2024-01-01".to_string(),
14656 layers: vec![],
14657 }];
14658
14659 app.handle_action(Action::NextDetailTab);
14661 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14662
14663 app.handle_action(Action::NextDetailTab);
14664 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14665
14666 app.handle_action(Action::NextDetailTab);
14667 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14668
14669 app.handle_action(Action::PrevDetailTab);
14671 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14672
14673 app.handle_action(Action::PrevDetailTab);
14674 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14675 }
14676
14677 #[test]
14678 fn test_lambda_aliases_arrow_key_expansion() {
14679 use crate::lambda::Alias;
14680
14681 let mut app = test_app();
14682 app.current_service = Service::LambdaFunctions;
14683 app.service_selected = true;
14684 app.lambda_state.current_function = Some("test-function".to_string());
14685 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
14686 app.mode = Mode::Normal;
14687
14688 app.lambda_state.alias_table.items = vec![Alias {
14689 name: "prod".to_string(),
14690 versions: "1".to_string(),
14691 description: "Production alias".to_string(),
14692 }];
14693
14694 app.lambda_state.alias_table.selected = 0;
14695
14696 app.handle_action(Action::NextPane);
14698 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
14699
14700 app.handle_action(Action::PrevPane);
14702 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
14703 }
14704
14705 #[test]
14706 fn test_lambda_functions_arrow_key_expansion() {
14707 use crate::lambda::Function;
14708
14709 let mut app = test_app();
14710 app.current_service = Service::LambdaFunctions;
14711 app.service_selected = true;
14712 app.mode = Mode::Normal;
14713
14714 app.lambda_state.table.items = vec![Function {
14715 name: "test-function".to_string(),
14716 arn: "arn".to_string(),
14717 application: None,
14718 description: "Test".to_string(),
14719 package_type: "Zip".to_string(),
14720 runtime: "python3.12".to_string(),
14721 architecture: "X86_64".to_string(),
14722 code_size: 1024,
14723 code_sha256: "hash".to_string(),
14724 memory_mb: 128,
14725 timeout_seconds: 30,
14726 last_modified: "2024-01-01".to_string(),
14727 layers: vec![],
14728 }];
14729
14730 app.lambda_state.table.selected = 0;
14731
14732 app.handle_action(Action::NextPane);
14734 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
14735
14736 app.handle_action(Action::PrevPane);
14738 assert_eq!(app.lambda_state.table.expanded_item, None);
14739 }
14740
14741 #[test]
14742 fn test_lambda_version_detail_with_application() {
14743 use crate::lambda::Function;
14744
14745 let mut app = test_app();
14746 app.current_service = Service::LambdaFunctions;
14747 app.service_selected = true;
14748 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
14749 app.lambda_state.current_version = Some("1".to_string());
14750 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14751 app.mode = Mode::Normal;
14752
14753 app.lambda_state.table.items = vec![Function {
14754 name: "storefront-studio-beta-api".to_string(),
14755 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
14756 .to_string(),
14757 application: Some("storefront-studio-beta".to_string()),
14758 description: "API function".to_string(),
14759 package_type: "Zip".to_string(),
14760 runtime: "python3.12".to_string(),
14761 architecture: "X86_64".to_string(),
14762 code_size: 1024,
14763 code_sha256: "hash".to_string(),
14764 memory_mb: 128,
14765 timeout_seconds: 30,
14766 last_modified: "2024-01-01".to_string(),
14767 layers: vec![],
14768 }];
14769
14770 assert_eq!(
14772 app.lambda_state.table.items[0].application,
14773 Some("storefront-studio-beta".to_string())
14774 );
14775 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
14776 }
14777
14778 #[test]
14779 fn test_lambda_layer_navigation() {
14780 use crate::lambda::{Function, Layer};
14781
14782 let mut app = test_app();
14783 app.current_service = Service::LambdaFunctions;
14784 app.service_selected = true;
14785 app.lambda_state.current_function = Some("test-function".to_string());
14786 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14787 app.mode = Mode::Normal;
14788
14789 app.lambda_state.table.items = vec![Function {
14790 name: "test-function".to_string(),
14791 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14792 application: None,
14793 description: "Test".to_string(),
14794 package_type: "Zip".to_string(),
14795 runtime: "python3.12".to_string(),
14796 architecture: "X86_64".to_string(),
14797 code_size: 1024,
14798 code_sha256: "hash".to_string(),
14799 memory_mb: 128,
14800 timeout_seconds: 30,
14801 last_modified: "2024-01-01".to_string(),
14802 layers: vec![
14803 Layer {
14804 merge_order: "1".to_string(),
14805 name: "layer1".to_string(),
14806 layer_version: "1".to_string(),
14807 compatible_runtimes: "python3.9".to_string(),
14808 compatible_architectures: "x86_64".to_string(),
14809 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
14810 },
14811 Layer {
14812 merge_order: "2".to_string(),
14813 name: "layer2".to_string(),
14814 layer_version: "2".to_string(),
14815 compatible_runtimes: "python3.9".to_string(),
14816 compatible_architectures: "x86_64".to_string(),
14817 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
14818 },
14819 Layer {
14820 merge_order: "3".to_string(),
14821 name: "layer3".to_string(),
14822 layer_version: "3".to_string(),
14823 compatible_runtimes: "python3.9".to_string(),
14824 compatible_architectures: "x86_64".to_string(),
14825 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
14826 },
14827 ],
14828 }];
14829
14830 assert_eq!(app.lambda_state.layer_selected, 0);
14831
14832 app.handle_action(Action::NextItem);
14833 assert_eq!(app.lambda_state.layer_selected, 1);
14834
14835 app.handle_action(Action::NextItem);
14836 assert_eq!(app.lambda_state.layer_selected, 2);
14837
14838 app.handle_action(Action::NextItem);
14839 assert_eq!(app.lambda_state.layer_selected, 2);
14840
14841 app.handle_action(Action::PrevItem);
14842 assert_eq!(app.lambda_state.layer_selected, 1);
14843
14844 app.handle_action(Action::PrevItem);
14845 assert_eq!(app.lambda_state.layer_selected, 0);
14846
14847 app.handle_action(Action::PrevItem);
14848 assert_eq!(app.lambda_state.layer_selected, 0);
14849 }
14850
14851 #[test]
14852 fn test_lambda_layer_expansion() {
14853 use crate::lambda::{Function, Layer};
14854
14855 let mut app = test_app();
14856 app.current_service = Service::LambdaFunctions;
14857 app.service_selected = true;
14858 app.lambda_state.current_function = Some("test-function".to_string());
14859 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14860 app.mode = Mode::Normal;
14861
14862 app.lambda_state.table.items = vec![Function {
14863 name: "test-function".to_string(),
14864 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14865 application: None,
14866 description: "Test".to_string(),
14867 package_type: "Zip".to_string(),
14868 runtime: "python3.12".to_string(),
14869 architecture: "X86_64".to_string(),
14870 code_size: 1024,
14871 code_sha256: "hash".to_string(),
14872 memory_mb: 128,
14873 timeout_seconds: 30,
14874 last_modified: "2024-01-01".to_string(),
14875 layers: vec![Layer {
14876 merge_order: "1".to_string(),
14877 name: "test-layer".to_string(),
14878 layer_version: "1".to_string(),
14879 compatible_runtimes: "python3.9".to_string(),
14880 compatible_architectures: "x86_64".to_string(),
14881 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
14882 }],
14883 }];
14884
14885 assert_eq!(app.lambda_state.layer_expanded, None);
14886
14887 app.handle_action(Action::NextPane);
14888 assert_eq!(app.lambda_state.layer_expanded, Some(0));
14889
14890 app.handle_action(Action::PrevPane);
14891 assert_eq!(app.lambda_state.layer_expanded, None);
14892
14893 app.handle_action(Action::NextPane);
14894 assert_eq!(app.lambda_state.layer_expanded, Some(0));
14895
14896 app.handle_action(Action::NextPane);
14897 assert_eq!(app.lambda_state.layer_expanded, None);
14898 }
14899
14900 #[test]
14901 fn test_lambda_layer_selection_and_expansion_workflow() {
14902 use crate::lambda::{Function, Layer};
14903
14904 let mut app = test_app();
14905 app.current_service = Service::LambdaFunctions;
14906 app.service_selected = true;
14907 app.lambda_state.current_function = Some("test-function".to_string());
14908 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14909 app.mode = Mode::Normal;
14910
14911 app.lambda_state.table.items = vec![Function {
14912 name: "test-function".to_string(),
14913 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14914 application: None,
14915 description: "Test".to_string(),
14916 package_type: "Zip".to_string(),
14917 runtime: "python3.12".to_string(),
14918 architecture: "X86_64".to_string(),
14919 code_size: 1024,
14920 code_sha256: "hash".to_string(),
14921 memory_mb: 128,
14922 timeout_seconds: 30,
14923 last_modified: "2024-01-01".to_string(),
14924 layers: vec![
14925 Layer {
14926 merge_order: "1".to_string(),
14927 name: "layer1".to_string(),
14928 layer_version: "1".to_string(),
14929 compatible_runtimes: "python3.9".to_string(),
14930 compatible_architectures: "x86_64".to_string(),
14931 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
14932 },
14933 Layer {
14934 merge_order: "2".to_string(),
14935 name: "layer2".to_string(),
14936 layer_version: "2".to_string(),
14937 compatible_runtimes: "python3.9".to_string(),
14938 compatible_architectures: "x86_64".to_string(),
14939 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
14940 },
14941 ],
14942 }];
14943
14944 assert_eq!(app.lambda_state.layer_selected, 0);
14946 assert_eq!(app.lambda_state.layer_expanded, None);
14947
14948 app.handle_action(Action::NextPane);
14950 assert_eq!(app.lambda_state.layer_selected, 0);
14951 assert_eq!(app.lambda_state.layer_expanded, Some(0));
14952
14953 app.handle_action(Action::NextItem);
14955 assert_eq!(app.lambda_state.layer_selected, 1);
14956 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
14960 assert_eq!(app.lambda_state.layer_selected, 1);
14961 assert_eq!(app.lambda_state.layer_expanded, Some(1));
14962
14963 app.handle_action(Action::PrevPane);
14965 assert_eq!(app.lambda_state.layer_selected, 1);
14966 assert_eq!(app.lambda_state.layer_expanded, None);
14967
14968 app.handle_action(Action::PrevItem);
14970 assert_eq!(app.lambda_state.layer_selected, 0);
14971 assert_eq!(app.lambda_state.layer_expanded, None);
14972 }
14973
14974 #[test]
14975 fn test_backtab_cycles_detail_tabs_backward() {
14976 let mut app = test_app();
14977 app.mode = Mode::Normal;
14978
14979 app.current_service = Service::LambdaFunctions;
14981 app.service_selected = true;
14982 app.lambda_state.current_function = Some("test-function".to_string());
14983 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14984
14985 app.handle_action(Action::PrevDetailTab);
14986 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14987
14988 app.handle_action(Action::PrevDetailTab);
14989 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14990
14991 app.current_service = Service::IamRoles;
14993 app.iam_state.current_role = Some("test-role".to_string());
14994 app.iam_state.role_tab = RoleTab::Permissions;
14995
14996 app.handle_action(Action::PrevDetailTab);
14997 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
14998
14999 app.current_service = Service::IamUsers;
15001 app.iam_state.current_user = Some("test-user".to_string());
15002 app.iam_state.user_tab = UserTab::Permissions;
15003
15004 app.handle_action(Action::PrevDetailTab);
15005 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
15006
15007 app.current_service = Service::IamUserGroups;
15009 app.iam_state.current_group = Some("test-group".to_string());
15010 app.iam_state.group_tab = GroupTab::Permissions;
15011
15012 app.handle_action(Action::PrevDetailTab);
15013 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
15014
15015 app.current_service = Service::S3Buckets;
15017 app.s3_state.current_bucket = Some("test-bucket".to_string());
15018 app.s3_state.object_tab = S3ObjectTab::Properties;
15019
15020 app.handle_action(Action::PrevDetailTab);
15021 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
15022
15023 app.current_service = Service::EcrRepositories;
15025 app.ecr_state.current_repository = None;
15026 app.ecr_state.tab = EcrTab::Private;
15027
15028 app.handle_action(Action::PrevDetailTab);
15029 assert_eq!(app.ecr_state.tab, EcrTab::Public);
15030
15031 app.current_service = Service::CloudFormationStacks;
15033 app.cfn_state.current_stack = Some("test-stack".to_string());
15034 app.cfn_state.detail_tab = CfnDetailTab::Resources;
15035 }
15036
15037 #[test]
15038 fn test_cloudformation_status_filter_active() {
15039 let filter = CfnStatusFilter::Active;
15040 assert!(filter.matches("CREATE_IN_PROGRESS"));
15041 assert!(filter.matches("UPDATE_IN_PROGRESS"));
15042 assert!(!filter.matches("CREATE_COMPLETE"));
15043 assert!(!filter.matches("DELETE_COMPLETE"));
15044 assert!(!filter.matches("CREATE_FAILED"));
15045 }
15046
15047 #[test]
15048 fn test_cloudformation_status_filter_complete() {
15049 let filter = CfnStatusFilter::Complete;
15050 assert!(filter.matches("CREATE_COMPLETE"));
15051 assert!(filter.matches("UPDATE_COMPLETE"));
15052 assert!(!filter.matches("DELETE_COMPLETE"));
15053 assert!(!filter.matches("CREATE_IN_PROGRESS"));
15054 }
15055
15056 #[test]
15057 fn test_cloudformation_status_filter_failed() {
15058 let filter = CfnStatusFilter::Failed;
15059 assert!(filter.matches("CREATE_FAILED"));
15060 assert!(filter.matches("UPDATE_FAILED"));
15061 assert!(!filter.matches("CREATE_COMPLETE"));
15062 }
15063
15064 #[test]
15065 fn test_cloudformation_status_filter_deleted() {
15066 let filter = CfnStatusFilter::Deleted;
15067 assert!(filter.matches("DELETE_COMPLETE"));
15068 assert!(filter.matches("DELETE_IN_PROGRESS"));
15069 assert!(!filter.matches("CREATE_COMPLETE"));
15070 }
15071
15072 #[test]
15073 fn test_cloudformation_status_filter_in_progress() {
15074 let filter = CfnStatusFilter::InProgress;
15075 assert!(filter.matches("CREATE_IN_PROGRESS"));
15076 assert!(filter.matches("UPDATE_IN_PROGRESS"));
15077 assert!(filter.matches("DELETE_IN_PROGRESS"));
15078 assert!(!filter.matches("CREATE_COMPLETE"));
15079 }
15080
15081 #[test]
15082 fn test_cloudformation_status_filter_cycle() {
15083 let filter = CfnStatusFilter::All;
15084 assert_eq!(filter.next(), CfnStatusFilter::Active);
15085 assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
15086 assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
15087 assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
15088 assert_eq!(
15089 filter.next().next().next().next().next(),
15090 CfnStatusFilter::InProgress
15091 );
15092 assert_eq!(
15093 filter.next().next().next().next().next().next(),
15094 CfnStatusFilter::All
15095 );
15096 }
15097
15098 #[test]
15099 fn test_cloudformation_default_columns() {
15100 let app = test_app();
15101 assert_eq!(app.cfn_visible_column_ids.len(), 4);
15102 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
15103 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
15104 assert!(app
15105 .cfn_visible_column_ids
15106 .contains(&CfnColumn::CreatedTime.id()));
15107 assert!(app
15108 .cfn_visible_column_ids
15109 .contains(&CfnColumn::Description.id()));
15110 }
15111
15112 #[test]
15113 fn test_cloudformation_all_columns() {
15114 let app = test_app();
15115 assert_eq!(app.cfn_column_ids.len(), 10);
15116 }
15117
15118 #[test]
15119 fn test_cloudformation_filter_by_name() {
15120 let mut app = test_app();
15121 app.cfn_state.status_filter = CfnStatusFilter::Complete;
15122 app.cfn_state.table.items = vec![
15123 CfnStack {
15124 name: "my-stack".to_string(),
15125 stack_id: "id1".to_string(),
15126 status: "CREATE_COMPLETE".to_string(),
15127 created_time: "2024-01-01".to_string(),
15128 updated_time: String::new(),
15129 deleted_time: String::new(),
15130 drift_status: String::new(),
15131 last_drift_check_time: String::new(),
15132 status_reason: String::new(),
15133 description: String::new(),
15134 detailed_status: String::new(),
15135 root_stack: String::new(),
15136 parent_stack: String::new(),
15137 termination_protection: false,
15138 iam_role: String::new(),
15139 tags: Vec::new(),
15140 stack_policy: String::new(),
15141 rollback_monitoring_time: String::new(),
15142 rollback_alarms: Vec::new(),
15143 notification_arns: Vec::new(),
15144 },
15145 CfnStack {
15146 name: "other-stack".to_string(),
15147 stack_id: "id2".to_string(),
15148 status: "CREATE_COMPLETE".to_string(),
15149 created_time: "2024-01-02".to_string(),
15150 updated_time: String::new(),
15151 deleted_time: String::new(),
15152 drift_status: String::new(),
15153 last_drift_check_time: String::new(),
15154 status_reason: String::new(),
15155 description: String::new(),
15156 detailed_status: String::new(),
15157 root_stack: String::new(),
15158 parent_stack: String::new(),
15159 termination_protection: false,
15160 iam_role: String::new(),
15161 tags: Vec::new(),
15162 stack_policy: String::new(),
15163 rollback_monitoring_time: String::new(),
15164 rollback_alarms: Vec::new(),
15165 notification_arns: Vec::new(),
15166 },
15167 ];
15168
15169 app.cfn_state.table.filter = "my".to_string();
15170 let filtered = filtered_cloudformation_stacks(&app);
15171 assert_eq!(filtered.len(), 1);
15172 assert_eq!(filtered[0].name, "my-stack");
15173 }
15174
15175 #[test]
15176 fn test_cloudformation_filter_by_description() {
15177 let mut app = test_app();
15178 app.cfn_state.status_filter = CfnStatusFilter::Complete;
15179 app.cfn_state.table.items = vec![CfnStack {
15180 name: "stack1".to_string(),
15181 stack_id: "id1".to_string(),
15182 status: "CREATE_COMPLETE".to_string(),
15183 created_time: "2024-01-01".to_string(),
15184 updated_time: String::new(),
15185 deleted_time: String::new(),
15186 drift_status: String::new(),
15187 last_drift_check_time: String::new(),
15188 status_reason: String::new(),
15189 description: "production stack".to_string(),
15190 detailed_status: String::new(),
15191 root_stack: String::new(),
15192 parent_stack: String::new(),
15193 termination_protection: false,
15194 iam_role: String::new(),
15195 tags: Vec::new(),
15196 stack_policy: String::new(),
15197 rollback_monitoring_time: String::new(),
15198 rollback_alarms: Vec::new(),
15199 notification_arns: Vec::new(),
15200 }];
15201
15202 app.cfn_state.table.filter = "production".to_string();
15203 let filtered = filtered_cloudformation_stacks(&app);
15204 assert_eq!(filtered.len(), 1);
15205 }
15206
15207 #[test]
15208 fn test_cloudformation_status_filter_applied() {
15209 let mut app = test_app();
15210 app.cfn_state.table.items = vec![
15211 CfnStack {
15212 name: "complete-stack".to_string(),
15213 stack_id: "id1".to_string(),
15214 status: "CREATE_COMPLETE".to_string(),
15215 created_time: "2024-01-01".to_string(),
15216 updated_time: String::new(),
15217 deleted_time: String::new(),
15218 drift_status: String::new(),
15219 last_drift_check_time: String::new(),
15220 status_reason: String::new(),
15221 description: String::new(),
15222 detailed_status: String::new(),
15223 root_stack: String::new(),
15224 parent_stack: String::new(),
15225 termination_protection: false,
15226 iam_role: String::new(),
15227 tags: Vec::new(),
15228 stack_policy: String::new(),
15229 rollback_monitoring_time: String::new(),
15230 rollback_alarms: Vec::new(),
15231 notification_arns: Vec::new(),
15232 },
15233 CfnStack {
15234 name: "failed-stack".to_string(),
15235 stack_id: "id2".to_string(),
15236 status: "CREATE_FAILED".to_string(),
15237 created_time: "2024-01-02".to_string(),
15238 updated_time: String::new(),
15239 deleted_time: String::new(),
15240 drift_status: String::new(),
15241 last_drift_check_time: String::new(),
15242 status_reason: String::new(),
15243 description: String::new(),
15244 detailed_status: String::new(),
15245 root_stack: String::new(),
15246 parent_stack: String::new(),
15247 termination_protection: false,
15248 iam_role: String::new(),
15249 tags: Vec::new(),
15250 stack_policy: String::new(),
15251 rollback_monitoring_time: String::new(),
15252 rollback_alarms: Vec::new(),
15253 notification_arns: Vec::new(),
15254 },
15255 ];
15256
15257 app.cfn_state.status_filter = CfnStatusFilter::Complete;
15258 let filtered = filtered_cloudformation_stacks(&app);
15259 assert_eq!(filtered.len(), 1);
15260 assert_eq!(filtered[0].name, "complete-stack");
15261
15262 app.cfn_state.status_filter = CfnStatusFilter::Failed;
15263 let filtered = filtered_cloudformation_stacks(&app);
15264 assert_eq!(filtered.len(), 1);
15265 assert_eq!(filtered[0].name, "failed-stack");
15266 }
15267
15268 #[test]
15269 fn test_cloudformation_default_page_size() {
15270 let app = test_app();
15271 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
15272 }
15273
15274 #[test]
15275 fn test_cloudformation_default_status_filter() {
15276 let app = test_app();
15277 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
15278 }
15279
15280 #[test]
15281 fn test_cloudformation_view_nested_default_false() {
15282 let app = test_app();
15283 assert!(!app.cfn_state.view_nested);
15284 }
15285
15286 #[test]
15287 fn test_cloudformation_pagination_hotkeys() {
15288 let mut app = test_app();
15289 app.current_service = Service::CloudFormationStacks;
15290 app.service_selected = true;
15291 app.cfn_state.status_filter = CfnStatusFilter::All;
15292
15293 for i in 0..150 {
15295 app.cfn_state.table.items.push(CfnStack {
15296 name: format!("stack-{}", i),
15297 stack_id: format!("id-{}", i),
15298 status: "CREATE_COMPLETE".to_string(),
15299 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15300 updated_time: String::new(),
15301 deleted_time: String::new(),
15302 drift_status: String::new(),
15303 last_drift_check_time: String::new(),
15304 status_reason: String::new(),
15305 description: String::new(),
15306 detailed_status: String::new(),
15307 root_stack: String::new(),
15308 parent_stack: String::new(),
15309 termination_protection: false,
15310 iam_role: String::new(),
15311 tags: vec![],
15312 stack_policy: String::new(),
15313 rollback_monitoring_time: String::new(),
15314 rollback_alarms: vec![],
15315 notification_arns: vec![],
15316 });
15317 }
15318
15319 app.go_to_page(2);
15321 assert_eq!(app.cfn_state.table.selected, 50);
15322
15323 app.go_to_page(3);
15325 assert_eq!(app.cfn_state.table.selected, 100);
15326
15327 app.go_to_page(1);
15329 assert_eq!(app.cfn_state.table.selected, 0);
15330 }
15331
15332 #[test]
15333 fn test_cloudformation_tab_cycling_in_filter_mode() {
15334 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15335 let mut app = test_app();
15336 app.current_service = Service::CloudFormationStacks;
15337 app.service_selected = true;
15338 app.mode = Mode::FilterInput;
15339 app.cfn_state.input_focus = InputFocus::Filter;
15340
15341 app.handle_action(Action::NextFilterFocus);
15343 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15344
15345 app.handle_action(Action::NextFilterFocus);
15347 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15348
15349 app.handle_action(Action::NextFilterFocus);
15351 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15352
15353 app.handle_action(Action::NextFilterFocus);
15355 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15356 }
15357
15358 #[test]
15359 fn test_cloudformation_timestamp_format_includes_utc() {
15360 let stack = CfnStack {
15361 name: "test-stack".to_string(),
15362 stack_id: "id-123".to_string(),
15363 status: "CREATE_COMPLETE".to_string(),
15364 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
15365 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
15366 deleted_time: String::new(),
15367 drift_status: String::new(),
15368 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
15369 status_reason: String::new(),
15370 description: String::new(),
15371 detailed_status: String::new(),
15372 root_stack: String::new(),
15373 parent_stack: String::new(),
15374 termination_protection: false,
15375 iam_role: String::new(),
15376 tags: vec![],
15377 stack_policy: String::new(),
15378 rollback_monitoring_time: String::new(),
15379 rollback_alarms: vec![],
15380 notification_arns: vec![],
15381 };
15382
15383 assert!(stack.created_time.contains("(UTC)"));
15384 assert!(stack.updated_time.contains("(UTC)"));
15385 assert!(stack.last_drift_check_time.contains("(UTC)"));
15386 assert_eq!(stack.created_time.len(), 25);
15387 }
15388
15389 #[test]
15390 fn test_cloudformation_enter_drills_into_stack_view() {
15391 let mut app = test_app();
15392 app.current_service = Service::CloudFormationStacks;
15393 app.service_selected = true;
15394 app.mode = Mode::Normal;
15395 app.cfn_state.status_filter = CfnStatusFilter::All;
15396 app.tabs = vec![Tab {
15397 service: Service::CloudFormationStacks,
15398 title: "CloudFormation › Stacks".to_string(),
15399 breadcrumb: "CloudFormation › Stacks".to_string(),
15400 }];
15401 app.current_tab = 0;
15402
15403 app.cfn_state.table.items.push(CfnStack {
15404 name: "test-stack".to_string(),
15405 stack_id: "id-123".to_string(),
15406 status: "CREATE_COMPLETE".to_string(),
15407 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15408 updated_time: String::new(),
15409 deleted_time: String::new(),
15410 drift_status: String::new(),
15411 last_drift_check_time: String::new(),
15412 status_reason: String::new(),
15413 description: String::new(),
15414 detailed_status: String::new(),
15415 root_stack: String::new(),
15416 parent_stack: String::new(),
15417 termination_protection: false,
15418 iam_role: String::new(),
15419 tags: vec![],
15420 stack_policy: String::new(),
15421 rollback_monitoring_time: String::new(),
15422 rollback_alarms: vec![],
15423 notification_arns: vec![],
15424 });
15425
15426 app.cfn_state.table.reset();
15427 assert_eq!(app.cfn_state.current_stack, None);
15428
15429 app.handle_action(Action::Select);
15431 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
15432 }
15433
15434 #[test]
15435 fn test_cloudformation_arrow_keys_expand_collapse() {
15436 let mut app = test_app();
15437 app.current_service = Service::CloudFormationStacks;
15438 app.service_selected = true;
15439 app.mode = Mode::Normal;
15440 app.cfn_state.status_filter = CfnStatusFilter::All;
15441
15442 app.cfn_state.table.items.push(CfnStack {
15443 name: "test-stack".to_string(),
15444 stack_id: "id-123".to_string(),
15445 status: "CREATE_COMPLETE".to_string(),
15446 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15447 updated_time: String::new(),
15448 deleted_time: String::new(),
15449 drift_status: String::new(),
15450 last_drift_check_time: String::new(),
15451 status_reason: String::new(),
15452 description: String::new(),
15453 detailed_status: String::new(),
15454 root_stack: String::new(),
15455 parent_stack: String::new(),
15456 termination_protection: false,
15457 iam_role: String::new(),
15458 tags: vec![],
15459 stack_policy: String::new(),
15460 rollback_monitoring_time: String::new(),
15461 rollback_alarms: vec![],
15462 notification_arns: vec![],
15463 });
15464
15465 app.cfn_state.table.reset();
15466 assert_eq!(app.cfn_state.table.expanded_item, None);
15467
15468 app.handle_action(Action::NextPane);
15470 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
15471
15472 app.handle_action(Action::PrevPane);
15474 assert_eq!(app.cfn_state.table.expanded_item, None);
15475
15476 assert_eq!(app.cfn_state.current_stack, None);
15478 }
15479
15480 #[test]
15481 fn test_cloudformation_tab_cycling() {
15482 use crate::ui::cfn::DetailTab;
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.status_filter = CfnStatusFilter::All;
15488 app.cfn_state.current_stack = Some("test-stack".to_string());
15489
15490 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
15491 }
15492
15493 #[test]
15494 fn test_cloudformation_console_url() {
15495 use crate::ui::cfn::DetailTab;
15496 let mut app = test_app();
15497 app.current_service = Service::CloudFormationStacks;
15498 app.service_selected = true;
15499 app.cfn_state.status_filter = CfnStatusFilter::All;
15500
15501 app.cfn_state.table.items.push(CfnStack {
15502 name: "test-stack".to_string(),
15503 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
15504 .to_string(),
15505 status: "CREATE_COMPLETE".to_string(),
15506 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15507 updated_time: String::new(),
15508 deleted_time: String::new(),
15509 drift_status: String::new(),
15510 last_drift_check_time: String::new(),
15511 status_reason: String::new(),
15512 description: String::new(),
15513 detailed_status: String::new(),
15514 root_stack: String::new(),
15515 parent_stack: String::new(),
15516 termination_protection: false,
15517 iam_role: String::new(),
15518 tags: vec![],
15519 stack_policy: String::new(),
15520 rollback_monitoring_time: String::new(),
15521 rollback_alarms: vec![],
15522 notification_arns: vec![],
15523 });
15524
15525 app.cfn_state.current_stack = Some("test-stack".to_string());
15526
15527 app.cfn_state.detail_tab = DetailTab::StackInfo;
15529 let url = app.get_console_url();
15530 assert!(url.contains("stackinfo"));
15531 assert!(url.contains("arn%3Aaws%3Acloudformation"));
15532
15533 app.cfn_state.detail_tab = DetailTab::Events;
15535 let url = app.get_console_url();
15536 assert!(url.contains("events"));
15537 assert!(url.contains("arn%3Aaws%3Acloudformation"));
15538 }
15539
15540 #[test]
15541 fn test_iam_role_select() {
15542 let mut app = test_app();
15543 app.current_service = Service::IamRoles;
15544 app.service_selected = true;
15545 app.mode = Mode::Normal;
15546
15547 app.iam_state.roles.items = vec![
15548 IamRole {
15549 role_name: "role1".to_string(),
15550 path: "/".to_string(),
15551 trusted_entities: "AWS Service: ec2".to_string(),
15552 last_activity: "-".to_string(),
15553 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
15554 creation_time: "2025-01-01".to_string(),
15555 description: "Test role 1".to_string(),
15556 max_session_duration: Some(3600),
15557 },
15558 IamRole {
15559 role_name: "role2".to_string(),
15560 path: "/".to_string(),
15561 trusted_entities: "AWS Service: lambda".to_string(),
15562 last_activity: "-".to_string(),
15563 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
15564 creation_time: "2025-01-02".to_string(),
15565 description: "Test role 2".to_string(),
15566 max_session_duration: Some(7200),
15567 },
15568 ];
15569
15570 app.iam_state.roles.selected = 0;
15572 app.handle_action(Action::Select);
15573
15574 assert_eq!(
15575 app.iam_state.current_role,
15576 Some("role1".to_string()),
15577 "Should open role detail view"
15578 );
15579 assert_eq!(
15580 app.iam_state.role_tab,
15581 RoleTab::Permissions,
15582 "Should default to Permissions tab"
15583 );
15584 }
15585
15586 #[test]
15587 fn test_iam_role_back_navigation() {
15588 let mut app = test_app();
15589 app.current_service = Service::IamRoles;
15590 app.service_selected = true;
15591 app.iam_state.current_role = Some("test-role".to_string());
15592
15593 app.handle_action(Action::GoBack);
15594
15595 assert_eq!(
15596 app.iam_state.current_role, None,
15597 "Should return to roles list"
15598 );
15599 }
15600
15601 #[test]
15602 fn test_iam_role_tab_navigation() {
15603 let mut app = test_app();
15604 app.current_service = Service::IamRoles;
15605 app.service_selected = true;
15606 app.iam_state.current_role = Some("test-role".to_string());
15607 app.iam_state.role_tab = RoleTab::Permissions;
15608
15609 app.handle_action(Action::NextDetailTab);
15610
15611 assert_eq!(
15612 app.iam_state.role_tab,
15613 RoleTab::TrustRelationships,
15614 "Should move to next tab"
15615 );
15616 }
15617
15618 #[test]
15619 fn test_iam_role_tab_cycle_order() {
15620 let mut app = test_app();
15621 app.current_service = Service::IamRoles;
15622 app.service_selected = true;
15623 app.iam_state.current_role = Some("test-role".to_string());
15624 app.iam_state.role_tab = RoleTab::Permissions;
15625
15626 app.handle_action(Action::NextDetailTab);
15627 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
15628
15629 app.handle_action(Action::NextDetailTab);
15630 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
15631
15632 app.handle_action(Action::NextDetailTab);
15633 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
15634
15635 app.handle_action(Action::NextDetailTab);
15636 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
15637
15638 app.handle_action(Action::NextDetailTab);
15639 assert_eq!(
15640 app.iam_state.role_tab,
15641 RoleTab::Permissions,
15642 "Should cycle back to first tab"
15643 );
15644 }
15645
15646 #[test]
15647 fn test_iam_role_pagination() {
15648 let mut app = test_app();
15649 app.current_service = Service::IamRoles;
15650 app.service_selected = true;
15651 app.iam_state.roles.page_size = PageSize::Ten;
15652
15653 app.iam_state.roles.items = (0..25)
15654 .map(|i| IamRole {
15655 role_name: format!("role{}", i),
15656 path: "/".to_string(),
15657 trusted_entities: "AWS Service: ec2".to_string(),
15658 last_activity: "-".to_string(),
15659 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
15660 creation_time: "2025-01-01".to_string(),
15661 description: format!("Test role {}", i),
15662 max_session_duration: Some(3600),
15663 })
15664 .collect();
15665
15666 app.go_to_page(2);
15668
15669 assert_eq!(
15670 app.iam_state.roles.selected, 10,
15671 "Should select first item of page 2"
15672 );
15673 assert_eq!(
15674 app.iam_state.roles.scroll_offset, 10,
15675 "Should update scroll offset"
15676 );
15677 }
15678
15679 #[test]
15680 fn test_tags_table_populated_on_role_detail() {
15681 let mut app = test_app();
15682 app.current_service = Service::IamRoles;
15683 app.service_selected = true;
15684 app.mode = Mode::Normal;
15685 app.iam_state.roles.items = vec![IamRole {
15686 role_name: "TestRole".to_string(),
15687 path: "/".to_string(),
15688 trusted_entities: String::new(),
15689 last_activity: String::new(),
15690 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
15691 creation_time: "2025-01-01".to_string(),
15692 description: String::new(),
15693 max_session_duration: Some(3600),
15694 }];
15695
15696 app.iam_state.tags.items = vec![
15698 IamRoleTag {
15699 key: "Environment".to_string(),
15700 value: "Production".to_string(),
15701 },
15702 IamRoleTag {
15703 key: "Team".to_string(),
15704 value: "Platform".to_string(),
15705 },
15706 ];
15707
15708 assert_eq!(app.iam_state.tags.items.len(), 2);
15709 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
15710 assert_eq!(app.iam_state.tags.items[0].value, "Production");
15711 assert_eq!(app.iam_state.tags.selected, 0);
15712 }
15713
15714 #[test]
15715 fn test_tags_table_navigation() {
15716 let mut app = test_app();
15717 app.current_service = Service::IamRoles;
15718 app.service_selected = true;
15719 app.mode = Mode::Normal;
15720 app.iam_state.current_role = Some("TestRole".to_string());
15721 app.iam_state.role_tab = RoleTab::Tags;
15722 app.iam_state.tags.items = vec![
15723 IamRoleTag {
15724 key: "Tag1".to_string(),
15725 value: "Value1".to_string(),
15726 },
15727 IamRoleTag {
15728 key: "Tag2".to_string(),
15729 value: "Value2".to_string(),
15730 },
15731 ];
15732
15733 app.handle_action(Action::NextItem);
15734 assert_eq!(app.iam_state.tags.selected, 1);
15735
15736 app.handle_action(Action::PrevItem);
15737 assert_eq!(app.iam_state.tags.selected, 0);
15738 }
15739
15740 #[test]
15741 fn test_last_accessed_table_navigation() {
15742 let mut app = test_app();
15743 app.current_service = Service::IamRoles;
15744 app.service_selected = true;
15745 app.mode = Mode::Normal;
15746 app.iam_state.current_role = Some("TestRole".to_string());
15747 app.iam_state.role_tab = RoleTab::LastAccessed;
15748 app.iam_state.last_accessed_services.items = vec![
15749 LastAccessedService {
15750 service: "S3".to_string(),
15751 policies_granting: "Policy1".to_string(),
15752 last_accessed: "2025-01-01".to_string(),
15753 },
15754 LastAccessedService {
15755 service: "EC2".to_string(),
15756 policies_granting: "Policy2".to_string(),
15757 last_accessed: "2025-01-02".to_string(),
15758 },
15759 ];
15760
15761 app.handle_action(Action::NextItem);
15762 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
15763
15764 app.handle_action(Action::PrevItem);
15765 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
15766 }
15767
15768 #[test]
15769 fn test_cfn_input_focus_next() {
15770 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15771 let mut app = test_app();
15772 app.current_service = Service::CloudFormationStacks;
15773 app.mode = Mode::FilterInput;
15774 app.cfn_state.input_focus = InputFocus::Filter;
15775
15776 app.handle_action(Action::NextFilterFocus);
15777 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15778
15779 app.handle_action(Action::NextFilterFocus);
15780 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15781
15782 app.handle_action(Action::NextFilterFocus);
15783 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15784
15785 app.handle_action(Action::NextFilterFocus);
15786 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15787 }
15788
15789 #[test]
15790 fn test_cfn_input_focus_prev() {
15791 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15792 let mut app = test_app();
15793 app.current_service = Service::CloudFormationStacks;
15794 app.mode = Mode::FilterInput;
15795 app.cfn_state.input_focus = InputFocus::Filter;
15796
15797 app.handle_action(Action::PrevFilterFocus);
15798 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15799
15800 app.handle_action(Action::PrevFilterFocus);
15801 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15802
15803 app.handle_action(Action::PrevFilterFocus);
15804 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15805
15806 app.handle_action(Action::PrevFilterFocus);
15807 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15808 }
15809
15810 #[test]
15811 fn test_cw_logs_input_focus_prev() {
15812 let mut app = test_app();
15813 app.current_service = Service::CloudWatchLogGroups;
15814 app.mode = Mode::FilterInput;
15815 app.view_mode = ViewMode::Detail;
15816 app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
15817 app.log_groups_state.input_focus = InputFocus::Filter;
15818
15819 app.handle_action(Action::PrevFilterFocus);
15820 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
15821
15822 app.handle_action(Action::PrevFilterFocus);
15823 assert_eq!(
15824 app.log_groups_state.input_focus,
15825 InputFocus::Checkbox("ShowExpired")
15826 );
15827
15828 app.handle_action(Action::PrevFilterFocus);
15829 assert_eq!(
15830 app.log_groups_state.input_focus,
15831 InputFocus::Checkbox("ExactMatch")
15832 );
15833
15834 app.handle_action(Action::PrevFilterFocus);
15835 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
15836 }
15837
15838 #[test]
15839 fn test_cw_events_input_focus_prev() {
15840 use crate::ui::cw::logs::EventFilterFocus;
15841 let mut app = test_app();
15842 app.mode = Mode::EventFilterInput;
15843 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
15844
15845 app.handle_action(Action::PrevFilterFocus);
15846 assert_eq!(
15847 app.log_groups_state.event_input_focus,
15848 EventFilterFocus::DateRange
15849 );
15850
15851 app.handle_action(Action::PrevFilterFocus);
15852 assert_eq!(
15853 app.log_groups_state.event_input_focus,
15854 EventFilterFocus::Filter
15855 );
15856 }
15857
15858 #[test]
15859 fn test_cfn_input_focus_cycle_complete() {
15860 let mut app = test_app();
15861 app.current_service = Service::CloudFormationStacks;
15862 app.mode = Mode::FilterInput;
15863 app.cfn_state.input_focus = InputFocus::Filter;
15864
15865 for _ in 0..4 {
15867 app.handle_action(Action::NextFilterFocus);
15868 }
15869 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15870
15871 for _ in 0..4 {
15873 app.handle_action(Action::PrevFilterFocus);
15874 }
15875 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15876 }
15877
15878 #[test]
15879 fn test_cfn_filter_status_arrow_keys() {
15880 use crate::ui::cfn::STATUS_FILTER;
15881 let mut app = test_app();
15882 app.current_service = Service::CloudFormationStacks;
15883 app.mode = Mode::FilterInput;
15884 app.cfn_state.input_focus = STATUS_FILTER;
15885 app.cfn_state.status_filter = CfnStatusFilter::All;
15886
15887 app.handle_action(Action::NextItem);
15888 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
15889
15890 app.handle_action(Action::PrevItem);
15891 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
15892 }
15893
15894 #[test]
15895 fn test_cfn_filter_shift_tab_cycles_backward() {
15896 use crate::ui::cfn::STATUS_FILTER;
15897 let mut app = test_app();
15898 app.current_service = Service::CloudFormationStacks;
15899 app.mode = Mode::FilterInput;
15900 app.cfn_state.input_focus = STATUS_FILTER;
15901
15902 app.handle_action(Action::PrevFilterFocus);
15904 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15905
15906 app.handle_action(Action::PrevFilterFocus);
15908 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15909 }
15910
15911 #[test]
15912 fn test_cfn_pagination_arrow_keys() {
15913 let mut app = test_app();
15914 app.current_service = Service::CloudFormationStacks;
15915 app.mode = Mode::FilterInput;
15916 app.cfn_state.input_focus = InputFocus::Pagination;
15917 app.cfn_state.table.scroll_offset = 0;
15918 app.cfn_state.table.page_size = PageSize::Ten;
15919
15920 app.cfn_state.table.items = (0..30)
15922 .map(|i| CfnStack {
15923 name: format!("stack-{}", i),
15924 stack_id: format!("id-{}", i),
15925 status: "CREATE_COMPLETE".to_string(),
15926 created_time: "2024-01-01".to_string(),
15927 updated_time: String::new(),
15928 deleted_time: String::new(),
15929 drift_status: String::new(),
15930 last_drift_check_time: String::new(),
15931 status_reason: String::new(),
15932 description: String::new(),
15933 detailed_status: String::new(),
15934 root_stack: String::new(),
15935 parent_stack: String::new(),
15936 termination_protection: false,
15937 iam_role: String::new(),
15938 tags: Vec::new(),
15939 stack_policy: String::new(),
15940 rollback_monitoring_time: String::new(),
15941 rollback_alarms: Vec::new(),
15942 notification_arns: Vec::new(),
15943 })
15944 .collect();
15945
15946 app.handle_action(Action::PageDown);
15948 assert_eq!(app.cfn_state.table.scroll_offset, 10);
15949 let page_size = app.cfn_state.table.page_size.value();
15951 let current_page = app.cfn_state.table.scroll_offset / page_size;
15952 assert_eq!(current_page, 1);
15953
15954 app.handle_action(Action::PageUp);
15956 assert_eq!(app.cfn_state.table.scroll_offset, 0);
15957 let current_page = app.cfn_state.table.scroll_offset / page_size;
15958 assert_eq!(current_page, 0);
15959 }
15960
15961 #[test]
15962 fn test_cfn_page_navigation_updates_selection() {
15963 let mut app = test_app();
15964 app.current_service = Service::CloudFormationStacks;
15965 app.mode = Mode::Normal;
15966
15967 app.cfn_state.table.items = (0..30)
15969 .map(|i| CfnStack {
15970 name: format!("stack-{}", i),
15971 stack_id: format!("id-{}", i),
15972 status: "CREATE_COMPLETE".to_string(),
15973 created_time: "2024-01-01".to_string(),
15974 updated_time: String::new(),
15975 deleted_time: String::new(),
15976 drift_status: String::new(),
15977 last_drift_check_time: String::new(),
15978 status_reason: String::new(),
15979 description: String::new(),
15980 detailed_status: String::new(),
15981 root_stack: String::new(),
15982 parent_stack: String::new(),
15983 termination_protection: false,
15984 iam_role: String::new(),
15985 tags: Vec::new(),
15986 stack_policy: String::new(),
15987 rollback_monitoring_time: String::new(),
15988 rollback_alarms: Vec::new(),
15989 notification_arns: Vec::new(),
15990 })
15991 .collect();
15992
15993 app.cfn_state.table.reset();
15994 app.cfn_state.table.scroll_offset = 0;
15995
15996 app.handle_action(Action::PageDown);
15998 assert_eq!(app.cfn_state.table.selected, 10);
15999
16000 app.handle_action(Action::PageDown);
16002 assert_eq!(app.cfn_state.table.selected, 20);
16003
16004 app.handle_action(Action::PageUp);
16006 assert_eq!(app.cfn_state.table.selected, 10);
16007 }
16008
16009 #[test]
16010 fn test_cfn_filter_input_only_when_focused() {
16011 use crate::ui::cfn::STATUS_FILTER;
16012 let mut app = test_app();
16013 app.current_service = Service::CloudFormationStacks;
16014 app.mode = Mode::FilterInput;
16015 app.cfn_state.input_focus = STATUS_FILTER;
16016 app.cfn_state.table.filter = String::new();
16017
16018 app.handle_action(Action::FilterInput('t'));
16020 app.handle_action(Action::FilterInput('e'));
16021 app.handle_action(Action::FilterInput('s'));
16022 app.handle_action(Action::FilterInput('t'));
16023 assert_eq!(app.cfn_state.table.filter, "");
16024
16025 app.cfn_state.input_focus = InputFocus::Filter;
16027 app.handle_action(Action::FilterInput('t'));
16028 app.handle_action(Action::FilterInput('e'));
16029 app.handle_action(Action::FilterInput('s'));
16030 app.handle_action(Action::FilterInput('t'));
16031 assert_eq!(app.cfn_state.table.filter, "test");
16032 }
16033
16034 #[test]
16035 fn test_cfn_input_focus_resets_on_start() {
16036 let mut app = test_app();
16037 app.current_service = Service::CloudFormationStacks;
16038 app.service_selected = true;
16039 app.mode = Mode::Normal;
16040 app.cfn_state.input_focus = InputFocus::Pagination;
16041
16042 app.handle_action(Action::StartFilter);
16044 assert_eq!(app.mode, Mode::FilterInput);
16045 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16046 }
16047
16048 #[test]
16049 fn test_iam_roles_input_focus_cycles_forward() {
16050 let mut app = test_app();
16051 app.current_service = Service::IamRoles;
16052 app.mode = Mode::FilterInput;
16053 app.iam_state.role_input_focus = InputFocus::Filter;
16054
16055 app.handle_action(Action::NextFilterFocus);
16056 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
16057
16058 app.handle_action(Action::NextFilterFocus);
16059 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
16060 }
16061
16062 #[test]
16063 fn test_iam_roles_input_focus_cycles_backward() {
16064 let mut app = test_app();
16065 app.current_service = Service::IamRoles;
16066 app.mode = Mode::FilterInput;
16067 app.iam_state.role_input_focus = InputFocus::Filter;
16068
16069 app.handle_action(Action::PrevFilterFocus);
16070 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
16071
16072 app.handle_action(Action::PrevFilterFocus);
16073 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
16074 }
16075
16076 #[test]
16077 fn test_iam_roles_filter_input_only_when_focused() {
16078 let mut app = test_app();
16079 app.current_service = Service::IamRoles;
16080 app.mode = Mode::FilterInput;
16081 app.iam_state.role_input_focus = InputFocus::Pagination;
16082 app.iam_state.roles.filter = String::new();
16083
16084 app.handle_action(Action::FilterInput('t'));
16086 app.handle_action(Action::FilterInput('e'));
16087 app.handle_action(Action::FilterInput('s'));
16088 app.handle_action(Action::FilterInput('t'));
16089 assert_eq!(app.iam_state.roles.filter, "");
16090
16091 app.iam_state.role_input_focus = InputFocus::Filter;
16093 app.handle_action(Action::FilterInput('t'));
16094 app.handle_action(Action::FilterInput('e'));
16095 app.handle_action(Action::FilterInput('s'));
16096 app.handle_action(Action::FilterInput('t'));
16097 assert_eq!(app.iam_state.roles.filter, "test");
16098 }
16099
16100 #[test]
16101 fn test_iam_roles_page_down_updates_scroll_offset() {
16102 let mut app = test_app();
16103 app.current_service = Service::IamRoles;
16104 app.mode = Mode::Normal;
16105 app.iam_state.roles.items = (0..50)
16106 .map(|i| IamRole {
16107 role_name: format!("role-{}", i),
16108 path: "/".to_string(),
16109 trusted_entities: "AWS Service".to_string(),
16110 last_activity: "N/A".to_string(),
16111 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
16112 creation_time: "2024-01-01".to_string(),
16113 description: String::new(),
16114 max_session_duration: Some(3600),
16115 })
16116 .collect();
16117
16118 app.iam_state.roles.selected = 0;
16119 app.iam_state.roles.scroll_offset = 0;
16120
16121 app.handle_action(Action::PageDown);
16123 assert_eq!(app.iam_state.roles.selected, 10);
16124 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
16126
16127 app.handle_action(Action::PageDown);
16129 assert_eq!(app.iam_state.roles.selected, 20);
16130 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
16131 }
16132
16133 #[test]
16134 fn test_application_selection_and_deployments_tab() {
16135 use crate::lambda::Application as LambdaApplication;
16136 use LambdaApplicationDetailTab;
16137
16138 let mut app = test_app();
16139 app.current_service = Service::LambdaApplications;
16140 app.service_selected = true;
16141 app.mode = Mode::Normal;
16142
16143 app.lambda_application_state.table.items = vec![LambdaApplication {
16144 name: "test-app".to_string(),
16145 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16146 description: "Test application".to_string(),
16147 status: "CREATE_COMPLETE".to_string(),
16148 last_modified: "2024-01-01".to_string(),
16149 }];
16150
16151 app.handle_action(Action::Select);
16153 assert_eq!(
16154 app.lambda_application_state.current_application,
16155 Some("test-app".to_string())
16156 );
16157 assert_eq!(
16158 app.lambda_application_state.detail_tab,
16159 LambdaApplicationDetailTab::Overview
16160 );
16161
16162 app.handle_action(Action::NextDetailTab);
16164 assert_eq!(
16165 app.lambda_application_state.detail_tab,
16166 LambdaApplicationDetailTab::Deployments
16167 );
16168
16169 app.handle_action(Action::GoBack);
16171 assert_eq!(app.lambda_application_state.current_application, None);
16172 }
16173
16174 #[test]
16175 fn test_application_resources_filter_and_pagination() {
16176 use crate::lambda::Application as LambdaApplication;
16177 use LambdaApplicationDetailTab;
16178
16179 let mut app = test_app();
16180 app.current_service = Service::LambdaApplications;
16181 app.service_selected = true;
16182 app.mode = Mode::Normal;
16183
16184 app.lambda_application_state.table.items = vec![LambdaApplication {
16185 name: "test-app".to_string(),
16186 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16187 description: "Test application".to_string(),
16188 status: "CREATE_COMPLETE".to_string(),
16189 last_modified: "2024-01-01".to_string(),
16190 }];
16191
16192 app.handle_action(Action::Select);
16194 assert_eq!(
16195 app.lambda_application_state.detail_tab,
16196 LambdaApplicationDetailTab::Overview
16197 );
16198
16199 assert!(!app.lambda_application_state.resources.items.is_empty());
16201
16202 app.mode = Mode::FilterInput;
16204 assert_eq!(
16205 app.lambda_application_state.resource_input_focus,
16206 InputFocus::Filter
16207 );
16208
16209 app.handle_action(Action::NextFilterFocus);
16210 assert_eq!(
16211 app.lambda_application_state.resource_input_focus,
16212 InputFocus::Pagination
16213 );
16214
16215 app.handle_action(Action::PrevFilterFocus);
16216 assert_eq!(
16217 app.lambda_application_state.resource_input_focus,
16218 InputFocus::Filter
16219 );
16220 }
16221
16222 #[test]
16223 fn test_application_deployments_filter_and_pagination() {
16224 use crate::lambda::Application as LambdaApplication;
16225 use LambdaApplicationDetailTab;
16226
16227 let mut app = test_app();
16228 app.current_service = Service::LambdaApplications;
16229 app.service_selected = true;
16230 app.mode = Mode::Normal;
16231
16232 app.lambda_application_state.table.items = vec![LambdaApplication {
16233 name: "test-app".to_string(),
16234 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16235 description: "Test application".to_string(),
16236 status: "CREATE_COMPLETE".to_string(),
16237 last_modified: "2024-01-01".to_string(),
16238 }];
16239
16240 app.handle_action(Action::Select);
16242 app.handle_action(Action::NextDetailTab);
16243 assert_eq!(
16244 app.lambda_application_state.detail_tab,
16245 LambdaApplicationDetailTab::Deployments
16246 );
16247
16248 assert!(!app.lambda_application_state.deployments.items.is_empty());
16250
16251 app.mode = Mode::FilterInput;
16253 assert_eq!(
16254 app.lambda_application_state.deployment_input_focus,
16255 InputFocus::Filter
16256 );
16257
16258 app.handle_action(Action::NextFilterFocus);
16259 assert_eq!(
16260 app.lambda_application_state.deployment_input_focus,
16261 InputFocus::Pagination
16262 );
16263
16264 app.handle_action(Action::PrevFilterFocus);
16265 assert_eq!(
16266 app.lambda_application_state.deployment_input_focus,
16267 InputFocus::Filter
16268 );
16269 }
16270
16271 #[test]
16272 fn test_application_resource_expansion() {
16273 use crate::lambda::Application as LambdaApplication;
16274 use LambdaApplicationDetailTab;
16275
16276 let mut app = test_app();
16277 app.current_service = Service::LambdaApplications;
16278 app.service_selected = true;
16279 app.mode = Mode::Normal;
16280
16281 app.lambda_application_state.table.items = vec![LambdaApplication {
16282 name: "test-app".to_string(),
16283 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16284 description: "Test application".to_string(),
16285 status: "CREATE_COMPLETE".to_string(),
16286 last_modified: "2024-01-01".to_string(),
16287 }];
16288
16289 app.handle_action(Action::Select);
16291 assert_eq!(
16292 app.lambda_application_state.detail_tab,
16293 LambdaApplicationDetailTab::Overview
16294 );
16295
16296 app.handle_action(Action::NextPane);
16298 assert_eq!(
16299 app.lambda_application_state.resources.expanded_item,
16300 Some(0)
16301 );
16302
16303 app.handle_action(Action::PrevPane);
16305 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
16306 }
16307
16308 #[test]
16309 fn test_application_deployment_expansion() {
16310 use crate::lambda::Application as LambdaApplication;
16311 use LambdaApplicationDetailTab;
16312
16313 let mut app = test_app();
16314 app.current_service = Service::LambdaApplications;
16315 app.service_selected = true;
16316 app.mode = Mode::Normal;
16317
16318 app.lambda_application_state.table.items = vec![LambdaApplication {
16319 name: "test-app".to_string(),
16320 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16321 description: "Test application".to_string(),
16322 status: "CREATE_COMPLETE".to_string(),
16323 last_modified: "2024-01-01".to_string(),
16324 }];
16325
16326 app.handle_action(Action::Select);
16328 app.handle_action(Action::NextDetailTab);
16329 assert_eq!(
16330 app.lambda_application_state.detail_tab,
16331 LambdaApplicationDetailTab::Deployments
16332 );
16333
16334 app.handle_action(Action::NextPane);
16336 assert_eq!(
16337 app.lambda_application_state.deployments.expanded_item,
16338 Some(0)
16339 );
16340
16341 app.handle_action(Action::PrevPane);
16343 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
16344 }
16345
16346 #[test]
16347 fn test_s3_nested_prefix_expansion() {
16348 use crate::s3::Bucket;
16349 use S3Object;
16350
16351 let mut app = test_app();
16352 app.current_service = Service::S3Buckets;
16353 app.service_selected = true;
16354 app.mode = Mode::Normal;
16355
16356 app.s3_state.buckets.items = vec![Bucket {
16358 name: "test-bucket".to_string(),
16359 region: "us-east-1".to_string(),
16360 creation_date: "2024-01-01".to_string(),
16361 }];
16362
16363 app.s3_state.bucket_preview.insert(
16365 "test-bucket".to_string(),
16366 vec![S3Object {
16367 key: "level1/".to_string(),
16368 size: 0,
16369 last_modified: "".to_string(),
16370 is_prefix: true,
16371 storage_class: "".to_string(),
16372 }],
16373 );
16374
16375 app.s3_state.prefix_preview.insert(
16377 "level1/".to_string(),
16378 vec![S3Object {
16379 key: "level1/level2/".to_string(),
16380 size: 0,
16381 last_modified: "".to_string(),
16382 is_prefix: true,
16383 storage_class: "".to_string(),
16384 }],
16385 );
16386
16387 app.s3_state.selected_row = 0;
16389 app.handle_action(Action::NextPane);
16390 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
16391
16392 app.s3_state.selected_row = 1;
16394 app.handle_action(Action::NextPane);
16395 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16396
16397 app.s3_state.selected_row = 2;
16399 app.handle_action(Action::NextPane);
16400 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
16401
16402 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
16404 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16405 }
16406
16407 #[test]
16408 fn test_s3_nested_prefix_collapse() {
16409 use crate::s3::Bucket;
16410 use S3Object;
16411
16412 let mut app = test_app();
16413 app.current_service = Service::S3Buckets;
16414 app.service_selected = true;
16415 app.mode = Mode::Normal;
16416
16417 app.s3_state.buckets.items = vec![Bucket {
16418 name: "test-bucket".to_string(),
16419 region: "us-east-1".to_string(),
16420 creation_date: "2024-01-01".to_string(),
16421 }];
16422
16423 app.s3_state.bucket_preview.insert(
16424 "test-bucket".to_string(),
16425 vec![S3Object {
16426 key: "level1/".to_string(),
16427 size: 0,
16428 last_modified: "".to_string(),
16429 is_prefix: true,
16430 storage_class: "".to_string(),
16431 }],
16432 );
16433
16434 app.s3_state.prefix_preview.insert(
16435 "level1/".to_string(),
16436 vec![S3Object {
16437 key: "level1/level2/".to_string(),
16438 size: 0,
16439 last_modified: "".to_string(),
16440 is_prefix: true,
16441 storage_class: "".to_string(),
16442 }],
16443 );
16444
16445 app.s3_state
16447 .expanded_prefixes
16448 .insert("test-bucket".to_string());
16449 app.s3_state.expanded_prefixes.insert("level1/".to_string());
16450 app.s3_state
16451 .expanded_prefixes
16452 .insert("level1/level2/".to_string());
16453
16454 app.s3_state.selected_row = 2;
16456 app.handle_action(Action::PrevPane);
16457 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
16458 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
16462 app.handle_action(Action::PrevPane);
16463 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
16464 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
16468 app.handle_action(Action::PrevPane);
16469 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
16470 }
16471}
16472
16473#[cfg(test)]
16474mod sqs_tests {
16475 use super::*;
16476 use test_helpers::*;
16477
16478 #[test]
16479 fn test_sqs_filter_input() {
16480 let mut app = test_app();
16481 app.current_service = Service::SqsQueues;
16482 app.service_selected = true;
16483 app.mode = Mode::FilterInput;
16484
16485 app.handle_action(Action::FilterInput('t'));
16486 app.handle_action(Action::FilterInput('e'));
16487 app.handle_action(Action::FilterInput('s'));
16488 app.handle_action(Action::FilterInput('t'));
16489 assert_eq!(app.sqs_state.queues.filter, "test");
16490
16491 app.handle_action(Action::FilterBackspace);
16492 assert_eq!(app.sqs_state.queues.filter, "tes");
16493 }
16494
16495 #[test]
16496 fn test_sqs_start_filter() {
16497 let mut app = test_app();
16498 app.current_service = Service::SqsQueues;
16499 app.service_selected = true;
16500 app.mode = Mode::Normal;
16501
16502 app.handle_action(Action::StartFilter);
16503 assert_eq!(app.mode, Mode::FilterInput);
16504 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
16505 }
16506
16507 #[test]
16508 fn test_sqs_filter_focus_cycling() {
16509 let mut app = test_app();
16510 app.current_service = Service::SqsQueues;
16511 app.service_selected = true;
16512 app.mode = Mode::FilterInput;
16513 app.sqs_state.input_focus = InputFocus::Filter;
16514
16515 app.handle_action(Action::NextFilterFocus);
16516 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
16517
16518 app.handle_action(Action::NextFilterFocus);
16519 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
16520
16521 app.handle_action(Action::PrevFilterFocus);
16522 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
16523 }
16524
16525 #[test]
16526 fn test_sqs_navigation() {
16527 let mut app = test_app();
16528 app.current_service = Service::SqsQueues;
16529 app.service_selected = true;
16530 app.mode = Mode::Normal;
16531 app.sqs_state.queues.items = (0..10)
16532 .map(|i| SqsQueue {
16533 name: format!("queue{}", i),
16534 url: String::new(),
16535 queue_type: "Standard".to_string(),
16536 created_timestamp: String::new(),
16537 messages_available: "0".to_string(),
16538 messages_in_flight: "0".to_string(),
16539 encryption: "Disabled".to_string(),
16540 content_based_deduplication: "Disabled".to_string(),
16541 last_modified_timestamp: String::new(),
16542 visibility_timeout: String::new(),
16543 message_retention_period: String::new(),
16544 maximum_message_size: String::new(),
16545 delivery_delay: String::new(),
16546 receive_message_wait_time: String::new(),
16547 high_throughput_fifo: "N/A".to_string(),
16548 deduplication_scope: "N/A".to_string(),
16549 fifo_throughput_limit: "N/A".to_string(),
16550 dead_letter_queue: "-".to_string(),
16551 messages_delayed: "0".to_string(),
16552 redrive_allow_policy: "-".to_string(),
16553 redrive_policy: "".to_string(),
16554 redrive_task_id: "-".to_string(),
16555 redrive_task_start_time: "-".to_string(),
16556 redrive_task_status: "-".to_string(),
16557 redrive_task_percent: "-".to_string(),
16558 redrive_task_destination: "-".to_string(),
16559 })
16560 .collect();
16561
16562 app.handle_action(Action::NextItem);
16563 assert_eq!(app.sqs_state.queues.selected, 1);
16564
16565 app.handle_action(Action::PrevItem);
16566 assert_eq!(app.sqs_state.queues.selected, 0);
16567 }
16568
16569 #[test]
16570 fn test_sqs_page_navigation() {
16571 let mut app = test_app();
16572 app.current_service = Service::SqsQueues;
16573 app.service_selected = true;
16574 app.mode = Mode::Normal;
16575 app.sqs_state.queues.items = (0..100)
16576 .map(|i| SqsQueue {
16577 name: format!("queue{}", i),
16578 url: String::new(),
16579 queue_type: "Standard".to_string(),
16580 created_timestamp: String::new(),
16581 messages_available: "0".to_string(),
16582 messages_in_flight: "0".to_string(),
16583 encryption: "Disabled".to_string(),
16584 content_based_deduplication: "Disabled".to_string(),
16585 last_modified_timestamp: String::new(),
16586 visibility_timeout: String::new(),
16587 message_retention_period: String::new(),
16588 maximum_message_size: String::new(),
16589 delivery_delay: String::new(),
16590 receive_message_wait_time: String::new(),
16591 high_throughput_fifo: "N/A".to_string(),
16592 deduplication_scope: "N/A".to_string(),
16593 fifo_throughput_limit: "N/A".to_string(),
16594 dead_letter_queue: "-".to_string(),
16595 messages_delayed: "0".to_string(),
16596 redrive_allow_policy: "-".to_string(),
16597 redrive_policy: "".to_string(),
16598 redrive_task_id: "-".to_string(),
16599 redrive_task_start_time: "-".to_string(),
16600 redrive_task_status: "-".to_string(),
16601 redrive_task_percent: "-".to_string(),
16602 redrive_task_destination: "-".to_string(),
16603 })
16604 .collect();
16605
16606 app.handle_action(Action::PageDown);
16607 assert_eq!(app.sqs_state.queues.selected, 10);
16608
16609 app.handle_action(Action::PageUp);
16610 assert_eq!(app.sqs_state.queues.selected, 0);
16611 }
16612
16613 #[test]
16614 fn test_sqs_queue_expansion() {
16615 let mut app = test_app();
16616 app.current_service = Service::SqsQueues;
16617 app.service_selected = true;
16618 app.sqs_state.queues.items = vec![SqsQueue {
16619 name: "my-queue".to_string(),
16620 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
16621 queue_type: "Standard".to_string(),
16622 created_timestamp: "2023-01-01".to_string(),
16623 messages_available: "5".to_string(),
16624 messages_in_flight: "2".to_string(),
16625 encryption: "Enabled".to_string(),
16626 content_based_deduplication: "Disabled".to_string(),
16627 last_modified_timestamp: "2023-01-02".to_string(),
16628 visibility_timeout: "30".to_string(),
16629 message_retention_period: "345600".to_string(),
16630 maximum_message_size: "262144".to_string(),
16631 delivery_delay: "0".to_string(),
16632 receive_message_wait_time: "0".to_string(),
16633 high_throughput_fifo: "N/A".to_string(),
16634 deduplication_scope: "N/A".to_string(),
16635 fifo_throughput_limit: "N/A".to_string(),
16636 dead_letter_queue: "-".to_string(),
16637 messages_delayed: "0".to_string(),
16638 redrive_allow_policy: "-".to_string(),
16639 redrive_policy: "".to_string(),
16640 redrive_task_id: "-".to_string(),
16641 redrive_task_start_time: "-".to_string(),
16642 redrive_task_status: "-".to_string(),
16643 redrive_task_percent: "-".to_string(),
16644 redrive_task_destination: "-".to_string(),
16645 }];
16646 app.sqs_state.queues.selected = 0;
16647
16648 assert_eq!(app.sqs_state.queues.expanded_item, None);
16649
16650 app.handle_action(Action::NextPane);
16652 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
16653
16654 app.handle_action(Action::NextPane);
16656 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
16657
16658 app.handle_action(Action::PrevPane);
16660 assert_eq!(app.sqs_state.queues.expanded_item, None);
16661
16662 app.handle_action(Action::PrevPane);
16664 assert_eq!(app.sqs_state.queues.expanded_item, None);
16665 }
16666
16667 #[test]
16668 fn test_sqs_column_toggle() {
16669 use crate::sqs::queue::Column as SqsColumn;
16670 let mut app = test_app();
16671 app.current_service = Service::SqsQueues;
16672 app.service_selected = true;
16673 app.mode = Mode::ColumnSelector;
16674
16675 app.sqs_visible_column_ids = SqsColumn::ids();
16677 let initial_count = app.sqs_visible_column_ids.len();
16678
16679 app.column_selector_index = 0;
16681 app.handle_action(Action::ToggleColumn);
16682
16683 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
16685 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
16686
16687 app.handle_action(Action::ToggleColumn);
16689 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
16690 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
16691 }
16692
16693 #[test]
16694 fn test_sqs_column_selector_navigation() {
16695 let mut app = test_app();
16696 app.current_service = Service::SqsQueues;
16697 app.service_selected = true;
16698 app.mode = Mode::ColumnSelector;
16699 app.column_selector_index = 0;
16700
16701 let max_index = app.sqs_column_ids.len() - 1;
16703
16704 for _ in 0..max_index {
16706 app.handle_action(Action::NextItem);
16707 }
16708 assert_eq!(app.column_selector_index, max_index);
16709
16710 for _ in 0..max_index {
16712 app.handle_action(Action::PrevItem);
16713 }
16714 assert_eq!(app.column_selector_index, 0);
16715 }
16716
16717 #[test]
16718 fn test_sqs_queue_selection() {
16719 let mut app = test_app();
16720 app.current_service = Service::SqsQueues;
16721 app.service_selected = true;
16722 app.mode = Mode::Normal;
16723 app.sqs_state.queues.items = vec![SqsQueue {
16724 name: "my-queue".to_string(),
16725 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
16726 queue_type: "Standard".to_string(),
16727 created_timestamp: "2023-01-01".to_string(),
16728 messages_available: "5".to_string(),
16729 messages_in_flight: "2".to_string(),
16730 encryption: "Enabled".to_string(),
16731 content_based_deduplication: "Disabled".to_string(),
16732 last_modified_timestamp: "2023-01-02".to_string(),
16733 visibility_timeout: "30".to_string(),
16734 message_retention_period: "345600".to_string(),
16735 maximum_message_size: "262144".to_string(),
16736 delivery_delay: "0".to_string(),
16737 receive_message_wait_time: "0".to_string(),
16738 high_throughput_fifo: "N/A".to_string(),
16739 deduplication_scope: "N/A".to_string(),
16740 fifo_throughput_limit: "N/A".to_string(),
16741 dead_letter_queue: "-".to_string(),
16742 messages_delayed: "0".to_string(),
16743 redrive_allow_policy: "-".to_string(),
16744 redrive_policy: "".to_string(),
16745 redrive_task_id: "-".to_string(),
16746 redrive_task_start_time: "-".to_string(),
16747 redrive_task_status: "-".to_string(),
16748 redrive_task_percent: "-".to_string(),
16749 redrive_task_destination: "-".to_string(),
16750 }];
16751 app.sqs_state.queues.selected = 0;
16752
16753 assert_eq!(app.sqs_state.current_queue, None);
16754
16755 app.handle_action(Action::Select);
16757 assert_eq!(
16758 app.sqs_state.current_queue,
16759 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
16760 );
16761
16762 app.handle_action(Action::GoBack);
16764 assert_eq!(app.sqs_state.current_queue, None);
16765 }
16766
16767 #[test]
16768 fn test_sqs_lambda_triggers_expand_collapse() {
16769 let mut app = test_app();
16770 app.current_service = Service::SqsQueues;
16771 app.service_selected = true;
16772 app.sqs_state.current_queue =
16773 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16774 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16775 app.sqs_state.triggers.items = vec![LambdaTrigger {
16776 uuid: "test-uuid".to_string(),
16777 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
16778 status: "Enabled".to_string(),
16779 last_modified: "2024-01-01T00:00:00Z".to_string(),
16780 }];
16781 app.sqs_state.triggers.selected = 0;
16782
16783 assert_eq!(app.sqs_state.triggers.expanded_item, None);
16784
16785 app.handle_action(Action::NextPane);
16787 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16788
16789 app.handle_action(Action::PrevPane);
16791 assert_eq!(app.sqs_state.triggers.expanded_item, None);
16792 }
16793
16794 #[test]
16795 fn test_sqs_lambda_triggers_expand_toggle() {
16796 let mut app = test_app();
16797 app.current_service = Service::SqsQueues;
16798 app.service_selected = true;
16799 app.sqs_state.current_queue =
16800 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16801 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16802 app.sqs_state.triggers.items = vec![LambdaTrigger {
16803 uuid: "test-uuid".to_string(),
16804 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
16805 status: "Enabled".to_string(),
16806 last_modified: "2024-01-01T00:00:00Z".to_string(),
16807 }];
16808 app.sqs_state.triggers.selected = 0;
16809
16810 app.handle_action(Action::NextPane);
16812 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16813
16814 app.handle_action(Action::NextPane);
16816 assert_eq!(app.sqs_state.triggers.expanded_item, None);
16817
16818 app.handle_action(Action::NextPane);
16820 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16821 }
16822
16823 #[test]
16824 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
16825 use crate::ui::sqs::filtered_lambda_triggers;
16826
16827 let mut app = test_app();
16828 app.current_service = Service::SqsQueues;
16829 app.service_selected = true;
16830 app.sqs_state.current_queue =
16831 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16832 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16833 app.sqs_state.triggers.items = vec![
16834 LambdaTrigger {
16835 uuid: "uuid-3".to_string(),
16836 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
16837 status: "Enabled".to_string(),
16838 last_modified: "2024-03-01T00:00:00Z".to_string(),
16839 },
16840 LambdaTrigger {
16841 uuid: "uuid-1".to_string(),
16842 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
16843 status: "Enabled".to_string(),
16844 last_modified: "2024-01-01T00:00:00Z".to_string(),
16845 },
16846 LambdaTrigger {
16847 uuid: "uuid-2".to_string(),
16848 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
16849 status: "Enabled".to_string(),
16850 last_modified: "2024-02-01T00:00:00Z".to_string(),
16851 },
16852 ];
16853
16854 let sorted = filtered_lambda_triggers(&app);
16855
16856 assert_eq!(sorted.len(), 3);
16858 assert_eq!(sorted[0].uuid, "uuid-1");
16859 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
16860 assert_eq!(sorted[1].uuid, "uuid-2");
16861 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
16862 assert_eq!(sorted[2].uuid, "uuid-3");
16863 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
16864 }
16865
16866 #[test]
16867 fn test_sqs_lambda_triggers_filter_input() {
16868 let mut app = test_app();
16869 app.current_service = Service::SqsQueues;
16870 app.service_selected = true;
16871 app.mode = Mode::FilterInput;
16872 app.sqs_state.current_queue =
16873 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16874 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16875 app.sqs_state.input_focus = InputFocus::Filter;
16876
16877 assert_eq!(app.sqs_state.triggers.filter, "");
16878
16879 app.handle_action(Action::FilterInput('t'));
16881 assert_eq!(app.sqs_state.triggers.filter, "t");
16882
16883 app.handle_action(Action::FilterInput('e'));
16884 assert_eq!(app.sqs_state.triggers.filter, "te");
16885
16886 app.handle_action(Action::FilterInput('s'));
16887 assert_eq!(app.sqs_state.triggers.filter, "tes");
16888
16889 app.handle_action(Action::FilterInput('t'));
16890 assert_eq!(app.sqs_state.triggers.filter, "test");
16891
16892 app.handle_action(Action::FilterBackspace);
16894 assert_eq!(app.sqs_state.triggers.filter, "tes");
16895 }
16896
16897 #[test]
16898 fn test_sqs_lambda_triggers_filter_applied() {
16899 use crate::ui::sqs::filtered_lambda_triggers;
16900
16901 let mut app = test_app();
16902 app.current_service = Service::SqsQueues;
16903 app.service_selected = true;
16904 app.sqs_state.current_queue =
16905 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16906 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16907 app.sqs_state.triggers.items = vec![
16908 LambdaTrigger {
16909 uuid: "uuid-1".to_string(),
16910 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
16911 status: "Enabled".to_string(),
16912 last_modified: "2024-01-01T00:00:00Z".to_string(),
16913 },
16914 LambdaTrigger {
16915 uuid: "uuid-2".to_string(),
16916 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
16917 status: "Enabled".to_string(),
16918 last_modified: "2024-02-01T00:00:00Z".to_string(),
16919 },
16920 LambdaTrigger {
16921 uuid: "uuid-3".to_string(),
16922 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
16923 status: "Enabled".to_string(),
16924 last_modified: "2024-03-01T00:00:00Z".to_string(),
16925 },
16926 ];
16927
16928 let filtered = filtered_lambda_triggers(&app);
16930 assert_eq!(filtered.len(), 3);
16931
16932 app.sqs_state.triggers.filter = "alpha".to_string();
16934 let filtered = filtered_lambda_triggers(&app);
16935 assert_eq!(filtered.len(), 1);
16936 assert_eq!(
16937 filtered[0].arn,
16938 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
16939 );
16940
16941 app.sqs_state.triggers.filter = "test".to_string();
16943 let filtered = filtered_lambda_triggers(&app);
16944 assert_eq!(filtered.len(), 2);
16945 assert_eq!(
16946 filtered[0].arn,
16947 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
16948 );
16949 assert_eq!(
16950 filtered[1].arn,
16951 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
16952 );
16953
16954 app.sqs_state.triggers.filter = "uuid-3".to_string();
16956 let filtered = filtered_lambda_triggers(&app);
16957 assert_eq!(filtered.len(), 1);
16958 assert_eq!(filtered[0].uuid, "uuid-3");
16959 }
16960
16961 #[test]
16962 fn test_sqs_triggers_navigation() {
16963 let mut app = test_app();
16964 app.service_selected = true;
16965 app.mode = Mode::Normal;
16966 app.current_service = Service::SqsQueues;
16967 app.sqs_state.current_queue = Some("test-queue".to_string());
16968 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16969 app.sqs_state.triggers.items = vec![
16970 LambdaTrigger {
16971 uuid: "1".to_string(),
16972 arn: "arn1".to_string(),
16973 status: "Enabled".to_string(),
16974 last_modified: "2024-01-01".to_string(),
16975 },
16976 LambdaTrigger {
16977 uuid: "2".to_string(),
16978 arn: "arn2".to_string(),
16979 status: "Enabled".to_string(),
16980 last_modified: "2024-01-02".to_string(),
16981 },
16982 ];
16983
16984 assert_eq!(app.sqs_state.triggers.selected, 0);
16985 app.next_item();
16986 assert_eq!(app.sqs_state.triggers.selected, 1);
16987 app.prev_item();
16988 assert_eq!(app.sqs_state.triggers.selected, 0);
16989 }
16990
16991 #[test]
16992 fn test_sqs_pipes_navigation() {
16993 let mut app = test_app();
16994 app.service_selected = true;
16995 app.mode = Mode::Normal;
16996 app.current_service = Service::SqsQueues;
16997 app.sqs_state.current_queue = Some("test-queue".to_string());
16998 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
16999 app.sqs_state.pipes.items = vec![
17000 EventBridgePipe {
17001 name: "pipe1".to_string(),
17002 status: "RUNNING".to_string(),
17003 target: "target1".to_string(),
17004 last_modified: "2024-01-01".to_string(),
17005 },
17006 EventBridgePipe {
17007 name: "pipe2".to_string(),
17008 status: "RUNNING".to_string(),
17009 target: "target2".to_string(),
17010 last_modified: "2024-01-02".to_string(),
17011 },
17012 ];
17013
17014 assert_eq!(app.sqs_state.pipes.selected, 0);
17015 app.next_item();
17016 assert_eq!(app.sqs_state.pipes.selected, 1);
17017 app.prev_item();
17018 assert_eq!(app.sqs_state.pipes.selected, 0);
17019 }
17020
17021 #[test]
17022 fn test_sqs_tags_navigation() {
17023 let mut app = test_app();
17024 app.service_selected = true;
17025 app.mode = Mode::Normal;
17026 app.current_service = Service::SqsQueues;
17027 app.sqs_state.current_queue = Some("test-queue".to_string());
17028 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
17029 app.sqs_state.tags.items = vec![
17030 SqsQueueTag {
17031 key: "Env".to_string(),
17032 value: "prod".to_string(),
17033 },
17034 SqsQueueTag {
17035 key: "Team".to_string(),
17036 value: "backend".to_string(),
17037 },
17038 ];
17039
17040 assert_eq!(app.sqs_state.tags.selected, 0);
17041 app.next_item();
17042 assert_eq!(app.sqs_state.tags.selected, 1);
17043 app.prev_item();
17044 assert_eq!(app.sqs_state.tags.selected, 0);
17045 }
17046
17047 #[test]
17048 fn test_sqs_queues_navigation() {
17049 let mut app = test_app();
17050 app.service_selected = true;
17051 app.mode = Mode::Normal;
17052 app.current_service = Service::SqsQueues;
17053 app.sqs_state.queues.items = vec![
17054 SqsQueue {
17055 name: "queue1".to_string(),
17056 url: "url1".to_string(),
17057 queue_type: "Standard".to_string(),
17058 created_timestamp: "".to_string(),
17059 messages_available: "0".to_string(),
17060 messages_in_flight: "0".to_string(),
17061 encryption: "Disabled".to_string(),
17062 content_based_deduplication: "Disabled".to_string(),
17063 last_modified_timestamp: "".to_string(),
17064 visibility_timeout: "".to_string(),
17065 message_retention_period: "".to_string(),
17066 maximum_message_size: "".to_string(),
17067 delivery_delay: "".to_string(),
17068 receive_message_wait_time: "".to_string(),
17069 high_throughput_fifo: "-".to_string(),
17070 deduplication_scope: "-".to_string(),
17071 fifo_throughput_limit: "-".to_string(),
17072 dead_letter_queue: "-".to_string(),
17073 messages_delayed: "0".to_string(),
17074 redrive_allow_policy: "-".to_string(),
17075 redrive_policy: "".to_string(),
17076 redrive_task_id: "-".to_string(),
17077 redrive_task_start_time: "-".to_string(),
17078 redrive_task_status: "-".to_string(),
17079 redrive_task_percent: "-".to_string(),
17080 redrive_task_destination: "-".to_string(),
17081 },
17082 SqsQueue {
17083 name: "queue2".to_string(),
17084 url: "url2".to_string(),
17085 queue_type: "Standard".to_string(),
17086 created_timestamp: "".to_string(),
17087 messages_available: "0".to_string(),
17088 messages_in_flight: "0".to_string(),
17089 encryption: "Disabled".to_string(),
17090 content_based_deduplication: "Disabled".to_string(),
17091 last_modified_timestamp: "".to_string(),
17092 visibility_timeout: "".to_string(),
17093 message_retention_period: "".to_string(),
17094 maximum_message_size: "".to_string(),
17095 delivery_delay: "".to_string(),
17096 receive_message_wait_time: "".to_string(),
17097 high_throughput_fifo: "-".to_string(),
17098 deduplication_scope: "-".to_string(),
17099 fifo_throughput_limit: "-".to_string(),
17100 dead_letter_queue: "-".to_string(),
17101 messages_delayed: "0".to_string(),
17102 redrive_allow_policy: "-".to_string(),
17103 redrive_policy: "".to_string(),
17104 redrive_task_id: "-".to_string(),
17105 redrive_task_start_time: "-".to_string(),
17106 redrive_task_status: "-".to_string(),
17107 redrive_task_percent: "-".to_string(),
17108 redrive_task_destination: "-".to_string(),
17109 },
17110 ];
17111
17112 assert_eq!(app.sqs_state.queues.selected, 0);
17113 app.next_item();
17114 assert_eq!(app.sqs_state.queues.selected, 1);
17115 app.prev_item();
17116 assert_eq!(app.sqs_state.queues.selected, 0);
17117 }
17118
17119 #[test]
17120 fn test_sqs_subscriptions_navigation() {
17121 let mut app = test_app();
17122 app.service_selected = true;
17123 app.mode = Mode::Normal;
17124 app.current_service = Service::SqsQueues;
17125 app.sqs_state.current_queue = Some("test-queue".to_string());
17126 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17127 app.sqs_state.subscriptions.items = vec![
17128 SnsSubscription {
17129 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
17130 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
17131 },
17132 SnsSubscription {
17133 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
17134 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
17135 },
17136 ];
17137
17138 assert_eq!(app.sqs_state.subscriptions.selected, 0);
17139 app.next_item();
17140 assert_eq!(app.sqs_state.subscriptions.selected, 1);
17141 app.prev_item();
17142 assert_eq!(app.sqs_state.subscriptions.selected, 0);
17143 }
17144
17145 #[test]
17146 fn test_sqs_subscription_region_dropdown_navigation() {
17147 let mut app = test_app();
17148 app.service_selected = true;
17149 app.mode = Mode::FilterInput;
17150 app.current_service = Service::SqsQueues;
17151 app.sqs_state.current_queue = Some("test-queue".to_string());
17152 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17153 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
17154
17155 assert_eq!(app.sqs_state.subscription_region_selected, 0);
17156 app.next_item();
17157 assert_eq!(app.sqs_state.subscription_region_selected, 1);
17158 app.next_item();
17159 assert_eq!(app.sqs_state.subscription_region_selected, 2);
17160 app.prev_item();
17161 assert_eq!(app.sqs_state.subscription_region_selected, 1);
17162 app.prev_item();
17163 assert_eq!(app.sqs_state.subscription_region_selected, 0);
17164 }
17165
17166 #[test]
17167 fn test_sqs_subscription_region_selection() {
17168 let mut app = test_app();
17169 app.service_selected = true;
17170 app.mode = Mode::FilterInput;
17171 app.current_service = Service::SqsQueues;
17172 app.sqs_state.current_queue = Some("test-queue".to_string());
17173 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17174 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
17175 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
17178 app.handle_action(Action::ApplyFilter);
17179 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
17180 assert_eq!(app.mode, Mode::Normal);
17181 }
17182
17183 #[test]
17184 fn test_sqs_subscription_region_change_resets_selection() {
17185 let mut app = test_app();
17186 app.service_selected = true;
17187 app.mode = Mode::FilterInput;
17188 app.current_service = Service::SqsQueues;
17189 app.sqs_state.current_queue = Some("test-queue".to_string());
17190 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17191 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
17192 app.sqs_state.subscription_region_selected = 0;
17193 app.sqs_state.subscriptions.selected = 5;
17194
17195 app.handle_action(Action::NextItem);
17196
17197 assert_eq!(app.sqs_state.subscription_region_selected, 1);
17198 assert_eq!(app.sqs_state.subscriptions.selected, 0);
17199 }
17200
17201 #[test]
17202 fn test_s3_object_filter_resets_selection() {
17203 let mut app = test_app();
17204 app.service_selected = true;
17205 app.current_service = Service::S3Buckets;
17206 app.s3_state.current_bucket = Some("test-bucket".to_string());
17207 app.s3_state.selected_row = 5;
17208 app.mode = Mode::FilterInput;
17209
17210 app.handle_action(Action::CloseMenu);
17211
17212 assert_eq!(app.s3_state.selected_row, 0);
17213 assert_eq!(app.mode, Mode::Normal);
17214 }
17215
17216 #[test]
17217 fn test_s3_bucket_filter_resets_selection() {
17218 let mut app = test_app();
17219 app.service_selected = true;
17220 app.current_service = Service::S3Buckets;
17221 app.s3_state.selected_row = 10;
17222 app.mode = Mode::FilterInput;
17223
17224 app.handle_action(Action::CloseMenu);
17225
17226 assert_eq!(app.s3_state.selected_row, 0);
17227 assert_eq!(app.mode, Mode::Normal);
17228 }
17229
17230 #[test]
17231 fn test_s3_selection_stays_in_bounds() {
17232 let mut app = test_app();
17233 app.service_selected = true;
17234 app.current_service = Service::S3Buckets;
17235 app.s3_state.selected_row = 0;
17236 app.s3_state.selected_object = 0;
17237
17238 app.prev_item();
17240
17241 assert_eq!(app.s3_state.selected_row, 0);
17243 assert_eq!(app.s3_state.selected_object, 0);
17244 }
17245
17246 #[test]
17247 fn test_cfn_filter_resets_selection() {
17248 let mut app = test_app();
17249 app.service_selected = true;
17250 app.current_service = Service::CloudFormationStacks;
17251 app.cfn_state.table.selected = 10;
17252 app.mode = Mode::FilterInput;
17253
17254 app.handle_action(Action::CloseMenu);
17255
17256 assert_eq!(app.cfn_state.table.selected, 0);
17257 assert_eq!(app.mode, Mode::Normal);
17258 }
17259
17260 #[test]
17261 fn test_lambda_filter_resets_selection() {
17262 let mut app = test_app();
17263 app.service_selected = true;
17264 app.current_service = Service::LambdaFunctions;
17265 app.lambda_state.table.selected = 8;
17266 app.mode = Mode::FilterInput;
17267
17268 app.handle_action(Action::CloseMenu);
17269
17270 assert_eq!(app.lambda_state.table.selected, 0);
17271 assert_eq!(app.mode, Mode::Normal);
17272 }
17273
17274 #[test]
17275 fn test_sqs_filter_resets_selection() {
17276 let mut app = test_app();
17277 app.service_selected = true;
17278 app.current_service = Service::SqsQueues;
17279 app.sqs_state.queues.selected = 7;
17280 app.mode = Mode::FilterInput;
17281
17282 app.handle_action(Action::CloseMenu);
17283
17284 assert_eq!(app.sqs_state.queues.selected, 0);
17285 assert_eq!(app.mode, Mode::Normal);
17286 }
17287
17288 #[test]
17289 fn test_sqs_queues_list_shows_preferences() {
17290 let mut app = test_app();
17291 app.service_selected = true;
17292 app.current_service = Service::SqsQueues;
17293 app.mode = Mode::Normal;
17294
17295 app.handle_action(Action::OpenColumnSelector);
17296
17297 assert_eq!(app.mode, Mode::ColumnSelector);
17298 }
17299
17300 #[test]
17301 fn test_sqs_queue_policies_tab_no_preferences() {
17302 let mut app = test_app();
17303 app.service_selected = true;
17304 app.current_service = Service::SqsQueues;
17305 app.sqs_state.current_queue = Some("test-queue".to_string());
17306 app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
17307 app.mode = Mode::Normal;
17308
17309 app.handle_action(Action::OpenColumnSelector);
17310
17311 assert_eq!(app.mode, Mode::Normal);
17312 }
17313
17314 #[test]
17315 fn test_sqs_sns_subscriptions_tab_shows_preferences() {
17316 let mut app = test_app();
17317 app.service_selected = true;
17318 app.current_service = Service::SqsQueues;
17319 app.sqs_state.current_queue = Some("test-queue".to_string());
17320 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17321 app.mode = Mode::Normal;
17322
17323 app.handle_action(Action::OpenColumnSelector);
17324
17325 assert_eq!(app.mode, Mode::ColumnSelector);
17326 }
17327
17328 #[test]
17329 fn test_sqs_monitoring_tab_no_preferences() {
17330 let mut app = test_app();
17331 app.service_selected = true;
17332 app.current_service = Service::SqsQueues;
17333 app.sqs_state.current_queue = Some("test-queue".to_string());
17334 app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
17335 app.mode = Mode::Normal;
17336
17337 app.handle_action(Action::OpenColumnSelector);
17338
17339 assert_eq!(app.mode, Mode::Normal);
17340 }
17341
17342 #[test]
17343 fn test_cfn_status_filter_change_resets_selection() {
17344 use crate::ui::cfn::STATUS_FILTER;
17345 let mut app = test_app();
17346 app.service_selected = true;
17347 app.current_service = Service::CloudFormationStacks;
17348 app.mode = Mode::FilterInput;
17349 app.cfn_state.input_focus = STATUS_FILTER;
17350 app.cfn_state.status_filter = CfnStatusFilter::All;
17351 app.cfn_state.table.items = vec![
17352 CfnStack {
17353 name: "stack1".to_string(),
17354 stack_id: "id1".to_string(),
17355 status: "CREATE_COMPLETE".to_string(),
17356 created_time: "2024-01-01".to_string(),
17357 updated_time: String::new(),
17358 deleted_time: String::new(),
17359 drift_status: String::new(),
17360 last_drift_check_time: String::new(),
17361 status_reason: String::new(),
17362 description: String::new(),
17363 detailed_status: String::new(),
17364 root_stack: String::new(),
17365 parent_stack: String::new(),
17366 termination_protection: false,
17367 iam_role: String::new(),
17368 tags: Vec::new(),
17369 stack_policy: String::new(),
17370 rollback_monitoring_time: String::new(),
17371 rollback_alarms: Vec::new(),
17372 notification_arns: Vec::new(),
17373 },
17374 CfnStack {
17375 name: "stack2".to_string(),
17376 stack_id: "id2".to_string(),
17377 status: "UPDATE_IN_PROGRESS".to_string(),
17378 created_time: "2024-01-02".to_string(),
17379 updated_time: String::new(),
17380 deleted_time: String::new(),
17381 drift_status: String::new(),
17382 last_drift_check_time: String::new(),
17383 status_reason: String::new(),
17384 description: String::new(),
17385 detailed_status: String::new(),
17386 root_stack: String::new(),
17387 parent_stack: String::new(),
17388 termination_protection: false,
17389 iam_role: String::new(),
17390 tags: Vec::new(),
17391 stack_policy: String::new(),
17392 rollback_monitoring_time: String::new(),
17393 rollback_alarms: Vec::new(),
17394 notification_arns: Vec::new(),
17395 },
17396 ];
17397 app.cfn_state.table.selected = 1;
17398
17399 app.handle_action(Action::NextItem);
17400
17401 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
17402 assert_eq!(app.cfn_state.table.selected, 0);
17403 }
17404
17405 #[test]
17406 fn test_cfn_view_nested_toggle_resets_selection() {
17407 use crate::ui::cfn::VIEW_NESTED;
17408 let mut app = test_app();
17409 app.service_selected = true;
17410 app.current_service = Service::CloudFormationStacks;
17411 app.mode = Mode::FilterInput;
17412 app.cfn_state.input_focus = VIEW_NESTED;
17413 app.cfn_state.view_nested = false;
17414 app.cfn_state.table.items = vec![CfnStack {
17415 name: "stack1".to_string(),
17416 stack_id: "id1".to_string(),
17417 status: "CREATE_COMPLETE".to_string(),
17418 created_time: "2024-01-01".to_string(),
17419 updated_time: String::new(),
17420 deleted_time: String::new(),
17421 drift_status: String::new(),
17422 last_drift_check_time: String::new(),
17423 status_reason: String::new(),
17424 description: String::new(),
17425 detailed_status: String::new(),
17426 root_stack: String::new(),
17427 parent_stack: String::new(),
17428 termination_protection: false,
17429 iam_role: String::new(),
17430 tags: Vec::new(),
17431 stack_policy: String::new(),
17432 rollback_monitoring_time: String::new(),
17433 rollback_alarms: Vec::new(),
17434 notification_arns: Vec::new(),
17435 }];
17436 app.cfn_state.table.selected = 5;
17437
17438 app.handle_action(Action::ToggleFilterCheckbox);
17439
17440 assert!(app.cfn_state.view_nested);
17441 assert_eq!(app.cfn_state.table.selected, 0);
17442 }
17443
17444 #[test]
17445 fn test_cfn_template_scroll_up() {
17446 let mut app = test_app();
17447 app.service_selected = true;
17448 app.current_service = Service::CloudFormationStacks;
17449 app.cfn_state.current_stack = Some("test-stack".to_string());
17450 app.cfn_state.detail_tab = CfnDetailTab::Template;
17451 app.cfn_state.template_scroll = 20;
17452
17453 app.page_up();
17454
17455 assert_eq!(app.cfn_state.template_scroll, 10);
17456 }
17457
17458 #[test]
17459 fn test_cfn_template_scroll_down() {
17460 let mut app = test_app();
17461 app.service_selected = true;
17462 app.current_service = Service::CloudFormationStacks;
17463 app.cfn_state.current_stack = Some("test-stack".to_string());
17464 app.cfn_state.detail_tab = CfnDetailTab::Template;
17465 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
17466 app.cfn_state.template_scroll = 0;
17467
17468 app.page_down();
17469
17470 assert_eq!(app.cfn_state.template_scroll, 10);
17471 }
17472
17473 #[test]
17474 fn test_cfn_template_scroll_down_respects_max() {
17475 let mut app = test_app();
17476 app.service_selected = true;
17477 app.current_service = Service::CloudFormationStacks;
17478 app.cfn_state.current_stack = Some("test-stack".to_string());
17479 app.cfn_state.detail_tab = CfnDetailTab::Template;
17480 app.cfn_state.template_body = "line1\nline2\nline3".to_string();
17481 app.cfn_state.template_scroll = 0;
17482
17483 app.page_down();
17484
17485 assert_eq!(app.cfn_state.template_scroll, 2);
17487 }
17488
17489 #[test]
17490 fn test_cfn_template_arrow_up() {
17491 let mut app = test_app();
17492 app.service_selected = true;
17493 app.current_service = Service::CloudFormationStacks;
17494 app.mode = Mode::Normal;
17495 app.cfn_state.current_stack = Some("test-stack".to_string());
17496 app.cfn_state.detail_tab = CfnDetailTab::Template;
17497 app.cfn_state.template_scroll = 5;
17498
17499 app.prev_item();
17500
17501 assert_eq!(app.cfn_state.template_scroll, 4);
17502 }
17503
17504 #[test]
17505 fn test_cfn_template_arrow_down() {
17506 let mut app = test_app();
17507 app.service_selected = true;
17508 app.current_service = Service::CloudFormationStacks;
17509 app.mode = Mode::Normal;
17510 app.cfn_state.current_stack = Some("test-stack".to_string());
17511 app.cfn_state.detail_tab = CfnDetailTab::Template;
17512 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
17513 app.cfn_state.template_scroll = 2;
17514
17515 app.next_item();
17516
17517 assert_eq!(app.cfn_state.template_scroll, 3);
17518 }
17519
17520 #[test]
17521 fn test_cfn_template_arrow_down_respects_max() {
17522 let mut app = test_app();
17523 app.service_selected = true;
17524 app.current_service = Service::CloudFormationStacks;
17525 app.mode = Mode::Normal;
17526 app.cfn_state.current_stack = Some("test-stack".to_string());
17527 app.cfn_state.detail_tab = CfnDetailTab::Template;
17528 app.cfn_state.template_body = "line1\nline2".to_string();
17529 app.cfn_state.template_scroll = 1;
17530
17531 app.next_item();
17532
17533 assert_eq!(app.cfn_state.template_scroll, 1);
17535 }
17536}
17537
17538#[cfg(test)]
17539mod lambda_version_tab_tests {
17540 use super::*;
17541 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
17542 use test_helpers::*;
17543
17544 #[test]
17545 fn test_lambda_version_tab_cycling_next() {
17546 let mut app = test_app();
17547 app.current_service = Service::LambdaFunctions;
17548 app.lambda_state.current_function = Some("test-function".to_string());
17549 app.lambda_state.current_version = Some("1".to_string());
17550 app.lambda_state.detail_tab = LambdaDetailTab::Code;
17551
17552 app.handle_action(Action::NextDetailTab);
17554 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17555 assert!(app.lambda_state.metrics_loading);
17556
17557 app.lambda_state.metrics_loading = false;
17559 app.handle_action(Action::NextDetailTab);
17560 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
17561
17562 app.handle_action(Action::NextDetailTab);
17564 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
17565 }
17566
17567 #[test]
17568 fn test_lambda_version_tab_cycling_prev() {
17569 let mut app = test_app();
17570 app.current_service = Service::LambdaFunctions;
17571 app.lambda_state.current_function = Some("test-function".to_string());
17572 app.lambda_state.current_version = Some("1".to_string());
17573 app.lambda_state.detail_tab = LambdaDetailTab::Code;
17574
17575 app.handle_action(Action::PrevDetailTab);
17577 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
17578
17579 app.handle_action(Action::PrevDetailTab);
17581 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17582 assert!(app.lambda_state.metrics_loading);
17583
17584 app.lambda_state.metrics_loading = false;
17586 app.handle_action(Action::PrevDetailTab);
17587 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
17588 }
17589
17590 #[test]
17591 fn test_lambda_version_monitor_clears_metrics() {
17592 let mut app = test_app();
17593 app.current_service = Service::LambdaFunctions;
17594 app.lambda_state.current_function = Some("test-function".to_string());
17595 app.lambda_state.current_version = Some("1".to_string());
17596 app.lambda_state.detail_tab = LambdaDetailTab::Code;
17597
17598 app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
17600 app.lambda_state.monitoring_scroll = 5;
17601
17602 app.handle_action(Action::NextDetailTab);
17604
17605 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17606 assert!(app.lambda_state.metrics_loading);
17607 assert_eq!(app.lambda_state.monitoring_scroll, 0);
17608 assert!(app.lambda_state.metric_data_invocations.is_empty());
17609 }
17610
17611 #[test]
17612 fn test_cfn_parameters_expand_collapse() {
17613 let mut app = test_app();
17614 app.current_service = Service::CloudFormationStacks;
17615 app.service_selected = true;
17616 app.cfn_state.current_stack = Some("test-stack".to_string());
17617 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17618 app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
17619 key: "Param1".to_string(),
17620 value: "Value1".to_string(),
17621 resolved_value: "Resolved1".to_string(),
17622 }];
17623 app.cfn_state.parameters.reset();
17624
17625 assert_eq!(app.cfn_state.parameters.expanded_item, None);
17626
17627 app.handle_action(Action::NextPane);
17629 assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
17630
17631 app.handle_action(Action::PrevPane);
17633 assert_eq!(app.cfn_state.parameters.expanded_item, None);
17634 }
17635
17636 #[test]
17637 fn test_cfn_parameters_filter_resets_selection() {
17638 let mut app = test_app();
17639 app.current_service = Service::CloudFormationStacks;
17640 app.service_selected = true;
17641 app.cfn_state.current_stack = Some("test-stack".to_string());
17642 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17643 app.cfn_state.parameters.items = vec![
17644 rusticity_core::cfn::StackParameter {
17645 key: "DatabaseName".to_string(),
17646 value: "mydb".to_string(),
17647 resolved_value: "mydb".to_string(),
17648 },
17649 rusticity_core::cfn::StackParameter {
17650 key: "InstanceType".to_string(),
17651 value: "t2.micro".to_string(),
17652 resolved_value: "t2.micro".to_string(),
17653 },
17654 rusticity_core::cfn::StackParameter {
17655 key: "Environment".to_string(),
17656 value: "production".to_string(),
17657 resolved_value: "production".to_string(),
17658 },
17659 ];
17660 app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
17662 app.cfn_state.parameters_input_focus = InputFocus::Filter;
17663
17664 app.handle_action(Action::FilterInput('D'));
17666 assert_eq!(app.cfn_state.parameters.selected, 0);
17667 assert_eq!(app.cfn_state.parameters.filter, "D");
17668
17669 app.cfn_state.parameters.selected = 1;
17671
17672 app.handle_action(Action::FilterInput('a'));
17674 assert_eq!(app.cfn_state.parameters.selected, 0);
17675 assert_eq!(app.cfn_state.parameters.filter, "Da");
17676
17677 app.cfn_state.parameters.selected = 1;
17679
17680 app.handle_action(Action::FilterBackspace);
17682 assert_eq!(app.cfn_state.parameters.selected, 0);
17683 assert_eq!(app.cfn_state.parameters.filter, "D");
17684 }
17685
17686 #[test]
17687 fn test_cfn_template_tab_no_preferences() {
17688 let mut app = test_app();
17689 app.current_service = Service::CloudFormationStacks;
17690 app.service_selected = true;
17691 app.cfn_state.current_stack = Some("test-stack".to_string());
17692 app.cfn_state.detail_tab = CfnDetailTab::Template;
17693 app.mode = Mode::Normal;
17694
17695 app.handle_action(Action::OpenColumnSelector);
17697 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::GitSync;
17701 app.handle_action(Action::OpenColumnSelector);
17702 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17706 app.handle_action(Action::OpenColumnSelector);
17707 assert_eq!(app.mode, Mode::ColumnSelector); app.mode = Mode::Normal;
17711 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17712 app.handle_action(Action::OpenColumnSelector);
17713 assert_eq!(app.mode, Mode::ColumnSelector); }
17715
17716 #[test]
17717 fn test_iam_user_groups_tab_shows_preferences() {
17718 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17719 app.current_service = Service::IamUsers;
17720 app.service_selected = true;
17721 app.mode = Mode::Normal;
17722 app.iam_state.current_user = Some("test-user".to_string());
17723 app.iam_state.user_tab = UserTab::Groups;
17724
17725 app.handle_action(Action::OpenColumnSelector);
17727 assert_eq!(app.mode, Mode::ColumnSelector);
17728 }
17729
17730 #[test]
17731 fn test_iam_user_tags_tab_shows_preferences() {
17732 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17733 app.current_service = Service::IamUsers;
17734 app.service_selected = true;
17735 app.mode = Mode::Normal;
17736 app.iam_state.current_user = Some("test-user".to_string());
17737 app.iam_state.user_tab = UserTab::Tags;
17738
17739 app.handle_action(Action::OpenColumnSelector);
17741 assert_eq!(app.mode, Mode::ColumnSelector);
17742 }
17743
17744 #[test]
17745 fn test_iam_user_last_accessed_tab_shows_preferences() {
17746 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17747 app.current_service = Service::IamUsers;
17748 app.service_selected = true;
17749 app.mode = Mode::Normal;
17750 app.iam_state.current_user = Some("test-user".to_string());
17751 app.iam_state.user_tab = UserTab::LastAccessed;
17752
17753 app.handle_action(Action::OpenColumnSelector);
17755 assert_eq!(app.mode, Mode::ColumnSelector);
17756 }
17757
17758 #[test]
17759 fn test_iam_user_security_credentials_tab_no_preferences() {
17760 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17761 app.current_service = Service::IamUsers;
17762 app.service_selected = true;
17763 app.mode = Mode::Normal;
17764 app.iam_state.current_user = Some("test-user".to_string());
17765 app.iam_state.user_tab = UserTab::SecurityCredentials;
17766
17767 app.handle_action(Action::OpenColumnSelector);
17769 assert_eq!(app.mode, Mode::Normal);
17770 }
17771
17772 #[test]
17773 fn test_iam_user_tabs_without_column_preferences() {
17774 let mut app = test_app();
17775 app.current_service = Service::IamUsers;
17776 app.service_selected = true;
17777 app.iam_state.current_user = Some("test-user".to_string());
17778 app.mode = Mode::Normal;
17779
17780 app.iam_state.user_tab = UserTab::Groups;
17782 app.handle_action(Action::OpenColumnSelector);
17783 assert_eq!(app.mode, Mode::ColumnSelector);
17784 app.mode = Mode::Normal;
17785
17786 app.iam_state.user_tab = UserTab::Tags;
17788 app.handle_action(Action::OpenColumnSelector);
17789 assert_eq!(app.mode, Mode::ColumnSelector);
17790 app.mode = Mode::Normal;
17791
17792 app.iam_state.user_tab = UserTab::SecurityCredentials;
17794 app.handle_action(Action::OpenColumnSelector);
17795 assert_eq!(app.mode, Mode::Normal);
17796
17797 app.iam_state.user_tab = UserTab::LastAccessed;
17799 app.handle_action(Action::OpenColumnSelector);
17800 assert_eq!(app.mode, Mode::ColumnSelector);
17801 app.mode = Mode::Normal;
17802
17803 app.iam_state.user_tab = UserTab::Permissions;
17805 app.handle_action(Action::OpenColumnSelector);
17806 assert_eq!(app.mode, Mode::ColumnSelector);
17807
17808 app.mode = Mode::Normal;
17810 app.iam_state.current_user = None;
17811 app.handle_action(Action::OpenColumnSelector);
17812 assert_eq!(app.mode, Mode::ColumnSelector);
17813 }
17814
17815 #[test]
17816 fn test_iam_role_policies_dropdown_cycling() {
17817 let mut app = test_app();
17818 app.current_service = Service::IamRoles;
17819 app.service_selected = true;
17820 app.iam_state.current_role = Some("test-role".to_string());
17821 app.iam_state.role_tab = RoleTab::Permissions;
17822 app.mode = Mode::FilterInput;
17823 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
17824 app.iam_state.policy_type_filter = "All types".to_string();
17825
17826 app.next_item();
17828 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17829 app.next_item();
17830 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17831 app.next_item();
17832 assert_eq!(app.iam_state.policy_type_filter, "All types");
17833
17834 app.prev_item();
17836 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17837 app.prev_item();
17838 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17839 app.prev_item();
17840 assert_eq!(app.iam_state.policy_type_filter, "All types");
17841 }
17842
17843 #[test]
17844 fn test_iam_user_policies_dropdown_cycling() {
17845 let mut app = test_app();
17846 app.current_service = Service::IamUsers;
17847 app.service_selected = true;
17848 app.iam_state.current_user = Some("test-user".to_string());
17849 app.iam_state.user_tab = UserTab::Permissions;
17850 app.mode = Mode::FilterInput;
17851 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
17852 app.iam_state.policy_type_filter = "All types".to_string();
17853
17854 app.next_item();
17856 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17857 app.next_item();
17858 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17859 app.next_item();
17860 assert_eq!(app.iam_state.policy_type_filter, "All types");
17861
17862 app.prev_item();
17864 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17865 app.prev_item();
17866 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17867 app.prev_item();
17868 assert_eq!(app.iam_state.policy_type_filter, "All types");
17869 }
17870
17871 #[test]
17872 fn test_iam_role_tabs_without_column_preferences() {
17873 let mut app = test_app();
17874 app.current_service = Service::IamRoles;
17875 app.service_selected = true;
17876 app.iam_state.current_role = Some("test-role".to_string());
17877 app.mode = Mode::Normal;
17878
17879 app.iam_state.role_tab = RoleTab::TrustRelationships;
17881 app.handle_action(Action::OpenColumnSelector);
17882 assert_eq!(app.mode, Mode::Normal);
17883
17884 app.iam_state.role_tab = RoleTab::RevokeSessions;
17886 app.handle_action(Action::OpenColumnSelector);
17887 assert_eq!(app.mode, Mode::Normal);
17888
17889 app.iam_state.role_tab = RoleTab::LastAccessed;
17891 app.handle_action(Action::OpenColumnSelector);
17892 assert_eq!(app.mode, Mode::ColumnSelector);
17893
17894 app.mode = Mode::Normal;
17896 app.iam_state.role_tab = RoleTab::Permissions;
17897 app.handle_action(Action::OpenColumnSelector);
17898 assert_eq!(app.mode, Mode::ColumnSelector);
17899
17900 app.mode = Mode::Normal;
17902 app.iam_state.role_tab = RoleTab::Tags;
17903 app.handle_action(Action::OpenColumnSelector);
17904 assert_eq!(app.mode, Mode::ColumnSelector);
17905
17906 app.mode = Mode::Normal;
17908 app.iam_state.current_role = None;
17909 app.handle_action(Action::OpenColumnSelector);
17910 assert_eq!(app.mode, Mode::ColumnSelector);
17911 }
17912
17913 #[test]
17914 fn test_iam_role_tags_tab_cycling() {
17915 let mut app = test_app();
17916 app.current_service = Service::IamRoles;
17917 app.service_selected = true;
17918 app.iam_state.current_role = Some("test-role".to_string());
17919 app.iam_state.role_tab = RoleTab::Tags;
17920 app.mode = Mode::ColumnSelector;
17921 app.column_selector_index = 0;
17922
17923 app.handle_action(Action::NextPreferences);
17925 assert_eq!(app.column_selector_index, 4);
17926
17927 app.handle_action(Action::NextPreferences);
17929 assert_eq!(app.column_selector_index, 0);
17930
17931 app.handle_action(Action::PrevPreferences);
17933 assert_eq!(app.column_selector_index, 4);
17934
17935 app.handle_action(Action::PrevPreferences);
17937 assert_eq!(app.column_selector_index, 0);
17938 }
17939
17940 #[test]
17941 fn test_cfn_outputs_expand_collapse() {
17942 let mut app = test_app();
17943 app.current_service = Service::CloudFormationStacks;
17944 app.service_selected = true;
17945 app.cfn_state.current_stack = Some("test-stack".to_string());
17946 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17947 app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
17948 key: "Output1".to_string(),
17949 value: "Value1".to_string(),
17950 description: "Description1".to_string(),
17951 export_name: "Export1".to_string(),
17952 }];
17953 app.cfn_state.outputs.reset();
17954
17955 assert_eq!(app.cfn_state.outputs.expanded_item, None);
17956
17957 app.handle_action(Action::NextPane);
17959 assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
17960
17961 app.handle_action(Action::PrevPane);
17963 assert_eq!(app.cfn_state.outputs.expanded_item, None);
17964 }
17965
17966 #[test]
17967 fn test_cfn_outputs_filter_resets_selection() {
17968 let mut app = test_app();
17969 app.current_service = Service::CloudFormationStacks;
17970 app.service_selected = true;
17971 app.cfn_state.current_stack = Some("test-stack".to_string());
17972 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17973 app.cfn_state.outputs.items = vec![
17974 rusticity_core::cfn::StackOutput {
17975 key: "ApiUrl".to_string(),
17976 value: "https://api.example.com".to_string(),
17977 description: "API endpoint".to_string(),
17978 export_name: "MyApiUrl".to_string(),
17979 },
17980 rusticity_core::cfn::StackOutput {
17981 key: "BucketName".to_string(),
17982 value: "my-bucket".to_string(),
17983 description: "S3 bucket".to_string(),
17984 export_name: "MyBucket".to_string(),
17985 },
17986 ];
17987 app.cfn_state.outputs.reset();
17988 app.cfn_state.outputs.selected = 1;
17989
17990 app.handle_action(Action::StartFilter);
17992 assert_eq!(app.mode, Mode::FilterInput);
17993
17994 app.handle_action(Action::FilterInput('A'));
17996 assert_eq!(app.cfn_state.outputs.selected, 0);
17997 assert_eq!(app.cfn_state.outputs.filter, "A");
17998
17999 app.cfn_state.outputs.selected = 1;
18001 app.handle_action(Action::FilterInput('p'));
18002 assert_eq!(app.cfn_state.outputs.selected, 0);
18003
18004 app.cfn_state.outputs.selected = 1;
18006 app.handle_action(Action::FilterBackspace);
18007 assert_eq!(app.cfn_state.outputs.selected, 0);
18008 }
18009
18010 #[test]
18011 fn test_ec2_service_in_picker() {
18012 let app = test_app();
18013 assert!(app.service_picker.services.contains(&"EC2 › Instances"));
18014 }
18015
18016 #[test]
18017 fn test_ec2_state_filter_cycles() {
18018 let mut app = test_app();
18019 app.current_service = Service::Ec2Instances;
18020 app.service_selected = true;
18021 app.mode = Mode::FilterInput;
18022 app.ec2_state.input_focus = EC2_STATE_FILTER;
18023
18024 let initial = app.ec2_state.state_filter;
18025 assert_eq!(initial, Ec2StateFilter::AllStates);
18026
18027 app.handle_action(Action::ToggleFilterCheckbox);
18029 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18030
18031 app.handle_action(Action::ToggleFilterCheckbox);
18032 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
18033
18034 app.handle_action(Action::ToggleFilterCheckbox);
18035 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
18036
18037 app.handle_action(Action::ToggleFilterCheckbox);
18038 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
18039
18040 app.handle_action(Action::ToggleFilterCheckbox);
18041 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
18042
18043 app.handle_action(Action::ToggleFilterCheckbox);
18044 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
18045
18046 app.handle_action(Action::ToggleFilterCheckbox);
18047 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
18048 }
18049
18050 #[test]
18051 fn test_ec2_filter_resets_table() {
18052 let mut app = test_app();
18053 app.current_service = Service::Ec2Instances;
18054 app.service_selected = true;
18055 app.mode = Mode::FilterInput;
18056 app.ec2_state.input_focus = EC2_STATE_FILTER;
18057 app.ec2_state.table.selected = 5;
18058
18059 app.handle_action(Action::ToggleFilterCheckbox);
18060 assert_eq!(app.ec2_state.table.selected, 0);
18061 }
18062
18063 #[test]
18064 fn test_ec2_columns_visible() {
18065 let app = test_app();
18066 assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
18069
18070 #[test]
18071 fn test_ec2_breadcrumbs() {
18072 let mut app = test_app();
18073 app.current_service = Service::Ec2Instances;
18074 app.service_selected = true;
18075 let breadcrumb = app.breadcrumbs();
18076 assert_eq!(breadcrumb, "EC2 > Instances");
18077 }
18078
18079 #[test]
18080 fn test_ec2_console_url() {
18081 let mut app = test_app();
18082 app.current_service = Service::Ec2Instances;
18083 app.service_selected = true;
18084 let url = app.get_console_url();
18085 assert!(url.contains("ec2"));
18086 assert!(url.contains("Instances"));
18087 }
18088
18089 #[test]
18090 fn test_ec2_filter_handling() {
18091 let mut app = test_app();
18092 app.current_service = Service::Ec2Instances;
18093 app.service_selected = true;
18094 app.mode = Mode::FilterInput;
18095
18096 app.handle_action(Action::FilterInput('t'));
18097 app.handle_action(Action::FilterInput('e'));
18098 app.handle_action(Action::FilterInput('s'));
18099 app.handle_action(Action::FilterInput('t'));
18100
18101 assert_eq!(app.ec2_state.table.filter, "test");
18102
18103 app.handle_action(Action::FilterBackspace);
18104 assert_eq!(app.ec2_state.table.filter, "tes");
18105 }
18106
18107 #[test]
18108 fn test_column_selector_page_down_ec2() {
18109 let mut app = test_app();
18110 app.current_service = Service::Ec2Instances;
18111 app.service_selected = true;
18112 app.mode = Mode::ColumnSelector;
18113 app.column_selector_index = 0;
18114
18115 app.handle_action(Action::PageDown);
18116 assert_eq!(app.column_selector_index, 10);
18117
18118 app.handle_action(Action::PageDown);
18119 assert_eq!(app.column_selector_index, 20);
18120 }
18121
18122 #[test]
18123 fn test_column_selector_page_up_ec2() {
18124 let mut app = test_app();
18125 app.current_service = Service::Ec2Instances;
18126 app.service_selected = true;
18127 app.mode = Mode::ColumnSelector;
18128 app.column_selector_index = 30;
18129
18130 app.handle_action(Action::PageUp);
18131 assert_eq!(app.column_selector_index, 20);
18132
18133 app.handle_action(Action::PageUp);
18134 assert_eq!(app.column_selector_index, 10);
18135 }
18136
18137 #[test]
18138 fn test_ec2_state_filter_dropdown_focus() {
18139 let mut app = test_app();
18140 app.current_service = Service::Ec2Instances;
18141 app.service_selected = true;
18142 app.mode = Mode::FilterInput;
18143
18144 app.handle_action(Action::NextFilterFocus);
18146 assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
18147
18148 app.handle_action(Action::ToggleFilterCheckbox);
18151 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18152 }
18153
18154 #[test]
18155 fn test_column_selector_ctrl_d_scrolling() {
18156 let mut app = test_app();
18157 app.current_service = Service::LambdaFunctions;
18158 app.mode = Mode::ColumnSelector;
18159 app.column_selector_index = 0;
18160
18161 app.handle_action(Action::PageDown);
18163 assert_eq!(app.column_selector_index, 11);
18164
18165 let max = app.get_column_selector_max();
18167 app.handle_action(Action::PageDown);
18168 assert_eq!(app.column_selector_index, max);
18169 }
18170
18171 #[test]
18172 fn test_column_selector_ctrl_u_scrolling() {
18173 let mut app = test_app();
18174 app.current_service = Service::CloudFormationStacks;
18175 app.mode = Mode::ColumnSelector;
18176 app.column_selector_index = 25;
18177
18178 app.handle_action(Action::PageUp);
18179 assert_eq!(app.column_selector_index, 15);
18180
18181 app.handle_action(Action::PageUp);
18182 assert_eq!(app.column_selector_index, 5);
18183 }
18184
18185 #[test]
18186 fn test_prev_preferences_lambda() {
18187 let mut app = test_app();
18188 app.current_service = Service::LambdaFunctions;
18189 app.mode = Mode::ColumnSelector;
18190 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
18191 app.column_selector_index = page_size_idx;
18192
18193 app.handle_action(Action::PrevPreferences);
18194 assert_eq!(app.column_selector_index, 0);
18195
18196 app.handle_action(Action::PrevPreferences);
18197 assert_eq!(app.column_selector_index, page_size_idx);
18198 }
18199
18200 #[test]
18201 fn test_prev_preferences_cloudformation() {
18202 let mut app = test_app();
18203 app.current_service = Service::CloudFormationStacks;
18204 app.mode = Mode::ColumnSelector;
18205 let page_size_idx = app.cfn_column_ids.len() + 2;
18206 app.column_selector_index = page_size_idx;
18207
18208 app.handle_action(Action::PrevPreferences);
18209 assert_eq!(app.column_selector_index, 0);
18210
18211 app.handle_action(Action::PrevPreferences);
18212 assert_eq!(app.column_selector_index, page_size_idx);
18213 }
18214
18215 #[test]
18216 fn test_prev_preferences_alarms() {
18217 let mut app = test_app();
18218 app.current_service = Service::CloudWatchAlarms;
18219 app.mode = Mode::ColumnSelector;
18220 app.column_selector_index = 28; app.handle_action(Action::PrevPreferences);
18223 assert_eq!(app.column_selector_index, 22); app.handle_action(Action::PrevPreferences);
18226 assert_eq!(app.column_selector_index, 18); app.handle_action(Action::PrevPreferences);
18229 assert_eq!(app.column_selector_index, 0); app.handle_action(Action::PrevPreferences);
18232 assert_eq!(app.column_selector_index, 28); }
18234
18235 #[test]
18236 fn test_ec2_page_size_in_preferences() {
18237 let mut app = test_app();
18238 app.current_service = Service::Ec2Instances;
18239 app.mode = Mode::ColumnSelector;
18240 app.ec2_state.table.page_size = PageSize::Fifty;
18241
18242 let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
18245 app.handle_action(Action::ToggleColumn);
18246
18247 assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
18248 }
18249
18250 #[test]
18251 fn test_ec2_next_preferences_with_page_size() {
18252 let mut app = test_app();
18253 app.current_service = Service::Ec2Instances;
18254 app.mode = Mode::ColumnSelector;
18255 app.column_selector_index = 0;
18256
18257 let page_size_idx = app.ec2_column_ids.len() + 2;
18258 app.handle_action(Action::NextPreferences);
18259 assert_eq!(app.column_selector_index, page_size_idx);
18260
18261 app.handle_action(Action::NextPreferences);
18262 assert_eq!(app.column_selector_index, 0);
18263 }
18264
18265 #[test]
18266 fn test_ec2_dropdown_next_item() {
18267 let mut app = test_app();
18268 app.current_service = Service::Ec2Instances;
18269 app.mode = Mode::FilterInput;
18270 app.ec2_state.input_focus = EC2_STATE_FILTER;
18271 app.ec2_state.state_filter = Ec2StateFilter::AllStates;
18272
18273 app.handle_action(Action::NextItem);
18274 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18275
18276 app.handle_action(Action::NextItem);
18277 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
18278 }
18279
18280 #[test]
18281 fn test_ec2_dropdown_prev_item() {
18282 let mut app = test_app();
18283 app.current_service = Service::Ec2Instances;
18284 app.mode = Mode::FilterInput;
18285 app.ec2_state.input_focus = EC2_STATE_FILTER;
18286 app.ec2_state.state_filter = Ec2StateFilter::Stopped;
18287
18288 app.handle_action(Action::PrevItem);
18289 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18290
18291 app.handle_action(Action::PrevItem);
18292 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
18293 }
18294
18295 #[test]
18296 fn test_ec2_dropdown_cycles_with_arrows() {
18297 let mut app = test_app();
18298 app.current_service = Service::Ec2Instances;
18299 app.mode = Mode::FilterInput;
18300 app.ec2_state.input_focus = EC2_STATE_FILTER;
18301 app.ec2_state.state_filter = Ec2StateFilter::Stopping;
18302
18303 app.handle_action(Action::NextItem);
18305 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
18306
18307 app.handle_action(Action::PrevItem);
18309 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
18310 }
18311
18312 #[test]
18313 fn test_collapse_row_ec2_instances() {
18314 let mut app = test_app();
18315 app.current_service = Service::Ec2Instances;
18316 app.ec2_state.table.expanded_item = Some(0);
18317
18318 app.handle_action(Action::CollapseRow);
18319 assert_eq!(app.ec2_state.table.expanded_item, None);
18320 }
18321
18322 #[test]
18323 fn test_collapse_row_ec2_tags() {
18324 let mut app = test_app();
18325 app.current_service = Service::Ec2Instances;
18326 app.ec2_state.current_instance = Some("i-123".to_string());
18327 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
18328 app.ec2_state.tags.expanded_item = Some(1);
18329
18330 app.handle_action(Action::CollapseRow);
18331 assert_eq!(app.ec2_state.tags.expanded_item, None);
18332 }
18333
18334 #[test]
18335 fn test_collapse_row_cloudwatch_log_groups() {
18336 let mut app = test_app();
18337 app.current_service = Service::CloudWatchLogGroups;
18338 app.log_groups_state.log_groups.expanded_item = Some(2);
18339
18340 app.handle_action(Action::CollapseRow);
18341 assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
18342 }
18343
18344 #[test]
18345 fn test_collapse_row_cloudwatch_alarms() {
18346 let mut app = test_app();
18347 app.current_service = Service::CloudWatchAlarms;
18348 app.alarms_state.table.expanded_item = Some(0);
18349
18350 app.handle_action(Action::CollapseRow);
18351 assert_eq!(app.alarms_state.table.expanded_item, None);
18352 }
18353
18354 #[test]
18355 fn test_collapse_row_lambda_functions() {
18356 let mut app = test_app();
18357 app.current_service = Service::LambdaFunctions;
18358 app.lambda_state.table.expanded_item = Some(1);
18359
18360 app.handle_action(Action::CollapseRow);
18361 assert_eq!(app.lambda_state.table.expanded_item, None);
18362 }
18363
18364 #[test]
18365 fn test_collapse_row_cfn_stacks() {
18366 let mut app = test_app();
18367 app.current_service = Service::CloudFormationStacks;
18368 app.cfn_state.table.expanded_item = Some(0);
18369
18370 app.handle_action(Action::CollapseRow);
18371 assert_eq!(app.cfn_state.table.expanded_item, None);
18372 }
18373
18374 #[test]
18375 fn test_collapse_row_cfn_resources() {
18376 let mut app = test_app();
18377 app.current_service = Service::CloudFormationStacks;
18378 app.cfn_state.current_stack = Some("test-stack".to_string());
18379 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
18380 app.cfn_state.resources.expanded_item = Some(2);
18381
18382 app.handle_action(Action::CollapseRow);
18383 assert_eq!(app.cfn_state.resources.expanded_item, None);
18384 }
18385
18386 #[test]
18387 fn test_collapse_row_iam_users() {
18388 let mut app = test_app();
18389 app.current_service = Service::IamUsers;
18390 app.iam_state.users.expanded_item = Some(1);
18391
18392 app.handle_action(Action::CollapseRow);
18393 assert_eq!(app.iam_state.users.expanded_item, None);
18394 }
18395
18396 #[test]
18397 fn test_collapse_row_does_nothing_when_not_expanded() {
18398 let mut app = test_app();
18399 app.current_service = Service::Ec2Instances;
18400 app.ec2_state.table.expanded_item = None;
18401
18402 app.handle_action(Action::CollapseRow);
18403 assert_eq!(app.ec2_state.table.expanded_item, None);
18404 }
18405
18406 #[test]
18407 fn test_s3_collapse_expanded_folder_moves_to_parent() {
18408 let mut app = test_app();
18409 app.current_service = Service::S3Buckets;
18410 app.service_selected = true;
18411 app.mode = Mode::Normal;
18412
18413 app.s3_state.buckets.items = vec![S3Bucket {
18415 name: "bucket1".to_string(),
18416 region: "us-east-1".to_string(),
18417 creation_date: "2024-01-01T00:00:00Z".to_string(),
18418 }];
18419
18420 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18422 app.s3_state.bucket_preview.insert(
18423 "bucket1".to_string(),
18424 vec![S3Object {
18425 key: "folder1/".to_string(),
18426 size: 0,
18427 last_modified: "2024-01-01T00:00:00Z".to_string(),
18428 is_prefix: true,
18429 storage_class: String::new(),
18430 }],
18431 );
18432
18433 app.s3_state
18435 .expanded_prefixes
18436 .insert("folder1/".to_string());
18437 app.s3_state.prefix_preview.insert(
18438 "folder1/".to_string(),
18439 vec![S3Object {
18440 key: "folder1/file.txt".to_string(),
18441 size: 0,
18442 last_modified: "2024-01-01T00:00:00Z".to_string(),
18443 is_prefix: false,
18444 storage_class: String::new(),
18445 }],
18446 );
18447
18448 app.s3_state.selected_row = 1;
18450
18451 app.handle_action(Action::PrevPane);
18453
18454 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
18456 assert_eq!(app.s3_state.selected_row, 0);
18458 }
18459
18460 #[test]
18461 fn test_s3_collapse_hierarchy_level_by_level() {
18462 let mut app = test_app();
18463 app.current_service = Service::S3Buckets;
18464 app.service_selected = true;
18465 app.mode = Mode::Normal;
18466
18467 app.s3_state.buckets.items = vec![S3Bucket {
18469 name: "bucket1".to_string(),
18470 region: "us-east-1".to_string(),
18471 creation_date: "2024-01-01T00:00:00Z".to_string(),
18472 }];
18473
18474 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18476 app.s3_state.bucket_preview.insert(
18477 "bucket1".to_string(),
18478 vec![S3Object {
18479 key: "level1/".to_string(),
18480 size: 0,
18481 last_modified: "2024-01-01T00:00:00Z".to_string(),
18482 is_prefix: true,
18483 storage_class: String::new(),
18484 }],
18485 );
18486
18487 app.s3_state.expanded_prefixes.insert("level1/".to_string());
18489 app.s3_state.prefix_preview.insert(
18490 "level1/".to_string(),
18491 vec![S3Object {
18492 key: "level1/level2/".to_string(),
18493 size: 0,
18494 last_modified: "2024-01-01T00:00:00Z".to_string(),
18495 is_prefix: true,
18496 storage_class: String::new(),
18497 }],
18498 );
18499
18500 app.s3_state
18502 .expanded_prefixes
18503 .insert("level1/level2/".to_string());
18504 app.s3_state.prefix_preview.insert(
18505 "level1/level2/".to_string(),
18506 vec![S3Object {
18507 key: "level1/level2/file.txt".to_string(),
18508 size: 100,
18509 last_modified: "2024-01-01T00:00:00Z".to_string(),
18510 is_prefix: false,
18511 storage_class: String::new(),
18512 }],
18513 );
18514
18515 app.s3_state.selected_row = 3;
18517
18518 app.handle_action(Action::PrevPane);
18520 assert_eq!(app.s3_state.selected_row, 2);
18521
18522 app.handle_action(Action::PrevPane);
18524 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
18525 assert_eq!(app.s3_state.selected_row, 1);
18526
18527 app.handle_action(Action::PrevPane);
18529 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
18530 assert_eq!(app.s3_state.selected_row, 0);
18531
18532 app.handle_action(Action::PrevPane);
18534 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18535 assert_eq!(app.s3_state.selected_row, 0);
18536 }
18537
18538 #[test]
18539 fn test_ec2_instance_detail_tabs_no_preferences() {
18540 let mut app = test_app();
18541 app.current_service = Service::Ec2Instances;
18542 app.service_selected = true;
18543 app.ec2_state.table.expanded_item = Some(0);
18544 app.mode = Mode::Normal;
18545
18546 app.ec2_state.detail_tab = Ec2DetailTab::Details;
18548 app.handle_action(Action::OpenColumnSelector);
18549 assert_eq!(app.mode, Mode::Normal);
18550
18551 app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
18553 app.handle_action(Action::OpenColumnSelector);
18554 assert_eq!(app.mode, Mode::Normal);
18555
18556 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
18558 app.handle_action(Action::OpenColumnSelector);
18559 assert_eq!(app.mode, Mode::Normal);
18560
18561 app.ec2_state.detail_tab = Ec2DetailTab::Security;
18563 app.handle_action(Action::OpenColumnSelector);
18564 assert_eq!(app.mode, Mode::Normal);
18565
18566 app.ec2_state.detail_tab = Ec2DetailTab::Networking;
18568 app.handle_action(Action::OpenColumnSelector);
18569 assert_eq!(app.mode, Mode::Normal);
18570
18571 app.ec2_state.detail_tab = Ec2DetailTab::Storage;
18573 app.handle_action(Action::OpenColumnSelector);
18574 assert_eq!(app.mode, Mode::Normal);
18575
18576 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
18578 app.handle_action(Action::OpenColumnSelector);
18579 assert_eq!(app.mode, Mode::ColumnSelector);
18580 }
18581
18582 #[test]
18583 fn test_log_streams_filter_only_updates_when_focused() {
18584 let mut app = test_app();
18585 app.current_service = Service::CloudWatchLogGroups;
18586 app.service_selected = true;
18587 app.view_mode = ViewMode::Detail;
18588 app.mode = Mode::FilterInput;
18589 app.log_groups_state.stream_filter = "test".to_string();
18590
18591 app.log_groups_state.input_focus = InputFocus::Filter;
18593 app.handle_action(Action::FilterInput('x'));
18594 assert_eq!(app.log_groups_state.stream_filter, "testx");
18595
18596 app.log_groups_state.input_focus = InputFocus::Pagination;
18598 app.handle_action(Action::FilterInput('y'));
18599 assert_eq!(app.log_groups_state.stream_filter, "testx"); }
18601
18602 #[test]
18603 fn test_log_streams_backspace_only_updates_when_focused() {
18604 let mut app = test_app();
18605 app.current_service = Service::CloudWatchLogGroups;
18606 app.service_selected = true;
18607 app.view_mode = ViewMode::Detail;
18608 app.mode = Mode::FilterInput;
18609 app.log_groups_state.stream_filter = "test".to_string();
18610
18611 app.log_groups_state.input_focus = InputFocus::Filter;
18613 app.handle_action(Action::FilterBackspace);
18614 assert_eq!(app.log_groups_state.stream_filter, "tes");
18615
18616 app.log_groups_state.input_focus = InputFocus::Pagination;
18618 app.handle_action(Action::FilterBackspace);
18619 assert_eq!(app.log_groups_state.stream_filter, "tes"); }
18621
18622 #[test]
18623 fn test_log_groups_filter_only_updates_when_focused() {
18624 let mut app = test_app();
18625 app.current_service = Service::CloudWatchLogGroups;
18626 app.service_selected = true;
18627 app.view_mode = ViewMode::List;
18628 app.mode = Mode::FilterInput;
18629 app.log_groups_state.log_groups.filter = "test".to_string();
18630
18631 app.log_groups_state.input_focus = InputFocus::Filter;
18633 app.handle_action(Action::FilterInput('x'));
18634 assert_eq!(app.log_groups_state.log_groups.filter, "testx");
18635
18636 app.log_groups_state.input_focus = InputFocus::Pagination;
18638 app.handle_action(Action::FilterInput('y'));
18639 assert_eq!(app.log_groups_state.log_groups.filter, "testx"); }
18641
18642 #[test]
18643 fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
18644 use S3Bucket;
18645 use S3Object;
18646
18647 let mut app = test_app();
18648 app.current_service = Service::S3Buckets;
18649 app.service_selected = true;
18650
18651 app.s3_state.buckets.items = vec![S3Bucket {
18653 name: "test-bucket".to_string(),
18654 region: "us-east-1".to_string(),
18655 creation_date: String::new(),
18656 }];
18657
18658 app.s3_state
18660 .expanded_prefixes
18661 .insert("test-bucket".to_string());
18662 app.s3_state.bucket_preview.insert(
18663 "test-bucket".to_string(),
18664 vec![S3Object {
18665 key: "folder1/".to_string(),
18666 is_prefix: true,
18667 size: 0,
18668 last_modified: String::new(),
18669 storage_class: String::new(),
18670 }],
18671 );
18672
18673 app.s3_state
18675 .expanded_prefixes
18676 .insert("folder1/".to_string());
18677 app.s3_state.prefix_preview.insert(
18678 "folder1/".to_string(),
18679 vec![S3Object {
18680 key: "folder1/folder2/".to_string(),
18681 is_prefix: true,
18682 size: 0,
18683 last_modified: String::new(),
18684 storage_class: String::new(),
18685 }],
18686 );
18687
18688 app.s3_state.selected_row = 2;
18690
18691 app.handle_action(Action::CollapseRow);
18693
18694 assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
18696 assert_eq!(app.s3_state.selected_row, 1);
18698 }
18699
18700 #[test]
18701 fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
18702 use S3Bucket;
18703 use S3Object;
18704
18705 let mut app = test_app();
18706 app.current_service = Service::S3Buckets;
18707 app.service_selected = true;
18708
18709 app.s3_state.buckets.items = vec![S3Bucket {
18711 name: "test-bucket".to_string(),
18712 region: "us-east-1".to_string(),
18713 creation_date: String::new(),
18714 }];
18715
18716 app.s3_state
18718 .expanded_prefixes
18719 .insert("test-bucket".to_string());
18720 app.s3_state.bucket_preview.insert(
18721 "test-bucket".to_string(),
18722 vec![S3Object {
18723 key: "folder1/".to_string(),
18724 is_prefix: true,
18725 size: 0,
18726 last_modified: String::new(),
18727 storage_class: String::new(),
18728 }],
18729 );
18730
18731 app.s3_state
18733 .expanded_prefixes
18734 .insert("folder1/".to_string());
18735 app.s3_state.prefix_preview.insert(
18736 "folder1/".to_string(),
18737 vec![S3Object {
18738 key: "folder1/file.txt".to_string(),
18739 is_prefix: false,
18740 size: 100,
18741 last_modified: String::new(),
18742 storage_class: String::new(),
18743 }],
18744 );
18745
18746 app.s3_state.selected_row = 1;
18748
18749 app.handle_action(Action::CollapseRow);
18751
18752 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
18754 assert_eq!(app.s3_state.selected_row, 0);
18756 }
18757
18758 #[test]
18759 fn test_log_streams_pagination_limits_table_content() {
18760 let mut app = test_app();
18761 app.current_service = Service::CloudWatchLogGroups;
18762 app.service_selected = true;
18763 app.view_mode = ViewMode::Detail;
18764
18765 app.log_groups_state.log_streams = (0..50)
18767 .map(|i| rusticity_core::LogStream {
18768 name: format!("stream-{}", i),
18769 creation_time: None,
18770 last_event_time: None,
18771 })
18772 .collect();
18773
18774 app.log_groups_state.stream_page_size = 10;
18776 app.log_groups_state.stream_current_page = 0;
18777
18778 assert_eq!(app.log_groups_state.stream_page_size, 10);
18781 assert_eq!(app.log_groups_state.stream_current_page, 0);
18782
18783 app.log_groups_state.stream_current_page = 1;
18785 assert_eq!(app.log_groups_state.stream_current_page, 1);
18786 }
18787
18788 #[test]
18789 fn test_log_streams_page_size_change_resets_page() {
18790 let mut app = test_app();
18791 app.current_service = Service::CloudWatchLogGroups;
18792 app.service_selected = true;
18793 app.view_mode = ViewMode::Detail;
18794 app.mode = Mode::ColumnSelector;
18795
18796 app.log_groups_state.stream_page_size = 10;
18797 app.log_groups_state.stream_current_page = 3;
18798
18799 app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; app.handle_action(Action::ToggleColumn);
18802
18803 assert_eq!(app.log_groups_state.stream_page_size, 25);
18804 assert_eq!(app.log_groups_state.stream_current_page, 0);
18805 }
18806
18807 #[test]
18808 fn test_s3_objects_expanded_rows_stay_visible() {
18809 use S3Object;
18810
18811 let mut app = test_app();
18812 app.current_service = Service::S3Buckets;
18813 app.service_selected = true;
18814 app.mode = Mode::Normal;
18815 app.s3_state.current_bucket = Some("test-bucket".to_string());
18816
18817 app.s3_state.objects = vec![S3Object {
18819 key: "folder1/".to_string(),
18820 is_prefix: true,
18821 size: 0,
18822 last_modified: String::new(),
18823 storage_class: String::new(),
18824 }];
18825
18826 app.s3_state
18828 .expanded_prefixes
18829 .insert("folder1/".to_string());
18830 app.s3_state.prefix_preview.insert(
18831 "folder1/".to_string(),
18832 (0..20)
18833 .map(|i| S3Object {
18834 key: format!("folder1/file{}.txt", i),
18835 is_prefix: false,
18836 size: 100,
18837 last_modified: String::new(),
18838 storage_class: String::new(),
18839 })
18840 .collect(),
18841 );
18842
18843 app.s3_state.object_visible_rows.set(10);
18845 app.s3_state.object_scroll_offset = 0;
18846 app.s3_state.selected_object = 0; for i in 1..=20 {
18850 app.handle_action(Action::NextItem);
18851 assert_eq!(app.s3_state.selected_object, i);
18852
18853 let visible_start = app.s3_state.object_scroll_offset;
18855 let visible_end = visible_start + app.s3_state.object_visible_rows.get();
18856 assert!(
18857 app.s3_state.selected_object >= visible_start
18858 && app.s3_state.selected_object < visible_end,
18859 "Selection {} should be visible in range [{}, {})",
18860 app.s3_state.selected_object,
18861 visible_start,
18862 visible_end
18863 );
18864 }
18865 }
18866
18867 #[test]
18868 fn test_s3_bucket_error_rows_counted_in_total() {
18869 use S3Bucket;
18870
18871 let mut app = test_app();
18872 app.current_service = Service::S3Buckets;
18873 app.service_selected = true;
18874
18875 app.s3_state.buckets.items = vec![
18877 S3Bucket {
18878 name: "bucket1".to_string(),
18879 region: "us-east-1".to_string(),
18880 creation_date: String::new(),
18881 },
18882 S3Bucket {
18883 name: "bucket2".to_string(),
18884 region: "us-east-1".to_string(),
18885 creation_date: String::new(),
18886 },
18887 ];
18888
18889 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18891 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();
18892 app.s3_state
18893 .bucket_errors
18894 .insert("bucket1".to_string(), long_error.clone());
18895
18896 let total = app.calculate_total_bucket_rows();
18898
18899 let error_rows = long_error.len().div_ceil(120);
18901 assert_eq!(total, 2 + error_rows);
18902 }
18903
18904 #[test]
18905 fn test_s3_bucket_with_error_can_be_collapsed() {
18906 use S3Bucket;
18907
18908 let mut app = test_app();
18909 app.current_service = Service::S3Buckets;
18910 app.service_selected = true;
18911 app.mode = Mode::Normal;
18912
18913 app.s3_state.buckets.items = vec![S3Bucket {
18915 name: "bucket1".to_string(),
18916 region: "us-east-1".to_string(),
18917 creation_date: String::new(),
18918 }];
18919
18920 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18922 let error = "service error: PermanentRedirect".to_string();
18923 app.s3_state
18924 .bucket_errors
18925 .insert("bucket1".to_string(), error);
18926
18927 app.s3_state.selected_row = 0;
18929
18930 app.handle_action(Action::CollapseRow);
18932
18933 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18935 assert_eq!(app.s3_state.selected_row, 0);
18937 }
18938
18939 #[test]
18940 fn test_s3_bucket_collapse_on_bucket_row() {
18941 use S3Bucket;
18942
18943 let mut app = test_app();
18944 app.current_service = Service::S3Buckets;
18945 app.service_selected = true;
18946 app.mode = Mode::Normal;
18947
18948 app.s3_state.buckets.items = vec![S3Bucket {
18950 name: "bucket1".to_string(),
18951 region: "us-east-1".to_string(),
18952 creation_date: String::new(),
18953 }];
18954
18955 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18957 let error = "service error: PermanentRedirect".to_string();
18958 app.s3_state
18959 .bucket_errors
18960 .insert("bucket1".to_string(), error);
18961
18962 app.s3_state.selected_row = 0;
18964
18965 app.handle_action(Action::CollapseRow);
18967
18968 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18970 assert_eq!(app.s3_state.selected_row, 0);
18972 }
18973
18974 #[test]
18975 fn test_s3_bucket_collapse_adjusts_scroll_offset() {
18976 use S3Bucket;
18977
18978 let mut app = test_app();
18979 app.current_service = Service::S3Buckets;
18980 app.service_selected = true;
18981 app.mode = Mode::Normal;
18982
18983 app.s3_state.buckets.items = (0..20)
18985 .map(|i| S3Bucket {
18986 name: format!("bucket{}", i),
18987 region: "us-east-1".to_string(),
18988 creation_date: String::new(),
18989 })
18990 .collect();
18991
18992 app.s3_state
18994 .expanded_prefixes
18995 .insert("bucket10".to_string());
18996 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();
18997 app.s3_state
18998 .bucket_errors
18999 .insert("bucket10".to_string(), long_error.clone());
19000
19001 app.s3_state.bucket_visible_rows.set(10);
19003 app.s3_state.bucket_scroll_offset = 10; app.s3_state.selected_row = 10;
19007
19008 app.handle_action(Action::CollapseRow);
19010
19011 assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
19013 assert_eq!(app.s3_state.selected_row, 10);
19015 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
19017 assert!(
19018 app.s3_state.selected_row
19019 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
19020 );
19021 }
19022
19023 #[test]
19024 fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
19025 use S3Bucket;
19026
19027 let mut app = test_app();
19028 app.current_service = Service::S3Buckets;
19029 app.service_selected = true;
19030 app.mode = Mode::Normal;
19031
19032 app.s3_state.buckets.items = vec![
19034 S3Bucket {
19035 name: "bucket1".to_string(),
19036 region: "us-east-1".to_string(),
19037 creation_date: String::new(),
19038 },
19039 S3Bucket {
19040 name: "bucket2".to_string(),
19041 region: "us-east-1".to_string(),
19042 creation_date: String::new(),
19043 },
19044 S3Bucket {
19045 name: "bucket3".to_string(),
19046 region: "us-east-1".to_string(),
19047 creation_date: String::new(),
19048 },
19049 ];
19050
19051 app.s3_state.expanded_prefixes.insert("bucket2".to_string());
19053 app.s3_state.bucket_preview.insert(
19054 "bucket2".to_string(),
19055 vec![
19056 S3Object {
19057 key: "folder1/".to_string(),
19058 is_prefix: true,
19059 size: 0,
19060 last_modified: String::new(),
19061 storage_class: String::new(),
19062 },
19063 S3Object {
19064 key: "file1.txt".to_string(),
19065 is_prefix: false,
19066 size: 100,
19067 last_modified: String::new(),
19068 storage_class: String::new(),
19069 },
19070 ],
19071 );
19072
19073 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
19075 let error = "service error: PermanentRedirect".to_string();
19076 app.s3_state
19077 .bucket_errors
19078 .insert("bucket3".to_string(), error);
19079
19080 app.s3_state.bucket_visible_rows.set(10);
19082 app.s3_state.bucket_scroll_offset = 0;
19083
19084 app.s3_state.selected_row = 3;
19086
19087 app.handle_action(Action::CollapseRow);
19089
19090 assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
19092 assert_eq!(app.s3_state.selected_row, 1);
19094 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
19096 assert!(
19097 app.s3_state.selected_row
19098 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
19099 );
19100 }
19101
19102 #[test]
19103 fn test_s3_collapse_bucket_with_error() {
19104 use S3Bucket;
19105
19106 let mut app = test_app();
19107 app.current_service = Service::S3Buckets;
19108 app.service_selected = true;
19109 app.mode = Mode::Normal;
19110
19111 app.s3_state.buckets.items = vec![
19112 S3Bucket {
19113 name: "bucket1".to_string(),
19114 region: "us-east-1".to_string(),
19115 creation_date: String::new(),
19116 },
19117 S3Bucket {
19118 name: "bucket2".to_string(),
19119 region: "us-east-1".to_string(),
19120 creation_date: String::new(),
19121 },
19122 ];
19123
19124 let error = "service error: unhandled error (PermanentRedirect)".to_string();
19125 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19126 app.s3_state
19127 .bucket_errors
19128 .insert("bucket1".to_string(), error);
19129
19130 app.s3_state.bucket_visible_rows.set(10);
19131 app.s3_state.bucket_scroll_offset = 0;
19132
19133 app.s3_state.selected_row = 0;
19135
19136 app.handle_action(Action::CollapseRow);
19138
19139 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19141 assert_eq!(app.s3_state.selected_row, 0);
19142 }
19143
19144 #[test]
19145 fn test_s3_collapse_row_with_multiple_error_buckets() {
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 = vec![
19154 S3Bucket {
19155 name: "bucket1".to_string(),
19156 region: "us-east-1".to_string(),
19157 creation_date: String::new(),
19158 },
19159 S3Bucket {
19160 name: "bucket2".to_string(),
19161 region: "us-east-1".to_string(),
19162 creation_date: String::new(),
19163 },
19164 S3Bucket {
19165 name: "bucket3".to_string(),
19166 region: "us-east-1".to_string(),
19167 creation_date: String::new(),
19168 },
19169 ];
19170
19171 let error = "service error: unhandled error (PermanentRedirect)".to_string();
19172
19173 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19175 app.s3_state
19176 .bucket_errors
19177 .insert("bucket1".to_string(), error.clone());
19178
19179 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
19181 app.s3_state
19182 .bucket_errors
19183 .insert("bucket3".to_string(), error.clone());
19184
19185 app.s3_state.bucket_visible_rows.set(30);
19186 app.s3_state.bucket_scroll_offset = 0;
19187
19188 app.s3_state.selected_row = 2;
19193
19194 app.handle_action(Action::CollapseRow);
19195
19196 assert!(
19198 !app.s3_state.expanded_prefixes.contains("bucket3"),
19199 "bucket3 should be collapsed"
19200 );
19201 assert!(
19202 app.s3_state.expanded_prefixes.contains("bucket1"),
19203 "bucket1 should still be expanded"
19204 );
19205 assert_eq!(app.s3_state.selected_row, 2);
19206 }
19207
19208 #[test]
19209 fn test_s3_collapse_row_nested_only_collapses_one_level() {
19210 use S3Bucket;
19211
19212 let mut app = test_app();
19213 app.current_service = Service::S3Buckets;
19214 app.service_selected = true;
19215 app.mode = Mode::Normal;
19216
19217 app.s3_state.buckets.items = vec![S3Bucket {
19218 name: "bucket1".to_string(),
19219 region: "us-east-1".to_string(),
19220 creation_date: String::new(),
19221 }];
19222
19223 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19225 app.s3_state.bucket_preview.insert(
19226 "bucket1".to_string(),
19227 vec![S3Object {
19228 key: "level1/".to_string(),
19229 size: 0,
19230 last_modified: "2024-01-01T00:00:00Z".to_string(),
19231 is_prefix: true,
19232 storage_class: String::new(),
19233 }],
19234 );
19235
19236 app.s3_state.expanded_prefixes.insert("level1/".to_string());
19238 app.s3_state.prefix_preview.insert(
19239 "level1/".to_string(),
19240 vec![S3Object {
19241 key: "level1/level2/".to_string(),
19242 size: 0,
19243 last_modified: "2024-01-01T00:00:00Z".to_string(),
19244 is_prefix: true,
19245 storage_class: String::new(),
19246 }],
19247 );
19248
19249 app.s3_state
19251 .expanded_prefixes
19252 .insert("level1/level2/".to_string());
19253 app.s3_state.prefix_preview.insert(
19254 "level1/level2/".to_string(),
19255 vec![S3Object {
19256 key: "level1/level2/file.txt".to_string(),
19257 size: 100,
19258 last_modified: "2024-01-01T00:00:00Z".to_string(),
19259 is_prefix: false,
19260 storage_class: String::new(),
19261 }],
19262 );
19263
19264 app.s3_state.bucket_visible_rows.set(10);
19265
19266 app.s3_state.selected_row = 2;
19268
19269 app.handle_action(Action::CollapseRow);
19271
19272 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
19274 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
19276 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
19278 assert_eq!(app.s3_state.selected_row, 1);
19280 }
19281
19282 #[test]
19283 fn test_s3_collapse_row_deeply_nested_file() {
19284 use S3Bucket;
19285
19286 let mut app = test_app();
19287 app.current_service = Service::S3Buckets;
19288 app.service_selected = true;
19289 app.mode = Mode::Normal;
19290
19291 app.s3_state.buckets.items = vec![S3Bucket {
19292 name: "bucket1".to_string(),
19293 region: "us-east-1".to_string(),
19294 creation_date: String::new(),
19295 }];
19296
19297 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19299 app.s3_state.bucket_preview.insert(
19300 "bucket1".to_string(),
19301 vec![S3Object {
19302 key: "level1/".to_string(),
19303 size: 0,
19304 last_modified: "2024-01-01T00:00:00Z".to_string(),
19305 is_prefix: true,
19306 storage_class: String::new(),
19307 }],
19308 );
19309
19310 app.s3_state.expanded_prefixes.insert("level1/".to_string());
19312 app.s3_state.prefix_preview.insert(
19313 "level1/".to_string(),
19314 vec![S3Object {
19315 key: "level1/level2/".to_string(),
19316 size: 0,
19317 last_modified: "2024-01-01T00:00:00Z".to_string(),
19318 is_prefix: true,
19319 storage_class: String::new(),
19320 }],
19321 );
19322
19323 app.s3_state
19325 .expanded_prefixes
19326 .insert("level1/level2/".to_string());
19327 app.s3_state.prefix_preview.insert(
19328 "level1/level2/".to_string(),
19329 vec![S3Object {
19330 key: "level1/level2/file.txt".to_string(),
19331 size: 100,
19332 last_modified: "2024-01-01T00:00:00Z".to_string(),
19333 is_prefix: false,
19334 storage_class: String::new(),
19335 }],
19336 );
19337
19338 app.s3_state.bucket_visible_rows.set(10);
19339
19340 app.s3_state.selected_row = 3;
19342
19343 app.handle_action(Action::CollapseRow);
19345
19346 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
19348 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
19349 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
19350 assert_eq!(app.s3_state.selected_row, 2);
19352 }
19353
19354 #[test]
19355 fn test_s3_bucket_pagination_adjusts_scroll() {
19356 use S3Bucket;
19357
19358 let mut app = test_app();
19359 app.current_service = Service::S3Buckets;
19360 app.service_selected = true;
19361 app.mode = Mode::Normal;
19362
19363 app.s3_state.buckets.items = (0..150)
19365 .map(|i| S3Bucket {
19366 name: format!("bucket{:03}", i),
19367 region: "us-east-1".to_string(),
19368 creation_date: String::new(),
19369 })
19370 .collect();
19371
19372 app.s3_state.bucket_visible_rows.set(20);
19373 app.s3_state.selected_row = 0;
19374 app.s3_state.bucket_scroll_offset = 0;
19375
19376 app.go_to_page(2);
19378
19379 assert_eq!(app.s3_state.selected_row, 50);
19380 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
19382
19383 app.go_to_page(3);
19385
19386 assert_eq!(app.s3_state.selected_row, 100);
19387 assert_eq!(app.s3_state.bucket_scroll_offset, 100);
19388
19389 app.go_to_page(1);
19391
19392 assert_eq!(app.s3_state.selected_row, 0);
19393 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
19394 }
19395
19396 #[test]
19397 fn test_s3_bucket_pagination_uses_page_size() {
19398 use S3Bucket;
19399
19400 let mut app = test_app();
19401 app.current_service = Service::S3Buckets;
19402 app.service_selected = true;
19403 app.mode = Mode::Normal;
19404
19405 app.s3_state.buckets.items = (0..100)
19407 .map(|i| S3Bucket {
19408 name: format!("bucket{:03}", i),
19409 region: "us-east-1".to_string(),
19410 creation_date: String::new(),
19411 })
19412 .collect();
19413
19414 app.s3_state.bucket_visible_rows.set(20);
19415 app.s3_state.selected_row = 0;
19416
19417 assert_eq!(app.s3_state.buckets.page_size.value(), 50);
19419
19420 app.go_to_page(2);
19422 assert_eq!(app.s3_state.selected_row, 50);
19423 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
19424
19425 app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
19427 assert_eq!(app.s3_state.buckets.page_size.value(), 25);
19428
19429 app.go_to_page(2);
19431 assert_eq!(app.s3_state.selected_row, 25);
19432 assert_eq!(app.s3_state.bucket_scroll_offset, 25);
19433 }
19434
19435 #[test]
19436 fn test_s3_bucket_page_size_limits_visible_rows() {
19437 use S3Bucket;
19438
19439 let mut app = test_app();
19440 app.current_service = Service::S3Buckets;
19441 app.service_selected = true;
19442 app.mode = Mode::Normal;
19443
19444 app.s3_state.buckets.items = (0..100)
19446 .map(|i| S3Bucket {
19447 name: format!("bucket{:03}", i),
19448 region: "us-east-1".to_string(),
19449 creation_date: String::new(),
19450 })
19451 .collect();
19452
19453 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19455 assert_eq!(app.s3_state.buckets.page_size.value(), 10);
19456
19457 let total_rows = app.calculate_total_bucket_rows();
19459 assert!(total_rows >= 10, "Should have at least 10 rows");
19463 }
19464
19465 #[test]
19466 fn test_s3_bucket_tab_cycling_in_filter() {
19467 use crate::common::InputFocus;
19468
19469 let mut app = test_app();
19470 app.current_service = Service::S3Buckets;
19471 app.mode = Mode::FilterInput;
19472
19473 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
19475
19476 app.handle_action(Action::NextFilterFocus);
19478 assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
19479
19480 app.handle_action(Action::NextFilterFocus);
19482 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
19483 }
19484
19485 #[test]
19486 fn test_s3_bucket_pagination_navigation_with_arrows() {
19487 use S3Bucket;
19488
19489 let mut app = test_app();
19490 app.current_service = Service::S3Buckets;
19491 app.mode = Mode::FilterInput;
19492
19493 app.s3_state.buckets.items = (0..100)
19495 .map(|i| S3Bucket {
19496 name: format!("bucket{:03}", i),
19497 region: "us-east-1".to_string(),
19498 creation_date: String::new(),
19499 })
19500 .collect();
19501
19502 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19503 app.s3_state.selected_row = 0;
19504
19505 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
19507
19508 app.handle_action(Action::NextItem);
19510 assert_eq!(app.s3_state.selected_row, 10);
19511
19512 app.handle_action(Action::NextItem);
19514 assert_eq!(app.s3_state.selected_row, 20);
19515
19516 app.handle_action(Action::PrevItem);
19518 assert_eq!(app.s3_state.selected_row, 10);
19519 }
19520
19521 #[test]
19522 fn test_s3_bucket_go_to_page_shows_correct_buckets() {
19523 use S3Bucket;
19524
19525 let mut app = test_app();
19526 app.current_service = Service::S3Buckets;
19527 app.service_selected = true;
19528 app.mode = Mode::Normal;
19529
19530 app.s3_state.buckets.items = (0..100)
19532 .map(|i| S3Bucket {
19533 name: format!("bucket{:03}", i),
19534 region: "us-east-1".to_string(),
19535 creation_date: String::new(),
19536 })
19537 .collect();
19538
19539 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19540
19541 app.go_to_page(2);
19543 assert_eq!(app.s3_state.selected_row, 10);
19544
19545 app.go_to_page(5);
19547 assert_eq!(app.s3_state.selected_row, 40);
19548 }
19549
19550 #[test]
19551 fn test_s3_bucket_left_right_arrows_change_pages() {
19552 use S3Bucket;
19553
19554 let mut app = test_app();
19555 app.current_service = Service::S3Buckets;
19556 app.mode = Mode::FilterInput;
19557
19558 app.s3_state.buckets.items = (0..100)
19560 .map(|i| S3Bucket {
19561 name: format!("bucket{:03}", i),
19562 region: "us-east-1".to_string(),
19563 creation_date: String::new(),
19564 })
19565 .collect();
19566
19567 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19568 app.s3_state.selected_row = 0;
19569 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
19570
19571 app.handle_action(Action::PageDown);
19573 assert_eq!(app.s3_state.selected_row, 10);
19574
19575 app.handle_action(Action::PageDown);
19577 assert_eq!(app.s3_state.selected_row, 20);
19578
19579 app.handle_action(Action::PageUp);
19581 assert_eq!(app.s3_state.selected_row, 10);
19582 }
19583
19584 #[test]
19585 fn test_s3_bucket_preview_uses_bucket_region_not_config_region() {
19586 use S3Bucket;
19594 let mut app = test_app();
19595 app.config.region = "ap-southeast-3".to_string(); app.s3_state.buckets.items = vec![S3Bucket {
19599 name: "my-bucket".to_string(),
19600 region: "us-east-1".to_string(), creation_date: String::new(),
19602 }];
19603
19604 let bucket = app
19606 .s3_state
19607 .buckets
19608 .items
19609 .iter()
19610 .find(|b| b.name == "my-bucket")
19611 .unwrap();
19612 let effective_region = if bucket.region.is_empty() {
19613 app.config.region.as_str()
19614 } else {
19615 bucket.region.as_str()
19616 };
19617 assert_eq!(
19618 effective_region, "us-east-1",
19619 "load_bucket_preview must use bucket's region, not app config region"
19620 );
19621 assert_ne!(
19622 effective_region, "ap-southeast-3",
19623 "Must NOT fall back to configured region for cross-region buckets"
19624 );
19625 }
19626
19627 #[test]
19628 fn test_s3_scroll_follows_selection_after_bucket_expand() {
19629 use S3Bucket;
19630 use S3Object;
19631 let mut app = test_app();
19634 app.current_service = Service::S3Buckets;
19635 app.service_selected = true;
19636 app.mode = Mode::Normal;
19637
19638 app.s3_state.buckets.items = (0..5)
19640 .map(|i| S3Bucket {
19641 name: format!("bucket{}", i),
19642 region: "us-east-1".to_string(),
19643 creation_date: String::new(),
19644 })
19645 .collect();
19646
19647 app.s3_state.bucket_visible_rows = std::cell::Cell::new(3);
19648
19649 app.s3_state.selected_row = 2;
19651 app.s3_state.bucket_scroll_offset = 0;
19652
19653 app.s3_state.bucket_preview.insert(
19655 "bucket2".to_string(),
19656 vec![
19657 S3Object {
19658 key: "file1.txt".to_string(),
19659 is_prefix: false,
19660 size: 100,
19661 last_modified: String::new(),
19662 storage_class: String::new(),
19663 },
19664 S3Object {
19665 key: "file2.txt".to_string(),
19666 is_prefix: false,
19667 size: 200,
19668 last_modified: String::new(),
19669 storage_class: String::new(),
19670 },
19671 ],
19672 );
19673
19674 app.handle_action(Action::CollapseRow); app.handle_action(Action::NextPane); let visible = app.s3_state.bucket_visible_rows.get();
19682 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
19683 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
19684 assert!(
19685 in_view,
19686 "selected_row {} must be visible in [{}, {})",
19687 app.s3_state.selected_row,
19688 app.s3_state.bucket_scroll_offset,
19689 app.s3_state.bucket_scroll_offset + visible
19690 );
19691 }
19692
19693 #[test]
19694 fn test_s3_expand_without_preview_does_not_move_selection() {
19695 use S3Bucket;
19699 let mut app = test_app();
19700 app.current_service = Service::S3Buckets;
19701 app.service_selected = true;
19702 app.mode = Mode::Normal;
19703
19704 app.s3_state.buckets.items = vec![
19705 S3Bucket {
19706 name: "bucket0".to_string(),
19707 region: "us-east-1".to_string(),
19708 creation_date: String::new(),
19709 },
19710 S3Bucket {
19711 name: "bucket1".to_string(),
19712 region: "us-east-1".to_string(),
19713 creation_date: String::new(),
19714 },
19715 ];
19716 app.s3_state.selected_row = 0; app.handle_action(Action::NextPane); assert_eq!(
19723 app.s3_state.selected_row, 0,
19724 "Selection must not move before preview is loaded"
19725 );
19726 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
19728 assert!(app.s3_state.buckets.loading);
19729 }
19730
19731 #[test]
19732 fn test_s3_expand_with_preview_moves_to_first_child() {
19733 use crate::s3::Object as S3Object;
19735 use S3Bucket;
19736 let mut app = test_app();
19737 app.current_service = Service::S3Buckets;
19738 app.service_selected = true;
19739 app.mode = Mode::Normal;
19740
19741 app.s3_state.buckets.items = vec![
19742 S3Bucket {
19743 name: "bucket0".to_string(),
19744 region: "us-east-1".to_string(),
19745 creation_date: String::new(),
19746 },
19747 S3Bucket {
19748 name: "bucket1".to_string(),
19749 region: "us-east-1".to_string(),
19750 creation_date: String::new(),
19751 },
19752 ];
19753 app.s3_state.selected_row = 0;
19754 app.s3_state.bucket_preview.insert(
19755 "bucket0".to_string(),
19756 vec![S3Object {
19757 key: "file.txt".to_string(),
19758 is_prefix: false,
19759 size: 0,
19760 last_modified: String::new(),
19761 storage_class: String::new(),
19762 }],
19763 );
19764 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19765
19766 app.handle_action(Action::NextPane);
19768 assert_eq!(app.s3_state.selected_row, 1, "Should move to first child");
19769 }
19770
19771 #[test]
19772 fn test_s3_expand_then_load_then_right_goes_to_first_child_not_sibling() {
19773 use crate::s3::Object as S3Object;
19777 use S3Bucket;
19778 let mut app = test_app();
19779 app.current_service = Service::S3Buckets;
19780 app.service_selected = true;
19781 app.mode = Mode::Normal;
19782 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19783
19784 app.s3_state.buckets.items = vec![
19785 S3Bucket {
19786 name: "bucket0".to_string(),
19787 region: "us-east-1".to_string(),
19788 creation_date: String::new(),
19789 },
19790 S3Bucket {
19791 name: "bucket1".to_string(),
19792 region: "us-east-1".to_string(),
19793 creation_date: String::new(),
19794 },
19795 ];
19796 app.s3_state.selected_row = 0;
19797
19798 app.handle_action(Action::NextPane);
19800 assert_eq!(
19801 app.s3_state.selected_row, 0,
19802 "Must stay on bucket0 while loading"
19803 );
19804 assert!(app.s3_state.buckets.loading);
19805
19806 app.s3_state.bucket_preview.insert(
19808 "bucket0".to_string(),
19809 vec![S3Object {
19810 key: "file.txt".to_string(),
19811 is_prefix: false,
19812 size: 0,
19813 last_modified: String::new(),
19814 storage_class: String::new(),
19815 }],
19816 );
19817 app.s3_state.buckets.loading = false;
19818
19819 app.handle_action(Action::NextPane);
19821 assert_eq!(
19822 app.s3_state.selected_row, 1,
19823 "After preview loads, right arrow must select first child (row 1), not sibling (row 2)"
19824 );
19825 }
19826
19827 #[test]
19828 fn test_s3_first_expand_never_moves_selection_before_children_visible() {
19829 use S3Bucket;
19833 let mut app = test_app();
19834 app.current_service = Service::S3Buckets;
19835 app.service_selected = true;
19836 app.mode = Mode::Normal;
19837 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19838
19839 app.s3_state.buckets.items = vec![
19840 S3Bucket {
19841 name: "bucket0".to_string(),
19842 region: "us-east-1".to_string(),
19843 creation_date: String::new(),
19844 },
19845 S3Bucket {
19846 name: "bucket1".to_string(),
19847 region: "us-east-1".to_string(),
19848 creation_date: String::new(),
19849 },
19850 ];
19851 app.s3_state.selected_row = 0;
19853
19854 app.handle_action(Action::ExpandRow);
19856 assert_eq!(
19857 app.s3_state.selected_row, 0,
19858 "Selection must stay on bucket0 until children are visible"
19859 );
19860 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
19861 assert!(app.s3_state.buckets.loading);
19862 }
19863
19864 #[test]
19865 fn test_s3_after_preview_loads_selection_moves_to_first_child() {
19866 use crate::s3::Object as S3Object;
19869 use S3Bucket;
19870 let mut app = test_app();
19871 app.current_service = Service::S3Buckets;
19872 app.service_selected = true;
19873 app.mode = Mode::Normal;
19874 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19875 app.s3_state.buckets.items = vec![
19876 S3Bucket {
19877 name: "b0".to_string(),
19878 region: "us-east-1".to_string(),
19879 creation_date: String::new(),
19880 },
19881 S3Bucket {
19882 name: "b1".to_string(),
19883 region: "us-east-1".to_string(),
19884 creation_date: String::new(),
19885 },
19886 ];
19887 app.s3_state.selected_row = 0;
19888
19889 app.handle_action(Action::ExpandRow);
19891 assert_eq!(
19892 app.s3_state.selected_row, 0,
19893 "Must stay on bucket while loading"
19894 );
19895
19896 app.s3_state.bucket_preview.insert(
19898 "b0".to_string(),
19899 vec![S3Object {
19900 key: "f.txt".to_string(),
19901 is_prefix: false,
19902 size: 0,
19903 last_modified: String::new(),
19904 storage_class: String::new(),
19905 }],
19906 );
19907 app.after_bucket_preview_loaded("b0");
19909 assert_eq!(
19910 app.s3_state.selected_row, 1,
19911 "After preview loads, selection must advance to first child (row 1)"
19912 );
19913 }
19914
19915 #[test]
19916 fn test_s3_right_arrow_on_expanded_loaded_bucket_enters_first_child() {
19917 use crate::s3::Object as S3Object;
19920 use S3Bucket;
19921 let mut app = test_app();
19922 app.current_service = Service::S3Buckets;
19923 app.service_selected = true;
19924 app.mode = Mode::Normal;
19925 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19926 app.s3_state.buckets.items = vec![
19927 S3Bucket {
19928 name: "b0".to_string(),
19929 region: "us-east-1".to_string(),
19930 creation_date: String::new(),
19931 },
19932 S3Bucket {
19933 name: "b1".to_string(),
19934 region: "us-east-1".to_string(),
19935 creation_date: String::new(),
19936 },
19937 ];
19938 app.s3_state.expanded_prefixes.insert("b0".to_string());
19939 app.s3_state.bucket_preview.insert(
19940 "b0".to_string(),
19941 vec![S3Object {
19942 key: "f.txt".to_string(),
19943 is_prefix: false,
19944 size: 0,
19945 last_modified: String::new(),
19946 storage_class: String::new(),
19947 }],
19948 );
19949 app.s3_state.selected_row = 0;
19950
19951 app.handle_action(Action::ExpandRow);
19952
19953 assert!(
19955 app.s3_state.expanded_prefixes.contains("b0"),
19956 "Must stay expanded"
19957 );
19958 assert_eq!(app.s3_state.selected_row, 1, "Must move to first child");
19959 }
19960
19961 #[test]
19962 fn test_s3_second_expand_press_collapses_when_already_expanded_with_children() {
19963 use crate::s3::Object as S3Object;
19966 use S3Bucket;
19967 let mut app = test_app();
19968 app.current_service = Service::S3Buckets;
19969 app.service_selected = true;
19970 app.mode = Mode::Normal;
19971 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19972 app.s3_state.buckets.items = vec![S3Bucket {
19973 name: "bucket0".to_string(),
19974 region: "us-east-1".to_string(),
19975 creation_date: String::new(),
19976 }];
19977 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
19978 app.s3_state.bucket_preview.insert(
19979 "bucket0".to_string(),
19980 vec![S3Object {
19981 key: "file.txt".to_string(),
19982 is_prefix: false,
19983 size: 0,
19984 last_modified: String::new(),
19985 storage_class: String::new(),
19986 }],
19987 );
19988 app.s3_state.selected_row = 0;
19989
19990 app.handle_action(Action::CollapseRow);
19992 assert!(
19993 !app.s3_state.expanded_prefixes.contains("bucket0"),
19994 "Left arrow must collapse the bucket"
19995 );
19996 assert_eq!(app.s3_state.selected_row, 0);
19997 }
19998
19999 #[test]
20000 fn test_s3_expand_row_index_correct_when_prior_bucket_has_error() {
20001 use S3Bucket;
20007 let mut app = test_app();
20008 app.current_service = Service::S3Buckets;
20009 app.service_selected = true;
20010 app.mode = Mode::Normal;
20011 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20012
20013 app.s3_state.buckets.items = vec![
20014 S3Bucket {
20015 name: "bucket0".to_string(),
20016 region: "us-east-1".to_string(),
20017 creation_date: String::new(),
20018 },
20019 S3Bucket {
20020 name: "bucket1".to_string(),
20021 region: "us-east-1".to_string(),
20022 creation_date: String::new(),
20023 },
20024 S3Bucket {
20025 name: "bucket2".to_string(),
20026 region: "us-east-1".to_string(),
20027 creation_date: String::new(),
20028 },
20029 ];
20030
20031 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20033 app.s3_state
20034 .bucket_errors
20035 .insert("bucket0".to_string(), "AccessDenied".to_string());
20036
20037 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
20040 assert_eq!(
20041 total, 4,
20042 "Should be 4 rows: bucket0 + error + bucket1 + bucket2"
20043 );
20044
20045 app.s3_state.selected_row = 3;
20047
20048 use crate::s3::Object as S3Object;
20050 app.s3_state.bucket_preview.insert(
20051 "bucket2".to_string(),
20052 vec![S3Object {
20053 key: "file.txt".to_string(),
20054 is_prefix: false,
20055 size: 0,
20056 last_modified: String::new(),
20057 storage_class: String::new(),
20058 }],
20059 );
20060
20061 app.handle_action(Action::NextPane);
20062
20063 assert!(
20065 app.s3_state.expanded_prefixes.contains("bucket2"),
20066 "Must expand bucket2, not bucket1"
20067 );
20068 assert_eq!(
20069 app.s3_state.selected_row, 4,
20070 "First child of bucket2 must be row 4"
20071 );
20072 }
20073
20074 #[test]
20075 fn test_s3_next_pane_row_count_matches_calculate_filtered_bucket_rows() {
20076 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 = vec![
20088 S3Bucket {
20089 name: "bucket0".to_string(),
20090 region: "us-east-1".to_string(),
20091 creation_date: String::new(),
20092 },
20093 S3Bucket {
20094 name: "bucket1".to_string(),
20095 region: "us-east-1".to_string(),
20096 creation_date: String::new(),
20097 },
20098 S3Bucket {
20099 name: "bucket2".to_string(),
20100 region: "us-east-1".to_string(),
20101 creation_date: String::new(),
20102 },
20103 ];
20104
20105 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20107 app.s3_state.bucket_preview.insert(
20108 "bucket0".to_string(),
20109 vec![
20110 S3Object {
20111 key: "a.txt".to_string(),
20112 is_prefix: false,
20113 size: 0,
20114 last_modified: String::new(),
20115 storage_class: String::new(),
20116 },
20117 S3Object {
20118 key: "b.txt".to_string(),
20119 is_prefix: false,
20120 size: 0,
20121 last_modified: String::new(),
20122 storage_class: String::new(),
20123 },
20124 S3Object {
20125 key: "c.txt".to_string(),
20126 is_prefix: false,
20127 size: 0,
20128 last_modified: String::new(),
20129 storage_class: String::new(),
20130 },
20131 ],
20132 );
20133
20134 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20136
20137 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
20139 assert_eq!(
20140 total, 6,
20141 "Total rows: bucket0(1)+3children+bucket1(1)+bucket2(1)=6"
20142 );
20143
20144 app.s3_state.selected_row = 5;
20147
20148 app.s3_state.bucket_preview.insert(
20150 "bucket2".to_string(),
20151 vec![S3Object {
20152 key: "x.txt".to_string(),
20153 is_prefix: false,
20154 size: 0,
20155 last_modified: String::new(),
20156 storage_class: String::new(),
20157 }],
20158 );
20159
20160 app.handle_action(Action::NextPane);
20161
20162 assert!(
20163 app.s3_state.expanded_prefixes.contains("bucket2"),
20164 "Must expand bucket2, not bucket1"
20165 );
20166 assert_eq!(
20167 app.s3_state.selected_row, 6,
20168 "First child of bucket2 must be row 6"
20169 );
20170 }
20171
20172 #[test]
20173 fn test_s3_expand_row_index_correct_when_prior_bucket_has_loaded_children() {
20174 use crate::s3::Object as S3Object;
20179 use S3Bucket;
20180 let mut app = test_app();
20181 app.current_service = Service::S3Buckets;
20182 app.service_selected = true;
20183 app.mode = Mode::Normal;
20184 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20185
20186 app.s3_state.buckets.items = vec![
20187 S3Bucket {
20188 name: "bucket0".to_string(),
20189 region: "us-east-1".to_string(),
20190 creation_date: String::new(),
20191 },
20192 S3Bucket {
20193 name: "bucket1".to_string(),
20194 region: "us-east-1".to_string(),
20195 creation_date: String::new(),
20196 },
20197 S3Bucket {
20198 name: "bucket2".to_string(),
20199 region: "us-east-1".to_string(),
20200 creation_date: String::new(),
20201 },
20202 ];
20203
20204 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20206 app.s3_state.bucket_preview.insert(
20207 "bucket0".to_string(),
20208 vec![
20209 S3Object {
20210 key: "a.txt".to_string(),
20211 is_prefix: false,
20212 size: 0,
20213 last_modified: String::new(),
20214 storage_class: String::new(),
20215 },
20216 S3Object {
20217 key: "b.txt".to_string(),
20218 is_prefix: false,
20219 size: 0,
20220 last_modified: String::new(),
20221 storage_class: String::new(),
20222 },
20223 ],
20224 );
20225
20226 app.s3_state.selected_row = 4;
20229
20230 app.s3_state.bucket_preview.insert(
20232 "bucket2".to_string(),
20233 vec![S3Object {
20234 key: "c.txt".to_string(),
20235 is_prefix: false,
20236 size: 0,
20237 last_modified: String::new(),
20238 storage_class: String::new(),
20239 }],
20240 );
20241
20242 app.handle_action(Action::NextPane);
20243
20244 assert!(
20246 app.s3_state.expanded_prefixes.contains("bucket2"),
20247 "bucket2 must be expanded"
20248 );
20249 assert_eq!(
20250 app.s3_state.selected_row, 5,
20251 "Must select first child of bucket2 (row 5), not some wrong row"
20252 );
20253 assert!(
20254 !app.s3_state.expanded_prefixes.contains("bucket1"),
20255 "bucket1 must NOT be expanded"
20256 );
20257 }
20258
20259 #[test]
20260 fn test_s3_expand_with_empty_preview_stays_on_bucket() {
20261 use S3Bucket;
20264 let mut app = test_app();
20265 app.current_service = Service::S3Buckets;
20266 app.service_selected = true;
20267 app.mode = Mode::Normal;
20268 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20269
20270 app.s3_state.buckets.items = vec![
20271 S3Bucket {
20272 name: "bucket0".to_string(),
20273 region: "us-east-1".to_string(),
20274 creation_date: String::new(),
20275 },
20276 S3Bucket {
20277 name: "bucket1".to_string(),
20278 region: "us-east-1".to_string(),
20279 creation_date: String::new(),
20280 },
20281 S3Bucket {
20282 name: "bucket2".to_string(),
20283 region: "us-east-1".to_string(),
20284 creation_date: String::new(),
20285 },
20286 ];
20287 app.s3_state
20289 .bucket_preview
20290 .insert("bucket1".to_string(), vec![]);
20291 app.s3_state.selected_row = 1; app.handle_action(Action::NextPane);
20294
20295 assert_eq!(
20297 app.s3_state.selected_row, 1,
20298 "Empty bucket: selection must stay on bucket row, not advance to next sibling"
20299 );
20300 assert!(
20301 app.s3_state.expanded_prefixes.contains("bucket1"),
20302 "Bucket must be expanded (showing ▼)"
20303 );
20304 }
20305
20306 #[test]
20307 fn test_s3_expand_with_preceding_loading_buckets_on_prior_page() {
20308 use crate::s3::Object as S3Object;
20313 use S3Bucket;
20314 let mut app = test_app();
20315 app.current_service = Service::S3Buckets;
20316 app.service_selected = true;
20317 app.mode = Mode::Normal;
20318 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20319
20320 app.s3_state.buckets.items = (0..60)
20322 .map(|i| S3Bucket {
20323 name: format!("bucket{:02}", i),
20324 region: "us-east-1".to_string(),
20325 creation_date: String::new(),
20326 })
20327 .collect();
20328
20329 for i in 0..3usize {
20331 app.s3_state
20332 .expanded_prefixes
20333 .insert(format!("bucket{:02}", i));
20334 }
20335
20336 app.s3_state.bucket_scroll_offset = 50;
20337
20338 app.s3_state.bucket_preview.insert(
20340 "bucket55".to_string(),
20341 vec![S3Object {
20342 key: "file.txt".to_string(),
20343 is_prefix: false,
20344 size: 0,
20345 last_modified: String::new(),
20346 storage_class: String::new(),
20347 }],
20348 );
20349
20350 app.s3_state.selected_row = 55;
20352
20353 app.handle_action(Action::NextPane);
20354
20355 assert!(
20357 app.s3_state.expanded_prefixes.contains("bucket55"),
20358 "bucket55 must be expanded"
20359 );
20360 assert_eq!(
20361 app.s3_state.selected_row, 56,
20362 "Must select first child of bucket55 (row 56)"
20363 );
20364 }
20365
20366 #[test]
20367 fn test_s3_expand_with_multiple_preceding_loading_buckets() {
20368 use crate::s3::Object as S3Object;
20371 use S3Bucket;
20372 let mut app = test_app();
20373 app.current_service = Service::S3Buckets;
20374 app.service_selected = true;
20375 app.mode = Mode::Normal;
20376 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20377
20378 app.s3_state.buckets.items = (0..5)
20379 .map(|i| S3Bucket {
20380 name: format!("bucket{}", i),
20381 region: "us-east-1".to_string(),
20382 creation_date: String::new(),
20383 })
20384 .collect();
20385
20386 for i in 0..3 {
20388 app.s3_state
20389 .expanded_prefixes
20390 .insert(format!("bucket{}", i));
20391 }
20392
20393 app.s3_state.bucket_preview.insert(
20395 "bucket3".to_string(),
20396 vec![S3Object {
20397 key: "file.txt".to_string(),
20398 is_prefix: false,
20399 size: 0,
20400 last_modified: String::new(),
20401 storage_class: String::new(),
20402 }],
20403 );
20404
20405 app.s3_state.selected_row = 3; app.handle_action(Action::NextPane);
20410
20411 assert!(
20413 app.s3_state.expanded_prefixes.contains("bucket3"),
20414 "bucket3 must be expanded"
20415 );
20416 assert_eq!(
20417 app.s3_state.selected_row, 4,
20418 "Must select first child of bucket3 (row 4)"
20419 );
20420 }
20421
20422 #[test]
20423 fn test_s3_expand_with_preceding_loading_bucket_selects_correct_row() {
20424 use crate::s3::Object as S3Object;
20429 use S3Bucket;
20430 let mut app = test_app();
20431 app.current_service = Service::S3Buckets;
20432 app.service_selected = true;
20433 app.mode = Mode::Normal;
20434 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20435
20436 app.s3_state.buckets.items = vec![
20437 S3Bucket {
20438 name: "bucket0".to_string(),
20439 region: "us-east-1".to_string(),
20440 creation_date: String::new(),
20441 },
20442 S3Bucket {
20443 name: "bucket1".to_string(),
20444 region: "us-east-1".to_string(),
20445 creation_date: String::new(),
20446 },
20447 S3Bucket {
20448 name: "bucket2".to_string(),
20449 region: "us-east-1".to_string(),
20450 creation_date: String::new(),
20451 },
20452 ];
20453
20454 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20456 app.s3_state.buckets.loading = true;
20457
20458 app.s3_state.bucket_preview.insert(
20460 "bucket1".to_string(),
20461 vec![S3Object {
20462 key: "file.txt".to_string(),
20463 is_prefix: false,
20464 size: 0,
20465 last_modified: String::new(),
20466 storage_class: String::new(),
20467 }],
20468 );
20469
20470 app.s3_state.selected_row = 1;
20472
20473 app.handle_action(Action::NextPane);
20475
20476 assert_eq!(
20478 app.s3_state.selected_row, 2,
20479 "Must expand bucket1's first child (row 2), not bucket2 (row 3)"
20480 );
20481 assert!(
20482 app.s3_state.expanded_prefixes.contains("bucket1"),
20483 "bucket1 must be in expanded_prefixes"
20484 );
20485 }
20486
20487 #[test]
20488 fn test_s3_expand_last_row_on_page_shows_first_child_not_next_page() {
20489 use crate::s3::Object as S3Object;
20494 use S3Bucket;
20495 let mut app = test_app();
20496 app.current_service = Service::S3Buckets;
20497 app.service_selected = true;
20498 app.mode = Mode::Normal;
20499
20500 app.s3_state.buckets.items = vec![
20502 S3Bucket {
20503 name: "bucket0".to_string(),
20504 region: "us-east-1".to_string(),
20505 creation_date: String::new(),
20506 },
20507 S3Bucket {
20508 name: "bucket1".to_string(),
20509 region: "us-east-1".to_string(),
20510 creation_date: String::new(),
20511 },
20512 S3Bucket {
20513 name: "bucket2".to_string(),
20514 region: "us-east-1".to_string(),
20515 creation_date: String::new(),
20516 },
20517 ];
20518 app.s3_state.bucket_visible_rows = std::cell::Cell::new(2);
20519 app.s3_state.selected_row = 1;
20521 app.s3_state.bucket_scroll_offset = 0;
20522
20523 app.s3_state.bucket_preview.insert(
20525 "bucket1".to_string(),
20526 vec![S3Object {
20527 key: "file.txt".to_string(),
20528 is_prefix: false,
20529 size: 0,
20530 last_modified: String::new(),
20531 storage_class: String::new(),
20532 }],
20533 );
20534
20535 app.handle_action(Action::NextPane); assert_eq!(app.s3_state.selected_row, 2, "Must select first child");
20539
20540 let visible = app.s3_state.bucket_visible_rows.get();
20542 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
20543 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
20544 assert!(
20545 in_view,
20546 "First child row {} must be visible in [{}, {})",
20547 app.s3_state.selected_row,
20548 app.s3_state.bucket_scroll_offset,
20549 app.s3_state.bucket_scroll_offset + visible
20550 );
20551 }
20552
20553 #[test]
20554 fn test_apig_detail_tab_navigation() {
20555 use crate::apig::api::RestApi;
20556 use crate::ui::apig::ApiDetailTab;
20557
20558 let mut app = test_app();
20559 app.current_service = Service::ApiGatewayApis;
20560 app.apig_state.current_api = Some(RestApi {
20561 id: "test123".to_string(),
20562 name: "Test API".to_string(),
20563 description: "Test".to_string(),
20564 created_date: "2024-01-01".to_string(),
20565 api_key_source: "HEADER".to_string(),
20566 endpoint_configuration: "REGIONAL".to_string(),
20567 protocol_type: "REST".to_string(),
20568 disable_execute_api_endpoint: false,
20569 status: "AVAILABLE".to_string(),
20570 });
20571
20572 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20574
20575 app.handle_action(Action::NextDetailTab);
20577 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20578
20579 app.handle_action(Action::PrevDetailTab);
20581 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20582
20583 app.handle_action(Action::NextDetailTab);
20585 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20586
20587 app.handle_action(Action::PrevDetailTab);
20589 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20590
20591 app.handle_action(Action::NextDetailTab);
20593 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20594 }
20595
20596 #[test]
20597 fn test_apig_routes_expand_collapse() {
20598 use crate::apig::api::RestApi;
20599 use crate::apig::route::Route;
20600 use crate::ui::apig::ApiDetailTab;
20601
20602 let mut app = test_app();
20603 app.current_service = Service::ApiGatewayApis;
20604 app.apig_state.current_api = Some(RestApi {
20605 id: "test123".to_string(),
20606 name: "Test API".to_string(),
20607 description: "Test".to_string(),
20608 created_date: "2024-01-01".to_string(),
20609 api_key_source: "HEADER".to_string(),
20610 endpoint_configuration: "REGIONAL".to_string(),
20611 protocol_type: "HTTP".to_string(),
20612 disable_execute_api_endpoint: false,
20613 status: "AVAILABLE".to_string(),
20614 });
20615 app.apig_state.detail_tab = ApiDetailTab::Routes;
20616
20617 let virtual_parent = Route {
20619 route_id: "virtual_/api".to_string(),
20620 route_key: "/api".to_string(),
20621 target: String::new(), authorization_type: String::new(),
20623 api_key_required: false,
20624 display_name: String::new(),
20625 arn: String::new(),
20626 };
20627 let child_route = Route {
20628 route_id: "1".to_string(),
20629 route_key: "/api/users".to_string(),
20630 target: "integration1".to_string(),
20631 authorization_type: "NONE".to_string(),
20632 api_key_required: false,
20633 display_name: String::new(),
20634 arn: String::new(),
20635 };
20636
20637 app.apig_state.routes.items = vec![virtual_parent];
20638 app.apig_state
20639 .route_children
20640 .insert("/api".to_string(), vec![child_route]);
20641
20642 assert!(app.apig_state.expanded_routes.is_empty());
20644
20645 app.apig_state.routes.selected = 0;
20647 app.handle_action(Action::ExpandRow);
20648 assert!(app.apig_state.expanded_routes.contains("/api"));
20649
20650 app.handle_action(Action::CollapseRow);
20652 assert!(!app.apig_state.expanded_routes.contains("/api"));
20653
20654 app.handle_action(Action::ExpandRow);
20656 assert!(app.apig_state.expanded_routes.contains("/api"));
20657 }
20658
20659 #[test]
20660 fn test_apig_routes_navigation() {
20661 use crate::apig::api::RestApi;
20662 use crate::apig::route::Route;
20663 use crate::ui::apig::ApiDetailTab;
20664
20665 let mut app = test_app();
20666 app.mode = Mode::Normal;
20667 app.service_selected = true;
20668 app.current_service = Service::ApiGatewayApis;
20669 app.apig_state.current_api = Some(RestApi {
20670 id: "test123".to_string(),
20671 name: "Test API".to_string(),
20672 description: "Test".to_string(),
20673 created_date: "2024-01-01".to_string(),
20674 api_key_source: "HEADER".to_string(),
20675 endpoint_configuration: "REGIONAL".to_string(),
20676 protocol_type: "HTTP".to_string(),
20677 disable_execute_api_endpoint: false,
20678 status: "AVAILABLE".to_string(),
20679 });
20680 app.apig_state.detail_tab = ApiDetailTab::Routes;
20681 app.apig_state.routes.items = vec![
20682 Route {
20683 route_id: "1".to_string(),
20684 route_key: "/api/users".to_string(),
20685 target: "integration1".to_string(),
20686 authorization_type: "NONE".to_string(),
20687 api_key_required: false,
20688 display_name: String::new(),
20689 arn: String::new(),
20690 },
20691 Route {
20692 route_id: "2".to_string(),
20693 route_key: "/health".to_string(),
20694 target: "integration2".to_string(),
20695 authorization_type: "NONE".to_string(),
20696 api_key_required: false,
20697 display_name: String::new(),
20698 arn: String::new(),
20699 },
20700 ];
20701
20702 assert_eq!(app.apig_state.routes.selected, 0);
20704
20705 app.handle_action(Action::NextItem);
20707 assert_eq!(app.apig_state.routes.selected, 1);
20708
20709 app.handle_action(Action::NextItem);
20711 assert_eq!(app.apig_state.routes.selected, 1);
20712
20713 app.handle_action(Action::PrevItem);
20715 assert_eq!(app.apig_state.routes.selected, 0);
20716
20717 app.handle_action(Action::PrevItem);
20719 assert_eq!(app.apig_state.routes.selected, 0);
20720 }
20721
20722 #[test]
20723 fn test_apig_routes_expand_jumps_to_child() {
20724 use crate::apig::api::RestApi;
20725 use crate::apig::route::Route;
20726 use crate::ui::apig::ApiDetailTab;
20727 use std::collections::HashMap;
20728
20729 let mut app = test_app();
20730 app.mode = Mode::Normal;
20731 app.service_selected = true;
20732 app.current_service = Service::ApiGatewayApis;
20733 app.apig_state.current_api = Some(RestApi {
20734 id: "test123".to_string(),
20735 name: "Test API".to_string(),
20736 description: "Test".to_string(),
20737 created_date: "2024-01-01".to_string(),
20738 api_key_source: "HEADER".to_string(),
20739 endpoint_configuration: "REGIONAL".to_string(),
20740 protocol_type: "HTTP".to_string(),
20741 disable_execute_api_endpoint: false,
20742 status: "AVAILABLE".to_string(),
20743 });
20744 app.apig_state.detail_tab = ApiDetailTab::Routes;
20745
20746 app.apig_state.routes.items = vec![Route {
20748 route_id: "virtual_/api".to_string(),
20749 route_key: "/api".to_string(),
20750 target: String::new(),
20751 authorization_type: String::new(),
20752 api_key_required: false,
20753 display_name: String::new(),
20754 arn: String::new(),
20755 }];
20756
20757 let mut children = HashMap::new();
20758 children.insert(
20759 "/api".to_string(),
20760 vec![Route {
20761 route_id: "1".to_string(),
20762 route_key: "/api/users".to_string(),
20763 target: "integration1".to_string(),
20764 authorization_type: "NONE".to_string(),
20765 api_key_required: false,
20766 display_name: String::new(),
20767 arn: String::new(),
20768 }],
20769 );
20770 app.apig_state.route_children = children;
20771
20772 assert_eq!(app.apig_state.routes.selected, 0);
20774 assert!(!app.apig_state.expanded_routes.contains("/api"));
20775
20776 app.handle_action(Action::ExpandRow);
20778 assert!(app.apig_state.expanded_routes.contains("/api"));
20779 assert_eq!(app.apig_state.routes.selected, 0);
20780
20781 app.handle_action(Action::ExpandRow);
20783 assert_eq!(app.apig_state.routes.selected, 1);
20784 }
20785
20786 #[test]
20787 fn test_apig_filter_only_when_focused() {
20788 use crate::apig::api::RestApi;
20789 use crate::ui::apig::ApiDetailTab;
20790
20791 let mut app = test_app();
20792 app.current_service = Service::ApiGatewayApis;
20793 app.apig_state.current_api = Some(RestApi {
20794 id: "test".to_string(),
20795 name: "Test API".to_string(),
20796 description: "Test".to_string(),
20797 created_date: "2024-01-01".to_string(),
20798 api_key_source: "HEADER".to_string(),
20799 endpoint_configuration: "REGIONAL".to_string(),
20800 protocol_type: "HTTP".to_string(),
20801 disable_execute_api_endpoint: false,
20802 status: "AVAILABLE".to_string(),
20803 });
20804 app.apig_state.detail_tab = ApiDetailTab::Routes;
20805 app.mode = Mode::FilterInput;
20806
20807 app.apig_state.input_focus = InputFocus::Pagination;
20809 app.handle_action(Action::FilterInput('x'));
20810 assert_eq!(app.apig_state.route_filter, "");
20811
20812 app.apig_state.input_focus = InputFocus::Filter;
20814 app.handle_action(Action::FilterInput('x'));
20815 assert_eq!(app.apig_state.route_filter, "x");
20816 }
20817
20818 #[test]
20819 fn test_apig_routes_and_resources_use_same_render_function() {
20820 let source = include_str!("ui/apig.rs");
20822 let render_calls: Vec<_> = source
20823 .match_indices("crate::ui::table::render_tree_table")
20824 .collect();
20825
20826 assert_eq!(
20828 render_calls.len(),
20829 2,
20830 "Both routes and resources must use render_tree_table"
20831 );
20832 }
20833
20834 #[test]
20835 fn test_s3_uses_same_render_function() {
20836 let source = include_str!("ui/s3.rs");
20838 let render_calls: Vec<_> = source
20839 .match_indices("crate::ui::table::render_tree_table")
20840 .collect();
20841
20842 assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
20844 }
20845
20846 #[test]
20847 fn test_search_icon_has_proper_border_spacing() {
20848 use crate::ui::SEARCH_ICON;
20851
20852 assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
20853 assert!(
20854 SEARCH_ICON.ends_with("─"),
20855 "Should end with dash for proper border spacing"
20856 );
20857 assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
20858 }
20859
20860 #[test]
20861 fn test_apig_expand_with_filter() {
20862 use crate::apig::api::RestApi;
20863 use crate::apig::route::Route;
20864 use crate::ui::apig::ApiDetailTab;
20865 use std::collections::HashMap;
20866
20867 let mut app = test_app();
20868 app.current_service = Service::ApiGatewayApis;
20869 app.apig_state.current_api = Some(RestApi {
20870 id: "test123".to_string(),
20871 name: "Test API".to_string(),
20872 description: "Test".to_string(),
20873 created_date: "2024-01-01".to_string(),
20874 api_key_source: "HEADER".to_string(),
20875 endpoint_configuration: "REGIONAL".to_string(),
20876 protocol_type: "HTTP".to_string(),
20877 disable_execute_api_endpoint: false,
20878 status: "AVAILABLE".to_string(),
20879 });
20880 app.apig_state.detail_tab = ApiDetailTab::Routes;
20881
20882 app.apig_state.routes.items = vec![Route {
20884 route_id: "0".to_string(),
20885 route_key: "/api".to_string(),
20886 target: "".to_string(),
20887 authorization_type: "NONE".to_string(),
20888 api_key_required: false,
20889 display_name: "/api".to_string(),
20890 arn: String::new(),
20891 }];
20892
20893 let mut children = HashMap::new();
20894 children.insert(
20895 "/api".to_string(),
20896 vec![Route {
20897 route_id: "1".to_string(),
20898 route_key: "GET".to_string(),
20899 target: "integration1".to_string(),
20900 authorization_type: "NONE".to_string(),
20901 api_key_required: false,
20902 display_name: "GET".to_string(),
20903 arn: String::new(),
20904 }],
20905 );
20906 app.apig_state.route_children = children;
20907
20908 app.apig_state.route_filter = "GET".to_string();
20910
20911 assert_eq!(app.apig_state.routes.selected, 0);
20914 assert!(!app.apig_state.expanded_routes.contains("/api"));
20915
20916 app.handle_action(Action::ExpandRow);
20917
20918 assert!(app.apig_state.expanded_routes.contains("/api"));
20920 }
20921
20922 #[test]
20923 fn test_apig_console_url_routes() {
20924 use crate::apig::api::RestApi;
20925 use crate::apig::route::Route;
20926 use crate::ui::apig::ApiDetailTab;
20927
20928 let mut app = test_app();
20929 app.current_service = Service::ApiGatewayApis;
20930 app.config.region = "us-east-1".to_string();
20931
20932 let url = app.get_console_url();
20934 assert!(url.contains("apigateway/main/apis"));
20935 assert!(url.contains("region=us-east-1"));
20936
20937 app.apig_state.current_api = Some(RestApi {
20939 id: "2todvod3n0".to_string(),
20940 name: "Test API".to_string(),
20941 description: "Test".to_string(),
20942 created_date: "2024-01-01".to_string(),
20943 api_key_source: "HEADER".to_string(),
20944 endpoint_configuration: "REGIONAL".to_string(),
20945 protocol_type: "HTTP".to_string(),
20946 disable_execute_api_endpoint: false,
20947 status: "AVAILABLE".to_string(),
20948 });
20949 app.apig_state.detail_tab = ApiDetailTab::Routes;
20950 app.apig_state.routes.items = vec![Route {
20951 route_id: "eizmisr".to_string(),
20952 route_key: "GET /test".to_string(),
20953 target: "integration1".to_string(),
20954 authorization_type: "NONE".to_string(),
20955 api_key_required: false,
20956 display_name: "GET /test".to_string(),
20957 arn: String::new(),
20958 }];
20959 app.apig_state.routes.selected = 0;
20960
20961 let url = app.get_console_url();
20962 assert!(url.contains("apigateway/main/develop/routes"));
20963 assert!(url.contains("api=2todvod3n0"));
20964 assert!(url.contains("routes=eizmisr"));
20965 assert!(url.contains("region=us-east-1"));
20966 }
20967
20968 #[test]
20969 fn test_apig_console_url_resources() {
20970 use crate::apig::api::RestApi;
20971 use crate::apig::resource::Resource;
20972 use crate::ui::apig::ApiDetailTab;
20973
20974 let mut app = test_app();
20975 app.current_service = Service::ApiGatewayApis;
20976 app.config.region = "us-east-1".to_string();
20977
20978 app.apig_state.current_api = Some(RestApi {
20980 id: "2j9j50ze47".to_string(),
20981 name: "Test API".to_string(),
20982 description: "Test".to_string(),
20983 created_date: "2024-01-01".to_string(),
20984 api_key_source: "HEADER".to_string(),
20985 endpoint_configuration: "REGIONAL".to_string(),
20986 protocol_type: "REST".to_string(),
20987 disable_execute_api_endpoint: false,
20988 status: "AVAILABLE".to_string(),
20989 });
20990 app.apig_state.detail_tab = ApiDetailTab::Routes;
20991 app.apig_state.resources.items = vec![Resource {
20992 id: "abc123".to_string(),
20993 path: "/test".to_string(),
20994 parent_id: None,
20995 methods: vec![],
20996 display_name: "/test".to_string(),
20997 arn: String::new(),
20998 }];
20999 app.apig_state.resources.selected = 0;
21000
21001 let url = app.get_console_url();
21002 assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
21003 assert!(url.contains("api=2j9j50ze47"));
21004 assert!(url.contains("#abc123"));
21005 assert!(url.contains("region=us-east-1"));
21006 }
21007
21008 #[test]
21009 fn test_apig_console_url_routes_parent_vs_leaf() {
21010 use crate::apig::api::RestApi;
21011 use crate::apig::route::Route;
21012 use crate::ui::apig::ApiDetailTab;
21013 use std::collections::HashMap;
21014
21015 let mut app = test_app();
21016 app.current_service = Service::ApiGatewayApis;
21017 app.config.region = "us-east-1".to_string();
21018
21019 app.apig_state.current_api = Some(RestApi {
21020 id: "2todvod3n0".to_string(),
21021 name: "Test API".to_string(),
21022 description: "Test".to_string(),
21023 created_date: "2024-01-01".to_string(),
21024 api_key_source: "HEADER".to_string(),
21025 endpoint_configuration: "REGIONAL".to_string(),
21026 protocol_type: "HTTP".to_string(),
21027 disable_execute_api_endpoint: false,
21028 status: "AVAILABLE".to_string(),
21029 });
21030 app.apig_state.detail_tab = ApiDetailTab::Routes;
21031
21032 app.apig_state.routes.items = vec![Route {
21034 route_id: "parent".to_string(),
21035 route_key: "/v1/get/jobs".to_string(),
21036 target: "".to_string(), authorization_type: "NONE".to_string(),
21038 api_key_required: false,
21039 display_name: "/v1/get/jobs".to_string(),
21040 arn: String::new(),
21041 }];
21042
21043 let mut children = HashMap::new();
21045 children.insert(
21046 "/v1/get/jobs".to_string(),
21047 vec![Route {
21048 route_id: "1iz9vtl".to_string(),
21049 route_key: "GET".to_string(),
21050 target: "integration1".to_string(), authorization_type: "NONE".to_string(),
21052 api_key_required: false,
21053 display_name: "GET".to_string(),
21054 arn: String::new(),
21055 }],
21056 );
21057 app.apig_state.route_children = children;
21058
21059 app.apig_state.routes.selected = 0;
21061 let url = app.get_console_url();
21062 assert!(url.contains("apigateway/main/develop/routes"));
21063 assert!(url.contains("api=2todvod3n0"));
21064 assert!(
21065 !url.contains("routes="),
21066 "Parent node should not include routes parameter"
21067 );
21068
21069 app.apig_state
21071 .expanded_routes
21072 .insert("/v1/get/jobs".to_string());
21073 app.apig_state.routes.selected = 1;
21074 let url = app.get_console_url();
21075 assert!(
21076 url.contains("routes=1iz9vtl"),
21077 "Leaf node should include routes parameter: {}",
21078 url
21079 );
21080 }
21081
21082 #[test]
21083 fn test_apig_preferences_context() {
21084 use crate::apig::api::RestApi;
21085 use crate::ui::apig::ApiDetailTab;
21086
21087 let mut app = test_app();
21088 app.current_service = Service::ApiGatewayApis;
21089
21090 assert!(
21092 app.apig_state.current_api.is_none(),
21093 "Should be in list view"
21094 );
21095
21096 app.apig_state.current_api = Some(RestApi {
21098 id: "test123".to_string(),
21099 name: "Test API".to_string(),
21100 description: "Test".to_string(),
21101 created_date: "2024-01-01".to_string(),
21102 api_key_source: "HEADER".to_string(),
21103 endpoint_configuration: "REGIONAL".to_string(),
21104 protocol_type: "HTTP".to_string(),
21105 disable_execute_api_endpoint: false,
21106 status: "AVAILABLE".to_string(),
21107 });
21108 app.apig_state.detail_tab = ApiDetailTab::Routes;
21109
21110 assert!(
21111 app.apig_state.current_api.is_some(),
21112 "Should be in detail view"
21113 );
21114 }
21116
21117 #[test]
21118 fn test_apig_route_columns() {
21119 use crate::apig::route::Column as RouteColumn;
21120
21121 let cols = RouteColumn::all();
21123 assert_eq!(cols.len(), 5);
21124
21125 assert_eq!(RouteColumn::RouteKey.id(), "route_key");
21127 assert_eq!(RouteColumn::RouteId.id(), "route_id");
21128 assert_eq!(RouteColumn::Arn.id(), "arn");
21129 assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
21130 assert_eq!(RouteColumn::Target.id(), "target");
21131
21132 assert_eq!(
21134 RouteColumn::from_id("route_key"),
21135 Some(RouteColumn::RouteKey)
21136 );
21137 assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
21138 assert_eq!(RouteColumn::from_id("invalid"), None);
21139 }
21140
21141 #[test]
21142 fn test_apig_yank_copies_route_arn() {
21143 use crate::apig::api::RestApi;
21144 use crate::ui::apig::ApiDetailTab;
21145
21146 let mut app = test_app();
21147 app.current_service = Service::ApiGatewayApis;
21148 app.apig_state.current_api = Some(RestApi {
21149 id: "test123".to_string(),
21150 name: "Test API".to_string(),
21151 description: "Test".to_string(),
21152 created_date: "2024-01-01".to_string(),
21153 api_key_source: "HEADER".to_string(),
21154 endpoint_configuration: "REGIONAL".to_string(),
21155 protocol_type: "HTTP".to_string(),
21156 disable_execute_api_endpoint: false,
21157 status: "AVAILABLE".to_string(),
21158 });
21159 app.apig_state.detail_tab = ApiDetailTab::Routes;
21160
21161 app.apig_state.routes.items = vec![
21163 Route {
21164 route_id: "route1".to_string(),
21165 route_key: "GET /users".to_string(),
21166 target: "integrations/abc".to_string(),
21167 authorization_type: "NONE".to_string(),
21168 api_key_required: false,
21169 display_name: "GET /users".to_string(),
21170 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
21171 },
21172 Route {
21173 route_id: "route2".to_string(),
21174 route_key: "POST /users".to_string(),
21175 target: "integrations/def".to_string(),
21176 authorization_type: "AWS_IAM".to_string(),
21177 api_key_required: true,
21178 display_name: "POST /users".to_string(),
21179 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
21180 },
21181 ];
21182
21183 app.apig_state.routes.selected = 0;
21185
21186 assert_eq!(
21188 app.apig_state.routes.items[0].arn,
21189 "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
21190 );
21191 }
21192
21193 #[test]
21194 fn test_apig_yank_ignores_empty_arn() {
21195 use crate::apig::api::RestApi;
21196 use crate::ui::apig::ApiDetailTab;
21197
21198 let mut app = test_app();
21199 app.current_service = Service::ApiGatewayApis;
21200 app.apig_state.current_api = Some(RestApi {
21201 id: "test123".to_string(),
21202 name: "Test API".to_string(),
21203 description: "Test".to_string(),
21204 created_date: "2024-01-01".to_string(),
21205 api_key_source: "HEADER".to_string(),
21206 endpoint_configuration: "REGIONAL".to_string(),
21207 protocol_type: "HTTP".to_string(),
21208 disable_execute_api_endpoint: false,
21209 status: "AVAILABLE".to_string(),
21210 });
21211 app.apig_state.detail_tab = ApiDetailTab::Routes;
21212
21213 app.apig_state.routes.items = vec![Route {
21215 route_id: String::new(),
21216 route_key: "/users".to_string(),
21217 target: String::new(), authorization_type: String::new(),
21219 api_key_required: false,
21220 display_name: "/users".to_string(),
21221 arn: String::new(), }];
21223
21224 app.apig_state.routes.selected = 0;
21225
21226 assert!(
21228 app.apig_state.routes.items[0].arn.is_empty(),
21229 "Virtual parent should have empty ARN"
21230 );
21231 }
21232
21233 #[test]
21234 fn test_apig_route_column_toggle() {
21235 use crate::apig::api::RestApi;
21236 use crate::apig::route::Column as RouteColumn;
21237 use crate::keymap::Action;
21238 use crate::ui::apig::ApiDetailTab;
21239
21240 let mut app = test_app();
21241 app.current_service = Service::ApiGatewayApis;
21242 app.mode = Mode::ColumnSelector;
21243 app.apig_state.current_api = Some(RestApi {
21244 id: "test123".to_string(),
21245 name: "Test API".to_string(),
21246 description: "Test".to_string(),
21247 created_date: "2024-01-01".to_string(),
21248 api_key_source: "HEADER".to_string(),
21249 endpoint_configuration: "REGIONAL".to_string(),
21250 protocol_type: "HTTP".to_string(),
21251 disable_execute_api_endpoint: false,
21252 status: "AVAILABLE".to_string(),
21253 });
21254 app.apig_state.detail_tab = ApiDetailTab::Routes;
21255
21256 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21258
21259 app.column_selector_index = 1;
21261 app.handle_action(Action::ToggleColumn);
21262 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21263 assert!(app
21264 .apig_route_visible_column_ids
21265 .contains(&RouteColumn::RouteKey.id()));
21266
21267 app.column_selector_index = 3;
21269 app.handle_action(Action::ToggleColumn);
21270
21271 assert_eq!(app.apig_route_visible_column_ids.len(), 4);
21273 assert!(!app
21274 .apig_route_visible_column_ids
21275 .contains(&RouteColumn::Arn.id()));
21276
21277 app.handle_action(Action::ToggleColumn);
21279 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21280 assert!(app
21281 .apig_route_visible_column_ids
21282 .contains(&RouteColumn::Arn.id()));
21283 }
21284
21285 #[test]
21286 fn test_apig_resource_column_toggle() {
21287 use crate::apig::api::RestApi;
21288 use crate::apig::resource::Column as ResourceColumn;
21289 use crate::keymap::Action;
21290 use crate::ui::apig::ApiDetailTab;
21291
21292 let mut app = test_app();
21293 app.current_service = Service::ApiGatewayApis;
21294 app.mode = Mode::ColumnSelector;
21295 app.apig_state.current_api = Some(RestApi {
21296 id: "test123".to_string(),
21297 name: "Test API".to_string(),
21298 description: "Test".to_string(),
21299 created_date: "2024-01-01".to_string(),
21300 api_key_source: "HEADER".to_string(),
21301 endpoint_configuration: "REGIONAL".to_string(),
21302 protocol_type: "REST".to_string(), disable_execute_api_endpoint: false,
21304 status: "AVAILABLE".to_string(),
21305 });
21306 app.apig_state.detail_tab = ApiDetailTab::Routes; assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21310
21311 app.column_selector_index = 1;
21313 app.handle_action(Action::ToggleColumn);
21314 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21315 assert!(app
21316 .apig_resource_visible_column_ids
21317 .contains(&ResourceColumn::Path.id()));
21318
21319 app.column_selector_index = 3;
21321 app.handle_action(Action::ToggleColumn);
21322
21323 assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
21325 assert!(!app
21326 .apig_resource_visible_column_ids
21327 .contains(&ResourceColumn::Arn.id()));
21328
21329 app.handle_action(Action::ToggleColumn);
21331 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21332 assert!(app
21333 .apig_resource_visible_column_ids
21334 .contains(&ResourceColumn::Arn.id()));
21335 }
21336
21337 #[test]
21338 fn test_cloudtrail_filter_input() {
21339 let mut app = App::new_without_client("default".to_string(), None);
21340 app.service_selected = true;
21341 app.current_service = Service::CloudTrailEvents;
21342
21343 app.handle_action(Action::StartFilter);
21344 assert_eq!(app.mode, Mode::FilterInput);
21345
21346 app.cloudtrail_state.table.filter = "test".to_string();
21347
21348 assert_eq!(app.cloudtrail_state.table.filter, "test");
21349 }
21350
21351 #[test]
21352 fn test_cloudtrail_row_expansion() {
21353 let mut app = App::new_without_client("default".to_string(), None);
21354 app.service_selected = true;
21355 app.current_service = Service::CloudTrailEvents;
21356 app.cloudtrail_state.table.items = vec![CloudTrailEvent {
21357 event_name: "Event1".to_string(),
21358 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21359 username: "user1".to_string(),
21360 event_source: "s3.amazonaws.com".to_string(),
21361 resource_type: "Bucket".to_string(),
21362 resource_name: "my-bucket".to_string(),
21363 read_only: "false".to_string(),
21364 aws_region: "us-east-1".to_string(),
21365 event_id: "abc123".to_string(),
21366 access_key_id: "AKIA...".to_string(),
21367 source_ip_address: "1.2.3.4".to_string(),
21368 error_code: "".to_string(),
21369 request_id: "req-123".to_string(),
21370 event_type: "AwsApiCall".to_string(),
21371 cloud_trail_event_json: "{}".to_string(),
21372 }];
21373
21374 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21375
21376 app.expand_row();
21377 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
21378
21379 app.collapse_row();
21380 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21381 }
21382
21383 #[test]
21384 fn test_cloudtrail_service_initialization() {
21385 let app = App::new_without_client("default".to_string(), None);
21386 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
21387 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21388 }
21389
21390 #[test]
21391 fn test_cloudtrail_service_name() {
21392 assert_eq!(
21393 Service::CloudTrailEvents.name(),
21394 "CloudTrail › Event History"
21395 );
21396 }
21397
21398 #[test]
21399 fn test_cloudtrail_in_service_picker() {
21400 let app = App::new_without_client("default".to_string(), None);
21401 assert!(app
21402 .service_picker
21403 .services
21404 .contains(&"CloudTrail › Event History"));
21405 }
21406
21407 #[test]
21408 fn test_cloudtrail_service_selection() {
21409 let mut app = App::new_without_client("default".to_string(), None);
21410 app.current_service = Service::CloudTrailEvents;
21411 app.service_selected = true;
21412
21413 assert_eq!(app.current_service, Service::CloudTrailEvents);
21414 assert!(app.service_selected);
21415 }
21416
21417 #[test]
21418 fn test_cloudtrail_filter_resets_selection() {
21419 let mut app = App::new_without_client("default".to_string(), None);
21420 app.service_selected = true;
21421 app.current_service = Service::CloudTrailEvents;
21422 app.cloudtrail_state.table.selected = 5;
21423 app.cloudtrail_state.table.expanded_item = Some(3);
21424
21425 app.handle_action(Action::StartFilter);
21426 app.apply_filter_operation(|_| {});
21427
21428 assert_eq!(app.cloudtrail_state.table.selected, 0);
21429 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21430 }
21431
21432 #[test]
21433 fn test_cloudtrail_column_toggle() {
21434 let mut app = App::new_without_client("default".to_string(), None);
21435 app.service_selected = true;
21436 app.current_service = Service::CloudTrailEvents;
21437 app.mode = Mode::ColumnSelector;
21438
21439 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21441
21442 app.column_selector_index = 7;
21444 app.handle_action(Action::ToggleColumn);
21445
21446 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
21448 }
21449
21450 #[test]
21451 fn test_cloudtrail_tab_cycles_filter_focus() {
21452 let mut app = App::new_without_client("default".to_string(), None);
21453 app.service_selected = true;
21454 app.current_service = Service::CloudTrailEvents;
21455 app.mode = Mode::FilterInput;
21456 app.cloudtrail_state.input_focus = InputFocus::Filter;
21457
21458 app.handle_action(Action::NextFilterFocus);
21460 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21461
21462 app.handle_action(Action::NextFilterFocus);
21464 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21465 }
21466
21467 #[test]
21468 fn test_cloudtrail_shift_tab_cycles_filter_focus() {
21469 let mut app = App::new_without_client("default".to_string(), None);
21470 app.service_selected = true;
21471 app.current_service = Service::CloudTrailEvents;
21472 app.mode = Mode::FilterInput;
21473 app.cloudtrail_state.input_focus = InputFocus::Filter;
21474
21475 app.handle_action(Action::PrevFilterFocus);
21477 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21478
21479 app.handle_action(Action::PrevFilterFocus);
21481 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21482 }
21483
21484 #[test]
21485 fn test_cloudtrail_detail_view_tab_cycles_focus() {
21486 let mut app = App::new_without_client("default".to_string(), None);
21487 app.service_selected = true;
21488 app.current_service = Service::CloudTrailEvents;
21489 app.mode = Mode::Normal;
21490 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21491 event_name: "PutObject".to_string(),
21492 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21493 username: "user".to_string(),
21494 event_source: "s3.amazonaws.com".to_string(),
21495 resource_type: "AWS::S3::Bucket".to_string(),
21496 resource_name: "my-bucket".to_string(),
21497 read_only: "false".to_string(),
21498 aws_region: "us-east-1".to_string(),
21499 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21500 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21501 source_ip_address: "192.0.2.1".to_string(),
21502 error_code: "".to_string(),
21503 request_id: "req-123".to_string(),
21504 event_type: "AwsApiCall".to_string(),
21505 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21506 });
21507
21508 assert_eq!(
21510 app.cloudtrail_state.detail_focus,
21511 CloudTrailDetailFocus::Resources
21512 );
21513
21514 app.handle_action(Action::NextDetailTab);
21516 assert_eq!(
21517 app.cloudtrail_state.detail_focus,
21518 CloudTrailDetailFocus::EventRecord
21519 );
21520
21521 app.handle_action(Action::NextDetailTab);
21523 assert_eq!(
21524 app.cloudtrail_state.detail_focus,
21525 CloudTrailDetailFocus::Resources
21526 );
21527 }
21528
21529 #[test]
21530 fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
21531 let mut app = App::new_without_client("default".to_string(), None);
21532 app.service_selected = true;
21533 app.current_service = Service::CloudTrailEvents;
21534 app.mode = Mode::Normal;
21535 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21536 event_name: "PutObject".to_string(),
21537 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21538 username: "user".to_string(),
21539 event_source: "s3.amazonaws.com".to_string(),
21540 resource_type: "AWS::S3::Bucket".to_string(),
21541 resource_name: "my-bucket".to_string(),
21542 read_only: "false".to_string(),
21543 aws_region: "us-east-1".to_string(),
21544 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21545 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21546 source_ip_address: "192.0.2.1".to_string(),
21547 error_code: "".to_string(),
21548 request_id: "req-123".to_string(),
21549 event_type: "AwsApiCall".to_string(),
21550 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21551 });
21552
21553 assert_eq!(
21555 app.cloudtrail_state.detail_focus,
21556 CloudTrailDetailFocus::Resources
21557 );
21558
21559 app.handle_action(Action::PrevDetailTab);
21561 assert_eq!(
21562 app.cloudtrail_state.detail_focus,
21563 CloudTrailDetailFocus::EventRecord
21564 );
21565
21566 app.handle_action(Action::PrevDetailTab);
21568 assert_eq!(
21569 app.cloudtrail_state.detail_focus,
21570 CloudTrailDetailFocus::Resources
21571 );
21572 }
21573
21574 #[test]
21575 fn test_cloudtrail_json_scroll_with_arrow_keys() {
21576 let mut app = App::new_without_client("default".to_string(), None);
21577 app.service_selected = true;
21578 app.current_service = Service::CloudTrailEvents;
21579 app.mode = Mode::Normal;
21580 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21581 event_name: "PutObject".to_string(),
21582 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21583 username: "user".to_string(),
21584 event_source: "s3.amazonaws.com".to_string(),
21585 resource_type: "AWS::S3::Bucket".to_string(),
21586 resource_name: "my-bucket".to_string(),
21587 read_only: "false".to_string(),
21588 aws_region: "us-east-1".to_string(),
21589 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21590 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21591 source_ip_address: "192.0.2.1".to_string(),
21592 error_code: "".to_string(),
21593 request_id: "req-123".to_string(),
21594 event_type: "AwsApiCall".to_string(),
21595 cloud_trail_event_json: (0..50)
21596 .map(|i| format!("line {}", i))
21597 .collect::<Vec<_>>()
21598 .join("\n"),
21599 });
21600 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21601 app.cloudtrail_state.event_json_scroll = 0;
21602
21603 app.handle_action(Action::NextItem);
21605 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21606
21607 app.handle_action(Action::NextItem);
21608 assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
21609
21610 app.handle_action(Action::PrevItem);
21612 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21613
21614 app.handle_action(Action::PrevItem);
21615 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21616
21617 app.handle_action(Action::PrevItem);
21619 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21620 }
21621
21622 #[test]
21623 fn test_cloudtrail_tab_works_with_no_resources() {
21624 let mut app = App::new_without_client("default".to_string(), None);
21625 app.service_selected = true;
21626 app.current_service = Service::CloudTrailEvents;
21627 app.mode = Mode::Normal;
21628 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21629 event_name: "PutObject".to_string(),
21630 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21631 username: "user".to_string(),
21632 event_source: "s3.amazonaws.com".to_string(),
21633 resource_type: "".to_string(), resource_name: "".to_string(), read_only: "false".to_string(),
21636 aws_region: "us-east-1".to_string(),
21637 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21638 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21639 source_ip_address: "192.0.2.1".to_string(),
21640 error_code: "".to_string(),
21641 request_id: "req-123".to_string(),
21642 event_type: "AwsApiCall".to_string(),
21643 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21644 });
21645 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21646
21647 app.handle_action(Action::NextDetailTab);
21649 assert_eq!(
21650 app.cloudtrail_state.detail_focus,
21651 CloudTrailDetailFocus::Resources
21652 );
21653
21654 app.handle_action(Action::NextDetailTab);
21656 assert_eq!(
21657 app.cloudtrail_state.detail_focus,
21658 CloudTrailDetailFocus::EventRecord
21659 );
21660 }
21661
21662 #[test]
21663 fn test_cloudtrail_resources_expand_collapse() {
21664 let mut app = App::new_without_client("default".to_string(), None);
21665 app.service_selected = true;
21666 app.current_service = Service::CloudTrailEvents;
21667 app.mode = Mode::Normal;
21668 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21669 event_name: "PutObject".to_string(),
21670 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21671 username: "user".to_string(),
21672 event_source: "s3.amazonaws.com".to_string(),
21673 resource_type: "AWS::S3::Bucket".to_string(),
21674 resource_name: "my-bucket".to_string(),
21675 read_only: "false".to_string(),
21676 aws_region: "us-east-1".to_string(),
21677 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21678 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21679 source_ip_address: "192.0.2.1".to_string(),
21680 error_code: "".to_string(),
21681 request_id: "req-123".to_string(),
21682 event_type: "AwsApiCall".to_string(),
21683 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21684 });
21685 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21686 app.cloudtrail_state.resources_expanded_index = None;
21687
21688 app.handle_action(Action::ExpandRow);
21690 assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
21691
21692 app.handle_action(Action::CollapseRow);
21694 assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
21695 }
21696
21697 #[test]
21698 fn test_cloudtrail_event_json_no_column_selector() {
21699 let mut app = App::new_without_client("default".to_string(), None);
21700 app.service_selected = true;
21701 app.current_service = Service::CloudTrailEvents;
21702 app.mode = Mode::Normal;
21703 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21704 event_name: "PutObject".to_string(),
21705 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21706 username: "user".to_string(),
21707 event_source: "s3.amazonaws.com".to_string(),
21708 resource_type: "AWS::S3::Bucket".to_string(),
21709 resource_name: "my-bucket".to_string(),
21710 read_only: "false".to_string(),
21711 aws_region: "us-east-1".to_string(),
21712 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21713 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21714 source_ip_address: "192.0.2.1".to_string(),
21715 error_code: "".to_string(),
21716 request_id: "req-123".to_string(),
21717 event_type: "AwsApiCall".to_string(),
21718 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21719 });
21720 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21721
21722 app.handle_action(Action::OpenColumnSelector);
21724 assert_eq!(app.mode, Mode::Normal);
21725
21726 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21728 app.handle_action(Action::OpenColumnSelector);
21729 assert_eq!(app.mode, Mode::ColumnSelector);
21730 }
21731
21732 #[test]
21733 fn test_cloudtrail_resources_preferences_show_only_3_columns() {
21734 let mut app = App::new_without_client("default".to_string(), None);
21735 app.service_selected = true;
21736 app.current_service = Service::CloudTrailEvents;
21737 app.mode = Mode::ColumnSelector;
21738 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21739 event_name: "PutObject".to_string(),
21740 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21741 username: "user".to_string(),
21742 event_source: "s3.amazonaws.com".to_string(),
21743 resource_type: "AWS::S3::Bucket".to_string(),
21744 resource_name: "my-bucket".to_string(),
21745 read_only: "false".to_string(),
21746 aws_region: "us-east-1".to_string(),
21747 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21748 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21749 source_ip_address: "192.0.2.1".to_string(),
21750 error_code: "".to_string(),
21751 request_id: "req-123".to_string(),
21752 event_type: "AwsApiCall".to_string(),
21753 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21754 });
21755 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21756
21757 assert_eq!(app.get_column_count(), 3);
21759 assert_eq!(app.get_column_selector_max(), 3);
21760
21761 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
21763
21764 app.column_selector_index = 1;
21766 app.handle_action(Action::ToggleColumn);
21767 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21768
21769 app.column_selector_index = 2;
21771 app.handle_action(Action::ToggleColumn);
21772 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21773
21774 app.column_selector_index = 3;
21776 app.handle_action(Action::ToggleColumn);
21777 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21778
21779 app.column_selector_index = 1;
21781 app.handle_action(Action::ToggleColumn);
21782 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21783 }
21784
21785 #[test]
21786 fn test_cloudtrail_resources_preferences_tab_cycles() {
21787 let mut app = App::new_without_client("default".to_string(), None);
21788 app.service_selected = true;
21789 app.current_service = Service::CloudTrailEvents;
21790 app.mode = Mode::ColumnSelector;
21791 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21792 event_name: "PutObject".to_string(),
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: "AWS::S3::Bucket".to_string(),
21797 resource_name: "my-bucket".to_string(),
21798 read_only: "false".to_string(),
21799 aws_region: "us-east-1".to_string(),
21800 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21801 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21802 source_ip_address: "192.0.2.1".to_string(),
21803 error_code: "".to_string(),
21804 request_id: "req-123".to_string(),
21805 event_type: "AwsApiCall".to_string(),
21806 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21807 });
21808 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21809 app.column_selector_index = 1;
21810
21811 app.handle_action(Action::NextPreferences);
21813 assert_eq!(app.column_selector_index, 0);
21814
21815 app.column_selector_index = 1;
21817 app.handle_action(Action::PrevPreferences);
21818 assert_eq!(app.column_selector_index, 0);
21819 }
21820
21821 #[test]
21822 fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
21823 let visible_cols_3 = 3;
21831 let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
21832 assert_eq!(height_3, 7);
21833
21834 let visible_cols_2 = 2;
21836 let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
21837 assert_eq!(height_2, 6);
21838
21839 let visible_cols_1 = 1;
21841 let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
21842 assert_eq!(height_1, 5);
21843 }
21844
21845 #[test]
21846 fn test_cloudtrail_default_focus_is_resources() {
21847 let app = App::new_without_client("default".to_string(), None);
21848 assert_eq!(
21849 app.cloudtrail_state.detail_focus,
21850 CloudTrailDetailFocus::Resources
21851 );
21852 }
21853
21854 fn setup_cloudtrail_pagination_test() -> App {
21855 let mut app = App::new_without_client("default".to_string(), None);
21856 app.service_selected = true;
21857 app.current_service = Service::CloudTrailEvents;
21858 app.mode = Mode::FilterInput;
21859 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21860 app.cloudtrail_state.table.page_size = PageSize::Ten;
21861 app.cloudtrail_state.table.items = (0..25)
21862 .map(|i| CloudTrailEvent {
21863 event_name: format!("Event{}", i),
21864 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21865 username: "user".to_string(),
21866 event_source: "s3.amazonaws.com".to_string(),
21867 resource_type: "Bucket".to_string(),
21868 resource_name: "bucket".to_string(),
21869 read_only: "false".to_string(),
21870 aws_region: "us-east-1".to_string(),
21871 event_id: "id".to_string(),
21872 access_key_id: "key".to_string(),
21873 source_ip_address: "1.2.3.4".to_string(),
21874 error_code: "".to_string(),
21875 request_id: "req".to_string(),
21876 event_type: "AwsApiCall".to_string(),
21877 cloud_trail_event_json: "{}".to_string(),
21878 })
21879 .collect();
21880 app
21881 }
21882
21883 #[test]
21884 fn test_cloudtrail_pagination_navigation() {
21885 let mut app = setup_cloudtrail_pagination_test();
21887 assert_eq!(app.cloudtrail_state.table.selected, 0);
21888 app.handle_action(Action::NextItem);
21889 assert_eq!(app.cloudtrail_state.table.selected, 10);
21890 app.handle_action(Action::NextItem);
21891 assert_eq!(app.cloudtrail_state.table.selected, 20);
21892 app.handle_action(Action::NextItem);
21893 assert_eq!(app.cloudtrail_state.table.selected, 20);
21894
21895 app.handle_action(Action::PrevItem);
21897 assert_eq!(app.cloudtrail_state.table.selected, 10);
21898 app.handle_action(Action::PrevItem);
21899 assert_eq!(app.cloudtrail_state.table.selected, 0);
21900 app.handle_action(Action::PrevItem);
21901 assert_eq!(app.cloudtrail_state.table.selected, 0);
21902 }
21903
21904 #[test]
21905 fn test_cloudtrail_pagination_navigation_right_arrow() {
21906 let mut app = setup_cloudtrail_pagination_test();
21907 app.current_service = Service::CloudTrailEvents;
21908 app.mode = Mode::FilterInput;
21909 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21910 app.cloudtrail_state.table.page_size = PageSize::Ten;
21911
21912 app.cloudtrail_state.table.items = (0..25)
21914 .map(|i| CloudTrailEvent {
21915 event_name: format!("Event{}", i),
21916 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21917 username: "user".to_string(),
21918 event_source: "s3.amazonaws.com".to_string(),
21919 resource_type: "Bucket".to_string(),
21920 resource_name: "bucket".to_string(),
21921 read_only: "false".to_string(),
21922 aws_region: "us-east-1".to_string(),
21923 event_id: "id".to_string(),
21924 access_key_id: "key".to_string(),
21925 source_ip_address: "1.2.3.4".to_string(),
21926 error_code: "".to_string(),
21927 request_id: "req".to_string(),
21928 event_type: "AwsApiCall".to_string(),
21929 cloud_trail_event_json: "{}".to_string(),
21930 })
21931 .collect();
21932
21933 assert_eq!(app.cloudtrail_state.table.selected, 0);
21935
21936 app.handle_action(Action::NextItem);
21938 assert_eq!(app.cloudtrail_state.table.selected, 10);
21939
21940 app.handle_action(Action::NextItem);
21942 assert_eq!(app.cloudtrail_state.table.selected, 20);
21943
21944 app.handle_action(Action::NextItem);
21946 assert_eq!(app.cloudtrail_state.table.selected, 20);
21947 }
21948
21949 #[test]
21950 fn test_cloudtrail_pagination_navigation_left_arrow() {
21951 let mut app = App::new_without_client("default".to_string(), None);
21952 app.service_selected = true;
21953 app.current_service = Service::CloudTrailEvents;
21954 app.mode = Mode::FilterInput;
21955 app.cloudtrail_state.input_focus = InputFocus::Pagination;
21956 app.cloudtrail_state.table.page_size = PageSize::Ten;
21957
21958 app.cloudtrail_state.table.items = (0..25)
21960 .map(|i| CloudTrailEvent {
21961 event_name: format!("Event{}", i),
21962 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21963 username: "user".to_string(),
21964 event_source: "s3.amazonaws.com".to_string(),
21965 resource_type: "Bucket".to_string(),
21966 resource_name: "bucket".to_string(),
21967 read_only: "false".to_string(),
21968 aws_region: "us-east-1".to_string(),
21969 event_id: "id".to_string(),
21970 access_key_id: "key".to_string(),
21971 source_ip_address: "1.2.3.4".to_string(),
21972 error_code: "".to_string(),
21973 request_id: "req".to_string(),
21974 event_type: "AwsApiCall".to_string(),
21975 cloud_trail_event_json: "{}".to_string(),
21976 })
21977 .collect();
21978
21979 app.cloudtrail_state.table.selected = 20;
21981
21982 app.handle_action(Action::PrevItem);
21984 assert_eq!(app.cloudtrail_state.table.selected, 10);
21985
21986 app.handle_action(Action::PrevItem);
21988 assert_eq!(app.cloudtrail_state.table.selected, 0);
21989
21990 app.handle_action(Action::PrevItem);
21992 assert_eq!(app.cloudtrail_state.table.selected, 0);
21993 }
21994
21995 #[test]
21996 fn test_cloudtrail_arrow_navigation() {
21997 let mut app = App::new_without_client("default".to_string(), None);
21998 app.service_selected = true;
21999 app.current_service = Service::CloudTrailEvents;
22000 app.mode = Mode::Normal;
22001
22002 app.cloudtrail_state.table.items = (0..5)
22004 .map(|i| CloudTrailEvent {
22005 event_name: format!("Event{}", i),
22006 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22007 username: "user".to_string(),
22008 event_source: "s3.amazonaws.com".to_string(),
22009 resource_type: "Bucket".to_string(),
22010 resource_name: "bucket".to_string(),
22011 read_only: "false".to_string(),
22012 aws_region: "us-east-1".to_string(),
22013 event_id: "id".to_string(),
22014 access_key_id: "key".to_string(),
22015 source_ip_address: "1.2.3.4".to_string(),
22016 error_code: "".to_string(),
22017 request_id: "req".to_string(),
22018 event_type: "AwsApiCall".to_string(),
22019 cloud_trail_event_json: "{}".to_string(),
22020 })
22021 .collect();
22022
22023 app.handle_action(Action::NextItem);
22025 assert_eq!(app.cloudtrail_state.table.selected, 1);
22026
22027 app.handle_action(Action::NextItem);
22028 assert_eq!(app.cloudtrail_state.table.selected, 2);
22029
22030 app.handle_action(Action::PrevItem);
22032 assert_eq!(app.cloudtrail_state.table.selected, 1);
22033 }
22034
22035 #[test]
22036 fn test_cloudtrail_page_down_navigation() {
22037 let mut app = App::new_without_client("default".to_string(), None);
22038 app.service_selected = true;
22039 app.current_service = Service::CloudTrailEvents;
22040 app.mode = Mode::Normal;
22041 app.cloudtrail_state.table.page_size = PageSize::Ten;
22042
22043 app.cloudtrail_state.table.items = (0..25)
22045 .map(|i| CloudTrailEvent {
22046 event_name: format!("Event{}", i),
22047 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22048 username: "user".to_string(),
22049 event_source: "s3.amazonaws.com".to_string(),
22050 resource_type: "Bucket".to_string(),
22051 resource_name: "bucket".to_string(),
22052 read_only: "false".to_string(),
22053 aws_region: "us-east-1".to_string(),
22054 event_id: "id".to_string(),
22055 access_key_id: "key".to_string(),
22056 source_ip_address: "1.2.3.4".to_string(),
22057 error_code: "".to_string(),
22058 request_id: "req".to_string(),
22059 event_type: "AwsApiCall".to_string(),
22060 cloud_trail_event_json: "{}".to_string(),
22061 })
22062 .collect();
22063
22064 app.handle_action(Action::PageDown);
22066 assert_eq!(app.cloudtrail_state.table.selected, 10);
22067
22068 app.handle_action(Action::PageDown);
22069 assert_eq!(app.cloudtrail_state.table.selected, 20);
22070 }
22071
22072 #[test]
22073 fn test_cloudtrail_page_up_navigation() {
22074 let mut app = App::new_without_client("default".to_string(), None);
22075 app.service_selected = true;
22076 app.current_service = Service::CloudTrailEvents;
22077 app.mode = Mode::Normal;
22078 app.cloudtrail_state.table.page_size = PageSize::Ten;
22079
22080 app.cloudtrail_state.table.items = (0..25)
22082 .map(|i| CloudTrailEvent {
22083 event_name: format!("Event{}", i),
22084 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22085 username: "user".to_string(),
22086 event_source: "s3.amazonaws.com".to_string(),
22087 resource_type: "Bucket".to_string(),
22088 resource_name: "bucket".to_string(),
22089 read_only: "false".to_string(),
22090 aws_region: "us-east-1".to_string(),
22091 event_id: "id".to_string(),
22092 access_key_id: "key".to_string(),
22093 source_ip_address: "1.2.3.4".to_string(),
22094 error_code: "".to_string(),
22095 request_id: "req".to_string(),
22096 event_type: "AwsApiCall".to_string(),
22097 cloud_trail_event_json: "{}".to_string(),
22098 })
22099 .collect();
22100
22101 app.cloudtrail_state.table.selected = 20;
22103
22104 app.handle_action(Action::PageUp);
22106 assert_eq!(app.cloudtrail_state.table.selected, 10);
22107
22108 app.handle_action(Action::PageUp);
22109 assert_eq!(app.cloudtrail_state.table.selected, 0);
22110 }
22111
22112 #[test]
22113 fn test_cloudtrail_page_size_change_updates_display() {
22114 let mut app = App::new_without_client("default".to_string(), None);
22115 app.service_selected = true;
22116 app.current_service = Service::CloudTrailEvents;
22117 app.mode = Mode::ColumnSelector;
22118
22119 app.cloudtrail_state.table.items = (0..50)
22121 .map(|i| CloudTrailEvent {
22122 event_name: format!("Event{}", i),
22123 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22124 username: "user".to_string(),
22125 event_source: "s3.amazonaws.com".to_string(),
22126 resource_type: "Bucket".to_string(),
22127 resource_name: "bucket".to_string(),
22128 read_only: "false".to_string(),
22129 aws_region: "us-east-1".to_string(),
22130 event_id: "id".to_string(),
22131 access_key_id: "key".to_string(),
22132 source_ip_address: "1.2.3.4".to_string(),
22133 error_code: "".to_string(),
22134 request_id: "req".to_string(),
22135 event_type: "AwsApiCall".to_string(),
22136 cloud_trail_event_json: "{}".to_string(),
22137 })
22138 .collect();
22139
22140 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
22142
22143 app.column_selector_index = 17;
22145 app.handle_action(Action::ToggleColumn);
22146
22147 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22148 }
22150
22151 #[test]
22152 fn test_cloudtrail_all_columns_toggleable() {
22153 let mut app = App::new_without_client("default".to_string(), None);
22154 app.service_selected = true;
22155 app.current_service = Service::CloudTrailEvents;
22156 app.mode = Mode::ColumnSelector;
22157
22158 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22160
22161 for idx in 1..=14 {
22164 app.column_selector_index = idx;
22165 let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
22166 let initial_count = initial_visible.len();
22167
22168 let col = app.cloudtrail_event_column_ids.get(idx - 1).unwrap();
22170 let is_visible = initial_visible.contains(col);
22171
22172 app.handle_action(Action::ToggleColumn);
22173
22174 if is_visible && initial_count == 1 {
22176 assert_eq!(
22177 app.cloudtrail_event_visible_column_ids, initial_visible,
22178 "Last visible column at index {} should not be toggleable",
22179 idx
22180 );
22181 } else {
22182 assert_ne!(
22184 app.cloudtrail_event_visible_column_ids, initial_visible,
22185 "Column at index {} should be toggleable when not the last one",
22186 idx
22187 );
22188 }
22189 }
22190 }
22191
22192 #[test]
22193 fn test_cloudtrail_readonly_column_toggleable() {
22194 use crate::cloudtrail::events::CloudTrailEventColumn;
22195
22196 let mut app = App::new_without_client("default".to_string(), None);
22197 app.service_selected = true;
22198 app.current_service = Service::CloudTrailEvents;
22199 app.mode = Mode::ColumnSelector;
22200
22201 let readonly_id = CloudTrailEventColumn::ReadOnly.id();
22204
22205 assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
22207
22208 assert!(!app
22210 .cloudtrail_event_visible_column_ids
22211 .contains(&readonly_id));
22212
22213 app.column_selector_index = 7;
22215 app.handle_action(Action::ToggleColumn);
22216
22217 assert!(
22219 app.cloudtrail_event_visible_column_ids
22220 .contains(&readonly_id),
22221 "ReadOnly column should be toggleable at index 7"
22222 );
22223
22224 app.handle_action(Action::ToggleColumn);
22226
22227 assert!(!app
22229 .cloudtrail_event_visible_column_ids
22230 .contains(&readonly_id));
22231 }
22232
22233 #[test]
22234 fn test_cloudtrail_pagination_limits_displayed_items() {
22235 let mut app = App::new_without_client("default".to_string(), None);
22236 app.service_selected = true;
22237 app.current_service = Service::CloudTrailEvents;
22238
22239 app.cloudtrail_state.table.items = (0..50)
22241 .map(|i| CloudTrailEvent {
22242 event_name: format!("Event{}", i),
22243 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22244 username: "user".to_string(),
22245 event_source: "s3.amazonaws.com".to_string(),
22246 resource_type: "Bucket".to_string(),
22247 resource_name: "bucket".to_string(),
22248 read_only: "false".to_string(),
22249 aws_region: "us-east-1".to_string(),
22250 event_id: "id".to_string(),
22251 access_key_id: "key".to_string(),
22252 source_ip_address: "1.2.3.4".to_string(),
22253 error_code: "".to_string(),
22254 request_id: "req".to_string(),
22255 event_type: "AwsApiCall".to_string(),
22256 cloud_trail_event_json: "{}".to_string(),
22257 })
22258 .collect();
22259
22260 app.cloudtrail_state.table.page_size = PageSize::Ten;
22262
22263 use ratatui::backend::TestBackend;
22265 use ratatui::Terminal;
22266
22267 let backend = TestBackend::new(100, 30);
22268 let mut terminal = Terminal::new(backend).unwrap();
22269
22270 terminal
22271 .draw(|frame| {
22272 let area = frame.area();
22273 crate::ui::cloudtrail::render_events(frame, &app, area);
22274 })
22275 .unwrap();
22276
22277 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22280 }
22281
22282 #[test]
22283 fn test_cloudtrail_readonly_column_selectable_in_preferences() {
22284 let mut app = App::new_without_client("default".to_string(), None);
22285 app.service_selected = true;
22286 app.current_service = Service::CloudTrailEvents;
22287 app.mode = Mode::ColumnSelector;
22288
22289 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22291
22292 app.column_selector_index = 7;
22294
22295 let max_index = app.get_column_selector_max();
22297 assert!(
22298 app.column_selector_index <= max_index,
22299 "Read-only column index {} should be <= max {}",
22300 app.column_selector_index,
22301 max_index
22302 );
22303
22304 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22306 let col = CloudTrailEventColumn::from_id(col_id);
22307 assert!(col.is_some(), "Column should exist at index 7");
22308 assert_eq!(
22309 col.unwrap().default_name(),
22310 "Read-only",
22311 "Column at index 7 should be Read-only"
22312 );
22313 }
22314
22315 #[test]
22316 fn test_cloudtrail_navigate_to_readonly_column() {
22317 let mut app = App::new_without_client("default".to_string(), None);
22318 app.service_selected = true;
22319 app.current_service = Service::CloudTrailEvents;
22320 app.mode = Mode::ColumnSelector;
22321 app.column_selector_index = 1; println!(
22325 "cloudtrail_event_column_ids.len() = {}",
22326 app.cloudtrail_event_column_ids.len()
22327 );
22328 assert_eq!(
22329 app.cloudtrail_event_column_ids.len(),
22330 14,
22331 "Should have 14 columns"
22332 );
22333
22334 let column_count = app.get_column_count();
22336 println!("Column count from get_column_count(): {}", column_count);
22337 assert_eq!(column_count, 14, "Column count should be 14");
22338
22339 assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
22341 assert!(app.is_blank_row_index(15), "Index 15 should be blank");
22342
22343 for _ in 0..6 {
22345 app.handle_action(Action::NextItem);
22346 }
22347
22348 assert_eq!(
22349 app.column_selector_index, 7,
22350 "Should navigate to Read-only column at index 7"
22351 );
22352
22353 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22355 let col = CloudTrailEventColumn::from_id(col_id).unwrap();
22356 assert_eq!(col.default_name(), "Read-only");
22357 }
22358
22359 #[test]
22360 fn test_cloudtrail_navigate_beyond_loaded_items() {
22361 let mut app = App::new_without_client("default".to_string(), None);
22362 app.service_selected = true;
22363 app.current_service = Service::CloudTrailEvents;
22364
22365 app.cloudtrail_state.table.items = (0..50)
22367 .map(|i| CloudTrailEvent {
22368 event_name: format!("Event{}", i),
22369 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22370 username: "user".to_string(),
22371 event_source: "s3.amazonaws.com".to_string(),
22372 resource_type: "Bucket".to_string(),
22373 resource_name: "bucket".to_string(),
22374 read_only: "false".to_string(),
22375 aws_region: "us-east-1".to_string(),
22376 event_id: "id".to_string(),
22377 access_key_id: "key".to_string(),
22378 source_ip_address: "1.2.3.4".to_string(),
22379 error_code: "".to_string(),
22380 request_id: "req".to_string(),
22381 event_type: "AwsApiCall".to_string(),
22382 cloud_trail_event_json: "{}".to_string(),
22383 })
22384 .collect();
22385
22386 let initial_selected = app.cloudtrail_state.table.selected;
22389 app.page_input = "10".to_string();
22390 app.go_to_page(10);
22391
22392 assert_eq!(
22394 app.cloudtrail_state.table.selected, initial_selected,
22395 "Should ignore navigation to page 10 when only 1 page is loaded"
22396 );
22397
22398 app.go_to_page(2);
22400 let page_size = app.cloudtrail_state.table.page_size.value();
22401 assert_eq!(
22402 app.cloudtrail_state.table.selected, page_size,
22403 "Should allow navigation to page 2 (loaded + 1)"
22404 );
22405 }
22406
22407 #[test]
22408 fn test_cloudtrail_page_change_resets_expansion() {
22409 let mut app = App::new_without_client("default".to_string(), None);
22410 app.service_selected = true;
22411 app.current_service = Service::CloudTrailEvents;
22412
22413 app.cloudtrail_state.table.items = (0..100)
22415 .map(|i| CloudTrailEvent {
22416 event_name: format!("Event{}", i),
22417 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22418 username: "user".to_string(),
22419 event_source: "s3.amazonaws.com".to_string(),
22420 resource_type: "Bucket".to_string(),
22421 resource_name: "bucket".to_string(),
22422 read_only: "false".to_string(),
22423 aws_region: "us-east-1".to_string(),
22424 event_id: "id".to_string(),
22425 access_key_id: "key".to_string(),
22426 source_ip_address: "1.2.3.4".to_string(),
22427 error_code: "".to_string(),
22428 request_id: "req".to_string(),
22429 event_type: "AwsApiCall".to_string(),
22430 cloud_trail_event_json: "{}".to_string(),
22431 })
22432 .collect();
22433
22434 app.cloudtrail_state.table.expanded_item = Some(5);
22436 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
22437
22438 app.go_to_page(2);
22440
22441 assert_eq!(
22443 app.cloudtrail_state.table.expanded_item, None,
22444 "Page change should reset expanded_item"
22445 );
22446 }
22447
22448 #[test]
22449 fn test_lambda_application_resources_have_columns() {
22450 let app = test_app();
22451
22452 assert_eq!(app.lambda_resource_visible_column_ids.len(), 4);
22454 assert_eq!(app.lambda_resource_column_ids.len(), 4);
22455
22456 assert!(app
22458 .lambda_resource_visible_column_ids
22459 .contains(&"column.lambda.resource.logical_id"));
22460 assert!(app
22461 .lambda_resource_column_ids
22462 .contains(&"column.lambda.resource.logical_id"));
22463 }
22464
22465 #[test]
22466 fn test_lambda_functions_list_right_expands_left_collapses() {
22467 use crate::lambda::Function;
22468 let mut app = test_app();
22469 app.mode = Mode::Normal;
22470 app.current_service = Service::LambdaFunctions;
22471 app.service_selected = true;
22472 app.lambda_state.table.items = vec![Function {
22473 name: "fn1".to_string(),
22474 arn: "arn::fn1".to_string(),
22475 application: None,
22476 description: String::new(),
22477 package_type: String::new(),
22478 runtime: "python3.12".to_string(),
22479 architecture: String::new(),
22480 code_size: 0,
22481 code_sha256: String::new(),
22482 memory_mb: 128,
22483 timeout_seconds: 3,
22484 last_modified: String::new(),
22485 layers: vec![],
22486 }];
22487 app.lambda_state.table.selected = 0;
22488
22489 app.handle_action(Action::NextPane);
22491 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
22492
22493 app.handle_action(Action::NextPane);
22495 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
22496
22497 app.handle_action(Action::CollapseRow);
22499 assert_eq!(app.lambda_state.table.expanded_item, None);
22500 }
22501
22502 #[test]
22503 fn test_lambda_applications_list_right_expands_left_collapses() {
22504 use crate::lambda::Application;
22505 let mut app = test_app();
22506 app.mode = Mode::Normal;
22507 app.current_service = Service::LambdaApplications;
22508 app.service_selected = true;
22509 app.lambda_application_state.table.items = vec![Application {
22510 name: "my-app".to_string(),
22511 arn: String::new(),
22512 description: String::new(),
22513 status: "ACTIVE".to_string(),
22514 last_modified: String::new(),
22515 }];
22516 app.lambda_application_state.table.selected = 0;
22517
22518 app.handle_action(Action::NextPane);
22520 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
22521
22522 app.handle_action(Action::NextPane);
22524 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
22525
22526 app.handle_action(Action::CollapseRow);
22528 assert_eq!(app.lambda_application_state.table.expanded_item, None);
22529 }
22530
22531 #[test]
22532 fn test_lambda_application_enter_on_deployments_tab_does_not_switch_to_overview() {
22533 use crate::lambda::Application;
22534 let mut app = test_app();
22535 app.mode = Mode::Normal;
22536 app.current_service = Service::LambdaApplications;
22537 app.service_selected = true;
22538 app.lambda_application_state.table.items = vec![Application {
22539 name: "my-app".to_string(),
22540 arn: String::new(),
22541 description: String::new(),
22542 status: "ACTIVE".to_string(),
22543 last_modified: String::new(),
22544 }];
22545
22546 app.handle_action(Action::Select);
22548 assert!(app.lambda_application_state.current_application.is_some());
22549
22550 use crate::ui::lambda::ApplicationDetailTab as LambdaApplicationDetailTab;
22552 app.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Deployments;
22553 assert_eq!(
22554 app.lambda_application_state.detail_tab,
22555 LambdaApplicationDetailTab::Deployments
22556 );
22557
22558 app.handle_action(Action::Select);
22560 assert_eq!(
22561 app.lambda_application_state.detail_tab,
22562 LambdaApplicationDetailTab::Deployments
22563 );
22564 }
22565
22566 #[test]
22567 fn test_lambda_applications_pagination_focus_left_right_jump_pages() {
22568 use crate::lambda::Application;
22569 let mut app = test_app();
22570 app.current_service = Service::LambdaApplications;
22571 app.service_selected = true;
22572 app.mode = Mode::FilterInput;
22573 app.lambda_application_state.input_focus = InputFocus::Pagination;
22574
22575 app.lambda_application_state.table.items = (0..100)
22577 .map(|i| Application {
22578 name: format!("app{:03}", i),
22579 arn: String::new(),
22580 description: String::new(),
22581 status: "ACTIVE".to_string(),
22582 last_modified: String::new(),
22583 })
22584 .collect();
22585 app.lambda_application_state.table.selected = 0;
22586
22587 app.handle_action(Action::PageDown);
22589 assert_eq!(
22590 app.lambda_application_state.table.selected, 50,
22591 "Right arrow with pagination focus should jump to page 2"
22592 );
22593
22594 app.handle_action(Action::PageUp);
22596 assert_eq!(
22597 app.lambda_application_state.table.selected, 0,
22598 "Left arrow with pagination focus should jump back to page 1"
22599 );
22600
22601 app.handle_action(Action::NextItem);
22603 assert_eq!(
22604 app.lambda_application_state.table.selected, 0,
22605 "Down arrow must not move table when pagination is focused"
22606 );
22607 app.lambda_application_state.table.selected = 5;
22608 app.handle_action(Action::PrevItem);
22609 assert_eq!(
22610 app.lambda_application_state.table.selected, 5,
22611 "Up arrow must not move table when pagination is focused"
22612 );
22613 }
22614
22615 #[test]
22616 fn test_cloudwatch_alarms_enter_drills_down() {
22617 let mut app = test_app();
22618 app.mode = Mode::Normal;
22619 app.current_service = Service::CloudWatchAlarms;
22620 app.service_selected = true;
22621 app.view_mode = ViewMode::List;
22622
22623 app.alarms_state.table.items = vec![Alarm {
22625 name: "test-alarm".to_string(),
22626 state: "ALARM".to_string(),
22627 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22628 description: "Test alarm".to_string(),
22629 metric_name: "CPUUtilization".to_string(),
22630 namespace: "AWS/EC2".to_string(),
22631 statistic: "Average".to_string(),
22632 period: 300,
22633 comparison_operator: "GreaterThanThreshold".to_string(),
22634 threshold: 80.0,
22635 actions_enabled: true,
22636 state_reason: "Threshold crossed".to_string(),
22637 resource: "".to_string(),
22638 dimensions: "".to_string(),
22639 expression: "".to_string(),
22640 alarm_type: "MetricAlarm".to_string(),
22641 cross_account: "".to_string(),
22642 }];
22643
22644 assert!(app.alarms_state.current_alarm.is_none());
22645 assert_eq!(app.view_mode, ViewMode::List);
22646 assert!(!app.alarms_state.metrics_loading);
22647
22648 app.handle_action(Action::Select);
22650
22651 assert_eq!(
22652 app.alarms_state.current_alarm,
22653 Some("test-alarm".to_string())
22654 );
22655 assert_eq!(app.view_mode, ViewMode::Detail);
22656 assert!(
22657 app.alarms_state.metrics_loading,
22658 "Should trigger metrics loading"
22659 );
22660 }
22661
22662 #[test]
22663 fn test_cloudwatch_alarms_metric_data_renders() {
22664 let mut app = test_app();
22665 app.mode = Mode::Normal;
22666 app.current_service = Service::CloudWatchAlarms;
22667 app.service_selected = true;
22668 app.view_mode = ViewMode::Detail;
22669
22670 app.alarms_state.table.items = vec![Alarm {
22672 name: "test-alarm".to_string(),
22673 state: "ALARM".to_string(),
22674 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22675 description: "Test alarm".to_string(),
22676 metric_name: "CPUUtilization".to_string(),
22677 namespace: "AWS/EC2".to_string(),
22678 statistic: "Average".to_string(),
22679 period: 300,
22680 comparison_operator: "GreaterThanThreshold".to_string(),
22681 threshold: 80.0,
22682 actions_enabled: true,
22683 state_reason: "Threshold crossed".to_string(),
22684 resource: "".to_string(),
22685 dimensions: "".to_string(),
22686 expression: "".to_string(),
22687 alarm_type: "MetricAlarm".to_string(),
22688 cross_account: "".to_string(),
22689 }];
22690
22691 app.alarms_state.current_alarm = Some("test-alarm".to_string());
22692
22693 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0), (3000, 70.0)];
22695 app.alarms_state.metrics_loading = false;
22696
22697 assert!(!app.alarms_state.metric_data.is_empty());
22699 assert_eq!(app.alarms_state.metric_data.len(), 3);
22700 assert!(!app.alarms_state.metrics_loading);
22701 }
22702
22703 #[test]
22704 fn test_cloudwatch_alarms_back_clears_metrics() {
22705 let mut app = test_app();
22706 app.mode = Mode::Normal;
22707 app.current_service = Service::CloudWatchAlarms;
22708 app.service_selected = true;
22709 app.view_mode = ViewMode::Detail;
22710
22711 app.alarms_state.current_alarm = Some("test-alarm".to_string());
22713 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
22714
22715 assert!(!app.alarms_state.metric_data.is_empty());
22716
22717 app.handle_action(Action::GoBack);
22719
22720 assert!(app.alarms_state.current_alarm.is_none());
22722 assert!(app.alarms_state.metric_data.is_empty());
22723 assert_eq!(app.view_mode, ViewMode::List);
22724 }
22725
22726 #[test]
22727 fn test_cloudwatch_alarms_refresh_reloads_metrics() {
22728 let mut app = test_app();
22729 app.mode = Mode::Normal;
22730 app.current_service = Service::CloudWatchAlarms;
22731 app.service_selected = true;
22732 app.view_mode = ViewMode::Detail;
22733
22734 app.alarms_state.current_alarm = Some("test-alarm".to_string());
22736 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
22737 app.alarms_state.metrics_loading = false;
22738
22739 assert!(!app.alarms_state.metric_data.is_empty());
22740 assert!(!app.alarms_state.metrics_loading);
22741
22742 app.handle_action(Action::Refresh);
22744
22745 assert!(!app.alarms_state.metric_data.is_empty());
22747 assert!(app.alarms_state.metrics_loading);
22748 }
22749
22750 #[test]
22751 fn test_cloudwatch_alarms_right_arrow_expands() {
22752 let mut app = test_app();
22753 app.mode = Mode::Normal;
22754 app.current_service = Service::CloudWatchAlarms;
22755 app.service_selected = true;
22756 app.view_mode = ViewMode::List;
22757
22758 app.alarms_state.table.items = vec![Alarm {
22760 name: "test-alarm".to_string(),
22761 state: "ALARM".to_string(),
22762 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22763 description: "Test alarm".to_string(),
22764 metric_name: "CPUUtilization".to_string(),
22765 namespace: "AWS/EC2".to_string(),
22766 statistic: "Average".to_string(),
22767 period: 300,
22768 comparison_operator: "GreaterThanThreshold".to_string(),
22769 threshold: 80.0,
22770 actions_enabled: true,
22771 state_reason: "Threshold crossed".to_string(),
22772 resource: "".to_string(),
22773 dimensions: "".to_string(),
22774 expression: "".to_string(),
22775 alarm_type: "MetricAlarm".to_string(),
22776 cross_account: "".to_string(),
22777 }];
22778
22779 assert!(!app.alarms_state.table.is_expanded());
22780
22781 app.handle_action(Action::NextPane);
22783 assert!(app.alarms_state.table.is_expanded());
22784
22785 app.handle_action(Action::PrevPane);
22787 assert!(!app.alarms_state.table.is_expanded());
22788 }
22789
22790 #[test]
22791 fn test_cloudwatch_alarms_tab_switches_tabs() {
22792 let mut app = test_app();
22793 app.mode = Mode::Normal;
22794 app.current_service = Service::CloudWatchAlarms;
22795 app.service_selected = true;
22796
22797 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
22799
22800 app.handle_action(Action::NextDetailTab);
22802 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
22803
22804 app.handle_action(Action::NextDetailTab);
22806 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
22807
22808 app.handle_action(Action::PrevDetailTab);
22810 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
22811 }
22812
22813 #[test]
22814 fn test_column_toggle_prevents_hiding_last_column() {
22815 let mut app = test_app();
22816 app.mode = Mode::Normal;
22817 app.current_service = Service::CloudWatchAlarms;
22818 app.service_selected = true;
22819
22820 app.cw_alarm_visible_column_ids = vec!["column.cw.alarm.name"];
22822
22823 app.handle_action(Action::OpenColumnSelector);
22825 assert_eq!(app.mode, Mode::ColumnSelector);
22826
22827 app.column_selector_index = 1;
22829
22830 app.handle_action(Action::ToggleColumn);
22832
22833 assert_eq!(app.cw_alarm_visible_column_ids.len(), 1);
22835 assert_eq!(app.cw_alarm_visible_column_ids[0], "column.cw.alarm.name");
22836 }
22837}