1use crate::apig::api::Column as ApigColumn;
2use crate::apig::resource::Column as ResourceColumn;
3use crate::apig::route::Column as RouteColumn;
4pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
5use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
6use crate::cloudtrail::{CloudTrailEvent, CloudTrailEventColumn, EventResourceColumn};
7use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
8pub use crate::cw::insights::InsightsFocus;
9use crate::cw::insights::InsightsState;
10pub use crate::cw::{Alarm, AlarmColumn};
11pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
12use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
13use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
14use crate::efs::fs::{Column as EfsColumn, FileSystem as EfsFileSystem};
15use crate::fsx::fs::Column as FsxColumn;
16use crate::iam::{
17 GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag, UserColumn,
18 UserTag as IamUserTag,
19};
20#[cfg(test)]
21use crate::iam::{IamRole, IamUser, LastAccessedService};
22use crate::keymap::{Action, Mode};
23use crate::kms::key::Key as KmsKey;
24pub use crate::lambda::{
25 Alias as LambdaAlias, Application as LambdaApplication,
26 ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
27 Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
28 ResourceColumn as LambdaResourceColumn, Version as LambdaVersion,
29};
30pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
31use crate::session::{Session, SessionTab};
32pub use crate::sqs::queue::Column as SqsColumn;
33pub use crate::sqs::trigger::Column as SqsTriggerColumn;
34#[cfg(test)]
35use crate::sqs::{
36 EventBridgePipe, LambdaTrigger, Queue as SqsQueue, QueueTag as SqsQueueTag, SnsSubscription,
37};
38use crate::table::TableState;
39pub use crate::ui::apig::{
40 filtered_apis, State as ApigState, FILTER_CONTROLS as APIG_FILTER_CONTROLS,
41};
42pub use crate::ui::cfn::{
43 change_set_column_ids, change_set_visible_column_ids, event_column_ids,
44 event_visible_column_ids, filtered_change_sets, filtered_cloudformation_stacks,
45 filtered_events, filtered_outputs, filtered_parameters, filtered_resources, output_column_ids,
46 parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
47 EventsView as CfnEventsView, State as CfnState, StatusFilter as CfnStatusFilter,
48};
49pub use crate::ui::cw::alarms::{
50 AlarmDetailTab, AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
51};
52pub use crate::ui::cw::logs::{
53 filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
54 DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
55};
56pub use crate::ui::ec2::{
57 DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
58 STATE_FILTER as EC2_STATE_FILTER,
59};
60pub use crate::ui::ecr::{
61 filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
62 FILTER_CONTROLS as ECR_FILTER_CONTROLS,
63};
64pub use crate::ui::efs::State as EfsState;
65pub use crate::ui::fsx::State as FsxState;
66use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
67pub use crate::ui::kms::{State as KmsState, Tab as KmsTab};
68pub use crate::ui::lambda::{
69 filtered_lambda_applications, filtered_lambda_functions,
70 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
71 DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
72};
73use crate::ui::monitoring::MonitoringState;
74pub use crate::ui::s3::{
75 calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
76 ObjectTab as S3ObjectTab, State as S3State,
77};
78pub use crate::ui::sqs::{
79 extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
80 filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
81 State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
82 SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
83};
84pub use crate::ui::{
85 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
86 StreamColumn, StreamSort, TimeUnit,
87};
88#[cfg(test)]
89use rusticity_core::LogStream;
90use rusticity_core::{
91 AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
92 CloudWatchClient, Ec2Client, EcrClient, EfsClient, FsxClient, IamClient, KmsClient,
93 LambdaClient, S3Client, SqsClient,
94};
95
96#[derive(Clone)]
97pub struct Tab {
98 pub service: Service,
99 pub title: String,
100 pub breadcrumb: String,
101}
102
103pub struct App {
104 pub running: bool,
105 pub mode: Mode,
106 pub config: AwsConfig,
107 pub cloudwatch_client: CloudWatchClient,
108 pub cloudtrail_client: CloudTrailClient,
109 pub s3_client: S3Client,
110 pub sqs_client: SqsClient,
111 pub alarms_client: AlarmsClient,
112 pub ec2_client: Ec2Client,
113 pub ecr_client: EcrClient,
114 pub kms_client: KmsClient,
115 pub efs_client: EfsClient,
116 pub fsx_client: FsxClient,
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 kms_state: KmsState,
136 pub efs_state: EfsState,
137 pub fsx_state: FsxState,
138 pub apig_state: ApigState,
139 pub lambda_state: LambdaState,
140 pub lambda_application_state: LambdaApplicationState,
141 pub cfn_state: CfnState,
142 pub iam_state: IamState,
143 pub service_picker: ServicePickerState,
144 pub service_selected: bool,
145 pub profile: String,
146 pub region: String,
147 pub region_selector_index: usize,
148 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
149 pub cw_log_group_column_ids: Vec<ColumnId>,
150 pub column_selector_index: usize,
151 pub preference_section: Preferences,
152 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
153 pub cw_log_stream_column_ids: Vec<ColumnId>,
154 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
155 pub cw_log_event_column_ids: Vec<ColumnId>,
156 pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
157 pub cw_log_tag_column_ids: Vec<ColumnId>,
158 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
159 pub cw_alarm_column_ids: Vec<ColumnId>,
160 pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
161 pub cloudtrail_event_column_ids: Vec<ColumnId>,
162 pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
163 pub cloudtrail_resource_column_ids: Vec<ColumnId>,
164 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
165 pub s3_bucket_column_ids: Vec<ColumnId>,
166 pub sqs_visible_column_ids: Vec<ColumnId>,
167 pub sqs_column_ids: Vec<ColumnId>,
168 pub ec2_visible_column_ids: Vec<ColumnId>,
169 pub ec2_column_ids: Vec<ColumnId>,
170 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
171 pub ecr_repo_column_ids: Vec<ColumnId>,
172 pub ecr_image_visible_column_ids: Vec<ColumnId>,
173 pub ecr_image_column_ids: Vec<ColumnId>,
174 pub efs_visible_column_ids: Vec<ColumnId>,
175 pub efs_column_ids: Vec<ColumnId>,
176 pub fsx_visible_column_ids: Vec<ColumnId>,
177 pub fsx_column_ids: Vec<ColumnId>,
178 pub apig_api_visible_column_ids: Vec<ColumnId>,
179 pub apig_api_column_ids: Vec<ColumnId>,
180 pub apig_route_visible_column_ids: Vec<ColumnId>,
181 pub apig_route_column_ids: Vec<ColumnId>,
182 pub apig_resource_visible_column_ids: Vec<ColumnId>,
183 pub apig_resource_column_ids: Vec<ColumnId>,
184 pub lambda_application_visible_column_ids: Vec<ColumnId>,
185 pub lambda_application_column_ids: Vec<ColumnId>,
186 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
187 pub lambda_deployment_column_ids: Vec<ColumnId>,
188 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
189 pub lambda_resource_column_ids: Vec<ColumnId>,
190 pub cfn_visible_column_ids: Vec<ColumnId>,
191 pub cfn_column_ids: Vec<ColumnId>,
192 pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
193 pub cfn_parameter_column_ids: Vec<ColumnId>,
194 pub cfn_output_visible_column_ids: Vec<ColumnId>,
195 pub cfn_output_column_ids: Vec<ColumnId>,
196 pub cfn_resource_visible_column_ids: Vec<ColumnId>,
197 pub cfn_resource_column_ids: Vec<ColumnId>,
198 pub cfn_event_visible_column_ids: Vec<ColumnId>,
199 pub cfn_event_column_ids: Vec<ColumnId>,
200 pub cfn_change_set_visible_column_ids: Vec<ColumnId>,
201 pub cfn_change_set_column_ids: Vec<ColumnId>,
202 pub iam_user_visible_column_ids: Vec<ColumnId>,
203 pub iam_user_column_ids: Vec<ColumnId>,
204 pub iam_role_visible_column_ids: Vec<ColumnId>,
205 pub iam_role_column_ids: Vec<ColumnId>,
206 pub iam_group_visible_column_ids: Vec<String>,
207 pub iam_group_column_ids: Vec<String>,
208 pub iam_policy_visible_column_ids: Vec<String>,
209 pub iam_policy_column_ids: Vec<String>,
210 pub view_mode: ViewMode,
211 pub error_message: Option<String>,
212 pub error_scroll: usize,
213 pub page_input: String,
214 pub calendar_date: Option<time::Date>,
215 pub calendar_selecting: CalendarField,
216 pub cursor_pos: usize,
217 pub current_session: Option<Session>,
218 pub sessions: Vec<Session>,
219 pub session_picker_selected: usize,
220 pub session_filter: String,
221 pub session_filter_active: bool,
222 pub region_filter: String,
223 pub region_picker_selected: usize,
224 pub region_filter_active: bool,
225 pub region_latencies: std::collections::HashMap<String, u64>,
226 pub profile_filter: String,
227 pub profile_picker_selected: usize,
228 pub profile_filter_active: bool,
229 pub available_profiles: Vec<AwsProfile>,
230 pub snapshot_requested: bool,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub enum CalendarField {
235 StartDate,
236 EndDate,
237}
238
239pub struct CloudWatchInsightsState {
240 pub insights: InsightsState,
241 pub loading: bool,
242}
243
244pub struct CloudWatchAlarmsState {
245 pub table: TableState<Alarm>,
246 pub current_alarm: Option<String>,
247 pub alarm_tab: AlarmTab,
248 pub detail_tab: AlarmDetailTab,
249 pub view_as: AlarmViewMode,
250 pub wrap_lines: bool,
251 pub sort_column: String,
252 pub sort_direction: SortDirection,
253 pub input_focus: InputFocus,
254 pub metric_data: Vec<(i64, f64)>,
255 pub metrics_loading: bool,
256}
257
258#[derive(Debug, Clone)]
259pub struct CloudTrailState {
260 pub table: TableState<CloudTrailEvent>,
261 pub input_focus: InputFocus,
262 pub current_event: Option<CloudTrailEvent>,
263 pub event_json_scroll: usize,
264 pub detail_focus: CloudTrailDetailFocus,
265 pub resources_expanded_index: Option<usize>,
266 pub active_event_name_filter: Option<String>,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq)]
271pub enum CloudTrailDetailFocus {
272 Resources,
273 EventRecord,
274}
275
276impl CyclicEnum for CloudTrailDetailFocus {
277 const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
278}
279
280impl PageSize {
281 pub fn value(&self) -> usize {
282 match self {
283 PageSize::Ten => 10,
284 PageSize::TwentyFive => 25,
285 PageSize::Fifty => 50,
286 PageSize::OneHundred => 100,
287 }
288 }
289
290 pub fn next(&self) -> Self {
291 match self {
292 PageSize::Ten => PageSize::TwentyFive,
293 PageSize::TwentyFive => PageSize::Fifty,
294 PageSize::Fifty => PageSize::OneHundred,
295 PageSize::OneHundred => PageSize::Ten,
296 }
297 }
298}
299
300pub struct ServicePickerState {
301 pub filter: String,
302 pub filter_active: bool,
303 pub selected: usize,
304 pub services: Vec<&'static str>,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq)]
308pub enum ViewMode {
309 List,
310 Detail,
311 Events,
312 InsightsResults,
313 PolicyView,
314}
315
316#[derive(Debug, Clone, Copy, PartialEq)]
317pub enum Service {
318 ApiGatewayApis,
319 CloudWatchLogGroups,
320 CloudWatchInsights,
321 CloudWatchAlarms,
322 CloudTrailEvents,
323 S3Buckets,
324 SqsQueues,
325 Ec2Instances,
326 EcrRepositories,
327 KmsKeys,
328 EfsFileSystems,
329 FsxFileSystems,
330 LambdaFunctions,
331 LambdaApplications,
332 CloudFormationStacks,
333 IamUsers,
334 IamRoles,
335 IamUserGroups,
336}
337
338impl Service {
339 pub fn name(&self) -> &str {
340 match self {
341 Service::ApiGatewayApis => "API Gateway › APIs",
342 Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
343 Service::CloudWatchInsights => "CloudWatch › Logs Insights",
344 Service::CloudWatchAlarms => "CloudWatch › Alarms",
345 Service::CloudTrailEvents => "CloudTrail › Event History",
346 Service::S3Buckets => "S3 › Buckets",
347 Service::SqsQueues => "SQS › Queues",
348 Service::Ec2Instances => "EC2 › Instances",
349 Service::EcrRepositories => "ECR › Repositories",
350 Service::KmsKeys => "KMS › Managed Keys",
351 Service::EfsFileSystems => "EFS › File Systems",
352 Service::FsxFileSystems => "FSx › File Systems",
353 Service::LambdaFunctions => "Lambda › Functions",
354 Service::LambdaApplications => "Lambda › Applications",
355 Service::CloudFormationStacks => "CloudFormation › Stacks",
356 Service::IamUsers => "IAM › Users",
357 Service::IamRoles => "IAM › Roles",
358 Service::IamUserGroups => "IAM › User Groups",
359 }
360 }
361}
362
363pub(crate) fn copy_to_clipboard(text: &str) {
364 use std::io::Write;
365 use std::process::{Command, Stdio};
366 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
367 if let Some(mut stdin) = child.stdin.take() {
368 let _ = stdin.write_all(text.as_bytes());
369 }
370 let _ = child.wait();
371 }
372}
373
374pub(crate) fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
375 if max > 0 {
376 *selected = (*selected + page_size).min(max - 1);
377 }
378}
379
380pub(crate) fn toggle_iam_preference(
381 idx: usize,
382 column_ids: &[String],
383 visible_column_ids: &mut Vec<String>,
384 page_size: &mut PageSize,
385) {
386 if idx > 0 && idx <= column_ids.len() {
387 if let Some(col) = column_ids.get(idx - 1) {
388 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
389 if visible_column_ids.len() > 1 {
391 visible_column_ids.remove(pos);
392 }
393 } else {
394 visible_column_ids.push(col.clone());
395 }
396 }
397 } else if idx == column_ids.len() + 3 {
398 *page_size = PageSize::Ten;
399 } else if idx == column_ids.len() + 4 {
400 *page_size = PageSize::TwentyFive;
401 } else if idx == column_ids.len() + 5 {
402 *page_size = PageSize::Fifty;
403 }
404}
405
406pub(crate) fn toggle_iam_preference_static(
407 idx: usize,
408 column_ids: &[ColumnId],
409 visible_column_ids: &mut Vec<ColumnId>,
410 page_size: &mut PageSize,
411) {
412 if idx > 0 && idx <= column_ids.len() {
413 if let Some(col) = column_ids.get(idx - 1) {
414 if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
415 if visible_column_ids.len() > 1 {
417 visible_column_ids.remove(pos);
418 }
419 } else {
420 visible_column_ids.push(*col);
421 }
422 }
423 } else if idx == column_ids.len() + 3 {
424 *page_size = PageSize::Ten;
425 } else if idx == column_ids.len() + 4 {
426 *page_size = PageSize::TwentyFive;
427 } else if idx == column_ids.len() + 5 {
428 *page_size = PageSize::Fifty;
429 }
430}
431
432pub(crate) fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
433 if idx == base_idx {
434 *page_size = PageSize::Ten;
435 } else if idx == base_idx + 1 {
436 *page_size = PageSize::TwentyFive;
437 } else if idx == base_idx + 2 {
438 *page_size = PageSize::Fifty;
439 }
440}
441
442fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
444 let page_size_idx = num_columns + 2;
445 if *current_idx < page_size_idx {
446 *current_idx = page_size_idx;
447 } else {
448 *current_idx = 0;
449 }
450}
451
452fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
454 let page_size_idx = num_columns + 2;
455 if *current_idx >= page_size_idx {
456 *current_idx = 0;
457 } else {
458 *current_idx = page_size_idx;
459 }
460}
461
462impl App {
463 pub fn get_input_focus(&self) -> InputFocus {
464 InputFocus::Filter
465 }
466
467 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
468 if self.current_service == Service::ApiGatewayApis {
469 crate::apig::actions::get_active_filter_mut(self)
470 } else if self.current_service == Service::CloudWatchAlarms {
471 Some(&mut self.alarms_state.table.filter)
472 } else if self.current_service == Service::CloudTrailEvents {
473 Some(&mut self.cloudtrail_state.table.filter)
474 } else if self.current_service == Service::Ec2Instances {
475 crate::ec2::actions::get_active_filter_mut(self)
476 } else if self.current_service == Service::S3Buckets {
477 if self.s3_state.current_bucket.is_some() {
478 Some(&mut self.s3_state.object_filter)
479 } else {
480 Some(&mut self.s3_state.buckets.filter)
481 }
482 } else if self.current_service == Service::EcrRepositories {
483 if self.ecr_state.current_repository.is_some() {
484 Some(&mut self.ecr_state.images.filter)
485 } else {
486 Some(&mut self.ecr_state.repositories.filter)
487 }
488 } else if self.current_service == Service::SqsQueues {
489 crate::sqs::actions::get_active_filter_mut(self)
490 } else if self.current_service == Service::LambdaFunctions {
491 crate::lambda::functions::get_active_filter_mut(self)
492 } else if self.current_service == Service::LambdaApplications {
493 crate::lambda::applications::get_active_filter_mut(self)
494 } else if self.current_service == Service::KmsKeys {
495 crate::kms::actions::get_active_filter_mut(self)
496 } else if self.current_service == Service::EfsFileSystems {
497 crate::efs::actions::get_active_filter_mut(self)
498 } else if self.current_service == Service::FsxFileSystems {
499 crate::fsx::actions::get_active_filter_mut(self)
500 } else if self.current_service == Service::CloudFormationStacks {
501 if self.cfn_state.current_stack.is_some()
502 && self.cfn_state.detail_tab == CfnDetailTab::Resources
503 {
504 Some(&mut self.cfn_state.resources.filter)
505 } else {
506 Some(&mut self.cfn_state.table.filter)
507 }
508 } else if self.current_service == Service::IamUsers {
509 if self.iam_state.current_user.is_some() {
510 if self.iam_state.user_tab == UserTab::Tags {
511 Some(&mut self.iam_state.user_tags.filter)
512 } else {
513 Some(&mut self.iam_state.policies.filter)
514 }
515 } else {
516 Some(&mut self.iam_state.users.filter)
517 }
518 } else if self.current_service == Service::IamRoles {
519 if self.iam_state.current_role.is_some() {
520 if self.iam_state.role_tab == RoleTab::Tags {
521 Some(&mut self.iam_state.tags.filter)
522 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
523 Some(&mut self.iam_state.last_accessed_filter)
524 } else {
525 Some(&mut self.iam_state.policies.filter)
526 }
527 } else {
528 Some(&mut self.iam_state.roles.filter)
529 }
530 } else if self.current_service == Service::IamUserGroups {
531 if self.iam_state.current_group.is_some() {
532 if self.iam_state.group_tab == GroupTab::Permissions {
533 Some(&mut self.iam_state.policies.filter)
534 } else if self.iam_state.group_tab == GroupTab::Users {
535 Some(&mut self.iam_state.group_users.filter)
536 } else {
537 None
538 }
539 } else {
540 Some(&mut self.iam_state.groups.filter)
541 }
542 } else if self.view_mode == ViewMode::List {
543 Some(&mut self.log_groups_state.log_groups.filter)
544 } else if self.view_mode == ViewMode::Detail
545 && self.log_groups_state.detail_tab == DetailTab::LogStreams
546 {
547 Some(&mut self.log_groups_state.stream_filter)
548 } else {
549 None
550 }
551 }
552
553 fn apply_filter_operation<F>(&mut self, op: F)
554 where
555 F: FnOnce(&mut String),
556 {
557 if let Some(filter) = self.get_active_filter_mut() {
558 op(filter);
559 if self.current_service == Service::CloudWatchAlarms {
561 crate::cw::actions::alarms_apply_filter_reset(self);
562 } else if self.current_service == Service::CloudTrailEvents {
563 crate::cloudtrail::actions::apply_filter_reset(self);
564 } else if self.current_service == Service::Ec2Instances {
565 crate::ec2::actions::apply_filter_reset(self);
566 } else if self.current_service == Service::S3Buckets {
567 crate::s3::actions::apply_filter_reset(self);
568 } else if self.current_service == Service::EcrRepositories {
569 crate::ecr::actions::apply_filter_reset(self);
570 } else if self.current_service == Service::KmsKeys {
571 crate::kms::actions::apply_filter_reset(self);
572 } else if self.current_service == Service::EfsFileSystems {
573 crate::efs::actions::apply_filter_reset(self);
574 } else if self.current_service == Service::FsxFileSystems {
575 crate::fsx::actions::apply_filter_reset(self);
576 } else if self.current_service == Service::SqsQueues {
577 crate::sqs::actions::apply_filter_reset(self);
578 } else if self.current_service == Service::LambdaFunctions {
579 crate::lambda::functions::apply_filter_reset(self);
580 } else if self.current_service == Service::LambdaApplications {
581 crate::lambda::applications::apply_filter_reset(self);
582 } else if self.current_service == Service::CloudFormationStacks {
583 crate::cfn::actions::apply_filter_reset(self);
584 } else if self.current_service == Service::IamUsers {
585 crate::iam::actions::apply_filter_reset_users(self);
586 } else if self.current_service == Service::IamRoles {
587 crate::iam::actions::apply_filter_reset_roles(self);
588 } else if self.current_service == Service::IamUserGroups {
589 crate::iam::actions::apply_filter_reset_groups(self);
590 } else if self.current_service == Service::CloudWatchLogGroups {
591 if self.view_mode == ViewMode::List {
592 self.log_groups_state.log_groups.reset();
593 } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
594 self.log_groups_state.selected_stream = 0;
595 }
596 } else if self.current_service == Service::ApiGatewayApis {
597 crate::apig::actions::apply_filter_reset(self);
598 }
599 }
600 }
601
602 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
603 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
604 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
605
606 std::env::set_var("AWS_PROFILE", &profile_name);
607
608 let config = AwsConfig::new(region).await?;
609 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
610 let cloudtrail_client = CloudTrailClient::new(config.clone());
611 let s3_client = S3Client::new(config.clone());
612 let sqs_client = SqsClient::new(config.clone());
613 let alarms_client = AlarmsClient::new(config.clone());
614 let ec2_client = Ec2Client::new(config.clone());
615 let ecr_client = EcrClient::new(config.clone());
616 let apig_client = ApiGatewayClient::new(config.clone());
617 let iam_client = IamClient::new(config.clone());
618 let kms_client = KmsClient::new(config.clone());
619 let efs_client = EfsClient::new(config.clone());
620 let fsx_client = FsxClient::new(config.clone());
621 let lambda_client = LambdaClient::new(config.clone());
622 let cloudformation_client = CloudFormationClient::new(config.clone());
623 let region_name = config.region.clone();
624
625 Ok(Self {
626 running: true,
627 mode: Mode::ServicePicker,
628 config,
629 cloudwatch_client,
630 cloudtrail_client,
631 s3_client,
632 sqs_client,
633 alarms_client,
634 ec2_client,
635 ecr_client,
636 kms_client,
637 efs_client,
638 fsx_client,
639 apig_client,
640 iam_client,
641 lambda_client,
642 cloudformation_client,
643 current_service: Service::CloudWatchLogGroups,
644 tabs: Vec::new(),
645 current_tab: 0,
646 tab_picker_selected: 0,
647 tab_filter: String::new(),
648 pending_key: None,
649 log_groups_state: CloudWatchLogGroupsState::new(),
650 insights_state: CloudWatchInsightsState::new(),
651 alarms_state: CloudWatchAlarmsState::new(),
652 cloudtrail_state: CloudTrailState {
653 table: TableState::new(),
654 input_focus: InputFocus::Filter,
655 current_event: None,
656 event_json_scroll: 0,
657 detail_focus: CloudTrailDetailFocus::Resources,
658 resources_expanded_index: None,
659 active_event_name_filter: None,
660 },
661 s3_state: S3State::new(),
662 sqs_state: SqsState::new(),
663 ec2_state: Ec2State::default(),
664 ecr_state: EcrState::new(),
665 kms_state: KmsState::new(),
666 efs_state: EfsState::new(),
667 fsx_state: FsxState::new(),
668 apig_state: ApigState::new(),
669 lambda_state: LambdaState::new(),
670 lambda_application_state: LambdaApplicationState::new(),
671 cfn_state: CfnState::new(),
672 iam_state: IamState::new(),
673 service_picker: ServicePickerState::new(),
674 service_selected: false,
675 profile: profile_name,
676 region: region_name,
677 region_selector_index: 0,
678 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
679 cw_log_group_column_ids: LogGroupColumn::ids(),
680 column_selector_index: 0,
681 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
682 cw_log_stream_column_ids: StreamColumn::ids(),
683 cw_log_event_visible_column_ids: EventColumn::default_visible(),
684 cw_log_event_column_ids: EventColumn::ids(),
685 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
686 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
687 cw_alarm_visible_column_ids: [
688 AlarmColumn::Name,
689 AlarmColumn::State,
690 AlarmColumn::LastStateUpdate,
691 AlarmColumn::Conditions,
692 AlarmColumn::Actions,
693 ]
694 .iter()
695 .map(|c| c.id())
696 .collect(),
697 cw_alarm_column_ids: AlarmColumn::ids(),
698 cloudtrail_event_visible_column_ids: [
699 CloudTrailEventColumn::EventName,
700 CloudTrailEventColumn::EventTime,
701 CloudTrailEventColumn::Username,
702 CloudTrailEventColumn::EventSource,
703 CloudTrailEventColumn::ResourceType,
704 CloudTrailEventColumn::ResourceName,
705 ]
706 .iter()
707 .map(|c| c.id())
708 .collect(),
709 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
710 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
711 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
712 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
713 s3_bucket_column_ids: S3BucketColumn::ids(),
714 sqs_visible_column_ids: [
715 SqsColumn::Name,
716 SqsColumn::Type,
717 SqsColumn::Created,
718 SqsColumn::MessagesAvailable,
719 SqsColumn::MessagesInFlight,
720 SqsColumn::Encryption,
721 SqsColumn::ContentBasedDeduplication,
722 ]
723 .iter()
724 .map(|c| c.id())
725 .collect(),
726 sqs_column_ids: SqsColumn::ids(),
727 ec2_visible_column_ids: [
728 Ec2Column::Name,
729 Ec2Column::InstanceId,
730 Ec2Column::InstanceState,
731 Ec2Column::InstanceType,
732 Ec2Column::StatusCheck,
733 Ec2Column::AlarmStatus,
734 Ec2Column::AvailabilityZone,
735 Ec2Column::PublicIpv4Dns,
736 Ec2Column::PublicIpv4Address,
737 Ec2Column::ElasticIp,
738 Ec2Column::Ipv6Ips,
739 Ec2Column::Monitoring,
740 Ec2Column::SecurityGroupName,
741 Ec2Column::KeyName,
742 Ec2Column::LaunchTime,
743 Ec2Column::PlatformDetails,
744 ]
745 .iter()
746 .map(|c| c.id())
747 .collect(),
748 ec2_column_ids: Ec2Column::ids(),
749 ecr_repo_visible_column_ids: EcrColumn::ids(),
750 ecr_repo_column_ids: EcrColumn::ids(),
751 ecr_image_visible_column_ids: EcrImageColumn::ids(),
752 ecr_image_column_ids: EcrImageColumn::ids(),
753 efs_visible_column_ids: EfsColumn::default_visible_ids(),
754 efs_column_ids: EfsColumn::ids(),
755 fsx_visible_column_ids: FsxColumn::default_visible_ids(),
756 fsx_column_ids: FsxColumn::ids(),
757 apig_api_visible_column_ids: ApigColumn::ids(),
758 apig_api_column_ids: ApigColumn::ids(),
759 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
760 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
761 apig_resource_visible_column_ids: ResourceColumn::all()
762 .iter()
763 .map(|c| c.id())
764 .collect(),
765 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
766 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
767 lambda_application_column_ids: LambdaApplicationColumn::ids(),
768 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
769 lambda_deployment_column_ids: DeploymentColumn::ids(),
770 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
771 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
772 cfn_visible_column_ids: [
773 CfnColumn::Name,
774 CfnColumn::Status,
775 CfnColumn::CreatedTime,
776 CfnColumn::Description,
777 ]
778 .iter()
779 .map(|c| c.id())
780 .collect(),
781 cfn_column_ids: CfnColumn::ids(),
782 cfn_parameter_visible_column_ids: parameter_column_ids(),
783 cfn_parameter_column_ids: parameter_column_ids(),
784 cfn_output_visible_column_ids: output_column_ids(),
785 cfn_output_column_ids: output_column_ids(),
786 cfn_resource_visible_column_ids: resource_column_ids(),
787 cfn_resource_column_ids: resource_column_ids(),
788 cfn_event_visible_column_ids: event_visible_column_ids(),
789 cfn_event_column_ids: event_column_ids(),
790 cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
791 cfn_change_set_column_ids: change_set_column_ids(),
792 iam_user_visible_column_ids: UserColumn::visible(),
793 iam_user_column_ids: UserColumn::ids(),
794 iam_role_visible_column_ids: RoleColumn::visible(),
795 iam_role_column_ids: RoleColumn::ids(),
796 iam_group_visible_column_ids: vec![
797 "Group name".to_string(),
798 "Users".to_string(),
799 "Permissions".to_string(),
800 "Creation time".to_string(),
801 ],
802 iam_group_column_ids: vec![
803 "Group name".to_string(),
804 "Path".to_string(),
805 "Users".to_string(),
806 "Permissions".to_string(),
807 "Creation time".to_string(),
808 ],
809 iam_policy_visible_column_ids: vec![
810 "Policy name".to_string(),
811 "Type".to_string(),
812 "Attached via".to_string(),
813 ],
814 iam_policy_column_ids: vec![
815 "Policy name".to_string(),
816 "Type".to_string(),
817 "Attached via".to_string(),
818 "Attached entities".to_string(),
819 "Description".to_string(),
820 "Creation time".to_string(),
821 "Edited time".to_string(),
822 ],
823 preference_section: Preferences::Columns,
824 view_mode: ViewMode::List,
825 error_message: None,
826 error_scroll: 0,
827 page_input: String::new(),
828 calendar_date: None,
829 calendar_selecting: CalendarField::StartDate,
830 cursor_pos: 0,
831 current_session: None,
832 sessions: Vec::new(),
833 session_picker_selected: 0,
834 session_filter: String::new(),
835 session_filter_active: false,
836 region_filter: String::new(),
837 region_filter_active: false,
838 region_picker_selected: 0,
839 region_latencies: std::collections::HashMap::new(),
840 profile_filter: String::new(),
841 profile_filter_active: false,
842 profile_picker_selected: 0,
843 available_profiles: Vec::new(),
844 snapshot_requested: false,
845 })
846 }
847
848 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
849 let config = AwsConfig::dummy(region.clone());
850 Self {
851 running: true,
852 mode: Mode::ServicePicker,
853 config: config.clone(),
854 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
855 cloudtrail_client: CloudTrailClient::new(config.clone()),
856 s3_client: S3Client::new(config.clone()),
857 sqs_client: SqsClient::new(config.clone()),
858 alarms_client: AlarmsClient::new(config.clone()),
859 ec2_client: Ec2Client::new(config.clone()),
860 ecr_client: EcrClient::new(config.clone()),
861 kms_client: KmsClient::new(config.clone()),
862 efs_client: EfsClient::new(config.clone()),
863 fsx_client: FsxClient::new(config.clone()),
864 apig_client: ApiGatewayClient::new(config.clone()),
865 iam_client: IamClient::new(config.clone()),
866 lambda_client: LambdaClient::new(config.clone()),
867 cloudformation_client: CloudFormationClient::new(config.clone()),
868 current_service: Service::CloudWatchLogGroups,
869 tabs: Vec::new(),
870 current_tab: 0,
871 tab_picker_selected: 0,
872 tab_filter: String::new(),
873 pending_key: None,
874 log_groups_state: CloudWatchLogGroupsState::new(),
875 insights_state: CloudWatchInsightsState::new(),
876 alarms_state: CloudWatchAlarmsState::new(),
877 s3_state: S3State::new(),
878 cloudtrail_state: CloudTrailState {
879 table: TableState::new(),
880 input_focus: InputFocus::Filter,
881 current_event: None,
882 event_json_scroll: 0,
883 detail_focus: CloudTrailDetailFocus::Resources,
884 resources_expanded_index: None,
885 active_event_name_filter: None,
886 },
887 sqs_state: SqsState::new(),
888 ec2_state: Ec2State::default(),
889 ecr_state: EcrState::new(),
890 kms_state: KmsState::new(),
891 efs_state: EfsState::new(),
892 fsx_state: FsxState::new(),
893 apig_state: ApigState::new(),
894 lambda_state: LambdaState::new(),
895 lambda_application_state: LambdaApplicationState::new(),
896 cfn_state: CfnState::new(),
897 iam_state: IamState::new(),
898 service_picker: ServicePickerState::new(),
899 service_selected: false,
900 profile,
901 region: region.unwrap_or_default(),
902 region_selector_index: 0,
903 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
904 cw_log_group_column_ids: LogGroupColumn::ids(),
905 column_selector_index: 0,
906 preference_section: Preferences::Columns,
907 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
908 cw_log_stream_column_ids: StreamColumn::ids(),
909 cw_log_event_visible_column_ids: EventColumn::default_visible(),
910 cw_log_event_column_ids: EventColumn::ids(),
911 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
912 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
913 cw_alarm_visible_column_ids: [
914 AlarmColumn::Name,
915 AlarmColumn::State,
916 AlarmColumn::LastStateUpdate,
917 AlarmColumn::Conditions,
918 AlarmColumn::Actions,
919 ]
920 .iter()
921 .map(|c| c.id())
922 .collect(),
923 cw_alarm_column_ids: AlarmColumn::ids(),
924 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
925 cloudtrail_event_visible_column_ids: [
926 CloudTrailEventColumn::EventName,
927 CloudTrailEventColumn::EventTime,
928 CloudTrailEventColumn::Username,
929 CloudTrailEventColumn::EventSource,
930 CloudTrailEventColumn::ResourceType,
931 CloudTrailEventColumn::ResourceName,
932 ]
933 .iter()
934 .map(|c| c.id())
935 .collect(),
936 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
937 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
938 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
939 s3_bucket_column_ids: S3BucketColumn::ids(),
940 sqs_visible_column_ids: [
941 SqsColumn::Name,
942 SqsColumn::Type,
943 SqsColumn::Created,
944 SqsColumn::MessagesAvailable,
945 SqsColumn::MessagesInFlight,
946 SqsColumn::Encryption,
947 SqsColumn::ContentBasedDeduplication,
948 ]
949 .iter()
950 .map(|c| c.id())
951 .collect(),
952 sqs_column_ids: SqsColumn::ids(),
953 ec2_visible_column_ids: [
954 Ec2Column::Name,
955 Ec2Column::InstanceId,
956 Ec2Column::InstanceState,
957 Ec2Column::InstanceType,
958 Ec2Column::StatusCheck,
959 Ec2Column::AlarmStatus,
960 Ec2Column::AvailabilityZone,
961 Ec2Column::PublicIpv4Dns,
962 Ec2Column::PublicIpv4Address,
963 Ec2Column::ElasticIp,
964 Ec2Column::Ipv6Ips,
965 Ec2Column::Monitoring,
966 Ec2Column::SecurityGroupName,
967 Ec2Column::KeyName,
968 Ec2Column::LaunchTime,
969 Ec2Column::PlatformDetails,
970 ]
971 .iter()
972 .map(|c| c.id())
973 .collect(),
974 ec2_column_ids: Ec2Column::ids(),
975 ecr_repo_visible_column_ids: EcrColumn::ids(),
976 ecr_repo_column_ids: EcrColumn::ids(),
977 ecr_image_visible_column_ids: EcrImageColumn::ids(),
978 ecr_image_column_ids: EcrImageColumn::ids(),
979 efs_visible_column_ids: EfsColumn::default_visible_ids(),
980 efs_column_ids: EfsColumn::ids(),
981 fsx_visible_column_ids: FsxColumn::default_visible_ids(),
982 fsx_column_ids: FsxColumn::ids(),
983 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
984 lambda_application_column_ids: LambdaApplicationColumn::ids(),
985 apig_api_visible_column_ids: ApigColumn::ids(),
986 apig_api_column_ids: ApigColumn::ids(),
987 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
988 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
989 apig_resource_visible_column_ids: ResourceColumn::all()
990 .iter()
991 .map(|c| c.id())
992 .collect(),
993 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
994 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
995 lambda_deployment_column_ids: DeploymentColumn::ids(),
996 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
997 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
998 cfn_visible_column_ids: [
999 CfnColumn::Name,
1000 CfnColumn::Status,
1001 CfnColumn::CreatedTime,
1002 CfnColumn::Description,
1003 ]
1004 .iter()
1005 .map(|c| c.id())
1006 .collect(),
1007 cfn_column_ids: CfnColumn::ids(),
1008 iam_user_visible_column_ids: UserColumn::visible(),
1009 cfn_parameter_visible_column_ids: parameter_column_ids(),
1010 cfn_parameter_column_ids: parameter_column_ids(),
1011 cfn_output_visible_column_ids: output_column_ids(),
1012 cfn_output_column_ids: output_column_ids(),
1013 cfn_resource_visible_column_ids: resource_column_ids(),
1014 cfn_resource_column_ids: resource_column_ids(),
1015 cfn_event_visible_column_ids: event_visible_column_ids(),
1016 cfn_event_column_ids: event_column_ids(),
1017 cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
1018 cfn_change_set_column_ids: change_set_column_ids(),
1019 iam_user_column_ids: UserColumn::ids(),
1020 iam_role_visible_column_ids: RoleColumn::visible(),
1021 iam_role_column_ids: RoleColumn::ids(),
1022 iam_group_visible_column_ids: vec![
1023 "Group name".to_string(),
1024 "Users".to_string(),
1025 "Permissions".to_string(),
1026 "Creation time".to_string(),
1027 ],
1028 iam_group_column_ids: vec![
1029 "Group name".to_string(),
1030 "Path".to_string(),
1031 "Users".to_string(),
1032 "Permissions".to_string(),
1033 "Creation time".to_string(),
1034 ],
1035 iam_policy_visible_column_ids: vec![
1036 "Policy name".to_string(),
1037 "Type".to_string(),
1038 "Attached via".to_string(),
1039 ],
1040 iam_policy_column_ids: vec![
1041 "Policy name".to_string(),
1042 "Type".to_string(),
1043 "Attached via".to_string(),
1044 "Attached entities".to_string(),
1045 "Description".to_string(),
1046 "Creation time".to_string(),
1047 "Edited time".to_string(),
1048 ],
1049 view_mode: ViewMode::List,
1050 error_message: None,
1051 error_scroll: 0,
1052 page_input: String::new(),
1053 calendar_date: None,
1054 calendar_selecting: CalendarField::StartDate,
1055 cursor_pos: 0,
1056 current_session: None,
1057 sessions: Vec::new(),
1058 session_picker_selected: 0,
1059 session_filter: String::new(),
1060 session_filter_active: false,
1061 region_filter: String::new(),
1062 region_filter_active: false,
1063 region_picker_selected: 0,
1064 region_latencies: std::collections::HashMap::new(),
1065 profile_filter: String::new(),
1066 profile_filter_active: false,
1067 profile_picker_selected: 0,
1068 available_profiles: Vec::new(),
1069 snapshot_requested: false,
1070 }
1071 }
1072
1073 pub fn handle_action(&mut self, action: Action) {
1074 match action {
1075 Action::Noop => {}
1076 Action::EnterFilterMode => match self.mode {
1077 Mode::ServicePicker => self.service_picker.filter_active = true,
1078 Mode::RegionPicker => self.region_filter_active = true,
1079 Mode::SessionPicker => self.session_filter_active = true,
1080 Mode::ProfilePicker => self.profile_filter_active = true,
1081 _ => {}
1082 },
1083 Action::ExitFilterMode => match self.mode {
1084 Mode::ServicePicker => {
1085 if self.service_picker.filter_active {
1086 self.service_picker.filter_active = false;
1087 } else {
1088 self.mode = Mode::Normal;
1089 }
1090 }
1091 Mode::RegionPicker => {
1092 if self.region_filter_active {
1093 self.region_filter_active = false;
1094 } else {
1095 self.mode = Mode::Normal;
1096 }
1097 }
1098 Mode::SessionPicker => {
1099 if self.session_filter_active {
1100 self.session_filter_active = false;
1101 } else {
1102 self.mode = Mode::Normal;
1103 }
1104 }
1105 Mode::ProfilePicker => {
1106 if self.profile_filter_active {
1107 self.profile_filter_active = false;
1108 } else {
1109 self.mode = Mode::Normal;
1110 }
1111 }
1112 _ => {}
1113 },
1114 Action::Quit => {
1115 self.mode = Mode::QuitConfirm;
1116 }
1117 Action::ConfirmQuit => {
1118 self.save_current_session();
1119 self.running = false;
1120 }
1121 Action::CancelQuit => {
1122 self.mode = Mode::Normal;
1123 }
1124 Action::CloseService => {
1125 if !self.tabs.is_empty() {
1126 self.tabs.remove(self.current_tab);
1128
1129 if self.tabs.is_empty() {
1130 self.service_selected = false;
1132 self.current_tab = 0;
1133 self.mode = Mode::ServicePicker;
1134 } else {
1135 if self.current_tab >= self.tabs.len() {
1137 self.current_tab = self.tabs.len() - 1;
1138 }
1139 self.current_service = self.tabs[self.current_tab].service;
1140 self.service_selected = true;
1141 self.mode = Mode::Normal;
1142 }
1143 } else {
1144 self.service_selected = false;
1146 self.mode = Mode::Normal;
1147 }
1148 self.service_picker.filter.clear();
1149 self.service_picker.selected = 0;
1150 }
1151 Action::NextItem => {
1152 let should_navigate = match self.mode {
1154 Mode::ServicePicker => !self.service_picker.filter_active,
1155 Mode::RegionPicker => !self.region_filter_active,
1156 Mode::SessionPicker => !self.session_filter_active,
1157 Mode::ProfilePicker => !self.profile_filter_active,
1158 _ => true,
1159 };
1160 if should_navigate {
1161 self.next_item();
1162 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1163 self.region_filter.push('j');
1165 self.region_picker_selected = 0;
1166 }
1167 }
1168 Action::PrevItem => {
1169 let should_navigate = match self.mode {
1171 Mode::ServicePicker => !self.service_picker.filter_active,
1172 Mode::RegionPicker => !self.region_filter_active,
1173 Mode::SessionPicker => !self.session_filter_active,
1174 Mode::ProfilePicker => !self.profile_filter_active,
1175 _ => true,
1176 };
1177 if should_navigate {
1178 self.prev_item();
1179 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1180 self.region_filter.push('k');
1182 self.region_picker_selected = 0;
1183 }
1184 }
1185 Action::PageUp => self.page_up(),
1186 Action::PageDown => self.page_down(),
1187 Action::NextPane => self.next_pane(),
1188 Action::PrevPane => self.prev_pane(),
1189 Action::CollapseRow => self.collapse_row(),
1190 Action::ExpandRow => self.expand_row(),
1191 Action::Select => self.select_item(),
1192 Action::OpenSpaceMenu => {
1193 use crate::app::InsightsFocus;
1195 if self.current_service == Service::CloudWatchInsights
1196 && self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
1197 {
1198 if self.insights_state.insights.show_dropdown
1199 && !self.insights_state.insights.log_group_matches.is_empty()
1200 {
1201 self.handle_action(Action::ToggleFilterCheckbox);
1203 } else {
1204 self.handle_action(Action::Select);
1206 }
1207 return;
1208 }
1209 self.mode = Mode::SpaceMenu;
1210 self.service_picker.filter.clear();
1211 self.service_picker.filter_active = false;
1212 self.service_picker.selected = 0;
1213 }
1214 Action::CloseMenu => {
1215 self.mode = Mode::Normal;
1216 self.service_picker.filter.clear();
1217 match self.current_service {
1219 Service::S3Buckets => {
1220 crate::s3::actions::reset_on_service_select(self);
1221 }
1222 Service::CloudFormationStacks => {
1223 crate::cfn::actions::apply_filter_reset(self);
1224 }
1225 Service::LambdaFunctions => {
1226 crate::lambda::functions::apply_filter_reset(self);
1227 }
1228 Service::SqsQueues => {
1229 self.sqs_state.queues.reset();
1230 }
1231 Service::IamRoles => {
1232 crate::iam::actions::reset_on_service_select_roles(self);
1233 }
1234 Service::IamUsers => {
1235 crate::iam::actions::reset_on_service_select_users(self);
1236 }
1237 Service::IamUserGroups => {
1238 crate::iam::actions::reset_on_service_select_groups(self);
1239 }
1240 Service::CloudWatchAlarms => {
1241 self.alarms_state.table.reset();
1242 }
1243 Service::Ec2Instances => {
1244 crate::ec2::actions::apply_filter_reset(self);
1245 }
1246 Service::EcrRepositories => {
1247 crate::ecr::actions::apply_filter_reset(self);
1248 }
1249 Service::KmsKeys => {
1250 crate::kms::actions::apply_filter_reset(self);
1251 }
1252 Service::EfsFileSystems => {
1253 crate::efs::actions::apply_filter_reset(self);
1254 }
1255 Service::FsxFileSystems => {
1256 crate::fsx::actions::apply_filter_reset(self);
1257 }
1258 Service::ApiGatewayApis => {
1259 crate::apig::actions::apply_filter_reset(self);
1260 }
1261 Service::LambdaApplications => {
1262 crate::lambda::applications::apply_filter_reset(self);
1263 }
1264 _ => {}
1265 }
1266 }
1267 Action::NextTab => {
1268 if !self.tabs.is_empty() {
1269 self.current_tab = (self.current_tab + 1) % self.tabs.len();
1270 self.current_service = self.tabs[self.current_tab].service;
1271 }
1272 }
1273 Action::PrevTab => {
1274 if !self.tabs.is_empty() {
1275 self.current_tab = if self.current_tab == 0 {
1276 self.tabs.len() - 1
1277 } else {
1278 self.current_tab - 1
1279 };
1280 self.current_service = self.tabs[self.current_tab].service;
1281 }
1282 }
1283 Action::CloseTab => {
1284 if !self.tabs.is_empty() {
1285 self.tabs.remove(self.current_tab);
1286 if self.tabs.is_empty() {
1287 self.service_selected = false;
1289 self.current_tab = 0;
1290 self.service_picker.filter.clear();
1291 self.service_picker.selected = 0;
1292 self.mode = Mode::ServicePicker;
1293 } else {
1294 if self.current_tab >= self.tabs.len() {
1297 self.current_tab = self.tabs.len() - 1;
1298 }
1299 self.current_service = self.tabs[self.current_tab].service;
1300 self.service_selected = true;
1301 self.mode = Mode::Normal;
1302 }
1303 }
1304 }
1305 Action::OpenTabPicker => {
1306 if !self.tabs.is_empty() {
1307 self.tab_picker_selected = self.current_tab;
1308 self.mode = Mode::TabPicker;
1309 } else {
1310 self.mode = Mode::Normal;
1311 }
1312 }
1313 Action::OpenSessionPicker => {
1314 self.save_current_session();
1315 self.sessions = Session::list_all().unwrap_or_default();
1316 self.session_picker_selected = 0;
1317 self.mode = Mode::SessionPicker;
1318 }
1319 Action::LoadSession => {
1320 let filtered_sessions = self.get_filtered_sessions();
1321 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1322 let session = session.clone();
1323 self.profile = session.profile.clone();
1325 self.region = session.region.clone();
1326 self.config.account_id = session.account_id.clone();
1327 self.config.role_arn = session.role_arn.clone();
1328
1329 self.tabs.clear();
1331 for session_tab in &session.tabs {
1332 let service = match session_tab.service.as_str() {
1334 "ApiGatewayApis" => Service::ApiGatewayApis,
1335 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1336 "CloudWatchInsights" => Service::CloudWatchInsights,
1337 "CloudWatchAlarms" => Service::CloudWatchAlarms,
1338 "CloudTrailEvents" => Service::CloudTrailEvents,
1339 "S3Buckets" => Service::S3Buckets,
1340 "SqsQueues" => Service::SqsQueues,
1341 "Ec2Instances" => Service::Ec2Instances,
1342 "EcrRepositories" => Service::EcrRepositories,
1343 "KmsKeys" => Service::KmsKeys,
1344 "EfsFileSystems" => Service::EfsFileSystems,
1345 "FsxFileSystems" => Service::FsxFileSystems,
1346 "LambdaFunctions" => Service::LambdaFunctions,
1347 "LambdaApplications" => Service::LambdaApplications,
1348 "CloudFormationStacks" => Service::CloudFormationStacks,
1349 "IamUsers" => Service::IamUsers,
1350 "IamRoles" => Service::IamRoles,
1351 "IamUserGroups" => Service::IamUserGroups,
1352 _ => continue,
1353 };
1354
1355 self.tabs.push(Tab {
1356 service,
1357 title: session_tab.title.clone(),
1358 breadcrumb: session_tab.breadcrumb.clone(),
1359 });
1360
1361 if let Some(filter) = &session_tab.filter {
1363 if service == Service::CloudWatchLogGroups {
1364 self.log_groups_state.log_groups.filter = filter.clone();
1365 }
1366 }
1367 }
1368
1369 if !self.tabs.is_empty() {
1370 self.current_tab = 0;
1371 self.current_service = self.tabs[0].service;
1372 self.service_selected = true;
1373 self.current_session = Some(session.clone());
1374 }
1375 }
1376 self.mode = Mode::Normal;
1377 }
1378 Action::SaveSession => {
1379 }
1381 Action::OpenServicePicker => {
1382 if self.mode == Mode::ServicePicker {
1383 self.tabs.push(Tab {
1384 service: Service::S3Buckets,
1385 title: "S3 › Buckets".to_string(),
1386 breadcrumb: "S3 › Buckets".to_string(),
1387 });
1388 self.current_tab = self.tabs.len() - 1;
1389 self.current_service = Service::S3Buckets;
1390 self.view_mode = ViewMode::List;
1391 self.service_selected = true;
1392 self.mode = Mode::Normal;
1393 } else {
1394 self.mode = Mode::ServicePicker;
1395 self.service_picker.filter.clear();
1396 self.service_picker.selected = 0;
1397 }
1398 }
1399 Action::OpenCloudWatch => {
1400 self.current_service = Service::CloudWatchLogGroups;
1401 self.view_mode = ViewMode::List;
1402 self.service_selected = true;
1403 self.mode = Mode::Normal;
1404 }
1405 Action::OpenCloudWatchSplit => {
1406 self.current_service = Service::CloudWatchInsights;
1407 self.view_mode = ViewMode::InsightsResults;
1408 self.service_selected = true;
1409 self.mode = Mode::Normal;
1410 }
1411 Action::OpenCloudWatchAlarms => {
1412 self.current_service = Service::CloudWatchAlarms;
1413 self.view_mode = ViewMode::List;
1414 self.service_selected = true;
1415 self.mode = Mode::Normal;
1416 }
1417 Action::FilterInput(c) => {
1418 if self.mode == Mode::TabPicker {
1419 self.tab_filter.push(c);
1420 self.tab_picker_selected = 0;
1421 } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1422 self.service_picker.filter.push(c);
1423 self.service_picker.selected = 0;
1424 } else if self.mode == Mode::ServicePicker && c == 'q' {
1425 self.mode = Mode::QuitConfirm;
1426 } else if self.mode == Mode::ServicePicker && c == 'i' {
1427 self.service_picker.filter_active = true;
1428 } else if self.mode == Mode::ServicePicker {
1429 self.service_picker.filter_active = true;
1431 self.service_picker.filter.push(c);
1432 self.service_picker.selected = 0;
1433 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1434 self.region_filter.push(c);
1435 self.region_picker_selected = 0;
1436 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1437 self.profile_filter.push(c);
1438 self.profile_picker_selected = 0;
1439 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1440 self.session_filter.push(c);
1441 self.session_picker_selected = 0;
1442 } else if self.mode == Mode::InsightsInput {
1443 match self.insights_state.insights.insights_focus {
1444 InsightsFocus::Query => {
1445 self.insights_state.insights.query_text.push(c);
1446 }
1447 InsightsFocus::LogGroupSearch => {
1448 self.insights_state.insights.log_group_search.push(c);
1449 if !self.insights_state.insights.log_group_search.is_empty() {
1451 self.insights_state.insights.log_group_matches = self
1452 .log_groups_state
1453 .log_groups
1454 .items
1455 .iter()
1456 .filter(|g| {
1457 g.name.to_lowercase().contains(
1458 &self
1459 .insights_state
1460 .insights
1461 .log_group_search
1462 .to_lowercase(),
1463 )
1464 })
1465 .take(50)
1466 .map(|g| g.name.clone())
1467 .collect();
1468 self.insights_state.insights.show_dropdown = true;
1469 } else {
1470 self.insights_state.insights.log_group_matches.clear();
1471 self.insights_state.insights.show_dropdown = false;
1472 }
1473 }
1474 _ => {}
1475 }
1476 } else if self.mode == Mode::FilterInput {
1477 let is_pagination_focused = if self.current_service
1479 == Service::LambdaApplications
1480 {
1481 crate::lambda::applications::is_pagination_focused(self)
1482 } else if self.current_service == Service::CloudFormationStacks {
1483 crate::cfn::actions::is_pagination_focused(self)
1484 } else if self.current_service == Service::IamRoles
1485 && self.iam_state.current_role.is_none()
1486 {
1487 crate::iam::actions::is_pagination_focused_roles(self)
1488 } else if self.view_mode == ViewMode::PolicyView {
1489 crate::iam::actions::is_pagination_focused_policy_view(self)
1490 } else if self.current_service == Service::CloudWatchAlarms {
1491 crate::cw::actions::alarms_is_pagination_focused(self)
1492 } else if self.current_service == Service::CloudTrailEvents {
1493 crate::cloudtrail::actions::is_pagination_focused(self)
1494 } else if self.current_service == Service::Ec2Instances {
1495 crate::ec2::actions::is_pagination_focused(self)
1496 } else if self.current_service == Service::CloudWatchLogGroups {
1497 crate::cw::actions::logs_is_pagination_focused(self)
1498 } else if self.current_service == Service::ApiGatewayApis {
1499 crate::apig::actions::is_pagination_focused(self)
1500 } else if self.current_service == Service::EcrRepositories
1501 && self.ecr_state.current_repository.is_none()
1502 {
1503 crate::ecr::actions::is_pagination_focused(self)
1504 } else if self.current_service == Service::KmsKeys {
1505 crate::kms::actions::is_pagination_focused(self)
1506 } else if self.current_service == Service::EfsFileSystems {
1507 crate::efs::actions::is_pagination_focused(self)
1508 } else if self.current_service == Service::FsxFileSystems {
1509 crate::fsx::actions::is_pagination_focused(self)
1510 } else if self.current_service == Service::LambdaFunctions {
1511 crate::lambda::functions::is_pagination_focused(self)
1512 } else if self.current_service == Service::SqsQueues {
1513 if self.sqs_state.current_queue.is_some()
1514 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1515 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1516 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1517 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1518 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1519 {
1520 self.sqs_state.input_focus == InputFocus::Pagination
1521 } else {
1522 false
1523 }
1524 } else {
1525 false
1526 };
1527
1528 if is_pagination_focused && c.is_ascii_digit() {
1529 self.page_input.push(c);
1530 } else if self.current_service == Service::LambdaApplications {
1531 if crate::lambda::applications::is_filter_focused(self) {
1532 self.apply_filter_operation(|f| f.push(c));
1533 }
1534 } else if self.current_service == Service::CloudFormationStacks {
1535 crate::cfn::actions::filter_char_push(self, c);
1536 } else if self.current_service == Service::EcrRepositories
1537 && self.ecr_state.current_repository.is_none()
1538 {
1539 if self.ecr_state.input_focus == InputFocus::Filter {
1540 self.apply_filter_operation(|f| f.push(c));
1541 }
1542 } else if self.current_service == Service::IamRoles
1543 && self.iam_state.current_role.is_none()
1544 {
1545 if crate::iam::actions::is_filter_focused_roles(self) {
1546 self.apply_filter_operation(|f| f.push(c));
1547 }
1548 } else if self.view_mode == ViewMode::PolicyView {
1549 if crate::iam::actions::is_filter_focused_policy_view(self) {
1550 self.apply_filter_operation(|f| f.push(c));
1551 }
1552 } else if self.current_service == Service::ApiGatewayApis
1553 && self.apig_state.current_api.is_some()
1554 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1555 {
1556 if crate::apig::actions::filter_char_allowed(self) {
1557 self.apply_filter_operation(|f| f.push(c));
1558 }
1559 } else if self.current_service == Service::LambdaFunctions {
1560 if crate::lambda::functions::filter_char_allowed(self) {
1561 self.apply_filter_operation(|f| f.push(c));
1562 }
1563 } else if self.current_service == Service::SqsQueues
1564 && self.sqs_state.current_queue.is_some()
1565 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1566 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1567 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1568 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1569 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1570 {
1571 if self.sqs_state.input_focus == InputFocus::Filter {
1572 self.apply_filter_operation(|f| f.push(c));
1573 }
1574 } else if self.current_service == Service::Ec2Instances
1575 && self.ec2_state.current_instance.is_some()
1576 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1577 {
1578 crate::ec2::actions::filter_char_push(self, c);
1579 } else if self.current_service == Service::CloudWatchLogGroups {
1580 if self.log_groups_state.input_focus == InputFocus::Filter {
1581 self.apply_filter_operation(|f| f.push(c));
1582 }
1583 } else {
1584 self.apply_filter_operation(|f| f.push(c));
1585 }
1586 } else if self.mode == Mode::EventFilterInput {
1587 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1588 self.log_groups_state.event_filter.push(c);
1589 } else if c.is_ascii_digit() {
1590 self.log_groups_state.relative_amount.push(c);
1591 }
1592 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1593 self.page_input.push(c);
1594 }
1595 }
1596 Action::FilterBackspace => {
1597 if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1598 self.service_picker.filter.pop();
1599 self.service_picker.selected = 0;
1600 } else if self.mode == Mode::TabPicker {
1601 self.tab_filter.pop();
1602 self.tab_picker_selected = 0;
1603 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1604 self.region_filter.pop();
1605 self.region_picker_selected = 0;
1606 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1607 self.profile_filter.pop();
1608 self.profile_picker_selected = 0;
1609 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1610 self.session_filter.pop();
1611 self.session_picker_selected = 0;
1612 } else if self.mode == Mode::InsightsInput {
1613 match self.insights_state.insights.insights_focus {
1614 InsightsFocus::Query => {
1615 self.insights_state.insights.query_text.pop();
1616 }
1617 InsightsFocus::LogGroupSearch => {
1618 self.insights_state.insights.log_group_search.pop();
1619 if !self.insights_state.insights.log_group_search.is_empty() {
1621 self.insights_state.insights.log_group_matches = self
1622 .log_groups_state
1623 .log_groups
1624 .items
1625 .iter()
1626 .filter(|g| {
1627 g.name.to_lowercase().contains(
1628 &self
1629 .insights_state
1630 .insights
1631 .log_group_search
1632 .to_lowercase(),
1633 )
1634 })
1635 .take(50)
1636 .map(|g| g.name.clone())
1637 .collect();
1638 self.insights_state.insights.show_dropdown = true;
1639 } else {
1640 self.insights_state.insights.log_group_matches.clear();
1641 self.insights_state.insights.show_dropdown = false;
1642 }
1643 }
1644 _ => {}
1645 }
1646 } else if self.mode == Mode::FilterInput {
1647 if self.current_service == Service::CloudFormationStacks {
1649 if self.cfn_state.current_stack.is_some()
1650 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1651 {
1652 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1653 self.cfn_state.parameters.filter.pop();
1654 self.cfn_state.parameters.selected = 0;
1655 }
1656 } else if self.cfn_state.current_stack.is_some()
1657 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1658 {
1659 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1660 self.cfn_state.outputs.filter.pop();
1661 self.cfn_state.outputs.selected = 0;
1662 }
1663 } else if self.cfn_state.current_stack.is_some()
1664 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1665 {
1666 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1667 self.cfn_state.resources.filter.pop();
1668 self.cfn_state.resources.selected = 0;
1669 }
1670 } else if self.cfn_state.input_focus == InputFocus::Filter {
1671 self.apply_filter_operation(|f| {
1672 f.pop();
1673 });
1674 }
1675 } else if self.current_service == Service::Ec2Instances
1676 && self.ec2_state.current_instance.is_some()
1677 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1678 {
1679 crate::ec2::actions::filter_char_pop(self);
1680 } else if self.current_service == Service::CloudWatchLogGroups {
1681 if self.log_groups_state.input_focus == InputFocus::Filter {
1682 self.apply_filter_operation(|f| {
1683 f.pop();
1684 });
1685 }
1686 } else {
1687 self.apply_filter_operation(|f| {
1688 f.pop();
1689 });
1690 }
1691 } else if self.mode == Mode::EventFilterInput {
1692 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1693 self.log_groups_state.event_filter.pop();
1694 } else {
1695 self.log_groups_state.relative_amount.pop();
1696 }
1697 }
1698 }
1699 Action::DeleteWord => {
1700 let text = if self.mode == Mode::ServicePicker {
1701 &mut self.service_picker.filter
1702 } else if self.mode == Mode::InsightsInput {
1703 use crate::app::InsightsFocus;
1704 match self.insights_state.insights.insights_focus {
1705 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1706 InsightsFocus::LogGroupSearch => {
1707 &mut self.insights_state.insights.log_group_search
1708 }
1709 _ => return,
1710 }
1711 } else if self.mode == Mode::FilterInput {
1712 if let Some(filter) = self.get_active_filter_mut() {
1713 filter
1714 } else {
1715 return;
1716 }
1717 } else if self.mode == Mode::EventFilterInput {
1718 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1719 &mut self.log_groups_state.event_filter
1720 } else {
1721 &mut self.log_groups_state.relative_amount
1722 }
1723 } else {
1724 return;
1725 };
1726
1727 if text.is_empty() {
1728 return;
1729 }
1730
1731 let mut chars: Vec<char> = text.chars().collect();
1732 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1733 chars.pop();
1734 }
1735 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1736 chars.pop();
1737 }
1738 *text = chars.into_iter().collect();
1739 }
1740 Action::WordLeft => {
1741 }
1743 Action::WordRight => {
1744 }
1746 Action::OpenColumnSelector => {
1747 if self.current_service == Service::CloudFormationStacks
1749 && crate::cfn::actions::block_column_selector(self)
1750 {
1751 return;
1752 }
1753
1754 if self.current_service == Service::IamUsers
1756 && self.iam_state.current_user.is_some()
1757 && self.iam_state.user_tab == UserTab::SecurityCredentials
1758 {
1759 return;
1760 }
1761
1762 if self.current_service == Service::IamRoles
1764 && self.iam_state.current_role.is_some()
1765 && (self.iam_state.role_tab == RoleTab::TrustRelationships
1766 || self.iam_state.role_tab == RoleTab::RevokeSessions)
1767 {
1768 return;
1769 }
1770
1771 if self.current_service == Service::SqsQueues
1773 && self.sqs_state.current_queue.is_some()
1774 && matches!(
1775 self.sqs_state.detail_tab,
1776 SqsQueueDetailTab::QueuePolicies
1777 | SqsQueueDetailTab::Monitoring
1778 | SqsQueueDetailTab::DeadLetterQueue
1779 | SqsQueueDetailTab::Encryption
1780 | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1781 )
1782 {
1783 return;
1784 }
1785
1786 if self.current_service == Service::Ec2Instances
1788 && crate::ec2::actions::block_column_selector(self)
1789 {
1790 return;
1791 }
1792
1793 if self.current_service == Service::CloudTrailEvents
1795 && self.cloudtrail_state.current_event.is_some()
1796 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1797 {
1798 return;
1799 }
1800
1801 if !self.page_input.is_empty() {
1803 if let Ok(page) = self.page_input.parse::<usize>() {
1804 self.go_to_page(page);
1805 }
1806 self.page_input.clear();
1807 } else {
1808 self.mode = Mode::ColumnSelector;
1809 self.column_selector_index = 0;
1810 }
1811 }
1812 Action::ToggleColumn => {
1813 if self.current_service == Service::S3Buckets
1814 && self.s3_state.current_bucket.is_none()
1815 {
1816 crate::s3::actions::toggle_column(self);
1817 } else if self.current_service == Service::CloudWatchAlarms {
1818 let idx = self.column_selector_index;
1822 if (1..=16).contains(&idx) {
1823 if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1825 Self::toggle_column_visibility(
1826 &mut self.cw_alarm_visible_column_ids,
1827 &self.cw_alarm_column_ids,
1828 *col,
1829 );
1830 }
1831 } else if idx == 19 {
1832 self.alarms_state.view_as = AlarmViewMode::Table;
1833 } else if idx == 20 {
1834 self.alarms_state.view_as = AlarmViewMode::Cards;
1835 } else if idx == 23 {
1836 self.alarms_state.table.page_size = PageSize::Ten;
1837 } else if idx == 24 {
1838 self.alarms_state.table.page_size = PageSize::TwentyFive;
1839 } else if idx == 25 {
1840 self.alarms_state.table.page_size = PageSize::Fifty;
1841 } else if idx == 26 {
1842 self.alarms_state.table.page_size = PageSize::OneHundred;
1843 } else if idx == 29 {
1844 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1845 }
1846 } else if self.current_service == Service::CloudTrailEvents {
1847 if self.cloudtrail_state.current_event.is_some()
1848 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
1849 {
1850 let idx = self.column_selector_index;
1852 if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
1853 if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
1854 Self::toggle_column_visibility(
1855 &mut self.cloudtrail_resource_visible_column_ids,
1856 &self.cloudtrail_resource_column_ids,
1857 *col,
1858 );
1859 }
1860 }
1861 } else {
1862 let idx = self.column_selector_index;
1864 if (1..=14).contains(&idx) {
1865 if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
1866 Self::toggle_column_visibility(
1867 &mut self.cloudtrail_event_visible_column_ids,
1868 &self.cloudtrail_event_column_ids,
1869 *col,
1870 );
1871 }
1872 } else if idx == 17 {
1873 self.cloudtrail_state.table.page_size = PageSize::Ten;
1874 self.cloudtrail_state.table.snap_to_page();
1875 } else if idx == 18 {
1876 self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
1877 self.cloudtrail_state.table.snap_to_page();
1878 } else if idx == 19 {
1879 self.cloudtrail_state.table.page_size = PageSize::Fifty;
1880 self.cloudtrail_state.table.snap_to_page();
1881 } else if idx == 20 {
1882 self.cloudtrail_state.table.page_size = PageSize::OneHundred;
1883 self.cloudtrail_state.table.snap_to_page();
1884 }
1885 }
1886 } else if self.current_service == Service::ApiGatewayApis {
1887 crate::apig::actions::toggle_column(self);
1888 } else if self.current_service == Service::EcrRepositories {
1889 crate::ecr::actions::toggle_column(self);
1890 } else if self.current_service == Service::KmsKeys {
1891 crate::kms::actions::toggle_column(self);
1892 } else if self.current_service == Service::EfsFileSystems {
1893 crate::efs::actions::toggle_column(self);
1894 } else if self.current_service == Service::FsxFileSystems {
1895 crate::fsx::actions::toggle_column(self);
1896 } else if self.current_service == Service::Ec2Instances {
1897 crate::ec2::actions::toggle_column(self);
1898 } else if self.current_service == Service::SqsQueues {
1899 crate::sqs::actions::toggle_column(self);
1900 } else if self.current_service == Service::LambdaFunctions {
1901 crate::lambda::functions::toggle_column(self);
1902 } else if self.current_service == Service::LambdaApplications {
1903 crate::lambda::applications::toggle_column(self);
1904 } else if self.view_mode == ViewMode::Events {
1905 if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
1906 {
1907 if let Some(pos) = self
1908 .cw_log_event_visible_column_ids
1909 .iter()
1910 .position(|c| c == col)
1911 {
1912 self.cw_log_event_visible_column_ids.remove(pos);
1913 } else {
1914 self.cw_log_event_visible_column_ids.push(*col);
1915 }
1916 }
1917 } else if self.view_mode == ViewMode::Detail {
1918 let idx = self.column_selector_index;
1919 if self.log_groups_state.detail_tab == DetailTab::Tags {
1920 if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
1922 if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
1923 if let Some(pos) = self
1924 .cw_log_tag_visible_column_ids
1925 .iter()
1926 .position(|c| c == col)
1927 {
1928 self.cw_log_tag_visible_column_ids.remove(pos);
1929 } else {
1930 self.cw_log_tag_visible_column_ids.push(*col);
1931 }
1932 }
1933 } else if idx == self.cw_log_tag_column_ids.len() + 3 {
1934 self.log_groups_state.tags.page_size = PageSize::Ten;
1935 } else if idx == self.cw_log_tag_column_ids.len() + 4 {
1936 self.log_groups_state.tags.page_size = PageSize::TwentyFive;
1937 } else if idx == self.cw_log_tag_column_ids.len() + 5 {
1938 self.log_groups_state.tags.page_size = PageSize::Fifty;
1939 } else if idx == self.cw_log_tag_column_ids.len() + 6 {
1940 self.log_groups_state.tags.page_size = PageSize::OneHundred;
1941 }
1942 } else {
1943 if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
1945 if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
1946 if let Some(pos) = self
1947 .cw_log_stream_visible_column_ids
1948 .iter()
1949 .position(|c| c == col)
1950 {
1951 self.cw_log_stream_visible_column_ids.remove(pos);
1952 } else {
1953 self.cw_log_stream_visible_column_ids.push(*col);
1954 }
1955 }
1956 } else if idx == self.cw_log_stream_column_ids.len() + 3 {
1957 self.log_groups_state.stream_page_size = 10;
1958 self.log_groups_state.stream_current_page = 0;
1959 } else if idx == self.cw_log_stream_column_ids.len() + 4 {
1960 self.log_groups_state.stream_page_size = 25;
1961 self.log_groups_state.stream_current_page = 0;
1962 } else if idx == self.cw_log_stream_column_ids.len() + 5 {
1963 self.log_groups_state.stream_page_size = 50;
1964 self.log_groups_state.stream_current_page = 0;
1965 } else if idx == self.cw_log_stream_column_ids.len() + 6 {
1966 self.log_groups_state.stream_page_size = 100;
1967 self.log_groups_state.stream_current_page = 0;
1968 }
1969 }
1970 } else if self.current_service == Service::CloudFormationStacks {
1971 crate::cfn::actions::toggle_column(self);
1972 } else if self.current_service == Service::IamUsers {
1973 crate::iam::actions::toggle_column_users(self);
1974 } else if self.current_service == Service::IamRoles {
1975 crate::iam::actions::toggle_column_roles(self);
1976 } else if self.current_service == Service::IamUserGroups {
1977 crate::iam::actions::toggle_column_groups(self);
1978 } else {
1979 let idx = self.column_selector_index;
1980 if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
1981 if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
1982 if let Some(pos) = self
1983 .cw_log_group_visible_column_ids
1984 .iter()
1985 .position(|c| c == col)
1986 {
1987 self.cw_log_group_visible_column_ids.remove(pos);
1988 } else {
1989 self.cw_log_group_visible_column_ids.push(*col);
1990 }
1991 }
1992 } else if idx == self.cw_log_group_column_ids.len() + 3 {
1993 self.log_groups_state.log_groups.page_size = PageSize::Ten;
1994 } else if idx == self.cw_log_group_column_ids.len() + 4 {
1995 self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
1996 } else if idx == self.cw_log_group_column_ids.len() + 5 {
1997 self.log_groups_state.log_groups.page_size = PageSize::Fifty;
1998 } else if idx == self.cw_log_group_column_ids.len() + 6 {
1999 self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2000 }
2001 }
2002 }
2003 Action::NextPreferences => {
2004 if self.current_service == Service::ApiGatewayApis {
2005 crate::apig::actions::next_preferences(self);
2006 } else if self.current_service == Service::CloudWatchAlarms {
2007 crate::cw::actions::alarms_next_preferences(self);
2008 } else if self.current_service == Service::EcrRepositories
2009 && self.ecr_state.current_repository.is_some()
2010 {
2011 crate::ecr::actions::next_preferences(self);
2012 } else if self.current_service == Service::KmsKeys {
2013 crate::kms::actions::next_preferences(self);
2014 } else if self.current_service == Service::EfsFileSystems {
2015 crate::efs::actions::next_preferences(self);
2016 } else if self.current_service == Service::FsxFileSystems {
2017 crate::fsx::actions::next_preferences(self);
2018 } else if self.current_service == Service::LambdaFunctions {
2019 crate::lambda::functions::next_preferences(self);
2020 } else if self.current_service == Service::LambdaApplications {
2021 crate::lambda::applications::next_preferences(self);
2022 } else if self.current_service == Service::CloudFormationStacks {
2023 crate::cfn::actions::next_preferences(self);
2024 } else if self.current_service == Service::CloudTrailEvents {
2025 if self.cloudtrail_state.current_event.is_some()
2026 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2027 {
2028 self.column_selector_index = 0;
2030 } else {
2031 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2032 if self.column_selector_index < page_size_idx {
2033 self.column_selector_index = page_size_idx;
2034 } else {
2035 self.column_selector_index = 0;
2036 }
2037 }
2038 } else if self.current_service == Service::Ec2Instances {
2039 let page_size_idx = self.ec2_column_ids.len() + 2;
2040 if self.column_selector_index < page_size_idx {
2041 self.column_selector_index = page_size_idx;
2042 } else {
2043 self.column_selector_index = 0;
2044 }
2045 } else if self.current_service == Service::IamUsers {
2046 crate::iam::actions::next_preferences_users(self);
2047 } else if self.current_service == Service::IamRoles {
2048 crate::iam::actions::next_preferences_roles(self);
2049 } else if self.current_service == Service::IamUserGroups {
2050 crate::iam::actions::next_preferences_groups(self);
2051 } else if self.current_service == Service::SqsQueues
2052 && self.sqs_state.current_queue.is_some()
2053 && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2054 {
2055 let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2057 if self.column_selector_index < page_size_idx {
2058 self.column_selector_index = page_size_idx;
2059 } else {
2060 self.column_selector_index = 0;
2061 }
2062 } else if self.current_service == Service::SqsQueues
2063 && self.sqs_state.current_queue.is_some()
2064 && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2065 {
2066 let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2068 if self.column_selector_index < page_size_idx {
2069 self.column_selector_index = page_size_idx;
2070 } else {
2071 self.column_selector_index = 0;
2072 }
2073 } else if self.current_service == Service::SqsQueues
2074 && self.sqs_state.current_queue.is_some()
2075 && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2076 {
2077 let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2079 if self.column_selector_index < page_size_idx {
2080 self.column_selector_index = page_size_idx;
2081 } else {
2082 self.column_selector_index = 0;
2083 }
2084 } else if self.current_service == Service::SqsQueues
2085 && self.sqs_state.current_queue.is_some()
2086 && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2087 {
2088 let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2090 if self.column_selector_index < page_size_idx {
2091 self.column_selector_index = page_size_idx;
2092 } else {
2093 self.column_selector_index = 0;
2094 }
2095 } else if self.current_service == Service::S3Buckets
2096 && self.s3_state.current_bucket.is_none()
2097 {
2098 crate::s3::actions::next_preferences(self);
2099 } else if self.current_service == Service::CloudWatchLogGroups {
2100 if self.view_mode == ViewMode::Events {
2101 } else if self.view_mode == ViewMode::Detail {
2103 if self.log_groups_state.detail_tab == DetailTab::Tags {
2104 cycle_preference_next(
2106 &mut self.column_selector_index,
2107 self.cw_log_tag_column_ids.len(),
2108 );
2109 } else {
2110 cycle_preference_next(
2112 &mut self.column_selector_index,
2113 self.cw_log_stream_column_ids.len(),
2114 );
2115 }
2116 } else {
2117 cycle_preference_next(
2119 &mut self.column_selector_index,
2120 self.cw_log_group_column_ids.len(),
2121 );
2122 }
2123 }
2124 }
2125 Action::PrevPreferences => {
2126 if self.current_service == Service::ApiGatewayApis {
2127 crate::apig::actions::prev_preferences(self);
2128 } else if self.current_service == Service::CloudWatchAlarms {
2129 crate::cw::actions::alarms_prev_preferences(self);
2130 } else if self.current_service == Service::EcrRepositories
2131 && self.ecr_state.current_repository.is_some()
2132 {
2133 crate::ecr::actions::prev_preferences(self);
2134 } else if self.current_service == Service::KmsKeys {
2135 crate::kms::actions::prev_preferences(self);
2136 } else if self.current_service == Service::EfsFileSystems {
2137 crate::efs::actions::prev_preferences(self);
2138 } else if self.current_service == Service::FsxFileSystems {
2139 crate::fsx::actions::prev_preferences(self);
2140 } else if self.current_service == Service::LambdaFunctions {
2141 crate::lambda::functions::prev_preferences(self);
2142 } else if self.current_service == Service::LambdaApplications {
2143 crate::lambda::applications::prev_preferences(self);
2144 } else if self.current_service == Service::CloudFormationStacks {
2145 crate::cfn::actions::prev_preferences(self);
2146 } else if self.current_service == Service::CloudTrailEvents {
2147 if self.cloudtrail_state.current_event.is_some()
2148 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2149 {
2150 self.column_selector_index = 0;
2152 } else {
2153 let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2154 if self.column_selector_index >= page_size_idx {
2155 self.column_selector_index = 0;
2156 } else {
2157 self.column_selector_index = page_size_idx;
2158 }
2159 }
2160 } else if self.current_service == Service::Ec2Instances {
2161 crate::ec2::actions::next_preferences(self);
2162 } else if self.current_service == Service::IamUsers {
2163 crate::iam::actions::prev_preferences_users(self);
2164 } else if self.current_service == Service::IamRoles {
2165 crate::iam::actions::prev_preferences_roles(self);
2166 } else if self.current_service == Service::IamUserGroups {
2167 crate::iam::actions::prev_preferences_groups(self);
2168 } else if self.current_service == Service::SqsQueues
2169 && self.sqs_state.current_queue.is_some()
2170 {
2171 let page_size_idx = match self.sqs_state.detail_tab {
2172 SqsQueueDetailTab::LambdaTriggers => {
2173 self.sqs_state.trigger_column_ids.len() + 2
2174 }
2175 SqsQueueDetailTab::EventBridgePipes => {
2176 self.sqs_state.pipe_column_ids.len() + 2
2177 }
2178 SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
2179 SqsQueueDetailTab::SnsSubscriptions => {
2180 self.sqs_state.subscription_column_ids.len() + 2
2181 }
2182 _ => 0,
2183 };
2184 if page_size_idx > 0 {
2185 if self.column_selector_index >= page_size_idx {
2186 self.column_selector_index = 0;
2187 } else {
2188 self.column_selector_index = page_size_idx;
2189 }
2190 }
2191 } else if self.current_service == Service::S3Buckets
2192 && self.s3_state.current_bucket.is_none()
2193 {
2194 crate::s3::actions::prev_preferences(self);
2195 } else if self.current_service == Service::CloudWatchLogGroups {
2196 if self.view_mode == ViewMode::Events {
2197 } else if self.view_mode == ViewMode::Detail {
2199 if self.log_groups_state.detail_tab == DetailTab::Tags {
2200 cycle_preference_prev(
2202 &mut self.column_selector_index,
2203 self.cw_log_tag_column_ids.len(),
2204 );
2205 } else {
2206 cycle_preference_prev(
2208 &mut self.column_selector_index,
2209 self.cw_log_stream_column_ids.len(),
2210 );
2211 }
2212 } else {
2213 cycle_preference_prev(
2215 &mut self.column_selector_index,
2216 self.cw_log_group_column_ids.len(),
2217 );
2218 }
2219 }
2220 }
2221 Action::CloseColumnSelector => {
2222 self.mode = Mode::Normal;
2223 self.preference_section = Preferences::Columns;
2224 }
2225 Action::NextDetailTab => {
2226 if self.current_service == Service::CloudTrailEvents
2227 && self.cloudtrail_state.current_event.is_some()
2228 {
2229 crate::cloudtrail::actions::next_detail_tab(self);
2230 } else if self.current_service == Service::ApiGatewayApis
2231 && self.apig_state.current_api.is_some()
2232 {
2233 crate::apig::actions::next_detail_tab(self);
2234 } else if self.current_service == Service::SqsQueues
2235 && self.sqs_state.current_queue.is_some()
2236 {
2237 crate::sqs::actions::next_detail_tab(self);
2238 } else if self.current_service == Service::Ec2Instances
2239 && self.ec2_state.current_instance.is_some()
2240 {
2241 crate::ec2::actions::next_detail_tab(self);
2242 } else if self.current_service == Service::LambdaApplications
2243 && self.lambda_application_state.current_application.is_some()
2244 {
2245 crate::lambda::applications::next_detail_tab(self);
2246 } else if self.current_service == Service::IamRoles
2247 && self.iam_state.current_role.is_some()
2248 {
2249 crate::iam::actions::next_detail_tab_roles(self);
2250 } else if self.current_service == Service::IamUsers
2251 && self.iam_state.current_user.is_some()
2252 {
2253 crate::iam::actions::next_detail_tab_users(self);
2254 } else if self.current_service == Service::IamUserGroups
2255 && self.iam_state.current_group.is_some()
2256 {
2257 crate::iam::actions::next_detail_tab_groups(self);
2258 } else if self.current_service == Service::CloudWatchAlarms {
2259 crate::cw::actions::alarms_next_detail_tab(self);
2260 } else if self.view_mode == ViewMode::Detail {
2261 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
2262 if self.log_groups_state.detail_tab == DetailTab::Tags {
2263 self.log_groups_state.tags.loading = true;
2264 }
2265 } else if self.current_service == Service::S3Buckets {
2266 crate::s3::actions::next_detail_tab(self);
2267 } else if self.current_service == Service::EcrRepositories
2268 && self.ecr_state.current_repository.is_none()
2269 {
2270 crate::ecr::actions::next_detail_tab(self);
2271 } else if self.current_service == Service::KmsKeys {
2272 crate::kms::actions::next_detail_tab(self);
2273 } else if self.current_service == Service::EfsFileSystems
2274 && self.efs_state.current_file_system.is_some()
2275 {
2276 crate::efs::actions::next_detail_tab(self);
2277 } else if self.current_service == Service::FsxFileSystems
2278 && self.fsx_state.current_file_system.is_some()
2279 {
2280 crate::fsx::actions::next_detail_tab(self);
2281 } else if self.current_service == Service::LambdaFunctions
2282 && self.lambda_state.current_function.is_some()
2283 {
2284 crate::lambda::functions::next_detail_tab(self);
2285 } else if self.current_service == Service::CloudFormationStacks
2286 && self.cfn_state.current_stack.is_some()
2287 {
2288 crate::cfn::actions::next_detail_tab(self);
2289 }
2290 }
2291 Action::PrevDetailTab => {
2292 if self.current_service == Service::CloudTrailEvents
2293 && self.cloudtrail_state.current_event.is_some()
2294 {
2295 crate::cloudtrail::actions::prev_detail_tab(self);
2296 } else if self.current_service == Service::ApiGatewayApis
2297 && self.apig_state.current_api.is_some()
2298 {
2299 crate::apig::actions::prev_detail_tab(self);
2300 } else if self.current_service == Service::SqsQueues
2301 && self.sqs_state.current_queue.is_some()
2302 {
2303 crate::sqs::actions::prev_detail_tab(self);
2304 } else if self.current_service == Service::Ec2Instances
2305 && self.ec2_state.current_instance.is_some()
2306 {
2307 crate::ec2::actions::prev_detail_tab(self);
2308 } else if self.current_service == Service::LambdaApplications
2309 && self.lambda_application_state.current_application.is_some()
2310 {
2311 crate::lambda::applications::prev_detail_tab(self);
2312 } else if self.current_service == Service::IamRoles
2313 && self.iam_state.current_role.is_some()
2314 {
2315 crate::iam::actions::prev_detail_tab_roles(self);
2316 } else if self.current_service == Service::IamUsers
2317 && self.iam_state.current_user.is_some()
2318 {
2319 crate::iam::actions::prev_detail_tab_users(self);
2320 } else if self.current_service == Service::IamUserGroups
2321 && self.iam_state.current_group.is_some()
2322 {
2323 crate::iam::actions::prev_detail_tab_groups(self);
2324 } else if self.current_service == Service::CloudWatchAlarms {
2325 crate::cw::actions::alarms_prev_detail_tab(self);
2326 } else if self.view_mode == ViewMode::Detail {
2327 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
2328 } else if self.current_service == Service::S3Buckets {
2329 crate::s3::actions::prev_detail_tab(self);
2330 } else if self.current_service == Service::EcrRepositories
2331 && self.ecr_state.current_repository.is_none()
2332 {
2333 crate::ecr::actions::prev_detail_tab(self);
2334 } else if self.current_service == Service::KmsKeys {
2335 crate::kms::actions::prev_detail_tab(self);
2336 } else if self.current_service == Service::EfsFileSystems
2337 && self.efs_state.current_file_system.is_some()
2338 {
2339 crate::efs::actions::prev_detail_tab(self);
2340 } else if self.current_service == Service::FsxFileSystems
2341 && self.fsx_state.current_file_system.is_some()
2342 {
2343 crate::fsx::actions::prev_detail_tab(self);
2344 } else if self.current_service == Service::LambdaFunctions
2345 && self.lambda_state.current_function.is_some()
2346 {
2347 crate::lambda::functions::prev_detail_tab(self);
2348 } else if self.current_service == Service::CloudFormationStacks
2349 && self.cfn_state.current_stack.is_some()
2350 {
2351 crate::cfn::actions::prev_detail_tab(self);
2352 }
2353 }
2354 Action::StartFilter => {
2355 if !self.service_selected && self.tabs.is_empty() {
2357 return;
2358 }
2359
2360 if self.current_service == Service::CloudWatchInsights {
2361 self.mode = Mode::InsightsInput;
2362 } else if self.current_service == Service::CloudWatchAlarms {
2363 self.mode = Mode::FilterInput;
2364 } else if self.current_service == Service::CloudTrailEvents {
2365 self.mode = Mode::FilterInput;
2366 self.cloudtrail_state.input_focus = InputFocus::Filter;
2367 } else if self.current_service == Service::S3Buckets {
2368 self.mode = Mode::FilterInput;
2369 crate::s3::actions::start_filter(self);
2370 } else if self.current_service == Service::ApiGatewayApis
2371 || self.current_service == Service::EcrRepositories
2372 || self.current_service == Service::KmsKeys
2373 || self.current_service == Service::EfsFileSystems
2374 || self.current_service == Service::FsxFileSystems
2375 || self.current_service == Service::IamUsers
2376 || self.current_service == Service::IamUserGroups
2377 {
2378 self.mode = Mode::FilterInput;
2379 if self.current_service == Service::ApiGatewayApis {
2380 crate::apig::actions::start_filter(self);
2381 } else if self.current_service == Service::EcrRepositories
2382 && self.ecr_state.current_repository.is_none()
2383 {
2384 self.ecr_state.input_focus = InputFocus::Filter;
2385 } else if self.current_service == Service::KmsKeys {
2386 crate::kms::actions::start_filter(self);
2387 } else if self.current_service == Service::EfsFileSystems {
2388 crate::efs::actions::start_filter(self);
2389 } else if self.current_service == Service::FsxFileSystems {
2390 crate::fsx::actions::start_filter(self);
2391 }
2392 } else if self.current_service == Service::LambdaFunctions {
2393 self.mode = Mode::FilterInput;
2394 crate::lambda::functions::start_filter(self);
2395 } else if self.current_service == Service::LambdaApplications {
2396 self.mode = Mode::FilterInput;
2397 crate::lambda::applications::start_filter(self);
2398 } else if self.current_service == Service::IamRoles {
2399 self.mode = Mode::FilterInput;
2400 } else if self.current_service == Service::CloudFormationStacks {
2401 self.mode = Mode::FilterInput;
2402 crate::cfn::actions::start_filter(self);
2403 } else if self.current_service == Service::SqsQueues {
2404 self.mode = Mode::FilterInput;
2405 self.sqs_state.input_focus = InputFocus::Filter;
2406 } else if self.view_mode == ViewMode::List
2407 || (self.view_mode == ViewMode::Detail
2408 && (self.log_groups_state.detail_tab == DetailTab::LogStreams
2409 || self.log_groups_state.detail_tab == DetailTab::Tags))
2410 {
2411 self.mode = Mode::FilterInput;
2412 self.log_groups_state.filter_mode = true;
2413 self.log_groups_state.input_focus = InputFocus::Filter;
2414 } else if self.view_mode == ViewMode::Events {
2415 self.mode = Mode::EventFilterInput;
2416 }
2417 }
2418 Action::StartEventFilter => {
2419 if self.current_service == Service::CloudWatchInsights {
2420 self.mode = Mode::InsightsInput;
2421 } else if self.view_mode == ViewMode::List {
2422 self.mode = Mode::FilterInput;
2423 self.log_groups_state.filter_mode = true;
2424 self.log_groups_state.input_focus = InputFocus::Filter;
2425 } else if self.view_mode == ViewMode::Events {
2426 self.mode = Mode::EventFilterInput;
2427 } else if self.view_mode == ViewMode::Detail
2428 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2429 {
2430 self.mode = Mode::FilterInput;
2431 self.log_groups_state.filter_mode = true;
2432 self.log_groups_state.input_focus = InputFocus::Filter;
2433 }
2434 }
2435 Action::NextFilterFocus => {
2436 if self.current_service == Service::CloudTrailEvents
2437 && self.cloudtrail_state.current_event.is_some()
2438 {
2439 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
2440 } else if self.mode == Mode::FilterInput
2441 && self.current_service == Service::S3Buckets
2442 {
2443 crate::s3::actions::next_filter_focus(self);
2444 } else if self.mode == Mode::FilterInput
2445 && self.current_service == Service::Ec2Instances
2446 {
2447 crate::ec2::actions::next_filter_focus(self);
2448 } else if self.mode == Mode::FilterInput
2449 && self.current_service == Service::LambdaApplications
2450 {
2451 crate::lambda::applications::next_filter_focus(self);
2452 } else if self.mode == Mode::FilterInput
2453 && self.current_service == Service::IamRoles
2454 {
2455 crate::iam::actions::next_filter_focus_roles(self);
2456 } else if self.mode == Mode::FilterInput
2457 && self.current_service == Service::IamUsers
2458 && self.iam_state.current_user.is_some()
2459 {
2460 crate::iam::actions::next_filter_focus_users(self);
2461 } else if self.mode == Mode::FilterInput
2462 && self.current_service == Service::IamUserGroups
2463 {
2464 crate::iam::actions::next_filter_focus_groups(self);
2465 } else if self.mode == Mode::InsightsInput {
2466 use crate::app::InsightsFocus;
2467 self.insights_state.insights.insights_focus =
2468 match self.insights_state.insights.insights_focus {
2469 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
2470 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
2471 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
2472 InsightsFocus::Query => InsightsFocus::QueryLanguage,
2473 };
2474 } else if self.mode == Mode::FilterInput
2475 && self.current_service == Service::CloudFormationStacks
2476 {
2477 crate::cfn::actions::next_filter_focus(self);
2478 } else if self.mode == Mode::FilterInput
2479 && self.current_service == Service::SqsQueues
2480 {
2481 crate::sqs::actions::next_filter_focus(self);
2482 } else if self.mode == Mode::FilterInput
2483 && self.current_service == Service::CloudWatchLogGroups
2484 {
2485 crate::cw::actions::logs_next_filter_focus(self);
2486 } else if self.mode == Mode::EventFilterInput {
2487 crate::cw::actions::logs_next_event_filter_focus(self);
2488 } else if self.mode == Mode::FilterInput
2489 && self.current_service == Service::CloudWatchAlarms
2490 {
2491 crate::cw::actions::alarms_next_filter_focus(self);
2492 } else if self.mode == Mode::FilterInput
2493 && self.current_service == Service::CloudTrailEvents
2494 {
2495 crate::cloudtrail::actions::next_filter_focus(self);
2496 } else if self.mode == Mode::FilterInput
2497 && self.current_service == Service::ApiGatewayApis
2498 {
2499 crate::apig::actions::next_filter_focus(self);
2500 } else if self.mode == Mode::FilterInput
2501 && self.current_service == Service::EcrRepositories
2502 && self.ecr_state.current_repository.is_none()
2503 {
2504 crate::ecr::actions::next_filter_focus(self);
2505 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys
2506 {
2507 crate::kms::actions::next_filter_focus(self);
2508 } else if self.mode == Mode::FilterInput
2509 && self.current_service == Service::EfsFileSystems
2510 {
2511 crate::efs::actions::next_filter_focus(self);
2512 } else if self.mode == Mode::FilterInput
2513 && self.current_service == Service::FsxFileSystems
2514 {
2515 crate::fsx::actions::next_filter_focus(self);
2516 } else if self.mode == Mode::FilterInput
2517 && self.current_service == Service::LambdaFunctions
2518 {
2519 crate::lambda::functions::next_filter_focus(self);
2520 }
2521 }
2522 Action::PrevFilterFocus => {
2523 if self.current_service == Service::CloudTrailEvents
2524 && self.cloudtrail_state.current_event.is_some()
2525 {
2526 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
2527 } else if self.mode == Mode::FilterInput
2528 && self.current_service == Service::ApiGatewayApis
2529 {
2530 crate::apig::actions::prev_filter_focus(self);
2531 } else if self.mode == Mode::FilterInput
2532 && self.current_service == Service::S3Buckets
2533 {
2534 crate::s3::actions::prev_filter_focus(self);
2535 } else if self.mode == Mode::FilterInput
2536 && self.current_service == Service::Ec2Instances
2537 {
2538 crate::ec2::actions::prev_filter_focus(self);
2539 } else if self.mode == Mode::FilterInput
2540 && self.current_service == Service::LambdaApplications
2541 {
2542 crate::lambda::applications::prev_filter_focus(self);
2543 } else if self.mode == Mode::FilterInput
2544 && self.current_service == Service::CloudFormationStacks
2545 {
2546 crate::cfn::actions::prev_filter_focus(self);
2547 } else if self.mode == Mode::FilterInput
2548 && self.current_service == Service::SqsQueues
2549 {
2550 crate::sqs::actions::prev_filter_focus(self);
2551 } else if self.mode == Mode::FilterInput
2552 && self.current_service == Service::IamRoles
2553 {
2554 crate::iam::actions::prev_filter_focus_roles(self);
2555 } else if self.mode == Mode::FilterInput
2556 && self.current_service == Service::IamUsers
2557 && self.iam_state.current_user.is_some()
2558 {
2559 crate::iam::actions::prev_filter_focus_users(self);
2560 } else if self.mode == Mode::FilterInput
2561 && self.current_service == Service::IamUserGroups
2562 {
2563 crate::iam::actions::prev_filter_focus_groups(self);
2564 } else if self.mode == Mode::FilterInput
2565 && self.current_service == Service::CloudWatchLogGroups
2566 {
2567 crate::cw::actions::logs_prev_filter_focus(self);
2568 } else if self.mode == Mode::EventFilterInput {
2569 crate::cw::actions::logs_prev_event_filter_focus(self);
2570 } else if self.mode == Mode::FilterInput
2571 && self.current_service == Service::IamRoles
2572 && self.iam_state.current_role.is_some()
2573 {
2574 crate::iam::actions::prev_filter_focus_roles(self);
2575 } else if self.mode == Mode::FilterInput
2576 && self.current_service == Service::CloudWatchAlarms
2577 {
2578 crate::cw::actions::alarms_prev_filter_focus(self);
2579 } else if self.mode == Mode::FilterInput
2580 && self.current_service == Service::CloudTrailEvents
2581 {
2582 crate::cloudtrail::actions::prev_filter_focus(self);
2583 } else if self.mode == Mode::FilterInput
2584 && self.current_service == Service::EcrRepositories
2585 && self.ecr_state.current_repository.is_none()
2586 {
2587 crate::ecr::actions::prev_filter_focus(self);
2588 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys
2589 {
2590 crate::kms::actions::prev_filter_focus(self);
2591 } else if self.mode == Mode::FilterInput
2592 && self.current_service == Service::EfsFileSystems
2593 {
2594 crate::efs::actions::prev_filter_focus(self);
2595 } else if self.mode == Mode::FilterInput
2596 && self.current_service == Service::FsxFileSystems
2597 {
2598 crate::fsx::actions::prev_filter_focus(self);
2599 } else if self.mode == Mode::FilterInput
2600 && self.current_service == Service::LambdaFunctions
2601 {
2602 crate::lambda::functions::prev_filter_focus(self);
2603 }
2604 }
2605 Action::ToggleFilterCheckbox => {
2606 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
2607 crate::ec2::actions::toggle_filter_checkbox(self);
2608 } else if self.mode == Mode::InsightsInput
2609 || (self.mode == Mode::Normal
2610 && self.current_service == Service::CloudWatchInsights)
2611 {
2612 use crate::app::InsightsFocus;
2613 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2614 && self.insights_state.insights.show_dropdown
2615 && !self.insights_state.insights.log_group_matches.is_empty()
2616 {
2617 let selected_idx = self.insights_state.insights.dropdown_selected;
2618 if let Some(group_name) = self
2619 .insights_state
2620 .insights
2621 .log_group_matches
2622 .get(selected_idx)
2623 {
2624 let group_name = group_name.clone();
2625 if let Some(pos) = self
2626 .insights_state
2627 .insights
2628 .selected_log_groups
2629 .iter()
2630 .position(|g| g == &group_name)
2631 {
2632 self.insights_state.insights.selected_log_groups.remove(pos);
2633 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2634 self.insights_state
2635 .insights
2636 .selected_log_groups
2637 .push(group_name);
2638 }
2639 }
2640 }
2641 } else if self.mode == Mode::FilterInput
2642 && self.current_service == Service::CloudFormationStacks
2643 {
2644 crate::cfn::actions::toggle_filter_checkbox(self);
2645 } else if self.mode == Mode::FilterInput
2646 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2647 {
2648 match self.log_groups_state.input_focus {
2649 InputFocus::Checkbox("ExactMatch") => {
2650 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2651 }
2652 InputFocus::Checkbox("ShowExpired") => {
2653 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2654 }
2655 _ => {}
2656 }
2657 } else if self.mode == Mode::EventFilterInput
2658 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2659 {
2660 self.log_groups_state.relative_unit =
2661 self.log_groups_state.relative_unit.next();
2662 }
2663 }
2664 Action::CycleSortColumn => {
2665 if self.view_mode == ViewMode::Detail
2666 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2667 {
2668 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2669 StreamSort::Name => StreamSort::CreationTime,
2670 StreamSort::CreationTime => StreamSort::LastEventTime,
2671 StreamSort::LastEventTime => StreamSort::Name,
2672 };
2673 }
2674 }
2675 Action::ToggleSortDirection => {
2676 if self.view_mode == ViewMode::Detail
2677 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2678 {
2679 self.log_groups_state.stream_sort_desc =
2680 !self.log_groups_state.stream_sort_desc;
2681 }
2682 }
2683 Action::ScrollUp => {
2684 if self.mode == Mode::ErrorModal {
2685 self.error_scroll = self.error_scroll.saturating_sub(1);
2686 } else if self.current_service == Service::CloudTrailEvents
2687 && self.cloudtrail_state.current_event.is_some()
2688 {
2689 self.cloudtrail_state.event_json_scroll =
2690 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
2691 } else if self.current_service == Service::LambdaFunctions {
2692 crate::lambda::functions::scroll_up(self);
2693 } else if self.current_service == Service::Ec2Instances {
2694 crate::ec2::actions::scroll_up(self);
2695 } else if self.current_service == Service::SqsQueues
2696 && self.sqs_state.current_queue.is_some()
2697 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2698 && !self.sqs_state.is_metrics_loading()
2699 {
2700 self.sqs_state.set_monitoring_scroll(
2701 self.sqs_state.monitoring_scroll().saturating_sub(1),
2702 );
2703 } else if self.current_service == Service::EfsFileSystems
2704 && self.efs_state.current_file_system.is_some()
2705 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
2706 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
2707 && !self.efs_state.is_metrics_loading()
2708 {
2709 crate::efs::actions::scroll_up_detail(self);
2710 } else if self.view_mode == ViewMode::PolicyView {
2711 crate::iam::actions::scroll_up_policy_view(self);
2712 } else if self.current_service == Service::IamRoles
2713 && self.iam_state.current_role.is_some()
2714 && self.iam_state.role_tab == RoleTab::TrustRelationships
2715 {
2716 crate::iam::actions::scroll_up_trust_policy(self);
2717 } else if self.view_mode == ViewMode::Events {
2718 if self.log_groups_state.event_scroll_offset == 0
2719 && self.log_groups_state.has_older_events
2720 {
2721 self.log_groups_state.loading = true;
2722 } else {
2723 self.log_groups_state.event_scroll_offset =
2724 self.log_groups_state.event_scroll_offset.saturating_sub(1);
2725 }
2726 } else if self.view_mode == ViewMode::InsightsResults {
2727 self.insights_state.insights.results_selected = self
2728 .insights_state
2729 .insights
2730 .results_selected
2731 .saturating_sub(1);
2732 } else if self.view_mode == ViewMode::Detail {
2733 self.log_groups_state.selected_stream =
2734 self.log_groups_state.selected_stream.saturating_sub(1);
2735 self.log_groups_state.expanded_stream = None;
2736 } else if self.view_mode == ViewMode::List
2737 && self.current_service == Service::CloudWatchLogGroups
2738 {
2739 self.log_groups_state.log_groups.selected =
2740 self.log_groups_state.log_groups.selected.saturating_sub(1);
2741 self.log_groups_state.log_groups.snap_to_page();
2742 } else if self.current_service == Service::EcrRepositories {
2743 crate::ecr::actions::scroll_up(self);
2744 }
2745 }
2746 Action::ScrollDown => {
2747 if self.mode == Mode::ErrorModal {
2748 if let Some(error_msg) = &self.error_message {
2749 let lines = error_msg.lines().count();
2750 let max_scroll = lines.saturating_sub(1);
2751 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
2752 }
2753 } else if self.current_service == Service::CloudTrailEvents
2754 && self.cloudtrail_state.current_event.is_some()
2755 {
2756 if let Some(event) = &self.cloudtrail_state.current_event {
2757 let lines = event.cloud_trail_event_json.lines().count();
2758 let max_scroll = lines.saturating_sub(1);
2759 self.cloudtrail_state.event_json_scroll =
2760 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
2761 }
2762 } else if self.current_service == Service::SqsQueues
2763 && self.sqs_state.current_queue.is_some()
2764 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2765 {
2766 self.sqs_state
2767 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
2768 } else if self.current_service == Service::EfsFileSystems
2769 && self.efs_state.current_file_system.is_some()
2770 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
2771 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
2772 {
2773 crate::efs::actions::scroll_down_detail(self);
2774 } else if self.view_mode == ViewMode::PolicyView {
2775 crate::iam::actions::scroll_down_policy_view(self);
2776 } else if self.current_service == Service::IamRoles
2777 && self.iam_state.current_role.is_some()
2778 && self.iam_state.role_tab == RoleTab::TrustRelationships
2779 {
2780 crate::iam::actions::scroll_down_trust_policy(self);
2781 } else if self.view_mode == ViewMode::Events {
2782 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
2783 if self.log_groups_state.event_scroll_offset >= max_scroll {
2784 } else {
2786 self.log_groups_state.event_scroll_offset =
2787 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
2788 }
2789 } else if self.view_mode == ViewMode::InsightsResults {
2790 let max = self
2791 .insights_state
2792 .insights
2793 .query_results
2794 .len()
2795 .saturating_sub(1);
2796 self.insights_state.insights.results_selected =
2797 (self.insights_state.insights.results_selected + 1).min(max);
2798 } else if self.view_mode == ViewMode::Detail {
2799 let filtered_streams = filtered_log_streams(self);
2800 let max = filtered_streams.len().saturating_sub(1);
2801 self.log_groups_state.selected_stream =
2802 (self.log_groups_state.selected_stream + 1).min(max);
2803 } else if self.view_mode == ViewMode::List
2804 && self.current_service == Service::CloudWatchLogGroups
2805 {
2806 let filtered_groups = filtered_log_groups(self);
2807 self.log_groups_state
2808 .log_groups
2809 .next_item(filtered_groups.len());
2810 } else if self.current_service == Service::EcrRepositories {
2811 crate::ecr::actions::scroll_down(self);
2812 }
2813 }
2814
2815 Action::MeasureLatency => {
2816 if self.mode == Mode::RegionPicker {
2817 self.measure_region_latencies();
2818 }
2819 }
2820 Action::Refresh => {
2821 if self.mode == Mode::ProfilePicker {
2822 self.log_groups_state.loading = true;
2823 } else if self.mode == Mode::RegionPicker {
2824 } else if self.mode == Mode::SessionPicker {
2826 self.sessions = Session::list_all().unwrap_or_default();
2827 } else if self.current_service == Service::CloudWatchAlarms
2828 && self.alarms_state.current_alarm.is_some()
2829 {
2830 crate::cw::actions::alarms_refresh(self);
2831 } else if self.current_service == Service::CloudWatchInsights
2832 && !self.insights_state.insights.selected_log_groups.is_empty()
2833 {
2834 crate::cw::actions::insights_refresh(self);
2835 } else if self.current_service == Service::LambdaFunctions {
2836 crate::lambda::functions::refresh(self);
2837 } else if self.current_service == Service::LambdaApplications {
2838 crate::lambda::applications::refresh(self);
2839 } else if matches!(
2840 self.view_mode,
2841 ViewMode::Events | ViewMode::Detail | ViewMode::List
2842 ) {
2843 self.log_groups_state.loading = true;
2844 }
2845 }
2846 Action::Yank => {
2847 if self.mode == Mode::ErrorModal {
2848 if let Some(error) = &self.error_message {
2850 copy_to_clipboard(error);
2851 }
2852 } else if self.view_mode == ViewMode::Events {
2853 if let Some(event) = self
2854 .log_groups_state
2855 .log_events
2856 .get(self.log_groups_state.event_scroll_offset)
2857 {
2858 copy_to_clipboard(&event.message);
2859 }
2860 } else if self.current_service == Service::EcrRepositories {
2861 crate::ecr::actions::yank(self);
2862 } else if self.current_service == Service::KmsKeys {
2863 crate::kms::actions::yank(self);
2864 } else if self.current_service == Service::EfsFileSystems {
2865 crate::efs::actions::yank(self);
2866 } else if self.current_service == Service::FsxFileSystems {
2867 crate::fsx::actions::yank(self);
2868 } else if self.current_service == Service::LambdaFunctions {
2869 crate::lambda::functions::yank(self);
2870 } else if self.current_service == Service::CloudFormationStacks {
2871 crate::cfn::actions::yank(self);
2872 } else if self.current_service == Service::CloudTrailEvents
2873 && self.cloudtrail_state.current_event.is_some()
2874 {
2875 crate::cloudtrail::actions::yank(self);
2876 } else if self.current_service == Service::IamUsers {
2877 crate::iam::actions::yank_users(self);
2878 } else if self.current_service == Service::IamRoles {
2879 crate::iam::actions::yank_roles(self);
2880 } else if self.current_service == Service::IamUserGroups {
2881 crate::iam::actions::yank_groups(self);
2882 } else if self.current_service == Service::SqsQueues {
2883 if self.sqs_state.current_queue.is_some() {
2884 if let Some(queue) = self
2886 .sqs_state
2887 .queues
2888 .items
2889 .iter()
2890 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
2891 {
2892 let arn = format!(
2893 "arn:aws:sqs:{}:{}:{}",
2894 extract_region(&queue.url),
2895 extract_account_id(&queue.url),
2896 queue.name
2897 );
2898 copy_to_clipboard(&arn);
2899 }
2900 } else {
2901 let filtered_queues = filtered_queues(
2903 &self.sqs_state.queues.items,
2904 &self.sqs_state.queues.filter,
2905 );
2906 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
2907 let arn = format!(
2908 "arn:aws:sqs:{}:{}:{}",
2909 extract_region(&queue.url),
2910 extract_account_id(&queue.url),
2911 queue.name
2912 );
2913 copy_to_clipboard(&arn);
2914 }
2915 }
2916 } else if self.current_service == Service::ApiGatewayApis {
2917 crate::apig::actions::yank(self);
2918 }
2919 }
2920 Action::CopyToClipboard => {
2921 self.snapshot_requested = true;
2923 }
2924 Action::RetryLoad => {
2925 self.error_message = None;
2926 self.mode = Mode::Normal;
2927 self.log_groups_state.loading = true;
2928 }
2929 Action::ApplyFilter => {
2930 if self.mode == Mode::FilterInput
2931 && self.current_service == Service::SqsQueues
2932 && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
2933 {
2934 let regions = AwsRegion::all();
2935 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
2936 self.sqs_state.subscription_region_filter = region.code.to_string();
2937 }
2938 self.mode = Mode::Normal;
2939 } else if self.mode == Mode::FilterInput
2940 && self.current_service == Service::CloudTrailEvents
2941 {
2942 let filter_text = self.cloudtrail_state.table.filter.trim().to_string();
2944 self.cloudtrail_state.active_event_name_filter = if filter_text.is_empty() {
2945 None
2946 } else {
2947 Some(filter_text)
2948 };
2949 self.cloudtrail_state.table.items.clear();
2951 self.cloudtrail_state.table.next_token = None;
2952 self.cloudtrail_state.table.selected = 0;
2953 self.cloudtrail_state.table.scroll_offset = 0;
2954 self.cloudtrail_state.table.loading = true;
2955 self.mode = Mode::Normal;
2956 } else if self.mode == Mode::InsightsInput {
2957 use crate::app::InsightsFocus;
2958 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2959 && self.insights_state.insights.show_dropdown
2960 {
2961 let selected_idx = self.insights_state.insights.dropdown_selected;
2963 if let Some(group_name) = self
2964 .insights_state
2965 .insights
2966 .log_group_matches
2967 .get(selected_idx)
2968 .cloned()
2969 {
2970 if let Some(pos) = self
2971 .insights_state
2972 .insights
2973 .selected_log_groups
2974 .iter()
2975 .position(|g| g == &group_name)
2976 {
2977 self.insights_state.insights.selected_log_groups.remove(pos);
2978 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2979 self.insights_state
2980 .insights
2981 .selected_log_groups
2982 .push(group_name);
2983 }
2984 }
2985 self.insights_state.insights.show_dropdown = false;
2987 self.insights_state.insights.log_group_search.clear();
2988 self.insights_state.insights.log_group_matches.clear();
2989 self.mode = Mode::Normal;
2990 }
2991 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
2992 if let Ok(page) = self.page_input.parse::<usize>() {
2993 self.go_to_page(page);
2994 }
2995 self.page_input.clear();
2996 } else {
2997 self.mode = Mode::Normal;
2998 self.log_groups_state.filter_mode = false;
2999 }
3000 }
3001 Action::ToggleExactMatch => {
3002 if self.view_mode == ViewMode::Detail
3003 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3004 {
3005 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
3006 }
3007 }
3008 Action::ToggleShowExpired => {
3009 if self.view_mode == ViewMode::Detail
3010 && self.log_groups_state.detail_tab == DetailTab::LogStreams
3011 {
3012 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
3013 }
3014 }
3015 Action::GoBack => {
3016 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
3018 self.mode = Mode::Normal;
3019 self.service_picker.filter.clear();
3020 }
3021 else if self.current_service == Service::ApiGatewayApis
3023 && self.apig_state.current_api.is_some()
3024 {
3025 crate::apig::actions::go_back(self);
3026 }
3027 else if self.current_service == Service::S3Buckets
3029 && self.s3_state.current_bucket.is_some()
3030 {
3031 crate::s3::actions::go_back(self);
3032 }
3033 else if self.current_service == Service::EcrRepositories
3035 && self.ecr_state.current_repository.is_some()
3036 {
3037 crate::ecr::actions::go_back(self);
3038 } else if self.current_service == Service::EfsFileSystems
3039 && self.efs_state.current_file_system.is_some()
3040 {
3041 crate::efs::actions::go_back(self);
3042 } else if self.current_service == Service::FsxFileSystems
3043 && self.fsx_state.current_file_system.is_some()
3044 {
3045 crate::fsx::actions::go_back(self);
3046 }
3047 else if self.current_service == Service::Ec2Instances
3049 && self.ec2_state.current_instance.is_some()
3050 {
3051 crate::ec2::actions::go_back(self);
3052 }
3053 else if self.current_service == Service::SqsQueues
3055 && self.sqs_state.current_queue.is_some()
3056 {
3057 self.sqs_state.current_queue = None;
3058 }
3059 else if self.current_service == Service::IamUsers
3061 && self.iam_state.current_user.is_some()
3062 {
3063 crate::iam::actions::go_back_users(self);
3064 } else if self.current_service == Service::IamUserGroups
3065 && self.iam_state.current_group.is_some()
3066 {
3067 crate::iam::actions::go_back_groups(self);
3068 } else if self.current_service == Service::IamRoles {
3069 crate::iam::actions::go_back_roles(self);
3070 }
3071 else if self.current_service == Service::LambdaFunctions
3073 && (self.lambda_state.current_version.is_some()
3074 || self.lambda_state.current_alias.is_some()
3075 || self.lambda_state.current_function.is_some())
3076 {
3077 crate::lambda::functions::go_back(self);
3078 }
3079 else if self.current_service == Service::CloudWatchAlarms
3081 && self.alarms_state.current_alarm.is_some()
3082 {
3083 crate::cw::actions::alarms_go_back(self);
3084 }
3085 else if self.current_service == Service::LambdaApplications
3087 && self.lambda_application_state.current_application.is_some()
3088 {
3089 crate::lambda::applications::go_back(self);
3090 }
3091 else if self.current_service == Service::CloudFormationStacks
3093 && self.cfn_state.current_stack.is_some()
3094 {
3095 crate::cfn::actions::go_back(self);
3096 }
3097 else if self.current_service == Service::CloudTrailEvents
3099 && self.cloudtrail_state.current_event.is_some()
3100 {
3101 crate::cloudtrail::actions::go_back(self);
3102 }
3103 else if self.view_mode == ViewMode::InsightsResults {
3105 if self.insights_state.insights.expanded_result.is_some() {
3106 self.insights_state.insights.expanded_result = None;
3107 }
3108 }
3109 else if self.current_service == Service::CloudWatchAlarms {
3111 crate::cw::actions::alarms_go_back_list(self);
3112 }
3113 else if self.current_service == Service::Ec2Instances {
3115 crate::ec2::actions::collapse_row(self);
3116 }
3117 else if self.view_mode == ViewMode::Events {
3119 if self.log_groups_state.expanded_event.is_some() {
3120 self.log_groups_state.expanded_event = None;
3121 } else {
3122 self.view_mode = ViewMode::Detail;
3123 self.log_groups_state.event_filter.clear();
3124 }
3125 }
3126 else if self.view_mode == ViewMode::Detail {
3128 self.view_mode = ViewMode::List;
3129 self.log_groups_state.stream_filter.clear();
3130 self.log_groups_state.exact_match = false;
3131 self.log_groups_state.show_expired = false;
3132 }
3133 }
3134 Action::OpenInConsole | Action::OpenInBrowser => {
3135 let url = self.get_console_url();
3136 let _ = webbrowser::open(&url);
3137 }
3138 Action::ShowHelp => {
3139 self.mode = Mode::HelpModal;
3140 }
3141 Action::OpenRegionPicker => {
3142 self.region_filter.clear();
3143 self.region_picker_selected = 0;
3144 self.mode = Mode::RegionPicker;
3145 }
3146 Action::OpenProfilePicker => {
3147 self.profile_filter.clear();
3148 self.profile_picker_selected = 0;
3149 self.available_profiles = Self::load_aws_profiles();
3150 self.mode = Mode::ProfilePicker;
3151 }
3152 Action::OpenCalendar => {
3153 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
3154 self.calendar_selecting = CalendarField::StartDate;
3155 self.mode = Mode::CalendarPicker;
3156 }
3157 Action::CloseCalendar => {
3158 self.mode = Mode::Normal;
3159 self.calendar_date = None;
3160 }
3161 Action::CalendarPrevDay => {
3162 if let Some(date) = self.calendar_date {
3163 self.calendar_date = date.checked_sub(time::Duration::days(1));
3164 }
3165 }
3166 Action::CalendarNextDay => {
3167 if let Some(date) = self.calendar_date {
3168 self.calendar_date = date.checked_add(time::Duration::days(1));
3169 }
3170 }
3171 Action::CalendarPrevWeek => {
3172 if let Some(date) = self.calendar_date {
3173 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
3174 }
3175 }
3176 Action::CalendarNextWeek => {
3177 if let Some(date) = self.calendar_date {
3178 self.calendar_date = date.checked_add(time::Duration::weeks(1));
3179 }
3180 }
3181 Action::CalendarPrevMonth => {
3182 if let Some(date) = self.calendar_date {
3183 self.calendar_date = Some(if date.month() == time::Month::January {
3184 date.replace_month(time::Month::December)
3185 .unwrap()
3186 .replace_year(date.year() - 1)
3187 .unwrap()
3188 } else {
3189 date.replace_month(date.month().previous()).unwrap()
3190 });
3191 }
3192 }
3193 Action::CalendarNextMonth => {
3194 if let Some(date) = self.calendar_date {
3195 self.calendar_date = Some(if date.month() == time::Month::December {
3196 date.replace_month(time::Month::January)
3197 .unwrap()
3198 .replace_year(date.year() + 1)
3199 .unwrap()
3200 } else {
3201 date.replace_month(date.month().next()).unwrap()
3202 });
3203 }
3204 }
3205 Action::CalendarSelect => {
3206 if let Some(date) = self.calendar_date {
3207 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
3208 .unix_timestamp()
3209 * 1000;
3210 match self.calendar_selecting {
3211 CalendarField::StartDate => {
3212 self.log_groups_state.start_time = Some(timestamp);
3213 self.calendar_selecting = CalendarField::EndDate;
3214 }
3215 CalendarField::EndDate => {
3216 self.log_groups_state.end_time = Some(timestamp);
3217 self.mode = Mode::Normal;
3218 self.calendar_date = None;
3219 }
3220 }
3221 }
3222 }
3223 }
3224 }
3225
3226 pub fn filtered_services(&self) -> Vec<&'static str> {
3227 let mut services = if self.service_picker.filter.is_empty() {
3228 self.service_picker.services.clone()
3229 } else {
3230 self.service_picker
3231 .services
3232 .iter()
3233 .filter(|s| {
3234 s.to_lowercase()
3235 .contains(&self.service_picker.filter.to_lowercase())
3236 })
3237 .copied()
3238 .collect()
3239 };
3240 services.sort();
3241 services
3242 }
3243
3244 pub fn breadcrumbs(&self) -> String {
3245 if !self.service_selected {
3246 return String::new();
3247 }
3248
3249 let mut parts = vec![];
3250
3251 match self.current_service {
3252 Service::CloudWatchLogGroups => {
3253 parts.push("CloudWatch".to_string());
3254 parts.push("Log groups".to_string());
3255
3256 if self.view_mode != ViewMode::List {
3257 if let Some(group) = selected_log_group(self) {
3258 parts.push(group.name.clone());
3259 }
3260 }
3261
3262 if self.view_mode == ViewMode::Events {
3263 if let Some(stream) = self
3264 .log_groups_state
3265 .log_streams
3266 .get(self.log_groups_state.selected_stream)
3267 {
3268 parts.push(stream.name.clone());
3269 }
3270 }
3271 }
3272 Service::CloudWatchInsights => {
3273 parts.push("CloudWatch".to_string());
3274 parts.push("Logs Insights".to_string());
3275 }
3276 Service::CloudWatchAlarms => {
3277 parts.extend(crate::cw::actions::alarms_breadcrumb());
3278 }
3279 Service::CloudTrailEvents => {
3280 parts.extend(crate::cloudtrail::actions::breadcrumb(self));
3281 }
3282 Service::S3Buckets => {
3283 parts.extend(crate::s3::actions::breadcrumb(self));
3284 }
3285 Service::SqsQueues => {
3286 parts.extend(crate::sqs::actions::breadcrumb());
3287 }
3288 Service::EcrRepositories => {
3289 parts.extend(crate::ecr::actions::breadcrumb(self));
3290 }
3291 Service::KmsKeys => {
3292 parts.extend(crate::kms::actions::breadcrumb(self));
3293 }
3294 Service::EfsFileSystems => {
3295 parts.extend(crate::efs::actions::breadcrumb(self));
3296 }
3297 Service::FsxFileSystems => {
3298 parts.extend(crate::fsx::actions::breadcrumb(self));
3299 }
3300 Service::LambdaFunctions => {
3301 parts.extend(crate::lambda::functions::breadcrumb(self));
3302 }
3303 Service::LambdaApplications => {
3304 parts.extend(crate::lambda::applications::breadcrumb());
3305 }
3306 Service::CloudFormationStacks => {
3307 parts.extend(crate::cfn::actions::breadcrumb(self));
3308 }
3309 Service::IamUsers => {
3310 parts.extend(crate::iam::actions::breadcrumb_users());
3311 }
3312 Service::IamRoles => {
3313 parts.extend(crate::iam::actions::breadcrumb_roles(self));
3314 }
3315 Service::IamUserGroups => {
3316 parts.extend(crate::iam::actions::breadcrumb_groups(self));
3317 }
3318 Service::Ec2Instances => {
3319 parts.extend(crate::ec2::actions::breadcrumb(self));
3320 }
3321 Service::ApiGatewayApis => {
3322 parts.extend(crate::apig::actions::breadcrumb(self));
3323 }
3324 }
3325
3326 parts.join(" > ")
3327 }
3328
3329 pub fn update_current_tab_breadcrumb(&mut self) {
3330 if !self.tabs.is_empty() {
3331 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
3332 }
3333 }
3334
3335 pub fn get_console_url(&self) -> String {
3336 use crate::cw;
3337
3338 match self.current_service {
3339 Service::CloudWatchLogGroups => {
3340 if self.view_mode == ViewMode::Events {
3341 if let Some(group) = selected_log_group(self) {
3342 if let Some(stream) = self
3343 .log_groups_state
3344 .log_streams
3345 .get(self.log_groups_state.selected_stream)
3346 {
3347 return cw::logs::console_url_stream(
3348 &self.config.region,
3349 &group.name,
3350 &stream.name,
3351 );
3352 }
3353 }
3354 } else if self.view_mode == ViewMode::Detail {
3355 if let Some(group) = selected_log_group(self) {
3356 return cw::logs::console_url_detail(&self.config.region, &group.name);
3357 }
3358 }
3359 cw::logs::console_url_list(&self.config.region)
3360 }
3361 Service::CloudWatchInsights => cw::insights::console_url(
3362 &self.config.region,
3363 &self.config.account_id,
3364 &self.insights_state.insights.query_text,
3365 &self.insights_state.insights.selected_log_groups,
3366 ),
3367 Service::CloudWatchAlarms => {
3368 let alarm_name = if let Some(name) = self.alarms_state.current_alarm.as_deref() {
3370 Some(name.to_string())
3371 } else {
3372 let page_size = self.alarms_state.table.page_size.value();
3374 let current_page = self.alarms_state.table.selected / page_size;
3375 let start = current_page * page_size;
3376 let filtered: Vec<_> = self.alarms_state.table.items.iter().collect();
3377 let relative = self.alarms_state.table.selected.saturating_sub(start);
3378 filtered.get(start + relative).map(|a| a.name.clone())
3379 };
3380 cw::alarms::console_url(&self.config.region, alarm_name.as_deref())
3381 }
3382 Service::CloudTrailEvents => crate::cloudtrail::actions::console_url(self),
3383 Service::S3Buckets => crate::s3::actions::console_url(self),
3384 Service::SqsQueues => crate::sqs::actions::console_url(self),
3385 Service::EcrRepositories => crate::ecr::actions::console_url(self),
3386 Service::KmsKeys => crate::kms::actions::console_url(self),
3387 Service::EfsFileSystems => crate::efs::actions::console_url(self),
3388 Service::FsxFileSystems => crate::fsx::actions::console_url(self),
3389 Service::LambdaFunctions => crate::lambda::functions::console_url(self),
3390 Service::LambdaApplications => crate::lambda::applications::console_url(self),
3391 Service::CloudFormationStacks => crate::cfn::actions::console_url(self),
3392 Service::IamUsers => crate::iam::actions::console_url_users(self),
3393 Service::IamRoles => crate::iam::actions::console_url_roles(self),
3394 Service::IamUserGroups => crate::iam::actions::console_url_groups(self),
3395 Service::Ec2Instances => crate::ec2::actions::console_url(self),
3396 Service::ApiGatewayApis => crate::apig::actions::console_url(self),
3397 }
3398 }
3399
3400 pub fn calculate_total_bucket_rows(&self) -> usize {
3401 calculate_total_bucket_rows(self)
3402 }
3403
3404 pub fn calculate_total_object_rows(&self) -> usize {
3405 calculate_total_object_rows(self)
3406 }
3407
3408 fn get_column_selector_max(&self) -> usize {
3409 if self.current_service == Service::ApiGatewayApis {
3410 crate::apig::actions::column_selector_max(self)
3411 } else if self.current_service == Service::S3Buckets
3412 && self.s3_state.current_bucket.is_none()
3413 {
3414 crate::s3::actions::column_selector_max(self)
3415 } else if self.view_mode == ViewMode::Events {
3416 self.cw_log_event_column_ids.len() - 1
3417 } else if self.view_mode == ViewMode::Detail {
3418 self.cw_log_stream_column_ids.len() + 6
3419 } else if self.current_service == Service::CloudWatchAlarms {
3420 crate::cw::actions::alarms_column_selector_max(self)
3421 } else if self.current_service == Service::CloudTrailEvents {
3422 crate::cloudtrail::actions::column_selector_max(self)
3423 } else if self.current_service == Service::Ec2Instances {
3424 crate::ec2::actions::column_selector_max(self)
3425 } else if self.current_service == Service::EcrRepositories {
3426 crate::ecr::actions::column_selector_max(self)
3427 } else if self.current_service == Service::KmsKeys {
3428 crate::kms::actions::column_selector_max(self)
3429 } else if self.current_service == Service::EfsFileSystems {
3430 crate::efs::actions::column_selector_max(self)
3431 } else if self.current_service == Service::FsxFileSystems {
3432 crate::fsx::actions::column_selector_max(self)
3433 } else if self.current_service == Service::SqsQueues {
3434 crate::sqs::actions::column_selector_max(self)
3435 } else if self.current_service == Service::LambdaFunctions {
3436 crate::lambda::functions::column_selector_max(self)
3437 } else if self.current_service == Service::LambdaApplications {
3438 crate::lambda::applications::column_selector_max(self)
3439 } else if self.current_service == Service::CloudFormationStacks {
3440 crate::cfn::actions::column_selector_max(self)
3441 } else if self.current_service == Service::IamUsers {
3442 crate::iam::actions::column_selector_max_users(self)
3443 } else if self.current_service == Service::IamRoles {
3444 crate::iam::actions::column_selector_max_roles(self)
3445 } else {
3446 self.cw_log_group_column_ids.len() + 6
3447 }
3448 }
3449
3450 fn get_column_count(&self) -> usize {
3451 if self.current_service == Service::ApiGatewayApis {
3452 crate::apig::actions::column_count(self)
3453 } else if self.current_service == Service::S3Buckets
3454 && self.s3_state.current_bucket.is_none()
3455 {
3456 crate::s3::actions::column_count(self)
3457 } else if self.view_mode == ViewMode::Events {
3458 self.cw_log_event_column_ids.len()
3459 } else if self.view_mode == ViewMode::Detail {
3460 self.cw_log_stream_column_ids.len()
3461 } else if self.current_service == Service::CloudWatchAlarms {
3462 crate::cw::actions::alarms_column_count(self)
3463 } else if self.current_service == Service::CloudTrailEvents {
3464 crate::cloudtrail::actions::column_count(self)
3465 } else if self.current_service == Service::Ec2Instances {
3466 crate::ec2::actions::column_count(self)
3467 } else if self.current_service == Service::EcrRepositories {
3468 crate::ecr::actions::column_count(self)
3469 } else if self.current_service == Service::KmsKeys {
3470 crate::kms::actions::column_count(self)
3471 } else if self.current_service == Service::EfsFileSystems {
3472 crate::efs::actions::column_count(self)
3473 } else if self.current_service == Service::FsxFileSystems {
3474 crate::fsx::actions::column_count(self)
3475 } else if self.current_service == Service::SqsQueues {
3476 crate::sqs::actions::column_count(self)
3477 } else if self.current_service == Service::LambdaFunctions {
3478 crate::lambda::functions::column_count(self)
3479 } else if self.current_service == Service::LambdaApplications {
3480 crate::lambda::applications::column_count(self)
3481 } else if self.current_service == Service::CloudFormationStacks {
3482 crate::cfn::actions::column_count(self)
3483 } else if self.current_service == Service::IamUsers {
3484 crate::iam::actions::column_count_users(self)
3485 } else if self.current_service == Service::IamRoles {
3486 crate::iam::actions::column_count_roles(self)
3487 } else {
3488 self.cw_log_group_column_ids.len()
3489 }
3490 }
3491
3492 fn is_blank_row_index(&self, idx: usize) -> bool {
3493 let column_count = self.get_column_count();
3494 idx == column_count + 1
3496 }
3497
3498 fn next_item(&mut self) {
3499 match self.mode {
3500 Mode::FilterInput => {
3501 if self.current_service == Service::S3Buckets
3502 && crate::s3::actions::is_pagination_focused(self)
3503 {
3504 crate::s3::actions::page_down_filter_input(self);
3505 } else if self.current_service == Service::CloudTrailEvents
3506 && self.cloudtrail_state.input_focus == InputFocus::Pagination
3507 {
3508 let page_size = self.cloudtrail_state.table.page_size.value();
3510 let total_items = self.cloudtrail_state.table.items.len();
3511 let current_page = self.cloudtrail_state.table.selected / page_size;
3512 let total_pages = total_items.div_ceil(page_size);
3513 if current_page + 1 < total_pages {
3514 self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
3515 }
3516 } else if self.current_service == Service::CloudFormationStacks {
3517 crate::cfn::actions::next_item_filter_input(self);
3518 } else if self.current_service == Service::IamUsers
3519 && self.iam_state.current_user.is_some()
3520 {
3521 crate::iam::actions::next_item_filter_input_users(self);
3522 } else if self.current_service == Service::IamRoles
3523 && self.iam_state.current_role.is_some()
3524 && self.iam_state.role_tab == RoleTab::Permissions
3525 {
3526 crate::iam::actions::next_item_filter_input_roles(self);
3527 } else if self.current_service == Service::Ec2Instances {
3528 crate::ec2::actions::toggle_state_filter_next(self);
3529 } else if self.current_service == Service::SqsQueues {
3530 crate::sqs::actions::next_item_filter_input(self);
3531 }
3532 }
3533 Mode::RegionPicker => {
3534 let filtered = self.get_filtered_regions();
3535 if !filtered.is_empty() {
3536 self.region_picker_selected =
3537 (self.region_picker_selected + 1).min(filtered.len() - 1);
3538 }
3539 }
3540 Mode::ProfilePicker => {
3541 let filtered = self.get_filtered_profiles();
3542 if !filtered.is_empty() {
3543 self.profile_picker_selected =
3544 (self.profile_picker_selected + 1).min(filtered.len() - 1);
3545 }
3546 }
3547 Mode::SessionPicker => {
3548 let filtered = self.get_filtered_sessions();
3549 if !filtered.is_empty() {
3550 self.session_picker_selected =
3551 (self.session_picker_selected + 1).min(filtered.len() - 1);
3552 }
3553 }
3554 Mode::InsightsInput => {
3555 crate::cw::actions::insights_next_item_dropdown(self);
3556 }
3557 Mode::ColumnSelector => {
3558 let max = self.get_column_selector_max();
3559 let mut next_idx = (self.column_selector_index + 1).min(max);
3560 if self.is_blank_row_index(next_idx) {
3562 next_idx = (next_idx + 1).min(max);
3563 }
3564 self.column_selector_index = next_idx;
3565 }
3566 Mode::ServicePicker => {
3567 let filtered = self.filtered_services();
3568 if !filtered.is_empty() {
3569 self.service_picker.selected =
3570 (self.service_picker.selected + 1).min(filtered.len() - 1);
3571 }
3572 }
3573 Mode::TabPicker => {
3574 let filtered = self.get_filtered_tabs();
3575 if !filtered.is_empty() {
3576 self.tab_picker_selected =
3577 (self.tab_picker_selected + 1).min(filtered.len() - 1);
3578 }
3579 }
3580 Mode::Normal => {
3581 if !self.service_selected {
3582 let filtered = self.filtered_services();
3583 if !filtered.is_empty() {
3584 self.service_picker.selected =
3585 (self.service_picker.selected + 1).min(filtered.len() - 1);
3586 }
3587 } else if self.current_service == Service::S3Buckets {
3588 crate::s3::actions::next_item(self);
3589 } else if self.view_mode == ViewMode::InsightsResults {
3590 crate::cw::actions::insights_next_item_results(self);
3591 } else if self.current_service == Service::CloudWatchInsights
3592 && self.insights_state.insights.show_dropdown
3593 {
3594 crate::cw::actions::insights_next_item_dropdown(self);
3596 } else if self.current_service == Service::ApiGatewayApis {
3597 crate::apig::actions::next_item(self);
3598 } else if self.current_service == Service::SqsQueues
3599 && self.sqs_state.current_queue.is_some()
3600 && (self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3601 || (self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3602 && !self.sqs_state.is_metrics_loading()))
3603 {
3604 crate::sqs::actions::scroll_down_detail(self);
3605 } else if self.view_mode == ViewMode::Events {
3606 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3607 if self.log_groups_state.event_scroll_offset < max_scroll {
3608 self.log_groups_state.event_scroll_offset =
3609 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3610 }
3611 } else if self.current_service == Service::CloudWatchLogGroups {
3612 crate::cw::actions::logs_next_item(self);
3613 } else if self.current_service == Service::CloudWatchAlarms {
3614 crate::cw::actions::alarms_next_item(self);
3615 } else if self.current_service == Service::CloudTrailEvents {
3616 crate::cloudtrail::actions::next_item(self);
3617 } else if self.current_service == Service::Ec2Instances {
3618 crate::ec2::actions::next_item(self);
3619 } else if self.current_service == Service::EcrRepositories {
3620 crate::ecr::actions::next_item(self);
3621 } else if self.current_service == Service::KmsKeys {
3622 crate::kms::actions::next_item(self);
3623 } else if self.current_service == Service::EfsFileSystems {
3624 crate::efs::actions::next_item(self);
3625 } else if self.current_service == Service::FsxFileSystems {
3626 crate::fsx::actions::next_item(self);
3627 } else if self.current_service == Service::SqsQueues {
3628 crate::sqs::actions::next_item(self);
3629 } else if self.current_service == Service::LambdaFunctions {
3630 crate::lambda::functions::next_item(self);
3631 } else if self.current_service == Service::LambdaApplications {
3632 crate::lambda::applications::next_item(self);
3633 } else if self.current_service == Service::CloudFormationStacks {
3634 crate::cfn::actions::next_item(self);
3635 } else if self.current_service == Service::IamUsers {
3636 crate::iam::actions::next_item_users(self);
3637 } else if self.current_service == Service::IamRoles {
3638 crate::iam::actions::next_item_roles(self);
3639 } else if self.current_service == Service::IamUserGroups {
3640 crate::iam::actions::next_item_groups(self);
3641 }
3642 }
3643 _ => {}
3644 }
3645 }
3646
3647 fn prev_item(&mut self) {
3648 match self.mode {
3649 Mode::FilterInput => {
3650 if self.current_service == Service::S3Buckets
3651 && crate::s3::actions::is_pagination_focused(self)
3652 {
3653 crate::s3::actions::page_up_filter_input(self);
3654 } else if self.current_service == Service::CloudTrailEvents
3655 && self.cloudtrail_state.input_focus == InputFocus::Pagination
3656 {
3657 let page_size = self.cloudtrail_state.table.page_size.value();
3659 let current_page = self.cloudtrail_state.table.selected / page_size;
3660 if current_page > 0 {
3661 self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
3662 }
3663 } else if self.current_service == Service::CloudFormationStacks {
3664 crate::cfn::actions::prev_item_filter_input(self);
3665 } else if self.current_service == Service::IamUsers
3666 && self.iam_state.current_user.is_some()
3667 {
3668 crate::iam::actions::prev_item_filter_input_users(self);
3669 } else if self.current_service == Service::IamRoles
3670 && self.iam_state.current_role.is_some()
3671 && self.iam_state.role_tab == RoleTab::Permissions
3672 {
3673 crate::iam::actions::prev_item_filter_input_roles(self);
3674 } else if self.current_service == Service::Ec2Instances {
3675 crate::ec2::actions::toggle_state_filter_prev(self);
3676 } else if self.current_service == Service::SqsQueues {
3677 use crate::ui::sqs::SUBSCRIPTION_REGION;
3678 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3679 self.sqs_state.subscription_region_selected = self
3680 .sqs_state
3681 .subscription_region_selected
3682 .saturating_sub(1);
3683 self.sqs_state.subscriptions.reset();
3684 }
3685 }
3686 }
3687 Mode::RegionPicker => {
3688 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
3689 }
3690 Mode::ProfilePicker => {
3691 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
3692 }
3693 Mode::SessionPicker => {
3694 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
3695 }
3696 Mode::InsightsInput => {
3697 crate::cw::actions::insights_prev_item_dropdown(self);
3698 }
3699 Mode::ColumnSelector => {
3700 let mut prev_idx = self.column_selector_index.saturating_sub(1);
3701 if self.is_blank_row_index(prev_idx) {
3703 prev_idx = prev_idx.saturating_sub(1);
3704 }
3705 self.column_selector_index = prev_idx;
3706 }
3707 Mode::ServicePicker => {
3708 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3709 }
3710 Mode::TabPicker => {
3711 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
3712 }
3713 Mode::Normal => {
3714 if !self.service_selected {
3715 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3716 } else if self.current_service == Service::S3Buckets {
3717 crate::s3::actions::prev_item(self);
3718 } else if self.current_service == Service::ApiGatewayApis {
3719 crate::apig::actions::prev_item(self);
3720 } else if self.view_mode == ViewMode::InsightsResults {
3721 if self.insights_state.insights.results_selected > 0 {
3722 self.insights_state.insights.results_selected -= 1;
3723 }
3724 } else if self.current_service == Service::CloudWatchInsights
3725 && self.insights_state.insights.show_dropdown
3726 {
3727 crate::cw::actions::insights_prev_item_dropdown(self);
3728 } else if self.view_mode == ViewMode::PolicyView {
3729 crate::iam::actions::scroll_up_policy_view_one(self);
3730 } else if self.current_service == Service::SqsQueues
3731 && self.sqs_state.current_queue.is_some()
3732 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3733 {
3734 } else if self.current_service == Service::LambdaFunctions
3735 && self.lambda_state.current_function.is_some()
3736 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3737 && !self.lambda_state.is_metrics_loading()
3738 {
3739 crate::lambda::functions::scroll_up(self);
3740 } else if self.current_service == Service::SqsQueues
3741 && self.sqs_state.current_queue.is_some()
3742 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3743 && !self.sqs_state.is_metrics_loading()
3744 {
3745 self.sqs_state.set_monitoring_scroll(
3746 self.sqs_state.monitoring_scroll().saturating_sub(1),
3747 );
3748 } else if self.view_mode == ViewMode::Events {
3749 if self.log_groups_state.event_scroll_offset == 0 {
3750 if self.log_groups_state.has_older_events {
3751 self.log_groups_state.loading = true;
3752 }
3753 } else {
3755 self.log_groups_state.event_scroll_offset =
3756 self.log_groups_state.event_scroll_offset.saturating_sub(1);
3757 }
3758 } else if self.current_service == Service::CloudWatchLogGroups {
3759 crate::cw::actions::logs_prev_item(self);
3760 } else if self.current_service == Service::CloudWatchAlarms {
3761 crate::cw::actions::alarms_prev_item(self);
3762 } else if self.current_service == Service::CloudTrailEvents {
3763 crate::cloudtrail::actions::prev_item(self);
3764 } else if self.current_service == Service::Ec2Instances {
3765 if self.ec2_state.current_instance.is_some()
3766 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
3767 {
3768 self.ec2_state.tags.prev_item();
3769 } else {
3770 self.ec2_state.table.prev_item();
3771 }
3772 } else if self.current_service == Service::EcrRepositories {
3773 crate::ecr::actions::prev_item(self);
3774 } else if self.current_service == Service::KmsKeys {
3775 crate::kms::actions::prev_item(self);
3776 } else if self.current_service == Service::EfsFileSystems {
3777 crate::efs::actions::prev_item(self);
3778 } else if self.current_service == Service::FsxFileSystems {
3779 crate::fsx::actions::prev_item(self);
3780 } else if self.current_service == Service::SqsQueues {
3781 crate::sqs::actions::prev_item(self);
3782 } else if self.current_service == Service::LambdaFunctions {
3783 crate::lambda::functions::prev_item(self);
3784 } else if self.current_service == Service::LambdaApplications {
3785 crate::lambda::applications::prev_item(self);
3786 } else if self.current_service == Service::CloudFormationStacks {
3787 crate::cfn::actions::prev_item(self);
3788 } else if self.current_service == Service::IamUsers {
3789 crate::iam::actions::prev_item_users(self);
3790 } else if self.current_service == Service::IamRoles {
3791 crate::iam::actions::prev_item_roles(self);
3792 } else if self.current_service == Service::IamUserGroups {
3793 crate::iam::actions::prev_item_groups(self);
3794 }
3795 }
3796 _ => {}
3797 }
3798 }
3799
3800 fn page_down(&mut self) {
3801 if self.current_service == Service::CloudTrailEvents
3802 && self.cloudtrail_state.current_event.is_some()
3803 {
3804 crate::cloudtrail::actions::page_down_fast(self);
3805 } else if self.mode == Mode::ColumnSelector {
3806 let max = self.get_column_selector_max();
3807 let mut next_idx = (self.column_selector_index + 10).min(max);
3808 if self.is_blank_row_index(next_idx) {
3810 next_idx = (next_idx + 1).min(max);
3811 }
3812 self.column_selector_index = next_idx;
3813 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
3814 crate::s3::actions::page_down_filter_input(self);
3815 } else if self.mode == Mode::FilterInput
3816 && self.current_service == Service::CloudFormationStacks
3817 {
3818 crate::cfn::actions::page_down_filter_input(self);
3819 } else if self.mode == Mode::FilterInput
3820 && self.current_service == Service::IamRoles
3821 && self.iam_state.current_role.is_none()
3822 {
3823 crate::iam::actions::page_down_filter_input_roles(self);
3824 } else if self.mode == Mode::FilterInput
3825 && self.current_service == Service::CloudWatchAlarms
3826 {
3827 crate::cw::actions::alarms_page_down_filter_input(self);
3828 } else if self.mode == Mode::FilterInput
3829 && self.current_service == Service::CloudTrailEvents
3830 {
3831 crate::cloudtrail::actions::page_down_filter_input(self);
3832 } else if self.mode == Mode::FilterInput
3833 && self.current_service == Service::CloudWatchLogGroups
3834 {
3835 crate::cw::actions::logs_page_down_filter_input(self);
3836 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3837 {
3838 crate::lambda::functions::page_down_filter_input(self);
3839 } else if self.mode == Mode::FilterInput
3840 && self.current_service == Service::LambdaApplications
3841 {
3842 crate::lambda::applications::page_down_filter_input(self);
3843 } else if self.mode == Mode::FilterInput
3844 && self.current_service == Service::EcrRepositories
3845 && self.ecr_state.current_repository.is_none()
3846 {
3847 crate::ecr::actions::page_down_filter_input(self);
3848 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys {
3849 crate::kms::actions::page_down_filter_input(self);
3850 } else if self.mode == Mode::FilterInput && self.current_service == Service::EfsFileSystems
3851 {
3852 crate::efs::actions::page_down_filter_input(self);
3853 } else if self.mode == Mode::FilterInput && self.current_service == Service::FsxFileSystems
3854 {
3855 crate::fsx::actions::page_down_filter_input(self);
3856 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3857 crate::iam::actions::page_down_filter_input_policy_view(self);
3858 } else if self.view_mode == ViewMode::PolicyView {
3859 crate::iam::actions::scroll_down_policy_view(self);
3860 } else if self.current_service == Service::CloudFormationStacks
3861 && self.cfn_state.current_stack.is_some()
3862 && self.cfn_state.detail_tab == CfnDetailTab::Template
3863 {
3864 crate::cfn::actions::scroll_down_template_fast(self);
3865 } else if self.current_service == Service::LambdaFunctions
3866 && self.lambda_state.current_function.is_some()
3867 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3868 && !self.lambda_state.is_metrics_loading()
3869 {
3870 crate::lambda::functions::scroll_down(self);
3871 } else if self.current_service == Service::Ec2Instances {
3872 crate::ec2::actions::scroll_down(self);
3873 } else if self.current_service == Service::SqsQueues
3874 && self.sqs_state.current_queue.is_some()
3875 {
3876 crate::sqs::actions::scroll_down_fast(self);
3877 } else if self.current_service == Service::EfsFileSystems
3878 && self.efs_state.current_file_system.is_some()
3879 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
3880 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
3881 && !self.efs_state.is_metrics_loading()
3882 {
3883 crate::efs::actions::scroll_down_detail(self);
3884 } else if self.current_service == Service::IamRoles
3885 && self.iam_state.current_role.is_some()
3886 && self.iam_state.role_tab == RoleTab::TrustRelationships
3887 {
3888 crate::iam::actions::scroll_down_trust_policy(self);
3889 } else if self.current_service == Service::IamRoles
3890 && self.iam_state.current_role.is_some()
3891 && self.iam_state.role_tab == RoleTab::RevokeSessions
3892 {
3893 crate::iam::actions::scroll_down_revoke_sessions(self);
3894 } else if self.mode == Mode::Normal {
3895 if self.current_service == Service::S3Buckets {
3896 crate::s3::actions::page_down_normal(self);
3897 } else if self.current_service == Service::CloudWatchLogGroups
3898 && self.view_mode == ViewMode::List
3899 {
3900 crate::cw::actions::logs_page_down_normal_list(self);
3901 } else if self.current_service == Service::CloudWatchLogGroups
3902 && self.view_mode == ViewMode::Detail
3903 {
3904 crate::cw::actions::logs_page_down_normal_detail(self);
3905 } else if self.view_mode == ViewMode::Events {
3906 crate::cw::actions::logs_page_down_events(self);
3907 } else if self.view_mode == ViewMode::InsightsResults {
3908 crate::cw::actions::insights_page_down(self);
3909 } else if self.current_service == Service::CloudWatchAlarms {
3910 crate::cw::actions::alarms_page_down_normal(self);
3911 } else if self.current_service == Service::CloudTrailEvents {
3912 crate::cloudtrail::actions::page_down_normal(self);
3913 } else if self.current_service == Service::Ec2Instances {
3914 crate::ec2::actions::page_down_normal(self);
3915 } else if self.current_service == Service::EcrRepositories {
3916 crate::ecr::actions::page_down_normal(self);
3917 } else if self.current_service == Service::KmsKeys {
3918 crate::kms::actions::page_down_normal(self);
3919 } else if self.current_service == Service::EfsFileSystems {
3920 crate::efs::actions::page_down_normal(self);
3921 } else if self.current_service == Service::FsxFileSystems {
3922 crate::fsx::actions::page_down_normal(self);
3923 } else if self.current_service == Service::SqsQueues {
3924 crate::sqs::actions::page_down_normal(self);
3925 } else if self.current_service == Service::LambdaFunctions {
3926 crate::lambda::functions::page_down_normal(self);
3927 } else if self.current_service == Service::LambdaApplications {
3928 crate::lambda::applications::page_down_normal(self);
3929 } else if self.current_service == Service::CloudFormationStacks {
3930 crate::cfn::actions::page_down_normal(self);
3931 } else if self.current_service == Service::IamUsers {
3932 crate::iam::actions::page_down_normal_users(self);
3933 } else if self.current_service == Service::IamRoles {
3934 crate::iam::actions::page_down_normal_roles(self);
3935 } else if self.current_service == Service::IamUserGroups {
3936 crate::iam::actions::page_down_normal_groups(self);
3937 }
3938 }
3939 }
3940
3941 fn page_up(&mut self) {
3942 if self.current_service == Service::CloudTrailEvents
3943 && self.cloudtrail_state.current_event.is_some()
3944 {
3945 crate::cloudtrail::actions::page_up_fast(self);
3946 } else if self.mode == Mode::ColumnSelector {
3947 let mut prev_idx = self.column_selector_index.saturating_sub(10);
3948 if self.is_blank_row_index(prev_idx) {
3950 prev_idx = prev_idx.saturating_sub(1);
3951 }
3952 self.column_selector_index = prev_idx;
3953 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
3954 crate::s3::actions::page_up_filter_input(self);
3955 } else if self.mode == Mode::FilterInput
3956 && self.current_service == Service::CloudFormationStacks
3957 {
3958 crate::cfn::actions::page_up_filter_input(self);
3959 } else if self.mode == Mode::FilterInput
3960 && self.current_service == Service::IamRoles
3961 && self.iam_state.current_role.is_none()
3962 {
3963 crate::iam::actions::page_up_filter_input_roles(self);
3964 } else if self.mode == Mode::FilterInput
3965 && self.current_service == Service::CloudWatchAlarms
3966 {
3967 crate::cw::actions::alarms_page_up_filter_input(self);
3968 } else if self.mode == Mode::FilterInput
3969 && self.current_service == Service::CloudTrailEvents
3970 {
3971 crate::cloudtrail::actions::page_up_filter_input(self);
3972 } else if self.mode == Mode::FilterInput
3973 && self.current_service == Service::CloudWatchLogGroups
3974 {
3975 crate::cw::actions::logs_page_up_filter_input(self);
3976 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3977 {
3978 crate::lambda::functions::page_up_filter_input(self);
3979 } else if self.mode == Mode::FilterInput
3980 && self.current_service == Service::LambdaApplications
3981 {
3982 crate::lambda::applications::page_up_filter_input(self);
3983 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys {
3984 crate::kms::actions::page_up_filter_input(self);
3985 } else if self.mode == Mode::FilterInput && self.current_service == Service::EfsFileSystems
3986 {
3987 crate::efs::actions::page_up_filter_input(self);
3988 } else if self.mode == Mode::FilterInput && self.current_service == Service::FsxFileSystems
3989 {
3990 crate::fsx::actions::page_up_filter_input(self);
3991 } else if self.mode == Mode::FilterInput
3992 && self.current_service == Service::EcrRepositories
3993 && self.ecr_state.current_repository.is_none()
3994 {
3995 crate::ecr::actions::page_up_filter_input(self);
3996 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3997 crate::iam::actions::page_up_filter_input_policy_view(self);
3998 } else if self.view_mode == ViewMode::PolicyView {
3999 crate::iam::actions::scroll_up_policy_view(self);
4000 } else if self.current_service == Service::CloudFormationStacks
4001 && self.cfn_state.current_stack.is_some()
4002 && self.cfn_state.detail_tab == CfnDetailTab::Template
4003 {
4004 crate::cfn::actions::scroll_up_template_fast(self);
4005 } else if self.current_service == Service::SqsQueues
4006 && self.sqs_state.current_queue.is_some()
4007 {
4008 crate::sqs::actions::scroll_up_fast(self);
4009 } else if self.current_service == Service::EfsFileSystems
4010 && self.efs_state.current_file_system.is_some()
4011 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
4012 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
4013 && !self.efs_state.is_metrics_loading()
4014 {
4015 crate::efs::actions::scroll_up_detail(self);
4016 } else if self.current_service == Service::IamRoles
4017 && self.iam_state.current_role.is_some()
4018 && self.iam_state.role_tab == RoleTab::TrustRelationships
4019 {
4020 crate::iam::actions::scroll_up_trust_policy(self);
4021 } else if self.current_service == Service::IamRoles
4022 && self.iam_state.current_role.is_some()
4023 && self.iam_state.role_tab == RoleTab::RevokeSessions
4024 {
4025 crate::iam::actions::scroll_up_revoke_sessions(self);
4026 } else if self.mode == Mode::Normal {
4027 if self.current_service == Service::S3Buckets {
4028 crate::s3::actions::page_up_normal(self);
4029 } else if self.current_service == Service::CloudWatchLogGroups
4030 && self.view_mode == ViewMode::List
4031 {
4032 crate::cw::actions::logs_page_up_normal_list(self);
4033 } else if self.current_service == Service::CloudWatchLogGroups
4034 && self.view_mode == ViewMode::Detail
4035 {
4036 crate::cw::actions::logs_page_up_normal_detail(self);
4037 } else if self.view_mode == ViewMode::Events {
4038 crate::cw::actions::logs_page_up_events(self);
4039 } else if self.view_mode == ViewMode::InsightsResults {
4040 crate::cw::actions::insights_page_up(self);
4041 } else if self.current_service == Service::CloudWatchAlarms {
4042 crate::cw::actions::alarms_page_up_normal(self);
4043 } else if self.current_service == Service::CloudTrailEvents {
4044 crate::cloudtrail::actions::page_up_normal(self);
4045 } else if self.current_service == Service::KmsKeys {
4046 crate::kms::actions::page_up_normal(self);
4047 } else if self.current_service == Service::EfsFileSystems {
4048 crate::efs::actions::page_up_normal(self);
4049 } else if self.current_service == Service::FsxFileSystems {
4050 crate::fsx::actions::page_up_normal(self);
4051 } else if self.current_service == Service::Ec2Instances {
4052 crate::ec2::actions::page_up_normal(self);
4053 } else if self.current_service == Service::EcrRepositories {
4054 crate::ecr::actions::page_up_normal(self);
4055 } else if self.current_service == Service::SqsQueues {
4056 self.sqs_state.queues.page_up();
4057 } else if self.current_service == Service::LambdaFunctions {
4058 crate::lambda::functions::page_up_normal(self);
4059 } else if self.current_service == Service::LambdaApplications {
4060 crate::lambda::applications::page_up_normal(self);
4061 } else if self.current_service == Service::CloudFormationStacks {
4062 crate::cfn::actions::page_up_normal(self);
4063 } else if self.current_service == Service::IamUsers {
4064 crate::iam::actions::page_up_normal_users(self);
4065 } else if self.current_service == Service::IamRoles {
4066 crate::iam::actions::page_up_normal_roles(self);
4067 }
4068 }
4069 }
4070
4071 fn next_pane(&mut self) {
4072 if self.current_service == Service::S3Buckets {
4073 crate::s3::actions::expand_row(self);
4074 } else if self.view_mode == ViewMode::InsightsResults {
4075 let max_cols = self
4077 .insights_state
4078 .insights
4079 .query_results
4080 .first()
4081 .map(|r| r.len())
4082 .unwrap_or(0);
4083 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
4084 self.insights_state.insights.results_horizontal_scroll += 1;
4085 }
4086 } else if self.current_service == Service::CloudWatchLogGroups
4087 || self.view_mode == ViewMode::Events
4088 {
4089 crate::cw::actions::logs_expand_row(self);
4090 } else if self.current_service == Service::CloudWatchAlarms {
4091 crate::cw::actions::alarms_expand_row(self);
4092 } else if self.current_service == Service::Ec2Instances {
4093 crate::ec2::actions::expand_row(self);
4094 } else if self.current_service == Service::EcrRepositories {
4095 crate::ecr::actions::next_pane(self);
4096 } else if self.current_service == Service::KmsKeys {
4097 crate::kms::actions::expand_row(self);
4098 } else if self.current_service == Service::EfsFileSystems {
4099 crate::efs::actions::expand_row(self);
4100 } else if self.current_service == Service::FsxFileSystems {
4101 crate::fsx::actions::expand_row(self);
4102 } else if self.current_service == Service::SqsQueues {
4103 crate::sqs::actions::expand_row(self);
4104 } else if self.current_service == Service::LambdaFunctions {
4105 crate::lambda::functions::expand_row(self);
4106 } else if self.current_service == Service::LambdaApplications {
4107 crate::lambda::applications::expand_row(self);
4108 } else if self.current_service == Service::CloudFormationStacks
4109 && self.cfn_state.current_stack.is_none()
4110 {
4111 self.cfn_state.table.toggle_expand();
4112 } else if self.current_service == Service::CloudFormationStacks
4113 && self.cfn_state.current_stack.is_some()
4114 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4115 {
4116 self.cfn_state.parameters.toggle_expand();
4117 } else if self.current_service == Service::CloudFormationStacks
4118 && self.cfn_state.current_stack.is_some()
4119 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4120 {
4121 self.cfn_state.outputs.toggle_expand();
4122 } else if self.current_service == Service::CloudFormationStacks
4123 && self.cfn_state.current_stack.is_some()
4124 && self.cfn_state.detail_tab == CfnDetailTab::Resources
4125 {
4126 self.cfn_state.resources.toggle_expand();
4127 } else if self.current_service == Service::IamUsers {
4128 crate::iam::actions::expand_row_users(self);
4129 } else if self.current_service == Service::IamRoles {
4130 crate::iam::actions::expand_row_roles(self);
4131 } else if self.current_service == Service::IamUserGroups {
4132 crate::iam::actions::expand_row_groups(self);
4133 }
4134 }
4135
4136 fn go_to_page(&mut self, page: usize) {
4137 if page == 0 {
4138 return;
4139 }
4140
4141 match self.current_service {
4142 Service::CloudWatchAlarms => {
4143 crate::cw::actions::alarms_go_to_page(self, page);
4144 }
4145 Service::CloudTrailEvents => {
4146 let page_size = self.cloudtrail_state.table.page_size.value();
4147 let filtered_count = self.cloudtrail_state.table.items.len();
4148 let max_page = (filtered_count / page_size) + 1; if page <= max_page {
4152 let target = (page - 1) * page_size;
4153 self.cloudtrail_state.table.scroll_offset = target;
4154 self.cloudtrail_state.table.selected = target;
4155 self.cloudtrail_state.table.expanded_item = None; }
4157 }
4159 Service::CloudWatchLogGroups => {
4160 crate::cw::actions::logs_go_to_page(self, page);
4161 }
4162 Service::EcrRepositories => {
4163 crate::ecr::actions::go_to_page(self, page);
4164 }
4165 Service::KmsKeys => {
4166 crate::kms::actions::go_to_page(self, page);
4167 }
4168 Service::EfsFileSystems => {
4169 crate::efs::actions::go_to_page(self, page);
4170 }
4171 Service::FsxFileSystems => {
4172 crate::fsx::actions::go_to_page(self, page);
4173 }
4174 Service::SqsQueues => {
4175 crate::sqs::actions::go_to_page(self, page);
4176 }
4177 Service::S3Buckets => {
4178 crate::s3::actions::go_to_page(self, page);
4179 }
4180 Service::LambdaFunctions => {
4181 crate::lambda::functions::go_to_page(self, page);
4182 }
4183 Service::LambdaApplications => {
4184 crate::lambda::applications::go_to_page(self, page);
4185 }
4186 Service::CloudFormationStacks => {
4187 crate::cfn::actions::go_to_page(self, page);
4188 }
4189 Service::IamUsers => {
4190 crate::iam::actions::go_to_page_users(self, page);
4191 }
4192 Service::IamRoles => {
4193 crate::iam::actions::go_to_page_roles(self, page);
4194 }
4195 _ => {}
4196 }
4197 }
4198
4199 fn prev_pane(&mut self) {
4200 if self.current_service == Service::S3Buckets {
4201 crate::s3::actions::prev_pane(self);
4202 } else if self.view_mode == ViewMode::InsightsResults {
4203 self.insights_state.insights.results_horizontal_scroll = self
4205 .insights_state
4206 .insights
4207 .results_horizontal_scroll
4208 .saturating_sub(1);
4209 } else if self.current_service == Service::CloudWatchLogGroups
4210 || self.view_mode == ViewMode::Events
4211 {
4212 crate::cw::actions::logs_prev_pane(self);
4213 } else if self.current_service == Service::CloudWatchAlarms {
4214 crate::cw::actions::alarms_prev_pane(self);
4215 } else if self.current_service == Service::Ec2Instances {
4216 crate::ec2::actions::prev_pane(self);
4217 } else if self.current_service == Service::ApiGatewayApis {
4218 crate::apig::actions::prev_pane(self);
4219 } else if self.current_service == Service::EcrRepositories {
4220 crate::ecr::actions::prev_pane(self);
4221 } else if self.current_service == Service::KmsKeys {
4222 crate::kms::actions::prev_pane(self);
4223 } else if self.current_service == Service::EfsFileSystems {
4224 crate::efs::actions::prev_pane(self);
4225 } else if self.current_service == Service::FsxFileSystems {
4226 crate::fsx::actions::prev_pane(self);
4227 } else if self.current_service == Service::SqsQueues {
4228 crate::sqs::actions::prev_pane(self);
4229 } else if self.current_service == Service::LambdaFunctions {
4230 crate::lambda::functions::prev_pane(self);
4231 } else if self.current_service == Service::LambdaApplications {
4232 crate::lambda::applications::prev_pane(self);
4233 } else if self.current_service == Service::CloudFormationStacks {
4234 crate::cfn::actions::prev_pane(self);
4235 } else if self.current_service == Service::IamUsers {
4236 crate::iam::actions::prev_pane_users(self);
4237 } else if self.current_service == Service::IamRoles {
4238 crate::iam::actions::prev_pane_roles(self);
4239 } else if self.current_service == Service::IamUserGroups {
4240 crate::iam::actions::prev_pane_groups(self);
4241 }
4242 }
4243
4244 fn collapse_row(&mut self) {
4245 match self.current_service {
4246 Service::S3Buckets => {
4247 crate::s3::actions::collapse_row(self);
4248 }
4249 Service::CloudWatchLogGroups => {
4250 crate::cw::actions::logs_collapse_row(self);
4251 }
4252 Service::CloudWatchAlarms => crate::cw::actions::alarms_collapse_row(self),
4253 Service::Ec2Instances => {
4254 crate::ec2::actions::collapse_row(self);
4255 }
4256 Service::EcrRepositories => {
4257 crate::ecr::actions::collapse_row(self);
4258 }
4259 Service::LambdaFunctions => crate::lambda::functions::collapse_row(self),
4260 Service::LambdaApplications => crate::lambda::applications::prev_pane(self),
4261 Service::SqsQueues => crate::sqs::actions::collapse_row(self),
4262 Service::CloudFormationStacks => {
4263 crate::cfn::actions::collapse_row(self);
4264 }
4265 Service::IamUsers => {
4266 crate::iam::actions::collapse_row_users(self);
4267 }
4268 Service::IamRoles => {
4269 crate::iam::actions::collapse_row_roles(self);
4270 }
4271 Service::IamUserGroups => {
4272 crate::iam::actions::collapse_row_groups(self);
4273 }
4274 Service::ApiGatewayApis => {
4275 crate::apig::actions::collapse_row(self);
4276 }
4277 Service::KmsKeys => {
4278 crate::kms::actions::collapse_row(self);
4279 }
4280 Service::EfsFileSystems => {
4281 crate::efs::actions::collapse_row(self);
4282 }
4283 Service::FsxFileSystems => {
4284 crate::fsx::actions::collapse_row(self);
4285 }
4286 Service::CloudTrailEvents => {
4287 if self.cloudtrail_state.current_event.is_some()
4288 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4289 {
4290 self.cloudtrail_state.resources_expanded_index = None;
4292 } else {
4293 self.cloudtrail_state.table.collapse();
4294 }
4295 }
4296 _ => {}
4297 }
4298 }
4299
4300 fn expand_row(&mut self) {
4301 match self.current_service {
4302 Service::S3Buckets => {
4303 crate::s3::actions::expand_row_left(self);
4304 }
4305 Service::ApiGatewayApis => {
4306 crate::apig::actions::expand_row(self);
4307 }
4308 Service::CloudTrailEvents => {
4309 if self.cloudtrail_state.current_event.is_some()
4310 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4311 {
4312 self.cloudtrail_state.resources_expanded_index = Some(0);
4314 } else {
4315 self.cloudtrail_state.table.expand();
4316 }
4317 }
4318 Service::CloudFormationStacks => {
4319 crate::cfn::actions::expand_row(self);
4320 }
4321 _ => {
4322 self.next_pane();
4324 }
4325 }
4326 }
4327
4328 fn select_item(&mut self) {
4329 if self.mode == Mode::RegionPicker {
4330 if self.region_filter_active {
4332 self.region_filter_active = false;
4333 return;
4334 }
4335 let filtered = self.get_filtered_regions();
4336 if let Some(region) = filtered.get(self.region_picker_selected) {
4337 if !self.tabs.is_empty() {
4339 let mut session = Session::new(
4340 self.profile.clone(),
4341 self.region.clone(),
4342 self.config.account_id.clone(),
4343 self.config.role_arn.clone(),
4344 );
4345
4346 for tab in &self.tabs {
4347 session.tabs.push(SessionTab {
4348 service: format!("{:?}", tab.service),
4349 title: tab.title.clone(),
4350 breadcrumb: tab.breadcrumb.clone(),
4351 filter: None,
4352 selected_item: None,
4353 });
4354 }
4355
4356 let _ = session.save();
4357 }
4358
4359 self.region = region.code.to_string();
4360 self.config.region = region.code.to_string();
4361
4362 self.tabs.clear();
4364 self.current_tab = 0;
4365 self.service_selected = false;
4366
4367 self.mode = Mode::Normal;
4368 }
4369 } else if self.mode == Mode::ProfilePicker {
4370 let filtered = self.get_filtered_profiles();
4371 if let Some(profile) = filtered.get(self.profile_picker_selected) {
4372 let profile_name = profile.name.clone();
4373 let profile_region = profile.region.clone();
4374
4375 self.profile = profile_name.clone();
4376 std::env::set_var("AWS_PROFILE", &profile_name);
4377
4378 if let Some(region) = profile_region {
4380 self.region = region;
4381 }
4382
4383 self.mode = Mode::Normal;
4384 }
4386 } else if self.mode == Mode::ServicePicker {
4387 if self.service_picker.filter_active {
4389 self.service_picker.filter_active = false;
4390 return;
4391 }
4392
4393 let filtered = self.filtered_services();
4394 if let Some(&service) = filtered.get(self.service_picker.selected) {
4395 let new_service = match service {
4396 "API Gateway › APIs" => Service::ApiGatewayApis,
4397 "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
4398 "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
4399 "CloudWatch › Alarms" => Service::CloudWatchAlarms,
4400 "CloudTrail › Event History" => Service::CloudTrailEvents,
4401 "CloudFormation › Stacks" => Service::CloudFormationStacks,
4402 "EC2 › Instances" => Service::Ec2Instances,
4403 "ECR › Repositories" => Service::EcrRepositories,
4404 "KMS › Managed Keys" => Service::KmsKeys,
4405 "EFS › File Systems" => Service::EfsFileSystems,
4406 "FSx › File Systems" => Service::FsxFileSystems,
4407 "IAM › Users" => Service::IamUsers,
4408 "IAM › Roles" => Service::IamRoles,
4409 "IAM › User Groups" => Service::IamUserGroups,
4410 "Lambda › Functions" => Service::LambdaFunctions,
4411 "Lambda › Applications" => Service::LambdaApplications,
4412 "S3 › Buckets" => Service::S3Buckets,
4413 "SQS › Queues" => Service::SqsQueues,
4414 _ => return,
4415 };
4416
4417 self.tabs.push(Tab {
4419 service: new_service,
4420 title: service.to_string(),
4421 breadcrumb: service.to_string(),
4422 });
4423 self.current_tab = self.tabs.len() - 1;
4424 self.current_service = new_service;
4425 self.view_mode = ViewMode::List;
4426 self.service_selected = true;
4427 self.mode = Mode::Normal;
4428 }
4429 } else if self.mode == Mode::TabPicker {
4430 let filtered = self.get_filtered_tabs();
4431 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
4432 self.current_tab = idx;
4433 self.current_service = self.tabs[idx].service;
4434 self.mode = Mode::Normal;
4435 self.tab_filter.clear();
4436 }
4437 } else if self.mode == Mode::SessionPicker {
4438 let filtered = self.get_filtered_sessions();
4439 if let Some(&session) = filtered.get(self.session_picker_selected) {
4440 let session = session.clone();
4441
4442 self.current_session = Some(session.clone());
4444 self.profile = session.profile.clone();
4445 self.region = session.region.clone();
4446 self.config.region = session.region.clone();
4447 self.config.account_id = session.account_id.clone();
4448 self.config.role_arn = session.role_arn.clone();
4449
4450 self.tabs = session
4452 .tabs
4453 .iter()
4454 .map(|st| Tab {
4455 service: match st.service.as_str() {
4456 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
4457 "CloudWatchInsights" => Service::CloudWatchInsights,
4458 "CloudWatchAlarms" => Service::CloudWatchAlarms,
4459 "S3Buckets" => Service::S3Buckets,
4460 "CloudTrailEvents" => Service::CloudTrailEvents,
4461 "SqsQueues" => Service::SqsQueues,
4462 _ => Service::CloudWatchLogGroups,
4463 },
4464 title: st.title.clone(),
4465 breadcrumb: st.breadcrumb.clone(),
4466 })
4467 .collect();
4468
4469 if !self.tabs.is_empty() {
4470 self.current_tab = 0;
4471 self.current_service = self.tabs[0].service;
4472 self.service_selected = true;
4473 }
4474
4475 self.mode = Mode::Normal;
4476 }
4477 } else if self.mode == Mode::InsightsInput {
4478 use crate::app::InsightsFocus;
4480 match self.insights_state.insights.insights_focus {
4481 InsightsFocus::Query => {
4482 self.insights_state.insights.query_text.push('\n');
4484 self.insights_state.insights.query_cursor_line += 1;
4485 self.insights_state.insights.query_cursor_col = 0;
4486 }
4487 InsightsFocus::LogGroupSearch => {
4488 let was_open = self.insights_state.insights.show_dropdown;
4490 self.insights_state.insights.show_dropdown = !was_open;
4491 if !was_open {
4492 if self.insights_state.insights.log_group_search.is_empty() {
4494 self.insights_state.insights.log_group_matches = self
4495 .log_groups_state
4496 .log_groups
4497 .items
4498 .iter()
4499 .take(50)
4500 .map(|g| g.name.clone())
4501 .collect();
4502 }
4503 self.insights_state.insights.dropdown_selected = 0;
4504 }
4505 }
4506 _ => {}
4507 }
4508 } else if self.mode == Mode::Normal {
4509 if !self.service_selected {
4511 let filtered = self.filtered_services();
4512 if let Some(&service) = filtered.get(self.service_picker.selected) {
4513 match service {
4514 "CloudWatch › Log Groups" => {
4515 self.current_service = Service::CloudWatchLogGroups;
4516 self.view_mode = ViewMode::List;
4517 self.service_selected = true;
4518 }
4519 "CloudWatch › Logs Insights" => {
4520 self.current_service = Service::CloudWatchInsights;
4521 self.view_mode = ViewMode::InsightsResults;
4522 self.service_selected = true;
4523 }
4524 "CloudWatch › Alarms" => {
4525 self.current_service = Service::CloudWatchAlarms;
4526 self.view_mode = ViewMode::List;
4527 self.service_selected = true;
4528 }
4529 "S3 › Buckets" => {
4530 self.current_service = Service::S3Buckets;
4531 self.view_mode = ViewMode::List;
4532 self.service_selected = true;
4533 }
4534 "EC2 › Instances" => {
4535 self.current_service = Service::Ec2Instances;
4536 self.view_mode = ViewMode::List;
4537 self.service_selected = true;
4538 }
4539 "ECR › Repositories" => {
4540 self.current_service = Service::EcrRepositories;
4541 self.view_mode = ViewMode::List;
4542 self.service_selected = true;
4543 }
4544 "Lambda › Functions" => {
4545 self.current_service = Service::LambdaFunctions;
4546 self.view_mode = ViewMode::List;
4547 self.service_selected = true;
4548 }
4549 "Lambda › Applications" => {
4550 self.current_service = Service::LambdaApplications;
4551 self.view_mode = ViewMode::List;
4552 self.service_selected = true;
4553 }
4554 _ => {}
4555 }
4556 }
4557 return;
4558 }
4559
4560 if self.view_mode == ViewMode::InsightsResults {
4562 if self.insights_state.insights.expanded_result
4564 == Some(self.insights_state.insights.results_selected)
4565 {
4566 self.insights_state.insights.expanded_result = None;
4567 } else {
4568 self.insights_state.insights.expanded_result =
4569 Some(self.insights_state.insights.results_selected);
4570 }
4571 } else if self.current_service == Service::S3Buckets {
4572 crate::s3::actions::select_item(self);
4573 } else if self.current_service == Service::ApiGatewayApis {
4574 crate::apig::actions::select_item(self);
4575 } else if self.current_service == Service::CloudFormationStacks {
4576 if self.cfn_state.current_stack.is_none() {
4577 let filtered_stacks = filtered_cloudformation_stacks(self);
4579 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
4580 let stack_name = stack.name.clone();
4581 let mut tags = stack.tags.clone();
4582 tags.sort_by(|a, b| a.0.cmp(&b.0));
4583
4584 self.cfn_state.current_stack = Some(stack_name);
4585 self.cfn_state.tags.items = tags;
4586 self.cfn_state.tags.reset();
4587 self.cfn_state.table.loading = true;
4588 self.update_current_tab_breadcrumb();
4589 }
4590 }
4591 } else if self.current_service == Service::CloudWatchAlarms {
4592 crate::cw::actions::alarms_select_item(self);
4593 } else if self.current_service == Service::CloudTrailEvents {
4594 crate::cloudtrail::actions::select_item(self);
4595 } else if self.current_service == Service::EcrRepositories {
4596 crate::ecr::actions::select_item(self);
4597 } else if self.current_service == Service::KmsKeys {
4598 } else if self.current_service == Service::EfsFileSystems {
4600 crate::efs::actions::select_item(self);
4601 } else if self.current_service == Service::FsxFileSystems {
4602 crate::fsx::actions::select_item(self);
4603 } else if self.current_service == Service::Ec2Instances {
4604 crate::ec2::actions::select_item(self);
4605 } else if self.current_service == Service::SqsQueues {
4606 crate::sqs::actions::select_item(self);
4607 } else if self.current_service == Service::IamUsers {
4608 crate::iam::actions::select_item_users(self);
4609 } else if self.current_service == Service::IamRoles {
4610 crate::iam::actions::select_item_roles(self);
4611 } else if self.current_service == Service::IamUserGroups {
4612 crate::iam::actions::select_item_groups(self);
4613 } else if self.current_service == Service::LambdaFunctions {
4614 crate::lambda::functions::select_item(self);
4615 } else if self.current_service == Service::LambdaApplications {
4616 crate::lambda::applications::select_item(self);
4617 } else if self.current_service == Service::CloudWatchLogGroups {
4618 crate::cw::actions::logs_select_item(self);
4619 } else if self.current_service == Service::CloudWatchAlarms
4620 && self.view_mode != ViewMode::Detail
4621 {
4622 crate::cw::actions::alarms_select_expand(self);
4623 } else if self.current_service == Service::CloudWatchInsights {
4624 if !self.insights_state.insights.selected_log_groups.is_empty() {
4626 self.log_groups_state.loading = true;
4627 self.insights_state.insights.query_completed = true;
4628 }
4629 }
4630 }
4631 }
4632
4633 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
4634 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
4635 Ok(())
4636 }
4637
4638 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
4639 let alarm_data = self.alarms_client.list_alarms().await?;
4640 self.alarms_state.table.items = alarm_data
4641 .into_iter()
4642 .map(|d| Alarm {
4643 name: d.name,
4644 state: d.state,
4645 state_updated_timestamp: d.state_updated,
4646 description: d.description,
4647 metric_name: d.metric_name,
4648 namespace: d.namespace,
4649 statistic: d.statistic,
4650 period: d.period,
4651 comparison_operator: d.comparison,
4652 threshold: d.threshold,
4653 actions_enabled: d.actions_enabled,
4654 state_reason: d.state_reason,
4655 resource: d.resource,
4656 dimensions: d.dimensions,
4657 expression: d.expression,
4658 alarm_type: d.alarm_type,
4659 cross_account: d.cross_account,
4660 alarm_arn: d.alarm_arn,
4661 datapoints_to_alarm: d.datapoints_to_alarm,
4662 evaluation_periods: d.evaluation_periods,
4663 treat_missing_data: d.treat_missing_data,
4664 evaluate_low_sample_percentile: d.evaluate_low_sample_percentile,
4665 sub_metrics: d.sub_metrics,
4666 })
4667 .collect();
4668 Ok(())
4669 }
4670
4671 pub async fn load_alarm_metric_math_data(&mut self, alarm_name: &str) -> anyhow::Result<()> {
4673 if let Some(alarm) = self
4674 .alarms_state
4675 .table
4676 .items
4677 .iter()
4678 .find(|a| a.name == alarm_name)
4679 {
4680 if alarm.sub_metrics.is_empty() {
4681 return Ok(());
4682 }
4683 let sub_metrics = alarm.sub_metrics.clone();
4684 let end_time = chrono::Utc::now();
4685 let start_time = end_time - chrono::TimeDelta::hours(72);
4686 let data = self
4687 .alarms_client
4688 .get_metric_math_data(&sub_metrics, start_time, end_time)
4689 .await?;
4690 self.alarms_state.metric_data = data;
4691 }
4692 Ok(())
4693 }
4694
4695 pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
4696 let filter = self.cloudtrail_state.active_event_name_filter.clone();
4697 let (events, next_token) = self
4698 .cloudtrail_client
4699 .lookup_events(None, None, filter)
4700 .await?;
4701 self.cloudtrail_state.table.items = events
4702 .into_iter()
4703 .map(
4704 |(
4705 event_name,
4706 event_time,
4707 username,
4708 event_source,
4709 resource_type,
4710 resource_name,
4711 read_only,
4712 aws_region,
4713 event_id,
4714 access_key_id,
4715 source_ip_address,
4716 error_code,
4717 request_id,
4718 event_type,
4719 cloud_trail_event_json,
4720 )| CloudTrailEvent {
4721 event_name,
4722 event_time,
4723 username,
4724 event_source,
4725 resource_type,
4726 resource_name,
4727 read_only,
4728 aws_region,
4729 event_id,
4730 access_key_id,
4731 source_ip_address,
4732 error_code,
4733 request_id,
4734 event_type,
4735 cloud_trail_event_json,
4736 },
4737 )
4738 .collect();
4739 self.cloudtrail_state.table.next_token = next_token;
4740 Ok(())
4741 }
4742
4743 pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
4744 if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
4745 let filter = self.cloudtrail_state.active_event_name_filter.clone();
4746 let (events, next_token) = self
4748 .cloudtrail_client
4749 .lookup_events(None, Some(token), filter)
4750 .await?;
4751 self.cloudtrail_state
4752 .table
4753 .items
4754 .extend(events.into_iter().map(
4755 |(
4756 event_name,
4757 event_time,
4758 username,
4759 event_source,
4760 resource_type,
4761 resource_name,
4762 read_only,
4763 aws_region,
4764 event_id,
4765 access_key_id,
4766 source_ip_address,
4767 error_code,
4768 request_id,
4769 event_type,
4770 cloud_trail_event_json,
4771 )| CloudTrailEvent {
4772 event_name,
4773 event_time,
4774 username,
4775 event_source,
4776 resource_type,
4777 resource_name,
4778 read_only,
4779 aws_region,
4780 event_id,
4781 access_key_id,
4782 source_ip_address,
4783 error_code,
4784 request_id,
4785 event_type,
4786 cloud_trail_event_json,
4787 },
4788 ));
4789 self.cloudtrail_state.table.next_token = next_token;
4790 }
4791 Ok(())
4792 }
4793
4794 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
4795 if let Some(bucket_name) = &self.s3_state.current_bucket {
4796 let bucket_region = if let Some(bucket) = self
4798 .s3_state
4799 .buckets
4800 .items
4801 .iter_mut()
4802 .find(|b| &b.name == bucket_name)
4803 {
4804 if bucket.region.is_empty() {
4805 let region = self.s3_client.get_bucket_location(bucket_name).await?;
4807 bucket.region = region.clone();
4808 region
4809 } else {
4810 bucket.region.clone()
4811 }
4812 } else {
4813 self.config.region.clone()
4814 };
4815
4816 let prefix = self
4817 .s3_state
4818 .prefix_stack
4819 .last()
4820 .cloned()
4821 .unwrap_or_default();
4822 let objects = self
4823 .s3_client
4824 .list_objects(bucket_name, &bucket_region, &prefix)
4825 .await?;
4826 self.s3_state.objects = objects
4827 .into_iter()
4828 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4829 key,
4830 size,
4831 last_modified: modified,
4832 is_prefix,
4833 storage_class,
4834 })
4835 .collect();
4836 self.s3_state.selected_object = 0;
4837 }
4838 Ok(())
4839 }
4840
4841 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
4842 let stored_region = self
4843 .s3_state
4844 .buckets
4845 .items
4846 .iter()
4847 .find(|b| b.name == bucket_name)
4848 .and_then(|b| {
4849 if b.region.is_empty() {
4850 None
4851 } else {
4852 Some(b.region.clone())
4853 }
4854 });
4855
4856 let bucket_region = match stored_region {
4857 Some(r) => r,
4858 None => {
4859 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
4860 if let Some(b) = self
4862 .s3_state
4863 .buckets
4864 .items
4865 .iter_mut()
4866 .find(|b| b.name == bucket_name)
4867 {
4868 b.region = r.clone();
4869 }
4870 r
4871 }
4872 };
4873 let objects = self
4874 .s3_client
4875 .list_objects(&bucket_name, &bucket_region, "")
4876 .await?;
4877 let preview: Vec<S3Object> = objects
4878 .into_iter()
4879 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4880 key,
4881 size,
4882 last_modified: modified,
4883 is_prefix,
4884 storage_class,
4885 })
4886 .collect();
4887 self.s3_state
4888 .bucket_preview
4889 .insert(bucket_name.clone(), preview);
4890 self.after_bucket_preview_loaded(&bucket_name);
4891 Ok(())
4892 }
4893
4894 pub fn after_bucket_preview_loaded(&mut self, bucket_name: &str) {
4898 if self.s3_state.current_bucket.is_some() {
4899 return; }
4901 let mut row_idx = 0usize;
4903 for b in &self.s3_state.buckets.items {
4904 if b.name == bucket_name {
4905 if self.s3_state.selected_row == row_idx {
4907 if let Some(preview) = self.s3_state.bucket_preview.get(bucket_name) {
4908 if !preview.is_empty() {
4909 self.s3_state.selected_row = row_idx + 1;
4910 let visible = self.s3_state.bucket_visible_rows.get();
4911 if self.s3_state.selected_row
4912 >= self.s3_state.bucket_scroll_offset + visible
4913 {
4914 self.s3_state.bucket_scroll_offset =
4915 self.s3_state.selected_row.saturating_sub(visible - 1);
4916 }
4917 }
4918 }
4919 }
4920 return;
4921 }
4922 row_idx += 1;
4923 if self.s3_state.expanded_prefixes.contains(&b.name) {
4925 if let Some(p) = self.s3_state.bucket_preview.get(&b.name) {
4926 row_idx += p.len();
4927 }
4928 }
4929 }
4930 }
4931
4932 pub async fn load_prefix_preview(
4933 &mut self,
4934 bucket_name: String,
4935 prefix: String,
4936 ) -> anyhow::Result<()> {
4937 let stored_region = self
4938 .s3_state
4939 .buckets
4940 .items
4941 .iter()
4942 .find(|b| b.name == bucket_name)
4943 .and_then(|b| {
4944 if b.region.is_empty() {
4945 None
4946 } else {
4947 Some(b.region.clone())
4948 }
4949 });
4950
4951 let bucket_region = match stored_region {
4952 Some(r) => r,
4953 None => {
4954 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
4955 if let Some(b) = self
4956 .s3_state
4957 .buckets
4958 .items
4959 .iter_mut()
4960 .find(|b| b.name == bucket_name)
4961 {
4962 b.region = r.clone();
4963 }
4964 r
4965 }
4966 };
4967 let objects = self
4968 .s3_client
4969 .list_objects(&bucket_name, &bucket_region, &prefix)
4970 .await?;
4971 let preview: Vec<S3Object> = objects
4972 .into_iter()
4973 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4974 key,
4975 size,
4976 last_modified: modified,
4977 is_prefix,
4978 storage_class,
4979 })
4980 .collect();
4981 self.s3_state.prefix_preview.insert(prefix, preview);
4982 Ok(())
4983 }
4984
4985 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
4986 let repos = match self.ecr_state.tab {
4987 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
4988 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
4989 };
4990
4991 self.ecr_state.repositories.items = repos
4992 .into_iter()
4993 .map(|r| EcrRepository {
4994 name: r.name,
4995 uri: r.uri,
4996 created_at: r.created_at,
4997 tag_immutability: r.tag_immutability,
4998 encryption_type: r.encryption_type,
4999 })
5000 .collect();
5001
5002 self.ecr_state
5003 .repositories
5004 .items
5005 .sort_by(|a, b| a.name.cmp(&b.name));
5006 Ok(())
5007 }
5008
5009 pub async fn load_kms_keys(&mut self) -> anyhow::Result<()> {
5010 let keys = self.kms_client.list_keys().await?;
5011 self.kms_state.keys.items = keys
5012 .into_iter()
5013 .map(|k| KmsKey {
5014 key_id: k.key_id,
5015 key_arn: k.key_arn,
5016 alias: k.alias,
5017 description: k.description,
5018 key_state: k.key_state,
5019 key_usage: k.key_usage,
5020 key_spec: k.key_spec,
5021 key_manager: k.key_manager,
5022 creation_date: k.creation_date,
5023 expiration_date: k.expiration_date,
5024 deletion_date: k.deletion_date,
5025 custom_key_store_id: k.custom_key_store_id,
5026 origin: k.origin,
5027 multi_region: k.multi_region,
5028 enabled: k.enabled,
5029 })
5030 .collect();
5031 Ok(())
5032 }
5033
5034 pub async fn load_efs_file_systems(&mut self) -> anyhow::Result<()> {
5035 let file_systems = self.efs_client.list_file_systems().await?;
5036 self.efs_state.file_systems.items = file_systems
5037 .into_iter()
5038 .map(|f| EfsFileSystem {
5039 file_system_id: f.file_system_id,
5040 file_system_arn: f.file_system_arn,
5041 name: f.name,
5042 creation_token: f.creation_token,
5043 encrypted: f.encrypted,
5044 kms_key_id: f.kms_key_id,
5045 total_size: f.total_size,
5046 size_in_standard: f.size_in_standard,
5047 size_in_ia: f.size_in_ia,
5048 size_in_archive: f.size_in_archive,
5049 provisioned_throughput: f.provisioned_throughput,
5050 throughput_mode: f.throughput_mode,
5051 life_cycle_state: f.life_cycle_state,
5052 number_of_mount_targets: f.number_of_mount_targets,
5053 owner_id: f.owner_id,
5054 creation_time: f.creation_time,
5055 performance_mode: f.performance_mode,
5056 availability_zone: f.availability_zone,
5057 replication_overwrite_protection: f.replication_overwrite_protection,
5058 dns_name: f.dns_name,
5059 tags: f.tags,
5060 total_size_bytes: f.total_size_bytes,
5061 size_in_standard_bytes: f.size_in_standard_bytes,
5062 size_in_ia_bytes: f.size_in_ia_bytes,
5063 size_in_archive_bytes: f.size_in_archive_bytes,
5064 })
5065 .collect();
5066 Ok(())
5067 }
5068
5069 pub async fn load_fsx_file_systems(&mut self) -> anyhow::Result<()> {
5070 let file_systems = self.fsx_client.list_file_systems().await?;
5071 self.fsx_state.file_systems.items = file_systems;
5072 Ok(())
5073 }
5074
5075 pub async fn load_fsx_backups(&mut self, file_system_id: &str) -> anyhow::Result<()> {
5076 let backups = self.fsx_client.describe_backups(file_system_id).await?;
5077 self.fsx_state.backups.items = backups;
5078 self.fsx_state.backups.selected = 0;
5079 self.fsx_state.backups.expanded_item = None;
5080 Ok(())
5081 }
5082
5083 pub async fn load_apis(&mut self) -> anyhow::Result<()> {
5084 let apis = self.apig_client.list_rest_apis().await?;
5085
5086 self.apig_state.apis.items = apis
5087 .into_iter()
5088 .map(|a| crate::apig::api::RestApi {
5089 id: a.id,
5090 name: a.name,
5091 description: a.description,
5092 created_date: a.created_date,
5093 api_key_source: a.api_key_source,
5094 endpoint_configuration: a.endpoint_configuration,
5095 protocol_type: a.protocol_type,
5096 disable_execute_api_endpoint: a.disable_execute_api_endpoint,
5097 status: a.status,
5098 })
5099 .collect();
5100
5101 self.apig_state
5102 .apis
5103 .items
5104 .sort_by(|a, b| a.name.cmp(&b.name));
5105 Ok(())
5106 }
5107
5108 pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
5109 let instances = self.ec2_client.list_instances().await?;
5110
5111 self.ec2_state.table.items = instances
5112 .into_iter()
5113 .map(|i| Ec2Instance {
5114 instance_id: i.instance_id,
5115 name: i.name,
5116 state: i.state,
5117 instance_type: i.instance_type,
5118 availability_zone: i.availability_zone,
5119 public_ipv4_dns: i.public_ipv4_dns,
5120 public_ipv4_address: i.public_ipv4_address,
5121 elastic_ip: i.elastic_ip,
5122 ipv6_ips: i.ipv6_ips,
5123 monitoring: i.monitoring,
5124 security_groups: i.security_groups,
5125 key_name: i.key_name,
5126 launch_time: i.launch_time,
5127 platform_details: i.platform_details,
5128 status_checks: i.status_checks,
5129 alarm_status: i.alarm_status,
5130 private_dns_name: i.private_dns_name,
5131 private_ip_address: i.private_ip_address,
5132 security_group_ids: String::new(),
5133 owner_id: String::new(),
5134 volume_id: String::new(),
5135 root_device_name: String::new(),
5136 root_device_type: String::new(),
5137 ebs_optimized: String::new(),
5138 image_id: String::new(),
5139 kernel_id: String::new(),
5140 ramdisk_id: String::new(),
5141 ami_launch_index: String::new(),
5142 reservation_id: String::new(),
5143 vpc_id: String::new(),
5144 subnet_ids: String::new(),
5145 instance_lifecycle: String::new(),
5146 architecture: String::new(),
5147 virtualization_type: String::new(),
5148 platform: String::new(),
5149 iam_instance_profile_arn: String::new(),
5150 tenancy: String::new(),
5151 affinity: String::new(),
5152 host_id: String::new(),
5153 placement_group: String::new(),
5154 partition_number: String::new(),
5155 capacity_reservation_id: String::new(),
5156 state_transition_reason_code: String::new(),
5157 state_transition_reason_message: String::new(),
5158 stop_hibernation_behavior: String::new(),
5159 outpost_arn: String::new(),
5160 product_codes: String::new(),
5161 availability_zone_id: String::new(),
5162 imdsv2: String::new(),
5163 usage_operation: String::new(),
5164 managed: String::new(),
5165 operator: String::new(),
5166 })
5167 .collect();
5168
5169 self.ec2_state
5171 .table
5172 .items
5173 .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
5174 Ok(())
5175 }
5176
5177 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
5178 if let Some(repo_name) = &self.ecr_state.current_repository {
5179 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
5180 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
5181
5182 self.ecr_state.images.items = images
5183 .into_iter()
5184 .map(|i| EcrImage {
5185 tag: i.tag,
5186 artifact_type: i.artifact_type,
5187 pushed_at: i.pushed_at,
5188 size_bytes: i.size_bytes,
5189 uri: i.uri,
5190 digest: i.digest,
5191 last_pull_time: i.last_pull_time,
5192 })
5193 .collect();
5194
5195 self.ecr_state
5196 .images
5197 .items
5198 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
5199 }
5200 }
5201 Ok(())
5202 }
5203
5204 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
5205 let stacks = self
5206 .cloudformation_client
5207 .list_stacks(self.cfn_state.view_nested)
5208 .await?;
5209
5210 let mut stacks: Vec<CfnStack> = stacks
5211 .into_iter()
5212 .map(|s| CfnStack {
5213 name: s.name,
5214 stack_id: s.stack_id,
5215 status: s.status,
5216 created_time: s.created_time,
5217 updated_time: s.updated_time,
5218 deleted_time: s.deleted_time,
5219 drift_status: s.drift_status,
5220 last_drift_check_time: s.last_drift_check_time,
5221 status_reason: s.status_reason,
5222 description: s.description,
5223 detailed_status: String::new(),
5224 root_stack: s.root_stack,
5225 parent_stack: s.parent_stack,
5226 termination_protection: false,
5227 iam_role: String::new(),
5228 tags: Vec::new(),
5229 stack_policy: String::new(),
5230 rollback_monitoring_time: String::new(),
5231 rollback_alarms: Vec::new(),
5232 notification_arns: Vec::new(),
5233 })
5234 .collect();
5235
5236 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
5238
5239 self.cfn_state.table.items = stacks;
5240
5241 Ok(())
5242 }
5243
5244 pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
5245 let template = self.cloudformation_client.get_template(stack_name).await?;
5246 self.cfn_state.template_body = template;
5247 self.cfn_state.template_scroll = 0;
5248 Ok(())
5249 }
5250
5251 pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
5252 let mut parameters = self
5253 .cloudformation_client
5254 .get_stack_parameters(stack_name)
5255 .await?;
5256 parameters.sort_by(|a, b| a.key.cmp(&b.key));
5257 self.cfn_state.parameters.items = parameters;
5258 self.cfn_state.parameters.reset();
5259 Ok(())
5260 }
5261
5262 pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
5263 let outputs = self
5264 .cloudformation_client
5265 .get_stack_outputs(stack_name)
5266 .await?;
5267 self.cfn_state.outputs.items = outputs;
5268 self.cfn_state.outputs.reset();
5269 Ok(())
5270 }
5271
5272 pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
5273 let resources = self
5274 .cloudformation_client
5275 .get_stack_resources(stack_name)
5276 .await?;
5277 self.cfn_state.resources.items = resources;
5278 self.cfn_state.resources.reset();
5279 Ok(())
5280 }
5281
5282 pub async fn load_cfn_events(&mut self, stack_name: &str) -> anyhow::Result<()> {
5283 let events = self
5284 .cloudformation_client
5285 .list_stack_events(stack_name)
5286 .await?;
5287 self.cfn_state.events.items = events;
5288 self.cfn_state.events.reset();
5289 Ok(())
5290 }
5291
5292 pub async fn load_cfn_change_sets(&mut self, stack_name: &str) -> anyhow::Result<()> {
5293 let change_sets = self
5294 .cloudformation_client
5295 .list_change_sets(stack_name)
5296 .await?;
5297 self.cfn_state.change_sets.items = change_sets;
5298 self.cfn_state.change_sets.reset();
5299 Ok(())
5300 }
5301
5302 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
5303 let attached_policies = self
5305 .iam_client
5306 .list_attached_role_policies(role_name)
5307 .await
5308 .map_err(|e| anyhow::anyhow!(e))?;
5309
5310 let mut policies: Vec<IamPolicy> = Vec::new();
5311 for p in attached_policies {
5312 let (creation_time, edited_time) = if let Some(arn) = p.policy_arn() {
5313 self.iam_client
5314 .get_policy_metadata(arn)
5315 .await
5316 .unwrap_or_else(|_| ("-".to_string(), "-".to_string()))
5317 } else {
5318 ("-".to_string(), "-".to_string())
5319 };
5320 policies.push(IamPolicy {
5321 policy_name: p.policy_name().unwrap_or("").to_string(),
5322 policy_type: "Managed".to_string(),
5323 attached_via: "Direct".to_string(),
5324 attached_entities: "-".to_string(),
5325 description: "-".to_string(),
5326 creation_time,
5327 edited_time,
5328 policy_arn: p.policy_arn().map(|s| s.to_string()),
5329 });
5330 }
5331
5332 let inline_policy_names = self
5334 .iam_client
5335 .list_role_policies(role_name)
5336 .await
5337 .map_err(|e| anyhow::anyhow!(e))?;
5338
5339 for policy_name in inline_policy_names {
5340 policies.push(IamPolicy {
5341 policy_name,
5342 policy_type: "Inline".to_string(),
5343 attached_via: "Direct".to_string(),
5344 attached_entities: "-".to_string(),
5345 description: "-".to_string(),
5346 creation_time: "-".to_string(),
5347 edited_time: "-".to_string(),
5348 policy_arn: None,
5349 });
5350 }
5351
5352 self.iam_state.policies.items = policies;
5353
5354 Ok(())
5355 }
5356
5357 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
5358 let attached_policies = self
5359 .iam_client
5360 .list_attached_group_policies(group_name)
5361 .await
5362 .map_err(|e| anyhow::anyhow!(e))?;
5363
5364 let mut policies: Vec<IamPolicy> = attached_policies
5365 .into_iter()
5366 .map(|p| IamPolicy {
5367 policy_name: p.policy_name().unwrap_or("").to_string(),
5368 policy_type: "AWS managed".to_string(),
5369 attached_via: "Direct".to_string(),
5370 attached_entities: "-".to_string(),
5371 description: "-".to_string(),
5372 creation_time: "-".to_string(),
5373 edited_time: "-".to_string(),
5374 policy_arn: p.policy_arn().map(|s| s.to_string()),
5375 })
5376 .collect();
5377
5378 let inline_policy_names = self
5379 .iam_client
5380 .list_group_policies(group_name)
5381 .await
5382 .map_err(|e| anyhow::anyhow!(e))?;
5383
5384 for policy_name in inline_policy_names {
5385 policies.push(IamPolicy {
5386 policy_name,
5387 policy_type: "Inline".to_string(),
5388 attached_via: "Direct".to_string(),
5389 attached_entities: "-".to_string(),
5390 description: "-".to_string(),
5391 creation_time: "-".to_string(),
5392 edited_time: "-".to_string(),
5393 policy_arn: None,
5394 });
5395 }
5396
5397 self.iam_state.policies.items = policies;
5398
5399 Ok(())
5400 }
5401
5402 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
5403 let users = self
5404 .iam_client
5405 .get_group_users(group_name)
5406 .await
5407 .map_err(|e| anyhow::anyhow!(e))?;
5408
5409 let group_users: Vec<IamGroupUser> = users
5410 .into_iter()
5411 .map(|u| {
5412 let creation_time = {
5413 let dt = u.create_date();
5414 let timestamp = dt.secs();
5415 let datetime =
5416 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
5417 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
5418 };
5419
5420 IamGroupUser {
5421 user_name: u.user_name().to_string(),
5422 groups: String::new(),
5423 last_activity: String::new(),
5424 creation_time,
5425 }
5426 })
5427 .collect();
5428
5429 self.iam_state.group_users.items = group_users;
5430
5431 Ok(())
5432 }
5433
5434 pub async fn load_policy_document(
5435 &mut self,
5436 role_name: &str,
5437 policy_name: &str,
5438 ) -> anyhow::Result<()> {
5439 let policy = self
5441 .iam_state
5442 .policies
5443 .items
5444 .iter()
5445 .find(|p| p.policy_name == policy_name)
5446 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
5447
5448 let document = if let Some(policy_arn) = &policy.policy_arn {
5449 self.iam_client
5451 .get_policy_version(policy_arn)
5452 .await
5453 .map_err(|e| anyhow::anyhow!(e))?
5454 } else {
5455 self.iam_client
5457 .get_role_policy(role_name, policy_name)
5458 .await
5459 .map_err(|e| anyhow::anyhow!(e))?
5460 };
5461
5462 self.iam_state.policy_document = document;
5463
5464 Ok(())
5465 }
5466
5467 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
5468 let document = self
5469 .iam_client
5470 .get_role(role_name)
5471 .await
5472 .map_err(|e| anyhow::anyhow!(e))?;
5473
5474 self.iam_state.trust_policy_document = document;
5475
5476 Ok(())
5477 }
5478
5479 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
5480 self.iam_state.last_accessed_services.items = vec![];
5482 self.iam_state.last_accessed_services.selected = 0;
5483
5484 Ok(())
5485 }
5486
5487 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
5488 let tags = self
5489 .iam_client
5490 .list_role_tags(role_name)
5491 .await
5492 .map_err(|e| anyhow::anyhow!(e))?;
5493 self.iam_state.tags.items = tags
5494 .into_iter()
5495 .map(|(k, v)| IamRoleTag { key: k, value: v })
5496 .collect();
5497 self.iam_state.tags.reset();
5498 Ok(())
5499 }
5500
5501 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
5502 let tags = self
5503 .iam_client
5504 .list_user_tags(user_name)
5505 .await
5506 .map_err(|e| anyhow::anyhow!(e))?;
5507 self.iam_state.user_tags.items = tags
5508 .into_iter()
5509 .map(|(k, v)| IamUserTag { key: k, value: v })
5510 .collect();
5511 self.iam_state.user_tags.reset();
5512 Ok(())
5513 }
5514
5515 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
5516 if let Some(group) = self
5517 .log_groups_state
5518 .log_groups
5519 .items
5520 .get(self.log_groups_state.log_groups.selected)
5521 {
5522 self.log_groups_state.log_streams =
5523 self.cloudwatch_client.list_log_streams(&group.name).await?;
5524 self.log_groups_state.selected_stream = 0;
5525 }
5526 Ok(())
5527 }
5528
5529 pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
5530 if let Some(group) = self
5531 .log_groups_state
5532 .log_groups
5533 .items
5534 .get(self.log_groups_state.log_groups.selected)
5535 {
5536 let arn = if let Some(arn) = &group.log_group_arn {
5538 arn.clone()
5539 } else if let Some(arn) = &group.arn {
5540 arn.clone()
5541 } else {
5542 let account_id = if self.config.account_id.is_empty() {
5544 "*"
5545 } else {
5546 &self.config.account_id
5547 };
5548 format!(
5549 "arn:aws:logs:{}:{}:log-group:{}",
5550 self.config.region, account_id, group.name
5551 )
5552 };
5553
5554 let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
5555 self.log_groups_state.tags.items = tags;
5556 self.log_groups_state.tags.selected = 0;
5557 self.log_groups_state.tags.scroll_offset = 0;
5558 }
5559 Ok(())
5560 }
5561
5562 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
5563 if let Some(group) = self
5564 .log_groups_state
5565 .log_groups
5566 .items
5567 .get(self.log_groups_state.log_groups.selected)
5568 {
5569 if let Some(stream) = self
5570 .log_groups_state
5571 .log_streams
5572 .get(self.log_groups_state.selected_stream)
5573 {
5574 let (start_time, end_time) =
5576 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
5577 let now = chrono::Utc::now().timestamp_millis();
5578 let duration_ms = match self.log_groups_state.relative_unit {
5579 TimeUnit::Minutes => amount * 60 * 1000,
5580 TimeUnit::Hours => amount * 60 * 60 * 1000,
5581 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
5582 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
5583 };
5584 (Some(now - duration_ms), Some(now))
5585 } else {
5586 (None, None)
5587 };
5588
5589 let (mut events, has_more, token) = self
5590 .cloudwatch_client
5591 .get_log_events(
5592 &group.name,
5593 &stream.name,
5594 self.log_groups_state.next_backward_token.clone(),
5595 start_time,
5596 end_time,
5597 )
5598 .await?;
5599
5600 if self.log_groups_state.next_backward_token.is_some() {
5601 events.append(&mut self.log_groups_state.log_events);
5603 self.log_groups_state.event_scroll_offset = 0;
5604 } else {
5605 self.log_groups_state.event_scroll_offset = 0;
5607 }
5608
5609 self.log_groups_state.log_events = events;
5610 self.log_groups_state.has_older_events =
5611 has_more && self.log_groups_state.log_events.len() >= 25;
5612 self.log_groups_state.next_backward_token = token;
5613 self.log_groups_state.selected_event = 0;
5614 }
5615 }
5616 Ok(())
5617 }
5618
5619 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
5620 if self.insights_state.insights.selected_log_groups.is_empty() {
5621 return Err(anyhow::anyhow!(
5622 "No log groups selected. Please select at least one log group."
5623 ));
5624 }
5625
5626 let now = chrono::Utc::now().timestamp_millis();
5627 let amount = self
5628 .insights_state
5629 .insights
5630 .insights_relative_amount
5631 .parse::<i64>()
5632 .unwrap_or(1);
5633 let duration_ms = match self.insights_state.insights.insights_relative_unit {
5634 TimeUnit::Minutes => amount * 60 * 1000,
5635 TimeUnit::Hours => amount * 60 * 60 * 1000,
5636 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
5637 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
5638 };
5639 let start_time = now - duration_ms;
5640
5641 let query_id = self
5642 .cloudwatch_client
5643 .start_query(
5644 self.insights_state.insights.selected_log_groups.clone(),
5645 self.insights_state.insights.query_text.trim().to_string(),
5646 start_time,
5647 now,
5648 )
5649 .await?;
5650
5651 for _ in 0..60 {
5653 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
5654 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
5655
5656 if status == "Complete" {
5657 self.insights_state.insights.query_results = results;
5658 self.insights_state.insights.query_completed = true;
5659 self.insights_state.insights.results_selected = 0;
5660 self.insights_state.insights.expanded_result = None;
5661 self.view_mode = ViewMode::InsightsResults;
5662 return Ok(());
5663 } else if status == "Failed" || status == "Cancelled" {
5664 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
5665 }
5666 }
5667
5668 Err(anyhow::anyhow!("Query timeout"))
5669 }
5670
5671 pub(crate) fn toggle_column_visibility<T: PartialEq + Copy>(
5673 visible_columns: &mut Vec<T>,
5674 _all_columns: &[T],
5675 column_to_toggle: T,
5676 ) {
5677 if let Some(pos) = visible_columns.iter().position(|c| c == &column_to_toggle) {
5678 if visible_columns.len() > 1 {
5680 visible_columns.remove(pos);
5681 }
5682 } else {
5683 visible_columns.push(column_to_toggle);
5684 }
5685 }
5686}
5687
5688impl CloudWatchInsightsState {
5689 fn new() -> Self {
5690 Self {
5691 insights: InsightsState::default(),
5692 loading: false,
5693 }
5694 }
5695}
5696
5697impl CloudWatchAlarmsState {
5698 fn new() -> Self {
5699 Self {
5700 table: TableState::new(),
5701 current_alarm: None,
5702 alarm_tab: AlarmTab::AllAlarms,
5703 detail_tab: AlarmDetailTab::Details,
5704 view_as: AlarmViewMode::Table,
5705 wrap_lines: false,
5706 sort_column: "Last state update".to_string(),
5707 sort_direction: SortDirection::Asc,
5708 input_focus: InputFocus::Filter,
5709 metric_data: Vec::new(),
5710 metrics_loading: false,
5711 }
5712 }
5713}
5714
5715impl ServicePickerState {
5716 fn new() -> Self {
5717 Self {
5718 filter: String::new(),
5719 filter_active: false,
5720 selected: 0,
5721 services: vec![
5722 "API Gateway › APIs",
5723 "CloudWatch › Log Groups",
5724 "CloudWatch › Logs Insights",
5725 "CloudWatch › Alarms",
5726 "CloudTrail › Event History",
5727 "CloudFormation › Stacks",
5728 "EC2 › Instances",
5729 "ECR › Repositories",
5730 "IAM › Users",
5731 "IAM › Roles",
5732 "IAM › User Groups",
5733 "KMS › Managed Keys",
5734 "EFS › File Systems",
5735 "FSx › File Systems",
5736 "Lambda › Functions",
5737 "Lambda › Applications",
5738 "S3 › Buckets",
5739 "SQS › Queues",
5740 ],
5741 }
5742 }
5743}
5744
5745#[cfg(test)]
5746mod test_helpers {
5747 use super::*;
5748
5749 pub fn test_app() -> App {
5751 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
5752 }
5753
5754 pub fn test_app_no_region() -> App {
5755 App::new_without_client("test".to_string(), None)
5756 }
5757}
5758
5759#[cfg(test)]
5760mod tests {
5761 use super::*;
5762 use crate::keymap::Action;
5763 use test_helpers::*;
5764
5765 #[test]
5766 fn test_next_tab_cycles_forward() {
5767 let mut app = test_app();
5768 app.tabs = vec![
5769 Tab {
5770 service: Service::CloudWatchLogGroups,
5771 title: "CloudWatch › Log Groups".to_string(),
5772 breadcrumb: "CloudWatch › Log Groups".to_string(),
5773 },
5774 Tab {
5775 service: Service::CloudWatchInsights,
5776 title: "CloudWatch › Logs Insights".to_string(),
5777 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5778 },
5779 Tab {
5780 service: Service::CloudWatchAlarms,
5781 title: "CloudWatch › Alarms".to_string(),
5782 breadcrumb: "CloudWatch › Alarms".to_string(),
5783 },
5784 ];
5785 app.current_tab = 0;
5786
5787 app.handle_action(Action::NextTab);
5788 assert_eq!(app.current_tab, 1);
5789 assert_eq!(app.current_service, Service::CloudWatchInsights);
5790
5791 app.handle_action(Action::NextTab);
5792 assert_eq!(app.current_tab, 2);
5793 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5794
5795 app.handle_action(Action::NextTab);
5797 assert_eq!(app.current_tab, 0);
5798 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
5799 }
5800
5801 #[test]
5802 fn test_prev_tab_cycles_backward() {
5803 let mut app = test_app();
5804 app.tabs = vec![
5805 Tab {
5806 service: Service::CloudWatchLogGroups,
5807 title: "CloudWatch › Log Groups".to_string(),
5808 breadcrumb: "CloudWatch › Log Groups".to_string(),
5809 },
5810 Tab {
5811 service: Service::CloudWatchInsights,
5812 title: "CloudWatch › Logs Insights".to_string(),
5813 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5814 },
5815 Tab {
5816 service: Service::CloudWatchAlarms,
5817 title: "CloudWatch › Alarms".to_string(),
5818 breadcrumb: "CloudWatch › Alarms".to_string(),
5819 },
5820 ];
5821 app.current_tab = 2;
5822
5823 app.handle_action(Action::PrevTab);
5824 assert_eq!(app.current_tab, 1);
5825 assert_eq!(app.current_service, Service::CloudWatchInsights);
5826
5827 app.handle_action(Action::PrevTab);
5828 assert_eq!(app.current_tab, 0);
5829 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
5830
5831 app.handle_action(Action::PrevTab);
5833 assert_eq!(app.current_tab, 2);
5834 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5835 }
5836
5837 #[test]
5838 fn test_close_tab_removes_current() {
5839 let mut app = test_app();
5840 app.tabs = vec![
5841 Tab {
5842 service: Service::CloudWatchLogGroups,
5843 title: "CloudWatch › Log Groups".to_string(),
5844 breadcrumb: "CloudWatch › Log Groups".to_string(),
5845 },
5846 Tab {
5847 service: Service::CloudWatchInsights,
5848 title: "CloudWatch › Logs Insights".to_string(),
5849 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5850 },
5851 Tab {
5852 service: Service::CloudWatchAlarms,
5853 title: "CloudWatch › Alarms".to_string(),
5854 breadcrumb: "CloudWatch › Alarms".to_string(),
5855 },
5856 ];
5857 app.current_tab = 1;
5858 app.service_selected = true;
5859
5860 app.handle_action(Action::CloseTab);
5861 assert_eq!(app.tabs.len(), 2);
5862 assert_eq!(app.current_tab, 1);
5863 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5864 }
5865
5866 #[test]
5867 fn test_close_last_tab_exits_service() {
5868 let mut app = test_app();
5869 app.tabs = vec![Tab {
5870 service: Service::CloudWatchLogGroups,
5871 title: "CloudWatch › Log Groups".to_string(),
5872 breadcrumb: "CloudWatch › Log Groups".to_string(),
5873 }];
5874 app.current_tab = 0;
5875 app.service_selected = true;
5876
5877 app.handle_action(Action::CloseTab);
5878 assert_eq!(app.tabs.len(), 0);
5879 assert!(!app.service_selected);
5880 assert_eq!(app.current_tab, 0);
5881 }
5882
5883 #[test]
5884 fn test_close_service_removes_current_tab() {
5885 let mut app = test_app();
5886 app.tabs = vec![
5887 Tab {
5888 service: Service::CloudWatchLogGroups,
5889 title: "CloudWatch › Log Groups".to_string(),
5890 breadcrumb: "CloudWatch › Log Groups".to_string(),
5891 },
5892 Tab {
5893 service: Service::CloudWatchInsights,
5894 title: "CloudWatch › Logs Insights".to_string(),
5895 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5896 },
5897 Tab {
5898 service: Service::CloudWatchAlarms,
5899 title: "CloudWatch › Alarms".to_string(),
5900 breadcrumb: "CloudWatch › Alarms".to_string(),
5901 },
5902 ];
5903 app.current_tab = 1;
5904 app.service_selected = true;
5905
5906 app.handle_action(Action::CloseService);
5907
5908 assert_eq!(app.tabs.len(), 2);
5910 assert_eq!(app.current_tab, 1);
5912 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5913 assert!(app.service_selected);
5915 assert_eq!(app.mode, Mode::Normal);
5916 }
5917
5918 #[test]
5919 fn test_close_service_last_tab_shows_picker() {
5920 let mut app = test_app();
5921 app.tabs = vec![Tab {
5922 service: Service::CloudWatchLogGroups,
5923 title: "CloudWatch › Log Groups".to_string(),
5924 breadcrumb: "CloudWatch › Log Groups".to_string(),
5925 }];
5926 app.current_tab = 0;
5927 app.service_selected = true;
5928
5929 app.handle_action(Action::CloseService);
5930
5931 assert_eq!(app.tabs.len(), 0);
5933 assert!(!app.service_selected);
5935 assert_eq!(app.mode, Mode::ServicePicker);
5936 }
5937
5938 #[test]
5939 fn test_open_tab_picker_with_tabs() {
5940 let mut app = test_app();
5941 app.tabs = vec![
5942 Tab {
5943 service: Service::CloudWatchLogGroups,
5944 title: "CloudWatch › Log Groups".to_string(),
5945 breadcrumb: "CloudWatch › Log Groups".to_string(),
5946 },
5947 Tab {
5948 service: Service::CloudWatchInsights,
5949 title: "CloudWatch › Logs Insights".to_string(),
5950 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5951 },
5952 ];
5953 app.current_tab = 1;
5954
5955 app.handle_action(Action::OpenTabPicker);
5956 assert_eq!(app.mode, Mode::TabPicker);
5957 assert_eq!(app.tab_picker_selected, 1);
5958 }
5959
5960 #[test]
5961 fn test_open_tab_picker_without_tabs() {
5962 let mut app = test_app();
5963 app.tabs = vec![];
5964
5965 app.handle_action(Action::OpenTabPicker);
5966 assert_eq!(app.mode, Mode::Normal);
5967 }
5968
5969 #[test]
5970 fn test_pending_key_state() {
5971 let mut app = test_app();
5972 assert_eq!(app.pending_key, None);
5973
5974 app.pending_key = Some('g');
5975 assert_eq!(app.pending_key, Some('g'));
5976 }
5977
5978 #[test]
5979 fn test_tab_breadcrumb_updates() {
5980 let mut app = test_app();
5981 app.tabs = vec![Tab {
5982 service: Service::CloudWatchLogGroups,
5983 title: "CloudWatch › Log Groups".to_string(),
5984 breadcrumb: "CloudWatch > Log groups".to_string(),
5985 }];
5986 app.current_tab = 0;
5987 app.service_selected = true;
5988 app.current_service = Service::CloudWatchLogGroups;
5989
5990 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
5992
5993 app.log_groups_state
5995 .log_groups
5996 .items
5997 .push(rusticity_core::LogGroup {
5998 name: "/aws/lambda/test".to_string(),
5999 creation_time: None,
6000 stored_bytes: Some(1024),
6001 retention_days: None,
6002 log_class: None,
6003 arn: None,
6004 log_group_arn: None,
6005 deletion_protection_enabled: None,
6006 });
6007 app.log_groups_state.log_groups.reset();
6008 app.view_mode = ViewMode::Detail;
6009 app.update_current_tab_breadcrumb();
6010
6011 assert_eq!(
6013 app.tabs[0].breadcrumb,
6014 "CloudWatch > Log groups > /aws/lambda/test"
6015 );
6016 }
6017
6018 #[test]
6019 fn test_s3_bucket_column_selector_navigation() {
6020 let mut app = test_app();
6021 app.current_service = Service::S3Buckets;
6022 app.mode = Mode::ColumnSelector;
6023 app.column_selector_index = 0;
6024
6025 app.handle_action(Action::NextItem);
6027 assert_eq!(app.column_selector_index, 1);
6028
6029 app.handle_action(Action::NextItem);
6030 assert_eq!(app.column_selector_index, 2);
6031
6032 app.handle_action(Action::NextItem);
6033 assert_eq!(app.column_selector_index, 3);
6034
6035 for _ in 0..10 {
6037 app.handle_action(Action::NextItem);
6038 }
6039 assert_eq!(app.column_selector_index, 9);
6040
6041 app.handle_action(Action::PrevItem);
6043 assert_eq!(app.column_selector_index, 8);
6044
6045 app.handle_action(Action::PrevItem);
6046 assert_eq!(app.column_selector_index, 7);
6047
6048 for _ in 0..10 {
6050 app.handle_action(Action::PrevItem);
6051 }
6052 assert_eq!(app.column_selector_index, 0);
6053 }
6054
6055 #[test]
6056 fn test_cloudwatch_alarms_state_initialized() {
6057 let app = test_app();
6058
6059 assert_eq!(app.alarms_state.table.items.len(), 0);
6061 assert_eq!(app.alarms_state.table.selected, 0);
6062 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
6063 assert!(!app.alarms_state.table.loading);
6064 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
6065 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
6066 }
6067
6068 #[test]
6069 fn test_cloudwatch_alarms_service_selection() {
6070 let mut app = test_app();
6071
6072 app.current_service = Service::CloudWatchAlarms;
6074 app.service_selected = true;
6075
6076 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6077 assert!(app.service_selected);
6078 }
6079
6080 #[test]
6081 fn test_cloudwatch_alarms_column_preferences() {
6082 let app = test_app();
6083
6084 assert!(!app.cw_alarm_column_ids.is_empty());
6086 assert!(!app.cw_alarm_visible_column_ids.is_empty());
6087
6088 assert!(app
6090 .cw_alarm_visible_column_ids
6091 .contains(&AlarmColumn::Name.id()));
6092 assert!(app
6093 .cw_alarm_visible_column_ids
6094 .contains(&AlarmColumn::State.id()));
6095 }
6096
6097 #[test]
6098 fn test_s3_bucket_navigation_without_expansion() {
6099 let mut app = test_app();
6100 app.current_service = Service::S3Buckets;
6101 app.service_selected = true;
6102 app.mode = Mode::Normal;
6103
6104 app.s3_state.buckets.items = vec![
6106 S3Bucket {
6107 name: "bucket1".to_string(),
6108 region: "us-east-1".to_string(),
6109 creation_date: "2024-01-01T00:00:00Z".to_string(),
6110 },
6111 S3Bucket {
6112 name: "bucket2".to_string(),
6113 region: "us-east-1".to_string(),
6114 creation_date: "2024-01-02T00:00:00Z".to_string(),
6115 },
6116 S3Bucket {
6117 name: "bucket3".to_string(),
6118 region: "us-east-1".to_string(),
6119 creation_date: "2024-01-03T00:00:00Z".to_string(),
6120 },
6121 ];
6122 app.s3_state.selected_row = 0;
6123
6124 app.handle_action(Action::NextItem);
6126 assert_eq!(app.s3_state.selected_row, 1);
6127
6128 app.handle_action(Action::NextItem);
6129 assert_eq!(app.s3_state.selected_row, 2);
6130
6131 app.handle_action(Action::NextItem);
6133 assert_eq!(app.s3_state.selected_row, 2);
6134
6135 app.handle_action(Action::PrevItem);
6137 assert_eq!(app.s3_state.selected_row, 1);
6138
6139 app.handle_action(Action::PrevItem);
6140 assert_eq!(app.s3_state.selected_row, 0);
6141
6142 app.handle_action(Action::PrevItem);
6144 assert_eq!(app.s3_state.selected_row, 0);
6145 }
6146
6147 #[test]
6148 fn test_s3_bucket_navigation_with_expansion() {
6149 let mut app = test_app();
6150 app.current_service = Service::S3Buckets;
6151 app.service_selected = true;
6152 app.mode = Mode::Normal;
6153
6154 app.s3_state.buckets.items = vec![
6156 S3Bucket {
6157 name: "bucket1".to_string(),
6158 region: "us-east-1".to_string(),
6159 creation_date: "2024-01-01T00:00:00Z".to_string(),
6160 },
6161 S3Bucket {
6162 name: "bucket2".to_string(),
6163 region: "us-east-1".to_string(),
6164 creation_date: "2024-01-02T00:00:00Z".to_string(),
6165 },
6166 ];
6167
6168 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6170 app.s3_state.bucket_preview.insert(
6171 "bucket1".to_string(),
6172 vec![
6173 S3Object {
6174 key: "file1.txt".to_string(),
6175 size: 100,
6176 last_modified: "2024-01-01T00:00:00Z".to_string(),
6177 is_prefix: false,
6178 storage_class: "STANDARD".to_string(),
6179 },
6180 S3Object {
6181 key: "folder/".to_string(),
6182 size: 0,
6183 last_modified: "2024-01-01T00:00:00Z".to_string(),
6184 is_prefix: true,
6185 storage_class: String::new(),
6186 },
6187 ],
6188 );
6189
6190 app.s3_state.selected_row = 0;
6191
6192 app.handle_action(Action::NextItem);
6195 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
6198 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
6201 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
6205 assert_eq!(app.s3_state.selected_row, 3);
6206 }
6207
6208 #[test]
6209 fn test_s3_bucket_navigation_with_nested_expansion() {
6210 let mut app = test_app();
6211 app.current_service = Service::S3Buckets;
6212 app.service_selected = true;
6213 app.mode = Mode::Normal;
6214
6215 app.s3_state.buckets.items = vec![S3Bucket {
6217 name: "bucket1".to_string(),
6218 region: "us-east-1".to_string(),
6219 creation_date: "2024-01-01T00:00:00Z".to_string(),
6220 }];
6221
6222 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6224 app.s3_state.bucket_preview.insert(
6225 "bucket1".to_string(),
6226 vec![S3Object {
6227 key: "folder/".to_string(),
6228 size: 0,
6229 last_modified: "2024-01-01T00:00:00Z".to_string(),
6230 is_prefix: true,
6231 storage_class: String::new(),
6232 }],
6233 );
6234
6235 app.s3_state.expanded_prefixes.insert("folder/".to_string());
6237 app.s3_state.prefix_preview.insert(
6238 "folder/".to_string(),
6239 vec![
6240 S3Object {
6241 key: "folder/file1.txt".to_string(),
6242 size: 100,
6243 last_modified: "2024-01-01T00:00:00Z".to_string(),
6244 is_prefix: false,
6245 storage_class: "STANDARD".to_string(),
6246 },
6247 S3Object {
6248 key: "folder/file2.txt".to_string(),
6249 size: 200,
6250 last_modified: "2024-01-01T00:00:00Z".to_string(),
6251 is_prefix: false,
6252 storage_class: "STANDARD".to_string(),
6253 },
6254 ],
6255 );
6256
6257 app.s3_state.selected_row = 0;
6258
6259 app.handle_action(Action::NextItem);
6261 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
6264 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
6267 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
6271 assert_eq!(app.s3_state.selected_row, 3);
6272 }
6273
6274 #[test]
6275 fn test_calculate_total_bucket_rows() {
6276 let mut app = test_app();
6277
6278 assert_eq!(app.calculate_total_bucket_rows(), 0);
6280
6281 app.s3_state.buckets.items = vec![
6283 S3Bucket {
6284 name: "bucket1".to_string(),
6285 region: "us-east-1".to_string(),
6286 creation_date: "2024-01-01T00:00:00Z".to_string(),
6287 },
6288 S3Bucket {
6289 name: "bucket2".to_string(),
6290 region: "us-east-1".to_string(),
6291 creation_date: "2024-01-02T00:00:00Z".to_string(),
6292 },
6293 ];
6294 assert_eq!(app.calculate_total_bucket_rows(), 2);
6295
6296 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6298 app.s3_state.bucket_preview.insert(
6299 "bucket1".to_string(),
6300 vec![
6301 S3Object {
6302 key: "file1.txt".to_string(),
6303 size: 100,
6304 last_modified: "2024-01-01T00:00:00Z".to_string(),
6305 is_prefix: false,
6306 storage_class: "STANDARD".to_string(),
6307 },
6308 S3Object {
6309 key: "file2.txt".to_string(),
6310 size: 200,
6311 last_modified: "2024-01-01T00:00:00Z".to_string(),
6312 is_prefix: false,
6313 storage_class: "STANDARD".to_string(),
6314 },
6315 S3Object {
6316 key: "folder/".to_string(),
6317 size: 0,
6318 last_modified: "2024-01-01T00:00:00Z".to_string(),
6319 is_prefix: true,
6320 storage_class: String::new(),
6321 },
6322 ],
6323 );
6324 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
6328 app.s3_state.prefix_preview.insert(
6329 "folder/".to_string(),
6330 vec![
6331 S3Object {
6332 key: "folder/nested1.txt".to_string(),
6333 size: 50,
6334 last_modified: "2024-01-01T00:00:00Z".to_string(),
6335 is_prefix: false,
6336 storage_class: "STANDARD".to_string(),
6337 },
6338 S3Object {
6339 key: "folder/nested2.txt".to_string(),
6340 size: 75,
6341 last_modified: "2024-01-01T00:00:00Z".to_string(),
6342 is_prefix: false,
6343 storage_class: "STANDARD".to_string(),
6344 },
6345 ],
6346 );
6347 assert_eq!(app.calculate_total_bucket_rows(), 7); }
6349
6350 #[test]
6351 fn test_calculate_total_object_rows() {
6352 let mut app = test_app();
6353 app.s3_state.current_bucket = Some("test-bucket".to_string());
6354
6355 assert_eq!(app.calculate_total_object_rows(), 0);
6357
6358 app.s3_state.objects = vec![
6360 S3Object {
6361 key: "file1.txt".to_string(),
6362 size: 100,
6363 last_modified: "2024-01-01T00:00:00Z".to_string(),
6364 is_prefix: false,
6365 storage_class: "STANDARD".to_string(),
6366 },
6367 S3Object {
6368 key: "folder/".to_string(),
6369 size: 0,
6370 last_modified: "2024-01-01T00:00:00Z".to_string(),
6371 is_prefix: true,
6372 storage_class: String::new(),
6373 },
6374 ];
6375 assert_eq!(app.calculate_total_object_rows(), 2);
6376
6377 app.s3_state.expanded_prefixes.insert("folder/".to_string());
6379 app.s3_state.prefix_preview.insert(
6380 "folder/".to_string(),
6381 vec![
6382 S3Object {
6383 key: "folder/file2.txt".to_string(),
6384 size: 200,
6385 last_modified: "2024-01-01T00:00:00Z".to_string(),
6386 is_prefix: false,
6387 storage_class: "STANDARD".to_string(),
6388 },
6389 S3Object {
6390 key: "folder/subfolder/".to_string(),
6391 size: 0,
6392 last_modified: "2024-01-01T00:00:00Z".to_string(),
6393 is_prefix: true,
6394 storage_class: String::new(),
6395 },
6396 ],
6397 );
6398 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
6402 .expanded_prefixes
6403 .insert("folder/subfolder/".to_string());
6404 app.s3_state.prefix_preview.insert(
6405 "folder/subfolder/".to_string(),
6406 vec![S3Object {
6407 key: "folder/subfolder/deep.txt".to_string(),
6408 size: 50,
6409 last_modified: "2024-01-01T00:00:00Z".to_string(),
6410 is_prefix: false,
6411 storage_class: "STANDARD".to_string(),
6412 }],
6413 );
6414 assert_eq!(app.calculate_total_object_rows(), 5); }
6416
6417 #[test]
6418 fn test_s3_object_navigation_with_deep_nesting() {
6419 let mut app = test_app();
6420 app.current_service = Service::S3Buckets;
6421 app.service_selected = true;
6422 app.mode = Mode::Normal;
6423 app.s3_state.current_bucket = Some("test-bucket".to_string());
6424
6425 app.s3_state.objects = vec![S3Object {
6427 key: "folder1/".to_string(),
6428 size: 0,
6429 last_modified: "2024-01-01T00:00:00Z".to_string(),
6430 is_prefix: true,
6431 storage_class: String::new(),
6432 }];
6433
6434 app.s3_state
6436 .expanded_prefixes
6437 .insert("folder1/".to_string());
6438 app.s3_state.prefix_preview.insert(
6439 "folder1/".to_string(),
6440 vec![S3Object {
6441 key: "folder1/folder2/".to_string(),
6442 size: 0,
6443 last_modified: "2024-01-01T00:00:00Z".to_string(),
6444 is_prefix: true,
6445 storage_class: String::new(),
6446 }],
6447 );
6448
6449 app.s3_state
6451 .expanded_prefixes
6452 .insert("folder1/folder2/".to_string());
6453 app.s3_state.prefix_preview.insert(
6454 "folder1/folder2/".to_string(),
6455 vec![S3Object {
6456 key: "folder1/folder2/file.txt".to_string(),
6457 size: 100,
6458 last_modified: "2024-01-01T00:00:00Z".to_string(),
6459 is_prefix: false,
6460 storage_class: "STANDARD".to_string(),
6461 }],
6462 );
6463
6464 app.s3_state.selected_object = 0;
6465
6466 app.handle_action(Action::NextItem);
6468 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
6471 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
6475 assert_eq!(app.s3_state.selected_object, 2);
6476 }
6477
6478 #[test]
6479 fn test_s3_expand_nested_folder_in_objects_view() {
6480 let mut app = test_app();
6481 app.current_service = Service::S3Buckets;
6482 app.service_selected = true;
6483 app.mode = Mode::Normal;
6484 app.s3_state.current_bucket = Some("test-bucket".to_string());
6485
6486 app.s3_state.objects = vec![S3Object {
6488 key: "parent/".to_string(),
6489 size: 0,
6490 last_modified: "2024-01-01T00:00:00Z".to_string(),
6491 is_prefix: true,
6492 storage_class: String::new(),
6493 }];
6494
6495 app.s3_state.expanded_prefixes.insert("parent/".to_string());
6497 app.s3_state.prefix_preview.insert(
6498 "parent/".to_string(),
6499 vec![S3Object {
6500 key: "parent/child/".to_string(),
6501 size: 0,
6502 last_modified: "2024-01-01T00:00:00Z".to_string(),
6503 is_prefix: true,
6504 storage_class: String::new(),
6505 }],
6506 );
6507
6508 app.s3_state.selected_object = 1;
6510
6511 app.handle_action(Action::NextPane);
6513
6514 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
6516 assert!(app.s3_state.buckets.loading); }
6518
6519 #[test]
6520 fn test_s3_drill_into_nested_folder() {
6521 let mut app = test_app();
6522 app.current_service = Service::S3Buckets;
6523 app.service_selected = true;
6524 app.mode = Mode::Normal;
6525 app.s3_state.current_bucket = Some("test-bucket".to_string());
6526
6527 app.s3_state.objects = vec![S3Object {
6529 key: "parent/".to_string(),
6530 size: 0,
6531 last_modified: "2024-01-01T00:00:00Z".to_string(),
6532 is_prefix: true,
6533 storage_class: String::new(),
6534 }];
6535
6536 app.s3_state.expanded_prefixes.insert("parent/".to_string());
6538 app.s3_state.prefix_preview.insert(
6539 "parent/".to_string(),
6540 vec![S3Object {
6541 key: "parent/child/".to_string(),
6542 size: 0,
6543 last_modified: "2024-01-01T00:00:00Z".to_string(),
6544 is_prefix: true,
6545 storage_class: String::new(),
6546 }],
6547 );
6548
6549 app.s3_state.selected_object = 1;
6551
6552 app.handle_action(Action::Select);
6554
6555 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
6557 assert!(app.s3_state.buckets.loading); }
6559
6560 #[test]
6561 fn test_s3_esc_pops_navigation_stack() {
6562 let mut app = test_app();
6563 app.current_service = Service::S3Buckets;
6564 app.s3_state.current_bucket = Some("test-bucket".to_string());
6565 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
6566
6567 app.handle_action(Action::GoBack);
6569 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
6570 assert!(app.s3_state.buckets.loading);
6571
6572 app.s3_state.buckets.loading = false;
6574 app.handle_action(Action::GoBack);
6575 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
6576 assert!(app.s3_state.buckets.loading);
6577
6578 app.s3_state.buckets.loading = false;
6580 app.handle_action(Action::GoBack);
6581 assert_eq!(app.s3_state.current_bucket, None);
6582 }
6583
6584 #[test]
6585 fn test_s3_esc_from_bucket_root_exits() {
6586 let mut app = test_app();
6587 app.current_service = Service::S3Buckets;
6588 app.s3_state.current_bucket = Some("test-bucket".to_string());
6589 app.s3_state.prefix_stack = vec![];
6590
6591 app.handle_action(Action::GoBack);
6593 assert_eq!(app.s3_state.current_bucket, None);
6594 assert_eq!(app.s3_state.objects.len(), 0);
6595 }
6596
6597 #[test]
6598 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
6599 let mut app = test_app();
6600 app.current_service = Service::S3Buckets;
6601 app.service_selected = true;
6602 app.mode = Mode::Normal;
6603
6604 app.s3_state.buckets.items = vec![S3Bucket {
6606 name: "test-bucket".to_string(),
6607 region: "us-east-1".to_string(),
6608 creation_date: "2024-01-01".to_string(),
6609 }];
6610
6611 app.s3_state
6613 .expanded_prefixes
6614 .insert("test-bucket".to_string());
6615 app.s3_state.bucket_preview.insert(
6616 "test-bucket".to_string(),
6617 vec![S3Object {
6618 key: "parent/".to_string(),
6619 size: 0,
6620 last_modified: "2024-01-01".to_string(),
6621 is_prefix: true,
6622 storage_class: String::new(),
6623 }],
6624 );
6625
6626 app.s3_state.expanded_prefixes.insert("parent/".to_string());
6628 app.s3_state.prefix_preview.insert(
6629 "parent/".to_string(),
6630 vec![S3Object {
6631 key: "parent/child/".to_string(),
6632 size: 0,
6633 last_modified: "2024-01-01".to_string(),
6634 is_prefix: true,
6635 storage_class: String::new(),
6636 }],
6637 );
6638
6639 app.s3_state.selected_row = 2;
6641
6642 app.handle_action(Action::Select);
6644
6645 assert_eq!(
6647 app.s3_state.prefix_stack,
6648 vec!["parent/".to_string(), "parent/child/".to_string()]
6649 );
6650 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
6651 assert!(app.s3_state.buckets.loading);
6652
6653 app.s3_state.buckets.loading = false;
6655 app.handle_action(Action::GoBack);
6656 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
6657 assert!(app.s3_state.buckets.loading);
6658
6659 app.s3_state.buckets.loading = false;
6661 app.handle_action(Action::GoBack);
6662 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
6663 assert!(app.s3_state.buckets.loading);
6664
6665 app.s3_state.buckets.loading = false;
6667 app.handle_action(Action::GoBack);
6668 assert_eq!(app.s3_state.current_bucket, None);
6669 }
6670
6671 #[test]
6672 fn test_region_picker_fuzzy_filter() {
6673 let mut app = test_app();
6674 app.region_latencies.insert("us-east-1".to_string(), 10);
6675 app.region_filter = "vir".to_string();
6676 let filtered = app.get_filtered_regions();
6677 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
6678 }
6679
6680 #[test]
6681 fn test_profile_picker_loads_profiles() {
6682 let profiles = App::load_aws_profiles();
6683 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
6685 }
6686
6687 #[test]
6688 fn test_profile_with_region_uses_it() {
6689 let mut app = test_app_no_region();
6690 app.available_profiles = vec![AwsProfile {
6691 name: "test-profile".to_string(),
6692 region: Some("eu-west-1".to_string()),
6693 account: Some("123456789".to_string()),
6694 role_arn: None,
6695 source_profile: None,
6696 }];
6697 app.profile_picker_selected = 0;
6698 app.mode = Mode::ProfilePicker;
6699
6700 let filtered = app.get_filtered_profiles();
6702 if let Some(profile) = filtered.first() {
6703 let profile_name = profile.name.clone();
6704 let profile_region = profile.region.clone();
6705
6706 app.profile = profile_name;
6707 if let Some(region) = profile_region {
6708 app.region = region;
6709 }
6710 }
6711
6712 assert_eq!(app.profile, "test-profile");
6713 assert_eq!(app.region, "eu-west-1");
6714 }
6715
6716 #[test]
6717 fn test_profile_without_region_keeps_unknown() {
6718 let mut app = test_app_no_region();
6719 let initial_region = app.region.clone();
6720
6721 app.available_profiles = vec![AwsProfile {
6722 name: "test-profile".to_string(),
6723 region: None,
6724 account: None,
6725 role_arn: None,
6726 source_profile: None,
6727 }];
6728 app.profile_picker_selected = 0;
6729 app.mode = Mode::ProfilePicker;
6730
6731 let filtered = app.get_filtered_profiles();
6732 if let Some(profile) = filtered.first() {
6733 let profile_name = profile.name.clone();
6734 let profile_region = profile.region.clone();
6735
6736 app.profile = profile_name;
6737 if let Some(region) = profile_region {
6738 app.region = region;
6739 }
6740 }
6741
6742 assert_eq!(app.profile, "test-profile");
6743 assert_eq!(app.region, initial_region); }
6745
6746 #[test]
6747 fn test_region_selection_closes_all_tabs() {
6748 let mut app = test_app();
6749
6750 app.tabs.push(Tab {
6752 service: Service::CloudWatchLogGroups,
6753 title: "CloudWatch".to_string(),
6754 breadcrumb: "CloudWatch".to_string(),
6755 });
6756 app.tabs.push(Tab {
6757 service: Service::S3Buckets,
6758 title: "S3".to_string(),
6759 breadcrumb: "S3".to_string(),
6760 });
6761 app.service_selected = true;
6762 app.current_tab = 1;
6763
6764 app.region_latencies.insert("eu-west-1".to_string(), 50);
6766
6767 app.mode = Mode::RegionPicker;
6769 app.region_picker_selected = 0;
6770
6771 let filtered = app.get_filtered_regions();
6772 if let Some(region) = filtered.first() {
6773 app.region = region.code.to_string();
6774 app.tabs.clear();
6775 app.current_tab = 0;
6776 app.service_selected = false;
6777 app.mode = Mode::Normal;
6778 }
6779
6780 assert_eq!(app.tabs.len(), 0);
6781 assert_eq!(app.current_tab, 0);
6782 assert!(!app.service_selected);
6783 assert_eq!(app.region, "eu-west-1");
6784 }
6785
6786 #[test]
6787 fn test_region_picker_can_be_closed_without_selection() {
6788 let mut app = test_app();
6789 let initial_region = app.region.clone();
6790
6791 app.mode = Mode::RegionPicker;
6792
6793 app.mode = Mode::Normal;
6795
6796 assert_eq!(app.region, initial_region);
6798 }
6799
6800 #[test]
6801 fn test_session_filter_works() {
6802 let mut app = test_app();
6803
6804 app.sessions = vec![
6805 Session {
6806 id: "1".to_string(),
6807 timestamp: "2024-01-01".to_string(),
6808 profile: "prod-profile".to_string(),
6809 region: "us-east-1".to_string(),
6810 account_id: "123456789".to_string(),
6811 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
6812 tabs: vec![],
6813 },
6814 Session {
6815 id: "2".to_string(),
6816 timestamp: "2024-01-02".to_string(),
6817 profile: "dev-profile".to_string(),
6818 region: "eu-west-1".to_string(),
6819 account_id: "987654321".to_string(),
6820 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
6821 tabs: vec![],
6822 },
6823 ];
6824
6825 app.session_filter = "prod".to_string();
6827 let filtered = app.get_filtered_sessions();
6828 assert_eq!(filtered.len(), 1);
6829 assert_eq!(filtered[0].profile, "prod-profile");
6830
6831 app.session_filter = "eu".to_string();
6833 let filtered = app.get_filtered_sessions();
6834 assert_eq!(filtered.len(), 1);
6835 assert_eq!(filtered[0].region, "eu-west-1");
6836
6837 app.session_filter.clear();
6839 let filtered = app.get_filtered_sessions();
6840 assert_eq!(filtered.len(), 2);
6841 }
6842
6843 #[test]
6844 fn test_profile_picker_shows_account() {
6845 let mut app = test_app_no_region();
6846 app.available_profiles = vec![AwsProfile {
6847 name: "test-profile".to_string(),
6848 region: Some("us-east-1".to_string()),
6849 account: Some("123456789".to_string()),
6850 role_arn: None,
6851 source_profile: None,
6852 }];
6853
6854 let filtered = app.get_filtered_profiles();
6855 assert_eq!(filtered.len(), 1);
6856 assert_eq!(filtered[0].account, Some("123456789".to_string()));
6857 }
6858
6859 #[test]
6860 fn test_profile_without_account() {
6861 let mut app = test_app_no_region();
6862 app.available_profiles = vec![AwsProfile {
6863 name: "test-profile".to_string(),
6864 region: Some("us-east-1".to_string()),
6865 account: None,
6866 role_arn: None,
6867 source_profile: None,
6868 }];
6869
6870 let filtered = app.get_filtered_profiles();
6871 assert_eq!(filtered.len(), 1);
6872 assert_eq!(filtered[0].account, None);
6873 }
6874
6875 #[test]
6876 fn test_profile_with_all_fields() {
6877 let mut app = test_app_no_region();
6878 app.available_profiles = vec![AwsProfile {
6879 name: "prod-profile".to_string(),
6880 region: Some("us-west-2".to_string()),
6881 account: Some("123456789".to_string()),
6882 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
6883 source_profile: Some("base-profile".to_string()),
6884 }];
6885
6886 let filtered = app.get_filtered_profiles();
6887 assert_eq!(filtered.len(), 1);
6888 assert_eq!(filtered[0].name, "prod-profile");
6889 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
6890 assert_eq!(filtered[0].account, Some("123456789".to_string()));
6891 assert_eq!(
6892 filtered[0].role_arn,
6893 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
6894 );
6895 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
6896 }
6897
6898 #[test]
6899 fn test_profile_filter_by_source_profile() {
6900 let mut app = test_app_no_region();
6901 app.available_profiles = vec![
6902 AwsProfile {
6903 name: "profile1".to_string(),
6904 region: None,
6905 account: None,
6906 role_arn: None,
6907 source_profile: Some("base".to_string()),
6908 },
6909 AwsProfile {
6910 name: "profile2".to_string(),
6911 region: None,
6912 account: None,
6913 role_arn: None,
6914 source_profile: Some("other".to_string()),
6915 },
6916 ];
6917
6918 app.profile_filter = "base".to_string();
6919 let filtered = app.get_filtered_profiles();
6920 assert_eq!(filtered.len(), 1);
6921 assert_eq!(filtered[0].name, "profile1");
6922 }
6923
6924 #[test]
6925 fn test_profile_filter_by_role() {
6926 let mut app = test_app_no_region();
6927 app.available_profiles = vec![
6928 AwsProfile {
6929 name: "admin-profile".to_string(),
6930 region: None,
6931 account: None,
6932 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
6933 source_profile: None,
6934 },
6935 AwsProfile {
6936 name: "dev-profile".to_string(),
6937 region: None,
6938 account: None,
6939 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
6940 source_profile: None,
6941 },
6942 ];
6943
6944 app.profile_filter = "Admin".to_string();
6945 let filtered = app.get_filtered_profiles();
6946 assert_eq!(filtered.len(), 1);
6947 assert_eq!(filtered[0].name, "admin-profile");
6948 }
6949
6950 #[test]
6951 fn test_profiles_sorted_by_name() {
6952 let mut app = test_app_no_region();
6953 app.available_profiles = vec![
6954 AwsProfile {
6955 name: "zebra-profile".to_string(),
6956 region: None,
6957 account: None,
6958 role_arn: None,
6959 source_profile: None,
6960 },
6961 AwsProfile {
6962 name: "alpha-profile".to_string(),
6963 region: None,
6964 account: None,
6965 role_arn: None,
6966 source_profile: None,
6967 },
6968 AwsProfile {
6969 name: "beta-profile".to_string(),
6970 region: None,
6971 account: None,
6972 role_arn: None,
6973 source_profile: None,
6974 },
6975 ];
6976
6977 let filtered = app.get_filtered_profiles();
6978 assert_eq!(filtered.len(), 3);
6979 assert_eq!(filtered[0].name, "alpha-profile");
6980 assert_eq!(filtered[1].name, "beta-profile");
6981 assert_eq!(filtered[2].name, "zebra-profile");
6982 }
6983
6984 #[test]
6985 fn test_profile_with_role_arn() {
6986 let mut app = test_app_no_region();
6987 app.available_profiles = vec![AwsProfile {
6988 name: "role-profile".to_string(),
6989 region: Some("us-east-1".to_string()),
6990 account: Some("123456789".to_string()),
6991 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
6992 source_profile: None,
6993 }];
6994
6995 let filtered = app.get_filtered_profiles();
6996 assert_eq!(filtered.len(), 1);
6997 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
6998 }
6999
7000 #[test]
7001 fn test_profile_with_user_arn() {
7002 let mut app = test_app_no_region();
7003 app.available_profiles = vec![AwsProfile {
7004 name: "user-profile".to_string(),
7005 region: Some("us-east-1".to_string()),
7006 account: Some("123456789".to_string()),
7007 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
7008 source_profile: None,
7009 }];
7010
7011 let filtered = app.get_filtered_profiles();
7012 assert_eq!(filtered.len(), 1);
7013 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
7014 }
7015
7016 #[test]
7017 fn test_filtered_profiles_also_sorted() {
7018 let mut app = test_app_no_region();
7019 app.available_profiles = vec![
7020 AwsProfile {
7021 name: "prod-zebra".to_string(),
7022 region: Some("us-east-1".to_string()),
7023 account: None,
7024 role_arn: None,
7025 source_profile: None,
7026 },
7027 AwsProfile {
7028 name: "prod-alpha".to_string(),
7029 region: Some("us-east-1".to_string()),
7030 account: None,
7031 role_arn: None,
7032 source_profile: None,
7033 },
7034 AwsProfile {
7035 name: "dev-profile".to_string(),
7036 region: Some("us-west-2".to_string()),
7037 account: None,
7038 role_arn: None,
7039 source_profile: None,
7040 },
7041 ];
7042
7043 app.profile_filter = "prod".to_string();
7044 let filtered = app.get_filtered_profiles();
7045 assert_eq!(filtered.len(), 2);
7046 assert_eq!(filtered[0].name, "prod-alpha");
7047 assert_eq!(filtered[1].name, "prod-zebra");
7048 }
7049
7050 #[test]
7051 fn test_profile_picker_has_all_columns() {
7052 let mut app = test_app_no_region();
7053 app.available_profiles = vec![AwsProfile {
7054 name: "test".to_string(),
7055 region: Some("us-east-1".to_string()),
7056 account: Some("123456789".to_string()),
7057 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
7058 source_profile: Some("base".to_string()),
7059 }];
7060
7061 let filtered = app.get_filtered_profiles();
7062 assert_eq!(filtered.len(), 1);
7063 assert!(filtered[0].name == "test");
7064 assert!(filtered[0].region.is_some());
7065 assert!(filtered[0].account.is_some());
7066 assert!(filtered[0].role_arn.is_some());
7067 assert!(filtered[0].source_profile.is_some());
7068 }
7069
7070 #[test]
7071 fn test_session_picker_shows_tab_count() {
7072 let mut app = test_app_no_region();
7073 app.sessions = vec![Session {
7074 id: "1".to_string(),
7075 timestamp: "2024-01-01".to_string(),
7076 profile: "test".to_string(),
7077 region: "us-east-1".to_string(),
7078 account_id: "123".to_string(),
7079 role_arn: String::new(),
7080 tabs: vec![
7081 SessionTab {
7082 service: "CloudWatch".to_string(),
7083 title: "Logs".to_string(),
7084 breadcrumb: String::new(),
7085 filter: None,
7086 selected_item: None,
7087 },
7088 SessionTab {
7089 service: "S3".to_string(),
7090 title: "Buckets".to_string(),
7091 breadcrumb: String::new(),
7092 filter: None,
7093 selected_item: None,
7094 },
7095 ],
7096 }];
7097
7098 let filtered = app.get_filtered_sessions();
7099 assert_eq!(filtered.len(), 1);
7100 assert_eq!(filtered[0].tabs.len(), 2);
7101 }
7102
7103 #[test]
7104 fn test_start_background_data_fetch_loads_profiles() {
7105 let mut app = test_app_no_region();
7106 assert!(app.available_profiles.is_empty());
7107
7108 app.available_profiles = App::load_aws_profiles();
7110
7111 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7113 }
7114
7115 #[test]
7116 fn test_refresh_in_profile_picker() {
7117 let mut app = test_app_no_region();
7118 app.mode = Mode::ProfilePicker;
7119 app.available_profiles = vec![AwsProfile {
7120 name: "test".to_string(),
7121 region: None,
7122 account: None,
7123 role_arn: None,
7124 source_profile: None,
7125 }];
7126
7127 app.handle_action(Action::Refresh);
7128
7129 assert!(app.log_groups_state.loading);
7131 assert!(
7132 app.log_groups_state.loading_message.is_empty()
7133 || !app.log_groups_state.loading_message.contains("Refreshing"),
7134 "loading_message must not say 'Refreshing...' as it leaks into other services"
7135 );
7136 }
7137
7138 #[test]
7139 fn test_refresh_sets_loading_for_profile_picker() {
7140 let mut app = test_app_no_region();
7141 app.mode = Mode::ProfilePicker;
7142
7143 assert!(!app.log_groups_state.loading);
7144
7145 app.handle_action(Action::Refresh);
7146
7147 assert!(app.log_groups_state.loading);
7148 }
7149
7150 #[test]
7151 fn test_profiles_loaded_on_demand() {
7152 let mut app = test_app_no_region();
7153
7154 assert!(app.available_profiles.is_empty());
7156
7157 app.available_profiles = App::load_aws_profiles();
7159
7160 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7162 }
7163
7164 #[test]
7165 fn test_profile_accounts_not_fetched_automatically() {
7166 let mut app = test_app_no_region();
7167 app.available_profiles = App::load_aws_profiles();
7168
7169 for profile in &app.available_profiles {
7171 assert!(profile.account.is_none() || profile.account.is_some());
7174 }
7175 }
7176
7177 #[test]
7178 fn test_ctrl_r_triggers_account_fetch() {
7179 let mut app = test_app_no_region();
7180 app.mode = Mode::ProfilePicker;
7181 app.available_profiles = vec![AwsProfile {
7182 name: "test".to_string(),
7183 region: Some("us-east-1".to_string()),
7184 account: None,
7185 role_arn: None,
7186 source_profile: None,
7187 }];
7188
7189 assert!(app.available_profiles[0].account.is_none());
7191
7192 app.handle_action(Action::Refresh);
7194
7195 assert!(app.log_groups_state.loading);
7197 }
7198
7199 #[test]
7200 fn test_refresh_in_region_picker() {
7201 let mut app = test_app_no_region();
7202 app.mode = Mode::RegionPicker;
7203
7204 let initial_latencies = app.region_latencies.len();
7205 app.handle_action(Action::Refresh);
7206
7207 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
7209 }
7210
7211 #[test]
7212 fn test_refresh_in_session_picker() {
7213 let mut app = test_app_no_region();
7214 app.mode = Mode::SessionPicker;
7215 app.sessions = vec![];
7216
7217 app.handle_action(Action::Refresh);
7218
7219 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
7221 }
7222
7223 #[test]
7224 fn test_session_picker_selection() {
7225 let mut app = test_app();
7226
7227 app.sessions = vec![Session {
7228 id: "1".to_string(),
7229 timestamp: "2024-01-01".to_string(),
7230 profile: "prod-profile".to_string(),
7231 region: "us-west-2".to_string(),
7232 account_id: "123456789".to_string(),
7233 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7234 tabs: vec![SessionTab {
7235 service: "CloudWatchLogGroups".to_string(),
7236 title: "Log Groups".to_string(),
7237 breadcrumb: "CloudWatch › Log Groups".to_string(),
7238 filter: Some("test".to_string()),
7239 selected_item: None,
7240 }],
7241 }];
7242
7243 app.mode = Mode::SessionPicker;
7244 app.session_picker_selected = 0;
7245
7246 app.handle_action(Action::Select);
7248
7249 assert_eq!(app.mode, Mode::Normal);
7250 assert_eq!(app.profile, "prod-profile");
7251 assert_eq!(app.region, "us-west-2");
7252 assert_eq!(app.config.account_id, "123456789");
7253 assert_eq!(app.tabs.len(), 1);
7254 assert_eq!(app.tabs[0].title, "Log Groups");
7255 }
7256
7257 #[test]
7258 fn test_save_session_creates_session() {
7259 let mut app =
7260 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7261 app.config.account_id = "123456789".to_string();
7262 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7263
7264 app.tabs.push(Tab {
7265 service: Service::CloudWatchLogGroups,
7266 title: "Log Groups".to_string(),
7267 breadcrumb: "CloudWatch › Log Groups".to_string(),
7268 });
7269
7270 app.save_current_session();
7271
7272 assert!(app.current_session.is_some());
7273 let session = app.current_session.clone().unwrap();
7274 assert_eq!(session.profile, "test-profile");
7275 assert_eq!(session.region, "us-east-1");
7276 assert_eq!(session.account_id, "123456789");
7277 assert_eq!(session.tabs.len(), 1);
7278
7279 let _ = session.delete();
7281 }
7282
7283 #[test]
7284 fn test_save_session_updates_existing() {
7285 let mut app =
7286 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7287 app.config.account_id = "123456789".to_string();
7288 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7289
7290 app.current_session = Some(Session {
7291 id: "existing".to_string(),
7292 timestamp: "2024-01-01".to_string(),
7293 profile: "test-profile".to_string(),
7294 region: "us-east-1".to_string(),
7295 account_id: "123456789".to_string(),
7296 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7297 tabs: vec![],
7298 });
7299
7300 app.tabs.push(Tab {
7301 service: Service::CloudWatchLogGroups,
7302 title: "Log Groups".to_string(),
7303 breadcrumb: "CloudWatch › Log Groups".to_string(),
7304 });
7305
7306 app.save_current_session();
7307
7308 let session = app.current_session.clone().unwrap();
7309 assert_eq!(session.id, "existing");
7310 assert_eq!(session.tabs.len(), 1);
7311
7312 let _ = session.delete();
7314 }
7315
7316 #[test]
7317 fn test_save_session_skips_empty_tabs() {
7318 let mut app =
7319 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7320 app.config.account_id = "123456789".to_string();
7321
7322 app.save_current_session();
7323
7324 assert!(app.current_session.is_none());
7325 }
7326
7327 #[test]
7328 fn test_save_session_deletes_when_tabs_closed() {
7329 let mut app =
7330 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7331 app.config.account_id = "123456789".to_string();
7332 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7333
7334 app.current_session = Some(Session {
7336 id: "test_delete".to_string(),
7337 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
7338 profile: "test-profile".to_string(),
7339 region: "us-east-1".to_string(),
7340 account_id: "123456789".to_string(),
7341 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7342 tabs: vec![],
7343 });
7344
7345 app.save_current_session();
7347
7348 assert!(app.current_session.is_none());
7349 }
7350
7351 #[test]
7352 fn test_closing_all_tabs_deletes_session() {
7353 let mut app =
7354 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7355 app.config.account_id = "123456789".to_string();
7356 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7357
7358 app.tabs.push(Tab {
7360 service: Service::CloudWatchLogGroups,
7361 title: "Log Groups".to_string(),
7362 breadcrumb: "CloudWatch › Log Groups".to_string(),
7363 });
7364
7365 app.save_current_session();
7367 assert!(app.current_session.is_some());
7368 let session_id = app.current_session.as_ref().unwrap().id.clone();
7369
7370 app.tabs.clear();
7372
7373 app.save_current_session();
7375 assert!(app.current_session.is_none());
7376
7377 let _ = Session::load(&session_id).map(|s| s.delete());
7379 }
7380
7381 #[test]
7382 fn test_credential_error_opens_profile_picker() {
7383 let mut app = App::new_without_client("default".to_string(), None);
7385 let error_str = "Unable to load credentials from any source";
7386
7387 if error_str.contains("credentials") {
7388 app.available_profiles = App::load_aws_profiles();
7389 app.mode = Mode::ProfilePicker;
7390 }
7391
7392 assert_eq!(app.mode, Mode::ProfilePicker);
7393 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7395 }
7396
7397 #[test]
7398 fn test_non_credential_error_shows_error_modal() {
7399 let mut app = App::new_without_client("default".to_string(), None);
7400 let error_str = "Network timeout";
7401
7402 if !error_str.contains("credentials") {
7403 app.error_message = Some(error_str.to_string());
7404 app.mode = Mode::ErrorModal;
7405 }
7406
7407 assert_eq!(app.mode, Mode::ErrorModal);
7408 assert!(app.error_message.is_some());
7409 }
7410
7411 #[tokio::test]
7412 async fn test_profile_selection_loads_credentials() {
7413 std::env::set_var("AWS_PROFILE", "default");
7415
7416 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
7418
7419 if let Ok(app) = result {
7420 assert!(!app.config.account_id.is_empty());
7422 assert!(!app.config.role_arn.is_empty());
7423 assert_eq!(app.profile, "default");
7424 assert_eq!(app.config.region, "us-east-1");
7425 }
7426 }
7428
7429 #[test]
7430 fn test_new_app_shows_service_picker_with_no_tabs() {
7431 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
7432
7433 assert!(!app.service_selected);
7435 assert_eq!(app.mode, Mode::ServicePicker);
7437 assert!(app.tabs.is_empty());
7439 }
7440
7441 #[tokio::test]
7442 async fn test_aws_profile_env_var_read_before_config_load() {
7443 std::env::set_var("AWS_PROFILE", "test-profile");
7445
7446 let profile_name = None
7448 .or_else(|| std::env::var("AWS_PROFILE").ok())
7449 .unwrap_or_else(|| "default".to_string());
7450
7451 assert_eq!(profile_name, "test-profile");
7453
7454 std::env::set_var("AWS_PROFILE", &profile_name);
7456
7457 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
7459
7460 std::env::remove_var("AWS_PROFILE");
7461 }
7462
7463 #[test]
7464 fn test_next_preferences_cloudformation() {
7465 let mut app = test_app();
7466 app.current_service = Service::CloudFormationStacks;
7467 app.mode = Mode::ColumnSelector;
7468 app.column_selector_index = 0;
7469
7470 let page_size_idx = app.cfn_column_ids.len() + 2;
7472 app.handle_action(Action::NextPreferences);
7473 assert_eq!(app.column_selector_index, page_size_idx);
7474
7475 app.handle_action(Action::NextPreferences);
7477 assert_eq!(app.column_selector_index, 0);
7478 }
7479
7480 #[test]
7481 fn test_s3_preferences_tab_cycling() {
7482 let mut app = test_app();
7483 app.current_service = Service::S3Buckets;
7484 app.mode = Mode::ColumnSelector;
7485 app.column_selector_index = 0;
7486
7487 let page_size_idx = app.s3_bucket_column_ids.len() + 2;
7488
7489 app.handle_action(Action::NextPreferences);
7491 assert_eq!(app.column_selector_index, page_size_idx);
7492
7493 app.handle_action(Action::NextPreferences);
7495 assert_eq!(app.column_selector_index, 0);
7496
7497 app.handle_action(Action::PrevPreferences);
7499 assert_eq!(app.column_selector_index, page_size_idx);
7500
7501 app.handle_action(Action::PrevPreferences);
7503 assert_eq!(app.column_selector_index, 0);
7504 }
7505
7506 #[test]
7507 fn test_s3_filter_resets_selection() {
7508 let mut app = test_app();
7509 app.current_service = Service::S3Buckets;
7510 app.service_selected = true;
7511
7512 app.s3_state.buckets.items = vec![
7514 S3Bucket {
7515 name: "bucket-1".to_string(),
7516 region: "us-east-1".to_string(),
7517 creation_date: "2023-01-01".to_string(),
7518 },
7519 S3Bucket {
7520 name: "bucket-2".to_string(),
7521 region: "us-east-1".to_string(),
7522 creation_date: "2023-01-02".to_string(),
7523 },
7524 S3Bucket {
7525 name: "other-bucket".to_string(),
7526 region: "us-east-1".to_string(),
7527 creation_date: "2023-01-03".to_string(),
7528 },
7529 ];
7530
7531 app.s3_state.selected_row = 1;
7533 app.s3_state.bucket_scroll_offset = 1;
7534
7535 app.mode = Mode::FilterInput;
7537 app.apply_filter_operation(|f| f.push_str("bucket-"));
7538
7539 assert_eq!(app.s3_state.selected_row, 0);
7541 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
7542 assert_eq!(app.s3_state.buckets.filter, "bucket-");
7543 }
7544
7545 #[test]
7546 fn test_s3_navigation_respects_filter() {
7547 let mut app = test_app();
7548 app.current_service = Service::S3Buckets;
7549 app.service_selected = true;
7550 app.mode = Mode::Normal;
7551
7552 app.s3_state.buckets.items = vec![
7554 S3Bucket {
7555 name: "prod-bucket".to_string(),
7556 region: "us-east-1".to_string(),
7557 creation_date: "2023-01-01".to_string(),
7558 },
7559 S3Bucket {
7560 name: "dev-bucket".to_string(),
7561 region: "us-east-1".to_string(),
7562 creation_date: "2023-01-02".to_string(),
7563 },
7564 S3Bucket {
7565 name: "prod-logs".to_string(),
7566 region: "us-east-1".to_string(),
7567 creation_date: "2023-01-03".to_string(),
7568 },
7569 ];
7570
7571 app.s3_state.buckets.filter = "prod".to_string();
7573
7574 assert_eq!(app.s3_state.selected_row, 0);
7576
7577 app.handle_action(Action::NextItem);
7579 assert_eq!(app.s3_state.selected_row, 1);
7580
7581 app.handle_action(Action::NextItem);
7583 assert_eq!(app.s3_state.selected_row, 1);
7584
7585 app.handle_action(Action::PrevItem);
7587 assert_eq!(app.s3_state.selected_row, 0);
7588 }
7589
7590 #[test]
7591 fn test_next_preferences_lambda_functions() {
7592 let mut app = test_app();
7593 app.current_service = Service::LambdaFunctions;
7594 app.mode = Mode::ColumnSelector;
7595 app.column_selector_index = 0;
7596
7597 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
7598 app.handle_action(Action::NextPreferences);
7599 assert_eq!(app.column_selector_index, page_size_idx);
7600
7601 app.handle_action(Action::NextPreferences);
7602 assert_eq!(app.column_selector_index, 0);
7603 }
7604
7605 #[test]
7606 fn test_next_preferences_lambda_applications() {
7607 let mut app = test_app();
7608 app.current_service = Service::LambdaApplications;
7609 app.mode = Mode::ColumnSelector;
7610 app.column_selector_index = 0;
7611
7612 let page_size_idx = app.lambda_application_column_ids.len() + 2;
7613 app.handle_action(Action::NextPreferences);
7614 assert_eq!(app.column_selector_index, page_size_idx);
7615
7616 app.handle_action(Action::NextPreferences);
7617 assert_eq!(app.column_selector_index, 0);
7618 }
7619
7620 #[test]
7621 fn test_next_preferences_ecr_images() {
7622 let mut app = test_app();
7623 app.current_service = Service::EcrRepositories;
7624 app.ecr_state.current_repository = Some("test-repo".to_string());
7625 app.mode = Mode::ColumnSelector;
7626 app.column_selector_index = 0;
7627
7628 let page_size_idx = app.ecr_image_column_ids.len() + 2;
7629 app.handle_action(Action::NextPreferences);
7630 assert_eq!(app.column_selector_index, page_size_idx);
7631
7632 app.handle_action(Action::NextPreferences);
7633 assert_eq!(app.column_selector_index, 0);
7634 }
7635
7636 #[test]
7637 fn test_next_preferences_cloudwatch_log_groups() {
7638 let mut app = test_app();
7639 app.current_service = Service::CloudWatchLogGroups;
7640 app.view_mode = ViewMode::List;
7641 app.mode = Mode::ColumnSelector;
7642 app.column_selector_index = 0;
7643
7644 let page_size_idx = app.cw_log_group_column_ids.len() + 2;
7646 app.handle_action(Action::NextPreferences);
7647 assert_eq!(app.column_selector_index, page_size_idx);
7648
7649 app.handle_action(Action::NextPreferences);
7651 assert_eq!(app.column_selector_index, 0);
7652
7653 app.handle_action(Action::PrevPreferences);
7655 assert_eq!(app.column_selector_index, page_size_idx);
7656
7657 app.handle_action(Action::PrevPreferences);
7659 assert_eq!(app.column_selector_index, 0);
7660 }
7661
7662 #[test]
7663 fn test_next_preferences_cloudwatch_log_streams() {
7664 let mut app = test_app();
7665 app.current_service = Service::CloudWatchLogGroups;
7666 app.view_mode = ViewMode::Detail;
7667 app.mode = Mode::ColumnSelector;
7668 app.column_selector_index = 0;
7669
7670 let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
7672 app.handle_action(Action::NextPreferences);
7673 assert_eq!(app.column_selector_index, page_size_idx);
7674
7675 app.handle_action(Action::NextPreferences);
7677 assert_eq!(app.column_selector_index, 0);
7678 }
7679
7680 #[test]
7681 fn test_cloudformation_next_item() {
7682 let mut app = test_app();
7683 app.current_service = Service::CloudFormationStacks;
7684 app.service_selected = true;
7685 app.mode = Mode::Normal;
7686 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7687 app.cfn_state.table.items = vec![
7688 CfnStack {
7689 name: "stack1".to_string(),
7690 stack_id: "id1".to_string(),
7691 status: "CREATE_COMPLETE".to_string(),
7692 created_time: "2024-01-01".to_string(),
7693 updated_time: String::new(),
7694 deleted_time: String::new(),
7695 drift_status: String::new(),
7696 last_drift_check_time: String::new(),
7697 status_reason: String::new(),
7698 description: String::new(),
7699 detailed_status: String::new(),
7700 root_stack: String::new(),
7701 parent_stack: String::new(),
7702 termination_protection: false,
7703 iam_role: String::new(),
7704 tags: Vec::new(),
7705 stack_policy: String::new(),
7706 rollback_monitoring_time: String::new(),
7707 rollback_alarms: Vec::new(),
7708 notification_arns: Vec::new(),
7709 },
7710 CfnStack {
7711 name: "stack2".to_string(),
7712 stack_id: "id2".to_string(),
7713 status: "UPDATE_COMPLETE".to_string(),
7714 created_time: "2024-01-02".to_string(),
7715 updated_time: String::new(),
7716 deleted_time: String::new(),
7717 drift_status: String::new(),
7718 last_drift_check_time: String::new(),
7719 status_reason: String::new(),
7720 description: String::new(),
7721 detailed_status: String::new(),
7722 root_stack: String::new(),
7723 parent_stack: String::new(),
7724 termination_protection: false,
7725 iam_role: String::new(),
7726 tags: Vec::new(),
7727 stack_policy: String::new(),
7728 rollback_monitoring_time: String::new(),
7729 rollback_alarms: Vec::new(),
7730 notification_arns: Vec::new(),
7731 },
7732 ];
7733 app.cfn_state.table.reset();
7734
7735 app.handle_action(Action::NextItem);
7736 assert_eq!(app.cfn_state.table.selected, 1);
7737
7738 app.handle_action(Action::NextItem);
7739 assert_eq!(app.cfn_state.table.selected, 1); }
7741
7742 #[test]
7743 fn test_cloudformation_prev_item() {
7744 let mut app = test_app();
7745 app.current_service = Service::CloudFormationStacks;
7746 app.service_selected = true;
7747 app.mode = Mode::Normal;
7748 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7749 app.cfn_state.table.items = vec![
7750 CfnStack {
7751 name: "stack1".to_string(),
7752 stack_id: "id1".to_string(),
7753 status: "CREATE_COMPLETE".to_string(),
7754 created_time: "2024-01-01".to_string(),
7755 updated_time: String::new(),
7756 deleted_time: String::new(),
7757 drift_status: String::new(),
7758 last_drift_check_time: String::new(),
7759 status_reason: String::new(),
7760 description: String::new(),
7761 detailed_status: String::new(),
7762 root_stack: String::new(),
7763 parent_stack: String::new(),
7764 termination_protection: false,
7765 iam_role: String::new(),
7766 tags: Vec::new(),
7767 stack_policy: String::new(),
7768 rollback_monitoring_time: String::new(),
7769 rollback_alarms: Vec::new(),
7770 notification_arns: Vec::new(),
7771 },
7772 CfnStack {
7773 name: "stack2".to_string(),
7774 stack_id: "id2".to_string(),
7775 status: "UPDATE_COMPLETE".to_string(),
7776 created_time: "2024-01-02".to_string(),
7777 updated_time: String::new(),
7778 deleted_time: String::new(),
7779 drift_status: String::new(),
7780 last_drift_check_time: String::new(),
7781 status_reason: String::new(),
7782 description: String::new(),
7783 detailed_status: String::new(),
7784 root_stack: String::new(),
7785 parent_stack: String::new(),
7786 termination_protection: false,
7787 iam_role: String::new(),
7788 tags: Vec::new(),
7789 stack_policy: String::new(),
7790 rollback_monitoring_time: String::new(),
7791 rollback_alarms: Vec::new(),
7792 notification_arns: Vec::new(),
7793 },
7794 ];
7795 app.cfn_state.table.selected = 1;
7796
7797 app.handle_action(Action::PrevItem);
7798 assert_eq!(app.cfn_state.table.selected, 0);
7799
7800 app.handle_action(Action::PrevItem);
7801 assert_eq!(app.cfn_state.table.selected, 0); }
7803
7804 #[test]
7805 fn test_cloudformation_page_down() {
7806 let mut app = test_app();
7807 app.current_service = Service::CloudFormationStacks;
7808 app.service_selected = true;
7809 app.mode = Mode::Normal;
7810 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7811
7812 for i in 0..20 {
7814 app.cfn_state.table.items.push(CfnStack {
7815 name: format!("stack{}", i),
7816 stack_id: format!("id{}", i),
7817 status: "CREATE_COMPLETE".to_string(),
7818 created_time: format!("2024-01-{:02}", i + 1),
7819 updated_time: String::new(),
7820 deleted_time: String::new(),
7821 drift_status: String::new(),
7822 last_drift_check_time: String::new(),
7823 status_reason: String::new(),
7824 description: String::new(),
7825 detailed_status: String::new(),
7826 root_stack: String::new(),
7827 parent_stack: String::new(),
7828 termination_protection: false,
7829 iam_role: String::new(),
7830 tags: Vec::new(),
7831 stack_policy: String::new(),
7832 rollback_monitoring_time: String::new(),
7833 rollback_alarms: Vec::new(),
7834 notification_arns: Vec::new(),
7835 });
7836 }
7837 app.cfn_state.table.reset();
7838
7839 app.handle_action(Action::PageDown);
7840 assert_eq!(app.cfn_state.table.selected, 10);
7841
7842 app.handle_action(Action::PageDown);
7843 assert_eq!(app.cfn_state.table.selected, 19); }
7845
7846 #[test]
7847 fn test_cloudformation_page_up() {
7848 let mut app = test_app();
7849 app.current_service = Service::CloudFormationStacks;
7850 app.service_selected = true;
7851 app.mode = Mode::Normal;
7852 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7853
7854 for i in 0..20 {
7856 app.cfn_state.table.items.push(CfnStack {
7857 name: format!("stack{}", i),
7858 stack_id: format!("id{}", i),
7859 status: "CREATE_COMPLETE".to_string(),
7860 created_time: format!("2024-01-{:02}", i + 1),
7861 updated_time: String::new(),
7862 deleted_time: String::new(),
7863 drift_status: String::new(),
7864 last_drift_check_time: String::new(),
7865 status_reason: String::new(),
7866 description: String::new(),
7867 detailed_status: String::new(),
7868 root_stack: String::new(),
7869 parent_stack: String::new(),
7870 termination_protection: false,
7871 iam_role: String::new(),
7872 tags: Vec::new(),
7873 stack_policy: String::new(),
7874 rollback_monitoring_time: String::new(),
7875 rollback_alarms: Vec::new(),
7876 notification_arns: Vec::new(),
7877 });
7878 }
7879 app.cfn_state.table.selected = 15;
7880
7881 app.handle_action(Action::PageUp);
7882 assert_eq!(app.cfn_state.table.selected, 5);
7883
7884 app.handle_action(Action::PageUp);
7885 assert_eq!(app.cfn_state.table.selected, 0); }
7887
7888 #[test]
7889 fn test_cloudformation_filter_input() {
7890 let mut app = test_app();
7891 app.current_service = Service::CloudFormationStacks;
7892 app.service_selected = true;
7893 app.mode = Mode::Normal;
7894
7895 app.handle_action(Action::StartFilter);
7896 assert_eq!(app.mode, Mode::FilterInput);
7897
7898 app.cfn_state.table.filter = "test".to_string();
7900 assert_eq!(app.cfn_state.table.filter, "test");
7901 }
7902
7903 #[test]
7904 fn test_cloudformation_filter_applies() {
7905 let mut app = test_app();
7906 app.current_service = Service::CloudFormationStacks;
7907 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7908 app.cfn_state.table.items = vec![
7909 CfnStack {
7910 name: "prod-stack".to_string(),
7911 stack_id: "id1".to_string(),
7912 status: "CREATE_COMPLETE".to_string(),
7913 created_time: "2024-01-01".to_string(),
7914 updated_time: String::new(),
7915 deleted_time: String::new(),
7916 drift_status: String::new(),
7917 last_drift_check_time: String::new(),
7918 status_reason: String::new(),
7919 description: "Production stack".to_string(),
7920 detailed_status: String::new(),
7921 root_stack: String::new(),
7922 parent_stack: String::new(),
7923 termination_protection: false,
7924 iam_role: String::new(),
7925 tags: Vec::new(),
7926 stack_policy: String::new(),
7927 rollback_monitoring_time: String::new(),
7928 rollback_alarms: Vec::new(),
7929 notification_arns: Vec::new(),
7930 },
7931 CfnStack {
7932 name: "dev-stack".to_string(),
7933 stack_id: "id2".to_string(),
7934 status: "UPDATE_COMPLETE".to_string(),
7935 created_time: "2024-01-02".to_string(),
7936 updated_time: String::new(),
7937 deleted_time: String::new(),
7938 drift_status: String::new(),
7939 last_drift_check_time: String::new(),
7940 status_reason: String::new(),
7941 description: "Development stack".to_string(),
7942 detailed_status: String::new(),
7943 root_stack: String::new(),
7944 parent_stack: String::new(),
7945 termination_protection: false,
7946 iam_role: String::new(),
7947 tags: Vec::new(),
7948 stack_policy: String::new(),
7949 rollback_monitoring_time: String::new(),
7950 rollback_alarms: Vec::new(),
7951 notification_arns: Vec::new(),
7952 },
7953 ];
7954 app.cfn_state.table.filter = "prod".to_string();
7955
7956 let filtered = filtered_cloudformation_stacks(&app);
7957 assert_eq!(filtered.len(), 1);
7958 assert_eq!(filtered[0].name, "prod-stack");
7959 }
7960
7961 #[test]
7962 fn test_cloudformation_right_arrow_expands() {
7963 let mut app = test_app();
7964 app.current_service = Service::CloudFormationStacks;
7965 app.service_selected = true;
7966 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7967 app.cfn_state.table.items = vec![CfnStack {
7968 name: "test-stack".to_string(),
7969 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
7970 .to_string(),
7971 status: "CREATE_COMPLETE".to_string(),
7972 created_time: "2024-01-01".to_string(),
7973 updated_time: String::new(),
7974 deleted_time: String::new(),
7975 drift_status: String::new(),
7976 last_drift_check_time: String::new(),
7977 status_reason: String::new(),
7978 description: "Test stack".to_string(),
7979 detailed_status: String::new(),
7980 root_stack: String::new(),
7981 parent_stack: String::new(),
7982 termination_protection: false,
7983 iam_role: String::new(),
7984 tags: Vec::new(),
7985 stack_policy: String::new(),
7986 rollback_monitoring_time: String::new(),
7987 rollback_alarms: Vec::new(),
7988 notification_arns: Vec::new(),
7989 }];
7990 app.cfn_state.table.reset();
7991
7992 assert_eq!(app.cfn_state.table.expanded_item, None);
7993
7994 app.handle_action(Action::NextPane);
7995 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
7996 }
7997
7998 #[test]
7999 fn test_cloudformation_left_arrow_collapses() {
8000 let mut app = test_app();
8001 app.current_service = Service::CloudFormationStacks;
8002 app.service_selected = true;
8003 app.cfn_state.status_filter = CfnStatusFilter::Complete;
8004 app.cfn_state.table.items = vec![CfnStack {
8005 name: "test-stack".to_string(),
8006 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8007 .to_string(),
8008 status: "CREATE_COMPLETE".to_string(),
8009 created_time: "2024-01-01".to_string(),
8010 updated_time: String::new(),
8011 deleted_time: String::new(),
8012 drift_status: String::new(),
8013 last_drift_check_time: String::new(),
8014 status_reason: String::new(),
8015 description: "Test stack".to_string(),
8016 detailed_status: String::new(),
8017 root_stack: String::new(),
8018 parent_stack: String::new(),
8019 termination_protection: false,
8020 iam_role: String::new(),
8021 tags: Vec::new(),
8022 stack_policy: String::new(),
8023 rollback_monitoring_time: String::new(),
8024 rollback_alarms: Vec::new(),
8025 notification_arns: Vec::new(),
8026 }];
8027 app.cfn_state.table.reset();
8028 app.cfn_state.table.expanded_item = Some(0);
8029
8030 app.handle_action(Action::PrevPane);
8031 assert_eq!(app.cfn_state.table.expanded_item, None);
8032 }
8033
8034 #[test]
8035 fn test_cloudformation_enter_drills_into_stack() {
8036 let mut app = test_app();
8037 app.current_service = Service::CloudFormationStacks;
8038 app.service_selected = true;
8039 app.mode = Mode::Normal;
8040 app.tabs = vec![Tab {
8041 service: Service::CloudFormationStacks,
8042 title: "CloudFormation › Stacks".to_string(),
8043 breadcrumb: "CloudFormation › Stacks".to_string(),
8044 }];
8045 app.current_tab = 0;
8046 app.cfn_state.status_filter = CfnStatusFilter::Complete;
8047 app.cfn_state.table.items = vec![CfnStack {
8048 name: "test-stack".to_string(),
8049 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8050 .to_string(),
8051 status: "CREATE_COMPLETE".to_string(),
8052 created_time: "2024-01-01".to_string(),
8053 updated_time: String::new(),
8054 deleted_time: String::new(),
8055 drift_status: String::new(),
8056 last_drift_check_time: String::new(),
8057 status_reason: String::new(),
8058 description: "Test stack".to_string(),
8059 detailed_status: String::new(),
8060 root_stack: String::new(),
8061 parent_stack: String::new(),
8062 termination_protection: false,
8063 iam_role: String::new(),
8064 tags: Vec::new(),
8065 stack_policy: String::new(),
8066 rollback_monitoring_time: String::new(),
8067 rollback_alarms: Vec::new(),
8068 notification_arns: Vec::new(),
8069 }];
8070 app.cfn_state.table.reset();
8071
8072 let filtered = filtered_cloudformation_stacks(&app);
8074 assert_eq!(filtered.len(), 1);
8075 assert_eq!(filtered[0].name, "test-stack");
8076
8077 assert_eq!(app.cfn_state.current_stack, None);
8078
8079 app.handle_action(Action::Select);
8081 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
8082 }
8083
8084 #[test]
8085 fn test_cloudformation_copy_to_clipboard() {
8086 let mut app = test_app();
8087 app.current_service = Service::CloudFormationStacks;
8088 app.service_selected = true;
8089 app.mode = Mode::Normal;
8090 app.cfn_state.status_filter = CfnStatusFilter::Complete;
8091 app.cfn_state.table.items = vec![
8092 CfnStack {
8093 name: "stack1".to_string(),
8094 stack_id: "id1".to_string(),
8095 status: "CREATE_COMPLETE".to_string(),
8096 created_time: "2024-01-01".to_string(),
8097 updated_time: String::new(),
8098 deleted_time: String::new(),
8099 drift_status: String::new(),
8100 last_drift_check_time: String::new(),
8101 status_reason: String::new(),
8102 description: String::new(),
8103 detailed_status: String::new(),
8104 root_stack: String::new(),
8105 parent_stack: String::new(),
8106 termination_protection: false,
8107 iam_role: String::new(),
8108 tags: Vec::new(),
8109 stack_policy: String::new(),
8110 rollback_monitoring_time: String::new(),
8111 rollback_alarms: Vec::new(),
8112 notification_arns: Vec::new(),
8113 },
8114 CfnStack {
8115 name: "stack2".to_string(),
8116 stack_id: "id2".to_string(),
8117 status: "UPDATE_COMPLETE".to_string(),
8118 created_time: "2024-01-02".to_string(),
8119 updated_time: String::new(),
8120 deleted_time: String::new(),
8121 drift_status: String::new(),
8122 last_drift_check_time: String::new(),
8123 status_reason: String::new(),
8124 description: String::new(),
8125 detailed_status: String::new(),
8126 root_stack: String::new(),
8127 parent_stack: String::new(),
8128 termination_protection: false,
8129 iam_role: String::new(),
8130 tags: Vec::new(),
8131 stack_policy: String::new(),
8132 rollback_monitoring_time: String::new(),
8133 rollback_alarms: Vec::new(),
8134 notification_arns: Vec::new(),
8135 },
8136 ];
8137
8138 assert!(!app.snapshot_requested);
8139 app.handle_action(Action::CopyToClipboard);
8140
8141 assert!(app.snapshot_requested);
8143 }
8144
8145 #[test]
8146 fn test_cloudformation_expansion_shows_all_visible_columns() {
8147 let mut app = test_app();
8148 app.current_service = Service::CloudFormationStacks;
8149 app.cfn_state.status_filter = CfnStatusFilter::Complete;
8150 app.cfn_state.table.items = vec![CfnStack {
8151 name: "test-stack".to_string(),
8152 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8153 .to_string(),
8154 status: "CREATE_COMPLETE".to_string(),
8155 created_time: "2024-01-01".to_string(),
8156 updated_time: "2024-01-02".to_string(),
8157 deleted_time: String::new(),
8158 drift_status: "IN_SYNC".to_string(),
8159 last_drift_check_time: "2024-01-03".to_string(),
8160 status_reason: String::new(),
8161 description: "Test description".to_string(),
8162 detailed_status: String::new(),
8163 root_stack: String::new(),
8164 parent_stack: String::new(),
8165 termination_protection: false,
8166 iam_role: String::new(),
8167 tags: Vec::new(),
8168 stack_policy: String::new(),
8169 rollback_monitoring_time: String::new(),
8170 rollback_alarms: Vec::new(),
8171 notification_arns: Vec::new(),
8172 }];
8173
8174 app.cfn_visible_column_ids = [
8176 CfnColumn::Name,
8177 CfnColumn::Status,
8178 CfnColumn::CreatedTime,
8179 CfnColumn::Description,
8180 ]
8181 .iter()
8182 .map(|c| c.id())
8183 .collect();
8184
8185 app.cfn_state.table.expanded_item = Some(0);
8186
8187 assert_eq!(app.cfn_visible_column_ids.len(), 4);
8190 assert!(app.cfn_state.table.has_expanded_item());
8191 }
8192
8193 #[test]
8194 fn test_cloudformation_empty_list_shows_page_1() {
8195 let mut app = test_app();
8196 app.current_service = Service::CloudFormationStacks;
8197 app.cfn_state.table.items = vec![];
8198
8199 let filtered = filtered_cloudformation_stacks(&app);
8200 assert_eq!(filtered.len(), 0);
8201
8202 let page_size = app.cfn_state.table.page_size.value();
8204 let total_pages = filtered.len().div_ceil(page_size);
8205 assert_eq!(total_pages, 0);
8206
8207 }
8210}
8211
8212impl App {
8213 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
8214 let mut all = AwsRegion::all();
8215
8216 for region in &mut all {
8218 region.latency_ms = self.region_latencies.get(region.code).copied();
8219 }
8220
8221 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
8223 all
8224 } else {
8225 let filter_lower = self.region_filter.to_lowercase();
8226 all.into_iter()
8227 .filter(|r| {
8228 r.name.to_lowercase().contains(&filter_lower)
8229 || r.code.to_lowercase().contains(&filter_lower)
8230 || r.group.to_lowercase().contains(&filter_lower)
8231 })
8232 .collect()
8233 };
8234
8235 let mut sorted = filtered;
8237 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
8238 sorted
8239 }
8240
8241 pub fn measure_region_latencies(&mut self) {
8242 use std::time::Instant;
8243 self.region_latencies.clear();
8244
8245 let regions = AwsRegion::all();
8246 let start_all = Instant::now();
8247 tracing::info!("Starting latency measurement for {} regions", regions.len());
8248
8249 let handles: Vec<_> = regions
8250 .iter()
8251 .map(|region| {
8252 let code = region.code.to_string();
8253 std::thread::spawn(move || {
8254 let endpoint = format!("https://sts.{}.amazonaws.com", code);
8256 let start = Instant::now();
8257
8258 match ureq::get(&endpoint)
8259 .timeout(std::time::Duration::from_secs(2))
8260 .call()
8261 {
8262 Ok(_) => {
8263 let latency = start.elapsed().as_millis() as u64;
8264 Some((code, latency))
8265 }
8266 Err(e) => {
8267 tracing::debug!("Failed to measure {}: {}", code, e);
8268 Some((code, 9999))
8269 }
8270 }
8271 })
8272 })
8273 .collect();
8274
8275 for handle in handles {
8276 if let Ok(Some((code, latency))) = handle.join() {
8277 self.region_latencies.insert(code, latency);
8278 }
8279 }
8280
8281 tracing::info!(
8282 "Measured {} regions in {:?}",
8283 self.region_latencies.len(),
8284 start_all.elapsed()
8285 );
8286 }
8287
8288 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
8289 filter_profiles(&self.available_profiles, &self.profile_filter)
8290 }
8291
8292 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
8293 if self.session_filter.is_empty() {
8294 return self.sessions.iter().collect();
8295 }
8296 let filter_lower = self.session_filter.to_lowercase();
8297 self.sessions
8298 .iter()
8299 .filter(|s| {
8300 s.profile.to_lowercase().contains(&filter_lower)
8301 || s.region.to_lowercase().contains(&filter_lower)
8302 || s.account_id.to_lowercase().contains(&filter_lower)
8303 || s.role_arn.to_lowercase().contains(&filter_lower)
8304 })
8305 .collect()
8306 }
8307
8308 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
8309 if self.tab_filter.is_empty() {
8310 return self.tabs.iter().enumerate().collect();
8311 }
8312 let filter_lower = self.tab_filter.to_lowercase();
8313 self.tabs
8314 .iter()
8315 .enumerate()
8316 .filter(|(_, tab)| {
8317 tab.title.to_lowercase().contains(&filter_lower)
8318 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
8319 })
8320 .collect()
8321 }
8322
8323 pub fn load_aws_profiles() -> Vec<AwsProfile> {
8324 AwsProfile::load_all()
8325 }
8326
8327 pub async fn fetch_profile_accounts(&mut self) {
8328 for profile in &mut self.available_profiles {
8329 if profile.account.is_none() {
8330 let region = profile
8331 .region
8332 .clone()
8333 .unwrap_or_else(|| "us-east-1".to_string());
8334 if let Ok(account) =
8335 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
8336 {
8337 profile.account = Some(account);
8338 }
8339 }
8340 }
8341 }
8342
8343 fn save_current_session(&mut self) {
8344 if self.tabs.is_empty() {
8346 if let Some(ref session) = self.current_session {
8347 let _ = session.delete();
8348 self.current_session = None;
8349 }
8350 return;
8351 }
8352
8353 let session = if let Some(ref mut current) = self.current_session {
8354 current.tabs = self
8356 .tabs
8357 .iter()
8358 .map(|t| SessionTab {
8359 service: format!("{:?}", t.service),
8360 title: t.title.clone(),
8361 breadcrumb: t.breadcrumb.clone(),
8362 filter: match t.service {
8363 Service::CloudWatchLogGroups => {
8364 Some(self.log_groups_state.log_groups.filter.clone())
8365 }
8366 _ => None,
8367 },
8368 selected_item: None,
8369 })
8370 .collect();
8371 current.clone()
8372 } else {
8373 let mut session = Session::new(
8375 self.profile.clone(),
8376 self.region.clone(),
8377 self.config.account_id.clone(),
8378 self.config.role_arn.clone(),
8379 );
8380 session.tabs = self
8381 .tabs
8382 .iter()
8383 .map(|t| SessionTab {
8384 service: format!("{:?}", t.service),
8385 title: t.title.clone(),
8386 breadcrumb: t.breadcrumb.clone(),
8387 filter: match t.service {
8388 Service::CloudWatchLogGroups => {
8389 Some(self.log_groups_state.log_groups.filter.clone())
8390 }
8391 _ => None,
8392 },
8393 selected_item: None,
8394 })
8395 .collect();
8396 self.current_session = Some(session.clone());
8397 session
8398 };
8399
8400 let _ = session.save();
8401 }
8402}
8403
8404#[cfg(test)]
8405mod iam_policy_view_tests {
8406 use super::*;
8407 use test_helpers::*;
8408
8409 #[test]
8410 fn test_enter_opens_policy_view() {
8411 let mut app = test_app();
8412 app.current_service = Service::IamRoles;
8413 app.service_selected = true;
8414 app.mode = Mode::Normal;
8415 app.view_mode = ViewMode::Detail;
8416 app.iam_state.current_role = Some("TestRole".to_string());
8417 app.iam_state.policies.items = vec![IamPolicy {
8418 policy_name: "TestPolicy".to_string(),
8419 policy_type: "Inline".to_string(),
8420 attached_via: "Direct".to_string(),
8421 attached_entities: "1".to_string(),
8422 description: "Test".to_string(),
8423 creation_time: "2023-01-01".to_string(),
8424 edited_time: "2023-01-01".to_string(),
8425 policy_arn: None,
8426 }];
8427 app.iam_state.policies.reset();
8428
8429 app.handle_action(Action::Select);
8430
8431 assert_eq!(app.view_mode, ViewMode::PolicyView);
8432 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
8433 assert_eq!(app.iam_state.policy_scroll, 0);
8434 assert!(app.iam_state.policies.loading);
8435 }
8436
8437 #[test]
8438 fn test_escape_closes_policy_view() {
8439 let mut app = test_app();
8440 app.current_service = Service::IamRoles;
8441 app.service_selected = true;
8442 app.mode = Mode::Normal;
8443 app.view_mode = ViewMode::PolicyView;
8444 app.iam_state.current_role = Some("TestRole".to_string());
8445 app.iam_state.current_policy = Some("TestPolicy".to_string());
8446 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
8447 app.iam_state.policy_scroll = 5;
8448
8449 app.handle_action(Action::PrevPane);
8450
8451 assert_eq!(app.view_mode, ViewMode::Detail);
8452 assert_eq!(app.iam_state.current_policy, None);
8453 assert_eq!(app.iam_state.policy_document, "");
8454 assert_eq!(app.iam_state.policy_scroll, 0);
8455 }
8456
8457 #[test]
8458 fn test_ctrl_d_scrolls_down_in_policy_view() {
8459 let mut app = test_app();
8460 app.current_service = Service::IamRoles;
8461 app.service_selected = true;
8462 app.mode = Mode::Normal;
8463 app.view_mode = ViewMode::PolicyView;
8464 app.iam_state.current_role = Some("TestRole".to_string());
8465 app.iam_state.current_policy = Some("TestPolicy".to_string());
8466 app.iam_state.policy_document = (0..100)
8467 .map(|i| format!("line {}", i))
8468 .collect::<Vec<_>>()
8469 .join("\n");
8470 app.iam_state.policy_scroll = 0;
8471
8472 app.handle_action(Action::ScrollDown);
8473
8474 assert_eq!(app.iam_state.policy_scroll, 10);
8475
8476 app.handle_action(Action::ScrollDown);
8477
8478 assert_eq!(app.iam_state.policy_scroll, 20);
8479 }
8480
8481 #[test]
8482 fn test_ctrl_u_scrolls_up_in_policy_view() {
8483 let mut app = test_app();
8484 app.current_service = Service::IamRoles;
8485 app.service_selected = true;
8486 app.mode = Mode::Normal;
8487 app.view_mode = ViewMode::PolicyView;
8488 app.iam_state.current_role = Some("TestRole".to_string());
8489 app.iam_state.current_policy = Some("TestPolicy".to_string());
8490 app.iam_state.policy_document = (0..100)
8491 .map(|i| format!("line {}", i))
8492 .collect::<Vec<_>>()
8493 .join("\n");
8494 app.iam_state.policy_scroll = 30;
8495
8496 app.handle_action(Action::ScrollUp);
8497
8498 assert_eq!(app.iam_state.policy_scroll, 20);
8499
8500 app.handle_action(Action::ScrollUp);
8501
8502 assert_eq!(app.iam_state.policy_scroll, 10);
8503 }
8504
8505 #[test]
8506 fn test_scroll_does_not_go_negative() {
8507 let mut app = test_app();
8508 app.current_service = Service::IamRoles;
8509 app.service_selected = true;
8510 app.mode = Mode::Normal;
8511 app.view_mode = ViewMode::PolicyView;
8512 app.iam_state.current_role = Some("TestRole".to_string());
8513 app.iam_state.current_policy = Some("TestPolicy".to_string());
8514 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8515 app.iam_state.policy_scroll = 0;
8516
8517 app.handle_action(Action::ScrollUp);
8518
8519 assert_eq!(app.iam_state.policy_scroll, 0);
8520 }
8521
8522 #[test]
8523 fn test_scroll_does_not_exceed_max() {
8524 let mut app = test_app();
8525 app.current_service = Service::IamRoles;
8526 app.service_selected = true;
8527 app.mode = Mode::Normal;
8528 app.view_mode = ViewMode::PolicyView;
8529 app.iam_state.current_role = Some("TestRole".to_string());
8530 app.iam_state.current_policy = Some("TestPolicy".to_string());
8531 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8532 app.iam_state.policy_scroll = 0;
8533
8534 app.handle_action(Action::ScrollDown);
8535
8536 assert_eq!(app.iam_state.policy_scroll, 2); }
8538
8539 #[test]
8540 fn test_policy_view_console_url() {
8541 let mut app = test_app();
8542 app.current_service = Service::IamRoles;
8543 app.service_selected = true;
8544 app.view_mode = ViewMode::PolicyView;
8545 app.iam_state.current_role = Some("TestRole".to_string());
8546 app.iam_state.current_policy = Some("TestPolicy".to_string());
8547
8548 let url = app.get_console_url();
8549
8550 assert!(url.contains("us-east-1.console.aws.amazon.com"));
8551 assert!(url.contains("/roles/details/TestRole"));
8552 assert!(url.contains("/editPolicy/TestPolicy"));
8553 assert!(url.contains("step=addPermissions"));
8554 }
8555
8556 #[test]
8557 fn test_esc_from_policy_view_goes_to_role_detail() {
8558 let mut app = test_app();
8559 app.current_service = Service::IamRoles;
8560 app.service_selected = true;
8561 app.mode = Mode::Normal;
8562 app.view_mode = ViewMode::PolicyView;
8563 app.iam_state.current_role = Some("TestRole".to_string());
8564 app.iam_state.current_policy = Some("TestPolicy".to_string());
8565 app.iam_state.policy_document = "test".to_string();
8566 app.iam_state.policy_scroll = 5;
8567
8568 app.handle_action(Action::GoBack);
8569
8570 assert_eq!(app.view_mode, ViewMode::Detail);
8571 assert_eq!(app.iam_state.current_policy, None);
8572 assert_eq!(app.iam_state.policy_document, "");
8573 assert_eq!(app.iam_state.policy_scroll, 0);
8574 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
8575 }
8576
8577 #[test]
8578 fn test_esc_from_role_detail_goes_to_role_list() {
8579 let mut app = test_app();
8580 app.current_service = Service::IamRoles;
8581 app.service_selected = true;
8582 app.mode = Mode::Normal;
8583 app.view_mode = ViewMode::Detail;
8584 app.iam_state.current_role = Some("TestRole".to_string());
8585
8586 app.handle_action(Action::GoBack);
8587
8588 assert_eq!(app.iam_state.current_role, None);
8589 }
8590
8591 #[test]
8592 fn test_right_arrow_expands_policy_row() {
8593 let mut app = test_app();
8594 app.current_service = Service::IamRoles;
8595 app.service_selected = true;
8596 app.mode = Mode::Normal;
8597 app.view_mode = ViewMode::Detail;
8598 app.iam_state.current_role = Some("TestRole".to_string());
8599 app.iam_state.policies.items = vec![IamPolicy {
8600 policy_name: "TestPolicy".to_string(),
8601 policy_type: "Inline".to_string(),
8602 attached_via: "Direct".to_string(),
8603 attached_entities: "1".to_string(),
8604 description: "Test".to_string(),
8605 creation_time: "2023-01-01".to_string(),
8606 edited_time: "2023-01-01".to_string(),
8607 policy_arn: None,
8608 }];
8609 app.iam_state.policies.reset();
8610
8611 app.handle_action(Action::NextPane);
8612
8613 assert_eq!(app.view_mode, ViewMode::Detail);
8615 assert_eq!(app.iam_state.current_policy, None);
8616 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
8617 }
8618
8619 #[test]
8620 fn test_managed_policy_has_creation_and_edit_time() {
8621 use crate::iam::Policy as IamPolicy;
8625 let managed = IamPolicy {
8626 policy_name: "MyManagedPolicy".to_string(),
8627 policy_type: "Managed".to_string(),
8628 attached_via: "Direct".to_string(),
8629 attached_entities: "-".to_string(),
8630 description: "-".to_string(),
8631 creation_time: "2024-01-01 00:00:00 (UTC)".to_string(),
8632 edited_time: "2024-06-01 00:00:00 (UTC)".to_string(),
8633 policy_arn: Some("arn:aws:iam::123:policy/MyManagedPolicy".to_string()),
8634 };
8635 assert_ne!(
8636 managed.creation_time, "-",
8637 "Managed policy must have a real creation time, not '-'"
8638 );
8639 assert_ne!(
8640 managed.edited_time, "-",
8641 "Managed policy must have a real edit time, not '-'"
8642 );
8643
8644 let inline = IamPolicy {
8646 policy_name: "InlinePolicy".to_string(),
8647 policy_type: "Inline".to_string(),
8648 attached_via: "Direct".to_string(),
8649 attached_entities: "-".to_string(),
8650 description: "-".to_string(),
8651 creation_time: "-".to_string(),
8652 edited_time: "-".to_string(),
8653 policy_arn: None,
8654 };
8655 assert_eq!(
8656 inline.creation_time, "-",
8657 "Inline policies have no creation time"
8658 );
8659 }
8660
8661 #[test]
8662 fn test_yank_in_policy_view_copies_document_not_arn() {
8663 let mut app = test_app();
8665 app.current_service = Service::IamRoles;
8666 app.service_selected = true;
8667 app.mode = Mode::Normal;
8668 app.view_mode = ViewMode::PolicyView;
8669 app.iam_state.current_role = Some("TestRole".to_string());
8670 app.iam_state.current_policy = Some("TestPolicy".to_string());
8671
8672 let policy_json = "{\n \"Version\": \"2012-10-17\",\n \"Statement\": []\n}".to_string();
8673 app.iam_state.policy_document = policy_json.clone();
8674
8675 use crate::iam::actions::yank_roles;
8677 yank_roles(&app);
8678
8679 assert_eq!(app.iam_state.policy_document, policy_json);
8681 assert_eq!(app.view_mode, ViewMode::PolicyView);
8682 }
8683}
8684
8685#[cfg(test)]
8686mod tab_filter_tests {
8687 use super::*;
8688 use test_helpers::*;
8689
8690 #[test]
8691 fn test_space_t_opens_tab_picker() {
8692 let mut app = test_app();
8693 app.tabs = vec![
8694 Tab {
8695 service: Service::CloudWatchLogGroups,
8696 title: "Tab 1".to_string(),
8697 breadcrumb: "CloudWatch > Log groups".to_string(),
8698 },
8699 Tab {
8700 service: Service::S3Buckets,
8701 title: "Tab 2".to_string(),
8702 breadcrumb: "S3 › Buckets".to_string(),
8703 },
8704 ];
8705 app.current_tab = 0;
8706
8707 app.handle_action(Action::OpenTabPicker);
8708
8709 assert_eq!(app.mode, Mode::TabPicker);
8710 assert_eq!(app.tab_picker_selected, 0);
8711 }
8712
8713 #[test]
8714 fn test_tab_filter_works() {
8715 let mut app = test_app();
8716 app.tabs = vec![
8717 Tab {
8718 service: Service::CloudWatchLogGroups,
8719 title: "CloudWatch Logs".to_string(),
8720 breadcrumb: "CloudWatch > Log groups".to_string(),
8721 },
8722 Tab {
8723 service: Service::S3Buckets,
8724 title: "S3 Buckets".to_string(),
8725 breadcrumb: "S3 › Buckets".to_string(),
8726 },
8727 Tab {
8728 service: Service::CloudWatchAlarms,
8729 title: "CloudWatch Alarms".to_string(),
8730 breadcrumb: "CloudWatch › Alarms".to_string(),
8731 },
8732 ];
8733 app.mode = Mode::TabPicker;
8734
8735 app.handle_action(Action::FilterInput('s'));
8737 app.handle_action(Action::FilterInput('3'));
8738
8739 let filtered = app.get_filtered_tabs();
8740 assert_eq!(filtered.len(), 1);
8741 assert_eq!(filtered[0].1.title, "S3 Buckets");
8742 }
8743
8744 #[test]
8745 fn test_tab_filter_by_breadcrumb() {
8746 let mut app = test_app();
8747 app.tabs = vec![
8748 Tab {
8749 service: Service::CloudWatchLogGroups,
8750 title: "Tab 1".to_string(),
8751 breadcrumb: "CloudWatch > Log groups".to_string(),
8752 },
8753 Tab {
8754 service: Service::S3Buckets,
8755 title: "Tab 2".to_string(),
8756 breadcrumb: "S3 › Buckets".to_string(),
8757 },
8758 ];
8759 app.mode = Mode::TabPicker;
8760
8761 app.handle_action(Action::FilterInput('c'));
8763 app.handle_action(Action::FilterInput('l'));
8764 app.handle_action(Action::FilterInput('o'));
8765 app.handle_action(Action::FilterInput('u'));
8766 app.handle_action(Action::FilterInput('d'));
8767
8768 let filtered = app.get_filtered_tabs();
8769 assert_eq!(filtered.len(), 1);
8770 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
8771 }
8772
8773 #[test]
8774 fn test_tab_filter_backspace() {
8775 let mut app = test_app();
8776 app.tabs = vec![
8777 Tab {
8778 service: Service::CloudWatchLogGroups,
8779 title: "CloudWatch Logs".to_string(),
8780 breadcrumb: "CloudWatch > Log groups".to_string(),
8781 },
8782 Tab {
8783 service: Service::S3Buckets,
8784 title: "S3 Buckets".to_string(),
8785 breadcrumb: "S3 › Buckets".to_string(),
8786 },
8787 ];
8788 app.mode = Mode::TabPicker;
8789
8790 app.handle_action(Action::FilterInput('s'));
8791 app.handle_action(Action::FilterInput('3'));
8792 assert_eq!(app.tab_filter, "s3");
8793
8794 app.handle_action(Action::FilterBackspace);
8795 assert_eq!(app.tab_filter, "s");
8796
8797 let filtered = app.get_filtered_tabs();
8798 assert_eq!(filtered.len(), 2); }
8800
8801 #[test]
8802 fn test_tab_selection_with_filter() {
8803 let mut app = test_app();
8804 app.tabs = vec![
8805 Tab {
8806 service: Service::CloudWatchLogGroups,
8807 title: "CloudWatch Logs".to_string(),
8808 breadcrumb: "CloudWatch > Log groups".to_string(),
8809 },
8810 Tab {
8811 service: Service::S3Buckets,
8812 title: "S3 Buckets".to_string(),
8813 breadcrumb: "S3 › Buckets".to_string(),
8814 },
8815 ];
8816 app.mode = Mode::TabPicker;
8817 app.current_tab = 0;
8818
8819 app.handle_action(Action::FilterInput('s'));
8821 app.handle_action(Action::FilterInput('3'));
8822
8823 app.handle_action(Action::Select);
8825
8826 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
8828 assert_eq!(app.tab_filter, ""); }
8830}
8831
8832#[cfg(test)]
8833mod region_latency_tests {
8834 use super::*;
8835 use test_helpers::*;
8836
8837 #[test]
8838 fn test_regions_sorted_by_latency() {
8839 let mut app = test_app();
8840
8841 app.region_latencies.insert("us-west-2".to_string(), 50);
8843 app.region_latencies.insert("us-east-1".to_string(), 10);
8844 app.region_latencies.insert("eu-west-1".to_string(), 100);
8845
8846 let filtered = app.get_filtered_regions();
8847
8848 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
8850
8851 assert!(with_latency.len() >= 3);
8852 assert_eq!(with_latency[0].code, "us-east-1");
8853 assert_eq!(with_latency[0].latency_ms, Some(10));
8854 assert_eq!(with_latency[1].code, "us-west-2");
8855 assert_eq!(with_latency[1].latency_ms, Some(50));
8856 assert_eq!(with_latency[2].code, "eu-west-1");
8857 assert_eq!(with_latency[2].latency_ms, Some(100));
8858 }
8859
8860 #[test]
8861 fn test_regions_with_latency_before_without() {
8862 let mut app = test_app();
8863
8864 app.region_latencies.insert("eu-west-1".to_string(), 100);
8866
8867 let filtered = app.get_filtered_regions();
8868
8869 assert_eq!(filtered[0].code, "eu-west-1");
8871 assert_eq!(filtered[0].latency_ms, Some(100));
8872
8873 for region in &filtered[1..] {
8875 assert!(region.latency_ms.is_none());
8876 }
8877 }
8878
8879 #[test]
8880 fn test_region_filter_with_latency() {
8881 let mut app = test_app();
8882
8883 app.region_latencies.insert("us-east-1".to_string(), 10);
8884 app.region_latencies.insert("us-west-2".to_string(), 50);
8885 app.region_filter = "us".to_string();
8886
8887 let filtered = app.get_filtered_regions();
8888
8889 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
8891 assert_eq!(filtered[0].code, "us-east-1");
8892 assert_eq!(filtered[1].code, "us-west-2");
8893 }
8894
8895 #[test]
8896 fn test_latency_persists_across_filters() {
8897 let mut app = test_app();
8898
8899 app.region_latencies.insert("us-east-1".to_string(), 10);
8900
8901 app.region_filter = "eu".to_string();
8903 let filtered = app.get_filtered_regions();
8904 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
8905
8906 app.region_filter.clear();
8908 let all = app.get_filtered_regions();
8909
8910 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
8912 assert_eq!(us_east.latency_ms, Some(10));
8913 }
8914
8915 #[test]
8916 fn test_measure_region_latencies_clears_previous() {
8917 let mut app = test_app();
8918
8919 app.region_latencies.insert("us-east-1".to_string(), 100);
8921 app.region_latencies.insert("eu-west-1".to_string(), 200);
8922
8923 app.measure_region_latencies();
8925
8926 assert!(
8928 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
8929 );
8930 }
8931
8932 #[test]
8933 fn test_regions_with_latency_sorted_first() {
8934 let mut app = test_app();
8935
8936 app.region_latencies.insert("us-east-1".to_string(), 50);
8938 app.region_latencies.insert("eu-west-1".to_string(), 500);
8939
8940 let filtered = app.get_filtered_regions();
8941
8942 assert!(filtered.len() > 2);
8944
8945 assert_eq!(filtered[0].code, "us-east-1");
8947 assert_eq!(filtered[0].latency_ms, Some(50));
8948 assert_eq!(filtered[1].code, "eu-west-1");
8949 assert_eq!(filtered[1].latency_ms, Some(500));
8950
8951 for region in &filtered[2..] {
8953 assert!(region.latency_ms.is_none());
8954 }
8955 }
8956
8957 #[test]
8958 fn test_regions_without_latency_sorted_as_1000ms() {
8959 let mut app = test_app();
8960
8961 app.region_latencies
8963 .insert("ap-southeast-2".to_string(), 1500);
8964 app.region_latencies.insert("us-east-1".to_string(), 50);
8966
8967 let filtered = app.get_filtered_regions();
8968
8969 assert_eq!(filtered[0].code, "us-east-1");
8971 assert_eq!(filtered[0].latency_ms, Some(50));
8972
8973 let slow_region_idx = filtered
8975 .iter()
8976 .position(|r| r.code == "ap-southeast-2")
8977 .unwrap();
8978 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
8982 assert!(region.latency_ms.is_none());
8983 }
8984 }
8985
8986 #[test]
8987 fn test_region_picker_opens_with_latencies() {
8988 let mut app = test_app();
8989
8990 app.region_filter.clear();
8992 app.region_picker_selected = 0;
8993 app.measure_region_latencies();
8994
8995 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
8998 }
8999
9000 #[test]
9001 fn test_region_picker_enter_in_filter_mode_exits_insert_not_selects() {
9002 let mut app = test_app();
9006 app.mode = Mode::RegionPicker;
9007 app.region_filter_active = true; app.region_filter = "us".to_string();
9009 let original_region = app.region.clone();
9010
9011 app.handle_action(Action::Select);
9013
9014 assert!(
9015 !app.region_filter_active,
9016 "Filter must be deactivated (switch to NORMAL/table focus)"
9017 );
9018 assert_eq!(
9019 app.mode,
9020 Mode::RegionPicker,
9021 "Must stay in RegionPicker, not close it"
9022 );
9023 assert_eq!(
9024 app.region, original_region,
9025 "Region must NOT change when Enter is pressed in INSERT mode"
9026 );
9027 }
9028
9029 #[test]
9030 fn test_region_picker_enter_in_normal_mode_selects_region() {
9031 let mut app = test_app();
9033 app.mode = Mode::RegionPicker;
9034 app.region_filter_active = false; app.region_filter.clear();
9036
9037 app.handle_action(Action::Select);
9038
9039 assert_eq!(
9041 app.mode,
9042 Mode::Normal,
9043 "Must close picker after selecting region in NORMAL mode"
9044 );
9045 }
9046
9047 #[test]
9048 fn test_region_filter_can_type_any_character_in_insert_mode() {
9049 use crate::keymap::{handle_key, Mode as KMode};
9053 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9054
9055 let mut app = test_app();
9056 app.mode = Mode::RegionPicker;
9057 app.region_filter_active = true;
9058 app.region_filter.clear();
9059
9060 let t_key = KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE);
9062 let o_key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
9063 let k_key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
9064
9065 if let Some(action) = handle_key(t_key, KMode::RegionPicker) {
9067 app.handle_action(action);
9068 }
9069 if let Some(action) = handle_key(o_key, KMode::RegionPicker) {
9070 app.handle_action(action);
9071 }
9072 if let Some(action) = handle_key(k_key, KMode::RegionPicker) {
9073 app.handle_action(action);
9074 }
9075
9076 assert_eq!(
9077 app.region_filter, "tok",
9078 "'k' must be typed into filter (not navigate up) when filter is active"
9079 );
9080 }
9081
9082 #[test]
9083 fn test_ecr_tab_next() {
9084 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
9085 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
9086 }
9087
9088 #[test]
9089 fn test_ecr_tab_switching() {
9090 let mut app = test_app();
9091 app.current_service = Service::EcrRepositories;
9092 app.service_selected = true;
9093 app.ecr_state.tab = EcrTab::Private;
9094
9095 app.handle_action(Action::NextDetailTab);
9096 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9097 assert_eq!(app.ecr_state.repositories.selected, 0);
9098
9099 app.handle_action(Action::NextDetailTab);
9100 assert_eq!(app.ecr_state.tab, EcrTab::Private);
9101 }
9102
9103 #[test]
9104 fn test_ecr_navigation() {
9105 let mut app = test_app();
9106 app.current_service = Service::EcrRepositories;
9107 app.service_selected = true;
9108 app.mode = Mode::Normal;
9109 app.ecr_state.repositories.items = vec![
9110 EcrRepository {
9111 name: "repo1".to_string(),
9112 uri: "uri1".to_string(),
9113 created_at: "2023-01-01".to_string(),
9114 tag_immutability: "MUTABLE".to_string(),
9115 encryption_type: "AES256".to_string(),
9116 },
9117 EcrRepository {
9118 name: "repo2".to_string(),
9119 uri: "uri2".to_string(),
9120 created_at: "2023-01-02".to_string(),
9121 tag_immutability: "IMMUTABLE".to_string(),
9122 encryption_type: "KMS".to_string(),
9123 },
9124 ];
9125
9126 app.handle_action(Action::NextItem);
9127 assert_eq!(app.ecr_state.repositories.selected, 1);
9128
9129 app.handle_action(Action::PrevItem);
9130 assert_eq!(app.ecr_state.repositories.selected, 0);
9131 }
9132
9133 #[test]
9134 fn test_ecr_filter() {
9135 let mut app = test_app();
9136 app.current_service = Service::EcrRepositories;
9137 app.service_selected = true;
9138 app.ecr_state.repositories.items = vec![
9139 EcrRepository {
9140 name: "my-app".to_string(),
9141 uri: "uri1".to_string(),
9142 created_at: "2023-01-01".to_string(),
9143 tag_immutability: "MUTABLE".to_string(),
9144 encryption_type: "AES256".to_string(),
9145 },
9146 EcrRepository {
9147 name: "other-service".to_string(),
9148 uri: "uri2".to_string(),
9149 created_at: "2023-01-02".to_string(),
9150 tag_immutability: "IMMUTABLE".to_string(),
9151 encryption_type: "KMS".to_string(),
9152 },
9153 ];
9154
9155 app.ecr_state.repositories.filter = "app".to_string();
9156 let filtered = filtered_ecr_repositories(&app);
9157 assert_eq!(filtered.len(), 1);
9158 assert_eq!(filtered[0].name, "my-app");
9159 }
9160
9161 #[test]
9162 fn test_ecr_filter_input() {
9163 let mut app = test_app();
9164 app.current_service = Service::EcrRepositories;
9165 app.service_selected = true;
9166 app.mode = Mode::FilterInput;
9167
9168 app.handle_action(Action::FilterInput('t'));
9169 app.handle_action(Action::FilterInput('e'));
9170 app.handle_action(Action::FilterInput('s'));
9171 app.handle_action(Action::FilterInput('t'));
9172 assert_eq!(app.ecr_state.repositories.filter, "test");
9173
9174 app.handle_action(Action::FilterBackspace);
9175 assert_eq!(app.ecr_state.repositories.filter, "tes");
9176 }
9177
9178 #[test]
9179 fn test_ecr_filter_resets_selection() {
9180 let mut app = test_app();
9181 app.current_service = Service::EcrRepositories;
9182 app.service_selected = true;
9183 app.mode = Mode::FilterInput;
9184 app.ecr_state.repositories.items = vec![
9185 EcrRepository {
9186 name: "repo1".to_string(),
9187 uri: "uri1".to_string(),
9188 created_at: "2023-01-01".to_string(),
9189 tag_immutability: "MUTABLE".to_string(),
9190 encryption_type: "AES256".to_string(),
9191 },
9192 EcrRepository {
9193 name: "repo2".to_string(),
9194 uri: "uri2".to_string(),
9195 created_at: "2023-01-02".to_string(),
9196 tag_immutability: "IMMUTABLE".to_string(),
9197 encryption_type: "KMS".to_string(),
9198 },
9199 EcrRepository {
9200 name: "repo3".to_string(),
9201 uri: "uri3".to_string(),
9202 created_at: "2023-01-03".to_string(),
9203 tag_immutability: "MUTABLE".to_string(),
9204 encryption_type: "AES256".to_string(),
9205 },
9206 ];
9207
9208 app.ecr_state.repositories.selected = 2;
9210 assert_eq!(app.ecr_state.repositories.selected, 2);
9211
9212 app.handle_action(Action::FilterInput('t'));
9214 assert_eq!(app.ecr_state.repositories.filter, "t");
9215 assert_eq!(app.ecr_state.repositories.selected, 0);
9216
9217 app.ecr_state.repositories.selected = 1;
9219
9220 app.handle_action(Action::FilterBackspace);
9222 assert_eq!(app.ecr_state.repositories.filter, "");
9223 assert_eq!(app.ecr_state.repositories.selected, 0);
9224 }
9225
9226 #[test]
9227 fn test_ecr_images_filter_resets_selection() {
9228 let mut app = test_app();
9229 app.current_service = Service::EcrRepositories;
9230 app.service_selected = true;
9231 app.mode = Mode::FilterInput;
9232 app.ecr_state.current_repository = Some("test-repo".to_string());
9233 app.ecr_state.images.items = vec![
9234 EcrImage {
9235 tag: "v1.0.0".to_string(),
9236 artifact_type: "container".to_string(),
9237 digest: "sha256:abc123".to_string(),
9238 pushed_at: "2023-01-01".to_string(),
9239 size_bytes: 1000,
9240 uri: "uri1".to_string(),
9241 last_pull_time: "".to_string(),
9242 },
9243 EcrImage {
9244 tag: "v2.0.0".to_string(),
9245 artifact_type: "container".to_string(),
9246 digest: "sha256:def456".to_string(),
9247 pushed_at: "2023-01-02".to_string(),
9248 size_bytes: 2000,
9249 uri: "uri2".to_string(),
9250 last_pull_time: "".to_string(),
9251 },
9252 ];
9253
9254 app.ecr_state.images.selected = 1;
9256 assert_eq!(app.ecr_state.images.selected, 1);
9257
9258 app.handle_action(Action::FilterInput('v'));
9260 assert_eq!(app.ecr_state.images.filter, "v");
9261 assert_eq!(app.ecr_state.images.selected, 0);
9262 }
9263
9264 #[test]
9265 fn test_iam_users_filter_input() {
9266 let mut app = test_app();
9267 app.current_service = Service::IamUsers;
9268 app.service_selected = true;
9269 app.mode = Mode::FilterInput;
9270
9271 app.handle_action(Action::FilterInput('a'));
9272 app.handle_action(Action::FilterInput('d'));
9273 app.handle_action(Action::FilterInput('m'));
9274 app.handle_action(Action::FilterInput('i'));
9275 app.handle_action(Action::FilterInput('n'));
9276 assert_eq!(app.iam_state.users.filter, "admin");
9277
9278 app.handle_action(Action::FilterBackspace);
9279 assert_eq!(app.iam_state.users.filter, "admi");
9280 }
9281
9282 #[test]
9283 fn test_iam_policies_filter_input() {
9284 let mut app = test_app();
9285 app.current_service = Service::IamUsers;
9286 app.service_selected = true;
9287 app.iam_state.current_user = Some("testuser".to_string());
9288 app.mode = Mode::FilterInput;
9289
9290 app.handle_action(Action::FilterInput('r'));
9291 app.handle_action(Action::FilterInput('e'));
9292 app.handle_action(Action::FilterInput('a'));
9293 app.handle_action(Action::FilterInput('d'));
9294 assert_eq!(app.iam_state.policies.filter, "read");
9295
9296 app.handle_action(Action::FilterBackspace);
9297 assert_eq!(app.iam_state.policies.filter, "rea");
9298 }
9299
9300 #[test]
9301 fn test_iam_start_filter() {
9302 let mut app = test_app();
9303 app.current_service = Service::IamUsers;
9304 app.service_selected = true;
9305 app.mode = Mode::Normal;
9306
9307 app.handle_action(Action::StartFilter);
9308 assert_eq!(app.mode, Mode::FilterInput);
9309 }
9310
9311 #[test]
9312 fn test_iam_roles_filter_input() {
9313 let mut app = test_app();
9314 app.current_service = Service::IamRoles;
9315 app.service_selected = true;
9316 app.mode = Mode::FilterInput;
9317
9318 app.handle_action(Action::FilterInput('a'));
9319 app.handle_action(Action::FilterInput('d'));
9320 app.handle_action(Action::FilterInput('m'));
9321 app.handle_action(Action::FilterInput('i'));
9322 app.handle_action(Action::FilterInput('n'));
9323 assert_eq!(app.iam_state.roles.filter, "admin");
9324
9325 app.handle_action(Action::FilterBackspace);
9326 assert_eq!(app.iam_state.roles.filter, "admi");
9327 }
9328
9329 #[test]
9330 fn test_iam_roles_start_filter() {
9331 let mut app = test_app();
9332 app.current_service = Service::IamRoles;
9333 app.service_selected = true;
9334 app.mode = Mode::Normal;
9335
9336 app.handle_action(Action::StartFilter);
9337 assert_eq!(app.mode, Mode::FilterInput);
9338 }
9339
9340 #[test]
9341 fn test_iam_roles_navigation() {
9342 let mut app = test_app();
9343 app.current_service = Service::IamRoles;
9344 app.service_selected = true;
9345 app.mode = Mode::Normal;
9346 app.iam_state.roles.items = (0..10)
9347 .map(|i| IamRole {
9348 role_name: format!("role{}", i),
9349 path: "/".to_string(),
9350 trusted_entities: String::new(),
9351 last_activity: String::new(),
9352 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9353 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9354 description: String::new(),
9355 max_session_duration: Some(3600),
9356 })
9357 .collect();
9358
9359 assert_eq!(app.iam_state.roles.selected, 0);
9360
9361 app.handle_action(Action::NextItem);
9362 assert_eq!(app.iam_state.roles.selected, 1);
9363
9364 app.handle_action(Action::NextItem);
9365 assert_eq!(app.iam_state.roles.selected, 2);
9366
9367 app.handle_action(Action::PrevItem);
9368 assert_eq!(app.iam_state.roles.selected, 1);
9369 }
9370
9371 #[test]
9372 fn test_iam_roles_page_hotkey() {
9373 let mut app = test_app();
9374 app.current_service = Service::IamRoles;
9375 app.service_selected = true;
9376 app.mode = Mode::Normal;
9377 app.iam_state.roles.page_size = PageSize::Ten;
9378 app.iam_state.roles.items = (0..100)
9379 .map(|i| IamRole {
9380 role_name: format!("role{}", i),
9381 path: "/".to_string(),
9382 trusted_entities: String::new(),
9383 last_activity: String::new(),
9384 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9385 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9386 description: String::new(),
9387 max_session_duration: Some(3600),
9388 })
9389 .collect();
9390
9391 app.handle_action(Action::FilterInput('2'));
9392 app.handle_action(Action::OpenColumnSelector);
9393 assert_eq!(app.iam_state.roles.selected, 10); }
9395
9396 #[test]
9397 fn test_iam_users_page_hotkey() {
9398 let mut app = test_app();
9399 app.current_service = Service::IamUsers;
9400 app.service_selected = true;
9401 app.mode = Mode::Normal;
9402 app.iam_state.users.page_size = PageSize::Ten;
9403 app.iam_state.users.items = (0..100)
9404 .map(|i| IamUser {
9405 user_name: format!("user{}", i),
9406 path: "/".to_string(),
9407 groups: String::new(),
9408 last_activity: String::new(),
9409 mfa: String::new(),
9410 password_age: String::new(),
9411 console_last_sign_in: String::new(),
9412 access_key_id: String::new(),
9413 active_key_age: String::new(),
9414 access_key_last_used: String::new(),
9415 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
9416 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9417 console_access: String::new(),
9418 signing_certs: String::new(),
9419 })
9420 .collect();
9421
9422 app.handle_action(Action::FilterInput('3'));
9423 app.handle_action(Action::OpenColumnSelector);
9424 assert_eq!(app.iam_state.users.selected, 20); }
9426
9427 #[test]
9428 fn test_ecr_scroll_navigation() {
9429 let mut app = test_app();
9430 app.current_service = Service::EcrRepositories;
9431 app.service_selected = true;
9432 app.ecr_state.repositories.items = (0..20)
9433 .map(|i| EcrRepository {
9434 name: format!("repo{}", i),
9435 uri: format!("uri{}", i),
9436 created_at: "2023-01-01".to_string(),
9437 tag_immutability: "MUTABLE".to_string(),
9438 encryption_type: "AES256".to_string(),
9439 })
9440 .collect();
9441
9442 app.handle_action(Action::ScrollDown);
9443 assert_eq!(app.ecr_state.repositories.selected, 10);
9444
9445 app.handle_action(Action::ScrollUp);
9446 assert_eq!(app.ecr_state.repositories.selected, 0);
9447 }
9448
9449 #[test]
9450 fn test_ecr_tab_switching_triggers_reload() {
9451 let mut app = test_app();
9452 app.current_service = Service::EcrRepositories;
9453 app.service_selected = true;
9454 app.ecr_state.tab = EcrTab::Private;
9455 app.ecr_state.repositories.loading = false;
9456 app.ecr_state.repositories.items = vec![EcrRepository {
9457 name: "private-repo".to_string(),
9458 uri: "uri".to_string(),
9459 created_at: "2023-01-01".to_string(),
9460 tag_immutability: "MUTABLE".to_string(),
9461 encryption_type: "AES256".to_string(),
9462 }];
9463
9464 app.handle_action(Action::NextDetailTab);
9465 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9466 assert!(app.ecr_state.repositories.loading);
9467 assert_eq!(app.ecr_state.repositories.selected, 0);
9468 }
9469
9470 #[test]
9471 fn test_ecr_tab_cycles_between_private_and_public() {
9472 let mut app = test_app();
9473 app.current_service = Service::EcrRepositories;
9474 app.service_selected = true;
9475 app.ecr_state.tab = EcrTab::Private;
9476
9477 app.handle_action(Action::NextDetailTab);
9478 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9479
9480 app.handle_action(Action::NextDetailTab);
9481 assert_eq!(app.ecr_state.tab, EcrTab::Private);
9482 }
9483
9484 #[test]
9485 fn test_page_size_values() {
9486 assert_eq!(PageSize::Ten.value(), 10);
9487 assert_eq!(PageSize::TwentyFive.value(), 25);
9488 assert_eq!(PageSize::Fifty.value(), 50);
9489 assert_eq!(PageSize::OneHundred.value(), 100);
9490 }
9491
9492 #[test]
9493 fn test_page_size_next() {
9494 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
9495 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
9496 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
9497 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
9498 }
9499
9500 #[test]
9501 fn test_ecr_enter_drills_into_repository() {
9502 let mut app = test_app();
9503 app.current_service = Service::EcrRepositories;
9504 app.service_selected = true;
9505 app.mode = Mode::Normal;
9506 app.ecr_state.repositories.items = vec![EcrRepository {
9507 name: "my-repo".to_string(),
9508 uri: "uri".to_string(),
9509 created_at: "2023-01-01".to_string(),
9510 tag_immutability: "MUTABLE".to_string(),
9511 encryption_type: "AES256".to_string(),
9512 }];
9513
9514 app.handle_action(Action::Select);
9515 assert_eq!(
9516 app.ecr_state.current_repository,
9517 Some("my-repo".to_string())
9518 );
9519 assert!(app.ecr_state.repositories.loading);
9520 }
9521
9522 #[test]
9523 fn test_ecr_repository_expansion() {
9524 let mut app = test_app();
9525 app.current_service = Service::EcrRepositories;
9526 app.service_selected = true;
9527 app.ecr_state.repositories.items = vec![EcrRepository {
9528 name: "my-repo".to_string(),
9529 uri: "uri".to_string(),
9530 created_at: "2023-01-01".to_string(),
9531 tag_immutability: "MUTABLE".to_string(),
9532 encryption_type: "AES256".to_string(),
9533 }];
9534 app.ecr_state.repositories.selected = 0;
9535
9536 assert_eq!(app.ecr_state.repositories.expanded_item, None);
9537
9538 app.handle_action(Action::NextPane);
9539 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
9540
9541 app.handle_action(Action::PrevPane);
9542 assert_eq!(app.ecr_state.repositories.expanded_item, None);
9543 }
9544
9545 #[test]
9546 fn test_ecr_ctrl_d_scrolls_down() {
9547 let mut app = test_app();
9548 app.current_service = Service::EcrRepositories;
9549 app.service_selected = true;
9550 app.mode = Mode::Normal;
9551 app.ecr_state.repositories.items = (0..30)
9552 .map(|i| EcrRepository {
9553 name: format!("repo{}", i),
9554 uri: format!("uri{}", i),
9555 created_at: "2023-01-01".to_string(),
9556 tag_immutability: "MUTABLE".to_string(),
9557 encryption_type: "AES256".to_string(),
9558 })
9559 .collect();
9560 app.ecr_state.repositories.selected = 0;
9561
9562 app.handle_action(Action::PageDown);
9563 assert_eq!(app.ecr_state.repositories.selected, 10);
9564 }
9565
9566 #[test]
9567 fn test_ecr_ctrl_u_scrolls_up() {
9568 let mut app = test_app();
9569 app.current_service = Service::EcrRepositories;
9570 app.service_selected = true;
9571 app.mode = Mode::Normal;
9572 app.ecr_state.repositories.items = (0..30)
9573 .map(|i| EcrRepository {
9574 name: format!("repo{}", i),
9575 uri: format!("uri{}", i),
9576 created_at: "2023-01-01".to_string(),
9577 tag_immutability: "MUTABLE".to_string(),
9578 encryption_type: "AES256".to_string(),
9579 })
9580 .collect();
9581 app.ecr_state.repositories.selected = 15;
9582
9583 app.handle_action(Action::PageUp);
9584 assert_eq!(app.ecr_state.repositories.selected, 5);
9585 }
9586
9587 #[test]
9588 fn test_ecr_images_ctrl_d_scrolls_down() {
9589 let mut app = test_app();
9590 app.current_service = Service::EcrRepositories;
9591 app.service_selected = true;
9592 app.mode = Mode::Normal;
9593 app.ecr_state.current_repository = Some("repo".to_string());
9594 app.ecr_state.images.items = (0..30)
9595 .map(|i| EcrImage {
9596 tag: format!("tag{}", i),
9597 artifact_type: "container".to_string(),
9598 pushed_at: "2023-01-01T12:00:00Z".to_string(),
9599 size_bytes: 104857600,
9600 uri: format!("uri{}", i),
9601 digest: format!("sha256:{}", i),
9602 last_pull_time: String::new(),
9603 })
9604 .collect();
9605 app.ecr_state.images.selected = 0;
9606
9607 app.handle_action(Action::PageDown);
9608 assert_eq!(app.ecr_state.images.selected, 10);
9609 }
9610
9611 #[test]
9612 fn test_ecr_esc_goes_back_from_images_to_repos() {
9613 let mut app = test_app();
9614 app.current_service = Service::EcrRepositories;
9615 app.service_selected = true;
9616 app.mode = Mode::Normal;
9617 app.ecr_state.current_repository = Some("my-repo".to_string());
9618 app.ecr_state.images.items = vec![EcrImage {
9619 tag: "latest".to_string(),
9620 artifact_type: "container".to_string(),
9621 pushed_at: "2023-01-01T12:00:00Z".to_string(),
9622 size_bytes: 104857600,
9623 uri: "uri".to_string(),
9624 digest: "sha256:abc".to_string(),
9625 last_pull_time: String::new(),
9626 }];
9627
9628 app.handle_action(Action::GoBack);
9629 assert_eq!(app.ecr_state.current_repository, None);
9630 assert!(app.ecr_state.images.items.is_empty());
9631 }
9632
9633 #[test]
9634 fn test_ecr_esc_collapses_expanded_image_first() {
9635 let mut app = test_app();
9636 app.current_service = Service::EcrRepositories;
9637 app.service_selected = true;
9638 app.mode = Mode::Normal;
9639 app.ecr_state.current_repository = Some("my-repo".to_string());
9640 app.ecr_state.images.expanded_item = Some(0);
9641
9642 app.handle_action(Action::GoBack);
9643 assert_eq!(app.ecr_state.images.expanded_item, None);
9644 assert_eq!(
9645 app.ecr_state.current_repository,
9646 Some("my-repo".to_string())
9647 );
9648 }
9649
9650 #[test]
9651 fn test_pagination_with_lowercase_p() {
9652 let mut app = test_app();
9653 app.current_service = Service::EcrRepositories;
9654 app.service_selected = true;
9655 app.mode = Mode::Normal;
9656 app.ecr_state.repositories.items = (0..100)
9657 .map(|i| EcrRepository {
9658 name: format!("repo{}", i),
9659 uri: format!("uri{}", i),
9660 created_at: "2023-01-01".to_string(),
9661 tag_immutability: "MUTABLE".to_string(),
9662 encryption_type: "AES256".to_string(),
9663 })
9664 .collect();
9665
9666 app.handle_action(Action::FilterInput('2'));
9668 assert_eq!(app.page_input, "2");
9669
9670 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
9674
9675 #[test]
9676 fn test_lowercase_p_without_number_opens_preferences() {
9677 let mut app = test_app();
9678 app.current_service = Service::EcrRepositories;
9679 app.service_selected = true;
9680 app.mode = Mode::Normal;
9681
9682 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
9684 }
9685
9686 #[test]
9687 fn test_ctrl_o_generates_correct_console_url() {
9688 let mut app = test_app();
9689 app.current_service = Service::EcrRepositories;
9690 app.service_selected = true;
9691 app.mode = Mode::Normal;
9692 app.config.account_id = "123456789012".to_string();
9693
9694 let url = app.get_console_url();
9696 assert!(url.contains("ecr/private-registry/repositories"));
9697 assert!(url.contains("region=us-east-1"));
9698
9699 app.ecr_state.current_repository = Some("my-repo".to_string());
9701 let url = app.get_console_url();
9702 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
9703 assert!(url.contains("region=us-east-1"));
9704 }
9705
9706 #[test]
9707 fn test_page_input_display_and_reset() {
9708 let mut app = test_app();
9709 app.current_service = Service::EcrRepositories;
9710 app.service_selected = true;
9711 app.mode = Mode::Normal;
9712 app.ecr_state.repositories.items = (0..100)
9713 .map(|i| EcrRepository {
9714 name: format!("repo{}", i),
9715 uri: format!("uri{}", i),
9716 created_at: "2023-01-01".to_string(),
9717 tag_immutability: "MUTABLE".to_string(),
9718 encryption_type: "AES256".to_string(),
9719 })
9720 .collect();
9721
9722 app.handle_action(Action::FilterInput('2'));
9724 assert_eq!(app.page_input, "2");
9725
9726 app.handle_action(Action::OpenColumnSelector);
9728 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
9731
9732 #[test]
9733 fn test_page_navigation_updates_scroll_offset_for_cfn() {
9734 let mut app = test_app();
9735 app.current_service = Service::CloudFormationStacks;
9736 app.service_selected = true;
9737 app.mode = Mode::Normal;
9738 app.cfn_state.table.items = (0..100)
9739 .map(|i| CfnStack {
9740 name: format!("stack-{}", i),
9741 stack_id: format!(
9742 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9743 i
9744 ),
9745 status: "CREATE_COMPLETE".to_string(),
9746 created_time: "2023-01-01T00:00:00Z".to_string(),
9747 updated_time: "2023-01-01T00:00:00Z".to_string(),
9748 deleted_time: String::new(),
9749 drift_status: "IN_SYNC".to_string(),
9750 last_drift_check_time: String::new(),
9751 status_reason: String::new(),
9752 description: String::new(),
9753 detailed_status: String::new(),
9754 root_stack: String::new(),
9755 parent_stack: String::new(),
9756 termination_protection: false,
9757 iam_role: String::new(),
9758 tags: vec![],
9759 stack_policy: String::new(),
9760 rollback_monitoring_time: String::new(),
9761 rollback_alarms: vec![],
9762 notification_arns: vec![],
9763 })
9764 .collect();
9765
9766 app.handle_action(Action::FilterInput('2'));
9768 assert_eq!(app.page_input, "2");
9769
9770 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
9775 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
9777 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
9778
9779 let current_page = app.cfn_state.table.scroll_offset / page_size;
9781 assert_eq!(
9782 current_page, 1,
9783 "2p should go to page 2 (0-indexed as 1), not page 3"
9784 ); }
9786
9787 #[test]
9788 fn test_3p_goes_to_page_3_not_page_5() {
9789 let mut app = test_app();
9790 app.current_service = Service::CloudFormationStacks;
9791 app.service_selected = true;
9792 app.mode = Mode::Normal;
9793 app.cfn_state.table.items = (0..200)
9794 .map(|i| CfnStack {
9795 name: format!("stack-{}", i),
9796 stack_id: format!(
9797 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9798 i
9799 ),
9800 status: "CREATE_COMPLETE".to_string(),
9801 created_time: "2023-01-01T00:00:00Z".to_string(),
9802 updated_time: "2023-01-01T00:00:00Z".to_string(),
9803 deleted_time: String::new(),
9804 drift_status: "IN_SYNC".to_string(),
9805 last_drift_check_time: String::new(),
9806 status_reason: String::new(),
9807 description: String::new(),
9808 detailed_status: String::new(),
9809 root_stack: String::new(),
9810 parent_stack: String::new(),
9811 termination_protection: false,
9812 iam_role: String::new(),
9813 tags: vec![],
9814 stack_policy: String::new(),
9815 rollback_monitoring_time: String::new(),
9816 rollback_alarms: vec![],
9817 notification_arns: vec![],
9818 })
9819 .collect();
9820
9821 app.handle_action(Action::FilterInput('3'));
9823 app.handle_action(Action::OpenColumnSelector);
9824
9825 let page_size = app.cfn_state.table.page_size.value();
9826 let current_page = app.cfn_state.table.scroll_offset / page_size;
9827 assert_eq!(
9828 current_page, 2,
9829 "3p should go to page 3 (0-indexed as 2), not page 5"
9830 );
9831 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
9832 }
9833
9834 #[test]
9835 fn test_log_streams_page_navigation_uses_correct_page_size() {
9836 let mut app = test_app();
9837 app.current_service = Service::CloudWatchLogGroups;
9838 app.view_mode = ViewMode::Detail;
9839 app.service_selected = true;
9840 app.mode = Mode::Normal;
9841 app.log_groups_state.log_streams = (0..100)
9842 .map(|i| LogStream {
9843 name: format!("stream-{}", i),
9844 creation_time: None,
9845 last_event_time: None,
9846 })
9847 .collect();
9848
9849 app.handle_action(Action::FilterInput('2'));
9851 app.handle_action(Action::OpenColumnSelector);
9852
9853 assert_eq!(app.log_groups_state.stream_current_page, 1);
9855 assert_eq!(app.log_groups_state.selected_stream, 0);
9856
9857 assert_eq!(
9859 app.log_groups_state.stream_current_page, 1,
9860 "2p should go to page 2 (0-indexed as 1), not page 3"
9861 );
9862 }
9863
9864 #[test]
9865 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
9866 let mut app = test_app();
9867 app.current_service = Service::EcrRepositories;
9868 app.service_selected = true;
9869 app.mode = Mode::Normal;
9870 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
9872 .map(|i| EcrRepository {
9873 name: format!("repo{}", i),
9874 uri: format!("uri{}", i),
9875 created_at: "2023-01-01".to_string(),
9876 tag_immutability: "MUTABLE".to_string(),
9877 encryption_type: "AES256".to_string(),
9878 })
9879 .collect();
9880
9881 app.handle_action(Action::FilterInput('3'));
9883 app.handle_action(Action::OpenColumnSelector);
9884
9885 assert_eq!(app.ecr_state.repositories.selected, 50);
9887
9888 let page_size = app.ecr_state.repositories.page_size.value();
9889 let current_page = app.ecr_state.repositories.selected / page_size;
9890 assert_eq!(
9891 current_page, 2,
9892 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
9893 );
9894 }
9895
9896 #[test]
9897 fn test_page_navigation_updates_scroll_offset_for_alarms() {
9898 let mut app = test_app();
9899 app.current_service = Service::CloudWatchAlarms;
9900 app.service_selected = true;
9901 app.mode = Mode::Normal;
9902 app.alarms_state.table.items = (0..100)
9903 .map(|i| Alarm {
9904 name: format!("alarm-{}", i),
9905 state: "OK".to_string(),
9906 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
9907 description: String::new(),
9908 metric_name: "CPUUtilization".to_string(),
9909 namespace: "AWS/EC2".to_string(),
9910 statistic: "Average".to_string(),
9911 period: 300,
9912 comparison_operator: "GreaterThanThreshold".to_string(),
9913 threshold: 80.0,
9914 actions_enabled: true,
9915 state_reason: String::new(),
9916 resource: String::new(),
9917 dimensions: String::new(),
9918 expression: String::new(),
9919 alarm_type: "MetricAlarm".to_string(),
9920 cross_account: String::new(),
9921 ..Default::default()
9922 })
9923 .collect();
9924
9925 app.handle_action(Action::FilterInput('2'));
9927 app.handle_action(Action::OpenColumnSelector);
9928
9929 let page_size = app.alarms_state.table.page_size.value();
9931 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
9933 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
9934 }
9935
9936 #[test]
9937 fn test_ecr_pagination_with_65_repos() {
9938 let mut app = test_app();
9939 app.current_service = Service::EcrRepositories;
9940 app.service_selected = true;
9941 app.mode = Mode::Normal;
9942 app.ecr_state.repositories.items = (0..65)
9943 .map(|i| EcrRepository {
9944 name: format!("repo{:02}", i),
9945 uri: format!("uri{}", i),
9946 created_at: "2023-01-01".to_string(),
9947 tag_immutability: "MUTABLE".to_string(),
9948 encryption_type: "AES256".to_string(),
9949 })
9950 .collect();
9951
9952 assert_eq!(app.ecr_state.repositories.selected, 0);
9954 let page_size = 50;
9955 let current_page = app.ecr_state.repositories.selected / page_size;
9956 assert_eq!(current_page, 0);
9957
9958 app.handle_action(Action::FilterInput('2'));
9960 app.handle_action(Action::OpenColumnSelector);
9961 assert_eq!(app.ecr_state.repositories.selected, 50);
9962
9963 let current_page = app.ecr_state.repositories.selected / page_size;
9965 assert_eq!(current_page, 1);
9966 }
9967
9968 #[test]
9969 fn test_ecr_repos_input_focus_tab_cycling() {
9970 let mut app = test_app();
9971 app.current_service = Service::EcrRepositories;
9972 app.service_selected = true;
9973 app.mode = Mode::FilterInput;
9974 app.ecr_state.input_focus = InputFocus::Filter;
9975
9976 app.handle_action(Action::NextFilterFocus);
9978 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
9979
9980 app.handle_action(Action::NextFilterFocus);
9982 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
9983
9984 app.handle_action(Action::PrevFilterFocus);
9986 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
9987
9988 app.handle_action(Action::PrevFilterFocus);
9990 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
9991 }
9992
9993 #[test]
9994 fn test_ecr_images_column_toggle_not_off_by_one() {
9995 use crate::ecr::image::Column as ImageColumn;
9996 let mut app = test_app();
9997 app.current_service = Service::EcrRepositories;
9998 app.service_selected = true;
9999 app.mode = Mode::ColumnSelector;
10000 app.ecr_state.current_repository = Some("test-repo".to_string());
10001
10002 app.ecr_image_visible_column_ids = ImageColumn::ids();
10004 let initial_count = app.ecr_image_visible_column_ids.len();
10005
10006 app.column_selector_index = 0;
10008 app.handle_action(Action::ToggleColumn);
10009
10010 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
10012 assert!(!app
10013 .ecr_image_visible_column_ids
10014 .contains(&ImageColumn::Tag.id()));
10015
10016 app.handle_action(Action::ToggleColumn);
10018 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
10019 assert!(app
10020 .ecr_image_visible_column_ids
10021 .contains(&ImageColumn::Tag.id()));
10022 }
10023
10024 #[test]
10025 fn test_ecr_repos_column_toggle_works() {
10026 let mut app = test_app();
10027 app.current_service = Service::EcrRepositories;
10028 app.service_selected = true;
10029 app.mode = Mode::ColumnSelector;
10030 app.ecr_state.current_repository = None;
10031
10032 app.ecr_repo_visible_column_ids = EcrColumn::ids();
10034 let initial_count = app.ecr_repo_visible_column_ids.len();
10035
10036 app.column_selector_index = 1;
10038 app.handle_action(Action::ToggleColumn);
10039
10040 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
10042 assert!(!app
10043 .ecr_repo_visible_column_ids
10044 .contains(&EcrColumn::Name.id()));
10045
10046 app.handle_action(Action::ToggleColumn);
10048 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
10049 assert!(app
10050 .ecr_repo_visible_column_ids
10051 .contains(&EcrColumn::Name.id()));
10052 }
10053
10054 #[test]
10055 fn test_ecr_repos_pagination_left_right_navigation() {
10056 use crate::ecr::repo::Repository as EcrRepository;
10057 let mut app = test_app();
10058 app.current_service = Service::EcrRepositories;
10059 app.service_selected = true;
10060 app.mode = Mode::FilterInput;
10061 app.ecr_state.input_focus = InputFocus::Pagination;
10062
10063 app.ecr_state.repositories.items = (0..150)
10065 .map(|i| EcrRepository {
10066 name: format!("repo{:03}", i),
10067 uri: format!("uri{}", i),
10068 created_at: "2023-01-01".to_string(),
10069 tag_immutability: "MUTABLE".to_string(),
10070 encryption_type: "AES256".to_string(),
10071 })
10072 .collect();
10073
10074 app.ecr_state.repositories.selected = 0;
10076 eprintln!(
10077 "Initial: selected={}, focus={:?}, mode={:?}",
10078 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
10079 );
10080
10081 app.handle_action(Action::PageDown);
10083 eprintln!(
10084 "After PageDown: selected={}",
10085 app.ecr_state.repositories.selected
10086 );
10087 assert_eq!(app.ecr_state.repositories.selected, 50);
10088
10089 app.handle_action(Action::PageDown);
10091 eprintln!(
10092 "After 2nd PageDown: selected={}",
10093 app.ecr_state.repositories.selected
10094 );
10095 assert_eq!(app.ecr_state.repositories.selected, 100);
10096
10097 app.handle_action(Action::PageDown);
10099 eprintln!(
10100 "After 3rd PageDown: selected={}",
10101 app.ecr_state.repositories.selected
10102 );
10103 assert_eq!(app.ecr_state.repositories.selected, 100);
10104
10105 app.handle_action(Action::PageUp);
10107 eprintln!(
10108 "After PageUp: selected={}",
10109 app.ecr_state.repositories.selected
10110 );
10111 assert_eq!(app.ecr_state.repositories.selected, 50);
10112
10113 app.handle_action(Action::PageUp);
10115 eprintln!(
10116 "After 2nd PageUp: selected={}",
10117 app.ecr_state.repositories.selected
10118 );
10119 assert_eq!(app.ecr_state.repositories.selected, 0);
10120
10121 app.handle_action(Action::PageUp);
10123 eprintln!(
10124 "After 3rd PageUp: selected={}",
10125 app.ecr_state.repositories.selected
10126 );
10127 assert_eq!(app.ecr_state.repositories.selected, 0);
10128 }
10129
10130 #[test]
10131 fn test_ecr_repos_filter_input_when_input_focused() {
10132 use crate::ecr::repo::Repository as EcrRepository;
10133 let mut app = test_app();
10134 app.current_service = Service::EcrRepositories;
10135 app.service_selected = true;
10136 app.mode = Mode::FilterInput;
10137 app.ecr_state.input_focus = InputFocus::Filter;
10138
10139 app.ecr_state.repositories.items = vec![
10141 EcrRepository {
10142 name: "test-repo".to_string(),
10143 uri: "uri1".to_string(),
10144 created_at: "2023-01-01".to_string(),
10145 tag_immutability: "MUTABLE".to_string(),
10146 encryption_type: "AES256".to_string(),
10147 },
10148 EcrRepository {
10149 name: "prod-repo".to_string(),
10150 uri: "uri2".to_string(),
10151 created_at: "2023-01-01".to_string(),
10152 tag_immutability: "MUTABLE".to_string(),
10153 encryption_type: "AES256".to_string(),
10154 },
10155 ];
10156
10157 assert_eq!(app.ecr_state.repositories.filter, "");
10159 app.handle_action(Action::FilterInput('t'));
10160 assert_eq!(app.ecr_state.repositories.filter, "t");
10161 app.handle_action(Action::FilterInput('e'));
10162 assert_eq!(app.ecr_state.repositories.filter, "te");
10163 app.handle_action(Action::FilterInput('s'));
10164 assert_eq!(app.ecr_state.repositories.filter, "tes");
10165 app.handle_action(Action::FilterInput('t'));
10166 assert_eq!(app.ecr_state.repositories.filter, "test");
10167 }
10168
10169 #[test]
10170 fn test_ecr_repos_digit_input_when_pagination_focused() {
10171 use crate::ecr::repo::Repository as EcrRepository;
10172 let mut app = test_app();
10173 app.current_service = Service::EcrRepositories;
10174 app.service_selected = true;
10175 app.mode = Mode::FilterInput;
10176 app.ecr_state.input_focus = InputFocus::Pagination;
10177
10178 app.ecr_state.repositories.items = vec![EcrRepository {
10180 name: "test-repo".to_string(),
10181 uri: "uri1".to_string(),
10182 created_at: "2023-01-01".to_string(),
10183 tag_immutability: "MUTABLE".to_string(),
10184 encryption_type: "AES256".to_string(),
10185 }];
10186
10187 assert_eq!(app.ecr_state.repositories.filter, "");
10189 assert_eq!(app.page_input, "");
10190 app.handle_action(Action::FilterInput('2'));
10191 assert_eq!(app.ecr_state.repositories.filter, "");
10192 assert_eq!(app.page_input, "2");
10193
10194 app.handle_action(Action::FilterInput('a'));
10196 assert_eq!(app.ecr_state.repositories.filter, "");
10197 assert_eq!(app.page_input, "2");
10198 }
10199
10200 #[test]
10201 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
10202 use crate::ecr::repo::Repository as EcrRepository;
10203 let mut app = test_app();
10204 app.current_service = Service::EcrRepositories;
10205 app.service_selected = true;
10206 app.mode = Mode::FilterInput;
10207 app.ecr_state.input_focus = InputFocus::Filter;
10208
10209 app.ecr_state.repositories.items = (0..150)
10211 .map(|i| EcrRepository {
10212 name: format!("repo{:03}", i),
10213 uri: format!("uri{}", i),
10214 created_at: "2023-01-01".to_string(),
10215 tag_immutability: "MUTABLE".to_string(),
10216 encryption_type: "AES256".to_string(),
10217 })
10218 .collect();
10219
10220 app.ecr_state.repositories.selected = 0;
10222
10223 app.handle_action(Action::PageDown);
10225 assert_eq!(
10226 app.ecr_state.repositories.selected, 10,
10227 "Should scroll down by 10"
10228 );
10229
10230 app.handle_action(Action::PageUp);
10231 assert_eq!(
10232 app.ecr_state.repositories.selected, 0,
10233 "Should scroll back up"
10234 );
10235 }
10236
10237 #[test]
10238 fn test_ecr_repos_pagination_control_actually_works() {
10239 use crate::ecr::repo::Repository as EcrRepository;
10240
10241 let mut app = test_app();
10243 app.current_service = Service::EcrRepositories;
10244 app.service_selected = true;
10245 app.mode = Mode::FilterInput;
10246 app.ecr_state.current_repository = None;
10247 app.ecr_state.input_focus = InputFocus::Pagination;
10248
10249 app.ecr_state.repositories.items = (0..100)
10251 .map(|i| EcrRepository {
10252 name: format!("repo{:03}", i),
10253 uri: format!("uri{}", i),
10254 created_at: "2023-01-01".to_string(),
10255 tag_immutability: "MUTABLE".to_string(),
10256 encryption_type: "AES256".to_string(),
10257 })
10258 .collect();
10259
10260 app.ecr_state.repositories.selected = 0;
10261
10262 assert_eq!(app.mode, Mode::FilterInput);
10264 assert_eq!(app.current_service, Service::EcrRepositories);
10265 assert_eq!(app.ecr_state.current_repository, None);
10266 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10267
10268 app.handle_action(Action::PageDown);
10270 assert_eq!(
10271 app.ecr_state.repositories.selected, 50,
10272 "PageDown should move to page 2"
10273 );
10274
10275 app.handle_action(Action::PageUp);
10276 assert_eq!(
10277 app.ecr_state.repositories.selected, 0,
10278 "PageUp should move back to page 1"
10279 );
10280 }
10281
10282 #[test]
10283 fn test_ecr_repos_can_reach_last_page_when_not_multiple_of_page_size() {
10284 use crate::ecr::repo::Repository as EcrRepository;
10285 let mut app = test_app();
10286 app.current_service = Service::EcrRepositories;
10287 app.service_selected = true;
10288 app.mode = Mode::FilterInput;
10289 app.ecr_state.input_focus = InputFocus::Pagination;
10290
10291 app.ecr_state.repositories.items = (0..754)
10293 .map(|i| EcrRepository {
10294 name: format!("repo{:03}", i),
10295 uri: format!("uri{}", i),
10296 created_at: "2023-01-01".to_string(),
10297 tag_immutability: "MUTABLE".to_string(),
10298 encryption_type: "AES256".to_string(),
10299 })
10300 .collect();
10301
10302 app.ecr_state.repositories.selected = 700;
10304
10305 app.handle_action(Action::PageDown);
10307 assert_eq!(
10308 app.ecr_state.repositories.selected, 750,
10309 "Should reach last page (750) but got {}",
10310 app.ecr_state.repositories.selected
10311 );
10312
10313 app.handle_action(Action::PageDown);
10315 assert_eq!(app.ecr_state.repositories.selected, 750);
10316 }
10317
10318 #[test]
10319 fn test_ecr_repos_start_filter_resets_focus_to_input() {
10320 let mut app = test_app();
10321 app.current_service = Service::EcrRepositories;
10322 app.service_selected = true;
10323 app.mode = Mode::Normal;
10324 app.ecr_state.current_repository = None;
10325
10326 app.ecr_state.input_focus = InputFocus::Pagination;
10328
10329 app.handle_action(Action::StartFilter);
10331
10332 assert_eq!(app.mode, Mode::FilterInput);
10334 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10335 }
10336
10337 #[test]
10338 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
10339 use crate::ecr::repo::Repository as EcrRepository;
10340
10341 let mut app = test_app();
10342 app.current_service = Service::EcrRepositories;
10343 app.service_selected = true;
10344 app.mode = Mode::Normal;
10345 app.ecr_state.current_repository = None;
10346
10347 app.ecr_state.repositories.items = (0..100)
10349 .map(|i| EcrRepository {
10350 name: format!("repo{:03}", i),
10351 uri: format!("uri{}", i),
10352 created_at: "2023-01-01".to_string(),
10353 tag_immutability: "MUTABLE".to_string(),
10354 encryption_type: "AES256".to_string(),
10355 })
10356 .collect();
10357
10358 app.ecr_state.repositories.selected = 0;
10359
10360 app.handle_action(Action::StartFilter);
10362 assert_eq!(app.mode, Mode::FilterInput);
10363 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10364
10365 app.handle_action(Action::NextFilterFocus);
10367 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10368
10369 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
10371 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
10372 app.handle_action(Action::PageDown);
10373 eprintln!(
10374 "After PageDown: selected={}",
10375 app.ecr_state.repositories.selected
10376 );
10377
10378 assert_eq!(
10380 app.ecr_state.repositories.selected, 50,
10381 "Right arrow should move to page 2"
10382 );
10383
10384 app.handle_action(Action::PageUp);
10386 assert_eq!(
10387 app.ecr_state.repositories.selected, 0,
10388 "Left arrow should move back to page 1"
10389 );
10390 }
10391
10392 #[test]
10393 fn test_apig_filter_input() {
10394 let mut app = test_app();
10395 app.current_service = Service::ApiGatewayApis;
10396 app.service_selected = true;
10397 app.mode = Mode::FilterInput;
10398
10399 app.handle_action(Action::FilterInput('t'));
10400 app.handle_action(Action::FilterInput('e'));
10401 app.handle_action(Action::FilterInput('s'));
10402 app.handle_action(Action::FilterInput('t'));
10403 assert_eq!(app.apig_state.apis.filter, "test");
10404
10405 app.handle_action(Action::FilterBackspace);
10406 assert_eq!(app.apig_state.apis.filter, "tes");
10407 }
10408
10409 #[test]
10410 fn test_apig_start_filter_enters_filter_mode() {
10411 let mut app = test_app();
10412 app.current_service = Service::ApiGatewayApis;
10413 app.service_selected = true;
10414 app.mode = Mode::Normal;
10415
10416 app.handle_action(Action::StartFilter);
10417 assert_eq!(app.mode, Mode::FilterInput);
10418 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10419 }
10420
10421 #[test]
10422 fn test_apig_input_focus_cycles_with_tab() {
10423 let mut app = test_app();
10424 app.current_service = Service::ApiGatewayApis;
10425 app.service_selected = true;
10426 app.mode = Mode::FilterInput;
10427 app.apig_state.input_focus = InputFocus::Filter;
10428
10429 app.handle_action(Action::NextFilterFocus);
10431 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10432
10433 app.handle_action(Action::NextFilterFocus);
10435 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10436 }
10437
10438 #[test]
10439 fn test_apig_input_focus_cycles_with_shift_tab() {
10440 let mut app = test_app();
10441 app.current_service = Service::ApiGatewayApis;
10442 app.service_selected = true;
10443 app.mode = Mode::FilterInput;
10444 app.apig_state.input_focus = InputFocus::Filter;
10445
10446 app.handle_action(Action::PrevFilterFocus);
10448 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10449
10450 app.handle_action(Action::PrevFilterFocus);
10452 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10453 }
10454
10455 #[test]
10456 fn test_apig_exact_user_flow_i_tab_filter() {
10457 let mut app = test_app();
10458 app.current_service = Service::ApiGatewayApis;
10459 app.service_selected = true;
10460 app.mode = Mode::Normal;
10461
10462 app.apig_state.apis.items = vec![
10464 crate::apig::api::RestApi {
10465 id: "api1".to_string(),
10466 name: "test-api".to_string(),
10467 description: "Test API".to_string(),
10468 created_date: "2023-01-01".to_string(),
10469 api_key_source: "HEADER".to_string(),
10470 endpoint_configuration: "REGIONAL".to_string(),
10471 protocol_type: "REST".to_string(),
10472 disable_execute_api_endpoint: false,
10473 status: "AVAILABLE".to_string(),
10474 },
10475 crate::apig::api::RestApi {
10476 id: "api2".to_string(),
10477 name: "prod-api".to_string(),
10478 description: "Production API".to_string(),
10479 created_date: "2023-01-02".to_string(),
10480 api_key_source: "HEADER".to_string(),
10481 endpoint_configuration: "REGIONAL".to_string(),
10482 protocol_type: "REST".to_string(),
10483 disable_execute_api_endpoint: false,
10484 status: "AVAILABLE".to_string(),
10485 },
10486 ];
10487
10488 app.handle_action(Action::StartFilter);
10490 assert_eq!(app.mode, Mode::FilterInput);
10491 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10492
10493 app.handle_action(Action::FilterInput('t'));
10495 app.handle_action(Action::FilterInput('e'));
10496 app.handle_action(Action::FilterInput('s'));
10497 app.handle_action(Action::FilterInput('t'));
10498 assert_eq!(app.apig_state.apis.filter, "test");
10499
10500 app.handle_action(Action::NextFilterFocus);
10502 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10503
10504 app.handle_action(Action::NextFilterFocus);
10506 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10507 }
10508
10509 #[test]
10510 fn test_apig_row_expansion() {
10511 let mut app = test_app();
10512 app.current_service = Service::ApiGatewayApis;
10513 app.service_selected = true;
10514 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
10515 id: "api1".to_string(),
10516 name: "test-api".to_string(),
10517 description: "Test API".to_string(),
10518 created_date: "2023-01-01".to_string(),
10519 api_key_source: "HEADER".to_string(),
10520 endpoint_configuration: "REGIONAL".to_string(),
10521 protocol_type: "REST".to_string(),
10522 disable_execute_api_endpoint: false,
10523 status: "AVAILABLE".to_string(),
10524 }];
10525 app.apig_state.apis.selected = 0;
10526
10527 assert_eq!(app.apig_state.apis.expanded_item, None);
10528
10529 app.handle_action(Action::NextPane);
10531 assert_eq!(app.apig_state.apis.expanded_item, None);
10532
10533 app.handle_action(Action::ExpandRow);
10535 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
10536
10537 app.handle_action(Action::NextPane);
10539 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
10540
10541 app.handle_action(Action::PrevPane);
10543 assert_eq!(app.apig_state.apis.expanded_item, None);
10544 }
10545
10546 #[test]
10547 fn test_apig_column_preferences() {
10548 let mut app = test_app();
10549 app.current_service = Service::ApiGatewayApis;
10550 app.service_selected = true;
10551
10552 let initial_count = app.apig_api_visible_column_ids.len();
10554 assert_eq!(initial_count, app.apig_api_column_ids.len());
10555
10556 app.handle_action(Action::OpenColumnSelector);
10558 assert_eq!(app.mode, Mode::ColumnSelector);
10559
10560 app.column_selector_index = 1;
10562 let first_col = app.apig_api_column_ids[0];
10563 assert!(app.apig_api_visible_column_ids.contains(&first_col));
10564
10565 app.handle_action(Action::ToggleColumn);
10566
10567 assert!(!app.apig_api_visible_column_ids.contains(&first_col));
10569 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
10570
10571 app.handle_action(Action::ToggleColumn);
10573 assert!(app.apig_api_visible_column_ids.contains(&first_col));
10574 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
10575 }
10576
10577 #[test]
10578 fn test_apig_page_size_preferences() {
10579 use crate::common::PageSize;
10580
10581 let mut app = test_app();
10582 app.current_service = Service::ApiGatewayApis;
10583 app.service_selected = true;
10584
10585 assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
10587
10588 app.handle_action(Action::OpenColumnSelector);
10590
10591 app.column_selector_index = app.apig_api_column_ids.len() + 3;
10596 app.handle_action(Action::ToggleColumn);
10597 assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
10598
10599 app.column_selector_index = app.apig_api_column_ids.len() + 6;
10601 app.handle_action(Action::ToggleColumn);
10602 assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
10603 }
10604
10605 #[test]
10606 fn test_apig_preferences_skip_blank_row() {
10607 let mut app = test_app();
10608 app.current_service = Service::ApiGatewayApis;
10609 app.service_selected = true;
10610 app.mode = Mode::ColumnSelector;
10611
10612 app.column_selector_index = 8;
10614
10615 app.handle_action(Action::NextItem);
10617 assert_eq!(app.column_selector_index, 10);
10618
10619 app.handle_action(Action::PrevItem);
10621 assert_eq!(app.column_selector_index, 8);
10622 }
10623
10624 #[test]
10625 fn test_apig_preferences_ctrl_d_skip_blank_row() {
10626 let mut app = test_app();
10627 app.current_service = Service::ApiGatewayApis;
10628 app.service_selected = true;
10629 app.mode = Mode::ColumnSelector;
10630
10631 app.column_selector_index = 0;
10633
10634 app.handle_action(Action::PageDown);
10636 assert_eq!(app.column_selector_index, 10);
10637
10638 app.handle_action(Action::PageUp);
10640 assert_eq!(app.column_selector_index, 0);
10641 }
10642
10643 #[test]
10644 fn test_apig_preferences_ctrl_u_skip_blank_row() {
10645 let mut app = test_app();
10646 app.current_service = Service::ApiGatewayApis;
10647 app.service_selected = true;
10648 app.mode = Mode::ColumnSelector;
10649
10650 app.column_selector_index = 10;
10652
10653 app.handle_action(Action::PageUp);
10655 assert_eq!(app.column_selector_index, 0);
10656
10657 app.column_selector_index = 14; app.handle_action(Action::PageUp);
10662 assert_eq!(app.column_selector_index, 4);
10663 }
10664
10665 #[test]
10666 fn test_apig_preferences_tab_cycles_sections() {
10667 let mut app = test_app();
10668 app.current_service = Service::ApiGatewayApis;
10669 app.service_selected = true;
10670 app.mode = Mode::ColumnSelector;
10671
10672 app.column_selector_index = 0;
10674
10675 app.handle_action(Action::NextPreferences);
10677 assert_eq!(app.column_selector_index, 10);
10678
10679 app.handle_action(Action::NextPreferences);
10681 assert_eq!(app.column_selector_index, 0);
10682 }
10683
10684 #[test]
10685 fn test_apig_preferences_shift_tab_cycles_sections() {
10686 let mut app = test_app();
10687 app.current_service = Service::ApiGatewayApis;
10688 app.service_selected = true;
10689 app.mode = Mode::ColumnSelector;
10690
10691 app.column_selector_index = 0;
10693
10694 app.handle_action(Action::PrevPreferences);
10696 assert_eq!(app.column_selector_index, 10);
10697
10698 app.handle_action(Action::PrevPreferences);
10700 assert_eq!(app.column_selector_index, 0);
10701 }
10702
10703 #[test]
10704 fn test_apig_arrow_navigation() {
10705 let mut app = test_app();
10706 app.current_service = Service::ApiGatewayApis;
10707 app.service_selected = true;
10708 app.mode = Mode::Normal; app.apig_state.apis.filter = String::new(); app.apig_state.apis.items = vec![
10711 crate::apig::api::RestApi {
10712 id: "api1".to_string(),
10713 name: "test-api-1".to_string(),
10714 description: "Test API 1".to_string(),
10715 created_date: "2023-01-01".to_string(),
10716 api_key_source: "HEADER".to_string(),
10717 endpoint_configuration: "REGIONAL".to_string(),
10718 protocol_type: "REST".to_string(),
10719 disable_execute_api_endpoint: false,
10720 status: "AVAILABLE".to_string(),
10721 },
10722 crate::apig::api::RestApi {
10723 id: "api2".to_string(),
10724 name: "test-api-2".to_string(),
10725 description: "Test API 2".to_string(),
10726 created_date: "2023-01-02".to_string(),
10727 api_key_source: "HEADER".to_string(),
10728 endpoint_configuration: "REGIONAL".to_string(),
10729 protocol_type: "HTTP".to_string(),
10730 disable_execute_api_endpoint: false,
10731 status: "AVAILABLE".to_string(),
10732 },
10733 ];
10734 app.apig_state.apis.selected = 0;
10735
10736 app.handle_action(Action::NextItem);
10738 assert_eq!(app.apig_state.apis.selected, 1);
10739
10740 app.handle_action(Action::PrevItem);
10742 assert_eq!(app.apig_state.apis.selected, 0);
10743 }
10744
10745 #[test]
10746 fn test_apig_collapse_row() {
10747 let mut app = test_app();
10748 app.current_service = Service::ApiGatewayApis;
10749 app.service_selected = true;
10750 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
10751 id: "api1".to_string(),
10752 name: "test-api".to_string(),
10753 description: "Test API".to_string(),
10754 created_date: "2023-01-01".to_string(),
10755 api_key_source: "HEADER".to_string(),
10756 endpoint_configuration: "REGIONAL".to_string(),
10757 protocol_type: "REST".to_string(),
10758 disable_execute_api_endpoint: false,
10759 status: "AVAILABLE".to_string(),
10760 }];
10761 app.apig_state.apis.selected = 0;
10762 app.apig_state.apis.expanded_item = Some(0);
10763
10764 app.handle_action(Action::CollapseRow);
10766 assert_eq!(app.apig_state.apis.expanded_item, None);
10767 }
10768
10769 #[test]
10770 fn test_apig_filter_resets_selection() {
10771 let mut app = test_app();
10772 app.current_service = Service::ApiGatewayApis;
10773 app.service_selected = true;
10774 app.mode = Mode::FilterInput;
10775 app.apig_state.input_focus = InputFocus::Filter;
10776 app.apig_state.apis.items = vec![
10777 crate::apig::api::RestApi {
10778 id: "api1".to_string(),
10779 name: "alpha-api".to_string(),
10780 description: "Alpha API".to_string(),
10781 created_date: "2023-01-01".to_string(),
10782 api_key_source: "HEADER".to_string(),
10783 endpoint_configuration: "REGIONAL".to_string(),
10784 protocol_type: "REST".to_string(),
10785 disable_execute_api_endpoint: false,
10786 status: "AVAILABLE".to_string(),
10787 },
10788 crate::apig::api::RestApi {
10789 id: "api2".to_string(),
10790 name: "beta-api".to_string(),
10791 description: "Beta API".to_string(),
10792 created_date: "2023-01-02".to_string(),
10793 api_key_source: "HEADER".to_string(),
10794 endpoint_configuration: "REGIONAL".to_string(),
10795 protocol_type: "HTTP".to_string(),
10796 disable_execute_api_endpoint: false,
10797 status: "AVAILABLE".to_string(),
10798 },
10799 ];
10800
10801 app.apig_state.apis.selected = 1;
10803 app.apig_state.apis.expanded_item = Some(1);
10804
10805 app.handle_action(Action::FilterInput('a'));
10807
10808 assert_eq!(app.apig_state.apis.selected, 0);
10810 assert_eq!(app.apig_state.apis.expanded_item, None);
10811 assert_eq!(app.apig_state.apis.filter, "a");
10812 }
10813
10814 #[test]
10815 fn test_service_picker_starts_in_normal_mode() {
10816 let app = test_app();
10817 assert_eq!(app.mode, Mode::ServicePicker);
10818 assert!(!app.service_picker.filter_active);
10819 }
10820
10821 #[test]
10822 fn test_service_picker_i_key_activates_filter() {
10823 let mut app = test_app();
10824
10825 assert_eq!(app.mode, Mode::ServicePicker);
10827 assert!(!app.service_picker.filter_active);
10828 assert!(app.service_picker.filter.is_empty());
10829
10830 app.handle_action(Action::EnterFilterMode);
10832
10833 assert_eq!(app.mode, Mode::ServicePicker);
10835 assert!(app.service_picker.filter_active);
10836 assert!(app.service_picker.filter.is_empty());
10837
10838 app.handle_action(Action::FilterInput('s'));
10840 assert_eq!(app.service_picker.filter, "s");
10841 }
10842
10843 #[test]
10844 fn test_service_picker_esc_exits_filter_mode() {
10845 let mut app = test_app();
10846 assert_eq!(app.mode, Mode::ServicePicker);
10847 assert!(!app.service_picker.filter_active);
10848
10849 app.handle_action(Action::EnterFilterMode);
10851 assert!(app.service_picker.filter_active);
10852 assert_eq!(app.mode, Mode::ServicePicker);
10853
10854 app.handle_action(Action::FilterInput('s'));
10856 assert_eq!(app.service_picker.filter, "s");
10857
10858 app.handle_action(Action::ExitFilterMode);
10860 assert!(!app.service_picker.filter_active);
10861 assert_eq!(app.mode, Mode::ServicePicker); app.handle_action(Action::ExitFilterMode);
10865 assert_eq!(app.mode, Mode::Normal);
10866 }
10867
10868 #[test]
10869 fn test_service_picker_navigation_only_works_in_normal_mode() {
10870 let mut app = test_app();
10871 app.service_picker.selected = 0;
10872
10873 assert!(!app.service_picker.filter_active);
10875 app.handle_action(Action::NextItem);
10876 assert_eq!(app.service_picker.selected, 1);
10877
10878 app.handle_action(Action::EnterFilterMode);
10880 assert!(app.service_picker.filter_active);
10881
10882 let prev_selected = app.service_picker.selected;
10884 app.handle_action(Action::NextItem);
10885 assert_eq!(app.service_picker.selected, prev_selected); app.handle_action(Action::ExitFilterMode);
10889 assert!(!app.service_picker.filter_active);
10890
10891 app.handle_action(Action::NextItem);
10893 assert_eq!(app.service_picker.selected, prev_selected + 1);
10894 }
10895
10896 #[test]
10897 fn test_service_picker_typing_filters_services() {
10898 let mut app = test_app();
10899
10900 assert_eq!(app.mode, Mode::ServicePicker);
10902 assert!(!app.service_picker.filter_active);
10903
10904 app.handle_action(Action::EnterFilterMode);
10906 assert!(app.service_picker.filter_active);
10907
10908 app.handle_action(Action::FilterInput('s'));
10910 app.handle_action(Action::FilterInput('3'));
10911
10912 assert_eq!(app.service_picker.filter, "s3");
10913 assert_eq!(app.mode, Mode::ServicePicker);
10914 }
10915
10916 #[test]
10917 fn test_service_picker_enter_exits_insert_mode() {
10918 let mut app = test_app();
10919 assert_eq!(app.mode, Mode::ServicePicker);
10920 assert!(!app.service_selected);
10921
10922 app.handle_action(Action::EnterFilterMode);
10924 assert!(app.service_picker.filter_active);
10925
10926 app.handle_action(Action::FilterInput('s'));
10928 app.handle_action(Action::FilterInput('3'));
10929
10930 app.handle_action(Action::Select);
10932 assert!(!app.service_selected, "Should not select service");
10933 assert!(!app.service_picker.filter_active, "Should exit INSERT mode");
10934 assert_eq!(app.mode, Mode::ServicePicker);
10935
10936 app.handle_action(Action::Select);
10938 assert!(app.service_selected, "Should select service in NORMAL mode");
10939 assert_eq!(app.mode, Mode::Normal);
10940 }
10941
10942 #[test]
10943 fn test_service_picker_resets_on_open() {
10944 let mut app = test_app();
10945
10946 app.service_selected = true;
10948 app.mode = Mode::Normal;
10949
10950 app.service_picker.filter = "previous".to_string();
10952 app.service_picker.filter_active = true;
10953 app.service_picker.selected = 5;
10954
10955 app.handle_action(Action::OpenSpaceMenu);
10957
10958 assert_eq!(app.mode, Mode::SpaceMenu);
10960 assert!(app.service_picker.filter.is_empty());
10961 assert!(!app.service_picker.filter_active);
10962 assert_eq!(app.service_picker.selected, 0);
10963 }
10964
10965 #[test]
10966 fn test_kms_in_service_picker_services_list() {
10967 let app = test_app();
10968 assert!(
10969 app.service_picker.services.contains(&"KMS › Managed Keys"),
10970 "KMS › Managed Keys must be in the service picker list"
10971 );
10972 }
10973
10974 #[test]
10975 fn test_kms_service_picker_selects_kms_service() {
10976 let mut app = test_app();
10977 app.mode = Mode::ServicePicker;
10978 app.service_picker.filter_active = true;
10979
10980 for c in ['k', 'm', 's'] {
10982 app.handle_action(Action::FilterInput(c));
10983 }
10984
10985 assert_eq!(app.service_picker.filter, "kms");
10986
10987 let filtered: Vec<_> = app
10989 .service_picker
10990 .services
10991 .iter()
10992 .filter(|s| s.to_lowercase().contains("kms"))
10993 .collect();
10994 assert_eq!(filtered.len(), 1);
10995 assert_eq!(*filtered[0], "KMS › Managed Keys");
10996 }
10997
10998 #[test]
10999 fn test_selecting_kms_in_picker_switches_service() {
11000 let mut app = test_app();
11001 app.mode = Mode::ServicePicker;
11002 app.service_selected = false;
11003
11004 let kms_idx = app
11007 .filtered_services()
11008 .iter()
11009 .position(|&s| s == "KMS › Managed Keys")
11010 .expect("KMS › Managed Keys must be in services list");
11011 app.service_picker.selected = kms_idx;
11012
11013 app.handle_action(Action::Select);
11014
11015 assert_eq!(app.current_service, Service::KmsKeys);
11016 assert!(app.service_selected);
11017 }
11018
11019 #[test]
11020 fn test_kms_picker_creates_session_tab_with_correct_title() {
11021 let mut app = test_app();
11023 app.mode = Mode::ServicePicker;
11024 app.service_selected = false;
11025
11026 let kms_idx = app
11027 .filtered_services()
11028 .iter()
11029 .position(|&s| s == "KMS › Managed Keys")
11030 .unwrap();
11031 app.service_picker.selected = kms_idx;
11032 app.handle_action(Action::Select);
11033
11034 assert!(!app.tabs.is_empty(), "a session tab must be created");
11035 let tab = &app.tabs[app.current_tab];
11036 assert_eq!(
11037 tab.title, "KMS › Managed Keys",
11038 "session tab title must match picker label"
11039 );
11040 assert_eq!(tab.service, Service::KmsKeys);
11041 }
11042
11043 #[test]
11044 fn test_kms_tab_switch_does_not_trigger_reload() {
11045 use crate::ui::kms::Tab as KmsTab;
11048 let mut app = test_app();
11049 app.current_service = Service::KmsKeys;
11050 app.service_selected = true;
11051 app.mode = Mode::Normal;
11052 app.kms_state.tab = KmsTab::AwsManaged;
11053 app.kms_state.keys.loading = false;
11054
11055 app.handle_action(Action::NextDetailTab);
11056
11057 assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
11058 assert!(
11059 !app.kms_state.keys.loading,
11060 "tab switch must NOT trigger a reload — keys are filtered client-side"
11061 );
11062 }
11063
11064 #[test]
11065 fn test_kms_tab_switching_cycles() {
11066 use crate::ui::kms::Tab as KmsTab;
11067 let mut app = test_app();
11068 app.current_service = Service::KmsKeys;
11069 app.service_selected = true;
11070 app.mode = Mode::Normal;
11071
11072 assert_eq!(app.kms_state.tab, KmsTab::AwsManaged);
11073
11074 app.handle_action(Action::NextDetailTab);
11075 assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
11076
11077 app.handle_action(Action::NextDetailTab);
11078 assert_eq!(app.kms_state.tab, KmsTab::AwsManaged, "must wrap around");
11079
11080 app.handle_action(Action::PrevDetailTab);
11081 assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
11082 }
11083
11084 #[test]
11085 fn test_kms_load_condition_triggers_on_loading_flag() {
11086 let mut app = test_app();
11090 app.current_service = Service::KmsKeys;
11091 app.service_selected = true;
11092 app.kms_state.keys.loading = false;
11093
11094 app.kms_state.keys.loading = true;
11096
11097 assert!(
11100 app.kms_state.keys.loading,
11101 "loading flag must be true after region change to trigger reload"
11102 );
11103 }
11104
11105 #[test]
11106 fn test_kms_session_tab_survives_service_switch() {
11107 let mut app = test_app();
11110 app.mode = Mode::ServicePicker;
11111 app.service_selected = false;
11112
11113 let kms_idx = app
11114 .filtered_services()
11115 .iter()
11116 .position(|&s| s == "KMS › Managed Keys")
11117 .unwrap();
11118 app.service_picker.selected = kms_idx;
11119 app.handle_action(Action::Select);
11120
11121 assert_eq!(app.tabs.len(), 1);
11122 assert_eq!(app.tabs[0].title, "KMS › Managed Keys");
11123 assert_eq!(app.tabs[0].service, Service::KmsKeys);
11124 assert_eq!(app.current_tab, 0);
11125 }
11126
11127 #[test]
11128 fn test_no_pii_in_test_data() {
11129 let test_repo = EcrRepository {
11131 name: "test-repo".to_string(),
11132 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
11133 created_at: "2024-01-01".to_string(),
11134 tag_immutability: "MUTABLE".to_string(),
11135 encryption_type: "AES256".to_string(),
11136 };
11137
11138 assert!(test_repo.uri.starts_with("123456789012"));
11140 assert!(!test_repo.uri.contains("123456789013")); }
11142
11143 #[test]
11144 fn test_lambda_versions_tab_triggers_loading() {
11145 let mut app = test_app();
11146 app.current_service = Service::LambdaFunctions;
11147 app.service_selected = true;
11148
11149 app.lambda_state.current_function = Some("test-function".to_string());
11151 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11152
11153 assert!(app.lambda_state.version_table.items.is_empty());
11155
11156 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11158
11159 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11162 assert!(app.lambda_state.current_function.is_some());
11163 }
11164
11165 #[test]
11166 fn test_lambda_versions_navigation() {
11167 let mut app = test_app();
11168 app.current_service = Service::LambdaFunctions;
11169 app.service_selected = true;
11170 app.lambda_state.current_function = Some("test-function".to_string());
11171 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11172
11173 app.lambda_state.version_table.items = vec![
11175 LambdaVersion {
11176 version: "3".to_string(),
11177 aliases: "prod".to_string(),
11178 description: "".to_string(),
11179 last_modified: "".to_string(),
11180 architecture: "X86_64".to_string(),
11181 },
11182 LambdaVersion {
11183 version: "2".to_string(),
11184 aliases: "".to_string(),
11185 description: "".to_string(),
11186 last_modified: "".to_string(),
11187 architecture: "X86_64".to_string(),
11188 },
11189 LambdaVersion {
11190 version: "1".to_string(),
11191 aliases: "".to_string(),
11192 description: "".to_string(),
11193 last_modified: "".to_string(),
11194 architecture: "X86_64".to_string(),
11195 },
11196 ];
11197
11198 assert_eq!(app.lambda_state.version_table.items.len(), 3);
11200 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
11201 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
11202
11203 app.lambda_state.version_table.selected = 1;
11205 assert_eq!(app.lambda_state.version_table.selected, 1);
11206 }
11207
11208 #[test]
11209 fn test_lambda_versions_with_aliases() {
11210 let version = LambdaVersion {
11211 version: "35".to_string(),
11212 aliases: "prod, staging".to_string(),
11213 description: "Production version".to_string(),
11214 last_modified: "2024-01-01".to_string(),
11215 architecture: "X86_64".to_string(),
11216 };
11217
11218 assert_eq!(version.aliases, "prod, staging");
11219 assert!(!version.aliases.is_empty());
11220 }
11221
11222 #[test]
11223 fn test_lambda_versions_expansion() {
11224 let mut app = test_app();
11225 app.current_service = Service::LambdaFunctions;
11226 app.service_selected = true;
11227 app.lambda_state.current_function = Some("test-function".to_string());
11228 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11229
11230 app.lambda_state.version_table.items = vec![
11232 LambdaVersion {
11233 version: "2".to_string(),
11234 aliases: "prod".to_string(),
11235 description: "Production".to_string(),
11236 last_modified: "2024-01-01".to_string(),
11237 architecture: "X86_64".to_string(),
11238 },
11239 LambdaVersion {
11240 version: "1".to_string(),
11241 aliases: "".to_string(),
11242 description: "".to_string(),
11243 last_modified: "2024-01-01".to_string(),
11244 architecture: "Arm64".to_string(),
11245 },
11246 ];
11247
11248 app.lambda_state.version_table.selected = 0;
11249
11250 app.lambda_state.version_table.expanded_item = Some(0);
11252 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11253
11254 app.lambda_state.version_table.selected = 1;
11256 app.lambda_state.version_table.expanded_item = Some(1);
11257 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
11258 }
11259
11260 #[test]
11261 fn test_lambda_versions_page_navigation() {
11262 let mut app = test_app();
11263 app.current_service = Service::LambdaFunctions;
11264 app.service_selected = true;
11265 app.lambda_state.current_function = Some("test-function".to_string());
11266 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11267
11268 app.lambda_state.version_table.items = (1..=30)
11270 .map(|i| LambdaVersion {
11271 version: i.to_string(),
11272 aliases: "".to_string(),
11273 description: "".to_string(),
11274 last_modified: "".to_string(),
11275 architecture: "X86_64".to_string(),
11276 })
11277 .collect();
11278
11279 app.lambda_state.version_table.page_size = PageSize::Ten;
11280 app.lambda_state.version_table.selected = 0;
11281
11282 app.page_input = "2".to_string();
11284 app.handle_action(Action::OpenColumnSelector);
11285
11286 assert_eq!(app.lambda_state.version_table.selected, 10);
11288 }
11289
11290 #[test]
11291 fn test_lambda_versions_pagination_arrow_keys() {
11292 let mut app = test_app();
11293 app.current_service = Service::LambdaFunctions;
11294 app.service_selected = true;
11295 app.lambda_state.current_function = Some("test-function".to_string());
11296 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11297 app.mode = Mode::FilterInput;
11298 app.lambda_state.version_input_focus = InputFocus::Pagination;
11299
11300 app.lambda_state.version_table.items = (1..=30)
11302 .map(|i| LambdaVersion {
11303 version: i.to_string(),
11304 aliases: "".to_string(),
11305 description: "".to_string(),
11306 last_modified: "".to_string(),
11307 architecture: "X86_64".to_string(),
11308 })
11309 .collect();
11310
11311 app.lambda_state.version_table.page_size = PageSize::Ten;
11312 app.lambda_state.version_table.selected = 0;
11313
11314 app.handle_action(Action::PageDown);
11316 assert_eq!(app.lambda_state.version_table.selected, 10);
11317
11318 app.handle_action(Action::PageUp);
11320 assert_eq!(app.lambda_state.version_table.selected, 0);
11321 }
11322
11323 #[test]
11324 fn test_lambda_versions_page_input_in_filter_mode() {
11325 let mut app = test_app();
11326 app.current_service = Service::LambdaFunctions;
11327 app.service_selected = true;
11328 app.lambda_state.current_function = Some("test-function".to_string());
11329 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11330 app.mode = Mode::FilterInput;
11331 app.lambda_state.version_input_focus = InputFocus::Pagination;
11332
11333 app.lambda_state.version_table.items = (1..=30)
11335 .map(|i| LambdaVersion {
11336 version: i.to_string(),
11337 aliases: "".to_string(),
11338 description: "".to_string(),
11339 last_modified: "".to_string(),
11340 architecture: "X86_64".to_string(),
11341 })
11342 .collect();
11343
11344 app.lambda_state.version_table.page_size = PageSize::Ten;
11345 app.lambda_state.version_table.selected = 0;
11346
11347 app.handle_action(Action::FilterInput('2'));
11349 assert_eq!(app.page_input, "2");
11350 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
11354 assert_eq!(app.lambda_state.version_table.selected, 10);
11355 assert_eq!(app.page_input, ""); }
11357
11358 #[test]
11359 fn test_lambda_versions_filter_input() {
11360 let mut app = test_app();
11361 app.current_service = Service::LambdaFunctions;
11362 app.service_selected = true;
11363 app.lambda_state.current_function = Some("test-function".to_string());
11364 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11365 app.mode = Mode::FilterInput;
11366 app.lambda_state.version_input_focus = InputFocus::Filter;
11367
11368 app.lambda_state.version_table.items = vec![
11370 LambdaVersion {
11371 version: "1".to_string(),
11372 aliases: "prod".to_string(),
11373 description: "Production".to_string(),
11374 last_modified: "".to_string(),
11375 architecture: "X86_64".to_string(),
11376 },
11377 LambdaVersion {
11378 version: "2".to_string(),
11379 aliases: "staging".to_string(),
11380 description: "Staging".to_string(),
11381 last_modified: "".to_string(),
11382 architecture: "X86_64".to_string(),
11383 },
11384 ];
11385
11386 app.handle_action(Action::FilterInput('p'));
11388 app.handle_action(Action::FilterInput('r'));
11389 app.handle_action(Action::FilterInput('o'));
11390 app.handle_action(Action::FilterInput('d'));
11391 assert_eq!(app.lambda_state.version_table.filter, "prod");
11392
11393 app.handle_action(Action::FilterBackspace);
11395 assert_eq!(app.lambda_state.version_table.filter, "pro");
11396 }
11397
11398 #[test]
11399 fn test_lambda_aliases_table_expansion() {
11400 use crate::lambda::Alias;
11401
11402 let mut app = test_app();
11403 app.current_service = Service::LambdaFunctions;
11404 app.service_selected = true;
11405 app.lambda_state.current_function = Some("test-function".to_string());
11406 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11407 app.mode = Mode::Normal;
11408
11409 app.lambda_state.alias_table.items = vec![
11410 Alias {
11411 name: "prod".to_string(),
11412 versions: "1".to_string(),
11413 description: "Production alias".to_string(),
11414 },
11415 Alias {
11416 name: "staging".to_string(),
11417 versions: "2".to_string(),
11418 description: "Staging alias".to_string(),
11419 },
11420 ];
11421
11422 app.lambda_state.alias_table.selected = 0;
11423
11424 app.handle_action(Action::Select);
11426 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
11427 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11428
11429 app.handle_action(Action::GoBack);
11431 assert_eq!(app.lambda_state.current_alias, None);
11432 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11433
11434 app.lambda_state.alias_table.selected = 1;
11436 app.handle_action(Action::Select);
11437 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
11438 }
11439
11440 #[test]
11441 fn test_lambda_versions_arrow_key_expansion() {
11442 let mut app = test_app();
11443 app.current_service = Service::LambdaFunctions;
11444 app.service_selected = true;
11445 app.lambda_state.current_function = Some("test-function".to_string());
11446 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11447 app.mode = Mode::Normal;
11448
11449 app.lambda_state.version_table.items = vec![LambdaVersion {
11450 version: "1".to_string(),
11451 aliases: "prod".to_string(),
11452 description: "Production".to_string(),
11453 last_modified: "2024-01-01".to_string(),
11454 architecture: "X86_64".to_string(),
11455 }];
11456
11457 app.lambda_state.version_table.selected = 0;
11458
11459 app.handle_action(Action::NextPane);
11461 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11462
11463 app.handle_action(Action::PrevPane);
11465 assert_eq!(app.lambda_state.version_table.expanded_item, None);
11466 }
11467
11468 #[test]
11469 fn test_lambda_version_detail_view() {
11470 use crate::lambda::Function;
11471
11472 let mut app = test_app();
11473 app.current_service = Service::LambdaFunctions;
11474 app.service_selected = true;
11475 app.lambda_state.current_function = Some("test-function".to_string());
11476 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11477 app.mode = Mode::Normal;
11478
11479 app.lambda_state.table.items = vec![Function {
11480 name: "test-function".to_string(),
11481 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11482 application: None,
11483 description: "Test".to_string(),
11484 package_type: "Zip".to_string(),
11485 runtime: "python3.12".to_string(),
11486 architecture: "X86_64".to_string(),
11487 code_size: 1024,
11488 code_sha256: "hash".to_string(),
11489 memory_mb: 128,
11490 timeout_seconds: 30,
11491 last_modified: "2024-01-01".to_string(),
11492 layers: vec![],
11493 }];
11494
11495 app.lambda_state.version_table.items = vec![LambdaVersion {
11496 version: "1".to_string(),
11497 aliases: "prod".to_string(),
11498 description: "Production".to_string(),
11499 last_modified: "2024-01-01".to_string(),
11500 architecture: "X86_64".to_string(),
11501 }];
11502
11503 app.lambda_state.version_table.selected = 0;
11504
11505 app.handle_action(Action::Select);
11507 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11508 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11509
11510 app.handle_action(Action::GoBack);
11512 assert_eq!(app.lambda_state.current_version, None);
11513 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11514 }
11515
11516 #[test]
11517 fn test_lambda_version_detail_tabs() {
11518 use crate::lambda::Function;
11519
11520 let mut app = test_app();
11521 app.current_service = Service::LambdaFunctions;
11522 app.service_selected = true;
11523 app.lambda_state.current_function = Some("test-function".to_string());
11524 app.lambda_state.current_version = Some("1".to_string());
11525 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11526 app.mode = Mode::Normal;
11527
11528 app.lambda_state.table.items = vec![Function {
11529 name: "test-function".to_string(),
11530 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11531 application: None,
11532 description: "Test".to_string(),
11533 package_type: "Zip".to_string(),
11534 runtime: "python3.12".to_string(),
11535 architecture: "X86_64".to_string(),
11536 code_size: 1024,
11537 code_sha256: "hash".to_string(),
11538 memory_mb: 128,
11539 timeout_seconds: 30,
11540 last_modified: "2024-01-01".to_string(),
11541 layers: vec![],
11542 }];
11543
11544 app.handle_action(Action::NextDetailTab);
11546 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
11547
11548 app.handle_action(Action::NextDetailTab);
11549 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11550
11551 app.handle_action(Action::NextDetailTab);
11552 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11553
11554 app.handle_action(Action::PrevDetailTab);
11556 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11557
11558 app.handle_action(Action::PrevDetailTab);
11559 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
11560 }
11561
11562 #[test]
11563 fn test_lambda_aliases_arrow_key_expansion() {
11564 use crate::lambda::Alias;
11565
11566 let mut app = test_app();
11567 app.current_service = Service::LambdaFunctions;
11568 app.service_selected = true;
11569 app.lambda_state.current_function = Some("test-function".to_string());
11570 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11571 app.mode = Mode::Normal;
11572
11573 app.lambda_state.alias_table.items = vec![Alias {
11574 name: "prod".to_string(),
11575 versions: "1".to_string(),
11576 description: "Production alias".to_string(),
11577 }];
11578
11579 app.lambda_state.alias_table.selected = 0;
11580
11581 app.handle_action(Action::NextPane);
11583 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
11584
11585 app.handle_action(Action::PrevPane);
11587 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
11588 }
11589
11590 #[test]
11591 fn test_lambda_functions_arrow_key_expansion() {
11592 use crate::lambda::Function;
11593
11594 let mut app = test_app();
11595 app.current_service = Service::LambdaFunctions;
11596 app.service_selected = true;
11597 app.mode = Mode::Normal;
11598
11599 app.lambda_state.table.items = vec![Function {
11600 name: "test-function".to_string(),
11601 arn: "arn".to_string(),
11602 application: None,
11603 description: "Test".to_string(),
11604 package_type: "Zip".to_string(),
11605 runtime: "python3.12".to_string(),
11606 architecture: "X86_64".to_string(),
11607 code_size: 1024,
11608 code_sha256: "hash".to_string(),
11609 memory_mb: 128,
11610 timeout_seconds: 30,
11611 last_modified: "2024-01-01".to_string(),
11612 layers: vec![],
11613 }];
11614
11615 app.lambda_state.table.selected = 0;
11616
11617 app.handle_action(Action::NextPane);
11619 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
11620
11621 app.handle_action(Action::PrevPane);
11623 assert_eq!(app.lambda_state.table.expanded_item, None);
11624 }
11625
11626 #[test]
11627 fn test_lambda_version_detail_with_application() {
11628 use crate::lambda::Function;
11629
11630 let mut app = test_app();
11631 app.current_service = Service::LambdaFunctions;
11632 app.service_selected = true;
11633 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
11634 app.lambda_state.current_version = Some("1".to_string());
11635 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11636 app.mode = Mode::Normal;
11637
11638 app.lambda_state.table.items = vec![Function {
11639 name: "storefront-studio-beta-api".to_string(),
11640 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
11641 .to_string(),
11642 application: Some("storefront-studio-beta".to_string()),
11643 description: "API function".to_string(),
11644 package_type: "Zip".to_string(),
11645 runtime: "python3.12".to_string(),
11646 architecture: "X86_64".to_string(),
11647 code_size: 1024,
11648 code_sha256: "hash".to_string(),
11649 memory_mb: 128,
11650 timeout_seconds: 30,
11651 last_modified: "2024-01-01".to_string(),
11652 layers: vec![],
11653 }];
11654
11655 assert_eq!(
11657 app.lambda_state.table.items[0].application,
11658 Some("storefront-studio-beta".to_string())
11659 );
11660 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11661 }
11662
11663 #[test]
11664 fn test_lambda_layer_navigation() {
11665 use crate::lambda::{Function, Layer};
11666
11667 let mut app = test_app();
11668 app.current_service = Service::LambdaFunctions;
11669 app.service_selected = true;
11670 app.lambda_state.current_function = Some("test-function".to_string());
11671 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11672 app.mode = Mode::Normal;
11673
11674 app.lambda_state.table.items = vec![Function {
11675 name: "test-function".to_string(),
11676 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11677 application: None,
11678 description: "Test".to_string(),
11679 package_type: "Zip".to_string(),
11680 runtime: "python3.12".to_string(),
11681 architecture: "X86_64".to_string(),
11682 code_size: 1024,
11683 code_sha256: "hash".to_string(),
11684 memory_mb: 128,
11685 timeout_seconds: 30,
11686 last_modified: "2024-01-01".to_string(),
11687 layers: vec![
11688 Layer {
11689 merge_order: "1".to_string(),
11690 name: "layer1".to_string(),
11691 layer_version: "1".to_string(),
11692 compatible_runtimes: "python3.9".to_string(),
11693 compatible_architectures: "x86_64".to_string(),
11694 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11695 },
11696 Layer {
11697 merge_order: "2".to_string(),
11698 name: "layer2".to_string(),
11699 layer_version: "2".to_string(),
11700 compatible_runtimes: "python3.9".to_string(),
11701 compatible_architectures: "x86_64".to_string(),
11702 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11703 },
11704 Layer {
11705 merge_order: "3".to_string(),
11706 name: "layer3".to_string(),
11707 layer_version: "3".to_string(),
11708 compatible_runtimes: "python3.9".to_string(),
11709 compatible_architectures: "x86_64".to_string(),
11710 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
11711 },
11712 ],
11713 }];
11714
11715 assert_eq!(app.lambda_state.layer_selected, 0);
11716
11717 app.handle_action(Action::NextItem);
11718 assert_eq!(app.lambda_state.layer_selected, 1);
11719
11720 app.handle_action(Action::NextItem);
11721 assert_eq!(app.lambda_state.layer_selected, 2);
11722
11723 app.handle_action(Action::NextItem);
11724 assert_eq!(app.lambda_state.layer_selected, 2);
11725
11726 app.handle_action(Action::PrevItem);
11727 assert_eq!(app.lambda_state.layer_selected, 1);
11728
11729 app.handle_action(Action::PrevItem);
11730 assert_eq!(app.lambda_state.layer_selected, 0);
11731
11732 app.handle_action(Action::PrevItem);
11733 assert_eq!(app.lambda_state.layer_selected, 0);
11734 }
11735
11736 #[test]
11737 fn test_lambda_layer_expansion() {
11738 use crate::lambda::{Function, Layer};
11739
11740 let mut app = test_app();
11741 app.current_service = Service::LambdaFunctions;
11742 app.service_selected = true;
11743 app.lambda_state.current_function = Some("test-function".to_string());
11744 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11745 app.mode = Mode::Normal;
11746
11747 app.lambda_state.table.items = vec![Function {
11748 name: "test-function".to_string(),
11749 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11750 application: None,
11751 description: "Test".to_string(),
11752 package_type: "Zip".to_string(),
11753 runtime: "python3.12".to_string(),
11754 architecture: "X86_64".to_string(),
11755 code_size: 1024,
11756 code_sha256: "hash".to_string(),
11757 memory_mb: 128,
11758 timeout_seconds: 30,
11759 last_modified: "2024-01-01".to_string(),
11760 layers: vec![Layer {
11761 merge_order: "1".to_string(),
11762 name: "test-layer".to_string(),
11763 layer_version: "1".to_string(),
11764 compatible_runtimes: "python3.9".to_string(),
11765 compatible_architectures: "x86_64".to_string(),
11766 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
11767 }],
11768 }];
11769
11770 assert_eq!(app.lambda_state.layer_expanded, None);
11771
11772 app.handle_action(Action::NextPane);
11773 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11774
11775 app.handle_action(Action::PrevPane);
11776 assert_eq!(app.lambda_state.layer_expanded, None);
11777
11778 app.handle_action(Action::NextPane);
11779 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11780
11781 app.handle_action(Action::NextPane);
11782 assert_eq!(app.lambda_state.layer_expanded, None);
11783 }
11784
11785 #[test]
11786 fn test_lambda_layer_selection_and_expansion_workflow() {
11787 use crate::lambda::{Function, Layer};
11788
11789 let mut app = test_app();
11790 app.current_service = Service::LambdaFunctions;
11791 app.service_selected = true;
11792 app.lambda_state.current_function = Some("test-function".to_string());
11793 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11794 app.mode = Mode::Normal;
11795
11796 app.lambda_state.table.items = vec![Function {
11797 name: "test-function".to_string(),
11798 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11799 application: None,
11800 description: "Test".to_string(),
11801 package_type: "Zip".to_string(),
11802 runtime: "python3.12".to_string(),
11803 architecture: "X86_64".to_string(),
11804 code_size: 1024,
11805 code_sha256: "hash".to_string(),
11806 memory_mb: 128,
11807 timeout_seconds: 30,
11808 last_modified: "2024-01-01".to_string(),
11809 layers: vec![
11810 Layer {
11811 merge_order: "1".to_string(),
11812 name: "layer1".to_string(),
11813 layer_version: "1".to_string(),
11814 compatible_runtimes: "python3.9".to_string(),
11815 compatible_architectures: "x86_64".to_string(),
11816 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11817 },
11818 Layer {
11819 merge_order: "2".to_string(),
11820 name: "layer2".to_string(),
11821 layer_version: "2".to_string(),
11822 compatible_runtimes: "python3.9".to_string(),
11823 compatible_architectures: "x86_64".to_string(),
11824 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11825 },
11826 ],
11827 }];
11828
11829 assert_eq!(app.lambda_state.layer_selected, 0);
11831 assert_eq!(app.lambda_state.layer_expanded, None);
11832
11833 app.handle_action(Action::NextPane);
11835 assert_eq!(app.lambda_state.layer_selected, 0);
11836 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11837
11838 app.handle_action(Action::NextItem);
11840 assert_eq!(app.lambda_state.layer_selected, 1);
11841 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
11845 assert_eq!(app.lambda_state.layer_selected, 1);
11846 assert_eq!(app.lambda_state.layer_expanded, Some(1));
11847
11848 app.handle_action(Action::PrevPane);
11850 assert_eq!(app.lambda_state.layer_selected, 1);
11851 assert_eq!(app.lambda_state.layer_expanded, None);
11852
11853 app.handle_action(Action::PrevItem);
11855 assert_eq!(app.lambda_state.layer_selected, 0);
11856 assert_eq!(app.lambda_state.layer_expanded, None);
11857 }
11858
11859 #[test]
11860 fn test_backtab_cycles_detail_tabs_backward() {
11861 let mut app = test_app();
11862 app.mode = Mode::Normal;
11863
11864 app.current_service = Service::LambdaFunctions;
11866 app.service_selected = true;
11867 app.lambda_state.current_function = Some("test-function".to_string());
11868 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11869
11870 app.handle_action(Action::PrevDetailTab);
11871 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11872
11873 app.handle_action(Action::PrevDetailTab);
11874 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11875
11876 app.current_service = Service::IamRoles;
11878 app.iam_state.current_role = Some("test-role".to_string());
11879 app.iam_state.role_tab = RoleTab::Permissions;
11880
11881 app.handle_action(Action::PrevDetailTab);
11882 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11883
11884 app.current_service = Service::IamUsers;
11886 app.iam_state.current_user = Some("test-user".to_string());
11887 app.iam_state.user_tab = UserTab::Permissions;
11888
11889 app.handle_action(Action::PrevDetailTab);
11890 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
11891
11892 app.current_service = Service::IamUserGroups;
11894 app.iam_state.current_group = Some("test-group".to_string());
11895 app.iam_state.group_tab = GroupTab::Permissions;
11896
11897 app.handle_action(Action::PrevDetailTab);
11898 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
11899
11900 app.current_service = Service::S3Buckets;
11902 app.s3_state.current_bucket = Some("test-bucket".to_string());
11903 app.s3_state.object_tab = S3ObjectTab::Properties;
11904
11905 app.handle_action(Action::PrevDetailTab);
11906 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
11907
11908 app.current_service = Service::EcrRepositories;
11910 app.ecr_state.current_repository = None;
11911 app.ecr_state.tab = EcrTab::Private;
11912
11913 app.handle_action(Action::PrevDetailTab);
11914 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11915
11916 app.current_service = Service::CloudFormationStacks;
11918 app.cfn_state.current_stack = Some("test-stack".to_string());
11919 app.cfn_state.detail_tab = CfnDetailTab::Resources;
11920 }
11921
11922 #[test]
11923 fn test_cloudformation_status_filter_active() {
11924 let filter = CfnStatusFilter::Active;
11925 assert!(filter.matches("CREATE_IN_PROGRESS"));
11926 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11927 assert!(!filter.matches("CREATE_COMPLETE"));
11928 assert!(!filter.matches("DELETE_COMPLETE"));
11929 assert!(!filter.matches("CREATE_FAILED"));
11930 }
11931
11932 #[test]
11933 fn test_cloudformation_status_filter_complete() {
11934 let filter = CfnStatusFilter::Complete;
11935 assert!(filter.matches("CREATE_COMPLETE"));
11936 assert!(filter.matches("UPDATE_COMPLETE"));
11937 assert!(!filter.matches("DELETE_COMPLETE"));
11938 assert!(!filter.matches("CREATE_IN_PROGRESS"));
11939 }
11940
11941 #[test]
11942 fn test_cloudformation_status_filter_failed() {
11943 let filter = CfnStatusFilter::Failed;
11944 assert!(filter.matches("CREATE_FAILED"));
11945 assert!(filter.matches("UPDATE_FAILED"));
11946 assert!(!filter.matches("CREATE_COMPLETE"));
11947 }
11948
11949 #[test]
11950 fn test_cloudformation_status_filter_deleted() {
11951 let filter = CfnStatusFilter::Deleted;
11952 assert!(filter.matches("DELETE_COMPLETE"));
11953 assert!(filter.matches("DELETE_IN_PROGRESS"));
11954 assert!(!filter.matches("CREATE_COMPLETE"));
11955 }
11956
11957 #[test]
11958 fn test_cloudformation_status_filter_in_progress() {
11959 let filter = CfnStatusFilter::InProgress;
11960 assert!(filter.matches("CREATE_IN_PROGRESS"));
11961 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11962 assert!(filter.matches("DELETE_IN_PROGRESS"));
11963 assert!(!filter.matches("CREATE_COMPLETE"));
11964 }
11965
11966 #[test]
11967 fn test_cloudformation_status_filter_cycle() {
11968 let filter = CfnStatusFilter::All;
11969 assert_eq!(filter.next(), CfnStatusFilter::Active);
11970 assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
11971 assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
11972 assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
11973 assert_eq!(
11974 filter.next().next().next().next().next(),
11975 CfnStatusFilter::InProgress
11976 );
11977 assert_eq!(
11978 filter.next().next().next().next().next().next(),
11979 CfnStatusFilter::All
11980 );
11981 }
11982
11983 #[test]
11984 fn test_cloudformation_default_columns() {
11985 let app = test_app();
11986 assert_eq!(app.cfn_visible_column_ids.len(), 4);
11987 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
11988 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
11989 assert!(app
11990 .cfn_visible_column_ids
11991 .contains(&CfnColumn::CreatedTime.id()));
11992 assert!(app
11993 .cfn_visible_column_ids
11994 .contains(&CfnColumn::Description.id()));
11995 }
11996
11997 #[test]
11998 fn test_cloudformation_all_columns() {
11999 let app = test_app();
12000 assert_eq!(app.cfn_column_ids.len(), 10);
12001 }
12002
12003 #[test]
12004 fn test_cloudformation_filter_by_name() {
12005 let mut app = test_app();
12006 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12007 app.cfn_state.table.items = vec![
12008 CfnStack {
12009 name: "my-stack".to_string(),
12010 stack_id: "id1".to_string(),
12011 status: "CREATE_COMPLETE".to_string(),
12012 created_time: "2024-01-01".to_string(),
12013 updated_time: String::new(),
12014 deleted_time: String::new(),
12015 drift_status: String::new(),
12016 last_drift_check_time: String::new(),
12017 status_reason: String::new(),
12018 description: String::new(),
12019 detailed_status: String::new(),
12020 root_stack: String::new(),
12021 parent_stack: String::new(),
12022 termination_protection: false,
12023 iam_role: String::new(),
12024 tags: Vec::new(),
12025 stack_policy: String::new(),
12026 rollback_monitoring_time: String::new(),
12027 rollback_alarms: Vec::new(),
12028 notification_arns: Vec::new(),
12029 },
12030 CfnStack {
12031 name: "other-stack".to_string(),
12032 stack_id: "id2".to_string(),
12033 status: "CREATE_COMPLETE".to_string(),
12034 created_time: "2024-01-02".to_string(),
12035 updated_time: String::new(),
12036 deleted_time: String::new(),
12037 drift_status: String::new(),
12038 last_drift_check_time: String::new(),
12039 status_reason: String::new(),
12040 description: String::new(),
12041 detailed_status: String::new(),
12042 root_stack: String::new(),
12043 parent_stack: String::new(),
12044 termination_protection: false,
12045 iam_role: String::new(),
12046 tags: Vec::new(),
12047 stack_policy: String::new(),
12048 rollback_monitoring_time: String::new(),
12049 rollback_alarms: Vec::new(),
12050 notification_arns: Vec::new(),
12051 },
12052 ];
12053
12054 app.cfn_state.table.filter = "my".to_string();
12055 let filtered = filtered_cloudformation_stacks(&app);
12056 assert_eq!(filtered.len(), 1);
12057 assert_eq!(filtered[0].name, "my-stack");
12058 }
12059
12060 #[test]
12061 fn test_cloudformation_filter_by_description() {
12062 let mut app = test_app();
12063 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12064 app.cfn_state.table.items = vec![CfnStack {
12065 name: "stack1".to_string(),
12066 stack_id: "id1".to_string(),
12067 status: "CREATE_COMPLETE".to_string(),
12068 created_time: "2024-01-01".to_string(),
12069 updated_time: String::new(),
12070 deleted_time: String::new(),
12071 drift_status: String::new(),
12072 last_drift_check_time: String::new(),
12073 status_reason: String::new(),
12074 description: "production stack".to_string(),
12075 detailed_status: String::new(),
12076 root_stack: String::new(),
12077 parent_stack: String::new(),
12078 termination_protection: false,
12079 iam_role: String::new(),
12080 tags: Vec::new(),
12081 stack_policy: String::new(),
12082 rollback_monitoring_time: String::new(),
12083 rollback_alarms: Vec::new(),
12084 notification_arns: Vec::new(),
12085 }];
12086
12087 app.cfn_state.table.filter = "production".to_string();
12088 let filtered = filtered_cloudformation_stacks(&app);
12089 assert_eq!(filtered.len(), 1);
12090 }
12091
12092 #[test]
12093 fn test_cloudformation_status_filter_applied() {
12094 let mut app = test_app();
12095 app.cfn_state.table.items = vec![
12096 CfnStack {
12097 name: "complete-stack".to_string(),
12098 stack_id: "id1".to_string(),
12099 status: "CREATE_COMPLETE".to_string(),
12100 created_time: "2024-01-01".to_string(),
12101 updated_time: String::new(),
12102 deleted_time: String::new(),
12103 drift_status: String::new(),
12104 last_drift_check_time: String::new(),
12105 status_reason: String::new(),
12106 description: String::new(),
12107 detailed_status: String::new(),
12108 root_stack: String::new(),
12109 parent_stack: String::new(),
12110 termination_protection: false,
12111 iam_role: String::new(),
12112 tags: Vec::new(),
12113 stack_policy: String::new(),
12114 rollback_monitoring_time: String::new(),
12115 rollback_alarms: Vec::new(),
12116 notification_arns: Vec::new(),
12117 },
12118 CfnStack {
12119 name: "failed-stack".to_string(),
12120 stack_id: "id2".to_string(),
12121 status: "CREATE_FAILED".to_string(),
12122 created_time: "2024-01-02".to_string(),
12123 updated_time: String::new(),
12124 deleted_time: String::new(),
12125 drift_status: String::new(),
12126 last_drift_check_time: String::new(),
12127 status_reason: String::new(),
12128 description: String::new(),
12129 detailed_status: String::new(),
12130 root_stack: String::new(),
12131 parent_stack: String::new(),
12132 termination_protection: false,
12133 iam_role: String::new(),
12134 tags: Vec::new(),
12135 stack_policy: String::new(),
12136 rollback_monitoring_time: String::new(),
12137 rollback_alarms: Vec::new(),
12138 notification_arns: Vec::new(),
12139 },
12140 ];
12141
12142 app.cfn_state.status_filter = CfnStatusFilter::Complete;
12143 let filtered = filtered_cloudformation_stacks(&app);
12144 assert_eq!(filtered.len(), 1);
12145 assert_eq!(filtered[0].name, "complete-stack");
12146
12147 app.cfn_state.status_filter = CfnStatusFilter::Failed;
12148 let filtered = filtered_cloudformation_stacks(&app);
12149 assert_eq!(filtered.len(), 1);
12150 assert_eq!(filtered[0].name, "failed-stack");
12151 }
12152
12153 #[test]
12154 fn test_cloudformation_default_page_size() {
12155 let app = test_app();
12156 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
12157 }
12158
12159 #[test]
12160 fn test_cloudformation_default_status_filter() {
12161 let app = test_app();
12162 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
12163 }
12164
12165 #[test]
12166 fn test_cloudformation_view_nested_default_false() {
12167 let app = test_app();
12168 assert!(!app.cfn_state.view_nested);
12169 }
12170
12171 #[test]
12172 fn test_cloudformation_pagination_hotkeys() {
12173 let mut app = test_app();
12174 app.current_service = Service::CloudFormationStacks;
12175 app.service_selected = true;
12176 app.cfn_state.status_filter = CfnStatusFilter::All;
12177
12178 for i in 0..150 {
12180 app.cfn_state.table.items.push(CfnStack {
12181 name: format!("stack-{}", i),
12182 stack_id: format!("id-{}", i),
12183 status: "CREATE_COMPLETE".to_string(),
12184 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12185 updated_time: String::new(),
12186 deleted_time: String::new(),
12187 drift_status: String::new(),
12188 last_drift_check_time: String::new(),
12189 status_reason: String::new(),
12190 description: String::new(),
12191 detailed_status: String::new(),
12192 root_stack: String::new(),
12193 parent_stack: String::new(),
12194 termination_protection: false,
12195 iam_role: String::new(),
12196 tags: vec![],
12197 stack_policy: String::new(),
12198 rollback_monitoring_time: String::new(),
12199 rollback_alarms: vec![],
12200 notification_arns: vec![],
12201 });
12202 }
12203
12204 app.go_to_page(2);
12206 assert_eq!(app.cfn_state.table.selected, 50);
12207
12208 app.go_to_page(3);
12210 assert_eq!(app.cfn_state.table.selected, 100);
12211
12212 app.go_to_page(1);
12214 assert_eq!(app.cfn_state.table.selected, 0);
12215 }
12216
12217 #[test]
12218 fn test_cloudformation_tab_cycling_in_filter_mode() {
12219 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12220 let mut app = test_app();
12221 app.current_service = Service::CloudFormationStacks;
12222 app.service_selected = true;
12223 app.mode = Mode::FilterInput;
12224 app.cfn_state.input_focus = InputFocus::Filter;
12225
12226 app.handle_action(Action::NextFilterFocus);
12228 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12229
12230 app.handle_action(Action::NextFilterFocus);
12232 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12233
12234 app.handle_action(Action::NextFilterFocus);
12236 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12237
12238 app.handle_action(Action::NextFilterFocus);
12240 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12241 }
12242
12243 #[test]
12244 fn test_cloudformation_timestamp_format_includes_utc() {
12245 let stack = CfnStack {
12246 name: "test-stack".to_string(),
12247 stack_id: "id-123".to_string(),
12248 status: "CREATE_COMPLETE".to_string(),
12249 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
12250 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
12251 deleted_time: String::new(),
12252 drift_status: String::new(),
12253 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
12254 status_reason: String::new(),
12255 description: String::new(),
12256 detailed_status: String::new(),
12257 root_stack: String::new(),
12258 parent_stack: String::new(),
12259 termination_protection: false,
12260 iam_role: String::new(),
12261 tags: vec![],
12262 stack_policy: String::new(),
12263 rollback_monitoring_time: String::new(),
12264 rollback_alarms: vec![],
12265 notification_arns: vec![],
12266 };
12267
12268 assert!(stack.created_time.contains("(UTC)"));
12269 assert!(stack.updated_time.contains("(UTC)"));
12270 assert!(stack.last_drift_check_time.contains("(UTC)"));
12271 assert_eq!(stack.created_time.len(), 25);
12272 }
12273
12274 #[test]
12275 fn test_cloudformation_enter_drills_into_stack_view() {
12276 let mut app = test_app();
12277 app.current_service = Service::CloudFormationStacks;
12278 app.service_selected = true;
12279 app.mode = Mode::Normal;
12280 app.cfn_state.status_filter = CfnStatusFilter::All;
12281 app.tabs = vec![Tab {
12282 service: Service::CloudFormationStacks,
12283 title: "CloudFormation › Stacks".to_string(),
12284 breadcrumb: "CloudFormation › Stacks".to_string(),
12285 }];
12286 app.current_tab = 0;
12287
12288 app.cfn_state.table.items.push(CfnStack {
12289 name: "test-stack".to_string(),
12290 stack_id: "id-123".to_string(),
12291 status: "CREATE_COMPLETE".to_string(),
12292 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12293 updated_time: String::new(),
12294 deleted_time: String::new(),
12295 drift_status: String::new(),
12296 last_drift_check_time: String::new(),
12297 status_reason: String::new(),
12298 description: String::new(),
12299 detailed_status: String::new(),
12300 root_stack: String::new(),
12301 parent_stack: String::new(),
12302 termination_protection: false,
12303 iam_role: String::new(),
12304 tags: vec![],
12305 stack_policy: String::new(),
12306 rollback_monitoring_time: String::new(),
12307 rollback_alarms: vec![],
12308 notification_arns: vec![],
12309 });
12310
12311 app.cfn_state.table.reset();
12312 assert_eq!(app.cfn_state.current_stack, None);
12313
12314 app.handle_action(Action::Select);
12316 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
12317 }
12318
12319 #[test]
12320 fn test_cloudformation_arrow_keys_expand_collapse() {
12321 let mut app = test_app();
12322 app.current_service = Service::CloudFormationStacks;
12323 app.service_selected = true;
12324 app.mode = Mode::Normal;
12325 app.cfn_state.status_filter = CfnStatusFilter::All;
12326
12327 app.cfn_state.table.items.push(CfnStack {
12328 name: "test-stack".to_string(),
12329 stack_id: "id-123".to_string(),
12330 status: "CREATE_COMPLETE".to_string(),
12331 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12332 updated_time: String::new(),
12333 deleted_time: String::new(),
12334 drift_status: String::new(),
12335 last_drift_check_time: String::new(),
12336 status_reason: String::new(),
12337 description: String::new(),
12338 detailed_status: String::new(),
12339 root_stack: String::new(),
12340 parent_stack: String::new(),
12341 termination_protection: false,
12342 iam_role: String::new(),
12343 tags: vec![],
12344 stack_policy: String::new(),
12345 rollback_monitoring_time: String::new(),
12346 rollback_alarms: vec![],
12347 notification_arns: vec![],
12348 });
12349
12350 app.cfn_state.table.reset();
12351 assert_eq!(app.cfn_state.table.expanded_item, None);
12352
12353 app.handle_action(Action::NextPane);
12355 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
12356
12357 app.handle_action(Action::PrevPane);
12359 assert_eq!(app.cfn_state.table.expanded_item, None);
12360
12361 assert_eq!(app.cfn_state.current_stack, None);
12363 }
12364
12365 #[test]
12366 fn test_cloudformation_tab_cycling() {
12367 use crate::ui::cfn::DetailTab;
12368 let mut app = test_app();
12369 app.current_service = Service::CloudFormationStacks;
12370 app.service_selected = true;
12371 app.mode = Mode::Normal;
12372 app.cfn_state.status_filter = CfnStatusFilter::All;
12373 app.cfn_state.current_stack = Some("test-stack".to_string());
12374
12375 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
12376 }
12377
12378 #[test]
12379 fn test_cloudformation_console_url() {
12380 use crate::ui::cfn::DetailTab;
12381 let mut app = test_app();
12382 app.current_service = Service::CloudFormationStacks;
12383 app.service_selected = true;
12384 app.cfn_state.status_filter = CfnStatusFilter::All;
12385
12386 app.cfn_state.table.items.push(CfnStack {
12387 name: "test-stack".to_string(),
12388 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12389 .to_string(),
12390 status: "CREATE_COMPLETE".to_string(),
12391 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12392 updated_time: String::new(),
12393 deleted_time: String::new(),
12394 drift_status: String::new(),
12395 last_drift_check_time: String::new(),
12396 status_reason: String::new(),
12397 description: String::new(),
12398 detailed_status: String::new(),
12399 root_stack: String::new(),
12400 parent_stack: String::new(),
12401 termination_protection: false,
12402 iam_role: String::new(),
12403 tags: vec![],
12404 stack_policy: String::new(),
12405 rollback_monitoring_time: String::new(),
12406 rollback_alarms: vec![],
12407 notification_arns: vec![],
12408 });
12409
12410 app.cfn_state.current_stack = Some("test-stack".to_string());
12411
12412 app.cfn_state.detail_tab = DetailTab::StackInfo;
12414 let url = app.get_console_url();
12415 assert!(url.contains("stackinfo"));
12416 assert!(url.contains("arn%3Aaws%3Acloudformation"));
12417
12418 app.cfn_state.detail_tab = DetailTab::Events;
12420 let url = app.get_console_url();
12421 assert!(url.contains("events"));
12422 assert!(url.contains("arn%3Aaws%3Acloudformation"));
12423 }
12424
12425 #[test]
12426 fn test_iam_role_select() {
12427 let mut app = test_app();
12428 app.current_service = Service::IamRoles;
12429 app.service_selected = true;
12430 app.mode = Mode::Normal;
12431
12432 app.iam_state.roles.items = vec![
12433 IamRole {
12434 role_name: "role1".to_string(),
12435 path: "/".to_string(),
12436 trusted_entities: "AWS Service: ec2".to_string(),
12437 last_activity: "-".to_string(),
12438 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
12439 creation_time: "2025-01-01".to_string(),
12440 description: "Test role 1".to_string(),
12441 max_session_duration: Some(3600),
12442 },
12443 IamRole {
12444 role_name: "role2".to_string(),
12445 path: "/".to_string(),
12446 trusted_entities: "AWS Service: lambda".to_string(),
12447 last_activity: "-".to_string(),
12448 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
12449 creation_time: "2025-01-02".to_string(),
12450 description: "Test role 2".to_string(),
12451 max_session_duration: Some(7200),
12452 },
12453 ];
12454
12455 app.iam_state.roles.selected = 0;
12457 app.handle_action(Action::Select);
12458
12459 assert_eq!(
12460 app.iam_state.current_role,
12461 Some("role1".to_string()),
12462 "Should open role detail view"
12463 );
12464 assert_eq!(
12465 app.iam_state.role_tab,
12466 RoleTab::Permissions,
12467 "Should default to Permissions tab"
12468 );
12469 }
12470
12471 #[test]
12472 fn test_iam_role_back_navigation() {
12473 let mut app = test_app();
12474 app.current_service = Service::IamRoles;
12475 app.service_selected = true;
12476 app.iam_state.current_role = Some("test-role".to_string());
12477
12478 app.handle_action(Action::GoBack);
12479
12480 assert_eq!(
12481 app.iam_state.current_role, None,
12482 "Should return to roles list"
12483 );
12484 }
12485
12486 #[test]
12487 fn test_iam_role_tab_navigation() {
12488 let mut app = test_app();
12489 app.current_service = Service::IamRoles;
12490 app.service_selected = true;
12491 app.iam_state.current_role = Some("test-role".to_string());
12492 app.iam_state.role_tab = RoleTab::Permissions;
12493
12494 app.handle_action(Action::NextDetailTab);
12495
12496 assert_eq!(
12497 app.iam_state.role_tab,
12498 RoleTab::TrustRelationships,
12499 "Should move to next tab"
12500 );
12501 }
12502
12503 #[test]
12504 fn test_iam_role_tab_cycle_order() {
12505 let mut app = test_app();
12506 app.current_service = Service::IamRoles;
12507 app.service_selected = true;
12508 app.iam_state.current_role = Some("test-role".to_string());
12509 app.iam_state.role_tab = RoleTab::Permissions;
12510
12511 app.handle_action(Action::NextDetailTab);
12512 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
12513
12514 app.handle_action(Action::NextDetailTab);
12515 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
12516
12517 app.handle_action(Action::NextDetailTab);
12518 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
12519
12520 app.handle_action(Action::NextDetailTab);
12521 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
12522
12523 app.handle_action(Action::NextDetailTab);
12524 assert_eq!(
12525 app.iam_state.role_tab,
12526 RoleTab::Permissions,
12527 "Should cycle back to first tab"
12528 );
12529 }
12530
12531 #[test]
12532 fn test_iam_role_pagination() {
12533 let mut app = test_app();
12534 app.current_service = Service::IamRoles;
12535 app.service_selected = true;
12536 app.iam_state.roles.page_size = PageSize::Ten;
12537
12538 app.iam_state.roles.items = (0..25)
12539 .map(|i| IamRole {
12540 role_name: format!("role{}", i),
12541 path: "/".to_string(),
12542 trusted_entities: "AWS Service: ec2".to_string(),
12543 last_activity: "-".to_string(),
12544 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12545 creation_time: "2025-01-01".to_string(),
12546 description: format!("Test role {}", i),
12547 max_session_duration: Some(3600),
12548 })
12549 .collect();
12550
12551 app.go_to_page(2);
12553
12554 assert_eq!(
12555 app.iam_state.roles.selected, 10,
12556 "Should select first item of page 2"
12557 );
12558 assert_eq!(
12559 app.iam_state.roles.scroll_offset, 10,
12560 "Should update scroll offset"
12561 );
12562 }
12563
12564 #[test]
12565 fn test_tags_table_populated_on_role_detail() {
12566 let mut app = test_app();
12567 app.current_service = Service::IamRoles;
12568 app.service_selected = true;
12569 app.mode = Mode::Normal;
12570 app.iam_state.roles.items = vec![IamRole {
12571 role_name: "TestRole".to_string(),
12572 path: "/".to_string(),
12573 trusted_entities: String::new(),
12574 last_activity: String::new(),
12575 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
12576 creation_time: "2025-01-01".to_string(),
12577 description: String::new(),
12578 max_session_duration: Some(3600),
12579 }];
12580
12581 app.iam_state.tags.items = vec![
12583 IamRoleTag {
12584 key: "Environment".to_string(),
12585 value: "Production".to_string(),
12586 },
12587 IamRoleTag {
12588 key: "Team".to_string(),
12589 value: "Platform".to_string(),
12590 },
12591 ];
12592
12593 assert_eq!(app.iam_state.tags.items.len(), 2);
12594 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
12595 assert_eq!(app.iam_state.tags.items[0].value, "Production");
12596 assert_eq!(app.iam_state.tags.selected, 0);
12597 }
12598
12599 #[test]
12600 fn test_tags_table_navigation() {
12601 let mut app = test_app();
12602 app.current_service = Service::IamRoles;
12603 app.service_selected = true;
12604 app.mode = Mode::Normal;
12605 app.iam_state.current_role = Some("TestRole".to_string());
12606 app.iam_state.role_tab = RoleTab::Tags;
12607 app.iam_state.tags.items = vec![
12608 IamRoleTag {
12609 key: "Tag1".to_string(),
12610 value: "Value1".to_string(),
12611 },
12612 IamRoleTag {
12613 key: "Tag2".to_string(),
12614 value: "Value2".to_string(),
12615 },
12616 ];
12617
12618 app.handle_action(Action::NextItem);
12619 assert_eq!(app.iam_state.tags.selected, 1);
12620
12621 app.handle_action(Action::PrevItem);
12622 assert_eq!(app.iam_state.tags.selected, 0);
12623 }
12624
12625 #[test]
12626 fn test_last_accessed_table_navigation() {
12627 let mut app = test_app();
12628 app.current_service = Service::IamRoles;
12629 app.service_selected = true;
12630 app.mode = Mode::Normal;
12631 app.iam_state.current_role = Some("TestRole".to_string());
12632 app.iam_state.role_tab = RoleTab::LastAccessed;
12633 app.iam_state.last_accessed_services.items = vec![
12634 LastAccessedService {
12635 service: "S3".to_string(),
12636 policies_granting: "Policy1".to_string(),
12637 last_accessed: "2025-01-01".to_string(),
12638 },
12639 LastAccessedService {
12640 service: "EC2".to_string(),
12641 policies_granting: "Policy2".to_string(),
12642 last_accessed: "2025-01-02".to_string(),
12643 },
12644 ];
12645
12646 app.handle_action(Action::NextItem);
12647 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
12648
12649 app.handle_action(Action::PrevItem);
12650 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
12651 }
12652
12653 #[test]
12654 fn test_cfn_input_focus_next() {
12655 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12656 let mut app = test_app();
12657 app.current_service = Service::CloudFormationStacks;
12658 app.mode = Mode::FilterInput;
12659 app.cfn_state.input_focus = InputFocus::Filter;
12660
12661 app.handle_action(Action::NextFilterFocus);
12662 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12663
12664 app.handle_action(Action::NextFilterFocus);
12665 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12666
12667 app.handle_action(Action::NextFilterFocus);
12668 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12669
12670 app.handle_action(Action::NextFilterFocus);
12671 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12672 }
12673
12674 #[test]
12675 fn test_cfn_input_focus_prev() {
12676 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12677 let mut app = test_app();
12678 app.current_service = Service::CloudFormationStacks;
12679 app.mode = Mode::FilterInput;
12680 app.cfn_state.input_focus = InputFocus::Filter;
12681
12682 app.handle_action(Action::PrevFilterFocus);
12683 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12684
12685 app.handle_action(Action::PrevFilterFocus);
12686 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12687
12688 app.handle_action(Action::PrevFilterFocus);
12689 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12690
12691 app.handle_action(Action::PrevFilterFocus);
12692 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12693 }
12694
12695 #[test]
12696 fn test_cw_logs_input_focus_prev() {
12697 let mut app = test_app();
12698 app.current_service = Service::CloudWatchLogGroups;
12699 app.mode = Mode::FilterInput;
12700 app.view_mode = ViewMode::Detail;
12701 app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
12702 app.log_groups_state.input_focus = InputFocus::Filter;
12703
12704 app.handle_action(Action::PrevFilterFocus);
12705 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
12706
12707 app.handle_action(Action::PrevFilterFocus);
12708 assert_eq!(
12709 app.log_groups_state.input_focus,
12710 InputFocus::Checkbox("ShowExpired")
12711 );
12712
12713 app.handle_action(Action::PrevFilterFocus);
12714 assert_eq!(
12715 app.log_groups_state.input_focus,
12716 InputFocus::Checkbox("ExactMatch")
12717 );
12718
12719 app.handle_action(Action::PrevFilterFocus);
12720 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
12721 }
12722
12723 #[test]
12724 fn test_cw_events_input_focus_prev() {
12725 use crate::ui::cw::logs::EventFilterFocus;
12726 let mut app = test_app();
12727 app.mode = Mode::EventFilterInput;
12728 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
12729
12730 app.handle_action(Action::PrevFilterFocus);
12731 assert_eq!(
12732 app.log_groups_state.event_input_focus,
12733 EventFilterFocus::DateRange
12734 );
12735
12736 app.handle_action(Action::PrevFilterFocus);
12737 assert_eq!(
12738 app.log_groups_state.event_input_focus,
12739 EventFilterFocus::Filter
12740 );
12741 }
12742
12743 #[test]
12744 fn test_cfn_input_focus_cycle_complete() {
12745 let mut app = test_app();
12746 app.current_service = Service::CloudFormationStacks;
12747 app.mode = Mode::FilterInput;
12748 app.cfn_state.input_focus = InputFocus::Filter;
12749
12750 for _ in 0..4 {
12752 app.handle_action(Action::NextFilterFocus);
12753 }
12754 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12755
12756 for _ in 0..4 {
12758 app.handle_action(Action::PrevFilterFocus);
12759 }
12760 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12761 }
12762
12763 #[test]
12764 fn test_cfn_resources_pagination_left_right_arrows() {
12765 let mut app = test_app();
12769 app.current_service = Service::CloudFormationStacks;
12770 app.service_selected = true;
12771 app.mode = Mode::FilterInput;
12772 app.cfn_state.current_stack = Some("test-stack".to_string());
12773 app.cfn_state.detail_tab = CfnDetailTab::Resources;
12774 app.cfn_state.resources_input_focus = InputFocus::Pagination;
12775
12776 app.cfn_state.resources.items = (0..60)
12778 .map(|i| rusticity_core::cfn::StackResource {
12779 logical_id: format!("Resource{}", i),
12780 physical_id: format!("phys{}", i),
12781 resource_type: "AWS::S3::Bucket".to_string(),
12782 status: String::new(),
12783 module_info: String::new(),
12784 })
12785 .collect();
12786 app.cfn_state.resources.page_size = crate::common::PageSize::Ten;
12787 app.cfn_state.resources.selected = 0;
12788
12789 app.handle_action(Action::PageDown);
12791 assert_eq!(
12792 app.cfn_state.resources.selected, 10,
12793 "Right arrow with pagination focus must move to page 2 of Resources"
12794 );
12795
12796 app.handle_action(Action::PageUp);
12798 assert_eq!(
12799 app.cfn_state.resources.selected, 0,
12800 "Left arrow with pagination focus must move back to page 1 of Resources"
12801 );
12802 }
12803
12804 #[test]
12805 fn test_cfn_filter_status_arrow_keys() {
12806 use crate::ui::cfn::STATUS_FILTER;
12807 let mut app = test_app();
12808 app.current_service = Service::CloudFormationStacks;
12809 app.mode = Mode::FilterInput;
12810 app.cfn_state.input_focus = STATUS_FILTER;
12811 app.cfn_state.status_filter = CfnStatusFilter::All;
12812
12813 app.handle_action(Action::NextItem);
12814 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
12815
12816 app.handle_action(Action::PrevItem);
12817 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
12818 }
12819
12820 #[test]
12821 fn test_cfn_filter_shift_tab_cycles_backward() {
12822 use crate::ui::cfn::STATUS_FILTER;
12823 let mut app = test_app();
12824 app.current_service = Service::CloudFormationStacks;
12825 app.mode = Mode::FilterInput;
12826 app.cfn_state.input_focus = STATUS_FILTER;
12827
12828 app.handle_action(Action::PrevFilterFocus);
12830 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12831
12832 app.handle_action(Action::PrevFilterFocus);
12834 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12835 }
12836
12837 #[test]
12838 fn test_cfn_pagination_arrow_keys() {
12839 let mut app = test_app();
12840 app.current_service = Service::CloudFormationStacks;
12841 app.mode = Mode::FilterInput;
12842 app.cfn_state.input_focus = InputFocus::Pagination;
12843 app.cfn_state.table.scroll_offset = 0;
12844 app.cfn_state.table.page_size = PageSize::Ten;
12845
12846 app.cfn_state.table.items = (0..30)
12848 .map(|i| CfnStack {
12849 name: format!("stack-{}", i),
12850 stack_id: format!("id-{}", i),
12851 status: "CREATE_COMPLETE".to_string(),
12852 created_time: "2024-01-01".to_string(),
12853 updated_time: String::new(),
12854 deleted_time: String::new(),
12855 drift_status: String::new(),
12856 last_drift_check_time: String::new(),
12857 status_reason: String::new(),
12858 description: String::new(),
12859 detailed_status: String::new(),
12860 root_stack: String::new(),
12861 parent_stack: String::new(),
12862 termination_protection: false,
12863 iam_role: String::new(),
12864 tags: Vec::new(),
12865 stack_policy: String::new(),
12866 rollback_monitoring_time: String::new(),
12867 rollback_alarms: Vec::new(),
12868 notification_arns: Vec::new(),
12869 })
12870 .collect();
12871
12872 app.handle_action(Action::PageDown);
12874 assert_eq!(app.cfn_state.table.scroll_offset, 10);
12875 let page_size = app.cfn_state.table.page_size.value();
12877 let current_page = app.cfn_state.table.scroll_offset / page_size;
12878 assert_eq!(current_page, 1);
12879
12880 app.handle_action(Action::PageUp);
12882 assert_eq!(app.cfn_state.table.scroll_offset, 0);
12883 let current_page = app.cfn_state.table.scroll_offset / page_size;
12884 assert_eq!(current_page, 0);
12885 }
12886
12887 #[test]
12888 fn test_cfn_page_navigation_updates_selection() {
12889 let mut app = test_app();
12890 app.current_service = Service::CloudFormationStacks;
12891 app.mode = Mode::Normal;
12892
12893 app.cfn_state.table.items = (0..30)
12895 .map(|i| CfnStack {
12896 name: format!("stack-{}", i),
12897 stack_id: format!("id-{}", i),
12898 status: "CREATE_COMPLETE".to_string(),
12899 created_time: "2024-01-01".to_string(),
12900 updated_time: String::new(),
12901 deleted_time: String::new(),
12902 drift_status: String::new(),
12903 last_drift_check_time: String::new(),
12904 status_reason: String::new(),
12905 description: String::new(),
12906 detailed_status: String::new(),
12907 root_stack: String::new(),
12908 parent_stack: String::new(),
12909 termination_protection: false,
12910 iam_role: String::new(),
12911 tags: Vec::new(),
12912 stack_policy: String::new(),
12913 rollback_monitoring_time: String::new(),
12914 rollback_alarms: Vec::new(),
12915 notification_arns: Vec::new(),
12916 })
12917 .collect();
12918
12919 app.cfn_state.table.reset();
12920 app.cfn_state.table.scroll_offset = 0;
12921
12922 app.handle_action(Action::PageDown);
12924 assert_eq!(app.cfn_state.table.selected, 10);
12925
12926 app.handle_action(Action::PageDown);
12928 assert_eq!(app.cfn_state.table.selected, 20);
12929
12930 app.handle_action(Action::PageUp);
12932 assert_eq!(app.cfn_state.table.selected, 10);
12933 }
12934
12935 #[test]
12936 fn test_cfn_filter_input_only_when_focused() {
12937 use crate::ui::cfn::STATUS_FILTER;
12938 let mut app = test_app();
12939 app.current_service = Service::CloudFormationStacks;
12940 app.mode = Mode::FilterInput;
12941 app.cfn_state.input_focus = STATUS_FILTER;
12942 app.cfn_state.table.filter = String::new();
12943
12944 app.handle_action(Action::FilterInput('t'));
12946 app.handle_action(Action::FilterInput('e'));
12947 app.handle_action(Action::FilterInput('s'));
12948 app.handle_action(Action::FilterInput('t'));
12949 assert_eq!(app.cfn_state.table.filter, "");
12950
12951 app.cfn_state.input_focus = InputFocus::Filter;
12953 app.handle_action(Action::FilterInput('t'));
12954 app.handle_action(Action::FilterInput('e'));
12955 app.handle_action(Action::FilterInput('s'));
12956 app.handle_action(Action::FilterInput('t'));
12957 assert_eq!(app.cfn_state.table.filter, "test");
12958 }
12959
12960 #[test]
12961 fn test_cfn_input_focus_resets_on_start() {
12962 let mut app = test_app();
12963 app.current_service = Service::CloudFormationStacks;
12964 app.service_selected = true;
12965 app.mode = Mode::Normal;
12966 app.cfn_state.input_focus = InputFocus::Pagination;
12967
12968 app.handle_action(Action::StartFilter);
12970 assert_eq!(app.mode, Mode::FilterInput);
12971 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12972 }
12973
12974 #[test]
12975 fn test_iam_roles_input_focus_cycles_forward() {
12976 let mut app = test_app();
12977 app.current_service = Service::IamRoles;
12978 app.mode = Mode::FilterInput;
12979 app.iam_state.role_input_focus = InputFocus::Filter;
12980
12981 app.handle_action(Action::NextFilterFocus);
12982 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12983
12984 app.handle_action(Action::NextFilterFocus);
12985 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12986 }
12987
12988 #[test]
12989 fn test_iam_roles_input_focus_cycles_backward() {
12990 let mut app = test_app();
12991 app.current_service = Service::IamRoles;
12992 app.mode = Mode::FilterInput;
12993 app.iam_state.role_input_focus = InputFocus::Filter;
12994
12995 app.handle_action(Action::PrevFilterFocus);
12996 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12997
12998 app.handle_action(Action::PrevFilterFocus);
12999 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
13000 }
13001
13002 #[test]
13003 fn test_iam_roles_filter_input_only_when_focused() {
13004 let mut app = test_app();
13005 app.current_service = Service::IamRoles;
13006 app.mode = Mode::FilterInput;
13007 app.iam_state.role_input_focus = InputFocus::Pagination;
13008 app.iam_state.roles.filter = String::new();
13009
13010 app.handle_action(Action::FilterInput('t'));
13012 app.handle_action(Action::FilterInput('e'));
13013 app.handle_action(Action::FilterInput('s'));
13014 app.handle_action(Action::FilterInput('t'));
13015 assert_eq!(app.iam_state.roles.filter, "");
13016
13017 app.iam_state.role_input_focus = InputFocus::Filter;
13019 app.handle_action(Action::FilterInput('t'));
13020 app.handle_action(Action::FilterInput('e'));
13021 app.handle_action(Action::FilterInput('s'));
13022 app.handle_action(Action::FilterInput('t'));
13023 assert_eq!(app.iam_state.roles.filter, "test");
13024 }
13025
13026 #[test]
13027 fn test_iam_roles_page_down_updates_scroll_offset() {
13028 let mut app = test_app();
13029 app.current_service = Service::IamRoles;
13030 app.mode = Mode::Normal;
13031 app.iam_state.roles.items = (0..50)
13032 .map(|i| IamRole {
13033 role_name: format!("role-{}", i),
13034 path: "/".to_string(),
13035 trusted_entities: "AWS Service".to_string(),
13036 last_activity: "N/A".to_string(),
13037 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
13038 creation_time: "2024-01-01".to_string(),
13039 description: String::new(),
13040 max_session_duration: Some(3600),
13041 })
13042 .collect();
13043
13044 app.iam_state.roles.selected = 0;
13045 app.iam_state.roles.scroll_offset = 0;
13046
13047 app.handle_action(Action::PageDown);
13049 assert_eq!(app.iam_state.roles.selected, 10);
13050 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
13052
13053 app.handle_action(Action::PageDown);
13055 assert_eq!(app.iam_state.roles.selected, 20);
13056 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
13057 }
13058
13059 #[test]
13060 fn test_application_selection_and_deployments_tab() {
13061 use crate::lambda::Application as LambdaApplication;
13062 use LambdaApplicationDetailTab;
13063
13064 let mut app = test_app();
13065 app.current_service = Service::LambdaApplications;
13066 app.service_selected = true;
13067 app.mode = Mode::Normal;
13068
13069 app.lambda_application_state.table.items = vec![LambdaApplication {
13070 name: "test-app".to_string(),
13071 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13072 description: "Test application".to_string(),
13073 status: "CREATE_COMPLETE".to_string(),
13074 last_modified: "2024-01-01".to_string(),
13075 }];
13076
13077 app.handle_action(Action::Select);
13079 assert_eq!(
13080 app.lambda_application_state.current_application,
13081 Some("test-app".to_string())
13082 );
13083 assert_eq!(
13084 app.lambda_application_state.detail_tab,
13085 LambdaApplicationDetailTab::Overview
13086 );
13087
13088 app.handle_action(Action::NextDetailTab);
13090 assert_eq!(
13091 app.lambda_application_state.detail_tab,
13092 LambdaApplicationDetailTab::Deployments
13093 );
13094
13095 app.handle_action(Action::GoBack);
13097 assert_eq!(app.lambda_application_state.current_application, None);
13098 }
13099
13100 #[test]
13101 fn test_application_resources_filter_and_pagination() {
13102 use crate::lambda::Application as LambdaApplication;
13103 use LambdaApplicationDetailTab;
13104
13105 let mut app = test_app();
13106 app.current_service = Service::LambdaApplications;
13107 app.service_selected = true;
13108 app.mode = Mode::Normal;
13109
13110 app.lambda_application_state.table.items = vec![LambdaApplication {
13111 name: "test-app".to_string(),
13112 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13113 description: "Test application".to_string(),
13114 status: "CREATE_COMPLETE".to_string(),
13115 last_modified: "2024-01-01".to_string(),
13116 }];
13117
13118 app.handle_action(Action::Select);
13120 assert_eq!(
13121 app.lambda_application_state.detail_tab,
13122 LambdaApplicationDetailTab::Overview
13123 );
13124
13125 assert!(!app.lambda_application_state.resources.items.is_empty());
13127
13128 app.mode = Mode::FilterInput;
13130 assert_eq!(
13131 app.lambda_application_state.resource_input_focus,
13132 InputFocus::Filter
13133 );
13134
13135 app.handle_action(Action::NextFilterFocus);
13136 assert_eq!(
13137 app.lambda_application_state.resource_input_focus,
13138 InputFocus::Pagination
13139 );
13140
13141 app.handle_action(Action::PrevFilterFocus);
13142 assert_eq!(
13143 app.lambda_application_state.resource_input_focus,
13144 InputFocus::Filter
13145 );
13146 }
13147
13148 #[test]
13149 fn test_application_deployments_filter_and_pagination() {
13150 use crate::lambda::Application as LambdaApplication;
13151 use LambdaApplicationDetailTab;
13152
13153 let mut app = test_app();
13154 app.current_service = Service::LambdaApplications;
13155 app.service_selected = true;
13156 app.mode = Mode::Normal;
13157
13158 app.lambda_application_state.table.items = vec![LambdaApplication {
13159 name: "test-app".to_string(),
13160 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13161 description: "Test application".to_string(),
13162 status: "CREATE_COMPLETE".to_string(),
13163 last_modified: "2024-01-01".to_string(),
13164 }];
13165
13166 app.handle_action(Action::Select);
13168 app.handle_action(Action::NextDetailTab);
13169 assert_eq!(
13170 app.lambda_application_state.detail_tab,
13171 LambdaApplicationDetailTab::Deployments
13172 );
13173
13174 assert!(!app.lambda_application_state.deployments.items.is_empty());
13176
13177 app.mode = Mode::FilterInput;
13179 assert_eq!(
13180 app.lambda_application_state.deployment_input_focus,
13181 InputFocus::Filter
13182 );
13183
13184 app.handle_action(Action::NextFilterFocus);
13185 assert_eq!(
13186 app.lambda_application_state.deployment_input_focus,
13187 InputFocus::Pagination
13188 );
13189
13190 app.handle_action(Action::PrevFilterFocus);
13191 assert_eq!(
13192 app.lambda_application_state.deployment_input_focus,
13193 InputFocus::Filter
13194 );
13195 }
13196
13197 #[test]
13198 fn test_application_resource_expansion() {
13199 use crate::lambda::Application as LambdaApplication;
13200 use LambdaApplicationDetailTab;
13201
13202 let mut app = test_app();
13203 app.current_service = Service::LambdaApplications;
13204 app.service_selected = true;
13205 app.mode = Mode::Normal;
13206
13207 app.lambda_application_state.table.items = vec![LambdaApplication {
13208 name: "test-app".to_string(),
13209 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13210 description: "Test application".to_string(),
13211 status: "CREATE_COMPLETE".to_string(),
13212 last_modified: "2024-01-01".to_string(),
13213 }];
13214
13215 app.handle_action(Action::Select);
13217 assert_eq!(
13218 app.lambda_application_state.detail_tab,
13219 LambdaApplicationDetailTab::Overview
13220 );
13221
13222 app.handle_action(Action::NextPane);
13224 assert_eq!(
13225 app.lambda_application_state.resources.expanded_item,
13226 Some(0)
13227 );
13228
13229 app.handle_action(Action::PrevPane);
13231 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
13232 }
13233
13234 #[test]
13235 fn test_application_deployment_expansion() {
13236 use crate::lambda::Application as LambdaApplication;
13237 use LambdaApplicationDetailTab;
13238
13239 let mut app = test_app();
13240 app.current_service = Service::LambdaApplications;
13241 app.service_selected = true;
13242 app.mode = Mode::Normal;
13243
13244 app.lambda_application_state.table.items = vec![LambdaApplication {
13245 name: "test-app".to_string(),
13246 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13247 description: "Test application".to_string(),
13248 status: "CREATE_COMPLETE".to_string(),
13249 last_modified: "2024-01-01".to_string(),
13250 }];
13251
13252 app.handle_action(Action::Select);
13254 app.handle_action(Action::NextDetailTab);
13255 assert_eq!(
13256 app.lambda_application_state.detail_tab,
13257 LambdaApplicationDetailTab::Deployments
13258 );
13259
13260 app.handle_action(Action::NextPane);
13262 assert_eq!(
13263 app.lambda_application_state.deployments.expanded_item,
13264 Some(0)
13265 );
13266
13267 app.handle_action(Action::PrevPane);
13269 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
13270 }
13271
13272 #[test]
13273 fn test_s3_nested_prefix_expansion() {
13274 use crate::s3::Bucket;
13275 use S3Object;
13276
13277 let mut app = test_app();
13278 app.current_service = Service::S3Buckets;
13279 app.service_selected = true;
13280 app.mode = Mode::Normal;
13281
13282 app.s3_state.buckets.items = vec![Bucket {
13284 name: "test-bucket".to_string(),
13285 region: "us-east-1".to_string(),
13286 creation_date: "2024-01-01".to_string(),
13287 }];
13288
13289 app.s3_state.bucket_preview.insert(
13291 "test-bucket".to_string(),
13292 vec![S3Object {
13293 key: "level1/".to_string(),
13294 size: 0,
13295 last_modified: "".to_string(),
13296 is_prefix: true,
13297 storage_class: "".to_string(),
13298 }],
13299 );
13300
13301 app.s3_state.prefix_preview.insert(
13303 "level1/".to_string(),
13304 vec![S3Object {
13305 key: "level1/level2/".to_string(),
13306 size: 0,
13307 last_modified: "".to_string(),
13308 is_prefix: true,
13309 storage_class: "".to_string(),
13310 }],
13311 );
13312
13313 app.s3_state.selected_row = 0;
13315 app.handle_action(Action::NextPane);
13316 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13317
13318 app.s3_state.selected_row = 1;
13320 app.handle_action(Action::NextPane);
13321 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13322
13323 app.s3_state.selected_row = 2;
13325 app.handle_action(Action::NextPane);
13326 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
13327
13328 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13330 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13331 }
13332
13333 #[test]
13334 fn test_s3_nested_prefix_collapse() {
13335 use crate::s3::Bucket;
13336 use S3Object;
13337
13338 let mut app = test_app();
13339 app.current_service = Service::S3Buckets;
13340 app.service_selected = true;
13341 app.mode = Mode::Normal;
13342
13343 app.s3_state.buckets.items = vec![Bucket {
13344 name: "test-bucket".to_string(),
13345 region: "us-east-1".to_string(),
13346 creation_date: "2024-01-01".to_string(),
13347 }];
13348
13349 app.s3_state.bucket_preview.insert(
13350 "test-bucket".to_string(),
13351 vec![S3Object {
13352 key: "level1/".to_string(),
13353 size: 0,
13354 last_modified: "".to_string(),
13355 is_prefix: true,
13356 storage_class: "".to_string(),
13357 }],
13358 );
13359
13360 app.s3_state.prefix_preview.insert(
13361 "level1/".to_string(),
13362 vec![S3Object {
13363 key: "level1/level2/".to_string(),
13364 size: 0,
13365 last_modified: "".to_string(),
13366 is_prefix: true,
13367 storage_class: "".to_string(),
13368 }],
13369 );
13370
13371 app.s3_state
13373 .expanded_prefixes
13374 .insert("test-bucket".to_string());
13375 app.s3_state.expanded_prefixes.insert("level1/".to_string());
13376 app.s3_state
13377 .expanded_prefixes
13378 .insert("level1/level2/".to_string());
13379
13380 app.s3_state.selected_row = 2;
13382 app.handle_action(Action::PrevPane);
13383 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
13384 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
13388 app.handle_action(Action::PrevPane);
13389 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
13390 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
13394 app.handle_action(Action::PrevPane);
13395 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
13396 }
13397}
13398
13399#[cfg(test)]
13400mod sqs_tests {
13401 use super::*;
13402 use test_helpers::*;
13403
13404 #[test]
13405 fn test_sqs_filter_input() {
13406 let mut app = test_app();
13407 app.current_service = Service::SqsQueues;
13408 app.service_selected = true;
13409 app.mode = Mode::FilterInput;
13410
13411 app.handle_action(Action::FilterInput('t'));
13412 app.handle_action(Action::FilterInput('e'));
13413 app.handle_action(Action::FilterInput('s'));
13414 app.handle_action(Action::FilterInput('t'));
13415 assert_eq!(app.sqs_state.queues.filter, "test");
13416
13417 app.handle_action(Action::FilterBackspace);
13418 assert_eq!(app.sqs_state.queues.filter, "tes");
13419 }
13420
13421 #[test]
13422 fn test_sqs_start_filter() {
13423 let mut app = test_app();
13424 app.current_service = Service::SqsQueues;
13425 app.service_selected = true;
13426 app.mode = Mode::Normal;
13427
13428 app.handle_action(Action::StartFilter);
13429 assert_eq!(app.mode, Mode::FilterInput);
13430 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13431 }
13432
13433 #[test]
13434 fn test_sqs_filter_focus_cycling() {
13435 let mut app = test_app();
13436 app.current_service = Service::SqsQueues;
13437 app.service_selected = true;
13438 app.mode = Mode::FilterInput;
13439 app.sqs_state.input_focus = InputFocus::Filter;
13440
13441 app.handle_action(Action::NextFilterFocus);
13442 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13443
13444 app.handle_action(Action::NextFilterFocus);
13445 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13446
13447 app.handle_action(Action::PrevFilterFocus);
13448 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13449 }
13450
13451 #[test]
13452 fn test_sqs_navigation() {
13453 let mut app = test_app();
13454 app.current_service = Service::SqsQueues;
13455 app.service_selected = true;
13456 app.mode = Mode::Normal;
13457 app.sqs_state.queues.items = (0..10)
13458 .map(|i| SqsQueue {
13459 name: format!("queue{}", i),
13460 url: String::new(),
13461 queue_type: "Standard".to_string(),
13462 created_timestamp: String::new(),
13463 messages_available: "0".to_string(),
13464 messages_in_flight: "0".to_string(),
13465 encryption: "Disabled".to_string(),
13466 content_based_deduplication: "Disabled".to_string(),
13467 last_modified_timestamp: String::new(),
13468 visibility_timeout: String::new(),
13469 message_retention_period: String::new(),
13470 maximum_message_size: String::new(),
13471 delivery_delay: String::new(),
13472 receive_message_wait_time: String::new(),
13473 high_throughput_fifo: "N/A".to_string(),
13474 deduplication_scope: "N/A".to_string(),
13475 fifo_throughput_limit: "N/A".to_string(),
13476 dead_letter_queue: "-".to_string(),
13477 messages_delayed: "0".to_string(),
13478 redrive_allow_policy: "-".to_string(),
13479 redrive_policy: "".to_string(),
13480 redrive_task_id: "-".to_string(),
13481 redrive_task_start_time: "-".to_string(),
13482 redrive_task_status: "-".to_string(),
13483 redrive_task_percent: "-".to_string(),
13484 redrive_task_destination: "-".to_string(),
13485 })
13486 .collect();
13487
13488 app.handle_action(Action::NextItem);
13489 assert_eq!(app.sqs_state.queues.selected, 1);
13490
13491 app.handle_action(Action::PrevItem);
13492 assert_eq!(app.sqs_state.queues.selected, 0);
13493 }
13494
13495 #[test]
13496 fn test_sqs_page_navigation() {
13497 let mut app = test_app();
13498 app.current_service = Service::SqsQueues;
13499 app.service_selected = true;
13500 app.mode = Mode::Normal;
13501 app.sqs_state.queues.items = (0..100)
13502 .map(|i| SqsQueue {
13503 name: format!("queue{}", i),
13504 url: String::new(),
13505 queue_type: "Standard".to_string(),
13506 created_timestamp: String::new(),
13507 messages_available: "0".to_string(),
13508 messages_in_flight: "0".to_string(),
13509 encryption: "Disabled".to_string(),
13510 content_based_deduplication: "Disabled".to_string(),
13511 last_modified_timestamp: String::new(),
13512 visibility_timeout: String::new(),
13513 message_retention_period: String::new(),
13514 maximum_message_size: String::new(),
13515 delivery_delay: String::new(),
13516 receive_message_wait_time: String::new(),
13517 high_throughput_fifo: "N/A".to_string(),
13518 deduplication_scope: "N/A".to_string(),
13519 fifo_throughput_limit: "N/A".to_string(),
13520 dead_letter_queue: "-".to_string(),
13521 messages_delayed: "0".to_string(),
13522 redrive_allow_policy: "-".to_string(),
13523 redrive_policy: "".to_string(),
13524 redrive_task_id: "-".to_string(),
13525 redrive_task_start_time: "-".to_string(),
13526 redrive_task_status: "-".to_string(),
13527 redrive_task_percent: "-".to_string(),
13528 redrive_task_destination: "-".to_string(),
13529 })
13530 .collect();
13531
13532 app.handle_action(Action::PageDown);
13533 assert_eq!(app.sqs_state.queues.selected, 10);
13534
13535 app.handle_action(Action::PageUp);
13536 assert_eq!(app.sqs_state.queues.selected, 0);
13537 }
13538
13539 #[test]
13540 fn test_sqs_queue_expansion() {
13541 let mut app = test_app();
13542 app.current_service = Service::SqsQueues;
13543 app.service_selected = true;
13544 app.sqs_state.queues.items = vec![SqsQueue {
13545 name: "my-queue".to_string(),
13546 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13547 queue_type: "Standard".to_string(),
13548 created_timestamp: "2023-01-01".to_string(),
13549 messages_available: "5".to_string(),
13550 messages_in_flight: "2".to_string(),
13551 encryption: "Enabled".to_string(),
13552 content_based_deduplication: "Disabled".to_string(),
13553 last_modified_timestamp: "2023-01-02".to_string(),
13554 visibility_timeout: "30".to_string(),
13555 message_retention_period: "345600".to_string(),
13556 maximum_message_size: "262144".to_string(),
13557 delivery_delay: "0".to_string(),
13558 receive_message_wait_time: "0".to_string(),
13559 high_throughput_fifo: "N/A".to_string(),
13560 deduplication_scope: "N/A".to_string(),
13561 fifo_throughput_limit: "N/A".to_string(),
13562 dead_letter_queue: "-".to_string(),
13563 messages_delayed: "0".to_string(),
13564 redrive_allow_policy: "-".to_string(),
13565 redrive_policy: "".to_string(),
13566 redrive_task_id: "-".to_string(),
13567 redrive_task_start_time: "-".to_string(),
13568 redrive_task_status: "-".to_string(),
13569 redrive_task_percent: "-".to_string(),
13570 redrive_task_destination: "-".to_string(),
13571 }];
13572 app.sqs_state.queues.selected = 0;
13573
13574 assert_eq!(app.sqs_state.queues.expanded_item, None);
13575
13576 app.handle_action(Action::NextPane);
13578 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13579
13580 app.handle_action(Action::NextPane);
13582 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13583
13584 app.handle_action(Action::PrevPane);
13586 assert_eq!(app.sqs_state.queues.expanded_item, None);
13587
13588 app.handle_action(Action::PrevPane);
13590 assert_eq!(app.sqs_state.queues.expanded_item, None);
13591 }
13592
13593 #[test]
13594 fn test_sqs_column_toggle() {
13595 use crate::sqs::queue::Column as SqsColumn;
13596 let mut app = test_app();
13597 app.current_service = Service::SqsQueues;
13598 app.service_selected = true;
13599 app.mode = Mode::ColumnSelector;
13600
13601 app.sqs_visible_column_ids = SqsColumn::ids();
13603 let initial_count = app.sqs_visible_column_ids.len();
13604
13605 app.column_selector_index = 0;
13607 app.handle_action(Action::ToggleColumn);
13608
13609 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
13611 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13612
13613 app.handle_action(Action::ToggleColumn);
13615 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
13616 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13617 }
13618
13619 #[test]
13620 fn test_sqs_column_selector_navigation() {
13621 let mut app = test_app();
13622 app.current_service = Service::SqsQueues;
13623 app.service_selected = true;
13624 app.mode = Mode::ColumnSelector;
13625 app.column_selector_index = 0;
13626
13627 let max_index = app.sqs_column_ids.len() - 1;
13629
13630 for _ in 0..max_index {
13632 app.handle_action(Action::NextItem);
13633 }
13634 assert_eq!(app.column_selector_index, max_index);
13635
13636 for _ in 0..max_index {
13638 app.handle_action(Action::PrevItem);
13639 }
13640 assert_eq!(app.column_selector_index, 0);
13641 }
13642
13643 #[test]
13644 fn test_sqs_queue_selection() {
13645 let mut app = test_app();
13646 app.current_service = Service::SqsQueues;
13647 app.service_selected = true;
13648 app.mode = Mode::Normal;
13649 app.sqs_state.queues.items = vec![SqsQueue {
13650 name: "my-queue".to_string(),
13651 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13652 queue_type: "Standard".to_string(),
13653 created_timestamp: "2023-01-01".to_string(),
13654 messages_available: "5".to_string(),
13655 messages_in_flight: "2".to_string(),
13656 encryption: "Enabled".to_string(),
13657 content_based_deduplication: "Disabled".to_string(),
13658 last_modified_timestamp: "2023-01-02".to_string(),
13659 visibility_timeout: "30".to_string(),
13660 message_retention_period: "345600".to_string(),
13661 maximum_message_size: "262144".to_string(),
13662 delivery_delay: "0".to_string(),
13663 receive_message_wait_time: "0".to_string(),
13664 high_throughput_fifo: "N/A".to_string(),
13665 deduplication_scope: "N/A".to_string(),
13666 fifo_throughput_limit: "N/A".to_string(),
13667 dead_letter_queue: "-".to_string(),
13668 messages_delayed: "0".to_string(),
13669 redrive_allow_policy: "-".to_string(),
13670 redrive_policy: "".to_string(),
13671 redrive_task_id: "-".to_string(),
13672 redrive_task_start_time: "-".to_string(),
13673 redrive_task_status: "-".to_string(),
13674 redrive_task_percent: "-".to_string(),
13675 redrive_task_destination: "-".to_string(),
13676 }];
13677 app.sqs_state.queues.selected = 0;
13678
13679 assert_eq!(app.sqs_state.current_queue, None);
13680
13681 app.handle_action(Action::Select);
13683 assert_eq!(
13684 app.sqs_state.current_queue,
13685 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
13686 );
13687
13688 app.handle_action(Action::GoBack);
13690 assert_eq!(app.sqs_state.current_queue, None);
13691 }
13692
13693 #[test]
13694 fn test_sqs_lambda_triggers_expand_collapse() {
13695 let mut app = test_app();
13696 app.current_service = Service::SqsQueues;
13697 app.service_selected = true;
13698 app.sqs_state.current_queue =
13699 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13700 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13701 app.sqs_state.triggers.items = vec![LambdaTrigger {
13702 uuid: "test-uuid".to_string(),
13703 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13704 status: "Enabled".to_string(),
13705 last_modified: "2024-01-01T00:00:00Z".to_string(),
13706 }];
13707 app.sqs_state.triggers.selected = 0;
13708
13709 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13710
13711 app.handle_action(Action::NextPane);
13713 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13714
13715 app.handle_action(Action::PrevPane);
13717 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13718 }
13719
13720 #[test]
13721 fn test_sqs_lambda_triggers_expand_toggle() {
13722 let mut app = test_app();
13723 app.current_service = Service::SqsQueues;
13724 app.service_selected = true;
13725 app.sqs_state.current_queue =
13726 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13727 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13728 app.sqs_state.triggers.items = vec![LambdaTrigger {
13729 uuid: "test-uuid".to_string(),
13730 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13731 status: "Enabled".to_string(),
13732 last_modified: "2024-01-01T00:00:00Z".to_string(),
13733 }];
13734 app.sqs_state.triggers.selected = 0;
13735
13736 app.handle_action(Action::NextPane);
13738 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13739
13740 app.handle_action(Action::NextPane);
13742 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13743
13744 app.handle_action(Action::NextPane);
13746 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13747 }
13748
13749 #[test]
13750 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
13751 use crate::ui::sqs::filtered_lambda_triggers;
13752
13753 let mut app = test_app();
13754 app.current_service = Service::SqsQueues;
13755 app.service_selected = true;
13756 app.sqs_state.current_queue =
13757 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13758 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13759 app.sqs_state.triggers.items = vec![
13760 LambdaTrigger {
13761 uuid: "uuid-3".to_string(),
13762 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
13763 status: "Enabled".to_string(),
13764 last_modified: "2024-03-01T00:00:00Z".to_string(),
13765 },
13766 LambdaTrigger {
13767 uuid: "uuid-1".to_string(),
13768 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
13769 status: "Enabled".to_string(),
13770 last_modified: "2024-01-01T00:00:00Z".to_string(),
13771 },
13772 LambdaTrigger {
13773 uuid: "uuid-2".to_string(),
13774 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
13775 status: "Enabled".to_string(),
13776 last_modified: "2024-02-01T00:00:00Z".to_string(),
13777 },
13778 ];
13779
13780 let sorted = filtered_lambda_triggers(&app);
13781
13782 assert_eq!(sorted.len(), 3);
13784 assert_eq!(sorted[0].uuid, "uuid-1");
13785 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
13786 assert_eq!(sorted[1].uuid, "uuid-2");
13787 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
13788 assert_eq!(sorted[2].uuid, "uuid-3");
13789 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
13790 }
13791
13792 #[test]
13793 fn test_sqs_lambda_triggers_filter_input() {
13794 let mut app = test_app();
13795 app.current_service = Service::SqsQueues;
13796 app.service_selected = true;
13797 app.mode = Mode::FilterInput;
13798 app.sqs_state.current_queue =
13799 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13800 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13801 app.sqs_state.input_focus = InputFocus::Filter;
13802
13803 assert_eq!(app.sqs_state.triggers.filter, "");
13804
13805 app.handle_action(Action::FilterInput('t'));
13807 assert_eq!(app.sqs_state.triggers.filter, "t");
13808
13809 app.handle_action(Action::FilterInput('e'));
13810 assert_eq!(app.sqs_state.triggers.filter, "te");
13811
13812 app.handle_action(Action::FilterInput('s'));
13813 assert_eq!(app.sqs_state.triggers.filter, "tes");
13814
13815 app.handle_action(Action::FilterInput('t'));
13816 assert_eq!(app.sqs_state.triggers.filter, "test");
13817
13818 app.handle_action(Action::FilterBackspace);
13820 assert_eq!(app.sqs_state.triggers.filter, "tes");
13821 }
13822
13823 #[test]
13824 fn test_sqs_lambda_triggers_filter_applied() {
13825 use crate::ui::sqs::filtered_lambda_triggers;
13826
13827 let mut app = test_app();
13828 app.current_service = Service::SqsQueues;
13829 app.service_selected = true;
13830 app.sqs_state.current_queue =
13831 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13832 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13833 app.sqs_state.triggers.items = vec![
13834 LambdaTrigger {
13835 uuid: "uuid-1".to_string(),
13836 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
13837 status: "Enabled".to_string(),
13838 last_modified: "2024-01-01T00:00:00Z".to_string(),
13839 },
13840 LambdaTrigger {
13841 uuid: "uuid-2".to_string(),
13842 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
13843 status: "Enabled".to_string(),
13844 last_modified: "2024-02-01T00:00:00Z".to_string(),
13845 },
13846 LambdaTrigger {
13847 uuid: "uuid-3".to_string(),
13848 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
13849 status: "Enabled".to_string(),
13850 last_modified: "2024-03-01T00:00:00Z".to_string(),
13851 },
13852 ];
13853
13854 let filtered = filtered_lambda_triggers(&app);
13856 assert_eq!(filtered.len(), 3);
13857
13858 app.sqs_state.triggers.filter = "alpha".to_string();
13860 let filtered = filtered_lambda_triggers(&app);
13861 assert_eq!(filtered.len(), 1);
13862 assert_eq!(
13863 filtered[0].arn,
13864 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13865 );
13866
13867 app.sqs_state.triggers.filter = "test".to_string();
13869 let filtered = filtered_lambda_triggers(&app);
13870 assert_eq!(filtered.len(), 2);
13871 assert_eq!(
13872 filtered[0].arn,
13873 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13874 );
13875 assert_eq!(
13876 filtered[1].arn,
13877 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
13878 );
13879
13880 app.sqs_state.triggers.filter = "uuid-3".to_string();
13882 let filtered = filtered_lambda_triggers(&app);
13883 assert_eq!(filtered.len(), 1);
13884 assert_eq!(filtered[0].uuid, "uuid-3");
13885 }
13886
13887 #[test]
13888 fn test_sqs_triggers_navigation() {
13889 let mut app = test_app();
13890 app.service_selected = true;
13891 app.mode = Mode::Normal;
13892 app.current_service = Service::SqsQueues;
13893 app.sqs_state.current_queue = Some("test-queue".to_string());
13894 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13895 app.sqs_state.triggers.items = vec![
13896 LambdaTrigger {
13897 uuid: "1".to_string(),
13898 arn: "arn1".to_string(),
13899 status: "Enabled".to_string(),
13900 last_modified: "2024-01-01".to_string(),
13901 },
13902 LambdaTrigger {
13903 uuid: "2".to_string(),
13904 arn: "arn2".to_string(),
13905 status: "Enabled".to_string(),
13906 last_modified: "2024-01-02".to_string(),
13907 },
13908 ];
13909
13910 assert_eq!(app.sqs_state.triggers.selected, 0);
13911 app.next_item();
13912 assert_eq!(app.sqs_state.triggers.selected, 1);
13913 app.prev_item();
13914 assert_eq!(app.sqs_state.triggers.selected, 0);
13915 }
13916
13917 #[test]
13918 fn test_sqs_pipes_navigation() {
13919 let mut app = test_app();
13920 app.service_selected = true;
13921 app.mode = Mode::Normal;
13922 app.current_service = Service::SqsQueues;
13923 app.sqs_state.current_queue = Some("test-queue".to_string());
13924 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
13925 app.sqs_state.pipes.items = vec![
13926 EventBridgePipe {
13927 name: "pipe1".to_string(),
13928 status: "RUNNING".to_string(),
13929 target: "target1".to_string(),
13930 last_modified: "2024-01-01".to_string(),
13931 },
13932 EventBridgePipe {
13933 name: "pipe2".to_string(),
13934 status: "RUNNING".to_string(),
13935 target: "target2".to_string(),
13936 last_modified: "2024-01-02".to_string(),
13937 },
13938 ];
13939
13940 assert_eq!(app.sqs_state.pipes.selected, 0);
13941 app.next_item();
13942 assert_eq!(app.sqs_state.pipes.selected, 1);
13943 app.prev_item();
13944 assert_eq!(app.sqs_state.pipes.selected, 0);
13945 }
13946
13947 #[test]
13948 fn test_sqs_tags_navigation() {
13949 let mut app = test_app();
13950 app.service_selected = true;
13951 app.mode = Mode::Normal;
13952 app.current_service = Service::SqsQueues;
13953 app.sqs_state.current_queue = Some("test-queue".to_string());
13954 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
13955 app.sqs_state.tags.items = vec![
13956 SqsQueueTag {
13957 key: "Env".to_string(),
13958 value: "prod".to_string(),
13959 },
13960 SqsQueueTag {
13961 key: "Team".to_string(),
13962 value: "backend".to_string(),
13963 },
13964 ];
13965
13966 assert_eq!(app.sqs_state.tags.selected, 0);
13967 app.next_item();
13968 assert_eq!(app.sqs_state.tags.selected, 1);
13969 app.prev_item();
13970 assert_eq!(app.sqs_state.tags.selected, 0);
13971 }
13972
13973 #[test]
13974 fn test_sqs_queues_navigation() {
13975 let mut app = test_app();
13976 app.service_selected = true;
13977 app.mode = Mode::Normal;
13978 app.current_service = Service::SqsQueues;
13979 app.sqs_state.queues.items = vec![
13980 SqsQueue {
13981 name: "queue1".to_string(),
13982 url: "url1".to_string(),
13983 queue_type: "Standard".to_string(),
13984 created_timestamp: "".to_string(),
13985 messages_available: "0".to_string(),
13986 messages_in_flight: "0".to_string(),
13987 encryption: "Disabled".to_string(),
13988 content_based_deduplication: "Disabled".to_string(),
13989 last_modified_timestamp: "".to_string(),
13990 visibility_timeout: "".to_string(),
13991 message_retention_period: "".to_string(),
13992 maximum_message_size: "".to_string(),
13993 delivery_delay: "".to_string(),
13994 receive_message_wait_time: "".to_string(),
13995 high_throughput_fifo: "-".to_string(),
13996 deduplication_scope: "-".to_string(),
13997 fifo_throughput_limit: "-".to_string(),
13998 dead_letter_queue: "-".to_string(),
13999 messages_delayed: "0".to_string(),
14000 redrive_allow_policy: "-".to_string(),
14001 redrive_policy: "".to_string(),
14002 redrive_task_id: "-".to_string(),
14003 redrive_task_start_time: "-".to_string(),
14004 redrive_task_status: "-".to_string(),
14005 redrive_task_percent: "-".to_string(),
14006 redrive_task_destination: "-".to_string(),
14007 },
14008 SqsQueue {
14009 name: "queue2".to_string(),
14010 url: "url2".to_string(),
14011 queue_type: "Standard".to_string(),
14012 created_timestamp: "".to_string(),
14013 messages_available: "0".to_string(),
14014 messages_in_flight: "0".to_string(),
14015 encryption: "Disabled".to_string(),
14016 content_based_deduplication: "Disabled".to_string(),
14017 last_modified_timestamp: "".to_string(),
14018 visibility_timeout: "".to_string(),
14019 message_retention_period: "".to_string(),
14020 maximum_message_size: "".to_string(),
14021 delivery_delay: "".to_string(),
14022 receive_message_wait_time: "".to_string(),
14023 high_throughput_fifo: "-".to_string(),
14024 deduplication_scope: "-".to_string(),
14025 fifo_throughput_limit: "-".to_string(),
14026 dead_letter_queue: "-".to_string(),
14027 messages_delayed: "0".to_string(),
14028 redrive_allow_policy: "-".to_string(),
14029 redrive_policy: "".to_string(),
14030 redrive_task_id: "-".to_string(),
14031 redrive_task_start_time: "-".to_string(),
14032 redrive_task_status: "-".to_string(),
14033 redrive_task_percent: "-".to_string(),
14034 redrive_task_destination: "-".to_string(),
14035 },
14036 ];
14037
14038 assert_eq!(app.sqs_state.queues.selected, 0);
14039 app.next_item();
14040 assert_eq!(app.sqs_state.queues.selected, 1);
14041 app.prev_item();
14042 assert_eq!(app.sqs_state.queues.selected, 0);
14043 }
14044
14045 #[test]
14046 fn test_sqs_subscriptions_navigation() {
14047 let mut app = test_app();
14048 app.service_selected = true;
14049 app.mode = Mode::Normal;
14050 app.current_service = Service::SqsQueues;
14051 app.sqs_state.current_queue = Some("test-queue".to_string());
14052 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14053 app.sqs_state.subscriptions.items = vec![
14054 SnsSubscription {
14055 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
14056 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
14057 },
14058 SnsSubscription {
14059 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
14060 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
14061 },
14062 ];
14063
14064 assert_eq!(app.sqs_state.subscriptions.selected, 0);
14065 app.next_item();
14066 assert_eq!(app.sqs_state.subscriptions.selected, 1);
14067 app.prev_item();
14068 assert_eq!(app.sqs_state.subscriptions.selected, 0);
14069 }
14070
14071 #[test]
14072 fn test_sqs_subscription_region_dropdown_navigation() {
14073 let mut app = test_app();
14074 app.service_selected = true;
14075 app.mode = Mode::FilterInput;
14076 app.current_service = Service::SqsQueues;
14077 app.sqs_state.current_queue = Some("test-queue".to_string());
14078 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14079 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
14080
14081 assert_eq!(app.sqs_state.subscription_region_selected, 0);
14082 app.next_item();
14083 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14084 app.next_item();
14085 assert_eq!(app.sqs_state.subscription_region_selected, 2);
14086 app.prev_item();
14087 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14088 app.prev_item();
14089 assert_eq!(app.sqs_state.subscription_region_selected, 0);
14090 }
14091
14092 #[test]
14093 fn test_sqs_subscription_region_selection() {
14094 let mut app = test_app();
14095 app.service_selected = true;
14096 app.mode = Mode::FilterInput;
14097 app.current_service = Service::SqsQueues;
14098 app.sqs_state.current_queue = Some("test-queue".to_string());
14099 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14100 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
14101 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
14104 app.handle_action(Action::ApplyFilter);
14105 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
14106 assert_eq!(app.mode, Mode::Normal);
14107 }
14108
14109 #[test]
14110 fn test_sqs_subscription_region_change_resets_selection() {
14111 let mut app = test_app();
14112 app.service_selected = true;
14113 app.mode = Mode::FilterInput;
14114 app.current_service = Service::SqsQueues;
14115 app.sqs_state.current_queue = Some("test-queue".to_string());
14116 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14117 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
14118 app.sqs_state.subscription_region_selected = 0;
14119 app.sqs_state.subscriptions.selected = 5;
14120
14121 app.handle_action(Action::NextItem);
14122
14123 assert_eq!(app.sqs_state.subscription_region_selected, 1);
14124 assert_eq!(app.sqs_state.subscriptions.selected, 0);
14125 }
14126
14127 #[test]
14128 fn test_s3_object_filter_resets_selection() {
14129 let mut app = test_app();
14130 app.service_selected = true;
14131 app.current_service = Service::S3Buckets;
14132 app.s3_state.current_bucket = Some("test-bucket".to_string());
14133 app.s3_state.selected_row = 5;
14134 app.mode = Mode::FilterInput;
14135
14136 app.handle_action(Action::CloseMenu);
14137
14138 assert_eq!(app.s3_state.selected_row, 0);
14139 assert_eq!(app.mode, Mode::Normal);
14140 }
14141
14142 #[test]
14143 fn test_s3_bucket_filter_resets_selection() {
14144 let mut app = test_app();
14145 app.service_selected = true;
14146 app.current_service = Service::S3Buckets;
14147 app.s3_state.selected_row = 10;
14148 app.mode = Mode::FilterInput;
14149
14150 app.handle_action(Action::CloseMenu);
14151
14152 assert_eq!(app.s3_state.selected_row, 0);
14153 assert_eq!(app.mode, Mode::Normal);
14154 }
14155
14156 #[test]
14157 fn test_s3_selection_stays_in_bounds() {
14158 let mut app = test_app();
14159 app.service_selected = true;
14160 app.current_service = Service::S3Buckets;
14161 app.s3_state.selected_row = 0;
14162 app.s3_state.selected_object = 0;
14163
14164 app.prev_item();
14166
14167 assert_eq!(app.s3_state.selected_row, 0);
14169 assert_eq!(app.s3_state.selected_object, 0);
14170 }
14171
14172 #[test]
14173 fn test_cfn_filter_resets_selection() {
14174 let mut app = test_app();
14175 app.service_selected = true;
14176 app.current_service = Service::CloudFormationStacks;
14177 app.cfn_state.table.selected = 10;
14178 app.mode = Mode::FilterInput;
14179
14180 app.handle_action(Action::CloseMenu);
14181
14182 assert_eq!(app.cfn_state.table.selected, 0);
14183 assert_eq!(app.mode, Mode::Normal);
14184 }
14185
14186 #[test]
14187 fn test_lambda_filter_resets_selection() {
14188 let mut app = test_app();
14189 app.service_selected = true;
14190 app.current_service = Service::LambdaFunctions;
14191 app.lambda_state.table.selected = 8;
14192 app.mode = Mode::FilterInput;
14193
14194 app.handle_action(Action::CloseMenu);
14195
14196 assert_eq!(app.lambda_state.table.selected, 0);
14197 assert_eq!(app.mode, Mode::Normal);
14198 }
14199
14200 #[test]
14201 fn test_sqs_filter_resets_selection() {
14202 let mut app = test_app();
14203 app.service_selected = true;
14204 app.current_service = Service::SqsQueues;
14205 app.sqs_state.queues.selected = 7;
14206 app.mode = Mode::FilterInput;
14207
14208 app.handle_action(Action::CloseMenu);
14209
14210 assert_eq!(app.sqs_state.queues.selected, 0);
14211 assert_eq!(app.mode, Mode::Normal);
14212 }
14213
14214 #[test]
14215 fn test_sqs_queues_list_shows_preferences() {
14216 let mut app = test_app();
14217 app.service_selected = true;
14218 app.current_service = Service::SqsQueues;
14219 app.mode = Mode::Normal;
14220
14221 app.handle_action(Action::OpenColumnSelector);
14222
14223 assert_eq!(app.mode, Mode::ColumnSelector);
14224 }
14225
14226 #[test]
14227 fn test_sqs_queue_policies_tab_no_preferences() {
14228 let mut app = test_app();
14229 app.service_selected = true;
14230 app.current_service = Service::SqsQueues;
14231 app.sqs_state.current_queue = Some("test-queue".to_string());
14232 app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
14233 app.mode = Mode::Normal;
14234
14235 app.handle_action(Action::OpenColumnSelector);
14236
14237 assert_eq!(app.mode, Mode::Normal);
14238 }
14239
14240 #[test]
14241 fn test_sqs_sns_subscriptions_tab_shows_preferences() {
14242 let mut app = test_app();
14243 app.service_selected = true;
14244 app.current_service = Service::SqsQueues;
14245 app.sqs_state.current_queue = Some("test-queue".to_string());
14246 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14247 app.mode = Mode::Normal;
14248
14249 app.handle_action(Action::OpenColumnSelector);
14250
14251 assert_eq!(app.mode, Mode::ColumnSelector);
14252 }
14253
14254 #[test]
14255 fn test_sqs_monitoring_tab_no_preferences() {
14256 let mut app = test_app();
14257 app.service_selected = true;
14258 app.current_service = Service::SqsQueues;
14259 app.sqs_state.current_queue = Some("test-queue".to_string());
14260 app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
14261 app.mode = Mode::Normal;
14262
14263 app.handle_action(Action::OpenColumnSelector);
14264
14265 assert_eq!(app.mode, Mode::Normal);
14266 }
14267
14268 #[test]
14269 fn test_cfn_status_filter_change_resets_selection() {
14270 use crate::ui::cfn::STATUS_FILTER;
14271 let mut app = test_app();
14272 app.service_selected = true;
14273 app.current_service = Service::CloudFormationStacks;
14274 app.mode = Mode::FilterInput;
14275 app.cfn_state.input_focus = STATUS_FILTER;
14276 app.cfn_state.status_filter = CfnStatusFilter::All;
14277 app.cfn_state.table.items = vec![
14278 CfnStack {
14279 name: "stack1".to_string(),
14280 stack_id: "id1".to_string(),
14281 status: "CREATE_COMPLETE".to_string(),
14282 created_time: "2024-01-01".to_string(),
14283 updated_time: String::new(),
14284 deleted_time: String::new(),
14285 drift_status: String::new(),
14286 last_drift_check_time: String::new(),
14287 status_reason: String::new(),
14288 description: String::new(),
14289 detailed_status: String::new(),
14290 root_stack: String::new(),
14291 parent_stack: String::new(),
14292 termination_protection: false,
14293 iam_role: String::new(),
14294 tags: Vec::new(),
14295 stack_policy: String::new(),
14296 rollback_monitoring_time: String::new(),
14297 rollback_alarms: Vec::new(),
14298 notification_arns: Vec::new(),
14299 },
14300 CfnStack {
14301 name: "stack2".to_string(),
14302 stack_id: "id2".to_string(),
14303 status: "UPDATE_IN_PROGRESS".to_string(),
14304 created_time: "2024-01-02".to_string(),
14305 updated_time: String::new(),
14306 deleted_time: String::new(),
14307 drift_status: String::new(),
14308 last_drift_check_time: String::new(),
14309 status_reason: String::new(),
14310 description: String::new(),
14311 detailed_status: String::new(),
14312 root_stack: String::new(),
14313 parent_stack: String::new(),
14314 termination_protection: false,
14315 iam_role: String::new(),
14316 tags: Vec::new(),
14317 stack_policy: String::new(),
14318 rollback_monitoring_time: String::new(),
14319 rollback_alarms: Vec::new(),
14320 notification_arns: Vec::new(),
14321 },
14322 ];
14323 app.cfn_state.table.selected = 1;
14324
14325 app.handle_action(Action::NextItem);
14326
14327 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
14328 assert_eq!(app.cfn_state.table.selected, 0);
14329 }
14330
14331 #[test]
14332 fn test_cfn_view_nested_toggle_resets_selection() {
14333 use crate::ui::cfn::VIEW_NESTED;
14334 let mut app = test_app();
14335 app.service_selected = true;
14336 app.current_service = Service::CloudFormationStacks;
14337 app.mode = Mode::FilterInput;
14338 app.cfn_state.input_focus = VIEW_NESTED;
14339 app.cfn_state.view_nested = false;
14340 app.cfn_state.table.items = vec![CfnStack {
14341 name: "stack1".to_string(),
14342 stack_id: "id1".to_string(),
14343 status: "CREATE_COMPLETE".to_string(),
14344 created_time: "2024-01-01".to_string(),
14345 updated_time: String::new(),
14346 deleted_time: String::new(),
14347 drift_status: String::new(),
14348 last_drift_check_time: String::new(),
14349 status_reason: String::new(),
14350 description: String::new(),
14351 detailed_status: String::new(),
14352 root_stack: String::new(),
14353 parent_stack: String::new(),
14354 termination_protection: false,
14355 iam_role: String::new(),
14356 tags: Vec::new(),
14357 stack_policy: String::new(),
14358 rollback_monitoring_time: String::new(),
14359 rollback_alarms: Vec::new(),
14360 notification_arns: Vec::new(),
14361 }];
14362 app.cfn_state.table.selected = 5;
14363
14364 app.handle_action(Action::ToggleFilterCheckbox);
14365
14366 assert!(app.cfn_state.view_nested, "view_nested must be toggled on");
14367 assert_eq!(app.cfn_state.table.selected, 0, "selection must reset");
14368 assert!(
14372 app.cfn_state.table.loading,
14373 "loading must be set to trigger reload in main.rs"
14374 );
14375 assert!(
14376 app.cfn_state.table.loading,
14377 "loading must be set to trigger reload in main.rs"
14378 );
14379 }
14380
14381 #[test]
14382 fn test_cfn_view_nested_toggle_off_also_triggers_reload() {
14383 use crate::ui::cfn::VIEW_NESTED;
14385 let mut app = test_app();
14386 app.service_selected = true;
14387 app.current_service = Service::CloudFormationStacks;
14388 app.mode = Mode::FilterInput;
14389 app.cfn_state.input_focus = VIEW_NESTED;
14390 app.cfn_state.view_nested = true; app.cfn_state.table.items = vec![CfnStack {
14392 name: "nested-stack".to_string(),
14393 stack_id: "id2".to_string(),
14394 status: "CREATE_COMPLETE".to_string(),
14395 created_time: "2024-01-01".to_string(),
14396 updated_time: String::new(),
14397 deleted_time: String::new(),
14398 drift_status: String::new(),
14399 last_drift_check_time: String::new(),
14400 status_reason: String::new(),
14401 description: String::new(),
14402 detailed_status: String::new(),
14403 root_stack: String::new(),
14404 parent_stack: String::new(),
14405 termination_protection: false,
14406 iam_role: String::new(),
14407 tags: Vec::new(),
14408 stack_policy: String::new(),
14409 rollback_monitoring_time: String::new(),
14410 rollback_alarms: Vec::new(),
14411 notification_arns: Vec::new(),
14412 }];
14413
14414 app.handle_action(Action::ToggleFilterCheckbox);
14415
14416 assert!(
14417 !app.cfn_state.view_nested,
14418 "view_nested must be toggled off"
14419 );
14420 assert!(
14422 !app.cfn_state.table.items.is_empty(),
14423 "items must stay visible during reload (no flash)"
14424 );
14425 assert!(
14426 app.cfn_state.table.loading,
14427 "loading must be set to trigger reload"
14428 );
14429 }
14430
14431 #[test]
14432 fn test_cfn_view_nested_hierarchical_shows_children_when_expanded() {
14433 use crate::ui::cfn::filtered_cloudformation_stacks;
14436
14437 let mut app = test_app();
14438 app.current_service = Service::CloudFormationStacks;
14439 app.service_selected = true;
14440 app.cfn_state.view_nested = true;
14441
14442 let parent_id = "arn:aws:cloudformation:us-east-1:123:stack/parent/abc123".to_string();
14443 let child_id =
14444 "arn:aws:cloudformation:us-east-1:123:stack/parent-nested/def456".to_string();
14445
14446 app.cfn_state.table.items = vec![
14447 CfnStack {
14448 name: "parent-stack".to_string(),
14449 stack_id: parent_id.clone(),
14450 status: "CREATE_COMPLETE".to_string(),
14451 parent_stack: String::new(), root_stack: parent_id.clone(),
14453 created_time: "2024-01-02".to_string(),
14454 ..Default::default()
14455 },
14456 CfnStack {
14457 name: "child-stack".to_string(),
14458 stack_id: child_id.clone(),
14459 status: "CREATE_COMPLETE".to_string(),
14460 parent_stack: parent_id.clone(), root_stack: parent_id.clone(),
14462 created_time: "2024-01-01".to_string(),
14463 ..Default::default()
14464 },
14465 ];
14466
14467 let filtered = filtered_cloudformation_stacks(&app);
14469 assert_eq!(
14470 filtered.len(),
14471 2,
14472 "Root + child shown when view_nested=true"
14473 );
14474 assert_eq!(filtered[0].name, "parent-stack", "Root first");
14475 assert_eq!(filtered[1].name, "child-stack", "Child after root");
14476 }
14477
14478 #[test]
14479 fn test_cfn_template_scroll_up() {
14480 let mut app = test_app();
14481 app.service_selected = true;
14482 app.current_service = Service::CloudFormationStacks;
14483 app.cfn_state.current_stack = Some("test-stack".to_string());
14484 app.cfn_state.detail_tab = CfnDetailTab::Template;
14485 app.cfn_state.template_scroll = 20;
14486
14487 app.page_up();
14488
14489 assert_eq!(app.cfn_state.template_scroll, 10);
14490 }
14491
14492 #[test]
14493 fn test_cfn_template_scroll_down() {
14494 let mut app = test_app();
14495 app.service_selected = true;
14496 app.current_service = Service::CloudFormationStacks;
14497 app.cfn_state.current_stack = Some("test-stack".to_string());
14498 app.cfn_state.detail_tab = CfnDetailTab::Template;
14499 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
14500 app.cfn_state.template_scroll = 0;
14501
14502 app.page_down();
14503
14504 assert_eq!(app.cfn_state.template_scroll, 10);
14505 }
14506
14507 #[test]
14508 fn test_cfn_template_scroll_down_respects_max() {
14509 let mut app = test_app();
14510 app.service_selected = true;
14511 app.current_service = Service::CloudFormationStacks;
14512 app.cfn_state.current_stack = Some("test-stack".to_string());
14513 app.cfn_state.detail_tab = CfnDetailTab::Template;
14514 app.cfn_state.template_body = "line1\nline2\nline3".to_string();
14515 app.cfn_state.template_scroll = 0;
14516
14517 app.page_down();
14518
14519 assert_eq!(app.cfn_state.template_scroll, 2);
14521 }
14522
14523 #[test]
14524 fn test_cfn_template_arrow_up() {
14525 let mut app = test_app();
14526 app.service_selected = true;
14527 app.current_service = Service::CloudFormationStacks;
14528 app.mode = Mode::Normal;
14529 app.cfn_state.current_stack = Some("test-stack".to_string());
14530 app.cfn_state.detail_tab = CfnDetailTab::Template;
14531 app.cfn_state.template_scroll = 5;
14532
14533 app.prev_item();
14534
14535 assert_eq!(app.cfn_state.template_scroll, 4);
14536 }
14537
14538 #[test]
14539 fn test_cfn_template_arrow_down() {
14540 let mut app = test_app();
14541 app.service_selected = true;
14542 app.current_service = Service::CloudFormationStacks;
14543 app.mode = Mode::Normal;
14544 app.cfn_state.current_stack = Some("test-stack".to_string());
14545 app.cfn_state.detail_tab = CfnDetailTab::Template;
14546 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
14547 app.cfn_state.template_scroll = 2;
14548
14549 app.next_item();
14550
14551 assert_eq!(app.cfn_state.template_scroll, 3);
14552 }
14553
14554 #[test]
14555 fn test_cfn_template_arrow_down_respects_max() {
14556 let mut app = test_app();
14557 app.service_selected = true;
14558 app.current_service = Service::CloudFormationStacks;
14559 app.mode = Mode::Normal;
14560 app.cfn_state.current_stack = Some("test-stack".to_string());
14561 app.cfn_state.detail_tab = CfnDetailTab::Template;
14562 app.cfn_state.template_body = "line1\nline2".to_string();
14563 app.cfn_state.template_scroll = 1;
14564
14565 app.next_item();
14566
14567 assert_eq!(app.cfn_state.template_scroll, 1);
14569 }
14570
14571 #[test]
14572 fn test_cfn_yank_on_template_tab_copies_template_body() {
14573 let mut app = test_app();
14576 app.current_service = Service::CloudFormationStacks;
14577 app.cfn_state.current_stack = Some("my-stack".to_string());
14578 app.cfn_state.detail_tab = CfnDetailTab::Template;
14579 app.cfn_state.template_body = "AWSTemplateFormatVersion: '2010-09-09'".to_string();
14580 assert!(!app.cfn_state.template_body.is_empty());
14584 crate::cfn::actions::yank(&app); }
14586
14587 #[test]
14588 fn test_cfn_change_sets_console_url_includes_events_view_graph() {
14589 use crate::cfn::console_url_stack_detail_with_tab;
14590 let url = console_url_stack_detail_with_tab(
14591 "us-east-1",
14592 "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
14593 &CfnDetailTab::ChangeSets,
14594 );
14595 assert!(
14596 url.contains("eventsView=graph"),
14597 "ChangeSets URL must include eventsView=graph, got: {url}"
14598 );
14599 assert!(url.contains("changesets"), "URL must contain 'changesets'");
14600 }
14601
14602 #[test]
14603 fn test_cfn_other_tabs_console_url_no_events_view_graph() {
14604 use crate::cfn::console_url_stack_detail_with_tab;
14605 for tab in [
14606 CfnDetailTab::StackInfo,
14607 CfnDetailTab::Events,
14608 CfnDetailTab::Resources,
14609 ] {
14610 let url = console_url_stack_detail_with_tab(
14611 "us-east-1",
14612 "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
14613 &tab,
14614 );
14615 assert!(
14616 !url.contains("eventsView=graph"),
14617 "{tab:?} URL must NOT include eventsView=graph, got: {url}"
14618 );
14619 }
14620 }
14621
14622 #[test]
14623 fn test_cfn_change_sets_expand_collapse() {
14624 let mut app = test_app();
14625 app.current_service = Service::CloudFormationStacks;
14626 app.service_selected = true;
14627 app.cfn_state.current_stack = Some("my-stack".to_string());
14628 app.cfn_state.detail_tab = CfnDetailTab::ChangeSets;
14629 app.cfn_state.change_sets.items = vec![rusticity_core::cfn::StackChangeSet {
14630 name: "cs-001".to_string(),
14631 change_set_id: "id-001".to_string(),
14632 created_time: String::new(),
14633 status: "CREATE_COMPLETE".to_string(),
14634 description: String::new(),
14635 root_change_set_id: String::new(),
14636 parent_change_set_id: String::new(),
14637 }];
14638 app.cfn_state.change_sets.selected = 0;
14639
14640 assert_eq!(app.cfn_state.change_sets.expanded_item, None);
14641 app.handle_action(Action::ExpandRow);
14642 assert_eq!(
14643 app.cfn_state.change_sets.expanded_item,
14644 Some(0),
14645 "Enter must expand change set row"
14646 );
14647 app.handle_action(Action::CollapseRow);
14648 assert_eq!(
14649 app.cfn_state.change_sets.expanded_item, None,
14650 "Left arrow must collapse change set row"
14651 );
14652 }
14653
14654 #[test]
14655 fn test_cfn_events_toggle_column_hides_visible_column() {
14656 let mut app = test_app();
14659 app.current_service = Service::CloudFormationStacks;
14660 app.mode = Mode::ColumnSelector;
14661 app.cfn_state.current_stack = Some("my-stack".to_string());
14662 app.cfn_state.detail_tab = CfnDetailTab::Events;
14663
14664 let first_col = app.cfn_event_column_ids[0];
14666 assert!(
14667 app.cfn_event_visible_column_ids.contains(&first_col),
14668 "First column must be visible before toggle"
14669 );
14670
14671 app.column_selector_index = 1; app.handle_action(Action::ToggleColumn);
14673
14674 assert!(
14675 !app.cfn_event_visible_column_ids.contains(&first_col),
14676 "ToggleColumn must hide the first Events column"
14677 );
14678
14679 app.handle_action(Action::ToggleColumn);
14681 assert!(
14682 app.cfn_event_visible_column_ids.contains(&first_col),
14683 "ToggleColumn again must re-show the column"
14684 );
14685 }
14686
14687 #[test]
14688 fn test_cfn_events_toggle_column_keeps_at_least_one_visible() {
14689 let mut app = test_app();
14691 app.current_service = Service::CloudFormationStacks;
14692 app.mode = Mode::ColumnSelector;
14693 app.cfn_state.current_stack = Some("my-stack".to_string());
14694 app.cfn_state.detail_tab = CfnDetailTab::Events;
14695
14696 app.cfn_event_visible_column_ids = vec![app.cfn_event_column_ids[0]];
14698 app.column_selector_index = 1;
14699
14700 app.handle_action(Action::ToggleColumn);
14701
14702 assert!(
14704 !app.cfn_event_visible_column_ids.is_empty(),
14705 "At least one column must remain visible"
14706 );
14707 }
14708
14709 #[test]
14710 fn test_space_menu_o_opens_service_picker() {
14711 let mut app = test_app();
14713 app.handle_action(Action::OpenSpaceMenu);
14714 assert_eq!(app.mode, Mode::SpaceMenu);
14715
14716 app.handle_action(Action::OpenServicePicker);
14717 assert_eq!(
14718 app.mode,
14719 Mode::ServicePicker,
14720 "'o' from SpaceMenu must open ServicePicker"
14721 );
14722 }
14723
14724 #[test]
14725 fn test_space_menu_c_closes_service() {
14726 let mut app = test_app();
14727 app.current_service = Service::CloudFormationStacks;
14728 app.service_selected = true;
14729 app.tabs.push(Tab {
14730 service: Service::CloudFormationStacks,
14731 title: "CFN".to_string(),
14732 breadcrumb: "CloudFormation".to_string(),
14733 });
14734 app.current_tab = 0;
14735
14736 app.handle_action(Action::OpenSpaceMenu);
14737 app.handle_action(Action::CloseService);
14738 assert_ne!(
14740 app.mode,
14741 Mode::SpaceMenu,
14742 "CloseService must exit SpaceMenu"
14743 );
14744 }
14745}
14746
14747#[cfg(test)]
14748mod lambda_version_tab_tests {
14749 use super::*;
14750 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
14751 use test_helpers::*;
14752
14753 #[test]
14754 fn test_lambda_version_tab_cycling_next() {
14755 let mut app = test_app();
14756 app.current_service = Service::LambdaFunctions;
14757 app.lambda_state.current_function = Some("test-function".to_string());
14758 app.lambda_state.current_version = Some("1".to_string());
14759 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14760
14761 app.handle_action(Action::NextDetailTab);
14763 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14764 assert!(app.lambda_state.metrics_loading);
14765
14766 app.lambda_state.metrics_loading = false;
14768 app.handle_action(Action::NextDetailTab);
14769 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14770
14771 app.handle_action(Action::NextDetailTab);
14773 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14774 }
14775
14776 #[test]
14777 fn test_lambda_version_tab_cycling_prev() {
14778 let mut app = test_app();
14779 app.current_service = Service::LambdaFunctions;
14780 app.lambda_state.current_function = Some("test-function".to_string());
14781 app.lambda_state.current_version = Some("1".to_string());
14782 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14783
14784 app.handle_action(Action::PrevDetailTab);
14786 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14787
14788 app.handle_action(Action::PrevDetailTab);
14790 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14791 assert!(app.lambda_state.metrics_loading);
14792
14793 app.lambda_state.metrics_loading = false;
14795 app.handle_action(Action::PrevDetailTab);
14796 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14797 }
14798
14799 #[test]
14800 fn test_lambda_version_monitor_clears_metrics() {
14801 let mut app = test_app();
14802 app.current_service = Service::LambdaFunctions;
14803 app.lambda_state.current_function = Some("test-function".to_string());
14804 app.lambda_state.current_version = Some("1".to_string());
14805 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14806
14807 app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
14809 app.lambda_state.monitoring_scroll = 5;
14810
14811 app.handle_action(Action::NextDetailTab);
14813
14814 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14815 assert!(app.lambda_state.metrics_loading);
14816 assert_eq!(app.lambda_state.monitoring_scroll, 0);
14817 assert!(app.lambda_state.metric_data_invocations.is_empty());
14818 }
14819
14820 #[test]
14821 fn test_cfn_parameters_expand_collapse() {
14822 let mut app = test_app();
14823 app.current_service = Service::CloudFormationStacks;
14824 app.service_selected = true;
14825 app.cfn_state.current_stack = Some("test-stack".to_string());
14826 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14827 app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
14828 key: "Param1".to_string(),
14829 value: "Value1".to_string(),
14830 resolved_value: "Resolved1".to_string(),
14831 }];
14832 app.cfn_state.parameters.reset();
14833
14834 assert_eq!(app.cfn_state.parameters.expanded_item, None);
14835
14836 app.handle_action(Action::NextPane);
14838 assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
14839
14840 app.handle_action(Action::PrevPane);
14842 assert_eq!(app.cfn_state.parameters.expanded_item, None);
14843 }
14844
14845 #[test]
14846 fn test_cfn_parameters_filter_resets_selection() {
14847 let mut app = test_app();
14848 app.current_service = Service::CloudFormationStacks;
14849 app.service_selected = true;
14850 app.cfn_state.current_stack = Some("test-stack".to_string());
14851 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14852 app.cfn_state.parameters.items = vec![
14853 rusticity_core::cfn::StackParameter {
14854 key: "DatabaseName".to_string(),
14855 value: "mydb".to_string(),
14856 resolved_value: "mydb".to_string(),
14857 },
14858 rusticity_core::cfn::StackParameter {
14859 key: "InstanceType".to_string(),
14860 value: "t2.micro".to_string(),
14861 resolved_value: "t2.micro".to_string(),
14862 },
14863 rusticity_core::cfn::StackParameter {
14864 key: "Environment".to_string(),
14865 value: "production".to_string(),
14866 resolved_value: "production".to_string(),
14867 },
14868 ];
14869 app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
14871 app.cfn_state.parameters_input_focus = InputFocus::Filter;
14872
14873 app.handle_action(Action::FilterInput('D'));
14875 assert_eq!(app.cfn_state.parameters.selected, 0);
14876 assert_eq!(app.cfn_state.parameters.filter, "D");
14877
14878 app.cfn_state.parameters.selected = 1;
14880
14881 app.handle_action(Action::FilterInput('a'));
14883 assert_eq!(app.cfn_state.parameters.selected, 0);
14884 assert_eq!(app.cfn_state.parameters.filter, "Da");
14885
14886 app.cfn_state.parameters.selected = 1;
14888
14889 app.handle_action(Action::FilterBackspace);
14891 assert_eq!(app.cfn_state.parameters.selected, 0);
14892 assert_eq!(app.cfn_state.parameters.filter, "D");
14893 }
14894
14895 #[test]
14896 fn test_cfn_template_tab_no_preferences() {
14897 let mut app = test_app();
14898 app.current_service = Service::CloudFormationStacks;
14899 app.service_selected = true;
14900 app.cfn_state.current_stack = Some("test-stack".to_string());
14901 app.cfn_state.detail_tab = CfnDetailTab::Template;
14902 app.mode = Mode::Normal;
14903
14904 app.handle_action(Action::OpenColumnSelector);
14906 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::GitSync;
14910 app.handle_action(Action::OpenColumnSelector);
14911 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14915 app.handle_action(Action::OpenColumnSelector);
14916 assert_eq!(app.mode, Mode::ColumnSelector); app.mode = Mode::Normal;
14920 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
14921 app.handle_action(Action::OpenColumnSelector);
14922 assert_eq!(app.mode, Mode::ColumnSelector); }
14924
14925 #[test]
14926 fn test_iam_user_groups_tab_shows_preferences() {
14927 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14928 app.current_service = Service::IamUsers;
14929 app.service_selected = true;
14930 app.mode = Mode::Normal;
14931 app.iam_state.current_user = Some("test-user".to_string());
14932 app.iam_state.user_tab = UserTab::Groups;
14933
14934 app.handle_action(Action::OpenColumnSelector);
14936 assert_eq!(app.mode, Mode::ColumnSelector);
14937 }
14938
14939 #[test]
14940 fn test_iam_user_tags_tab_shows_preferences() {
14941 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14942 app.current_service = Service::IamUsers;
14943 app.service_selected = true;
14944 app.mode = Mode::Normal;
14945 app.iam_state.current_user = Some("test-user".to_string());
14946 app.iam_state.user_tab = UserTab::Tags;
14947
14948 app.handle_action(Action::OpenColumnSelector);
14950 assert_eq!(app.mode, Mode::ColumnSelector);
14951 }
14952
14953 #[test]
14954 fn test_iam_user_last_accessed_tab_shows_preferences() {
14955 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14956 app.current_service = Service::IamUsers;
14957 app.service_selected = true;
14958 app.mode = Mode::Normal;
14959 app.iam_state.current_user = Some("test-user".to_string());
14960 app.iam_state.user_tab = UserTab::LastAccessed;
14961
14962 app.handle_action(Action::OpenColumnSelector);
14964 assert_eq!(app.mode, Mode::ColumnSelector);
14965 }
14966
14967 #[test]
14968 fn test_iam_user_security_credentials_tab_no_preferences() {
14969 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14970 app.current_service = Service::IamUsers;
14971 app.service_selected = true;
14972 app.mode = Mode::Normal;
14973 app.iam_state.current_user = Some("test-user".to_string());
14974 app.iam_state.user_tab = UserTab::SecurityCredentials;
14975
14976 app.handle_action(Action::OpenColumnSelector);
14978 assert_eq!(app.mode, Mode::Normal);
14979 }
14980
14981 #[test]
14982 fn test_iam_user_tabs_without_column_preferences() {
14983 let mut app = test_app();
14984 app.current_service = Service::IamUsers;
14985 app.service_selected = true;
14986 app.iam_state.current_user = Some("test-user".to_string());
14987 app.mode = Mode::Normal;
14988
14989 app.iam_state.user_tab = UserTab::Groups;
14991 app.handle_action(Action::OpenColumnSelector);
14992 assert_eq!(app.mode, Mode::ColumnSelector);
14993 app.mode = Mode::Normal;
14994
14995 app.iam_state.user_tab = UserTab::Tags;
14997 app.handle_action(Action::OpenColumnSelector);
14998 assert_eq!(app.mode, Mode::ColumnSelector);
14999 app.mode = Mode::Normal;
15000
15001 app.iam_state.user_tab = UserTab::SecurityCredentials;
15003 app.handle_action(Action::OpenColumnSelector);
15004 assert_eq!(app.mode, Mode::Normal);
15005
15006 app.iam_state.user_tab = UserTab::LastAccessed;
15008 app.handle_action(Action::OpenColumnSelector);
15009 assert_eq!(app.mode, Mode::ColumnSelector);
15010 app.mode = Mode::Normal;
15011
15012 app.iam_state.user_tab = UserTab::Permissions;
15014 app.handle_action(Action::OpenColumnSelector);
15015 assert_eq!(app.mode, Mode::ColumnSelector);
15016
15017 app.mode = Mode::Normal;
15019 app.iam_state.current_user = None;
15020 app.handle_action(Action::OpenColumnSelector);
15021 assert_eq!(app.mode, Mode::ColumnSelector);
15022 }
15023
15024 #[test]
15025 fn test_iam_role_policies_dropdown_cycling() {
15026 let mut app = test_app();
15027 app.current_service = Service::IamRoles;
15028 app.service_selected = true;
15029 app.iam_state.current_role = Some("test-role".to_string());
15030 app.iam_state.role_tab = RoleTab::Permissions;
15031 app.mode = Mode::FilterInput;
15032 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
15033 app.iam_state.policy_type_filter = "All types".to_string();
15034
15035 app.next_item();
15037 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15038 app.next_item();
15039 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15040 app.next_item();
15041 assert_eq!(app.iam_state.policy_type_filter, "All types");
15042
15043 app.prev_item();
15045 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15046 app.prev_item();
15047 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15048 app.prev_item();
15049 assert_eq!(app.iam_state.policy_type_filter, "All types");
15050 }
15051
15052 #[test]
15053 fn test_iam_user_policies_dropdown_cycling() {
15054 let mut app = test_app();
15055 app.current_service = Service::IamUsers;
15056 app.service_selected = true;
15057 app.iam_state.current_user = Some("test-user".to_string());
15058 app.iam_state.user_tab = UserTab::Permissions;
15059 app.mode = Mode::FilterInput;
15060 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
15061 app.iam_state.policy_type_filter = "All types".to_string();
15062
15063 app.next_item();
15065 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15066 app.next_item();
15067 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15068 app.next_item();
15069 assert_eq!(app.iam_state.policy_type_filter, "All types");
15070
15071 app.prev_item();
15073 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15074 app.prev_item();
15075 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15076 app.prev_item();
15077 assert_eq!(app.iam_state.policy_type_filter, "All types");
15078 }
15079
15080 #[test]
15081 fn test_iam_role_tabs_without_column_preferences() {
15082 let mut app = test_app();
15083 app.current_service = Service::IamRoles;
15084 app.service_selected = true;
15085 app.iam_state.current_role = Some("test-role".to_string());
15086 app.mode = Mode::Normal;
15087
15088 app.iam_state.role_tab = RoleTab::TrustRelationships;
15090 app.handle_action(Action::OpenColumnSelector);
15091 assert_eq!(app.mode, Mode::Normal);
15092
15093 app.iam_state.role_tab = RoleTab::RevokeSessions;
15095 app.handle_action(Action::OpenColumnSelector);
15096 assert_eq!(app.mode, Mode::Normal);
15097
15098 app.iam_state.role_tab = RoleTab::LastAccessed;
15100 app.handle_action(Action::OpenColumnSelector);
15101 assert_eq!(app.mode, Mode::ColumnSelector);
15102
15103 app.mode = Mode::Normal;
15105 app.iam_state.role_tab = RoleTab::Permissions;
15106 app.handle_action(Action::OpenColumnSelector);
15107 assert_eq!(app.mode, Mode::ColumnSelector);
15108
15109 app.mode = Mode::Normal;
15111 app.iam_state.role_tab = RoleTab::Tags;
15112 app.handle_action(Action::OpenColumnSelector);
15113 assert_eq!(app.mode, Mode::ColumnSelector);
15114
15115 app.mode = Mode::Normal;
15117 app.iam_state.current_role = None;
15118 app.handle_action(Action::OpenColumnSelector);
15119 assert_eq!(app.mode, Mode::ColumnSelector);
15120 }
15121
15122 #[test]
15123 fn test_iam_role_tags_tab_cycling() {
15124 let mut app = test_app();
15125 app.current_service = Service::IamRoles;
15126 app.service_selected = true;
15127 app.iam_state.current_role = Some("test-role".to_string());
15128 app.iam_state.role_tab = RoleTab::Tags;
15129 app.mode = Mode::ColumnSelector;
15130 app.column_selector_index = 0;
15131
15132 app.handle_action(Action::NextPreferences);
15134 assert_eq!(app.column_selector_index, 4);
15135
15136 app.handle_action(Action::NextPreferences);
15138 assert_eq!(app.column_selector_index, 0);
15139
15140 app.handle_action(Action::PrevPreferences);
15142 assert_eq!(app.column_selector_index, 4);
15143
15144 app.handle_action(Action::PrevPreferences);
15146 assert_eq!(app.column_selector_index, 0);
15147 }
15148
15149 #[test]
15150 fn test_cfn_outputs_expand_collapse() {
15151 let mut app = test_app();
15152 app.current_service = Service::CloudFormationStacks;
15153 app.service_selected = true;
15154 app.cfn_state.current_stack = Some("test-stack".to_string());
15155 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
15156 app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
15157 key: "Output1".to_string(),
15158 value: "Value1".to_string(),
15159 description: "Description1".to_string(),
15160 export_name: "Export1".to_string(),
15161 }];
15162 app.cfn_state.outputs.reset();
15163
15164 assert_eq!(app.cfn_state.outputs.expanded_item, None);
15165
15166 app.handle_action(Action::NextPane);
15168 assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
15169
15170 app.handle_action(Action::PrevPane);
15172 assert_eq!(app.cfn_state.outputs.expanded_item, None);
15173 }
15174
15175 #[test]
15176 fn test_cfn_outputs_filter_resets_selection() {
15177 let mut app = test_app();
15178 app.current_service = Service::CloudFormationStacks;
15179 app.service_selected = true;
15180 app.cfn_state.current_stack = Some("test-stack".to_string());
15181 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
15182 app.cfn_state.outputs.items = vec![
15183 rusticity_core::cfn::StackOutput {
15184 key: "ApiUrl".to_string(),
15185 value: "https://api.example.com".to_string(),
15186 description: "API endpoint".to_string(),
15187 export_name: "MyApiUrl".to_string(),
15188 },
15189 rusticity_core::cfn::StackOutput {
15190 key: "BucketName".to_string(),
15191 value: "my-bucket".to_string(),
15192 description: "S3 bucket".to_string(),
15193 export_name: "MyBucket".to_string(),
15194 },
15195 ];
15196 app.cfn_state.outputs.reset();
15197 app.cfn_state.outputs.selected = 1;
15198
15199 app.handle_action(Action::StartFilter);
15201 assert_eq!(app.mode, Mode::FilterInput);
15202
15203 app.handle_action(Action::FilterInput('A'));
15205 assert_eq!(app.cfn_state.outputs.selected, 0);
15206 assert_eq!(app.cfn_state.outputs.filter, "A");
15207
15208 app.cfn_state.outputs.selected = 1;
15210 app.handle_action(Action::FilterInput('p'));
15211 assert_eq!(app.cfn_state.outputs.selected, 0);
15212
15213 app.cfn_state.outputs.selected = 1;
15215 app.handle_action(Action::FilterBackspace);
15216 assert_eq!(app.cfn_state.outputs.selected, 0);
15217 }
15218
15219 #[test]
15220 fn test_ec2_service_in_picker() {
15221 let app = test_app();
15222 assert!(app.service_picker.services.contains(&"EC2 › Instances"));
15223 }
15224
15225 #[test]
15226 fn test_ec2_state_filter_cycles() {
15227 let mut app = test_app();
15228 app.current_service = Service::Ec2Instances;
15229 app.service_selected = true;
15230 app.mode = Mode::FilterInput;
15231 app.ec2_state.input_focus = EC2_STATE_FILTER;
15232
15233 let initial = app.ec2_state.state_filter;
15234 assert_eq!(initial, Ec2StateFilter::AllStates);
15235
15236 app.handle_action(Action::ToggleFilterCheckbox);
15238 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15239
15240 app.handle_action(Action::ToggleFilterCheckbox);
15241 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15242
15243 app.handle_action(Action::ToggleFilterCheckbox);
15244 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
15245
15246 app.handle_action(Action::ToggleFilterCheckbox);
15247 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
15248
15249 app.handle_action(Action::ToggleFilterCheckbox);
15250 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
15251
15252 app.handle_action(Action::ToggleFilterCheckbox);
15253 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15254
15255 app.handle_action(Action::ToggleFilterCheckbox);
15256 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15257 }
15258
15259 #[test]
15260 fn test_ec2_filter_resets_table() {
15261 let mut app = test_app();
15262 app.current_service = Service::Ec2Instances;
15263 app.service_selected = true;
15264 app.mode = Mode::FilterInput;
15265 app.ec2_state.input_focus = EC2_STATE_FILTER;
15266 app.ec2_state.table.selected = 5;
15267
15268 app.handle_action(Action::ToggleFilterCheckbox);
15269 assert_eq!(app.ec2_state.table.selected, 0);
15270 }
15271
15272 #[test]
15273 fn test_ec2_columns_visible() {
15274 let app = test_app();
15275 assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
15278
15279 #[test]
15280 fn test_ec2_breadcrumbs() {
15281 let mut app = test_app();
15282 app.current_service = Service::Ec2Instances;
15283 app.service_selected = true;
15284 let breadcrumb = app.breadcrumbs();
15285 assert_eq!(breadcrumb, "EC2 > Instances");
15286 }
15287
15288 #[test]
15289 fn test_ec2_console_url() {
15290 let mut app = test_app();
15291 app.current_service = Service::Ec2Instances;
15292 app.service_selected = true;
15293 let url = app.get_console_url();
15294 assert!(url.contains("ec2"));
15295 assert!(url.contains("Instances"));
15296 }
15297
15298 #[test]
15299 fn test_ec2_filter_handling() {
15300 let mut app = test_app();
15301 app.current_service = Service::Ec2Instances;
15302 app.service_selected = true;
15303 app.mode = Mode::FilterInput;
15304
15305 app.handle_action(Action::FilterInput('t'));
15306 app.handle_action(Action::FilterInput('e'));
15307 app.handle_action(Action::FilterInput('s'));
15308 app.handle_action(Action::FilterInput('t'));
15309
15310 assert_eq!(app.ec2_state.table.filter, "test");
15311
15312 app.handle_action(Action::FilterBackspace);
15313 assert_eq!(app.ec2_state.table.filter, "tes");
15314 }
15315
15316 #[test]
15317 fn test_column_selector_page_down_ec2() {
15318 let mut app = test_app();
15319 app.current_service = Service::Ec2Instances;
15320 app.service_selected = true;
15321 app.mode = Mode::ColumnSelector;
15322 app.column_selector_index = 0;
15323
15324 app.handle_action(Action::PageDown);
15325 assert_eq!(app.column_selector_index, 10);
15326
15327 app.handle_action(Action::PageDown);
15328 assert_eq!(app.column_selector_index, 20);
15329 }
15330
15331 #[test]
15332 fn test_column_selector_page_up_ec2() {
15333 let mut app = test_app();
15334 app.current_service = Service::Ec2Instances;
15335 app.service_selected = true;
15336 app.mode = Mode::ColumnSelector;
15337 app.column_selector_index = 30;
15338
15339 app.handle_action(Action::PageUp);
15340 assert_eq!(app.column_selector_index, 20);
15341
15342 app.handle_action(Action::PageUp);
15343 assert_eq!(app.column_selector_index, 10);
15344 }
15345
15346 #[test]
15347 fn test_ec2_state_filter_dropdown_focus() {
15348 let mut app = test_app();
15349 app.current_service = Service::Ec2Instances;
15350 app.service_selected = true;
15351 app.mode = Mode::FilterInput;
15352
15353 app.handle_action(Action::NextFilterFocus);
15355 assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
15356
15357 app.handle_action(Action::ToggleFilterCheckbox);
15360 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15361 }
15362
15363 #[test]
15364 fn test_column_selector_ctrl_d_scrolling() {
15365 let mut app = test_app();
15366 app.current_service = Service::LambdaFunctions;
15367 app.mode = Mode::ColumnSelector;
15368 app.column_selector_index = 0;
15369
15370 app.handle_action(Action::PageDown);
15372 assert_eq!(app.column_selector_index, 11);
15373
15374 let max = app.get_column_selector_max();
15376 app.handle_action(Action::PageDown);
15377 assert_eq!(app.column_selector_index, max);
15378 }
15379
15380 #[test]
15381 fn test_column_selector_ctrl_u_scrolling() {
15382 let mut app = test_app();
15383 app.current_service = Service::CloudFormationStacks;
15384 app.mode = Mode::ColumnSelector;
15385 app.column_selector_index = 25;
15386
15387 app.handle_action(Action::PageUp);
15388 assert_eq!(app.column_selector_index, 15);
15389
15390 app.handle_action(Action::PageUp);
15391 assert_eq!(app.column_selector_index, 5);
15392 }
15393
15394 #[test]
15395 fn test_prev_preferences_lambda() {
15396 let mut app = test_app();
15397 app.current_service = Service::LambdaFunctions;
15398 app.mode = Mode::ColumnSelector;
15399 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
15400 app.column_selector_index = page_size_idx;
15401
15402 app.handle_action(Action::PrevPreferences);
15403 assert_eq!(app.column_selector_index, 0);
15404
15405 app.handle_action(Action::PrevPreferences);
15406 assert_eq!(app.column_selector_index, page_size_idx);
15407 }
15408
15409 #[test]
15410 fn test_prev_preferences_cloudformation() {
15411 let mut app = test_app();
15412 app.current_service = Service::CloudFormationStacks;
15413 app.mode = Mode::ColumnSelector;
15414 let page_size_idx = app.cfn_column_ids.len() + 2;
15415 app.column_selector_index = page_size_idx;
15416
15417 app.handle_action(Action::PrevPreferences);
15418 assert_eq!(app.column_selector_index, 0);
15419
15420 app.handle_action(Action::PrevPreferences);
15421 assert_eq!(app.column_selector_index, page_size_idx);
15422 }
15423
15424 #[test]
15425 fn test_prev_preferences_alarms() {
15426 let mut app = test_app();
15427 app.current_service = Service::CloudWatchAlarms;
15428 app.mode = Mode::ColumnSelector;
15429 app.column_selector_index = 28; app.handle_action(Action::PrevPreferences);
15432 assert_eq!(app.column_selector_index, 22); app.handle_action(Action::PrevPreferences);
15435 assert_eq!(app.column_selector_index, 18); app.handle_action(Action::PrevPreferences);
15438 assert_eq!(app.column_selector_index, 0); app.handle_action(Action::PrevPreferences);
15441 assert_eq!(app.column_selector_index, 28); }
15443
15444 #[test]
15445 fn test_ec2_page_size_in_preferences() {
15446 let mut app = test_app();
15447 app.current_service = Service::Ec2Instances;
15448 app.mode = Mode::ColumnSelector;
15449 app.ec2_state.table.page_size = PageSize::Fifty;
15450
15451 let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
15454 app.handle_action(Action::ToggleColumn);
15455
15456 assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
15457 }
15458
15459 #[test]
15460 fn test_ec2_next_preferences_with_page_size() {
15461 let mut app = test_app();
15462 app.current_service = Service::Ec2Instances;
15463 app.mode = Mode::ColumnSelector;
15464 app.column_selector_index = 0;
15465
15466 let page_size_idx = app.ec2_column_ids.len() + 2;
15467 app.handle_action(Action::NextPreferences);
15468 assert_eq!(app.column_selector_index, page_size_idx);
15469
15470 app.handle_action(Action::NextPreferences);
15471 assert_eq!(app.column_selector_index, 0);
15472 }
15473
15474 #[test]
15475 fn test_ec2_dropdown_next_item() {
15476 let mut app = test_app();
15477 app.current_service = Service::Ec2Instances;
15478 app.mode = Mode::FilterInput;
15479 app.ec2_state.input_focus = EC2_STATE_FILTER;
15480 app.ec2_state.state_filter = Ec2StateFilter::AllStates;
15481
15482 app.handle_action(Action::NextItem);
15483 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15484
15485 app.handle_action(Action::NextItem);
15486 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15487 }
15488
15489 #[test]
15490 fn test_ec2_dropdown_prev_item() {
15491 let mut app = test_app();
15492 app.current_service = Service::Ec2Instances;
15493 app.mode = Mode::FilterInput;
15494 app.ec2_state.input_focus = EC2_STATE_FILTER;
15495 app.ec2_state.state_filter = Ec2StateFilter::Stopped;
15496
15497 app.handle_action(Action::PrevItem);
15498 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15499
15500 app.handle_action(Action::PrevItem);
15501 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15502 }
15503
15504 #[test]
15505 fn test_ec2_dropdown_cycles_with_arrows() {
15506 let mut app = test_app();
15507 app.current_service = Service::Ec2Instances;
15508 app.mode = Mode::FilterInput;
15509 app.ec2_state.input_focus = EC2_STATE_FILTER;
15510 app.ec2_state.state_filter = Ec2StateFilter::Stopping;
15511
15512 app.handle_action(Action::NextItem);
15514 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15515
15516 app.handle_action(Action::PrevItem);
15518 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15519 }
15520
15521 #[test]
15522 fn test_collapse_row_ec2_instances() {
15523 let mut app = test_app();
15524 app.current_service = Service::Ec2Instances;
15525 app.ec2_state.table.expanded_item = Some(0);
15526
15527 app.handle_action(Action::CollapseRow);
15528 assert_eq!(app.ec2_state.table.expanded_item, None);
15529 }
15530
15531 #[test]
15532 fn test_collapse_row_ec2_tags() {
15533 let mut app = test_app();
15534 app.current_service = Service::Ec2Instances;
15535 app.ec2_state.current_instance = Some("i-123".to_string());
15536 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
15537 app.ec2_state.tags.expanded_item = Some(1);
15538
15539 app.handle_action(Action::CollapseRow);
15540 assert_eq!(app.ec2_state.tags.expanded_item, None);
15541 }
15542
15543 #[test]
15544 fn test_collapse_row_cloudwatch_log_groups() {
15545 let mut app = test_app();
15546 app.current_service = Service::CloudWatchLogGroups;
15547 app.log_groups_state.log_groups.expanded_item = Some(2);
15548
15549 app.handle_action(Action::CollapseRow);
15550 assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
15551 }
15552
15553 #[test]
15554 fn test_collapse_row_cloudwatch_alarms() {
15555 let mut app = test_app();
15556 app.current_service = Service::CloudWatchAlarms;
15557 app.alarms_state.table.expanded_item = Some(0);
15558
15559 app.handle_action(Action::CollapseRow);
15560 assert_eq!(app.alarms_state.table.expanded_item, None);
15561 }
15562
15563 #[test]
15564 fn test_collapse_row_lambda_functions() {
15565 let mut app = test_app();
15566 app.current_service = Service::LambdaFunctions;
15567 app.lambda_state.table.expanded_item = Some(1);
15568
15569 app.handle_action(Action::CollapseRow);
15570 assert_eq!(app.lambda_state.table.expanded_item, None);
15571 }
15572
15573 #[test]
15574 fn test_collapse_row_cfn_stacks() {
15575 let mut app = test_app();
15576 app.current_service = Service::CloudFormationStacks;
15577 app.cfn_state.table.expanded_item = Some(0);
15578
15579 app.handle_action(Action::CollapseRow);
15580 assert_eq!(app.cfn_state.table.expanded_item, None);
15581 }
15582
15583 #[test]
15584 fn test_collapse_row_cfn_resources() {
15585 let mut app = test_app();
15586 app.current_service = Service::CloudFormationStacks;
15587 app.cfn_state.current_stack = Some("test-stack".to_string());
15588 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
15589 app.cfn_state.resources.expanded_item = Some(2);
15590
15591 app.handle_action(Action::CollapseRow);
15592 assert_eq!(app.cfn_state.resources.expanded_item, None);
15593 }
15594
15595 #[test]
15596 fn test_collapse_row_iam_users() {
15597 let mut app = test_app();
15598 app.current_service = Service::IamUsers;
15599 app.iam_state.users.expanded_item = Some(1);
15600
15601 app.handle_action(Action::CollapseRow);
15602 assert_eq!(app.iam_state.users.expanded_item, None);
15603 }
15604
15605 #[test]
15606 fn test_collapse_row_does_nothing_when_not_expanded() {
15607 let mut app = test_app();
15608 app.current_service = Service::Ec2Instances;
15609 app.ec2_state.table.expanded_item = None;
15610
15611 app.handle_action(Action::CollapseRow);
15612 assert_eq!(app.ec2_state.table.expanded_item, None);
15613 }
15614
15615 #[test]
15616 fn test_s3_collapse_expanded_folder_moves_to_parent() {
15617 let mut app = test_app();
15618 app.current_service = Service::S3Buckets;
15619 app.service_selected = true;
15620 app.mode = Mode::Normal;
15621
15622 app.s3_state.buckets.items = vec![S3Bucket {
15624 name: "bucket1".to_string(),
15625 region: "us-east-1".to_string(),
15626 creation_date: "2024-01-01T00:00:00Z".to_string(),
15627 }];
15628
15629 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15631 app.s3_state.bucket_preview.insert(
15632 "bucket1".to_string(),
15633 vec![S3Object {
15634 key: "folder1/".to_string(),
15635 size: 0,
15636 last_modified: "2024-01-01T00:00:00Z".to_string(),
15637 is_prefix: true,
15638 storage_class: String::new(),
15639 }],
15640 );
15641
15642 app.s3_state
15644 .expanded_prefixes
15645 .insert("folder1/".to_string());
15646 app.s3_state.prefix_preview.insert(
15647 "folder1/".to_string(),
15648 vec![S3Object {
15649 key: "folder1/file.txt".to_string(),
15650 size: 0,
15651 last_modified: "2024-01-01T00:00:00Z".to_string(),
15652 is_prefix: false,
15653 storage_class: String::new(),
15654 }],
15655 );
15656
15657 app.s3_state.selected_row = 1;
15659
15660 app.handle_action(Action::PrevPane);
15662
15663 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
15665 assert_eq!(app.s3_state.selected_row, 0);
15667 }
15668
15669 #[test]
15670 fn test_s3_collapse_hierarchy_level_by_level() {
15671 let mut app = test_app();
15672 app.current_service = Service::S3Buckets;
15673 app.service_selected = true;
15674 app.mode = Mode::Normal;
15675
15676 app.s3_state.buckets.items = vec![S3Bucket {
15678 name: "bucket1".to_string(),
15679 region: "us-east-1".to_string(),
15680 creation_date: "2024-01-01T00:00:00Z".to_string(),
15681 }];
15682
15683 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15685 app.s3_state.bucket_preview.insert(
15686 "bucket1".to_string(),
15687 vec![S3Object {
15688 key: "level1/".to_string(),
15689 size: 0,
15690 last_modified: "2024-01-01T00:00:00Z".to_string(),
15691 is_prefix: true,
15692 storage_class: String::new(),
15693 }],
15694 );
15695
15696 app.s3_state.expanded_prefixes.insert("level1/".to_string());
15698 app.s3_state.prefix_preview.insert(
15699 "level1/".to_string(),
15700 vec![S3Object {
15701 key: "level1/level2/".to_string(),
15702 size: 0,
15703 last_modified: "2024-01-01T00:00:00Z".to_string(),
15704 is_prefix: true,
15705 storage_class: String::new(),
15706 }],
15707 );
15708
15709 app.s3_state
15711 .expanded_prefixes
15712 .insert("level1/level2/".to_string());
15713 app.s3_state.prefix_preview.insert(
15714 "level1/level2/".to_string(),
15715 vec![S3Object {
15716 key: "level1/level2/file.txt".to_string(),
15717 size: 100,
15718 last_modified: "2024-01-01T00:00:00Z".to_string(),
15719 is_prefix: false,
15720 storage_class: String::new(),
15721 }],
15722 );
15723
15724 app.s3_state.selected_row = 3;
15726
15727 app.handle_action(Action::PrevPane);
15729 assert_eq!(app.s3_state.selected_row, 2);
15730
15731 app.handle_action(Action::PrevPane);
15733 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
15734 assert_eq!(app.s3_state.selected_row, 1);
15735
15736 app.handle_action(Action::PrevPane);
15738 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
15739 assert_eq!(app.s3_state.selected_row, 0);
15740
15741 app.handle_action(Action::PrevPane);
15743 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
15744 assert_eq!(app.s3_state.selected_row, 0);
15745 }
15746
15747 #[test]
15748 fn test_ec2_instance_detail_tabs_no_preferences() {
15749 let mut app = test_app();
15750 app.current_service = Service::Ec2Instances;
15751 app.service_selected = true;
15752 app.ec2_state.table.expanded_item = Some(0);
15753 app.mode = Mode::Normal;
15754
15755 app.ec2_state.detail_tab = Ec2DetailTab::Details;
15757 app.handle_action(Action::OpenColumnSelector);
15758 assert_eq!(app.mode, Mode::Normal);
15759
15760 app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
15762 app.handle_action(Action::OpenColumnSelector);
15763 assert_eq!(app.mode, Mode::Normal);
15764
15765 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
15767 app.handle_action(Action::OpenColumnSelector);
15768 assert_eq!(app.mode, Mode::Normal);
15769
15770 app.ec2_state.detail_tab = Ec2DetailTab::Security;
15772 app.handle_action(Action::OpenColumnSelector);
15773 assert_eq!(app.mode, Mode::Normal);
15774
15775 app.ec2_state.detail_tab = Ec2DetailTab::Networking;
15777 app.handle_action(Action::OpenColumnSelector);
15778 assert_eq!(app.mode, Mode::Normal);
15779
15780 app.ec2_state.detail_tab = Ec2DetailTab::Storage;
15782 app.handle_action(Action::OpenColumnSelector);
15783 assert_eq!(app.mode, Mode::Normal);
15784
15785 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
15787 app.handle_action(Action::OpenColumnSelector);
15788 assert_eq!(app.mode, Mode::ColumnSelector);
15789 }
15790
15791 #[test]
15792 fn test_ec2_monitoring_ctrl_u_scrolls_up() {
15793 let mut app = test_app();
15797 app.current_service = Service::Ec2Instances;
15798 app.service_selected = true;
15799 app.mode = Mode::Normal;
15800 app.ec2_state.current_instance = Some("i-abc".to_string());
15801 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
15802
15803 app.ec2_state.set_monitoring_scroll(3);
15805 assert_eq!(app.ec2_state.monitoring_scroll(), 3);
15806
15807 app.handle_action(Action::PageUp);
15809 assert_eq!(
15810 app.ec2_state.monitoring_scroll(),
15811 2,
15812 "Ctrl+U must scroll monitoring up by 1"
15813 );
15814
15815 app.handle_action(Action::PageDown);
15817 assert_eq!(
15818 app.ec2_state.monitoring_scroll(),
15819 3,
15820 "Ctrl+D must scroll monitoring down by 1"
15821 );
15822 }
15823
15824 #[test]
15825 fn test_ec2_instance_always_shows_when_state_filter_is_all_states() {
15826 use crate::app::Ec2Instance;
15829 use crate::ui::ec2::{filtered_ec2_instances, StateFilter};
15830 let mut app = test_app();
15831 app.current_service = Service::Ec2Instances;
15832 app.service_selected = true;
15833 app.ec2_state.table.filter.clear();
15834 app.ec2_state.state_filter = StateFilter::AllStates;
15835
15836 app.ec2_state.table.items = vec![Ec2Instance {
15838 instance_id: "i-05f181d4aaabadf5a".to_string(),
15839 state: "running".to_string(),
15840 ..Default::default()
15841 }];
15842
15843 let filtered = filtered_ec2_instances(&app);
15844 assert_eq!(
15845 filtered.len(),
15846 1,
15847 "Instance must appear with AllStates filter and no text filter"
15848 );
15849 assert_eq!(filtered[0].instance_id, "i-05f181d4aaabadf5a");
15850 }
15851
15852 #[test]
15853 fn test_ec2_instance_private_ip_populated_from_load() {
15854 use crate::app::Ec2Instance;
15858 let instance = Ec2Instance {
15859 instance_id: "i-abc".to_string(),
15860 name: "my-server".to_string(),
15861 state: "running".to_string(),
15862 private_dns_name: "ip-10-0-1-5.ec2.internal".to_string(),
15863 private_ip_address: "10.0.1.5".to_string(),
15864 security_group_ids: "sg-abc123".to_string(),
15865 ..Default::default()
15866 };
15867 assert!(
15869 !instance.private_ip_address.is_empty(),
15870 "private_ip_address must be populated"
15871 );
15872 assert!(
15873 !instance.private_dns_name.is_empty(),
15874 "private_dns_name must be populated"
15875 );
15876 }
15877
15878 #[test]
15879 fn test_log_streams_filter_only_updates_when_focused() {
15880 let mut app = test_app();
15881 app.current_service = Service::CloudWatchLogGroups;
15882 app.service_selected = true;
15883 app.view_mode = ViewMode::Detail;
15884 app.mode = Mode::FilterInput;
15885 app.log_groups_state.stream_filter = "test".to_string();
15886
15887 app.log_groups_state.input_focus = InputFocus::Filter;
15889 app.handle_action(Action::FilterInput('x'));
15890 assert_eq!(app.log_groups_state.stream_filter, "testx");
15891
15892 app.log_groups_state.input_focus = InputFocus::Pagination;
15894 app.handle_action(Action::FilterInput('y'));
15895 assert_eq!(app.log_groups_state.stream_filter, "testx"); }
15897
15898 #[test]
15899 fn test_log_streams_backspace_only_updates_when_focused() {
15900 let mut app = test_app();
15901 app.current_service = Service::CloudWatchLogGroups;
15902 app.service_selected = true;
15903 app.view_mode = ViewMode::Detail;
15904 app.mode = Mode::FilterInput;
15905 app.log_groups_state.stream_filter = "test".to_string();
15906
15907 app.log_groups_state.input_focus = InputFocus::Filter;
15909 app.handle_action(Action::FilterBackspace);
15910 assert_eq!(app.log_groups_state.stream_filter, "tes");
15911
15912 app.log_groups_state.input_focus = InputFocus::Pagination;
15914 app.handle_action(Action::FilterBackspace);
15915 assert_eq!(app.log_groups_state.stream_filter, "tes"); }
15917
15918 #[test]
15919 fn test_log_groups_filter_only_updates_when_focused() {
15920 let mut app = test_app();
15921 app.current_service = Service::CloudWatchLogGroups;
15922 app.service_selected = true;
15923 app.view_mode = ViewMode::List;
15924 app.mode = Mode::FilterInput;
15925 app.log_groups_state.log_groups.filter = "test".to_string();
15926
15927 app.log_groups_state.input_focus = InputFocus::Filter;
15929 app.handle_action(Action::FilterInput('x'));
15930 assert_eq!(app.log_groups_state.log_groups.filter, "testx");
15931
15932 app.log_groups_state.input_focus = InputFocus::Pagination;
15934 app.handle_action(Action::FilterInput('y'));
15935 assert_eq!(app.log_groups_state.log_groups.filter, "testx"); }
15937
15938 #[test]
15939 fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
15940 use S3Bucket;
15941 use S3Object;
15942
15943 let mut app = test_app();
15944 app.current_service = Service::S3Buckets;
15945 app.service_selected = true;
15946
15947 app.s3_state.buckets.items = vec![S3Bucket {
15949 name: "test-bucket".to_string(),
15950 region: "us-east-1".to_string(),
15951 creation_date: String::new(),
15952 }];
15953
15954 app.s3_state
15956 .expanded_prefixes
15957 .insert("test-bucket".to_string());
15958 app.s3_state.bucket_preview.insert(
15959 "test-bucket".to_string(),
15960 vec![S3Object {
15961 key: "folder1/".to_string(),
15962 is_prefix: true,
15963 size: 0,
15964 last_modified: String::new(),
15965 storage_class: String::new(),
15966 }],
15967 );
15968
15969 app.s3_state
15971 .expanded_prefixes
15972 .insert("folder1/".to_string());
15973 app.s3_state.prefix_preview.insert(
15974 "folder1/".to_string(),
15975 vec![S3Object {
15976 key: "folder1/folder2/".to_string(),
15977 is_prefix: true,
15978 size: 0,
15979 last_modified: String::new(),
15980 storage_class: String::new(),
15981 }],
15982 );
15983
15984 app.s3_state.selected_row = 2;
15986
15987 app.handle_action(Action::CollapseRow);
15989
15990 assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
15992 assert_eq!(app.s3_state.selected_row, 1);
15994 }
15995
15996 #[test]
15997 fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
15998 use S3Bucket;
15999 use S3Object;
16000
16001 let mut app = test_app();
16002 app.current_service = Service::S3Buckets;
16003 app.service_selected = true;
16004
16005 app.s3_state.buckets.items = vec![S3Bucket {
16007 name: "test-bucket".to_string(),
16008 region: "us-east-1".to_string(),
16009 creation_date: String::new(),
16010 }];
16011
16012 app.s3_state
16014 .expanded_prefixes
16015 .insert("test-bucket".to_string());
16016 app.s3_state.bucket_preview.insert(
16017 "test-bucket".to_string(),
16018 vec![S3Object {
16019 key: "folder1/".to_string(),
16020 is_prefix: true,
16021 size: 0,
16022 last_modified: String::new(),
16023 storage_class: String::new(),
16024 }],
16025 );
16026
16027 app.s3_state
16029 .expanded_prefixes
16030 .insert("folder1/".to_string());
16031 app.s3_state.prefix_preview.insert(
16032 "folder1/".to_string(),
16033 vec![S3Object {
16034 key: "folder1/file.txt".to_string(),
16035 is_prefix: false,
16036 size: 100,
16037 last_modified: String::new(),
16038 storage_class: String::new(),
16039 }],
16040 );
16041
16042 app.s3_state.selected_row = 1;
16044
16045 app.handle_action(Action::CollapseRow);
16047
16048 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
16050 assert_eq!(app.s3_state.selected_row, 0);
16052 }
16053
16054 #[test]
16055 fn test_log_streams_pagination_limits_table_content() {
16056 let mut app = test_app();
16057 app.current_service = Service::CloudWatchLogGroups;
16058 app.service_selected = true;
16059 app.view_mode = ViewMode::Detail;
16060
16061 app.log_groups_state.log_streams = (0..50)
16063 .map(|i| rusticity_core::LogStream {
16064 name: format!("stream-{}", i),
16065 creation_time: None,
16066 last_event_time: None,
16067 })
16068 .collect();
16069
16070 app.log_groups_state.stream_page_size = 10;
16072 app.log_groups_state.stream_current_page = 0;
16073
16074 assert_eq!(app.log_groups_state.stream_page_size, 10);
16077 assert_eq!(app.log_groups_state.stream_current_page, 0);
16078
16079 app.log_groups_state.stream_current_page = 1;
16081 assert_eq!(app.log_groups_state.stream_current_page, 1);
16082 }
16083
16084 #[test]
16085 fn test_log_streams_page_size_change_resets_page() {
16086 let mut app = test_app();
16087 app.current_service = Service::CloudWatchLogGroups;
16088 app.service_selected = true;
16089 app.view_mode = ViewMode::Detail;
16090 app.mode = Mode::ColumnSelector;
16091
16092 app.log_groups_state.stream_page_size = 10;
16093 app.log_groups_state.stream_current_page = 3;
16094
16095 app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; app.handle_action(Action::ToggleColumn);
16098
16099 assert_eq!(app.log_groups_state.stream_page_size, 25);
16100 assert_eq!(app.log_groups_state.stream_current_page, 0);
16101 }
16102
16103 #[test]
16104 fn test_s3_objects_expanded_rows_stay_visible() {
16105 use S3Object;
16106
16107 let mut app = test_app();
16108 app.current_service = Service::S3Buckets;
16109 app.service_selected = true;
16110 app.mode = Mode::Normal;
16111 app.s3_state.current_bucket = Some("test-bucket".to_string());
16112
16113 app.s3_state.objects = vec![S3Object {
16115 key: "folder1/".to_string(),
16116 is_prefix: true,
16117 size: 0,
16118 last_modified: String::new(),
16119 storage_class: String::new(),
16120 }];
16121
16122 app.s3_state
16124 .expanded_prefixes
16125 .insert("folder1/".to_string());
16126 app.s3_state.prefix_preview.insert(
16127 "folder1/".to_string(),
16128 (0..20)
16129 .map(|i| S3Object {
16130 key: format!("folder1/file{}.txt", i),
16131 is_prefix: false,
16132 size: 100,
16133 last_modified: String::new(),
16134 storage_class: String::new(),
16135 })
16136 .collect(),
16137 );
16138
16139 app.s3_state.object_visible_rows.set(10);
16141 app.s3_state.object_scroll_offset = 0;
16142 app.s3_state.selected_object = 0; for i in 1..=20 {
16146 app.handle_action(Action::NextItem);
16147 assert_eq!(app.s3_state.selected_object, i);
16148
16149 let visible_start = app.s3_state.object_scroll_offset;
16151 let visible_end = visible_start + app.s3_state.object_visible_rows.get();
16152 assert!(
16153 app.s3_state.selected_object >= visible_start
16154 && app.s3_state.selected_object < visible_end,
16155 "Selection {} should be visible in range [{}, {})",
16156 app.s3_state.selected_object,
16157 visible_start,
16158 visible_end
16159 );
16160 }
16161 }
16162
16163 #[test]
16164 fn test_s3_bucket_error_rows_counted_in_total() {
16165 use S3Bucket;
16166
16167 let mut app = test_app();
16168 app.current_service = Service::S3Buckets;
16169 app.service_selected = true;
16170
16171 app.s3_state.buckets.items = vec![
16173 S3Bucket {
16174 name: "bucket1".to_string(),
16175 region: "us-east-1".to_string(),
16176 creation_date: String::new(),
16177 },
16178 S3Bucket {
16179 name: "bucket2".to_string(),
16180 region: "us-east-1".to_string(),
16181 creation_date: String::new(),
16182 },
16183 ];
16184
16185 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16187 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();
16188 app.s3_state
16189 .bucket_errors
16190 .insert("bucket1".to_string(), long_error.clone());
16191
16192 let total = app.calculate_total_bucket_rows();
16194
16195 let error_rows = long_error.len().div_ceil(120);
16197 assert_eq!(total, 2 + error_rows);
16198 }
16199
16200 #[test]
16201 fn test_s3_bucket_with_error_can_be_collapsed() {
16202 use S3Bucket;
16203
16204 let mut app = test_app();
16205 app.current_service = Service::S3Buckets;
16206 app.service_selected = true;
16207 app.mode = Mode::Normal;
16208
16209 app.s3_state.buckets.items = vec![S3Bucket {
16211 name: "bucket1".to_string(),
16212 region: "us-east-1".to_string(),
16213 creation_date: String::new(),
16214 }];
16215
16216 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16218 let error = "service error: PermanentRedirect".to_string();
16219 app.s3_state
16220 .bucket_errors
16221 .insert("bucket1".to_string(), error);
16222
16223 app.s3_state.selected_row = 0;
16225
16226 app.handle_action(Action::CollapseRow);
16228
16229 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16231 assert_eq!(app.s3_state.selected_row, 0);
16233 }
16234
16235 #[test]
16236 fn test_s3_bucket_collapse_on_bucket_row() {
16237 use S3Bucket;
16238
16239 let mut app = test_app();
16240 app.current_service = Service::S3Buckets;
16241 app.service_selected = true;
16242 app.mode = Mode::Normal;
16243
16244 app.s3_state.buckets.items = vec![S3Bucket {
16246 name: "bucket1".to_string(),
16247 region: "us-east-1".to_string(),
16248 creation_date: String::new(),
16249 }];
16250
16251 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16253 let error = "service error: PermanentRedirect".to_string();
16254 app.s3_state
16255 .bucket_errors
16256 .insert("bucket1".to_string(), error);
16257
16258 app.s3_state.selected_row = 0;
16260
16261 app.handle_action(Action::CollapseRow);
16263
16264 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16266 assert_eq!(app.s3_state.selected_row, 0);
16268 }
16269
16270 #[test]
16271 fn test_s3_bucket_collapse_adjusts_scroll_offset() {
16272 use S3Bucket;
16273
16274 let mut app = test_app();
16275 app.current_service = Service::S3Buckets;
16276 app.service_selected = true;
16277 app.mode = Mode::Normal;
16278
16279 app.s3_state.buckets.items = (0..20)
16281 .map(|i| S3Bucket {
16282 name: format!("bucket{}", i),
16283 region: "us-east-1".to_string(),
16284 creation_date: String::new(),
16285 })
16286 .collect();
16287
16288 app.s3_state
16290 .expanded_prefixes
16291 .insert("bucket10".to_string());
16292 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();
16293 app.s3_state
16294 .bucket_errors
16295 .insert("bucket10".to_string(), long_error.clone());
16296
16297 app.s3_state.bucket_visible_rows.set(10);
16299 app.s3_state.bucket_scroll_offset = 10; app.s3_state.selected_row = 10;
16303
16304 app.handle_action(Action::CollapseRow);
16306
16307 assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
16309 assert_eq!(app.s3_state.selected_row, 10);
16311 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
16313 assert!(
16314 app.s3_state.selected_row
16315 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
16316 );
16317 }
16318
16319 #[test]
16320 fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
16321 use S3Bucket;
16322
16323 let mut app = test_app();
16324 app.current_service = Service::S3Buckets;
16325 app.service_selected = true;
16326 app.mode = Mode::Normal;
16327
16328 app.s3_state.buckets.items = vec![
16330 S3Bucket {
16331 name: "bucket1".to_string(),
16332 region: "us-east-1".to_string(),
16333 creation_date: String::new(),
16334 },
16335 S3Bucket {
16336 name: "bucket2".to_string(),
16337 region: "us-east-1".to_string(),
16338 creation_date: String::new(),
16339 },
16340 S3Bucket {
16341 name: "bucket3".to_string(),
16342 region: "us-east-1".to_string(),
16343 creation_date: String::new(),
16344 },
16345 ];
16346
16347 app.s3_state.expanded_prefixes.insert("bucket2".to_string());
16349 app.s3_state.bucket_preview.insert(
16350 "bucket2".to_string(),
16351 vec![
16352 S3Object {
16353 key: "folder1/".to_string(),
16354 is_prefix: true,
16355 size: 0,
16356 last_modified: String::new(),
16357 storage_class: String::new(),
16358 },
16359 S3Object {
16360 key: "file1.txt".to_string(),
16361 is_prefix: false,
16362 size: 100,
16363 last_modified: String::new(),
16364 storage_class: String::new(),
16365 },
16366 ],
16367 );
16368
16369 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
16371 let error = "service error: PermanentRedirect".to_string();
16372 app.s3_state
16373 .bucket_errors
16374 .insert("bucket3".to_string(), error);
16375
16376 app.s3_state.bucket_visible_rows.set(10);
16378 app.s3_state.bucket_scroll_offset = 0;
16379
16380 app.s3_state.selected_row = 3;
16382
16383 app.handle_action(Action::CollapseRow);
16385
16386 assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
16388 assert_eq!(app.s3_state.selected_row, 1);
16390 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
16392 assert!(
16393 app.s3_state.selected_row
16394 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
16395 );
16396 }
16397
16398 #[test]
16399 fn test_s3_collapse_bucket_with_error() {
16400 use S3Bucket;
16401
16402 let mut app = test_app();
16403 app.current_service = Service::S3Buckets;
16404 app.service_selected = true;
16405 app.mode = Mode::Normal;
16406
16407 app.s3_state.buckets.items = vec![
16408 S3Bucket {
16409 name: "bucket1".to_string(),
16410 region: "us-east-1".to_string(),
16411 creation_date: String::new(),
16412 },
16413 S3Bucket {
16414 name: "bucket2".to_string(),
16415 region: "us-east-1".to_string(),
16416 creation_date: String::new(),
16417 },
16418 ];
16419
16420 let error = "service error: unhandled error (PermanentRedirect)".to_string();
16421 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16422 app.s3_state
16423 .bucket_errors
16424 .insert("bucket1".to_string(), error);
16425
16426 app.s3_state.bucket_visible_rows.set(10);
16427 app.s3_state.bucket_scroll_offset = 0;
16428
16429 app.s3_state.selected_row = 0;
16431
16432 app.handle_action(Action::CollapseRow);
16434
16435 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16437 assert_eq!(app.s3_state.selected_row, 0);
16438 }
16439
16440 #[test]
16441 fn test_s3_collapse_row_with_multiple_error_buckets() {
16442 use S3Bucket;
16443
16444 let mut app = test_app();
16445 app.current_service = Service::S3Buckets;
16446 app.service_selected = true;
16447 app.mode = Mode::Normal;
16448
16449 app.s3_state.buckets.items = vec![
16450 S3Bucket {
16451 name: "bucket1".to_string(),
16452 region: "us-east-1".to_string(),
16453 creation_date: String::new(),
16454 },
16455 S3Bucket {
16456 name: "bucket2".to_string(),
16457 region: "us-east-1".to_string(),
16458 creation_date: String::new(),
16459 },
16460 S3Bucket {
16461 name: "bucket3".to_string(),
16462 region: "us-east-1".to_string(),
16463 creation_date: String::new(),
16464 },
16465 ];
16466
16467 let error = "service error: unhandled error (PermanentRedirect)".to_string();
16468
16469 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16471 app.s3_state
16472 .bucket_errors
16473 .insert("bucket1".to_string(), error.clone());
16474
16475 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
16477 app.s3_state
16478 .bucket_errors
16479 .insert("bucket3".to_string(), error.clone());
16480
16481 app.s3_state.bucket_visible_rows.set(30);
16482 app.s3_state.bucket_scroll_offset = 0;
16483
16484 app.s3_state.selected_row = 2;
16489
16490 app.handle_action(Action::CollapseRow);
16491
16492 assert!(
16494 !app.s3_state.expanded_prefixes.contains("bucket3"),
16495 "bucket3 should be collapsed"
16496 );
16497 assert!(
16498 app.s3_state.expanded_prefixes.contains("bucket1"),
16499 "bucket1 should still be expanded"
16500 );
16501 assert_eq!(app.s3_state.selected_row, 2);
16502 }
16503
16504 #[test]
16505 fn test_s3_collapse_row_nested_only_collapses_one_level() {
16506 use S3Bucket;
16507
16508 let mut app = test_app();
16509 app.current_service = Service::S3Buckets;
16510 app.service_selected = true;
16511 app.mode = Mode::Normal;
16512
16513 app.s3_state.buckets.items = vec![S3Bucket {
16514 name: "bucket1".to_string(),
16515 region: "us-east-1".to_string(),
16516 creation_date: String::new(),
16517 }];
16518
16519 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16521 app.s3_state.bucket_preview.insert(
16522 "bucket1".to_string(),
16523 vec![S3Object {
16524 key: "level1/".to_string(),
16525 size: 0,
16526 last_modified: "2024-01-01T00:00:00Z".to_string(),
16527 is_prefix: true,
16528 storage_class: String::new(),
16529 }],
16530 );
16531
16532 app.s3_state.expanded_prefixes.insert("level1/".to_string());
16534 app.s3_state.prefix_preview.insert(
16535 "level1/".to_string(),
16536 vec![S3Object {
16537 key: "level1/level2/".to_string(),
16538 size: 0,
16539 last_modified: "2024-01-01T00:00:00Z".to_string(),
16540 is_prefix: true,
16541 storage_class: String::new(),
16542 }],
16543 );
16544
16545 app.s3_state
16547 .expanded_prefixes
16548 .insert("level1/level2/".to_string());
16549 app.s3_state.prefix_preview.insert(
16550 "level1/level2/".to_string(),
16551 vec![S3Object {
16552 key: "level1/level2/file.txt".to_string(),
16553 size: 100,
16554 last_modified: "2024-01-01T00:00:00Z".to_string(),
16555 is_prefix: false,
16556 storage_class: String::new(),
16557 }],
16558 );
16559
16560 app.s3_state.bucket_visible_rows.set(10);
16561
16562 app.s3_state.selected_row = 2;
16564
16565 app.handle_action(Action::CollapseRow);
16567
16568 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
16570 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16572 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
16574 assert_eq!(app.s3_state.selected_row, 1);
16576 }
16577
16578 #[test]
16579 fn test_s3_collapse_row_deeply_nested_file() {
16580 use S3Bucket;
16581
16582 let mut app = test_app();
16583 app.current_service = Service::S3Buckets;
16584 app.service_selected = true;
16585 app.mode = Mode::Normal;
16586
16587 app.s3_state.buckets.items = vec![S3Bucket {
16588 name: "bucket1".to_string(),
16589 region: "us-east-1".to_string(),
16590 creation_date: String::new(),
16591 }];
16592
16593 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16595 app.s3_state.bucket_preview.insert(
16596 "bucket1".to_string(),
16597 vec![S3Object {
16598 key: "level1/".to_string(),
16599 size: 0,
16600 last_modified: "2024-01-01T00:00:00Z".to_string(),
16601 is_prefix: true,
16602 storage_class: String::new(),
16603 }],
16604 );
16605
16606 app.s3_state.expanded_prefixes.insert("level1/".to_string());
16608 app.s3_state.prefix_preview.insert(
16609 "level1/".to_string(),
16610 vec![S3Object {
16611 key: "level1/level2/".to_string(),
16612 size: 0,
16613 last_modified: "2024-01-01T00:00:00Z".to_string(),
16614 is_prefix: true,
16615 storage_class: String::new(),
16616 }],
16617 );
16618
16619 app.s3_state
16621 .expanded_prefixes
16622 .insert("level1/level2/".to_string());
16623 app.s3_state.prefix_preview.insert(
16624 "level1/level2/".to_string(),
16625 vec![S3Object {
16626 key: "level1/level2/file.txt".to_string(),
16627 size: 100,
16628 last_modified: "2024-01-01T00:00:00Z".to_string(),
16629 is_prefix: false,
16630 storage_class: String::new(),
16631 }],
16632 );
16633
16634 app.s3_state.bucket_visible_rows.set(10);
16635
16636 app.s3_state.selected_row = 3;
16638
16639 app.handle_action(Action::CollapseRow);
16641
16642 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
16644 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16645 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
16646 assert_eq!(app.s3_state.selected_row, 2);
16648 }
16649
16650 #[test]
16651 fn test_s3_bucket_pagination_adjusts_scroll() {
16652 use S3Bucket;
16653
16654 let mut app = test_app();
16655 app.current_service = Service::S3Buckets;
16656 app.service_selected = true;
16657 app.mode = Mode::Normal;
16658
16659 app.s3_state.buckets.items = (0..150)
16661 .map(|i| S3Bucket {
16662 name: format!("bucket{:03}", i),
16663 region: "us-east-1".to_string(),
16664 creation_date: String::new(),
16665 })
16666 .collect();
16667
16668 app.s3_state.bucket_visible_rows.set(20);
16669 app.s3_state.selected_row = 0;
16670 app.s3_state.bucket_scroll_offset = 0;
16671
16672 app.go_to_page(2);
16674
16675 assert_eq!(app.s3_state.selected_row, 50);
16676 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
16678
16679 app.go_to_page(3);
16681
16682 assert_eq!(app.s3_state.selected_row, 100);
16683 assert_eq!(app.s3_state.bucket_scroll_offset, 100);
16684
16685 app.go_to_page(1);
16687
16688 assert_eq!(app.s3_state.selected_row, 0);
16689 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
16690 }
16691
16692 #[test]
16693 fn test_s3_bucket_pagination_uses_page_size() {
16694 use S3Bucket;
16695
16696 let mut app = test_app();
16697 app.current_service = Service::S3Buckets;
16698 app.service_selected = true;
16699 app.mode = Mode::Normal;
16700
16701 app.s3_state.buckets.items = (0..100)
16703 .map(|i| S3Bucket {
16704 name: format!("bucket{:03}", i),
16705 region: "us-east-1".to_string(),
16706 creation_date: String::new(),
16707 })
16708 .collect();
16709
16710 app.s3_state.bucket_visible_rows.set(20);
16711 app.s3_state.selected_row = 0;
16712
16713 assert_eq!(app.s3_state.buckets.page_size.value(), 50);
16715
16716 app.go_to_page(2);
16718 assert_eq!(app.s3_state.selected_row, 50);
16719 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
16720
16721 app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
16723 assert_eq!(app.s3_state.buckets.page_size.value(), 25);
16724
16725 app.go_to_page(2);
16727 assert_eq!(app.s3_state.selected_row, 25);
16728 assert_eq!(app.s3_state.bucket_scroll_offset, 25);
16729 }
16730
16731 #[test]
16732 fn test_s3_bucket_page_size_limits_visible_rows() {
16733 use S3Bucket;
16734
16735 let mut app = test_app();
16736 app.current_service = Service::S3Buckets;
16737 app.service_selected = true;
16738 app.mode = Mode::Normal;
16739
16740 app.s3_state.buckets.items = (0..100)
16742 .map(|i| S3Bucket {
16743 name: format!("bucket{:03}", i),
16744 region: "us-east-1".to_string(),
16745 creation_date: String::new(),
16746 })
16747 .collect();
16748
16749 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16751 assert_eq!(app.s3_state.buckets.page_size.value(), 10);
16752
16753 let total_rows = app.calculate_total_bucket_rows();
16755 assert!(total_rows >= 10, "Should have at least 10 rows");
16759 }
16760
16761 #[test]
16762 fn test_s3_bucket_tab_cycling_in_filter() {
16763 use crate::common::InputFocus;
16764
16765 let mut app = test_app();
16766 app.current_service = Service::S3Buckets;
16767 app.mode = Mode::FilterInput;
16768
16769 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
16771
16772 app.handle_action(Action::NextFilterFocus);
16774 assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
16775
16776 app.handle_action(Action::NextFilterFocus);
16778 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
16779 }
16780
16781 #[test]
16782 fn test_s3_bucket_pagination_navigation_with_arrows() {
16783 use S3Bucket;
16784
16785 let mut app = test_app();
16786 app.current_service = Service::S3Buckets;
16787 app.mode = Mode::FilterInput;
16788
16789 app.s3_state.buckets.items = (0..100)
16791 .map(|i| S3Bucket {
16792 name: format!("bucket{:03}", i),
16793 region: "us-east-1".to_string(),
16794 creation_date: String::new(),
16795 })
16796 .collect();
16797
16798 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16799 app.s3_state.selected_row = 0;
16800
16801 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
16803
16804 app.handle_action(Action::NextItem);
16806 assert_eq!(app.s3_state.selected_row, 10);
16807
16808 app.handle_action(Action::NextItem);
16810 assert_eq!(app.s3_state.selected_row, 20);
16811
16812 app.handle_action(Action::PrevItem);
16814 assert_eq!(app.s3_state.selected_row, 10);
16815 }
16816
16817 #[test]
16818 fn test_s3_bucket_go_to_page_shows_correct_buckets() {
16819 use S3Bucket;
16820
16821 let mut app = test_app();
16822 app.current_service = Service::S3Buckets;
16823 app.service_selected = true;
16824 app.mode = Mode::Normal;
16825
16826 app.s3_state.buckets.items = (0..100)
16828 .map(|i| S3Bucket {
16829 name: format!("bucket{:03}", i),
16830 region: "us-east-1".to_string(),
16831 creation_date: String::new(),
16832 })
16833 .collect();
16834
16835 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16836
16837 app.go_to_page(2);
16839 assert_eq!(app.s3_state.selected_row, 10);
16840
16841 app.go_to_page(5);
16843 assert_eq!(app.s3_state.selected_row, 40);
16844 }
16845
16846 #[test]
16847 fn test_s3_bucket_left_right_arrows_change_pages() {
16848 use S3Bucket;
16849
16850 let mut app = test_app();
16851 app.current_service = Service::S3Buckets;
16852 app.mode = Mode::FilterInput;
16853
16854 app.s3_state.buckets.items = (0..100)
16856 .map(|i| S3Bucket {
16857 name: format!("bucket{:03}", i),
16858 region: "us-east-1".to_string(),
16859 creation_date: String::new(),
16860 })
16861 .collect();
16862
16863 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16864 app.s3_state.selected_row = 0;
16865 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
16866
16867 app.handle_action(Action::PageDown);
16869 assert_eq!(app.s3_state.selected_row, 10);
16870
16871 app.handle_action(Action::PageDown);
16873 assert_eq!(app.s3_state.selected_row, 20);
16874
16875 app.handle_action(Action::PageUp);
16877 assert_eq!(app.s3_state.selected_row, 10);
16878 }
16879
16880 #[test]
16881 fn test_s3_bucket_preview_uses_bucket_region_not_config_region() {
16882 use S3Bucket;
16890 let mut app = test_app();
16891 app.config.region = "ap-southeast-3".to_string(); app.s3_state.buckets.items = vec![S3Bucket {
16895 name: "my-bucket".to_string(),
16896 region: "us-east-1".to_string(), creation_date: String::new(),
16898 }];
16899
16900 let bucket = app
16902 .s3_state
16903 .buckets
16904 .items
16905 .iter()
16906 .find(|b| b.name == "my-bucket")
16907 .unwrap();
16908 let effective_region = if bucket.region.is_empty() {
16909 app.config.region.as_str()
16910 } else {
16911 bucket.region.as_str()
16912 };
16913 assert_eq!(
16914 effective_region, "us-east-1",
16915 "load_bucket_preview must use bucket's region, not app config region"
16916 );
16917 assert_ne!(
16918 effective_region, "ap-southeast-3",
16919 "Must NOT fall back to configured region for cross-region buckets"
16920 );
16921 }
16922
16923 #[test]
16924 fn test_s3_scroll_follows_selection_after_bucket_expand() {
16925 use S3Bucket;
16926 use S3Object;
16927 let mut app = test_app();
16930 app.current_service = Service::S3Buckets;
16931 app.service_selected = true;
16932 app.mode = Mode::Normal;
16933
16934 app.s3_state.buckets.items = (0..5)
16936 .map(|i| S3Bucket {
16937 name: format!("bucket{}", i),
16938 region: "us-east-1".to_string(),
16939 creation_date: String::new(),
16940 })
16941 .collect();
16942
16943 app.s3_state.bucket_visible_rows = std::cell::Cell::new(3);
16944
16945 app.s3_state.selected_row = 2;
16947 app.s3_state.bucket_scroll_offset = 0;
16948
16949 app.s3_state.bucket_preview.insert(
16951 "bucket2".to_string(),
16952 vec![
16953 S3Object {
16954 key: "file1.txt".to_string(),
16955 is_prefix: false,
16956 size: 100,
16957 last_modified: String::new(),
16958 storage_class: String::new(),
16959 },
16960 S3Object {
16961 key: "file2.txt".to_string(),
16962 is_prefix: false,
16963 size: 200,
16964 last_modified: String::new(),
16965 storage_class: String::new(),
16966 },
16967 ],
16968 );
16969
16970 app.handle_action(Action::CollapseRow); app.handle_action(Action::NextPane); let visible = app.s3_state.bucket_visible_rows.get();
16978 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
16979 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
16980 assert!(
16981 in_view,
16982 "selected_row {} must be visible in [{}, {})",
16983 app.s3_state.selected_row,
16984 app.s3_state.bucket_scroll_offset,
16985 app.s3_state.bucket_scroll_offset + visible
16986 );
16987 }
16988
16989 #[test]
16990 fn test_s3_expand_without_preview_does_not_move_selection() {
16991 use S3Bucket;
16995 let mut app = test_app();
16996 app.current_service = Service::S3Buckets;
16997 app.service_selected = true;
16998 app.mode = Mode::Normal;
16999
17000 app.s3_state.buckets.items = vec![
17001 S3Bucket {
17002 name: "bucket0".to_string(),
17003 region: "us-east-1".to_string(),
17004 creation_date: String::new(),
17005 },
17006 S3Bucket {
17007 name: "bucket1".to_string(),
17008 region: "us-east-1".to_string(),
17009 creation_date: String::new(),
17010 },
17011 ];
17012 app.s3_state.selected_row = 0; app.handle_action(Action::NextPane); assert_eq!(
17019 app.s3_state.selected_row, 0,
17020 "Selection must not move before preview is loaded"
17021 );
17022 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
17024 assert!(app.s3_state.buckets.loading);
17025 }
17026
17027 #[test]
17028 fn test_s3_expand_with_preview_moves_to_first_child() {
17029 use crate::s3::Object as S3Object;
17031 use S3Bucket;
17032 let mut app = test_app();
17033 app.current_service = Service::S3Buckets;
17034 app.service_selected = true;
17035 app.mode = Mode::Normal;
17036
17037 app.s3_state.buckets.items = vec![
17038 S3Bucket {
17039 name: "bucket0".to_string(),
17040 region: "us-east-1".to_string(),
17041 creation_date: String::new(),
17042 },
17043 S3Bucket {
17044 name: "bucket1".to_string(),
17045 region: "us-east-1".to_string(),
17046 creation_date: String::new(),
17047 },
17048 ];
17049 app.s3_state.selected_row = 0;
17050 app.s3_state.bucket_preview.insert(
17051 "bucket0".to_string(),
17052 vec![S3Object {
17053 key: "file.txt".to_string(),
17054 is_prefix: false,
17055 size: 0,
17056 last_modified: String::new(),
17057 storage_class: String::new(),
17058 }],
17059 );
17060 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17061
17062 app.handle_action(Action::NextPane);
17064 assert_eq!(app.s3_state.selected_row, 1, "Should move to first child");
17065 }
17066
17067 #[test]
17068 fn test_s3_expand_then_load_then_right_goes_to_first_child_not_sibling() {
17069 use crate::s3::Object as S3Object;
17073 use S3Bucket;
17074 let mut app = test_app();
17075 app.current_service = Service::S3Buckets;
17076 app.service_selected = true;
17077 app.mode = Mode::Normal;
17078 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17079
17080 app.s3_state.buckets.items = vec![
17081 S3Bucket {
17082 name: "bucket0".to_string(),
17083 region: "us-east-1".to_string(),
17084 creation_date: String::new(),
17085 },
17086 S3Bucket {
17087 name: "bucket1".to_string(),
17088 region: "us-east-1".to_string(),
17089 creation_date: String::new(),
17090 },
17091 ];
17092 app.s3_state.selected_row = 0;
17093
17094 app.handle_action(Action::NextPane);
17096 assert_eq!(
17097 app.s3_state.selected_row, 0,
17098 "Must stay on bucket0 while loading"
17099 );
17100 assert!(app.s3_state.buckets.loading);
17101
17102 app.s3_state.bucket_preview.insert(
17104 "bucket0".to_string(),
17105 vec![S3Object {
17106 key: "file.txt".to_string(),
17107 is_prefix: false,
17108 size: 0,
17109 last_modified: String::new(),
17110 storage_class: String::new(),
17111 }],
17112 );
17113 app.s3_state.buckets.loading = false;
17114
17115 app.handle_action(Action::NextPane);
17117 assert_eq!(
17118 app.s3_state.selected_row, 1,
17119 "After preview loads, right arrow must select first child (row 1), not sibling (row 2)"
17120 );
17121 }
17122
17123 #[test]
17124 fn test_s3_first_expand_never_moves_selection_before_children_visible() {
17125 use S3Bucket;
17129 let mut app = test_app();
17130 app.current_service = Service::S3Buckets;
17131 app.service_selected = true;
17132 app.mode = Mode::Normal;
17133 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17134
17135 app.s3_state.buckets.items = vec![
17136 S3Bucket {
17137 name: "bucket0".to_string(),
17138 region: "us-east-1".to_string(),
17139 creation_date: String::new(),
17140 },
17141 S3Bucket {
17142 name: "bucket1".to_string(),
17143 region: "us-east-1".to_string(),
17144 creation_date: String::new(),
17145 },
17146 ];
17147 app.s3_state.selected_row = 0;
17149
17150 app.handle_action(Action::ExpandRow);
17152 assert_eq!(
17153 app.s3_state.selected_row, 0,
17154 "Selection must stay on bucket0 until children are visible"
17155 );
17156 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
17157 assert!(app.s3_state.buckets.loading);
17158 }
17159
17160 #[test]
17161 fn test_s3_after_preview_loads_selection_moves_to_first_child() {
17162 use crate::s3::Object as S3Object;
17165 use S3Bucket;
17166 let mut app = test_app();
17167 app.current_service = Service::S3Buckets;
17168 app.service_selected = true;
17169 app.mode = Mode::Normal;
17170 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17171 app.s3_state.buckets.items = vec![
17172 S3Bucket {
17173 name: "b0".to_string(),
17174 region: "us-east-1".to_string(),
17175 creation_date: String::new(),
17176 },
17177 S3Bucket {
17178 name: "b1".to_string(),
17179 region: "us-east-1".to_string(),
17180 creation_date: String::new(),
17181 },
17182 ];
17183 app.s3_state.selected_row = 0;
17184
17185 app.handle_action(Action::ExpandRow);
17187 assert_eq!(
17188 app.s3_state.selected_row, 0,
17189 "Must stay on bucket while loading"
17190 );
17191
17192 app.s3_state.bucket_preview.insert(
17194 "b0".to_string(),
17195 vec![S3Object {
17196 key: "f.txt".to_string(),
17197 is_prefix: false,
17198 size: 0,
17199 last_modified: String::new(),
17200 storage_class: String::new(),
17201 }],
17202 );
17203 app.after_bucket_preview_loaded("b0");
17205 assert_eq!(
17206 app.s3_state.selected_row, 1,
17207 "After preview loads, selection must advance to first child (row 1)"
17208 );
17209 }
17210
17211 #[test]
17212 fn test_s3_right_arrow_on_expanded_loaded_bucket_enters_first_child() {
17213 use crate::s3::Object as S3Object;
17216 use S3Bucket;
17217 let mut app = test_app();
17218 app.current_service = Service::S3Buckets;
17219 app.service_selected = true;
17220 app.mode = Mode::Normal;
17221 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17222 app.s3_state.buckets.items = vec![
17223 S3Bucket {
17224 name: "b0".to_string(),
17225 region: "us-east-1".to_string(),
17226 creation_date: String::new(),
17227 },
17228 S3Bucket {
17229 name: "b1".to_string(),
17230 region: "us-east-1".to_string(),
17231 creation_date: String::new(),
17232 },
17233 ];
17234 app.s3_state.expanded_prefixes.insert("b0".to_string());
17235 app.s3_state.bucket_preview.insert(
17236 "b0".to_string(),
17237 vec![S3Object {
17238 key: "f.txt".to_string(),
17239 is_prefix: false,
17240 size: 0,
17241 last_modified: String::new(),
17242 storage_class: String::new(),
17243 }],
17244 );
17245 app.s3_state.selected_row = 0;
17246
17247 app.handle_action(Action::ExpandRow);
17248
17249 assert!(
17251 app.s3_state.expanded_prefixes.contains("b0"),
17252 "Must stay expanded"
17253 );
17254 assert_eq!(app.s3_state.selected_row, 1, "Must move to first child");
17255 }
17256
17257 #[test]
17258 fn test_s3_second_expand_press_collapses_when_already_expanded_with_children() {
17259 use crate::s3::Object as S3Object;
17262 use S3Bucket;
17263 let mut app = test_app();
17264 app.current_service = Service::S3Buckets;
17265 app.service_selected = true;
17266 app.mode = Mode::Normal;
17267 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17268 app.s3_state.buckets.items = vec![S3Bucket {
17269 name: "bucket0".to_string(),
17270 region: "us-east-1".to_string(),
17271 creation_date: String::new(),
17272 }];
17273 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17274 app.s3_state.bucket_preview.insert(
17275 "bucket0".to_string(),
17276 vec![S3Object {
17277 key: "file.txt".to_string(),
17278 is_prefix: false,
17279 size: 0,
17280 last_modified: String::new(),
17281 storage_class: String::new(),
17282 }],
17283 );
17284 app.s3_state.selected_row = 0;
17285
17286 app.handle_action(Action::CollapseRow);
17288 assert!(
17289 !app.s3_state.expanded_prefixes.contains("bucket0"),
17290 "Left arrow must collapse the bucket"
17291 );
17292 assert_eq!(app.s3_state.selected_row, 0);
17293 }
17294
17295 #[test]
17296 fn test_s3_expand_row_index_correct_when_prior_bucket_has_error() {
17297 use S3Bucket;
17303 let mut app = test_app();
17304 app.current_service = Service::S3Buckets;
17305 app.service_selected = true;
17306 app.mode = Mode::Normal;
17307 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17308
17309 app.s3_state.buckets.items = vec![
17310 S3Bucket {
17311 name: "bucket0".to_string(),
17312 region: "us-east-1".to_string(),
17313 creation_date: String::new(),
17314 },
17315 S3Bucket {
17316 name: "bucket1".to_string(),
17317 region: "us-east-1".to_string(),
17318 creation_date: String::new(),
17319 },
17320 S3Bucket {
17321 name: "bucket2".to_string(),
17322 region: "us-east-1".to_string(),
17323 creation_date: String::new(),
17324 },
17325 ];
17326
17327 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17329 app.s3_state
17330 .bucket_errors
17331 .insert("bucket0".to_string(), "AccessDenied".to_string());
17332
17333 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
17336 assert_eq!(
17337 total, 4,
17338 "Should be 4 rows: bucket0 + error + bucket1 + bucket2"
17339 );
17340
17341 app.s3_state.selected_row = 3;
17343
17344 use crate::s3::Object as S3Object;
17346 app.s3_state.bucket_preview.insert(
17347 "bucket2".to_string(),
17348 vec![S3Object {
17349 key: "file.txt".to_string(),
17350 is_prefix: false,
17351 size: 0,
17352 last_modified: String::new(),
17353 storage_class: String::new(),
17354 }],
17355 );
17356
17357 app.handle_action(Action::NextPane);
17358
17359 assert!(
17361 app.s3_state.expanded_prefixes.contains("bucket2"),
17362 "Must expand bucket2, not bucket1"
17363 );
17364 assert_eq!(
17365 app.s3_state.selected_row, 4,
17366 "First child of bucket2 must be row 4"
17367 );
17368 }
17369
17370 #[test]
17371 fn test_s3_next_pane_row_count_matches_calculate_filtered_bucket_rows() {
17372 use crate::s3::Object as S3Object;
17376 use S3Bucket;
17377 let mut app = test_app();
17378 app.current_service = Service::S3Buckets;
17379 app.service_selected = true;
17380 app.mode = Mode::Normal;
17381 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17382
17383 app.s3_state.buckets.items = vec![
17384 S3Bucket {
17385 name: "bucket0".to_string(),
17386 region: "us-east-1".to_string(),
17387 creation_date: String::new(),
17388 },
17389 S3Bucket {
17390 name: "bucket1".to_string(),
17391 region: "us-east-1".to_string(),
17392 creation_date: String::new(),
17393 },
17394 S3Bucket {
17395 name: "bucket2".to_string(),
17396 region: "us-east-1".to_string(),
17397 creation_date: String::new(),
17398 },
17399 ];
17400
17401 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17403 app.s3_state.bucket_preview.insert(
17404 "bucket0".to_string(),
17405 vec![
17406 S3Object {
17407 key: "a.txt".to_string(),
17408 is_prefix: false,
17409 size: 0,
17410 last_modified: String::new(),
17411 storage_class: String::new(),
17412 },
17413 S3Object {
17414 key: "b.txt".to_string(),
17415 is_prefix: false,
17416 size: 0,
17417 last_modified: String::new(),
17418 storage_class: String::new(),
17419 },
17420 S3Object {
17421 key: "c.txt".to_string(),
17422 is_prefix: false,
17423 size: 0,
17424 last_modified: String::new(),
17425 storage_class: String::new(),
17426 },
17427 ],
17428 );
17429
17430 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17432
17433 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
17435 assert_eq!(
17436 total, 6,
17437 "Total rows: bucket0(1)+3children+bucket1(1)+bucket2(1)=6"
17438 );
17439
17440 app.s3_state.selected_row = 5;
17443
17444 app.s3_state.bucket_preview.insert(
17446 "bucket2".to_string(),
17447 vec![S3Object {
17448 key: "x.txt".to_string(),
17449 is_prefix: false,
17450 size: 0,
17451 last_modified: String::new(),
17452 storage_class: String::new(),
17453 }],
17454 );
17455
17456 app.handle_action(Action::NextPane);
17457
17458 assert!(
17459 app.s3_state.expanded_prefixes.contains("bucket2"),
17460 "Must expand bucket2, not bucket1"
17461 );
17462 assert_eq!(
17463 app.s3_state.selected_row, 6,
17464 "First child of bucket2 must be row 6"
17465 );
17466 }
17467
17468 #[test]
17469 fn test_s3_expand_row_index_correct_when_prior_bucket_has_loaded_children() {
17470 use crate::s3::Object as S3Object;
17475 use S3Bucket;
17476 let mut app = test_app();
17477 app.current_service = Service::S3Buckets;
17478 app.service_selected = true;
17479 app.mode = Mode::Normal;
17480 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17481
17482 app.s3_state.buckets.items = vec![
17483 S3Bucket {
17484 name: "bucket0".to_string(),
17485 region: "us-east-1".to_string(),
17486 creation_date: String::new(),
17487 },
17488 S3Bucket {
17489 name: "bucket1".to_string(),
17490 region: "us-east-1".to_string(),
17491 creation_date: String::new(),
17492 },
17493 S3Bucket {
17494 name: "bucket2".to_string(),
17495 region: "us-east-1".to_string(),
17496 creation_date: String::new(),
17497 },
17498 ];
17499
17500 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17502 app.s3_state.bucket_preview.insert(
17503 "bucket0".to_string(),
17504 vec![
17505 S3Object {
17506 key: "a.txt".to_string(),
17507 is_prefix: false,
17508 size: 0,
17509 last_modified: String::new(),
17510 storage_class: String::new(),
17511 },
17512 S3Object {
17513 key: "b.txt".to_string(),
17514 is_prefix: false,
17515 size: 0,
17516 last_modified: String::new(),
17517 storage_class: String::new(),
17518 },
17519 ],
17520 );
17521
17522 app.s3_state.selected_row = 4;
17525
17526 app.s3_state.bucket_preview.insert(
17528 "bucket2".to_string(),
17529 vec![S3Object {
17530 key: "c.txt".to_string(),
17531 is_prefix: false,
17532 size: 0,
17533 last_modified: String::new(),
17534 storage_class: String::new(),
17535 }],
17536 );
17537
17538 app.handle_action(Action::NextPane);
17539
17540 assert!(
17542 app.s3_state.expanded_prefixes.contains("bucket2"),
17543 "bucket2 must be expanded"
17544 );
17545 assert_eq!(
17546 app.s3_state.selected_row, 5,
17547 "Must select first child of bucket2 (row 5), not some wrong row"
17548 );
17549 assert!(
17550 !app.s3_state.expanded_prefixes.contains("bucket1"),
17551 "bucket1 must NOT be expanded"
17552 );
17553 }
17554
17555 #[test]
17556 fn test_s3_expand_with_empty_preview_stays_on_bucket() {
17557 use S3Bucket;
17560 let mut app = test_app();
17561 app.current_service = Service::S3Buckets;
17562 app.service_selected = true;
17563 app.mode = Mode::Normal;
17564 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17565
17566 app.s3_state.buckets.items = vec![
17567 S3Bucket {
17568 name: "bucket0".to_string(),
17569 region: "us-east-1".to_string(),
17570 creation_date: String::new(),
17571 },
17572 S3Bucket {
17573 name: "bucket1".to_string(),
17574 region: "us-east-1".to_string(),
17575 creation_date: String::new(),
17576 },
17577 S3Bucket {
17578 name: "bucket2".to_string(),
17579 region: "us-east-1".to_string(),
17580 creation_date: String::new(),
17581 },
17582 ];
17583 app.s3_state
17585 .bucket_preview
17586 .insert("bucket1".to_string(), vec![]);
17587 app.s3_state.selected_row = 1; app.handle_action(Action::NextPane);
17590
17591 assert_eq!(
17593 app.s3_state.selected_row, 1,
17594 "Empty bucket: selection must stay on bucket row, not advance to next sibling"
17595 );
17596 assert!(
17597 app.s3_state.expanded_prefixes.contains("bucket1"),
17598 "Bucket must be expanded (showing ▼)"
17599 );
17600 }
17601
17602 #[test]
17603 fn test_s3_expand_with_preceding_loading_buckets_on_prior_page() {
17604 use crate::s3::Object as S3Object;
17609 use S3Bucket;
17610 let mut app = test_app();
17611 app.current_service = Service::S3Buckets;
17612 app.service_selected = true;
17613 app.mode = Mode::Normal;
17614 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17615
17616 app.s3_state.buckets.items = (0..60)
17618 .map(|i| S3Bucket {
17619 name: format!("bucket{:02}", i),
17620 region: "us-east-1".to_string(),
17621 creation_date: String::new(),
17622 })
17623 .collect();
17624
17625 for i in 0..3usize {
17627 app.s3_state
17628 .expanded_prefixes
17629 .insert(format!("bucket{:02}", i));
17630 }
17631
17632 app.s3_state.bucket_scroll_offset = 50;
17633
17634 app.s3_state.bucket_preview.insert(
17636 "bucket55".to_string(),
17637 vec![S3Object {
17638 key: "file.txt".to_string(),
17639 is_prefix: false,
17640 size: 0,
17641 last_modified: String::new(),
17642 storage_class: String::new(),
17643 }],
17644 );
17645
17646 app.s3_state.selected_row = 55;
17648
17649 app.handle_action(Action::NextPane);
17650
17651 assert!(
17653 app.s3_state.expanded_prefixes.contains("bucket55"),
17654 "bucket55 must be expanded"
17655 );
17656 assert_eq!(
17657 app.s3_state.selected_row, 56,
17658 "Must select first child of bucket55 (row 56)"
17659 );
17660 }
17661
17662 #[test]
17663 fn test_s3_expand_with_multiple_preceding_loading_buckets() {
17664 use crate::s3::Object as S3Object;
17667 use S3Bucket;
17668 let mut app = test_app();
17669 app.current_service = Service::S3Buckets;
17670 app.service_selected = true;
17671 app.mode = Mode::Normal;
17672 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17673
17674 app.s3_state.buckets.items = (0..5)
17675 .map(|i| S3Bucket {
17676 name: format!("bucket{}", i),
17677 region: "us-east-1".to_string(),
17678 creation_date: String::new(),
17679 })
17680 .collect();
17681
17682 for i in 0..3 {
17684 app.s3_state
17685 .expanded_prefixes
17686 .insert(format!("bucket{}", i));
17687 }
17688
17689 app.s3_state.bucket_preview.insert(
17691 "bucket3".to_string(),
17692 vec![S3Object {
17693 key: "file.txt".to_string(),
17694 is_prefix: false,
17695 size: 0,
17696 last_modified: String::new(),
17697 storage_class: String::new(),
17698 }],
17699 );
17700
17701 app.s3_state.selected_row = 3; app.handle_action(Action::NextPane);
17706
17707 assert!(
17709 app.s3_state.expanded_prefixes.contains("bucket3"),
17710 "bucket3 must be expanded"
17711 );
17712 assert_eq!(
17713 app.s3_state.selected_row, 4,
17714 "Must select first child of bucket3 (row 4)"
17715 );
17716 }
17717
17718 #[test]
17719 fn test_s3_expand_with_preceding_loading_bucket_selects_correct_row() {
17720 use crate::s3::Object as S3Object;
17725 use S3Bucket;
17726 let mut app = test_app();
17727 app.current_service = Service::S3Buckets;
17728 app.service_selected = true;
17729 app.mode = Mode::Normal;
17730 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17731
17732 app.s3_state.buckets.items = vec![
17733 S3Bucket {
17734 name: "bucket0".to_string(),
17735 region: "us-east-1".to_string(),
17736 creation_date: String::new(),
17737 },
17738 S3Bucket {
17739 name: "bucket1".to_string(),
17740 region: "us-east-1".to_string(),
17741 creation_date: String::new(),
17742 },
17743 S3Bucket {
17744 name: "bucket2".to_string(),
17745 region: "us-east-1".to_string(),
17746 creation_date: String::new(),
17747 },
17748 ];
17749
17750 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17752 app.s3_state.buckets.loading = true;
17753
17754 app.s3_state.bucket_preview.insert(
17756 "bucket1".to_string(),
17757 vec![S3Object {
17758 key: "file.txt".to_string(),
17759 is_prefix: false,
17760 size: 0,
17761 last_modified: String::new(),
17762 storage_class: String::new(),
17763 }],
17764 );
17765
17766 app.s3_state.selected_row = 1;
17768
17769 app.handle_action(Action::NextPane);
17771
17772 assert_eq!(
17774 app.s3_state.selected_row, 2,
17775 "Must expand bucket1's first child (row 2), not bucket2 (row 3)"
17776 );
17777 assert!(
17778 app.s3_state.expanded_prefixes.contains("bucket1"),
17779 "bucket1 must be in expanded_prefixes"
17780 );
17781 }
17782
17783 #[test]
17784 fn test_s3_expand_last_row_on_page_shows_first_child_not_next_page() {
17785 use crate::s3::Object as S3Object;
17790 use S3Bucket;
17791 let mut app = test_app();
17792 app.current_service = Service::S3Buckets;
17793 app.service_selected = true;
17794 app.mode = Mode::Normal;
17795
17796 app.s3_state.buckets.items = vec![
17798 S3Bucket {
17799 name: "bucket0".to_string(),
17800 region: "us-east-1".to_string(),
17801 creation_date: String::new(),
17802 },
17803 S3Bucket {
17804 name: "bucket1".to_string(),
17805 region: "us-east-1".to_string(),
17806 creation_date: String::new(),
17807 },
17808 S3Bucket {
17809 name: "bucket2".to_string(),
17810 region: "us-east-1".to_string(),
17811 creation_date: String::new(),
17812 },
17813 ];
17814 app.s3_state.bucket_visible_rows = std::cell::Cell::new(2);
17815 app.s3_state.selected_row = 1;
17817 app.s3_state.bucket_scroll_offset = 0;
17818
17819 app.s3_state.bucket_preview.insert(
17821 "bucket1".to_string(),
17822 vec![S3Object {
17823 key: "file.txt".to_string(),
17824 is_prefix: false,
17825 size: 0,
17826 last_modified: String::new(),
17827 storage_class: String::new(),
17828 }],
17829 );
17830
17831 app.handle_action(Action::NextPane); assert_eq!(app.s3_state.selected_row, 2, "Must select first child");
17835
17836 let visible = app.s3_state.bucket_visible_rows.get();
17838 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
17839 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
17840 assert!(
17841 in_view,
17842 "First child row {} must be visible in [{}, {})",
17843 app.s3_state.selected_row,
17844 app.s3_state.bucket_scroll_offset,
17845 app.s3_state.bucket_scroll_offset + visible
17846 );
17847 }
17848
17849 #[test]
17850 fn test_apig_detail_tab_navigation() {
17851 use crate::apig::api::RestApi;
17852 use crate::ui::apig::ApiDetailTab;
17853
17854 let mut app = test_app();
17855 app.current_service = Service::ApiGatewayApis;
17856 app.apig_state.current_api = Some(RestApi {
17857 id: "test123".to_string(),
17858 name: "Test API".to_string(),
17859 description: "Test".to_string(),
17860 created_date: "2024-01-01".to_string(),
17861 api_key_source: "HEADER".to_string(),
17862 endpoint_configuration: "REGIONAL".to_string(),
17863 protocol_type: "REST".to_string(),
17864 disable_execute_api_endpoint: false,
17865 status: "AVAILABLE".to_string(),
17866 });
17867
17868 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17870
17871 app.handle_action(Action::NextDetailTab);
17873 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17874
17875 app.handle_action(Action::PrevDetailTab);
17877 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17878
17879 app.handle_action(Action::NextDetailTab);
17881 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17882
17883 app.handle_action(Action::PrevDetailTab);
17885 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17886
17887 app.handle_action(Action::NextDetailTab);
17889 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17890 }
17891
17892 #[test]
17893 fn test_apig_routes_expand_collapse() {
17894 use crate::apig::api::RestApi;
17895 use crate::apig::route::Route;
17896 use crate::ui::apig::ApiDetailTab;
17897
17898 let mut app = test_app();
17899 app.current_service = Service::ApiGatewayApis;
17900 app.apig_state.current_api = Some(RestApi {
17901 id: "test123".to_string(),
17902 name: "Test API".to_string(),
17903 description: "Test".to_string(),
17904 created_date: "2024-01-01".to_string(),
17905 api_key_source: "HEADER".to_string(),
17906 endpoint_configuration: "REGIONAL".to_string(),
17907 protocol_type: "HTTP".to_string(),
17908 disable_execute_api_endpoint: false,
17909 status: "AVAILABLE".to_string(),
17910 });
17911 app.apig_state.detail_tab = ApiDetailTab::Routes;
17912
17913 let virtual_parent = Route {
17915 route_id: "virtual_/api".to_string(),
17916 route_key: "/api".to_string(),
17917 target: String::new(), authorization_type: String::new(),
17919 api_key_required: false,
17920 display_name: String::new(),
17921 arn: String::new(),
17922 };
17923 let child_route = Route {
17924 route_id: "1".to_string(),
17925 route_key: "/api/users".to_string(),
17926 target: "integration1".to_string(),
17927 authorization_type: "NONE".to_string(),
17928 api_key_required: false,
17929 display_name: String::new(),
17930 arn: String::new(),
17931 };
17932
17933 app.apig_state.routes.items = vec![virtual_parent];
17934 app.apig_state
17935 .route_children
17936 .insert("/api".to_string(), vec![child_route]);
17937
17938 assert!(app.apig_state.expanded_routes.is_empty());
17940
17941 app.apig_state.routes.selected = 0;
17943 app.handle_action(Action::ExpandRow);
17944 assert!(app.apig_state.expanded_routes.contains("/api"));
17945
17946 app.handle_action(Action::CollapseRow);
17948 assert!(!app.apig_state.expanded_routes.contains("/api"));
17949
17950 app.handle_action(Action::ExpandRow);
17952 assert!(app.apig_state.expanded_routes.contains("/api"));
17953 }
17954
17955 #[test]
17956 fn test_apig_routes_navigation() {
17957 use crate::apig::api::RestApi;
17958 use crate::apig::route::Route;
17959 use crate::ui::apig::ApiDetailTab;
17960
17961 let mut app = test_app();
17962 app.mode = Mode::Normal;
17963 app.service_selected = true;
17964 app.current_service = Service::ApiGatewayApis;
17965 app.apig_state.current_api = Some(RestApi {
17966 id: "test123".to_string(),
17967 name: "Test API".to_string(),
17968 description: "Test".to_string(),
17969 created_date: "2024-01-01".to_string(),
17970 api_key_source: "HEADER".to_string(),
17971 endpoint_configuration: "REGIONAL".to_string(),
17972 protocol_type: "HTTP".to_string(),
17973 disable_execute_api_endpoint: false,
17974 status: "AVAILABLE".to_string(),
17975 });
17976 app.apig_state.detail_tab = ApiDetailTab::Routes;
17977 app.apig_state.routes.items = vec![
17978 Route {
17979 route_id: "1".to_string(),
17980 route_key: "/api/users".to_string(),
17981 target: "integration1".to_string(),
17982 authorization_type: "NONE".to_string(),
17983 api_key_required: false,
17984 display_name: String::new(),
17985 arn: String::new(),
17986 },
17987 Route {
17988 route_id: "2".to_string(),
17989 route_key: "/health".to_string(),
17990 target: "integration2".to_string(),
17991 authorization_type: "NONE".to_string(),
17992 api_key_required: false,
17993 display_name: String::new(),
17994 arn: String::new(),
17995 },
17996 ];
17997
17998 assert_eq!(app.apig_state.routes.selected, 0);
18000
18001 app.handle_action(Action::NextItem);
18003 assert_eq!(app.apig_state.routes.selected, 1);
18004
18005 app.handle_action(Action::NextItem);
18007 assert_eq!(app.apig_state.routes.selected, 1);
18008
18009 app.handle_action(Action::PrevItem);
18011 assert_eq!(app.apig_state.routes.selected, 0);
18012
18013 app.handle_action(Action::PrevItem);
18015 assert_eq!(app.apig_state.routes.selected, 0);
18016 }
18017
18018 #[test]
18019 fn test_apig_routes_expand_jumps_to_child() {
18020 use crate::apig::api::RestApi;
18021 use crate::apig::route::Route;
18022 use crate::ui::apig::ApiDetailTab;
18023 use std::collections::HashMap;
18024
18025 let mut app = test_app();
18026 app.mode = Mode::Normal;
18027 app.service_selected = true;
18028 app.current_service = Service::ApiGatewayApis;
18029 app.apig_state.current_api = Some(RestApi {
18030 id: "test123".to_string(),
18031 name: "Test API".to_string(),
18032 description: "Test".to_string(),
18033 created_date: "2024-01-01".to_string(),
18034 api_key_source: "HEADER".to_string(),
18035 endpoint_configuration: "REGIONAL".to_string(),
18036 protocol_type: "HTTP".to_string(),
18037 disable_execute_api_endpoint: false,
18038 status: "AVAILABLE".to_string(),
18039 });
18040 app.apig_state.detail_tab = ApiDetailTab::Routes;
18041
18042 app.apig_state.routes.items = vec![Route {
18044 route_id: "virtual_/api".to_string(),
18045 route_key: "/api".to_string(),
18046 target: String::new(),
18047 authorization_type: String::new(),
18048 api_key_required: false,
18049 display_name: String::new(),
18050 arn: String::new(),
18051 }];
18052
18053 let mut children = HashMap::new();
18054 children.insert(
18055 "/api".to_string(),
18056 vec![Route {
18057 route_id: "1".to_string(),
18058 route_key: "/api/users".to_string(),
18059 target: "integration1".to_string(),
18060 authorization_type: "NONE".to_string(),
18061 api_key_required: false,
18062 display_name: String::new(),
18063 arn: String::new(),
18064 }],
18065 );
18066 app.apig_state.route_children = children;
18067
18068 assert_eq!(app.apig_state.routes.selected, 0);
18070 assert!(!app.apig_state.expanded_routes.contains("/api"));
18071
18072 app.handle_action(Action::ExpandRow);
18074 assert!(app.apig_state.expanded_routes.contains("/api"));
18075 assert_eq!(app.apig_state.routes.selected, 0);
18076
18077 app.handle_action(Action::ExpandRow);
18079 assert_eq!(app.apig_state.routes.selected, 1);
18080 }
18081
18082 #[test]
18083 fn test_apig_filter_only_when_focused() {
18084 use crate::apig::api::RestApi;
18085 use crate::ui::apig::ApiDetailTab;
18086
18087 let mut app = test_app();
18088 app.current_service = Service::ApiGatewayApis;
18089 app.apig_state.current_api = Some(RestApi {
18090 id: "test".to_string(),
18091 name: "Test API".to_string(),
18092 description: "Test".to_string(),
18093 created_date: "2024-01-01".to_string(),
18094 api_key_source: "HEADER".to_string(),
18095 endpoint_configuration: "REGIONAL".to_string(),
18096 protocol_type: "HTTP".to_string(),
18097 disable_execute_api_endpoint: false,
18098 status: "AVAILABLE".to_string(),
18099 });
18100 app.apig_state.detail_tab = ApiDetailTab::Routes;
18101 app.mode = Mode::FilterInput;
18102
18103 app.apig_state.input_focus = InputFocus::Pagination;
18105 app.handle_action(Action::FilterInput('x'));
18106 assert_eq!(app.apig_state.route_filter, "");
18107
18108 app.apig_state.input_focus = InputFocus::Filter;
18110 app.handle_action(Action::FilterInput('x'));
18111 assert_eq!(app.apig_state.route_filter, "x");
18112 }
18113
18114 #[test]
18115 fn test_apig_routes_and_resources_use_same_render_function() {
18116 let source = include_str!("ui/apig.rs");
18118 let render_calls: Vec<_> = source
18119 .match_indices("crate::ui::table::render_tree_table")
18120 .collect();
18121
18122 assert_eq!(
18124 render_calls.len(),
18125 2,
18126 "Both routes and resources must use render_tree_table"
18127 );
18128 }
18129
18130 #[test]
18131 fn test_s3_uses_same_render_function() {
18132 let source = include_str!("ui/s3.rs");
18134 let render_calls: Vec<_> = source
18135 .match_indices("crate::ui::table::render_tree_table")
18136 .collect();
18137
18138 assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
18140 }
18141
18142 #[test]
18143 fn test_s3_page_down_skips_expanded_children() {
18144 use crate::keymap::Action;
18149
18150 let mut app = test_app();
18151 app.current_service = Service::S3Buckets;
18152 app.service_selected = true;
18153 app.mode = Mode::Normal;
18154 app.s3_state.bucket_visible_rows = std::cell::Cell::new(30);
18155
18156 app.s3_state.buckets.items = (0..3)
18157 .map(|i| S3Bucket {
18158 name: format!("bucket{}", i),
18159 region: "us-east-1".to_string(),
18160 creation_date: String::new(),
18161 })
18162 .collect();
18163
18164 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
18166 app.s3_state.bucket_preview.insert(
18167 "bucket0".to_string(),
18168 (0..20)
18169 .map(|i| crate::app::S3Object {
18170 key: format!("key{}", i),
18171 size: 0,
18172 last_modified: String::new(),
18173 storage_class: String::new(),
18174 is_prefix: false,
18175 })
18176 .collect(),
18177 );
18178
18179 app.s3_state.selected_row = 0;
18180
18181 app.handle_action(Action::PageDown);
18183 let row = app.s3_state.selected_row;
18185 assert_eq!(
18186 row, 22,
18187 "PageDown must land on bucket2 (visual row 22), got {}",
18188 row
18189 );
18190
18191 app.handle_action(Action::PageUp);
18193 assert_eq!(
18194 app.s3_state.selected_row, 0,
18195 "PageUp must return to bucket0"
18196 );
18197 }
18198
18199 #[test]
18200 fn test_s3_page_down_with_many_buckets_skips_expanded() {
18201 use crate::keymap::Action;
18204
18205 let mut app = test_app();
18206 app.current_service = Service::S3Buckets;
18207 app.service_selected = true;
18208 app.mode = Mode::Normal;
18209 app.s3_state.bucket_visible_rows = std::cell::Cell::new(30);
18210
18211 app.s3_state.buckets.items = (0..25)
18212 .map(|i| S3Bucket {
18213 name: format!("bucket{}", i),
18214 region: "us-east-1".to_string(),
18215 creation_date: String::new(),
18216 })
18217 .collect();
18218
18219 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
18221 app.s3_state.bucket_preview.insert(
18222 "bucket0".to_string(),
18223 (0..100)
18224 .map(|i| crate::app::S3Object {
18225 key: format!("key{}", i),
18226 size: 0,
18227 last_modified: String::new(),
18228 storage_class: String::new(),
18229 is_prefix: false,
18230 })
18231 .collect(),
18232 );
18233
18234 app.s3_state.selected_row = 0;
18235
18236 app.handle_action(Action::PageDown);
18238 let row = app.s3_state.selected_row;
18239 assert_eq!(
18241 row, 110,
18242 "PageDown must land on bucket10 (row 110), got {}",
18243 row
18244 );
18245 }
18246
18247 #[test]
18248 fn test_search_icon_has_proper_border_spacing() {
18249 use crate::ui::SEARCH_ICON;
18252
18253 assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
18254 assert!(
18255 SEARCH_ICON.ends_with("─"),
18256 "Should end with dash for proper border spacing"
18257 );
18258 assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
18259 }
18260
18261 #[test]
18262 fn test_apig_expand_with_filter() {
18263 use crate::apig::api::RestApi;
18264 use crate::apig::route::Route;
18265 use crate::ui::apig::ApiDetailTab;
18266 use std::collections::HashMap;
18267
18268 let mut app = test_app();
18269 app.current_service = Service::ApiGatewayApis;
18270 app.apig_state.current_api = Some(RestApi {
18271 id: "test123".to_string(),
18272 name: "Test API".to_string(),
18273 description: "Test".to_string(),
18274 created_date: "2024-01-01".to_string(),
18275 api_key_source: "HEADER".to_string(),
18276 endpoint_configuration: "REGIONAL".to_string(),
18277 protocol_type: "HTTP".to_string(),
18278 disable_execute_api_endpoint: false,
18279 status: "AVAILABLE".to_string(),
18280 });
18281 app.apig_state.detail_tab = ApiDetailTab::Routes;
18282
18283 app.apig_state.routes.items = vec![Route {
18285 route_id: "0".to_string(),
18286 route_key: "/api".to_string(),
18287 target: "".to_string(),
18288 authorization_type: "NONE".to_string(),
18289 api_key_required: false,
18290 display_name: "/api".to_string(),
18291 arn: String::new(),
18292 }];
18293
18294 let mut children = HashMap::new();
18295 children.insert(
18296 "/api".to_string(),
18297 vec![Route {
18298 route_id: "1".to_string(),
18299 route_key: "GET".to_string(),
18300 target: "integration1".to_string(),
18301 authorization_type: "NONE".to_string(),
18302 api_key_required: false,
18303 display_name: "GET".to_string(),
18304 arn: String::new(),
18305 }],
18306 );
18307 app.apig_state.route_children = children;
18308
18309 app.apig_state.route_filter = "GET".to_string();
18311
18312 assert_eq!(app.apig_state.routes.selected, 0);
18315 assert!(!app.apig_state.expanded_routes.contains("/api"));
18316
18317 app.handle_action(Action::ExpandRow);
18318
18319 assert!(app.apig_state.expanded_routes.contains("/api"));
18321 }
18322
18323 #[test]
18324 fn test_apig_console_url_routes() {
18325 use crate::apig::api::RestApi;
18326 use crate::apig::route::Route;
18327 use crate::ui::apig::ApiDetailTab;
18328
18329 let mut app = test_app();
18330 app.current_service = Service::ApiGatewayApis;
18331 app.config.region = "us-east-1".to_string();
18332
18333 let url = app.get_console_url();
18335 assert!(url.contains("apigateway/main/apis"));
18336 assert!(url.contains("region=us-east-1"));
18337
18338 app.apig_state.current_api = Some(RestApi {
18340 id: "2todvod3n0".to_string(),
18341 name: "Test API".to_string(),
18342 description: "Test".to_string(),
18343 created_date: "2024-01-01".to_string(),
18344 api_key_source: "HEADER".to_string(),
18345 endpoint_configuration: "REGIONAL".to_string(),
18346 protocol_type: "HTTP".to_string(),
18347 disable_execute_api_endpoint: false,
18348 status: "AVAILABLE".to_string(),
18349 });
18350 app.apig_state.detail_tab = ApiDetailTab::Routes;
18351 app.apig_state.routes.items = vec![Route {
18352 route_id: "eizmisr".to_string(),
18353 route_key: "GET /test".to_string(),
18354 target: "integration1".to_string(),
18355 authorization_type: "NONE".to_string(),
18356 api_key_required: false,
18357 display_name: "GET /test".to_string(),
18358 arn: String::new(),
18359 }];
18360 app.apig_state.routes.selected = 0;
18361
18362 let url = app.get_console_url();
18363 assert!(url.contains("apigateway/main/develop/routes"));
18364 assert!(url.contains("api=2todvod3n0"));
18365 assert!(url.contains("routes=eizmisr"));
18366 assert!(url.contains("region=us-east-1"));
18367 }
18368
18369 #[test]
18370 fn test_apig_console_url_resources() {
18371 use crate::apig::api::RestApi;
18372 use crate::apig::resource::Resource;
18373 use crate::ui::apig::ApiDetailTab;
18374
18375 let mut app = test_app();
18376 app.current_service = Service::ApiGatewayApis;
18377 app.config.region = "us-east-1".to_string();
18378
18379 app.apig_state.current_api = Some(RestApi {
18381 id: "2j9j50ze47".to_string(),
18382 name: "Test API".to_string(),
18383 description: "Test".to_string(),
18384 created_date: "2024-01-01".to_string(),
18385 api_key_source: "HEADER".to_string(),
18386 endpoint_configuration: "REGIONAL".to_string(),
18387 protocol_type: "REST".to_string(),
18388 disable_execute_api_endpoint: false,
18389 status: "AVAILABLE".to_string(),
18390 });
18391 app.apig_state.detail_tab = ApiDetailTab::Routes;
18392 app.apig_state.resources.items = vec![Resource {
18393 id: "abc123".to_string(),
18394 path: "/test".to_string(),
18395 parent_id: None,
18396 methods: vec![],
18397 display_name: "/test".to_string(),
18398 arn: String::new(),
18399 }];
18400 app.apig_state.resources.selected = 0;
18401
18402 let url = app.get_console_url();
18403 assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
18404 assert!(url.contains("api=2j9j50ze47"));
18405 assert!(url.contains("#abc123"));
18406 assert!(url.contains("region=us-east-1"));
18407 }
18408
18409 #[test]
18410 fn test_apig_console_url_routes_parent_vs_leaf() {
18411 use crate::apig::api::RestApi;
18412 use crate::apig::route::Route;
18413 use crate::ui::apig::ApiDetailTab;
18414 use std::collections::HashMap;
18415
18416 let mut app = test_app();
18417 app.current_service = Service::ApiGatewayApis;
18418 app.config.region = "us-east-1".to_string();
18419
18420 app.apig_state.current_api = Some(RestApi {
18421 id: "2todvod3n0".to_string(),
18422 name: "Test API".to_string(),
18423 description: "Test".to_string(),
18424 created_date: "2024-01-01".to_string(),
18425 api_key_source: "HEADER".to_string(),
18426 endpoint_configuration: "REGIONAL".to_string(),
18427 protocol_type: "HTTP".to_string(),
18428 disable_execute_api_endpoint: false,
18429 status: "AVAILABLE".to_string(),
18430 });
18431 app.apig_state.detail_tab = ApiDetailTab::Routes;
18432
18433 app.apig_state.routes.items = vec![Route {
18435 route_id: "parent".to_string(),
18436 route_key: "/v1/get/jobs".to_string(),
18437 target: "".to_string(), authorization_type: "NONE".to_string(),
18439 api_key_required: false,
18440 display_name: "/v1/get/jobs".to_string(),
18441 arn: String::new(),
18442 }];
18443
18444 let mut children = HashMap::new();
18446 children.insert(
18447 "/v1/get/jobs".to_string(),
18448 vec![Route {
18449 route_id: "1iz9vtl".to_string(),
18450 route_key: "GET".to_string(),
18451 target: "integration1".to_string(), authorization_type: "NONE".to_string(),
18453 api_key_required: false,
18454 display_name: "GET".to_string(),
18455 arn: String::new(),
18456 }],
18457 );
18458 app.apig_state.route_children = children;
18459
18460 app.apig_state.routes.selected = 0;
18462 let url = app.get_console_url();
18463 assert!(url.contains("apigateway/main/develop/routes"));
18464 assert!(url.contains("api=2todvod3n0"));
18465 assert!(
18466 !url.contains("routes="),
18467 "Parent node should not include routes parameter"
18468 );
18469
18470 app.apig_state
18472 .expanded_routes
18473 .insert("/v1/get/jobs".to_string());
18474 app.apig_state.routes.selected = 1;
18475 let url = app.get_console_url();
18476 assert!(
18477 url.contains("routes=1iz9vtl"),
18478 "Leaf node should include routes parameter: {}",
18479 url
18480 );
18481 }
18482
18483 #[test]
18484 fn test_apig_preferences_context() {
18485 use crate::apig::api::RestApi;
18486 use crate::ui::apig::ApiDetailTab;
18487
18488 let mut app = test_app();
18489 app.current_service = Service::ApiGatewayApis;
18490
18491 assert!(
18493 app.apig_state.current_api.is_none(),
18494 "Should be in list view"
18495 );
18496
18497 app.apig_state.current_api = Some(RestApi {
18499 id: "test123".to_string(),
18500 name: "Test API".to_string(),
18501 description: "Test".to_string(),
18502 created_date: "2024-01-01".to_string(),
18503 api_key_source: "HEADER".to_string(),
18504 endpoint_configuration: "REGIONAL".to_string(),
18505 protocol_type: "HTTP".to_string(),
18506 disable_execute_api_endpoint: false,
18507 status: "AVAILABLE".to_string(),
18508 });
18509 app.apig_state.detail_tab = ApiDetailTab::Routes;
18510
18511 assert!(
18512 app.apig_state.current_api.is_some(),
18513 "Should be in detail view"
18514 );
18515 }
18517
18518 #[test]
18519 fn test_apig_route_columns() {
18520 use crate::apig::route::Column as RouteColumn;
18521
18522 let cols = RouteColumn::all();
18524 assert_eq!(cols.len(), 5);
18525
18526 assert_eq!(RouteColumn::RouteKey.id(), "route_key");
18528 assert_eq!(RouteColumn::RouteId.id(), "route_id");
18529 assert_eq!(RouteColumn::Arn.id(), "arn");
18530 assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
18531 assert_eq!(RouteColumn::Target.id(), "target");
18532
18533 assert_eq!(
18535 RouteColumn::from_id("route_key"),
18536 Some(RouteColumn::RouteKey)
18537 );
18538 assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
18539 assert_eq!(RouteColumn::from_id("invalid"), None);
18540 }
18541
18542 #[test]
18543 fn test_apig_yank_copies_route_arn() {
18544 use crate::apig::api::RestApi;
18545 use crate::apig::route::Route;
18546 use crate::ui::apig::ApiDetailTab;
18547
18548 let mut app = test_app();
18549 app.current_service = Service::ApiGatewayApis;
18550 app.apig_state.current_api = Some(RestApi {
18551 id: "test123".to_string(),
18552 name: "Test API".to_string(),
18553 description: "Test".to_string(),
18554 created_date: "2024-01-01".to_string(),
18555 api_key_source: "HEADER".to_string(),
18556 endpoint_configuration: "REGIONAL".to_string(),
18557 protocol_type: "HTTP".to_string(),
18558 disable_execute_api_endpoint: false,
18559 status: "AVAILABLE".to_string(),
18560 });
18561 app.apig_state.detail_tab = ApiDetailTab::Routes;
18562
18563 app.apig_state.routes.items = vec![
18565 Route {
18566 route_id: "route1".to_string(),
18567 route_key: "GET /users".to_string(),
18568 target: "integrations/abc".to_string(),
18569 authorization_type: "NONE".to_string(),
18570 api_key_required: false,
18571 display_name: "GET /users".to_string(),
18572 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
18573 },
18574 Route {
18575 route_id: "route2".to_string(),
18576 route_key: "POST /users".to_string(),
18577 target: "integrations/def".to_string(),
18578 authorization_type: "AWS_IAM".to_string(),
18579 api_key_required: true,
18580 display_name: "POST /users".to_string(),
18581 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
18582 },
18583 ];
18584
18585 app.apig_state.routes.selected = 0;
18587
18588 assert_eq!(
18590 app.apig_state.routes.items[0].arn,
18591 "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
18592 );
18593 }
18594
18595 #[test]
18596 fn test_apig_yank_ignores_empty_arn() {
18597 use crate::apig::api::RestApi;
18598 use crate::apig::route::Route;
18599 use crate::ui::apig::ApiDetailTab;
18600
18601 let mut app = test_app();
18602 app.current_service = Service::ApiGatewayApis;
18603 app.apig_state.current_api = Some(RestApi {
18604 id: "test123".to_string(),
18605 name: "Test API".to_string(),
18606 description: "Test".to_string(),
18607 created_date: "2024-01-01".to_string(),
18608 api_key_source: "HEADER".to_string(),
18609 endpoint_configuration: "REGIONAL".to_string(),
18610 protocol_type: "HTTP".to_string(),
18611 disable_execute_api_endpoint: false,
18612 status: "AVAILABLE".to_string(),
18613 });
18614 app.apig_state.detail_tab = ApiDetailTab::Routes;
18615
18616 app.apig_state.routes.items = vec![Route {
18618 route_id: String::new(),
18619 route_key: "/users".to_string(),
18620 target: String::new(), authorization_type: String::new(),
18622 api_key_required: false,
18623 display_name: "/users".to_string(),
18624 arn: String::new(), }];
18626
18627 app.apig_state.routes.selected = 0;
18628
18629 assert!(
18631 app.apig_state.routes.items[0].arn.is_empty(),
18632 "Virtual parent should have empty ARN"
18633 );
18634 }
18635
18636 #[test]
18637 fn test_apig_route_column_toggle() {
18638 use crate::apig::api::RestApi;
18639 use crate::apig::route::Column as RouteColumn;
18640 use crate::keymap::Action;
18641 use crate::ui::apig::ApiDetailTab;
18642
18643 let mut app = test_app();
18644 app.current_service = Service::ApiGatewayApis;
18645 app.mode = Mode::ColumnSelector;
18646 app.apig_state.current_api = Some(RestApi {
18647 id: "test123".to_string(),
18648 name: "Test API".to_string(),
18649 description: "Test".to_string(),
18650 created_date: "2024-01-01".to_string(),
18651 api_key_source: "HEADER".to_string(),
18652 endpoint_configuration: "REGIONAL".to_string(),
18653 protocol_type: "HTTP".to_string(),
18654 disable_execute_api_endpoint: false,
18655 status: "AVAILABLE".to_string(),
18656 });
18657 app.apig_state.detail_tab = ApiDetailTab::Routes;
18658
18659 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18661
18662 app.column_selector_index = 1;
18664 app.handle_action(Action::ToggleColumn);
18665 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18666 assert!(app
18667 .apig_route_visible_column_ids
18668 .contains(&RouteColumn::RouteKey.id()));
18669
18670 app.column_selector_index = 3;
18672 app.handle_action(Action::ToggleColumn);
18673
18674 assert_eq!(app.apig_route_visible_column_ids.len(), 4);
18676 assert!(!app
18677 .apig_route_visible_column_ids
18678 .contains(&RouteColumn::Arn.id()));
18679
18680 app.handle_action(Action::ToggleColumn);
18682 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18683 assert!(app
18684 .apig_route_visible_column_ids
18685 .contains(&RouteColumn::Arn.id()));
18686 }
18687
18688 #[test]
18689 fn test_apig_resource_column_toggle() {
18690 use crate::apig::api::RestApi;
18691 use crate::apig::resource::Column as ResourceColumn;
18692 use crate::keymap::Action;
18693 use crate::ui::apig::ApiDetailTab;
18694
18695 let mut app = test_app();
18696 app.current_service = Service::ApiGatewayApis;
18697 app.mode = Mode::ColumnSelector;
18698 app.apig_state.current_api = Some(RestApi {
18699 id: "test123".to_string(),
18700 name: "Test API".to_string(),
18701 description: "Test".to_string(),
18702 created_date: "2024-01-01".to_string(),
18703 api_key_source: "HEADER".to_string(),
18704 endpoint_configuration: "REGIONAL".to_string(),
18705 protocol_type: "REST".to_string(), disable_execute_api_endpoint: false,
18707 status: "AVAILABLE".to_string(),
18708 });
18709 app.apig_state.detail_tab = ApiDetailTab::Routes; assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18713
18714 app.column_selector_index = 1;
18716 app.handle_action(Action::ToggleColumn);
18717 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18718 assert!(app
18719 .apig_resource_visible_column_ids
18720 .contains(&ResourceColumn::Path.id()));
18721
18722 app.column_selector_index = 3;
18724 app.handle_action(Action::ToggleColumn);
18725
18726 assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
18728 assert!(!app
18729 .apig_resource_visible_column_ids
18730 .contains(&ResourceColumn::Arn.id()));
18731
18732 app.handle_action(Action::ToggleColumn);
18734 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18735 assert!(app
18736 .apig_resource_visible_column_ids
18737 .contains(&ResourceColumn::Arn.id()));
18738 }
18739
18740 #[test]
18741 fn test_cloudtrail_filter_input() {
18742 let mut app = App::new_without_client("default".to_string(), None);
18743 app.service_selected = true;
18744 app.current_service = Service::CloudTrailEvents;
18745
18746 app.handle_action(Action::StartFilter);
18747 assert_eq!(app.mode, Mode::FilterInput);
18748
18749 app.cloudtrail_state.table.filter = "test".to_string();
18750
18751 assert_eq!(app.cloudtrail_state.table.filter, "test");
18752 }
18753
18754 #[test]
18755 fn test_cloudtrail_row_expansion() {
18756 let mut app = App::new_without_client("default".to_string(), None);
18757 app.service_selected = true;
18758 app.current_service = Service::CloudTrailEvents;
18759 app.cloudtrail_state.table.items = vec![CloudTrailEvent {
18760 event_name: "Event1".to_string(),
18761 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18762 username: "user1".to_string(),
18763 event_source: "s3.amazonaws.com".to_string(),
18764 resource_type: "Bucket".to_string(),
18765 resource_name: "my-bucket".to_string(),
18766 read_only: "false".to_string(),
18767 aws_region: "us-east-1".to_string(),
18768 event_id: "abc123".to_string(),
18769 access_key_id: "AKIA...".to_string(),
18770 source_ip_address: "1.2.3.4".to_string(),
18771 error_code: "".to_string(),
18772 request_id: "req-123".to_string(),
18773 event_type: "AwsApiCall".to_string(),
18774 cloud_trail_event_json: "{}".to_string(),
18775 }];
18776
18777 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18778
18779 app.expand_row();
18780 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
18781
18782 app.collapse_row();
18783 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18784 }
18785
18786 #[test]
18787 fn test_cloudtrail_service_initialization() {
18788 let app = App::new_without_client("default".to_string(), None);
18789 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
18790 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
18791 }
18792
18793 #[test]
18794 fn test_cloudtrail_service_name() {
18795 assert_eq!(
18796 Service::CloudTrailEvents.name(),
18797 "CloudTrail › Event History"
18798 );
18799 }
18800
18801 #[test]
18802 fn test_cloudtrail_in_service_picker() {
18803 let app = App::new_without_client("default".to_string(), None);
18804 assert!(app
18805 .service_picker
18806 .services
18807 .contains(&"CloudTrail › Event History"));
18808 }
18809
18810 #[test]
18811 fn test_cloudtrail_service_selection() {
18812 let mut app = App::new_without_client("default".to_string(), None);
18813 app.current_service = Service::CloudTrailEvents;
18814 app.service_selected = true;
18815
18816 assert_eq!(app.current_service, Service::CloudTrailEvents);
18817 assert!(app.service_selected);
18818 }
18819
18820 #[test]
18821 fn test_cloudtrail_filter_resets_selection() {
18822 let mut app = App::new_without_client("default".to_string(), None);
18823 app.service_selected = true;
18824 app.current_service = Service::CloudTrailEvents;
18825 app.cloudtrail_state.table.selected = 5;
18826 app.cloudtrail_state.table.expanded_item = Some(3);
18827
18828 app.handle_action(Action::StartFilter);
18829 app.apply_filter_operation(|_| {});
18830
18831 assert_eq!(app.cloudtrail_state.table.selected, 0);
18832 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18833 }
18834
18835 #[test]
18836 fn test_cloudtrail_column_toggle() {
18837 let mut app = App::new_without_client("default".to_string(), None);
18838 app.service_selected = true;
18839 app.current_service = Service::CloudTrailEvents;
18840 app.mode = Mode::ColumnSelector;
18841
18842 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
18844
18845 app.column_selector_index = 7;
18847 app.handle_action(Action::ToggleColumn);
18848
18849 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
18851 }
18852
18853 #[test]
18854 fn test_cloudtrail_tab_cycles_filter_focus() {
18855 let mut app = App::new_without_client("default".to_string(), None);
18856 app.service_selected = true;
18857 app.current_service = Service::CloudTrailEvents;
18858 app.mode = Mode::FilterInput;
18859 app.cloudtrail_state.input_focus = InputFocus::Filter;
18860
18861 app.handle_action(Action::NextFilterFocus);
18863 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
18864
18865 app.handle_action(Action::NextFilterFocus);
18867 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
18868 }
18869
18870 #[test]
18871 fn test_cloudtrail_shift_tab_cycles_filter_focus() {
18872 let mut app = App::new_without_client("default".to_string(), None);
18873 app.service_selected = true;
18874 app.current_service = Service::CloudTrailEvents;
18875 app.mode = Mode::FilterInput;
18876 app.cloudtrail_state.input_focus = InputFocus::Filter;
18877
18878 app.handle_action(Action::PrevFilterFocus);
18880 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
18881
18882 app.handle_action(Action::PrevFilterFocus);
18884 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
18885 }
18886
18887 #[test]
18888 fn test_cloudtrail_detail_view_tab_cycles_focus() {
18889 let mut app = App::new_without_client("default".to_string(), None);
18890 app.service_selected = true;
18891 app.current_service = Service::CloudTrailEvents;
18892 app.mode = Mode::Normal;
18893 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18894 event_name: "PutObject".to_string(),
18895 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18896 username: "user".to_string(),
18897 event_source: "s3.amazonaws.com".to_string(),
18898 resource_type: "AWS::S3::Bucket".to_string(),
18899 resource_name: "my-bucket".to_string(),
18900 read_only: "false".to_string(),
18901 aws_region: "us-east-1".to_string(),
18902 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18903 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18904 source_ip_address: "192.0.2.1".to_string(),
18905 error_code: "".to_string(),
18906 request_id: "req-123".to_string(),
18907 event_type: "AwsApiCall".to_string(),
18908 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18909 });
18910
18911 assert_eq!(
18913 app.cloudtrail_state.detail_focus,
18914 CloudTrailDetailFocus::Resources
18915 );
18916
18917 app.handle_action(Action::NextDetailTab);
18919 assert_eq!(
18920 app.cloudtrail_state.detail_focus,
18921 CloudTrailDetailFocus::EventRecord
18922 );
18923
18924 app.handle_action(Action::NextDetailTab);
18926 assert_eq!(
18927 app.cloudtrail_state.detail_focus,
18928 CloudTrailDetailFocus::Resources
18929 );
18930 }
18931
18932 #[test]
18933 fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
18934 let mut app = App::new_without_client("default".to_string(), None);
18935 app.service_selected = true;
18936 app.current_service = Service::CloudTrailEvents;
18937 app.mode = Mode::Normal;
18938 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18939 event_name: "PutObject".to_string(),
18940 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18941 username: "user".to_string(),
18942 event_source: "s3.amazonaws.com".to_string(),
18943 resource_type: "AWS::S3::Bucket".to_string(),
18944 resource_name: "my-bucket".to_string(),
18945 read_only: "false".to_string(),
18946 aws_region: "us-east-1".to_string(),
18947 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18948 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18949 source_ip_address: "192.0.2.1".to_string(),
18950 error_code: "".to_string(),
18951 request_id: "req-123".to_string(),
18952 event_type: "AwsApiCall".to_string(),
18953 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18954 });
18955
18956 assert_eq!(
18958 app.cloudtrail_state.detail_focus,
18959 CloudTrailDetailFocus::Resources
18960 );
18961
18962 app.handle_action(Action::PrevDetailTab);
18964 assert_eq!(
18965 app.cloudtrail_state.detail_focus,
18966 CloudTrailDetailFocus::EventRecord
18967 );
18968
18969 app.handle_action(Action::PrevDetailTab);
18971 assert_eq!(
18972 app.cloudtrail_state.detail_focus,
18973 CloudTrailDetailFocus::Resources
18974 );
18975 }
18976
18977 #[test]
18978 fn test_cloudtrail_json_scroll_with_arrow_keys() {
18979 let mut app = App::new_without_client("default".to_string(), None);
18980 app.service_selected = true;
18981 app.current_service = Service::CloudTrailEvents;
18982 app.mode = Mode::Normal;
18983 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18984 event_name: "PutObject".to_string(),
18985 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18986 username: "user".to_string(),
18987 event_source: "s3.amazonaws.com".to_string(),
18988 resource_type: "AWS::S3::Bucket".to_string(),
18989 resource_name: "my-bucket".to_string(),
18990 read_only: "false".to_string(),
18991 aws_region: "us-east-1".to_string(),
18992 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18993 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18994 source_ip_address: "192.0.2.1".to_string(),
18995 error_code: "".to_string(),
18996 request_id: "req-123".to_string(),
18997 event_type: "AwsApiCall".to_string(),
18998 cloud_trail_event_json: (0..50)
18999 .map(|i| format!("line {}", i))
19000 .collect::<Vec<_>>()
19001 .join("\n"),
19002 });
19003 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
19004 app.cloudtrail_state.event_json_scroll = 0;
19005
19006 app.handle_action(Action::NextItem);
19008 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
19009
19010 app.handle_action(Action::NextItem);
19011 assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
19012
19013 app.handle_action(Action::PrevItem);
19015 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
19016
19017 app.handle_action(Action::PrevItem);
19018 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
19019
19020 app.handle_action(Action::PrevItem);
19022 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
19023 }
19024
19025 #[test]
19026 fn test_cloudtrail_tab_works_with_no_resources() {
19027 let mut app = App::new_without_client("default".to_string(), None);
19028 app.service_selected = true;
19029 app.current_service = Service::CloudTrailEvents;
19030 app.mode = Mode::Normal;
19031 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19032 event_name: "PutObject".to_string(),
19033 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19034 username: "user".to_string(),
19035 event_source: "s3.amazonaws.com".to_string(),
19036 resource_type: "".to_string(), resource_name: "".to_string(), read_only: "false".to_string(),
19039 aws_region: "us-east-1".to_string(),
19040 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19041 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19042 source_ip_address: "192.0.2.1".to_string(),
19043 error_code: "".to_string(),
19044 request_id: "req-123".to_string(),
19045 event_type: "AwsApiCall".to_string(),
19046 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19047 });
19048 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
19049
19050 app.handle_action(Action::NextDetailTab);
19052 assert_eq!(
19053 app.cloudtrail_state.detail_focus,
19054 CloudTrailDetailFocus::Resources
19055 );
19056
19057 app.handle_action(Action::NextDetailTab);
19059 assert_eq!(
19060 app.cloudtrail_state.detail_focus,
19061 CloudTrailDetailFocus::EventRecord
19062 );
19063 }
19064
19065 #[test]
19066 fn test_cloudtrail_resources_expand_collapse() {
19067 let mut app = App::new_without_client("default".to_string(), None);
19068 app.service_selected = true;
19069 app.current_service = Service::CloudTrailEvents;
19070 app.mode = Mode::Normal;
19071 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19072 event_name: "PutObject".to_string(),
19073 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19074 username: "user".to_string(),
19075 event_source: "s3.amazonaws.com".to_string(),
19076 resource_type: "AWS::S3::Bucket".to_string(),
19077 resource_name: "my-bucket".to_string(),
19078 read_only: "false".to_string(),
19079 aws_region: "us-east-1".to_string(),
19080 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19081 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19082 source_ip_address: "192.0.2.1".to_string(),
19083 error_code: "".to_string(),
19084 request_id: "req-123".to_string(),
19085 event_type: "AwsApiCall".to_string(),
19086 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19087 });
19088 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19089 app.cloudtrail_state.resources_expanded_index = None;
19090
19091 app.handle_action(Action::ExpandRow);
19093 assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
19094
19095 app.handle_action(Action::CollapseRow);
19097 assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
19098 }
19099
19100 #[test]
19101 fn test_cloudtrail_event_json_no_column_selector() {
19102 let mut app = App::new_without_client("default".to_string(), None);
19103 app.service_selected = true;
19104 app.current_service = Service::CloudTrailEvents;
19105 app.mode = Mode::Normal;
19106 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19107 event_name: "PutObject".to_string(),
19108 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19109 username: "user".to_string(),
19110 event_source: "s3.amazonaws.com".to_string(),
19111 resource_type: "AWS::S3::Bucket".to_string(),
19112 resource_name: "my-bucket".to_string(),
19113 read_only: "false".to_string(),
19114 aws_region: "us-east-1".to_string(),
19115 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19116 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19117 source_ip_address: "192.0.2.1".to_string(),
19118 error_code: "".to_string(),
19119 request_id: "req-123".to_string(),
19120 event_type: "AwsApiCall".to_string(),
19121 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19122 });
19123 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
19124
19125 app.handle_action(Action::OpenColumnSelector);
19127 assert_eq!(app.mode, Mode::Normal);
19128
19129 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19131 app.handle_action(Action::OpenColumnSelector);
19132 assert_eq!(app.mode, Mode::ColumnSelector);
19133 }
19134
19135 #[test]
19136 fn test_cloudtrail_resources_preferences_show_only_3_columns() {
19137 let mut app = App::new_without_client("default".to_string(), None);
19138 app.service_selected = true;
19139 app.current_service = Service::CloudTrailEvents;
19140 app.mode = Mode::ColumnSelector;
19141 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19142 event_name: "PutObject".to_string(),
19143 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19144 username: "user".to_string(),
19145 event_source: "s3.amazonaws.com".to_string(),
19146 resource_type: "AWS::S3::Bucket".to_string(),
19147 resource_name: "my-bucket".to_string(),
19148 read_only: "false".to_string(),
19149 aws_region: "us-east-1".to_string(),
19150 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19151 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19152 source_ip_address: "192.0.2.1".to_string(),
19153 error_code: "".to_string(),
19154 request_id: "req-123".to_string(),
19155 event_type: "AwsApiCall".to_string(),
19156 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19157 });
19158 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19159
19160 assert_eq!(app.get_column_count(), 3);
19162 assert_eq!(app.get_column_selector_max(), 3);
19163
19164 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
19166
19167 app.column_selector_index = 1;
19169 app.handle_action(Action::ToggleColumn);
19170 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
19171
19172 app.column_selector_index = 2;
19174 app.handle_action(Action::ToggleColumn);
19175 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
19176
19177 app.column_selector_index = 3;
19179 app.handle_action(Action::ToggleColumn);
19180 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
19181
19182 app.column_selector_index = 1;
19184 app.handle_action(Action::ToggleColumn);
19185 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
19186 }
19187
19188 #[test]
19189 fn test_cloudtrail_resources_preferences_tab_cycles() {
19190 let mut app = App::new_without_client("default".to_string(), None);
19191 app.service_selected = true;
19192 app.current_service = Service::CloudTrailEvents;
19193 app.mode = Mode::ColumnSelector;
19194 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19195 event_name: "PutObject".to_string(),
19196 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19197 username: "user".to_string(),
19198 event_source: "s3.amazonaws.com".to_string(),
19199 resource_type: "AWS::S3::Bucket".to_string(),
19200 resource_name: "my-bucket".to_string(),
19201 read_only: "false".to_string(),
19202 aws_region: "us-east-1".to_string(),
19203 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19204 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19205 source_ip_address: "192.0.2.1".to_string(),
19206 error_code: "".to_string(),
19207 request_id: "req-123".to_string(),
19208 event_type: "AwsApiCall".to_string(),
19209 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19210 });
19211 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19212 app.column_selector_index = 1;
19213
19214 app.handle_action(Action::NextPreferences);
19216 assert_eq!(app.column_selector_index, 0);
19217
19218 app.column_selector_index = 1;
19220 app.handle_action(Action::PrevPreferences);
19221 assert_eq!(app.column_selector_index, 0);
19222 }
19223
19224 #[test]
19225 fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
19226 let visible_cols_3 = 3;
19234 let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
19235 assert_eq!(height_3, 7);
19236
19237 let visible_cols_2 = 2;
19239 let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
19240 assert_eq!(height_2, 6);
19241
19242 let visible_cols_1 = 1;
19244 let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
19245 assert_eq!(height_1, 5);
19246 }
19247
19248 #[test]
19249 fn test_cloudtrail_default_focus_is_resources() {
19250 let app = App::new_without_client("default".to_string(), None);
19251 assert_eq!(
19252 app.cloudtrail_state.detail_focus,
19253 CloudTrailDetailFocus::Resources
19254 );
19255 }
19256
19257 fn setup_cloudtrail_pagination_test() -> App {
19258 let mut app = App::new_without_client("default".to_string(), None);
19259 app.service_selected = true;
19260 app.current_service = Service::CloudTrailEvents;
19261 app.mode = Mode::FilterInput;
19262 app.cloudtrail_state.input_focus = InputFocus::Pagination;
19263 app.cloudtrail_state.table.page_size = PageSize::Ten;
19264 app.cloudtrail_state.table.items = (0..25)
19265 .map(|i| CloudTrailEvent {
19266 event_name: format!("Event{}", i),
19267 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19268 username: "user".to_string(),
19269 event_source: "s3.amazonaws.com".to_string(),
19270 resource_type: "Bucket".to_string(),
19271 resource_name: "bucket".to_string(),
19272 read_only: "false".to_string(),
19273 aws_region: "us-east-1".to_string(),
19274 event_id: "id".to_string(),
19275 access_key_id: "key".to_string(),
19276 source_ip_address: "1.2.3.4".to_string(),
19277 error_code: "".to_string(),
19278 request_id: "req".to_string(),
19279 event_type: "AwsApiCall".to_string(),
19280 cloud_trail_event_json: "{}".to_string(),
19281 })
19282 .collect();
19283 app
19284 }
19285
19286 #[test]
19287 fn test_cloudtrail_pagination_navigation() {
19288 let mut app = setup_cloudtrail_pagination_test();
19290 assert_eq!(app.cloudtrail_state.table.selected, 0);
19291 app.handle_action(Action::NextItem);
19292 assert_eq!(app.cloudtrail_state.table.selected, 10);
19293 app.handle_action(Action::NextItem);
19294 assert_eq!(app.cloudtrail_state.table.selected, 20);
19295 app.handle_action(Action::NextItem);
19296 assert_eq!(app.cloudtrail_state.table.selected, 20);
19297
19298 app.handle_action(Action::PrevItem);
19300 assert_eq!(app.cloudtrail_state.table.selected, 10);
19301 app.handle_action(Action::PrevItem);
19302 assert_eq!(app.cloudtrail_state.table.selected, 0);
19303 app.handle_action(Action::PrevItem);
19304 assert_eq!(app.cloudtrail_state.table.selected, 0);
19305 }
19306
19307 #[test]
19308 fn test_cloudtrail_pagination_navigation_right_arrow() {
19309 let mut app = setup_cloudtrail_pagination_test();
19310 app.current_service = Service::CloudTrailEvents;
19311 app.mode = Mode::FilterInput;
19312 app.cloudtrail_state.input_focus = InputFocus::Pagination;
19313 app.cloudtrail_state.table.page_size = PageSize::Ten;
19314
19315 app.cloudtrail_state.table.items = (0..25)
19317 .map(|i| CloudTrailEvent {
19318 event_name: format!("Event{}", i),
19319 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19320 username: "user".to_string(),
19321 event_source: "s3.amazonaws.com".to_string(),
19322 resource_type: "Bucket".to_string(),
19323 resource_name: "bucket".to_string(),
19324 read_only: "false".to_string(),
19325 aws_region: "us-east-1".to_string(),
19326 event_id: "id".to_string(),
19327 access_key_id: "key".to_string(),
19328 source_ip_address: "1.2.3.4".to_string(),
19329 error_code: "".to_string(),
19330 request_id: "req".to_string(),
19331 event_type: "AwsApiCall".to_string(),
19332 cloud_trail_event_json: "{}".to_string(),
19333 })
19334 .collect();
19335
19336 assert_eq!(app.cloudtrail_state.table.selected, 0);
19338
19339 app.handle_action(Action::NextItem);
19341 assert_eq!(app.cloudtrail_state.table.selected, 10);
19342
19343 app.handle_action(Action::NextItem);
19345 assert_eq!(app.cloudtrail_state.table.selected, 20);
19346
19347 app.handle_action(Action::NextItem);
19349 assert_eq!(app.cloudtrail_state.table.selected, 20);
19350 }
19351
19352 #[test]
19353 fn test_cloudtrail_pagination_navigation_left_arrow() {
19354 let mut app = App::new_without_client("default".to_string(), None);
19355 app.service_selected = true;
19356 app.current_service = Service::CloudTrailEvents;
19357 app.mode = Mode::FilterInput;
19358 app.cloudtrail_state.input_focus = InputFocus::Pagination;
19359 app.cloudtrail_state.table.page_size = PageSize::Ten;
19360
19361 app.cloudtrail_state.table.items = (0..25)
19363 .map(|i| CloudTrailEvent {
19364 event_name: format!("Event{}", i),
19365 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19366 username: "user".to_string(),
19367 event_source: "s3.amazonaws.com".to_string(),
19368 resource_type: "Bucket".to_string(),
19369 resource_name: "bucket".to_string(),
19370 read_only: "false".to_string(),
19371 aws_region: "us-east-1".to_string(),
19372 event_id: "id".to_string(),
19373 access_key_id: "key".to_string(),
19374 source_ip_address: "1.2.3.4".to_string(),
19375 error_code: "".to_string(),
19376 request_id: "req".to_string(),
19377 event_type: "AwsApiCall".to_string(),
19378 cloud_trail_event_json: "{}".to_string(),
19379 })
19380 .collect();
19381
19382 app.cloudtrail_state.table.selected = 20;
19384
19385 app.handle_action(Action::PrevItem);
19387 assert_eq!(app.cloudtrail_state.table.selected, 10);
19388
19389 app.handle_action(Action::PrevItem);
19391 assert_eq!(app.cloudtrail_state.table.selected, 0);
19392
19393 app.handle_action(Action::PrevItem);
19395 assert_eq!(app.cloudtrail_state.table.selected, 0);
19396 }
19397
19398 #[test]
19399 fn test_cloudtrail_arrow_navigation() {
19400 let mut app = App::new_without_client("default".to_string(), None);
19401 app.service_selected = true;
19402 app.current_service = Service::CloudTrailEvents;
19403 app.mode = Mode::Normal;
19404
19405 app.cloudtrail_state.table.items = (0..5)
19407 .map(|i| CloudTrailEvent {
19408 event_name: format!("Event{}", i),
19409 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19410 username: "user".to_string(),
19411 event_source: "s3.amazonaws.com".to_string(),
19412 resource_type: "Bucket".to_string(),
19413 resource_name: "bucket".to_string(),
19414 read_only: "false".to_string(),
19415 aws_region: "us-east-1".to_string(),
19416 event_id: "id".to_string(),
19417 access_key_id: "key".to_string(),
19418 source_ip_address: "1.2.3.4".to_string(),
19419 error_code: "".to_string(),
19420 request_id: "req".to_string(),
19421 event_type: "AwsApiCall".to_string(),
19422 cloud_trail_event_json: "{}".to_string(),
19423 })
19424 .collect();
19425
19426 app.handle_action(Action::NextItem);
19428 assert_eq!(app.cloudtrail_state.table.selected, 1);
19429
19430 app.handle_action(Action::NextItem);
19431 assert_eq!(app.cloudtrail_state.table.selected, 2);
19432
19433 app.handle_action(Action::PrevItem);
19435 assert_eq!(app.cloudtrail_state.table.selected, 1);
19436 }
19437
19438 #[test]
19439 fn test_cloudtrail_page_down_navigation() {
19440 let mut app = App::new_without_client("default".to_string(), None);
19441 app.service_selected = true;
19442 app.current_service = Service::CloudTrailEvents;
19443 app.mode = Mode::Normal;
19444 app.cloudtrail_state.table.page_size = PageSize::Ten;
19445
19446 app.cloudtrail_state.table.items = (0..25)
19448 .map(|i| CloudTrailEvent {
19449 event_name: format!("Event{}", i),
19450 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19451 username: "user".to_string(),
19452 event_source: "s3.amazonaws.com".to_string(),
19453 resource_type: "Bucket".to_string(),
19454 resource_name: "bucket".to_string(),
19455 read_only: "false".to_string(),
19456 aws_region: "us-east-1".to_string(),
19457 event_id: "id".to_string(),
19458 access_key_id: "key".to_string(),
19459 source_ip_address: "1.2.3.4".to_string(),
19460 error_code: "".to_string(),
19461 request_id: "req".to_string(),
19462 event_type: "AwsApiCall".to_string(),
19463 cloud_trail_event_json: "{}".to_string(),
19464 })
19465 .collect();
19466
19467 app.handle_action(Action::PageDown);
19469 assert_eq!(app.cloudtrail_state.table.selected, 10);
19470
19471 app.handle_action(Action::PageDown);
19472 assert_eq!(app.cloudtrail_state.table.selected, 20);
19473 }
19474
19475 #[test]
19476 fn test_cloudtrail_page_up_navigation() {
19477 let mut app = App::new_without_client("default".to_string(), None);
19478 app.service_selected = true;
19479 app.current_service = Service::CloudTrailEvents;
19480 app.mode = Mode::Normal;
19481 app.cloudtrail_state.table.page_size = PageSize::Ten;
19482
19483 app.cloudtrail_state.table.items = (0..25)
19485 .map(|i| CloudTrailEvent {
19486 event_name: format!("Event{}", i),
19487 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19488 username: "user".to_string(),
19489 event_source: "s3.amazonaws.com".to_string(),
19490 resource_type: "Bucket".to_string(),
19491 resource_name: "bucket".to_string(),
19492 read_only: "false".to_string(),
19493 aws_region: "us-east-1".to_string(),
19494 event_id: "id".to_string(),
19495 access_key_id: "key".to_string(),
19496 source_ip_address: "1.2.3.4".to_string(),
19497 error_code: "".to_string(),
19498 request_id: "req".to_string(),
19499 event_type: "AwsApiCall".to_string(),
19500 cloud_trail_event_json: "{}".to_string(),
19501 })
19502 .collect();
19503
19504 app.cloudtrail_state.table.selected = 20;
19506
19507 app.handle_action(Action::PageUp);
19509 assert_eq!(app.cloudtrail_state.table.selected, 10);
19510
19511 app.handle_action(Action::PageUp);
19512 assert_eq!(app.cloudtrail_state.table.selected, 0);
19513 }
19514
19515 #[test]
19516 fn test_cloudtrail_page_size_change_updates_display() {
19517 let mut app = App::new_without_client("default".to_string(), None);
19518 app.service_selected = true;
19519 app.current_service = Service::CloudTrailEvents;
19520 app.mode = Mode::ColumnSelector;
19521
19522 app.cloudtrail_state.table.items = (0..50)
19524 .map(|i| CloudTrailEvent {
19525 event_name: format!("Event{}", i),
19526 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19527 username: "user".to_string(),
19528 event_source: "s3.amazonaws.com".to_string(),
19529 resource_type: "Bucket".to_string(),
19530 resource_name: "bucket".to_string(),
19531 read_only: "false".to_string(),
19532 aws_region: "us-east-1".to_string(),
19533 event_id: "id".to_string(),
19534 access_key_id: "key".to_string(),
19535 source_ip_address: "1.2.3.4".to_string(),
19536 error_code: "".to_string(),
19537 request_id: "req".to_string(),
19538 event_type: "AwsApiCall".to_string(),
19539 cloud_trail_event_json: "{}".to_string(),
19540 })
19541 .collect();
19542
19543 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
19545
19546 app.column_selector_index = 17;
19548 app.handle_action(Action::ToggleColumn);
19549
19550 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
19551 }
19553
19554 #[test]
19555 fn test_cloudtrail_all_columns_toggleable() {
19556 let mut app = App::new_without_client("default".to_string(), None);
19557 app.service_selected = true;
19558 app.current_service = Service::CloudTrailEvents;
19559 app.mode = Mode::ColumnSelector;
19560
19561 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
19563
19564 for idx in 1..=14 {
19567 app.column_selector_index = idx;
19568 let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
19569 let initial_count = initial_visible.len();
19570
19571 let col = app.cloudtrail_event_column_ids.get(idx - 1).unwrap();
19573 let is_visible = initial_visible.contains(col);
19574
19575 app.handle_action(Action::ToggleColumn);
19576
19577 if is_visible && initial_count == 1 {
19579 assert_eq!(
19580 app.cloudtrail_event_visible_column_ids, initial_visible,
19581 "Last visible column at index {} should not be toggleable",
19582 idx
19583 );
19584 } else {
19585 assert_ne!(
19587 app.cloudtrail_event_visible_column_ids, initial_visible,
19588 "Column at index {} should be toggleable when not the last one",
19589 idx
19590 );
19591 }
19592 }
19593 }
19594
19595 #[test]
19596 fn test_cloudtrail_readonly_column_toggleable() {
19597 use crate::cloudtrail::events::CloudTrailEventColumn;
19598
19599 let mut app = App::new_without_client("default".to_string(), None);
19600 app.service_selected = true;
19601 app.current_service = Service::CloudTrailEvents;
19602 app.mode = Mode::ColumnSelector;
19603
19604 let readonly_id = CloudTrailEventColumn::ReadOnly.id();
19607
19608 assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
19610
19611 assert!(!app
19613 .cloudtrail_event_visible_column_ids
19614 .contains(&readonly_id));
19615
19616 app.column_selector_index = 7;
19618 app.handle_action(Action::ToggleColumn);
19619
19620 assert!(
19622 app.cloudtrail_event_visible_column_ids
19623 .contains(&readonly_id),
19624 "ReadOnly column should be toggleable at index 7"
19625 );
19626
19627 app.handle_action(Action::ToggleColumn);
19629
19630 assert!(!app
19632 .cloudtrail_event_visible_column_ids
19633 .contains(&readonly_id));
19634 }
19635
19636 #[test]
19637 fn test_cloudtrail_pagination_limits_displayed_items() {
19638 let mut app = App::new_without_client("default".to_string(), None);
19639 app.service_selected = true;
19640 app.current_service = Service::CloudTrailEvents;
19641
19642 app.cloudtrail_state.table.items = (0..50)
19644 .map(|i| CloudTrailEvent {
19645 event_name: format!("Event{}", i),
19646 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19647 username: "user".to_string(),
19648 event_source: "s3.amazonaws.com".to_string(),
19649 resource_type: "Bucket".to_string(),
19650 resource_name: "bucket".to_string(),
19651 read_only: "false".to_string(),
19652 aws_region: "us-east-1".to_string(),
19653 event_id: "id".to_string(),
19654 access_key_id: "key".to_string(),
19655 source_ip_address: "1.2.3.4".to_string(),
19656 error_code: "".to_string(),
19657 request_id: "req".to_string(),
19658 event_type: "AwsApiCall".to_string(),
19659 cloud_trail_event_json: "{}".to_string(),
19660 })
19661 .collect();
19662
19663 app.cloudtrail_state.table.page_size = PageSize::Ten;
19665
19666 use ratatui::backend::TestBackend;
19668 use ratatui::Terminal;
19669
19670 let backend = TestBackend::new(100, 30);
19671 let mut terminal = Terminal::new(backend).unwrap();
19672
19673 terminal
19674 .draw(|frame| {
19675 let area = frame.area();
19676 crate::ui::cloudtrail::render_events(frame, &app, area);
19677 })
19678 .unwrap();
19679
19680 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
19683 }
19684
19685 #[test]
19686 fn test_cloudtrail_readonly_column_selectable_in_preferences() {
19687 let mut app = App::new_without_client("default".to_string(), None);
19688 app.service_selected = true;
19689 app.current_service = Service::CloudTrailEvents;
19690 app.mode = Mode::ColumnSelector;
19691
19692 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
19694
19695 app.column_selector_index = 7;
19697
19698 let max_index = app.get_column_selector_max();
19700 assert!(
19701 app.column_selector_index <= max_index,
19702 "Read-only column index {} should be <= max {}",
19703 app.column_selector_index,
19704 max_index
19705 );
19706
19707 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
19709 let col = CloudTrailEventColumn::from_id(col_id);
19710 assert!(col.is_some(), "Column should exist at index 7");
19711 assert_eq!(
19712 col.unwrap().default_name(),
19713 "Read-only",
19714 "Column at index 7 should be Read-only"
19715 );
19716 }
19717
19718 #[test]
19719 fn test_cloudtrail_navigate_to_readonly_column() {
19720 let mut app = App::new_without_client("default".to_string(), None);
19721 app.service_selected = true;
19722 app.current_service = Service::CloudTrailEvents;
19723 app.mode = Mode::ColumnSelector;
19724 app.column_selector_index = 1; println!(
19728 "cloudtrail_event_column_ids.len() = {}",
19729 app.cloudtrail_event_column_ids.len()
19730 );
19731 assert_eq!(
19732 app.cloudtrail_event_column_ids.len(),
19733 14,
19734 "Should have 14 columns"
19735 );
19736
19737 let column_count = app.get_column_count();
19739 println!("Column count from get_column_count(): {}", column_count);
19740 assert_eq!(column_count, 14, "Column count should be 14");
19741
19742 assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
19744 assert!(app.is_blank_row_index(15), "Index 15 should be blank");
19745
19746 for _ in 0..6 {
19748 app.handle_action(Action::NextItem);
19749 }
19750
19751 assert_eq!(
19752 app.column_selector_index, 7,
19753 "Should navigate to Read-only column at index 7"
19754 );
19755
19756 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
19758 let col = CloudTrailEventColumn::from_id(col_id).unwrap();
19759 assert_eq!(col.default_name(), "Read-only");
19760 }
19761
19762 #[test]
19763 fn test_cloudtrail_navigate_beyond_loaded_items() {
19764 let mut app = App::new_without_client("default".to_string(), None);
19765 app.service_selected = true;
19766 app.current_service = Service::CloudTrailEvents;
19767
19768 app.cloudtrail_state.table.items = (0..50)
19770 .map(|i| CloudTrailEvent {
19771 event_name: format!("Event{}", i),
19772 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19773 username: "user".to_string(),
19774 event_source: "s3.amazonaws.com".to_string(),
19775 resource_type: "Bucket".to_string(),
19776 resource_name: "bucket".to_string(),
19777 read_only: "false".to_string(),
19778 aws_region: "us-east-1".to_string(),
19779 event_id: "id".to_string(),
19780 access_key_id: "key".to_string(),
19781 source_ip_address: "1.2.3.4".to_string(),
19782 error_code: "".to_string(),
19783 request_id: "req".to_string(),
19784 event_type: "AwsApiCall".to_string(),
19785 cloud_trail_event_json: "{}".to_string(),
19786 })
19787 .collect();
19788
19789 let initial_selected = app.cloudtrail_state.table.selected;
19792 app.page_input = "10".to_string();
19793 app.go_to_page(10);
19794
19795 assert_eq!(
19797 app.cloudtrail_state.table.selected, initial_selected,
19798 "Should ignore navigation to page 10 when only 1 page is loaded"
19799 );
19800
19801 app.go_to_page(2);
19803 let page_size = app.cloudtrail_state.table.page_size.value();
19804 assert_eq!(
19805 app.cloudtrail_state.table.selected, page_size,
19806 "Should allow navigation to page 2 (loaded + 1)"
19807 );
19808 }
19809
19810 #[test]
19811 fn test_cloudtrail_page_change_resets_expansion() {
19812 let mut app = App::new_without_client("default".to_string(), None);
19813 app.service_selected = true;
19814 app.current_service = Service::CloudTrailEvents;
19815
19816 app.cloudtrail_state.table.items = (0..100)
19818 .map(|i| CloudTrailEvent {
19819 event_name: format!("Event{}", i),
19820 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19821 username: "user".to_string(),
19822 event_source: "s3.amazonaws.com".to_string(),
19823 resource_type: "Bucket".to_string(),
19824 resource_name: "bucket".to_string(),
19825 read_only: "false".to_string(),
19826 aws_region: "us-east-1".to_string(),
19827 event_id: "id".to_string(),
19828 access_key_id: "key".to_string(),
19829 source_ip_address: "1.2.3.4".to_string(),
19830 error_code: "".to_string(),
19831 request_id: "req".to_string(),
19832 event_type: "AwsApiCall".to_string(),
19833 cloud_trail_event_json: "{}".to_string(),
19834 })
19835 .collect();
19836
19837 app.cloudtrail_state.table.expanded_item = Some(5);
19839 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
19840
19841 app.go_to_page(2);
19843
19844 assert_eq!(
19846 app.cloudtrail_state.table.expanded_item, None,
19847 "Page change should reset expanded_item"
19848 );
19849 }
19850
19851 #[test]
19852 fn test_cloudtrail_pagination_right_left_arrow_works_when_filter_focused() {
19853 let mut app = App::new_without_client("default".to_string(), None);
19857 app.service_selected = true;
19858 app.current_service = Service::CloudTrailEvents;
19859 app.mode = Mode::FilterInput;
19860 app.cloudtrail_state.input_focus = InputFocus::Filter;
19862 app.cloudtrail_state.table.page_size = PageSize::Ten;
19863
19864 app.cloudtrail_state.table.items = (0..25)
19865 .map(|i| crate::cloudtrail::CloudTrailEvent {
19866 event_name: format!("Event{}", i),
19867 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19868 username: "user".to_string(),
19869 event_source: "s3.amazonaws.com".to_string(),
19870 resource_type: "Bucket".to_string(),
19871 resource_name: "bucket".to_string(),
19872 read_only: "false".to_string(),
19873 aws_region: "us-east-1".to_string(),
19874 event_id: format!("id-{}", i),
19875 access_key_id: "key".to_string(),
19876 source_ip_address: "1.2.3.4".to_string(),
19877 error_code: String::new(),
19878 request_id: "req".to_string(),
19879 event_type: "AwsApiCall".to_string(),
19880 cloud_trail_event_json: "{}".to_string(),
19881 })
19882 .collect();
19883
19884 assert_eq!(app.cloudtrail_state.table.selected, 0, "starts on page 1");
19885
19886 app.handle_action(Action::PageDown);
19888 assert_eq!(
19889 app.cloudtrail_state.table.selected, 10,
19890 "Right arrow must advance to page 2 even when filter text box is focused"
19891 );
19892
19893 app.handle_action(Action::PageUp);
19895 assert_eq!(
19896 app.cloudtrail_state.table.selected, 0,
19897 "Left arrow must go back to page 1 even when filter text box is focused"
19898 );
19899 }
19900
19901 #[test]
19902 fn test_cloudtrail_yank_in_event_detail_copies_json() {
19903 let mut app = test_app();
19905 app.current_service = Service::CloudTrailEvents;
19906 app.service_selected = true;
19907 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19908 event_name: "PutObject".to_string(),
19909 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19910 username: "alice".to_string(),
19911 event_source: "s3.amazonaws.com".to_string(),
19912 resource_type: "Bucket".to_string(),
19913 resource_name: "my-bucket".to_string(),
19914 read_only: "false".to_string(),
19915 aws_region: "us-east-1".to_string(),
19916 event_id: "evt-001".to_string(),
19917 access_key_id: "AKIA".to_string(),
19918 source_ip_address: "1.2.3.4".to_string(),
19919 error_code: String::new(),
19920 request_id: "req-001".to_string(),
19921 event_type: "AwsApiCall".to_string(),
19922 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19923 });
19924
19925 app.handle_action(Action::Yank);
19928 assert!(
19930 app.cloudtrail_state.current_event.is_some(),
19931 "Event must remain open after yank"
19932 );
19933 }
19934
19935 #[test]
19936 fn test_cloudtrail_filter_reduces_visible_events() {
19937 let mut app = test_app();
19942 app.current_service = Service::CloudTrailEvents;
19943 app.service_selected = true;
19944
19945 let items: Vec<CloudTrailEvent> = vec![
19946 CloudTrailEvent {
19947 event_name: "PutObject".to_string(),
19948 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19949 username: "alice".to_string(),
19950 event_source: "s3.amazonaws.com".to_string(),
19951 resource_type: "Bucket".to_string(),
19952 resource_name: "my-bucket".to_string(),
19953 read_only: "false".to_string(),
19954 aws_region: "us-east-1".to_string(),
19955 event_id: "evt-001".to_string(),
19956 access_key_id: "AKIA".to_string(),
19957 source_ip_address: "1.2.3.4".to_string(),
19958 error_code: String::new(),
19959 request_id: "req-001".to_string(),
19960 event_type: "AwsApiCall".to_string(),
19961 cloud_trail_event_json: "{}".to_string(),
19962 },
19963 CloudTrailEvent {
19964 event_name: "GetObject".to_string(),
19965 event_time: "2024-01-01 11:00:00 (UTC)".to_string(),
19966 username: "bob".to_string(),
19967 event_source: "s3.amazonaws.com".to_string(),
19968 resource_type: "Object".to_string(),
19969 resource_name: "my-key".to_string(),
19970 read_only: "true".to_string(),
19971 aws_region: "us-east-1".to_string(),
19972 event_id: "evt-002".to_string(),
19973 access_key_id: "AKIA2".to_string(),
19974 source_ip_address: "5.6.7.8".to_string(),
19975 error_code: String::new(),
19976 request_id: "req-002".to_string(),
19977 event_type: "AwsApiCall".to_string(),
19978 cloud_trail_event_json: "{}".to_string(),
19979 },
19980 ];
19981 app.cloudtrail_state.table.items = items;
19982
19983 let q = app.cloudtrail_state.table.filter.to_lowercase();
19985 let filtered: Vec<_> = app
19986 .cloudtrail_state
19987 .table
19988 .items
19989 .iter()
19990 .filter(|e| {
19991 q.is_empty()
19992 || e.event_name.to_lowercase().contains(&q)
19993 || e.username.to_lowercase().contains(&q)
19994 })
19995 .collect();
19996 assert_eq!(
19997 filtered.len(),
19998 2,
19999 "No filter: both events should be visible"
20000 );
20001
20002 app.cloudtrail_state.table.filter = "PutObject".to_string();
20004 let q = app.cloudtrail_state.table.filter.to_lowercase();
20005 let filtered: Vec<_> = app
20006 .cloudtrail_state
20007 .table
20008 .items
20009 .iter()
20010 .filter(|e| {
20011 q.is_empty()
20012 || e.event_name.to_lowercase().contains(&q)
20013 || e.username.to_lowercase().contains(&q)
20014 })
20015 .collect();
20016 assert_eq!(
20017 filtered.len(),
20018 1,
20019 "Filter 'PutObject' should show only 1 event"
20020 );
20021 assert_eq!(filtered[0].event_name, "PutObject");
20022
20023 app.cloudtrail_state.table.filter = "bob".to_string();
20025 let q = app.cloudtrail_state.table.filter.to_lowercase();
20026 let filtered: Vec<_> = app
20027 .cloudtrail_state
20028 .table
20029 .items
20030 .iter()
20031 .filter(|e| {
20032 q.is_empty()
20033 || e.event_name.to_lowercase().contains(&q)
20034 || e.username.to_lowercase().contains(&q)
20035 })
20036 .collect();
20037 assert_eq!(filtered.len(), 1, "Filter 'bob' should show only 1 event");
20038 assert_eq!(filtered[0].username, "bob");
20039 }
20040
20041 #[test]
20042 fn test_lambda_application_resources_have_columns() {
20043 let app = test_app();
20044
20045 assert_eq!(app.lambda_resource_visible_column_ids.len(), 4);
20047 assert_eq!(app.lambda_resource_column_ids.len(), 4);
20048
20049 assert!(app
20051 .lambda_resource_visible_column_ids
20052 .contains(&"column.lambda.resource.logical_id"));
20053 assert!(app
20054 .lambda_resource_column_ids
20055 .contains(&"column.lambda.resource.logical_id"));
20056 }
20057
20058 #[test]
20059 fn test_lambda_functions_list_right_expands_left_collapses() {
20060 use crate::lambda::Function;
20061 let mut app = test_app();
20062 app.mode = Mode::Normal;
20063 app.current_service = Service::LambdaFunctions;
20064 app.service_selected = true;
20065 app.lambda_state.table.items = vec![Function {
20066 name: "fn1".to_string(),
20067 arn: "arn::fn1".to_string(),
20068 application: None,
20069 description: String::new(),
20070 package_type: String::new(),
20071 runtime: "python3.12".to_string(),
20072 architecture: String::new(),
20073 code_size: 0,
20074 code_sha256: String::new(),
20075 memory_mb: 128,
20076 timeout_seconds: 3,
20077 last_modified: String::new(),
20078 layers: vec![],
20079 }];
20080 app.lambda_state.table.selected = 0;
20081
20082 app.handle_action(Action::NextPane);
20084 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
20085
20086 app.handle_action(Action::NextPane);
20088 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
20089
20090 app.handle_action(Action::CollapseRow);
20092 assert_eq!(app.lambda_state.table.expanded_item, None);
20093 }
20094
20095 #[test]
20096 fn test_lambda_applications_list_right_expands_left_collapses() {
20097 use crate::lambda::Application;
20098 let mut app = test_app();
20099 app.mode = Mode::Normal;
20100 app.current_service = Service::LambdaApplications;
20101 app.service_selected = true;
20102 app.lambda_application_state.table.items = vec![Application {
20103 name: "my-app".to_string(),
20104 arn: String::new(),
20105 description: String::new(),
20106 status: "ACTIVE".to_string(),
20107 last_modified: String::new(),
20108 }];
20109 app.lambda_application_state.table.selected = 0;
20110
20111 app.handle_action(Action::NextPane);
20113 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
20114
20115 app.handle_action(Action::NextPane);
20117 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
20118
20119 app.handle_action(Action::CollapseRow);
20121 assert_eq!(app.lambda_application_state.table.expanded_item, None);
20122 }
20123
20124 #[test]
20125 fn test_lambda_application_enter_on_deployments_tab_does_not_switch_to_overview() {
20126 use crate::lambda::Application;
20127 let mut app = test_app();
20128 app.mode = Mode::Normal;
20129 app.current_service = Service::LambdaApplications;
20130 app.service_selected = true;
20131 app.lambda_application_state.table.items = vec![Application {
20132 name: "my-app".to_string(),
20133 arn: String::new(),
20134 description: String::new(),
20135 status: "ACTIVE".to_string(),
20136 last_modified: String::new(),
20137 }];
20138
20139 app.handle_action(Action::Select);
20141 assert!(app.lambda_application_state.current_application.is_some());
20142
20143 use crate::ui::lambda::ApplicationDetailTab as LambdaApplicationDetailTab;
20145 app.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Deployments;
20146 assert_eq!(
20147 app.lambda_application_state.detail_tab,
20148 LambdaApplicationDetailTab::Deployments
20149 );
20150
20151 app.handle_action(Action::Select);
20153 assert_eq!(
20154 app.lambda_application_state.detail_tab,
20155 LambdaApplicationDetailTab::Deployments
20156 );
20157 }
20158
20159 #[test]
20160 fn test_lambda_applications_pagination_focus_left_right_jump_pages() {
20161 use crate::lambda::Application;
20162 let mut app = test_app();
20163 app.current_service = Service::LambdaApplications;
20164 app.service_selected = true;
20165 app.mode = Mode::FilterInput;
20166 app.lambda_application_state.input_focus = InputFocus::Pagination;
20167
20168 app.lambda_application_state.table.items = (0..100)
20170 .map(|i| Application {
20171 name: format!("app{:03}", i),
20172 arn: String::new(),
20173 description: String::new(),
20174 status: "ACTIVE".to_string(),
20175 last_modified: String::new(),
20176 })
20177 .collect();
20178 app.lambda_application_state.table.selected = 0;
20179
20180 app.handle_action(Action::PageDown);
20182 assert_eq!(
20183 app.lambda_application_state.table.selected, 50,
20184 "Right arrow with pagination focus should jump to page 2"
20185 );
20186
20187 app.handle_action(Action::PageUp);
20189 assert_eq!(
20190 app.lambda_application_state.table.selected, 0,
20191 "Left arrow with pagination focus should jump back to page 1"
20192 );
20193
20194 app.handle_action(Action::NextItem);
20196 assert_eq!(
20197 app.lambda_application_state.table.selected, 0,
20198 "Down arrow must not move table when pagination is focused"
20199 );
20200 app.lambda_application_state.table.selected = 5;
20201 app.handle_action(Action::PrevItem);
20202 assert_eq!(
20203 app.lambda_application_state.table.selected, 5,
20204 "Up arrow must not move table when pagination is focused"
20205 );
20206 }
20207
20208 #[test]
20209 fn test_cloudwatch_alarms_enter_drills_down() {
20210 let mut app = test_app();
20211 app.mode = Mode::Normal;
20212 app.current_service = Service::CloudWatchAlarms;
20213 app.service_selected = true;
20214 app.view_mode = ViewMode::List;
20215
20216 app.alarms_state.table.items = vec![Alarm {
20218 name: "test-alarm".to_string(),
20219 state: "ALARM".to_string(),
20220 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20221 description: "Test alarm".to_string(),
20222 metric_name: "CPUUtilization".to_string(),
20223 namespace: "AWS/EC2".to_string(),
20224 statistic: "Average".to_string(),
20225 period: 300,
20226 comparison_operator: "GreaterThanThreshold".to_string(),
20227 threshold: 80.0,
20228 actions_enabled: true,
20229 state_reason: "Threshold crossed".to_string(),
20230 resource: "".to_string(),
20231 dimensions: "".to_string(),
20232 expression: "".to_string(),
20233 alarm_type: "MetricAlarm".to_string(),
20234 cross_account: "".to_string(),
20235 ..Default::default()
20236 }];
20237
20238 assert!(app.alarms_state.current_alarm.is_none());
20239 assert_eq!(app.view_mode, ViewMode::List);
20240 assert!(!app.alarms_state.metrics_loading);
20241
20242 app.handle_action(Action::Select);
20244
20245 assert_eq!(
20246 app.alarms_state.current_alarm,
20247 Some("test-alarm".to_string())
20248 );
20249 assert_eq!(app.view_mode, ViewMode::Detail);
20250 assert!(
20251 app.alarms_state.metrics_loading,
20252 "Should trigger metrics loading"
20253 );
20254 }
20255
20256 #[test]
20257 fn test_cloudwatch_alarms_metric_data_renders() {
20258 let mut app = test_app();
20259 app.mode = Mode::Normal;
20260 app.current_service = Service::CloudWatchAlarms;
20261 app.service_selected = true;
20262 app.view_mode = ViewMode::Detail;
20263
20264 app.alarms_state.table.items = vec![Alarm {
20266 name: "test-alarm".to_string(),
20267 state: "ALARM".to_string(),
20268 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20269 description: "Test alarm".to_string(),
20270 metric_name: "CPUUtilization".to_string(),
20271 namespace: "AWS/EC2".to_string(),
20272 statistic: "Average".to_string(),
20273 period: 300,
20274 comparison_operator: "GreaterThanThreshold".to_string(),
20275 threshold: 80.0,
20276 actions_enabled: true,
20277 state_reason: "Threshold crossed".to_string(),
20278 resource: "".to_string(),
20279 dimensions: "".to_string(),
20280 expression: "".to_string(),
20281 alarm_type: "MetricAlarm".to_string(),
20282 cross_account: "".to_string(),
20283 ..Default::default()
20284 }];
20285
20286 app.alarms_state.current_alarm = Some("test-alarm".to_string());
20287
20288 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0), (3000, 70.0)];
20290 app.alarms_state.metrics_loading = false;
20291
20292 assert!(!app.alarms_state.metric_data.is_empty());
20294 assert_eq!(app.alarms_state.metric_data.len(), 3);
20295 assert!(!app.alarms_state.metrics_loading);
20296 }
20297
20298 #[test]
20299 fn test_cloudwatch_alarms_back_clears_metrics() {
20300 let mut app = test_app();
20301 app.mode = Mode::Normal;
20302 app.current_service = Service::CloudWatchAlarms;
20303 app.service_selected = true;
20304 app.view_mode = ViewMode::Detail;
20305
20306 app.alarms_state.current_alarm = Some("test-alarm".to_string());
20308 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
20309
20310 assert!(!app.alarms_state.metric_data.is_empty());
20311
20312 app.handle_action(Action::GoBack);
20314
20315 assert!(app.alarms_state.current_alarm.is_none());
20317 assert!(app.alarms_state.metric_data.is_empty());
20318 assert_eq!(app.view_mode, ViewMode::List);
20319 }
20320
20321 #[test]
20322 fn test_cloudwatch_alarms_refresh_reloads_metrics() {
20323 let mut app = test_app();
20324 app.mode = Mode::Normal;
20325 app.current_service = Service::CloudWatchAlarms;
20326 app.service_selected = true;
20327 app.view_mode = ViewMode::Detail;
20328
20329 app.alarms_state.current_alarm = Some("test-alarm".to_string());
20331 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
20332 app.alarms_state.metrics_loading = false;
20333
20334 assert!(!app.alarms_state.metric_data.is_empty());
20335 assert!(!app.alarms_state.metrics_loading);
20336
20337 app.handle_action(Action::Refresh);
20339
20340 assert!(!app.alarms_state.metric_data.is_empty());
20342 assert!(app.alarms_state.metrics_loading);
20343 }
20344
20345 #[test]
20346 fn test_cloudwatch_alarms_right_arrow_expands() {
20347 let mut app = test_app();
20348 app.mode = Mode::Normal;
20349 app.current_service = Service::CloudWatchAlarms;
20350 app.service_selected = true;
20351 app.view_mode = ViewMode::List;
20352
20353 app.alarms_state.table.items = vec![Alarm {
20355 name: "test-alarm".to_string(),
20356 state: "ALARM".to_string(),
20357 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20358 description: "Test alarm".to_string(),
20359 metric_name: "CPUUtilization".to_string(),
20360 namespace: "AWS/EC2".to_string(),
20361 statistic: "Average".to_string(),
20362 period: 300,
20363 comparison_operator: "GreaterThanThreshold".to_string(),
20364 threshold: 80.0,
20365 actions_enabled: true,
20366 state_reason: "Threshold crossed".to_string(),
20367 resource: "".to_string(),
20368 dimensions: "".to_string(),
20369 expression: "".to_string(),
20370 alarm_type: "MetricAlarm".to_string(),
20371 cross_account: "".to_string(),
20372 ..Default::default()
20373 }];
20374
20375 assert!(!app.alarms_state.table.is_expanded());
20376
20377 app.handle_action(Action::NextPane);
20379 assert!(app.alarms_state.table.is_expanded());
20380
20381 app.handle_action(Action::PrevPane);
20383 assert!(!app.alarms_state.table.is_expanded());
20384 }
20385
20386 #[test]
20387 fn test_cloudwatch_alarms_tab_switches_tabs() {
20388 let mut app = test_app();
20389 app.mode = Mode::Normal;
20390 app.current_service = Service::CloudWatchAlarms;
20391 app.service_selected = true;
20392
20393 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
20395
20396 app.handle_action(Action::NextDetailTab);
20398 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
20399
20400 app.handle_action(Action::NextDetailTab);
20402 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
20403
20404 app.handle_action(Action::PrevDetailTab);
20406 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
20407 }
20408
20409 #[test]
20410 fn test_cloudwatch_alarms_pagination_changes_table_contents() {
20411 let mut app = test_app();
20415 app.current_service = Service::CloudWatchAlarms;
20416 app.service_selected = true;
20417 app.mode = Mode::FilterInput;
20418 app.alarms_state.input_focus = InputFocus::Pagination;
20419 app.alarms_state.table.page_size = crate::common::PageSize::Ten;
20420
20421 app.alarms_state.table.items = (0..25)
20423 .map(|i| crate::cw::Alarm {
20424 name: format!("alarm-{:02}", i),
20425 state: "OK".to_string(),
20426 state_updated_timestamp: String::new(),
20427 description: String::new(),
20428 metric_name: String::new(),
20429 namespace: String::new(),
20430 statistic: String::new(),
20431 period: 60,
20432 comparison_operator: String::new(),
20433 threshold: 0.0,
20434 actions_enabled: false,
20435 state_reason: String::new(),
20436 resource: String::new(),
20437 dimensions: String::new(),
20438 expression: String::new(),
20439 alarm_type: String::new(),
20440 cross_account: String::new(),
20441 ..Default::default()
20442 })
20443 .collect();
20444
20445 assert_eq!(app.alarms_state.table.selected, 0, "starts on page 1");
20446
20447 app.handle_action(Action::PageDown);
20449 assert_eq!(
20450 app.alarms_state.table.selected, 10,
20451 "Right arrow must advance selected to page 2"
20452 );
20453
20454 app.handle_action(Action::PageUp);
20456 assert_eq!(
20457 app.alarms_state.table.selected, 0,
20458 "Left arrow must go back"
20459 );
20460
20461 app.go_to_page(3);
20463 assert_eq!(
20464 app.alarms_state.table.selected, 20,
20465 "go_to_page(3) must set selected to start of page 3"
20466 );
20467 }
20468
20469 #[test]
20470 fn test_cloudwatch_alarms_num_p_page_jump() {
20471 let mut app = test_app();
20474 app.current_service = Service::CloudWatchAlarms;
20475 app.service_selected = true;
20476 app.mode = Mode::Normal;
20477 app.alarms_state.table.page_size = crate::common::PageSize::Ten;
20478 app.alarms_state.table.items = (0..25)
20479 .map(|i| crate::cw::Alarm {
20480 name: format!("alarm-{:02}", i),
20481 state: "OK".to_string(),
20482 state_updated_timestamp: String::new(),
20483 description: String::new(),
20484 metric_name: String::new(),
20485 namespace: String::new(),
20486 statistic: String::new(),
20487 period: 60,
20488 comparison_operator: String::new(),
20489 threshold: 0.0,
20490 actions_enabled: false,
20491 state_reason: String::new(),
20492 resource: String::new(),
20493 dimensions: String::new(),
20494 expression: String::new(),
20495 alarm_type: String::new(),
20496 cross_account: String::new(),
20497 ..Default::default()
20498 })
20499 .collect();
20500
20501 app.handle_action(Action::FilterInput('2'));
20503 assert_eq!(app.page_input, "2");
20504 app.handle_action(Action::OpenColumnSelector);
20505 assert_eq!(
20506 app.alarms_state.table.selected, 10,
20507 "2p must jump to page 2 (selected=10)"
20508 );
20509 assert_eq!(app.page_input, "", "page_input must be cleared after jump");
20510 assert_ne!(
20512 app.mode,
20513 Mode::ColumnSelector,
20514 "mode must not open column selector when page_input was set"
20515 );
20516 }
20517
20518 #[test]
20519 fn test_cloudwatch_alarms_console_url_list_view() {
20520 let url = crate::cw::alarms::console_url("us-east-1", None);
20521 assert!(url.contains("alarmsV2"), "must contain alarmsV2");
20522 assert!(
20523 !url.contains("table") && !url.contains("card"),
20524 "must not embed view_mode"
20525 );
20526 }
20527
20528 #[test]
20529 fn test_cloudwatch_alarms_console_url_alarm_detail() {
20530 let url = crate::cw::alarms::console_url("us-east-1", Some("My-Alarm"));
20531 assert!(
20532 url.contains("alarmsV2:alarm/My-Alarm"),
20533 "must link directly to alarm, got: {url}"
20534 );
20535 }
20536
20537 #[test]
20538 fn test_cloudwatch_alarms_console_url_from_list_selected_alarm() {
20539 let mut app = test_app();
20542 app.current_service = Service::CloudWatchAlarms;
20543 app.service_selected = true;
20544 app.alarms_state.current_alarm = None; app.alarms_state.table.items = vec![
20546 crate::cw::Alarm {
20547 name: "alarm-0".to_string(),
20548 ..Default::default()
20549 },
20550 crate::cw::Alarm {
20551 name: "alarm-1".to_string(),
20552 ..Default::default()
20553 },
20554 ];
20555 app.alarms_state.table.selected = 1; let url = app.get_console_url();
20558 assert!(
20559 url.contains("alarmsV2:alarm/alarm-1"),
20560 "^o from list view must link to selected alarm, got: {url}"
20561 );
20562 }
20563
20564 #[test]
20565 fn test_cloudwatch_alarms_metric_math_alarm_does_not_error() {
20566 let alarm = crate::cw::Alarm {
20570 name: "math-alarm".to_string(),
20571 state: "OK".to_string(),
20572 state_updated_timestamp: String::new(),
20573 description: String::new(),
20574 metric_name: String::new(), namespace: String::new(), statistic: String::new(), period: 60,
20578 comparison_operator: "GreaterThanOrEqualToThreshold".to_string(),
20579 threshold: 1.0,
20580 actions_enabled: true,
20581 state_reason: String::new(),
20582 resource: String::new(),
20583 dimensions: String::new(),
20584 expression: "SUM([m1, m2])".to_string(),
20585 alarm_type: "expression".to_string(),
20586 cross_account: String::new(),
20587 ..Default::default()
20588 };
20589 assert!(
20591 alarm.statistic.is_empty(),
20592 "Metric math alarm must have empty statistic"
20593 );
20594 assert!(
20595 alarm.metric_name.is_empty(),
20596 "Metric math alarm must have empty metric_name"
20597 );
20598 let should_skip = alarm.statistic.is_empty() || alarm.metric_name.is_empty();
20601 assert!(
20602 should_skip,
20603 "Should skip get_metric_statistics for metric math alarms"
20604 );
20605 }
20606
20607 #[test]
20608 fn test_column_toggle_prevents_hiding_last_column() {
20609 let mut app = test_app();
20610 app.mode = Mode::Normal;
20611 app.current_service = Service::CloudWatchAlarms;
20612 app.service_selected = true;
20613
20614 app.cw_alarm_visible_column_ids = vec!["column.cw.alarm.name"];
20616
20617 app.handle_action(Action::OpenColumnSelector);
20619 assert_eq!(app.mode, Mode::ColumnSelector);
20620
20621 app.column_selector_index = 1;
20623
20624 app.handle_action(Action::ToggleColumn);
20626
20627 assert_eq!(app.cw_alarm_visible_column_ids.len(), 1);
20629 assert_eq!(app.cw_alarm_visible_column_ids[0], "column.cw.alarm.name");
20630 }
20631}
20632
20633#[cfg(test)]
20634mod insights_log_group_dropdown_tests {
20635 use super::*;
20636 use test_helpers::*;
20637
20638 fn setup_insights_with_log_groups(app: &mut App) {
20639 app.current_service = Service::CloudWatchInsights;
20640 app.service_selected = true;
20641 app.mode = Mode::InsightsInput;
20642 app.insights_state.insights.insights_focus = InsightsFocus::LogGroupSearch;
20643
20644 app.log_groups_state.log_groups.items = vec![
20646 rusticity_core::LogGroup {
20647 name: "/aws/lambda/my-function".to_string(),
20648 stored_bytes: None,
20649 retention_days: None,
20650 log_class: None,
20651 arn: None,
20652 log_group_arn: None,
20653 deletion_protection_enabled: None,
20654 creation_time: None,
20655 },
20656 rusticity_core::LogGroup {
20657 name: "/aws/containerinsights/my-cluster/application".to_string(),
20658 stored_bytes: None,
20659 retention_days: None,
20660 log_class: None,
20661 arn: None,
20662 log_group_arn: None,
20663 deletion_protection_enabled: None,
20664 creation_time: None,
20665 },
20666 ];
20667 }
20668
20669 #[test]
20670 fn test_typing_in_log_group_search_shows_dropdown() {
20671 let mut app = test_app();
20672 setup_insights_with_log_groups(&mut app);
20673
20674 app.handle_action(Action::FilterInput('a'));
20676 app.handle_action(Action::FilterInput('w'));
20677
20678 assert!(
20679 app.insights_state.insights.show_dropdown,
20680 "Dropdown must show when log group search has matches"
20681 );
20682 assert!(
20683 !app.insights_state.insights.log_group_matches.is_empty(),
20684 "Must have matches for 'aw'"
20685 );
20686 }
20687
20688 #[test]
20689 fn test_space_toggles_highlighted_log_group_in_dropdown() {
20690 let mut app = test_app();
20693 setup_insights_with_log_groups(&mut app);
20694
20695 app.handle_action(Action::FilterInput('a'));
20697 app.handle_action(Action::FilterInput('w'));
20698
20699 assert!(app.insights_state.insights.show_dropdown);
20700 assert!(app.insights_state.insights.selected_log_groups.is_empty());
20701
20702 app.handle_action(Action::ToggleFilterCheckbox);
20704
20705 assert!(
20706 !app.insights_state.insights.selected_log_groups.is_empty(),
20707 "Space must add the highlighted log group to selected_log_groups"
20708 );
20709 }
20710
20711 #[test]
20712 fn test_space_key_dispatches_toggle_not_filter_input_in_insights_mode() {
20713 use crate::keymap::handle_key;
20714 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
20715
20716 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
20717 let action = handle_key(key, Mode::InsightsInput);
20718 assert_eq!(
20719 action,
20720 Some(Action::ToggleFilterCheckbox),
20721 "Space in InsightsInput must dispatch ToggleFilterCheckbox, not FilterInput(' ')"
20722 );
20723 }
20724
20725 #[test]
20726 fn test_down_up_arrows_navigate_dropdown() {
20727 let mut app = test_app();
20728 setup_insights_with_log_groups(&mut app);
20729 app.handle_action(Action::FilterInput('a'));
20730 app.handle_action(Action::FilterInput('w'));
20731
20732 assert_eq!(app.insights_state.insights.dropdown_selected, 0);
20733
20734 app.handle_action(Action::NextItem);
20735 assert!(
20736 app.insights_state.insights.dropdown_selected >= 1
20737 || app.insights_state.insights.log_group_matches.len() == 1,
20738 "Down arrow must advance dropdown selection"
20739 );
20740 }
20741
20742 #[test]
20743 fn test_space_in_normal_mode_toggles_dropdown_entry_not_open_space_menu() {
20744 let mut app = test_app();
20747 setup_insights_with_log_groups(&mut app);
20748 app.handle_action(Action::StartFilter);
20750 assert_eq!(app.mode, Mode::InsightsInput);
20751 app.handle_action(Action::FilterInput('a'));
20752 app.handle_action(Action::FilterInput('w'));
20753 assert!(app.insights_state.insights.show_dropdown);
20754
20755 app.mode = Mode::Normal;
20757
20758 app.handle_action(Action::OpenSpaceMenu); assert_ne!(
20761 app.mode,
20762 Mode::SpaceMenu,
20763 "Space must NOT open space menu when Insights dropdown is showing"
20764 );
20765 assert!(
20766 !app.insights_state.insights.selected_log_groups.is_empty(),
20767 "Space must toggle the highlighted log group"
20768 );
20769 }
20770
20771 #[test]
20772 fn test_enter_selects_highlighted_entry_and_closes_dropdown() {
20773 let mut app = test_app();
20775 setup_insights_with_log_groups(&mut app);
20776 app.handle_action(Action::StartFilter);
20777 app.handle_action(Action::FilterInput('a'));
20778 app.handle_action(Action::FilterInput('w'));
20779 assert!(app.insights_state.insights.show_dropdown);
20780
20781 app.handle_action(Action::ApplyFilter);
20783
20784 assert!(
20785 !app.insights_state.insights.show_dropdown,
20786 "Dropdown must close after Enter"
20787 );
20788 assert!(
20789 !app.insights_state.insights.selected_log_groups.is_empty(),
20790 "Enter must select the highlighted log group before closing"
20791 );
20792 }
20793}