1pub use crate::aws::{Profile as AwsProfile, Region as AwsRegion};
2use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
3use crate::common::{ColumnTrait, CyclicEnum, InputFocus, PageSize, SortDirection};
4use crate::cw::insights::{InsightsFocus, InsightsState};
5pub use crate::cw::{Alarm, AlarmColumn};
6use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
7use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
8use crate::iam;
9use crate::keymap::{Action, Mode};
10pub use crate::lambda::DeploymentColumn;
11pub use crate::lambda::ResourceColumn;
12pub use crate::lambda::{
13 Application as LambdaApplication, ApplicationColumn as LambdaApplicationColumn,
14 Column as LambdaColumn, Function as LambdaFunction,
15};
16pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
17pub use crate::ui::cfn::{
18 DetailTab as CfnDetailTab, State as CfnState, StatusFilter as CfnStatusFilter,
19};
20pub use crate::ui::cw::alarms::{AlarmTab, AlarmViewMode};
21pub use crate::ui::ecr::{State as EcrState, Tab as EcrTab};
22use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
23pub use crate::ui::lambda::{
24 ApplicationState as LambdaApplicationState, DetailTab as LambdaDetailTab, State as LambdaState,
25};
26pub use crate::ui::s3::{BucketType as S3BucketType, ObjectTab as S3ObjectTab, State as S3State};
27pub use crate::ui::{
28 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, EventFilterFocus,
29 LogGroupColumn, Preferences, StreamColumn, StreamSort, TimeUnit,
30};
31use rusticity_core::{
32 AlarmsClient, AwsConfig, CloudFormationClient, CloudWatchClient, EcrClient, IamClient,
33 LambdaClient, LogEvent, LogGroup, LogStream, S3Client,
34};
35
36#[derive(Clone)]
37pub struct Tab {
38 pub service: Service,
39 pub title: String,
40 pub breadcrumb: String,
41}
42
43pub struct App {
44 pub running: bool,
45 pub mode: Mode,
46 pub config: AwsConfig,
47 pub cloudwatch_client: CloudWatchClient,
48 pub s3_client: S3Client,
49 pub alarms_client: AlarmsClient,
50 pub ecr_client: EcrClient,
51 pub iam_client: IamClient,
52 pub lambda_client: LambdaClient,
53 pub cloudformation_client: CloudFormationClient,
54 pub current_service: Service,
55 pub tabs: Vec<Tab>,
56 pub current_tab: usize,
57 pub tab_picker_selected: usize,
58 pub tab_filter: String,
59 pub pending_key: Option<char>,
60 pub log_groups_state: CloudWatchLogGroupsState,
61 pub insights_state: CloudWatchInsightsState,
62 pub alarms_state: CloudWatchAlarmsState,
63 pub s3_state: S3State,
64 pub ecr_state: EcrState,
65 pub lambda_state: LambdaState,
66 pub lambda_application_state: LambdaApplicationState,
67 pub cfn_state: CfnState,
68 pub iam_state: IamState,
69 pub service_picker: ServicePickerState,
70 pub service_selected: bool,
71 pub profile: String,
72 pub region: String,
73 pub region_selector_index: usize,
74 pub visible_columns: Vec<LogGroupColumn>,
75 pub all_columns: Vec<LogGroupColumn>,
76 pub column_selector_index: usize,
77 pub preference_section: Preferences,
78 pub visible_stream_columns: Vec<StreamColumn>,
79 pub all_stream_columns: Vec<StreamColumn>,
80 pub visible_event_columns: Vec<EventColumn>,
81 pub all_event_columns: Vec<EventColumn>,
82 pub visible_alarm_columns: Vec<AlarmColumn>,
83 pub all_alarm_columns: Vec<AlarmColumn>,
84 pub visible_bucket_columns: Vec<S3BucketColumn>,
85 pub all_bucket_columns: Vec<S3BucketColumn>,
86 pub visible_ecr_columns: Vec<EcrColumn>,
87 pub all_ecr_columns: Vec<EcrColumn>,
88 pub visible_ecr_image_columns: Vec<EcrImageColumn>,
89 pub all_ecr_image_columns: Vec<EcrImageColumn>,
90 pub visible_lambda_application_columns: Vec<LambdaApplicationColumn>,
91 pub all_lambda_application_columns: Vec<LambdaApplicationColumn>,
92 pub visible_deployment_columns: Vec<DeploymentColumn>,
93 pub all_deployment_columns: Vec<DeploymentColumn>,
94 pub visible_resource_columns: Vec<ResourceColumn>,
95 pub all_resource_columns: Vec<ResourceColumn>,
96 pub visible_cfn_columns: Vec<CfnColumn>,
97 pub all_cfn_columns: Vec<CfnColumn>,
98 pub visible_iam_columns: Vec<String>,
99 pub all_iam_columns: Vec<String>,
100 pub visible_role_columns: Vec<String>,
101 pub all_role_columns: Vec<String>,
102 pub visible_group_columns: Vec<String>,
103 pub all_group_columns: Vec<String>,
104 pub visible_policy_columns: Vec<String>,
105 pub all_policy_columns: Vec<String>,
106 pub view_mode: ViewMode,
107 pub error_message: Option<String>,
108 pub page_input: String,
109 pub calendar_date: Option<time::Date>,
110 pub calendar_selecting: CalendarField,
111 pub cursor_pos: usize,
112 pub current_session: Option<crate::session::Session>,
113 pub sessions: Vec<crate::session::Session>,
114 pub session_picker_selected: usize,
115 pub session_filter: String,
116 pub region_filter: String,
117 pub region_picker_selected: usize,
118 pub region_latencies: std::collections::HashMap<String, u64>,
119 pub profile_filter: String,
120 pub profile_picker_selected: usize,
121 pub available_profiles: Vec<AwsProfile>,
122 pub snapshot_requested: bool,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq)]
126pub enum CalendarField {
127 StartDate,
128 EndDate,
129}
130
131pub struct CloudWatchInsightsState {
132 pub insights: InsightsState,
133 pub loading: bool,
134}
135
136pub struct CloudWatchAlarmsState {
137 pub table: crate::table::TableState<Alarm>,
138 pub alarm_tab: AlarmTab,
139 pub view_as: AlarmViewMode,
140 pub wrap_lines: bool,
141 pub sort_column: String,
142 pub sort_direction: SortDirection,
143 pub input_focus: InputFocus,
144}
145
146impl ColumnTrait for S3ObjectColumn {
147 fn name(&self) -> &'static str {
148 self.name()
149 }
150}
151
152impl PageSize {
153 pub fn value(&self) -> usize {
154 match self {
155 PageSize::Ten => 10,
156 PageSize::TwentyFive => 25,
157 PageSize::Fifty => 50,
158 PageSize::OneHundred => 100,
159 }
160 }
161
162 pub fn next(&self) -> Self {
163 match self {
164 PageSize::Ten => PageSize::TwentyFive,
165 PageSize::TwentyFive => PageSize::Fifty,
166 PageSize::Fifty => PageSize::OneHundred,
167 PageSize::OneHundred => PageSize::Ten,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq)]
173pub enum S3ObjectColumn {
174 Name,
175 Type,
176 LastModified,
177 Size,
178 StorageClass,
179}
180
181impl S3ObjectColumn {
182 pub fn name(&self) -> &'static str {
183 match self {
184 S3ObjectColumn::Name => "Name",
185 S3ObjectColumn::Type => "Type",
186 S3ObjectColumn::LastModified => "Last modified",
187 S3ObjectColumn::Size => "Size",
188 S3ObjectColumn::StorageClass => "Storage class",
189 }
190 }
191
192 pub fn all() -> Vec<S3ObjectColumn> {
193 vec![
194 S3ObjectColumn::Name,
195 S3ObjectColumn::Type,
196 S3ObjectColumn::LastModified,
197 S3ObjectColumn::Size,
198 S3ObjectColumn::StorageClass,
199 ]
200 }
201}
202
203pub struct ServicePickerState {
204 pub filter: String,
205 pub selected: usize,
206 pub services: Vec<&'static str>,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq)]
210pub enum ViewMode {
211 List,
212 Detail,
213 Events,
214 InsightsResults,
215 PolicyView,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq)]
219pub enum Service {
220 CloudWatchLogGroups,
221 CloudWatchInsights,
222 CloudWatchAlarms,
223 S3Buckets,
224 EcrRepositories,
225 LambdaFunctions,
226 LambdaApplications,
227 CloudFormationStacks,
228 IamUsers,
229 IamRoles,
230 IamUserGroups,
231}
232
233impl Service {
234 pub fn name(&self) -> &str {
235 match self {
236 Service::CloudWatchLogGroups => "CloudWatch > Log Groups",
237 Service::CloudWatchInsights => "CloudWatch > Logs Insights",
238 Service::CloudWatchAlarms => "CloudWatch > Alarms",
239 Service::S3Buckets => "S3 > Buckets",
240 Service::EcrRepositories => "ECR > Repositories",
241 Service::LambdaFunctions => "Lambda > Functions",
242 Service::LambdaApplications => "Lambda > Applications",
243 Service::CloudFormationStacks => "CloudFormation > Stacks",
244 Service::IamUsers => "IAM > Users",
245 Service::IamRoles => "IAM > Roles",
246 Service::IamUserGroups => "IAM > User Groups",
247 }
248 }
249}
250
251fn copy_to_clipboard(text: &str) {
252 use std::io::Write;
253 use std::process::{Command, Stdio};
254 if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
255 if let Some(mut stdin) = child.stdin.take() {
256 let _ = stdin.write_all(text.as_bytes());
257 }
258 let _ = child.wait();
259 }
260}
261
262fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
263 if max > 0 {
264 *selected = (*selected + page_size).min(max - 1);
265 }
266}
267
268impl App {
269 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
270 if self.current_service == Service::CloudWatchAlarms {
271 Some(&mut self.alarms_state.table.filter)
272 } else if self.current_service == Service::S3Buckets {
273 if self.s3_state.current_bucket.is_some() {
274 Some(&mut self.s3_state.object_filter)
275 } else {
276 Some(&mut self.s3_state.buckets.filter)
277 }
278 } else if self.current_service == Service::EcrRepositories {
279 if self.ecr_state.current_repository.is_some() {
280 Some(&mut self.ecr_state.images.filter)
281 } else {
282 Some(&mut self.ecr_state.repositories.filter)
283 }
284 } else if self.current_service == Service::LambdaFunctions {
285 if self.lambda_state.current_version.is_some()
286 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
287 {
288 Some(&mut self.lambda_state.alias_table.filter)
289 } else if self.lambda_state.current_function.is_some()
290 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
291 {
292 Some(&mut self.lambda_state.version_table.filter)
293 } else if self.lambda_state.current_function.is_some()
294 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
295 {
296 Some(&mut self.lambda_state.alias_table.filter)
297 } else {
298 Some(&mut self.lambda_state.table.filter)
299 }
300 } else if self.current_service == Service::LambdaApplications {
301 if self.lambda_application_state.current_application.is_some() {
302 if self.lambda_application_state.detail_tab
303 == crate::ui::lambda::ApplicationDetailTab::Deployments
304 {
305 Some(&mut self.lambda_application_state.deployments.filter)
306 } else {
307 Some(&mut self.lambda_application_state.resources.filter)
308 }
309 } else {
310 Some(&mut self.lambda_application_state.table.filter)
311 }
312 } else if self.current_service == Service::CloudFormationStacks {
313 Some(&mut self.cfn_state.table.filter)
314 } else if self.current_service == Service::IamUsers {
315 if self.iam_state.current_user.is_some() {
316 if self.iam_state.user_tab == UserTab::Tags {
317 Some(&mut self.iam_state.user_tags.filter)
318 } else {
319 Some(&mut self.iam_state.policies.filter)
320 }
321 } else {
322 Some(&mut self.iam_state.users.filter)
323 }
324 } else if self.current_service == Service::IamRoles {
325 if self.iam_state.current_role.is_some() {
326 if self.iam_state.role_tab == RoleTab::Tags {
327 Some(&mut self.iam_state.tags.filter)
328 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
329 Some(&mut self.iam_state.last_accessed_filter)
330 } else {
331 Some(&mut self.iam_state.policies.filter)
332 }
333 } else {
334 Some(&mut self.iam_state.roles.filter)
335 }
336 } else if self.current_service == Service::IamUserGroups {
337 if self.iam_state.current_group.is_some() {
338 if self.iam_state.group_tab == GroupTab::Permissions {
339 Some(&mut self.iam_state.policies.filter)
340 } else if self.iam_state.group_tab == GroupTab::Users {
341 Some(&mut self.iam_state.group_users.filter)
342 } else {
343 None
344 }
345 } else {
346 Some(&mut self.iam_state.groups.filter)
347 }
348 } else if self.view_mode == ViewMode::List {
349 Some(&mut self.log_groups_state.log_groups.filter)
350 } else if self.view_mode == ViewMode::Detail
351 && self.log_groups_state.detail_tab == DetailTab::LogStreams
352 {
353 Some(&mut self.log_groups_state.stream_filter)
354 } else {
355 None
356 }
357 }
358
359 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
360 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
361 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
362
363 std::env::set_var("AWS_PROFILE", &profile_name);
364
365 let config = AwsConfig::new(region).await?;
366 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
367 let s3_client = S3Client::new(config.clone());
368 let alarms_client = AlarmsClient::new(config.clone());
369 let ecr_client = EcrClient::new(config.clone());
370 let iam_client = IamClient::new(config.clone());
371 let lambda_client = LambdaClient::new(config.clone());
372 let cloudformation_client = CloudFormationClient::new(config.clone());
373 let region_name = config.region.clone();
374
375 Ok(Self {
376 running: true,
377 mode: Mode::ServicePicker,
378 config,
379 cloudwatch_client,
380 s3_client,
381 alarms_client,
382 ecr_client,
383 iam_client,
384 lambda_client,
385 cloudformation_client,
386 current_service: Service::CloudWatchLogGroups,
387 tabs: Vec::new(),
388 current_tab: 0,
389 tab_picker_selected: 0,
390 tab_filter: String::new(),
391 pending_key: None,
392 log_groups_state: CloudWatchLogGroupsState::new(),
393 insights_state: CloudWatchInsightsState::new(),
394 alarms_state: CloudWatchAlarmsState::new(),
395 s3_state: S3State::new(),
396 ecr_state: EcrState::new(),
397 lambda_state: LambdaState::new(),
398 lambda_application_state: LambdaApplicationState::new(),
399 cfn_state: CfnState::new(),
400 iam_state: IamState::new(),
401 service_picker: ServicePickerState::new(),
402 service_selected: false,
403 profile: profile_name,
404 region: region_name,
405 region_selector_index: 0,
406 visible_columns: LogGroupColumn::default_visible(),
407 all_columns: LogGroupColumn::all(),
408 column_selector_index: 0,
409 visible_stream_columns: StreamColumn::default_visible(),
410 all_stream_columns: StreamColumn::all(),
411 visible_event_columns: EventColumn::default_visible(),
412 all_event_columns: EventColumn::all(),
413 visible_alarm_columns: vec![
414 AlarmColumn::Name,
415 AlarmColumn::State,
416 AlarmColumn::LastStateUpdate,
417 AlarmColumn::Conditions,
418 AlarmColumn::Actions,
419 ],
420 all_alarm_columns: AlarmColumn::all(),
421 visible_bucket_columns: vec![
422 S3BucketColumn::Name,
423 S3BucketColumn::Region,
424 S3BucketColumn::CreationDate,
425 ],
426 all_bucket_columns: S3BucketColumn::all(),
427 visible_ecr_columns: EcrColumn::all(),
428 all_ecr_columns: EcrColumn::all(),
429 visible_ecr_image_columns: EcrImageColumn::all(),
430 all_ecr_image_columns: EcrImageColumn::all(),
431 visible_lambda_application_columns: vec![
432 LambdaApplicationColumn::Name,
433 LambdaApplicationColumn::Description,
434 LambdaApplicationColumn::Status,
435 LambdaApplicationColumn::LastModified,
436 ],
437 all_lambda_application_columns: LambdaApplicationColumn::all(),
438 visible_deployment_columns: DeploymentColumn::all(),
439 all_deployment_columns: DeploymentColumn::all(),
440 visible_resource_columns: ResourceColumn::all(),
441 all_resource_columns: ResourceColumn::all(),
442 visible_cfn_columns: vec![
443 CfnColumn::Name,
444 CfnColumn::Status,
445 CfnColumn::CreatedTime,
446 CfnColumn::Description,
447 ],
448 all_cfn_columns: CfnColumn::all(),
449 visible_iam_columns: vec![
450 "User name".to_string(),
451 "Path".to_string(),
452 "Groups".to_string(),
453 "Last activity".to_string(),
454 "MFA".to_string(),
455 "Password age".to_string(),
456 "Console last sign-in".to_string(),
457 "Access key ID".to_string(),
458 "Active key age".to_string(),
459 "Access key last used".to_string(),
460 "ARN".to_string(),
461 ],
462 all_iam_columns: vec![
463 "User name".to_string(),
464 "Path".to_string(),
465 "Groups".to_string(),
466 "Last activity".to_string(),
467 "MFA".to_string(),
468 "Password age".to_string(),
469 "Console last sign-in".to_string(),
470 "Access key ID".to_string(),
471 "Active key age".to_string(),
472 "Access key last used".to_string(),
473 "ARN".to_string(),
474 "Creation time".to_string(),
475 "Console access".to_string(),
476 "Signing certs".to_string(),
477 ],
478 visible_role_columns: vec![
479 "Role name".to_string(),
480 "Trusted entities".to_string(),
481 "Creation time".to_string(),
482 ],
483 all_role_columns: vec![
484 "Role name".to_string(),
485 "Path".to_string(),
486 "Trusted entities".to_string(),
487 "ARN".to_string(),
488 "Creation time".to_string(),
489 "Description".to_string(),
490 "Max session duration".to_string(),
491 ],
492 visible_group_columns: vec![
493 "Group name".to_string(),
494 "Users".to_string(),
495 "Permissions".to_string(),
496 "Creation time".to_string(),
497 ],
498 all_group_columns: vec![
499 "Group name".to_string(),
500 "Path".to_string(),
501 "Users".to_string(),
502 "Permissions".to_string(),
503 "Creation time".to_string(),
504 ],
505 visible_policy_columns: vec![
506 "Policy name".to_string(),
507 "Type".to_string(),
508 "Attached via".to_string(),
509 ],
510 all_policy_columns: vec![
511 "Policy name".to_string(),
512 "Type".to_string(),
513 "Attached via".to_string(),
514 "Attached entities".to_string(),
515 "Description".to_string(),
516 "Creation time".to_string(),
517 "Edited time".to_string(),
518 ],
519 preference_section: Preferences::Columns,
520 view_mode: ViewMode::List,
521 error_message: None,
522 page_input: String::new(),
523 calendar_date: None,
524 calendar_selecting: CalendarField::StartDate,
525 cursor_pos: 0,
526 current_session: None,
527 sessions: Vec::new(),
528 session_picker_selected: 0,
529 session_filter: String::new(),
530 region_filter: String::new(),
531 region_picker_selected: 0,
532 region_latencies: std::collections::HashMap::new(),
533 profile_filter: String::new(),
534 profile_picker_selected: 0,
535 available_profiles: Vec::new(),
536 snapshot_requested: false,
537 })
538 }
539
540 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
541 let config = AwsConfig::dummy(region.clone());
542 Self {
543 running: true,
544 mode: Mode::ServicePicker,
545 config: config.clone(),
546 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
547 s3_client: S3Client::new(config.clone()),
548 alarms_client: AlarmsClient::new(config.clone()),
549 ecr_client: EcrClient::new(config.clone()),
550 iam_client: IamClient::new(config.clone()),
551 lambda_client: LambdaClient::new(config.clone()),
552 cloudformation_client: CloudFormationClient::new(config.clone()),
553 current_service: Service::CloudWatchLogGroups,
554 tabs: Vec::new(),
555 current_tab: 0,
556 tab_picker_selected: 0,
557 tab_filter: String::new(),
558 pending_key: None,
559 log_groups_state: CloudWatchLogGroupsState::new(),
560 insights_state: CloudWatchInsightsState::new(),
561 alarms_state: CloudWatchAlarmsState::new(),
562 s3_state: S3State::new(),
563 ecr_state: EcrState::new(),
564 lambda_state: LambdaState::new(),
565 lambda_application_state: LambdaApplicationState::new(),
566 cfn_state: CfnState::new(),
567 iam_state: IamState::new(),
568 service_picker: ServicePickerState::new(),
569 service_selected: false,
570 profile,
571 region: region.unwrap_or_default(),
572 region_selector_index: 0,
573 visible_columns: LogGroupColumn::default_visible(),
574 all_columns: LogGroupColumn::all(),
575 column_selector_index: 0,
576 preference_section: Preferences::Columns,
577 visible_stream_columns: StreamColumn::default_visible(),
578 all_stream_columns: StreamColumn::all(),
579 visible_event_columns: EventColumn::default_visible(),
580 all_event_columns: EventColumn::all(),
581 visible_alarm_columns: vec![
582 AlarmColumn::Name,
583 AlarmColumn::State,
584 AlarmColumn::LastStateUpdate,
585 AlarmColumn::Conditions,
586 AlarmColumn::Actions,
587 ],
588 all_alarm_columns: AlarmColumn::all(),
589 visible_bucket_columns: vec![
590 S3BucketColumn::Name,
591 S3BucketColumn::Region,
592 S3BucketColumn::CreationDate,
593 ],
594 all_bucket_columns: S3BucketColumn::all(),
595 visible_ecr_columns: EcrColumn::all(),
596 all_ecr_columns: EcrColumn::all(),
597 visible_ecr_image_columns: EcrImageColumn::all(),
598 all_ecr_image_columns: EcrImageColumn::all(),
599 visible_lambda_application_columns: vec![
600 LambdaApplicationColumn::Name,
601 LambdaApplicationColumn::Description,
602 LambdaApplicationColumn::Status,
603 LambdaApplicationColumn::LastModified,
604 ],
605 all_lambda_application_columns: LambdaApplicationColumn::all(),
606 visible_deployment_columns: DeploymentColumn::all(),
607 all_deployment_columns: DeploymentColumn::all(),
608 visible_resource_columns: ResourceColumn::all(),
609 all_resource_columns: ResourceColumn::all(),
610 visible_cfn_columns: vec![
611 CfnColumn::Name,
612 CfnColumn::Status,
613 CfnColumn::CreatedTime,
614 CfnColumn::Description,
615 ],
616 all_cfn_columns: CfnColumn::all(),
617 visible_iam_columns: vec![
618 "User name".to_string(),
619 "Path".to_string(),
620 "Groups".to_string(),
621 "Last activity".to_string(),
622 "MFA".to_string(),
623 "Password age".to_string(),
624 "Console last sign-in".to_string(),
625 "Access key ID".to_string(),
626 "Active key age".to_string(),
627 "Access key last used".to_string(),
628 "ARN".to_string(),
629 ],
630 all_iam_columns: vec![
631 "User name".to_string(),
632 "Path".to_string(),
633 "Groups".to_string(),
634 "Last activity".to_string(),
635 "MFA".to_string(),
636 "Password age".to_string(),
637 "Console last sign-in".to_string(),
638 "Access key ID".to_string(),
639 "Active key age".to_string(),
640 "Access key last used".to_string(),
641 "ARN".to_string(),
642 "Creation time".to_string(),
643 "Console access".to_string(),
644 "Signing certs".to_string(),
645 ],
646 visible_role_columns: vec![
647 "Role name".to_string(),
648 "Trusted entities".to_string(),
649 "Creation time".to_string(),
650 ],
651 all_role_columns: vec![
652 "Role name".to_string(),
653 "Path".to_string(),
654 "Trusted entities".to_string(),
655 "ARN".to_string(),
656 "Creation time".to_string(),
657 "Description".to_string(),
658 "Max session duration".to_string(),
659 ],
660 visible_group_columns: vec![
661 "Group name".to_string(),
662 "Users".to_string(),
663 "Permissions".to_string(),
664 "Creation time".to_string(),
665 ],
666 all_group_columns: vec![
667 "Group name".to_string(),
668 "Path".to_string(),
669 "Users".to_string(),
670 "Permissions".to_string(),
671 "Creation time".to_string(),
672 ],
673 visible_policy_columns: vec![
674 "Policy name".to_string(),
675 "Type".to_string(),
676 "Attached via".to_string(),
677 ],
678 all_policy_columns: vec![
679 "Policy name".to_string(),
680 "Type".to_string(),
681 "Attached via".to_string(),
682 "Attached entities".to_string(),
683 "Description".to_string(),
684 "Creation time".to_string(),
685 "Edited time".to_string(),
686 ],
687 view_mode: ViewMode::List,
688 error_message: None,
689 page_input: String::new(),
690 calendar_date: None,
691 calendar_selecting: CalendarField::StartDate,
692 cursor_pos: 0,
693 current_session: None,
694 sessions: Vec::new(),
695 session_picker_selected: 0,
696 session_filter: String::new(),
697 region_filter: String::new(),
698 region_picker_selected: 0,
699 region_latencies: std::collections::HashMap::new(),
700 profile_filter: String::new(),
701 profile_picker_selected: 0,
702 available_profiles: Vec::new(),
703 snapshot_requested: false,
704 }
705 }
706
707 pub fn handle_action(&mut self, action: Action) {
708 match action {
709 Action::Quit => {
710 self.save_current_session();
711 self.running = false;
712 }
713 Action::CloseService => {
714 if !self.tabs.is_empty() {
715 self.tabs.remove(self.current_tab);
717
718 if self.tabs.is_empty() {
719 self.service_selected = false;
721 self.current_tab = 0;
722 self.mode = Mode::ServicePicker;
723 } else {
724 if self.current_tab >= self.tabs.len() {
726 self.current_tab = self.tabs.len() - 1;
727 }
728 self.current_service = self.tabs[self.current_tab].service;
729 self.service_selected = true;
730 self.mode = Mode::Normal;
731 }
732 } else {
733 self.service_selected = false;
735 self.mode = Mode::Normal;
736 }
737 self.service_picker.filter.clear();
738 self.service_picker.selected = 0;
739 }
740 Action::NextItem => self.next_item(),
741 Action::PrevItem => self.prev_item(),
742 Action::PageUp => self.page_up(),
743 Action::PageDown => self.page_down(),
744 Action::NextPane => self.next_pane(),
745 Action::PrevPane => self.prev_pane(),
746 Action::Select => self.select_item(),
747 Action::OpenSpaceMenu => {
748 self.mode = Mode::SpaceMenu;
749 self.service_picker.filter.clear();
750 self.service_picker.selected = 0;
751 }
752 Action::CloseMenu => {
753 self.mode = Mode::Normal;
754 self.service_picker.filter.clear();
755 }
756 Action::NextTab => {
757 if !self.tabs.is_empty() {
758 self.current_tab = (self.current_tab + 1) % self.tabs.len();
759 self.current_service = self.tabs[self.current_tab].service;
760 }
761 }
762 Action::PrevTab => {
763 if !self.tabs.is_empty() {
764 self.current_tab = if self.current_tab == 0 {
765 self.tabs.len() - 1
766 } else {
767 self.current_tab - 1
768 };
769 self.current_service = self.tabs[self.current_tab].service;
770 }
771 }
772 Action::CloseTab => {
773 if !self.tabs.is_empty() {
774 self.tabs.remove(self.current_tab);
775 if self.tabs.is_empty() {
776 self.service_selected = false;
778 self.current_tab = 0;
779 self.service_picker.filter.clear();
780 self.service_picker.selected = 0;
781 self.mode = Mode::ServicePicker;
782 } else {
783 if self.current_tab >= self.tabs.len() {
786 self.current_tab = self.tabs.len() - 1;
787 }
788 self.current_service = self.tabs[self.current_tab].service;
789 self.service_selected = true;
790 self.mode = Mode::Normal;
791 }
792 }
793 }
794 Action::OpenTabPicker => {
795 if !self.tabs.is_empty() {
796 self.tab_picker_selected = self.current_tab;
797 self.mode = Mode::TabPicker;
798 } else {
799 self.mode = Mode::Normal;
800 }
801 }
802 Action::OpenSessionPicker => {
803 self.save_current_session();
804 self.sessions = crate::session::Session::list_all().unwrap_or_default();
805 self.session_picker_selected = 0;
806 self.mode = Mode::SessionPicker;
807 }
808 Action::LoadSession => {
809 let filtered_sessions = self.get_filtered_sessions();
810 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
811 let session = session.clone();
812 self.profile = session.profile.clone();
814 self.region = session.region.clone();
815 self.config.account_id = session.account_id.clone();
816 self.config.role_arn = session.role_arn.clone();
817
818 self.tabs.clear();
820 for session_tab in &session.tabs {
821 let service = match session_tab.service.as_str() {
823 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
824 "CloudWatchInsights" => Service::CloudWatchInsights,
825 "CloudWatchAlarms" => Service::CloudWatchAlarms,
826 "S3Buckets" => Service::S3Buckets,
827 "EcrRepositories" => Service::EcrRepositories,
828 "LambdaFunctions" => Service::LambdaFunctions,
829 "LambdaApplications" => Service::LambdaApplications,
830 "CloudFormationStacks" => Service::CloudFormationStacks,
831 "IamUsers" => Service::IamUsers,
832 "IamRoles" => Service::IamRoles,
833 "IamUserGroups" => Service::IamUserGroups,
834 _ => continue,
835 };
836
837 self.tabs.push(Tab {
838 service,
839 title: session_tab.title.clone(),
840 breadcrumb: session_tab.breadcrumb.clone(),
841 });
842
843 if let Some(filter) = &session_tab.filter {
845 if service == Service::CloudWatchLogGroups {
846 self.log_groups_state.log_groups.filter = filter.clone();
847 }
848 }
849 }
850
851 if !self.tabs.is_empty() {
852 self.current_tab = 0;
853 self.current_service = self.tabs[0].service;
854 self.service_selected = true;
855 self.current_session = Some(session.clone());
856 }
857 }
858 self.mode = Mode::Normal;
859 }
860 Action::SaveSession => {
861 }
863 Action::OpenServicePicker => {
864 if self.mode == Mode::ServicePicker {
865 self.tabs.push(Tab {
866 service: Service::S3Buckets,
867 title: "S3 > Buckets".to_string(),
868 breadcrumb: "S3 > Buckets".to_string(),
869 });
870 self.current_tab = self.tabs.len() - 1;
871 self.current_service = Service::S3Buckets;
872 self.view_mode = ViewMode::List;
873 self.service_selected = true;
874 self.mode = Mode::Normal;
875 } else {
876 self.mode = Mode::ServicePicker;
877 self.service_picker.filter.clear();
878 self.service_picker.selected = 0;
879 }
880 }
881 Action::OpenCloudWatch => {
882 self.current_service = Service::CloudWatchLogGroups;
883 self.view_mode = ViewMode::List;
884 self.service_selected = true;
885 self.mode = Mode::Normal;
886 }
887 Action::OpenCloudWatchSplit => {
888 self.current_service = Service::CloudWatchInsights;
889 self.view_mode = ViewMode::InsightsResults;
890 self.service_selected = true;
891 self.mode = Mode::Normal;
892 }
893 Action::OpenCloudWatchAlarms => {
894 self.current_service = Service::CloudWatchAlarms;
895 self.view_mode = ViewMode::List;
896 self.service_selected = true;
897 self.mode = Mode::Normal;
898 }
899 Action::FilterInput(c) => {
900 if self.mode == Mode::TabPicker {
901 self.tab_filter.push(c);
902 self.tab_picker_selected = 0;
903 } else if self.mode == Mode::RegionPicker {
904 self.region_filter.push(c);
905 self.region_picker_selected = 0;
906 } else if self.mode == Mode::ProfilePicker {
907 self.profile_filter.push(c);
908 self.profile_picker_selected = 0;
909 } else if self.mode == Mode::SessionPicker {
910 self.session_filter.push(c);
911 self.session_picker_selected = 0;
912 } else if self.mode == Mode::ServicePicker {
913 self.service_picker.filter.push(c);
914 self.service_picker.selected = 0;
915 } else if self.mode == Mode::InsightsInput {
916 use crate::app::InsightsFocus;
917 match self.insights_state.insights.insights_focus {
918 InsightsFocus::Query => {
919 self.insights_state.insights.query_text.push(c);
920 }
921 InsightsFocus::LogGroupSearch => {
922 self.insights_state.insights.log_group_search.push(c);
923 if !self.insights_state.insights.log_group_search.is_empty() {
925 self.insights_state.insights.log_group_matches = self
926 .log_groups_state
927 .log_groups
928 .items
929 .iter()
930 .filter(|g| {
931 g.name.to_lowercase().contains(
932 &self
933 .insights_state
934 .insights
935 .log_group_search
936 .to_lowercase(),
937 )
938 })
939 .take(50)
940 .map(|g| g.name.clone())
941 .collect();
942 self.insights_state.insights.show_dropdown = true;
943 } else {
944 self.insights_state.insights.log_group_matches.clear();
945 self.insights_state.insights.show_dropdown = false;
946 }
947 }
948 _ => {}
949 }
950 } else if self.mode == Mode::FilterInput {
951 let is_pagination_focused =
953 if self.current_service == Service::LambdaApplications {
954 if self.lambda_application_state.current_application.is_some() {
955 if self.lambda_application_state.detail_tab
956 == crate::ui::lambda::ApplicationDetailTab::Deployments
957 {
958 self.lambda_application_state.deployment_input_focus
959 == InputFocus::Pagination
960 } else {
961 self.lambda_application_state.resource_input_focus
962 == InputFocus::Pagination
963 }
964 } else {
965 self.lambda_application_state.input_focus == InputFocus::Pagination
966 }
967 } else if self.current_service == Service::CloudFormationStacks {
968 self.cfn_state.input_focus == InputFocus::Pagination
969 } else if self.current_service == Service::IamRoles
970 && self.iam_state.current_role.is_none()
971 {
972 self.iam_state.role_input_focus == InputFocus::Pagination
973 } else if self.view_mode == ViewMode::PolicyView {
974 self.iam_state.policy_input_focus == InputFocus::Pagination
975 } else if self.current_service == Service::CloudWatchAlarms {
976 self.alarms_state.input_focus == InputFocus::Pagination
977 } else if self.current_service == Service::CloudWatchLogGroups {
978 self.log_groups_state.input_focus == InputFocus::Pagination
979 } else if self.current_service == Service::EcrRepositories
980 && self.ecr_state.current_repository.is_none()
981 {
982 self.ecr_state.input_focus == InputFocus::Pagination
983 } else if self.current_service == Service::LambdaFunctions {
984 if self.lambda_state.current_function.is_some()
985 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
986 {
987 self.lambda_state.version_input_focus == InputFocus::Pagination
988 } else if self.lambda_state.current_function.is_none() {
989 self.lambda_state.input_focus == InputFocus::Pagination
990 } else {
991 false
992 }
993 } else {
994 false
995 };
996
997 if is_pagination_focused && c.is_ascii_digit() {
998 self.page_input.push(c);
999 } else if self.current_service == Service::LambdaApplications {
1000 let is_input_focused =
1001 if self.lambda_application_state.current_application.is_some() {
1002 if self.lambda_application_state.detail_tab
1003 == crate::ui::lambda::ApplicationDetailTab::Deployments
1004 {
1005 self.lambda_application_state.deployment_input_focus
1006 == InputFocus::Filter
1007 } else {
1008 self.lambda_application_state.resource_input_focus
1009 == InputFocus::Filter
1010 }
1011 } else {
1012 self.lambda_application_state.input_focus == InputFocus::Filter
1013 };
1014 if is_input_focused {
1015 if let Some(filter) = self.get_active_filter_mut() {
1016 filter.push(c);
1017 }
1018 }
1019 } else if self.current_service == Service::CloudFormationStacks {
1020 if self.cfn_state.input_focus == InputFocus::Filter {
1021 if let Some(filter) = self.get_active_filter_mut() {
1022 filter.push(c);
1023 }
1024 }
1025 } else if self.current_service == Service::EcrRepositories
1026 && self.ecr_state.current_repository.is_none()
1027 {
1028 if self.ecr_state.input_focus == InputFocus::Filter {
1029 if let Some(filter) = self.get_active_filter_mut() {
1030 filter.push(c);
1031 }
1032 }
1033 } else if self.current_service == Service::IamRoles
1034 && self.iam_state.current_role.is_none()
1035 {
1036 if self.iam_state.role_input_focus == InputFocus::Filter {
1037 if let Some(filter) = self.get_active_filter_mut() {
1038 filter.push(c);
1039 }
1040 }
1041 } else if self.view_mode == ViewMode::PolicyView {
1042 if self.iam_state.policy_input_focus == InputFocus::Filter {
1043 if let Some(filter) = self.get_active_filter_mut() {
1044 filter.push(c);
1045 }
1046 }
1047 } else if self.current_service == Service::LambdaFunctions
1048 && self.lambda_state.current_version.is_some()
1049 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1050 {
1051 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1052 if let Some(filter) = self.get_active_filter_mut() {
1053 filter.push(c);
1054 }
1055 }
1056 } else if self.current_service == Service::LambdaFunctions
1057 && self.lambda_state.current_function.is_some()
1058 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1059 {
1060 if self.lambda_state.version_input_focus == InputFocus::Filter {
1061 if let Some(filter) = self.get_active_filter_mut() {
1062 filter.push(c);
1063 }
1064 }
1065 } else if self.current_service == Service::LambdaFunctions
1066 && self.lambda_state.current_function.is_some()
1067 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1068 {
1069 if self.lambda_state.alias_input_focus == InputFocus::Filter {
1070 if let Some(filter) = self.get_active_filter_mut() {
1071 filter.push(c);
1072 }
1073 }
1074 } else if let Some(filter) = self.get_active_filter_mut() {
1075 filter.push(c);
1076 }
1077 } else if self.mode == Mode::EventFilterInput {
1078 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1079 self.log_groups_state.event_filter.push(c);
1080 } else if c.is_ascii_digit() {
1081 self.log_groups_state.relative_amount.push(c);
1082 }
1083 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1084 self.page_input.push(c);
1085 }
1086 }
1087 Action::FilterBackspace => {
1088 if self.mode == Mode::TabPicker {
1089 self.tab_filter.pop();
1090 self.tab_picker_selected = 0;
1091 } else if self.mode == Mode::RegionPicker {
1092 self.region_filter.pop();
1093 self.region_picker_selected = 0;
1094 } else if self.mode == Mode::ProfilePicker {
1095 self.profile_filter.pop();
1096 self.profile_picker_selected = 0;
1097 } else if self.mode == Mode::SessionPicker {
1098 self.session_filter.pop();
1099 self.session_picker_selected = 0;
1100 } else if self.mode == Mode::InsightsInput {
1101 use crate::app::InsightsFocus;
1102 match self.insights_state.insights.insights_focus {
1103 InsightsFocus::Query => {
1104 self.insights_state.insights.query_text.pop();
1105 }
1106 InsightsFocus::LogGroupSearch => {
1107 self.insights_state.insights.log_group_search.pop();
1108 if !self.insights_state.insights.log_group_search.is_empty() {
1110 self.insights_state.insights.log_group_matches = self
1111 .log_groups_state
1112 .log_groups
1113 .items
1114 .iter()
1115 .filter(|g| {
1116 g.name.to_lowercase().contains(
1117 &self
1118 .insights_state
1119 .insights
1120 .log_group_search
1121 .to_lowercase(),
1122 )
1123 })
1124 .take(50)
1125 .map(|g| g.name.clone())
1126 .collect();
1127 self.insights_state.insights.show_dropdown = true;
1128 } else {
1129 self.insights_state.insights.log_group_matches.clear();
1130 self.insights_state.insights.show_dropdown = false;
1131 }
1132 }
1133 _ => {}
1134 }
1135 } else if self.mode == Mode::FilterInput {
1136 if self.current_service == Service::CloudFormationStacks {
1138 if self.cfn_state.input_focus == InputFocus::Filter {
1139 if let Some(filter) = self.get_active_filter_mut() {
1140 filter.pop();
1141 }
1142 }
1143 } else if let Some(filter) = self.get_active_filter_mut() {
1144 filter.pop();
1145 }
1146 } else if self.mode == Mode::EventFilterInput {
1147 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1148 self.log_groups_state.event_filter.pop();
1149 } else {
1150 self.log_groups_state.relative_amount.pop();
1151 }
1152 }
1153 }
1154 Action::DeleteWord => {
1155 let text = if self.mode == Mode::ServicePicker {
1156 &mut self.service_picker.filter
1157 } else if self.mode == Mode::InsightsInput {
1158 use crate::app::InsightsFocus;
1159 match self.insights_state.insights.insights_focus {
1160 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1161 InsightsFocus::LogGroupSearch => {
1162 &mut self.insights_state.insights.log_group_search
1163 }
1164 _ => return,
1165 }
1166 } else if self.mode == Mode::FilterInput {
1167 if let Some(filter) = self.get_active_filter_mut() {
1168 filter
1169 } else {
1170 return;
1171 }
1172 } else if self.mode == Mode::EventFilterInput {
1173 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1174 &mut self.log_groups_state.event_filter
1175 } else {
1176 &mut self.log_groups_state.relative_amount
1177 }
1178 } else {
1179 return;
1180 };
1181
1182 if text.is_empty() {
1183 return;
1184 }
1185
1186 let mut chars: Vec<char> = text.chars().collect();
1187 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1188 chars.pop();
1189 }
1190 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1191 chars.pop();
1192 }
1193 *text = chars.into_iter().collect();
1194 }
1195 Action::WordLeft => {
1196 }
1198 Action::WordRight => {
1199 }
1201 Action::OpenColumnSelector => {
1202 if !self.page_input.is_empty() {
1204 if let Ok(page) = self.page_input.parse::<usize>() {
1205 self.go_to_page(page);
1206 }
1207 self.page_input.clear();
1208 } else {
1209 self.mode = Mode::ColumnSelector;
1210 self.column_selector_index = 0;
1211 }
1212 }
1213 Action::ToggleColumn => {
1214 if self.current_service == Service::S3Buckets
1215 && self.s3_state.current_bucket.is_none()
1216 {
1217 if let Some(&col) = self.all_bucket_columns.get(self.column_selector_index) {
1218 if let Some(pos) =
1219 self.visible_bucket_columns.iter().position(|&c| c == col)
1220 {
1221 self.visible_bucket_columns.remove(pos);
1222 } else {
1223 self.visible_bucket_columns.push(col);
1224 }
1225 }
1226 } else if self.current_service == Service::CloudWatchAlarms {
1227 let idx = self.column_selector_index;
1231 if (1..=16).contains(&idx) {
1232 if let Some(&col) = self.all_alarm_columns.get(idx - 1) {
1234 if let Some(pos) =
1235 self.visible_alarm_columns.iter().position(|&c| c == col)
1236 {
1237 self.visible_alarm_columns.remove(pos);
1238 } else {
1239 self.visible_alarm_columns.push(col);
1240 }
1241 }
1242 } else if idx == 19 {
1243 self.alarms_state.view_as = AlarmViewMode::Table;
1244 } else if idx == 20 {
1245 self.alarms_state.view_as = AlarmViewMode::Cards;
1246 } else if idx == 23 {
1247 self.alarms_state.table.page_size = PageSize::Ten;
1248 } else if idx == 24 {
1249 self.alarms_state.table.page_size = PageSize::TwentyFive;
1250 } else if idx == 25 {
1251 self.alarms_state.table.page_size = PageSize::Fifty;
1252 } else if idx == 26 {
1253 self.alarms_state.table.page_size = PageSize::OneHundred;
1254 } else if idx == 29 {
1255 self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1256 }
1257 } else if self.current_service == Service::EcrRepositories {
1258 if self.ecr_state.current_repository.is_some() {
1259 let idx = self.column_selector_index;
1261 if let Some(&col) = self.all_ecr_image_columns.get(idx) {
1262 if let Some(pos) = self
1263 .visible_ecr_image_columns
1264 .iter()
1265 .position(|&c| c == col)
1266 {
1267 self.visible_ecr_image_columns.remove(pos);
1268 } else {
1269 self.visible_ecr_image_columns.push(col);
1270 }
1271 }
1272 } else {
1273 if let Some(&col) = self.all_ecr_columns.get(self.column_selector_index) {
1275 if let Some(pos) =
1276 self.visible_ecr_columns.iter().position(|&c| c == col)
1277 {
1278 self.visible_ecr_columns.remove(pos);
1279 } else {
1280 self.visible_ecr_columns.push(col);
1281 }
1282 }
1283 }
1284 } else if self.current_service == Service::LambdaFunctions {
1285 let idx = self.column_selector_index;
1286 if self.lambda_state.current_function.is_some()
1288 && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
1289 {
1290 if idx > 0 && idx <= self.lambda_state.all_version_columns.len() {
1292 if let Some(&col) = self.lambda_state.all_version_columns.get(idx - 1) {
1293 if let Some(pos) = self
1294 .lambda_state
1295 .visible_version_columns
1296 .iter()
1297 .position(|&c| c == col)
1298 {
1299 self.lambda_state.visible_version_columns.remove(pos);
1300 } else {
1301 self.lambda_state.visible_version_columns.push(col);
1302 }
1303 }
1304 } else if idx == self.lambda_state.all_version_columns.len() + 3 {
1305 self.lambda_state.version_table.page_size = PageSize::Ten;
1306 } else if idx == self.lambda_state.all_version_columns.len() + 4 {
1307 self.lambda_state.version_table.page_size = PageSize::TwentyFive;
1308 } else if idx == self.lambda_state.all_version_columns.len() + 5 {
1309 self.lambda_state.version_table.page_size = PageSize::Fifty;
1310 } else if idx == self.lambda_state.all_version_columns.len() + 6 {
1311 self.lambda_state.version_table.page_size = PageSize::OneHundred;
1312 }
1313 } else if (self.lambda_state.current_function.is_some()
1314 && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Aliases)
1315 || (self.lambda_state.current_version.is_some()
1316 && self.lambda_state.detail_tab
1317 == crate::app::LambdaDetailTab::Configuration)
1318 {
1319 if idx > 0 && idx <= self.lambda_state.all_alias_columns.len() {
1321 if let Some(&col) = self.lambda_state.all_alias_columns.get(idx - 1) {
1322 if let Some(pos) = self
1323 .lambda_state
1324 .visible_alias_columns
1325 .iter()
1326 .position(|&c| c == col)
1327 {
1328 self.lambda_state.visible_alias_columns.remove(pos);
1329 } else {
1330 self.lambda_state.visible_alias_columns.push(col);
1331 }
1332 }
1333 } else if idx == self.lambda_state.all_alias_columns.len() + 3 {
1334 self.lambda_state.alias_table.page_size = PageSize::Ten;
1335 } else if idx == self.lambda_state.all_alias_columns.len() + 4 {
1336 self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
1337 } else if idx == self.lambda_state.all_alias_columns.len() + 5 {
1338 self.lambda_state.alias_table.page_size = PageSize::Fifty;
1339 } else if idx == self.lambda_state.all_alias_columns.len() + 6 {
1340 self.lambda_state.alias_table.page_size = PageSize::OneHundred;
1341 }
1342 } else {
1343 if idx > 0 && idx <= self.lambda_state.all_columns.len() {
1345 if let Some(&col) = self.lambda_state.all_columns.get(idx - 1) {
1346 if let Some(pos) = self
1347 .lambda_state
1348 .visible_columns
1349 .iter()
1350 .position(|&c| c == col)
1351 {
1352 self.lambda_state.visible_columns.remove(pos);
1353 } else {
1354 self.lambda_state.visible_columns.push(col);
1355 }
1356 }
1357 } else if idx == self.lambda_state.all_columns.len() + 3 {
1358 self.lambda_state.table.page_size = PageSize::Ten;
1359 } else if idx == self.lambda_state.all_columns.len() + 4 {
1360 self.lambda_state.table.page_size = PageSize::TwentyFive;
1361 } else if idx == self.lambda_state.all_columns.len() + 5 {
1362 self.lambda_state.table.page_size = PageSize::Fifty;
1363 } else if idx == self.lambda_state.all_columns.len() + 6 {
1364 self.lambda_state.table.page_size = PageSize::OneHundred;
1365 }
1366 }
1367 } else if self.current_service == Service::LambdaApplications {
1368 if self.lambda_application_state.current_application.is_some() {
1369 if self.lambda_application_state.detail_tab
1371 == crate::ui::lambda::ApplicationDetailTab::Overview
1372 {
1373 let idx = self.column_selector_index;
1375 if idx > 0 && idx <= self.all_resource_columns.len() {
1376 if let Some(&col) = self.all_resource_columns.get(idx - 1) {
1377 if let Some(pos) =
1378 self.visible_resource_columns.iter().position(|&c| c == col)
1379 {
1380 self.visible_resource_columns.remove(pos);
1381 } else {
1382 self.visible_resource_columns.push(col);
1383 }
1384 }
1385 } else if idx == self.all_resource_columns.len() + 3 {
1386 self.lambda_application_state.resources.page_size = PageSize::Ten;
1387 } else if idx == self.all_resource_columns.len() + 4 {
1388 self.lambda_application_state.resources.page_size =
1389 PageSize::TwentyFive;
1390 } else if idx == self.all_resource_columns.len() + 5 {
1391 self.lambda_application_state.resources.page_size = PageSize::Fifty;
1392 }
1393 } else {
1394 let idx = self.column_selector_index;
1396 if idx > 0 && idx <= self.all_deployment_columns.len() {
1397 if let Some(&col) = self.all_deployment_columns.get(idx - 1) {
1398 if let Some(pos) = self
1399 .visible_deployment_columns
1400 .iter()
1401 .position(|&c| c == col)
1402 {
1403 self.visible_deployment_columns.remove(pos);
1404 } else {
1405 self.visible_deployment_columns.push(col);
1406 }
1407 }
1408 } else if idx == self.all_deployment_columns.len() + 3 {
1409 self.lambda_application_state.deployments.page_size = PageSize::Ten;
1410 } else if idx == self.all_deployment_columns.len() + 4 {
1411 self.lambda_application_state.deployments.page_size =
1412 PageSize::TwentyFive;
1413 } else if idx == self.all_deployment_columns.len() + 5 {
1414 self.lambda_application_state.deployments.page_size =
1415 PageSize::Fifty;
1416 }
1417 }
1418 } else {
1419 let idx = self.column_selector_index;
1421 if idx > 0 && idx <= self.all_lambda_application_columns.len() {
1422 if let Some(&col) = self.all_lambda_application_columns.get(idx - 1) {
1423 if let Some(pos) = self
1424 .visible_lambda_application_columns
1425 .iter()
1426 .position(|&c| c == col)
1427 {
1428 self.visible_lambda_application_columns.remove(pos);
1429 } else {
1430 self.visible_lambda_application_columns.push(col);
1431 }
1432 }
1433 } else if idx == self.all_lambda_application_columns.len() + 3 {
1434 self.lambda_application_state.table.page_size = PageSize::Ten;
1435 } else if idx == self.all_lambda_application_columns.len() + 4 {
1436 self.lambda_application_state.table.page_size = PageSize::TwentyFive;
1437 } else if idx == self.all_lambda_application_columns.len() + 5 {
1438 self.lambda_application_state.table.page_size = PageSize::Fifty;
1439 }
1440 }
1441 } else if self.view_mode == ViewMode::Events {
1442 if let Some(&col) = self.all_event_columns.get(self.column_selector_index) {
1443 if let Some(pos) = self.visible_event_columns.iter().position(|&c| c == col)
1444 {
1445 self.visible_event_columns.remove(pos);
1446 } else {
1447 self.visible_event_columns.push(col);
1448 }
1449 }
1450 } else if self.view_mode == ViewMode::Detail {
1451 if let Some(&col) = self.all_stream_columns.get(self.column_selector_index) {
1452 if let Some(pos) =
1453 self.visible_stream_columns.iter().position(|&c| c == col)
1454 {
1455 self.visible_stream_columns.remove(pos);
1456 } else {
1457 self.visible_stream_columns.push(col);
1458 }
1459 }
1460 } else if self.current_service == Service::CloudFormationStacks {
1461 let idx = self.column_selector_index;
1462 if idx > 0 && idx <= self.all_cfn_columns.len() {
1463 if let Some(&col) = self.all_cfn_columns.get(idx - 1) {
1464 if let Some(pos) =
1465 self.visible_cfn_columns.iter().position(|&c| c == col)
1466 {
1467 self.visible_cfn_columns.remove(pos);
1468 } else {
1469 self.visible_cfn_columns.push(col);
1470 }
1471 }
1472 } else if idx == self.all_cfn_columns.len() + 3 {
1473 self.cfn_state.table.page_size = PageSize::Ten;
1474 } else if idx == self.all_cfn_columns.len() + 4 {
1475 self.cfn_state.table.page_size = PageSize::TwentyFive;
1476 } else if idx == self.all_cfn_columns.len() + 5 {
1477 self.cfn_state.table.page_size = PageSize::Fifty;
1478 } else if idx == self.all_cfn_columns.len() + 6 {
1479 self.cfn_state.table.page_size = PageSize::OneHundred;
1480 }
1481 } else if self.current_service == Service::IamUsers {
1482 let idx = self.column_selector_index;
1483 if self.iam_state.current_user.is_some() {
1484 if idx > 0 && idx <= self.all_policy_columns.len() {
1486 if let Some(col) = self.all_policy_columns.get(idx - 1) {
1487 if let Some(pos) =
1488 self.visible_policy_columns.iter().position(|c| c == col)
1489 {
1490 self.visible_policy_columns.remove(pos);
1491 } else {
1492 self.visible_policy_columns.push(col.clone());
1493 }
1494 }
1495 } else if idx == self.all_policy_columns.len() + 3 {
1496 self.iam_state.policies.page_size = PageSize::Ten;
1497 } else if idx == self.all_policy_columns.len() + 4 {
1498 self.iam_state.policies.page_size = PageSize::TwentyFive;
1499 } else if idx == self.all_policy_columns.len() + 5 {
1500 self.iam_state.policies.page_size = PageSize::Fifty;
1501 }
1502 } else {
1503 if idx > 0 && idx <= self.all_iam_columns.len() {
1505 if let Some(col) = self.all_iam_columns.get(idx - 1) {
1506 if let Some(pos) =
1507 self.visible_iam_columns.iter().position(|c| c == col)
1508 {
1509 self.visible_iam_columns.remove(pos);
1510 } else {
1511 self.visible_iam_columns.push(col.clone());
1512 }
1513 }
1514 } else if idx == self.all_iam_columns.len() + 3 {
1515 self.iam_state.users.page_size = PageSize::Ten;
1516 } else if idx == self.all_iam_columns.len() + 4 {
1517 self.iam_state.users.page_size = PageSize::TwentyFive;
1518 } else if idx == self.all_iam_columns.len() + 5 {
1519 self.iam_state.users.page_size = PageSize::Fifty;
1520 }
1521 }
1522 } else if self.current_service == Service::IamRoles {
1523 let idx = self.column_selector_index;
1524 if self.iam_state.current_role.is_some() {
1525 if idx > 0 && idx <= self.all_policy_columns.len() {
1527 if let Some(col) = self.all_policy_columns.get(idx - 1) {
1528 if let Some(pos) =
1529 self.visible_policy_columns.iter().position(|c| c == col)
1530 {
1531 self.visible_policy_columns.remove(pos);
1532 } else {
1533 self.visible_policy_columns.push(col.clone());
1534 }
1535 }
1536 } else if idx == self.all_policy_columns.len() + 3 {
1537 self.iam_state.policies.page_size = PageSize::Ten;
1538 } else if idx == self.all_policy_columns.len() + 4 {
1539 self.iam_state.policies.page_size = PageSize::TwentyFive;
1540 } else if idx == self.all_policy_columns.len() + 5 {
1541 self.iam_state.policies.page_size = PageSize::Fifty;
1542 }
1543 } else {
1544 if idx > 0 && idx <= self.all_role_columns.len() {
1546 if let Some(col) = self.all_role_columns.get(idx - 1) {
1547 if let Some(pos) =
1548 self.visible_role_columns.iter().position(|c| c == col)
1549 {
1550 self.visible_role_columns.remove(pos);
1551 } else {
1552 self.visible_role_columns.push(col.clone());
1553 }
1554 }
1555 } else if idx == self.all_role_columns.len() + 3 {
1556 self.iam_state.roles.page_size = PageSize::Ten;
1557 } else if idx == self.all_role_columns.len() + 4 {
1558 self.iam_state.roles.page_size = PageSize::TwentyFive;
1559 } else if idx == self.all_role_columns.len() + 5 {
1560 self.iam_state.roles.page_size = PageSize::Fifty;
1561 }
1562 }
1563 } else if self.current_service == Service::IamUserGroups {
1564 let idx = self.column_selector_index;
1565 if idx > 0 && idx <= self.all_group_columns.len() {
1566 if let Some(col) = self.all_group_columns.get(idx - 1) {
1567 if let Some(pos) =
1568 self.visible_group_columns.iter().position(|c| c == col)
1569 {
1570 self.visible_group_columns.remove(pos);
1571 } else {
1572 self.visible_group_columns.push(col.clone());
1573 }
1574 }
1575 } else if idx == self.all_group_columns.len() + 3 {
1576 self.iam_state.groups.page_size = PageSize::Ten;
1577 } else if idx == self.all_group_columns.len() + 4 {
1578 self.iam_state.groups.page_size = PageSize::TwentyFive;
1579 } else if idx == self.all_group_columns.len() + 5 {
1580 self.iam_state.groups.page_size = PageSize::Fifty;
1581 }
1582 } else if let Some(&col) = self.all_columns.get(self.column_selector_index) {
1583 if let Some(pos) = self.visible_columns.iter().position(|&c| c == col) {
1584 self.visible_columns.remove(pos);
1585 } else {
1586 self.visible_columns.push(col);
1587 }
1588 }
1589 }
1590 Action::NextPreferences => {
1591 if self.current_service == Service::CloudWatchAlarms {
1592 if self.column_selector_index < 18 {
1594 self.column_selector_index = 18; } else if self.column_selector_index < 22 {
1596 self.column_selector_index = 22; } else if self.column_selector_index < 28 {
1598 self.column_selector_index = 28; } else {
1600 self.column_selector_index = 0; }
1602 } else if self.current_service == Service::EcrRepositories
1603 && self.ecr_state.current_repository.is_some()
1604 {
1605 let page_size_idx = self.all_ecr_image_columns.len() + 2;
1607 if self.column_selector_index < page_size_idx {
1608 self.column_selector_index = page_size_idx;
1609 } else {
1610 self.column_selector_index = 0;
1611 }
1612 } else if self.current_service == Service::LambdaFunctions {
1613 let page_size_idx = self.lambda_state.all_columns.len() + 2;
1615 if self.column_selector_index < page_size_idx {
1616 self.column_selector_index = page_size_idx;
1617 } else {
1618 self.column_selector_index = 0;
1619 }
1620 } else if self.current_service == Service::LambdaApplications {
1621 let page_size_idx = self.all_lambda_application_columns.len() + 2;
1623 if self.column_selector_index < page_size_idx {
1624 self.column_selector_index = page_size_idx;
1625 } else {
1626 self.column_selector_index = 0;
1627 }
1628 } else if self.current_service == Service::CloudFormationStacks {
1629 let page_size_idx = self.all_cfn_columns.len() + 2;
1631 if self.column_selector_index < page_size_idx {
1632 self.column_selector_index = page_size_idx;
1633 } else {
1634 self.column_selector_index = 0;
1635 }
1636 } else if self.current_service == Service::IamUsers {
1637 if self.iam_state.current_user.is_some() {
1638 if self.iam_state.user_tab == UserTab::Permissions {
1640 let page_size_idx = self.all_policy_columns.len() + 2;
1641 if self.column_selector_index < page_size_idx {
1642 self.column_selector_index = page_size_idx;
1643 } else {
1644 self.column_selector_index = 0;
1645 }
1646 }
1647 } else {
1649 let page_size_idx = self.all_iam_columns.len() + 2;
1651 if self.column_selector_index < page_size_idx {
1652 self.column_selector_index = page_size_idx;
1653 } else {
1654 self.column_selector_index = 0;
1655 }
1656 }
1657 } else if self.current_service == Service::IamRoles {
1658 if self.iam_state.current_role.is_some() {
1659 let page_size_idx = self.all_policy_columns.len() + 2;
1661 if self.column_selector_index < page_size_idx {
1662 self.column_selector_index = page_size_idx;
1663 } else {
1664 self.column_selector_index = 0;
1665 }
1666 } else {
1667 let page_size_idx = self.all_role_columns.len() + 2;
1669 if self.column_selector_index < page_size_idx {
1670 self.column_selector_index = page_size_idx;
1671 } else {
1672 self.column_selector_index = 0;
1673 }
1674 }
1675 } else if self.current_service == Service::IamUserGroups {
1676 let page_size_idx = self.all_group_columns.len() + 2;
1678 if self.column_selector_index < page_size_idx {
1679 self.column_selector_index = page_size_idx;
1680 } else {
1681 self.column_selector_index = 0;
1682 }
1683 } else if let Some(&col) = self.all_columns.get(self.column_selector_index) {
1684 if let Some(pos) = self.visible_columns.iter().position(|&c| c == col) {
1685 self.visible_columns.remove(pos);
1686 } else {
1687 self.visible_columns.push(col);
1688 }
1689 }
1690 }
1691 Action::CloseColumnSelector => {
1692 self.mode = Mode::Normal;
1693 self.preference_section = Preferences::Columns;
1694 }
1695 Action::NextDetailTab => {
1696 if self.current_service == Service::LambdaApplications
1697 && self.lambda_application_state.current_application.is_some()
1698 {
1699 self.lambda_application_state.detail_tab =
1700 self.lambda_application_state.detail_tab.next();
1701 } else if self.current_service == Service::IamRoles
1702 && self.iam_state.current_role.is_some()
1703 {
1704 self.iam_state.role_tab = self.iam_state.role_tab.next();
1705 if self.iam_state.role_tab == RoleTab::Tags {
1706 self.iam_state.tags.loading = true;
1707 }
1708 } else if self.current_service == Service::IamUsers
1709 && self.iam_state.current_user.is_some()
1710 {
1711 self.iam_state.user_tab = self.iam_state.user_tab.next();
1712 if self.iam_state.user_tab == UserTab::Tags {
1713 self.iam_state.user_tags.loading = true;
1714 }
1715 } else if self.current_service == Service::IamUserGroups
1716 && self.iam_state.current_group.is_some()
1717 {
1718 self.iam_state.group_tab = self.iam_state.group_tab.next();
1719 } else if self.view_mode == ViewMode::Detail {
1720 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
1721 } else if self.current_service == Service::S3Buckets {
1722 if self.s3_state.current_bucket.is_some() {
1723 self.s3_state.object_tab = self.s3_state.object_tab.next();
1724 } else {
1725 self.s3_state.bucket_type = match self.s3_state.bucket_type {
1726 S3BucketType::GeneralPurpose => S3BucketType::Directory,
1727 S3BucketType::Directory => S3BucketType::GeneralPurpose,
1728 };
1729 self.s3_state.buckets.reset();
1730 }
1731 } else if self.current_service == Service::CloudWatchAlarms {
1732 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
1733 AlarmTab::AllAlarms => AlarmTab::InAlarm,
1734 AlarmTab::InAlarm => AlarmTab::AllAlarms,
1735 };
1736 self.alarms_state.table.reset();
1737 } else if self.current_service == Service::EcrRepositories
1738 && self.ecr_state.current_repository.is_none()
1739 {
1740 self.ecr_state.tab = self.ecr_state.tab.next();
1741 self.ecr_state.repositories.reset();
1742 self.ecr_state.repositories.loading = true;
1743 } else if self.current_service == Service::LambdaFunctions
1744 && self.lambda_state.current_function.is_some()
1745 {
1746 if self.lambda_state.current_version.is_some() {
1747 self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
1749 LambdaDetailTab::Code => LambdaDetailTab::Configuration,
1750 _ => LambdaDetailTab::Code,
1751 };
1752 } else {
1753 self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
1754 }
1755 } else if self.current_service == Service::CloudFormationStacks
1756 && self.cfn_state.current_stack.is_some()
1757 {
1758 self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
1759 }
1760 }
1761 Action::PrevDetailTab => {
1762 if self.current_service == Service::LambdaApplications
1763 && self.lambda_application_state.current_application.is_some()
1764 {
1765 self.lambda_application_state.detail_tab =
1766 self.lambda_application_state.detail_tab.prev();
1767 } else if self.current_service == Service::IamRoles
1768 && self.iam_state.current_role.is_some()
1769 {
1770 self.iam_state.role_tab = self.iam_state.role_tab.prev();
1771 } else if self.current_service == Service::IamUsers
1772 && self.iam_state.current_user.is_some()
1773 {
1774 self.iam_state.user_tab = self.iam_state.user_tab.prev();
1775 } else if self.current_service == Service::IamUserGroups
1776 && self.iam_state.current_group.is_some()
1777 {
1778 self.iam_state.group_tab = self.iam_state.group_tab.prev();
1779 } else if self.view_mode == ViewMode::Detail {
1780 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
1781 } else if self.current_service == Service::S3Buckets {
1782 if self.s3_state.current_bucket.is_some() {
1783 self.s3_state.object_tab = self.s3_state.object_tab.prev();
1784 }
1785 } else if self.current_service == Service::CloudWatchAlarms {
1786 self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
1787 AlarmTab::AllAlarms => AlarmTab::InAlarm,
1788 AlarmTab::InAlarm => AlarmTab::AllAlarms,
1789 };
1790 } else if self.current_service == Service::EcrRepositories
1791 && self.ecr_state.current_repository.is_none()
1792 {
1793 self.ecr_state.tab = self.ecr_state.tab.prev();
1794 self.ecr_state.repositories.reset();
1795 self.ecr_state.repositories.loading = true;
1796 } else if self.current_service == Service::LambdaFunctions
1797 && self.lambda_state.current_function.is_some()
1798 {
1799 if self.lambda_state.current_version.is_some() {
1800 self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
1802 LambdaDetailTab::Configuration => LambdaDetailTab::Code,
1803 _ => LambdaDetailTab::Configuration,
1804 };
1805 } else {
1806 self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
1807 }
1808 } else if self.current_service == Service::CloudFormationStacks
1809 && self.cfn_state.current_stack.is_some()
1810 {
1811 self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
1812 }
1813 }
1814 Action::StartFilter => {
1815 if self.current_service == Service::CloudWatchInsights {
1816 self.mode = Mode::InsightsInput;
1817 } else if self.current_service == Service::CloudWatchAlarms {
1818 self.mode = Mode::FilterInput;
1819 } else if self.current_service == Service::S3Buckets {
1820 self.mode = Mode::FilterInput;
1821 self.log_groups_state.filter_mode = true;
1822 } else if self.current_service == Service::EcrRepositories
1823 || self.current_service == Service::IamUsers
1824 || self.current_service == Service::IamUserGroups
1825 {
1826 self.mode = Mode::FilterInput;
1827 if self.current_service == Service::EcrRepositories
1828 && self.ecr_state.current_repository.is_none()
1829 {
1830 self.ecr_state.input_focus = InputFocus::Filter;
1831 }
1832 } else if self.current_service == Service::LambdaFunctions {
1833 self.mode = Mode::FilterInput;
1834 if self.lambda_state.current_version.is_some()
1835 && self.lambda_state.detail_tab
1836 == crate::app::LambdaDetailTab::Configuration
1837 {
1838 self.lambda_state.alias_input_focus = InputFocus::Filter;
1839 } else if self.lambda_state.current_function.is_some()
1840 && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
1841 {
1842 self.lambda_state.version_input_focus = InputFocus::Filter;
1843 } else if self.lambda_state.current_function.is_none() {
1844 self.lambda_state.input_focus = InputFocus::Filter;
1845 }
1846 } else if self.current_service == Service::LambdaApplications {
1847 self.mode = Mode::FilterInput;
1848 use crate::ui::lambda::ApplicationDetailTab;
1849 if self.lambda_application_state.current_application.is_some() {
1850 if self.lambda_application_state.detail_tab
1852 == ApplicationDetailTab::Overview
1853 {
1854 self.lambda_application_state.resource_input_focus = InputFocus::Filter;
1855 } else {
1856 self.lambda_application_state.deployment_input_focus =
1857 InputFocus::Filter;
1858 }
1859 } else {
1860 self.lambda_application_state.input_focus = InputFocus::Filter;
1861 }
1862 } else if self.current_service == Service::IamRoles {
1863 self.mode = Mode::FilterInput;
1864 } else if self.current_service == Service::CloudFormationStacks {
1865 self.mode = Mode::FilterInput;
1866 self.cfn_state.input_focus = InputFocus::Filter;
1867 } else if self.view_mode == ViewMode::List
1868 || (self.view_mode == ViewMode::Detail
1869 && self.log_groups_state.detail_tab == DetailTab::LogStreams)
1870 {
1871 self.mode = Mode::FilterInput;
1872 self.log_groups_state.filter_mode = true;
1873 self.log_groups_state.input_focus = InputFocus::Filter;
1874 }
1875 }
1876 Action::StartEventFilter => {
1877 if self.current_service == Service::CloudWatchInsights {
1878 self.mode = Mode::InsightsInput;
1879 } else if self.view_mode == ViewMode::List {
1880 self.mode = Mode::FilterInput;
1881 self.log_groups_state.filter_mode = true;
1882 self.log_groups_state.input_focus = InputFocus::Filter;
1883 } else if self.view_mode == ViewMode::Events {
1884 self.mode = Mode::EventFilterInput;
1885 } else if self.view_mode == ViewMode::Detail
1886 && self.log_groups_state.detail_tab == DetailTab::LogStreams
1887 {
1888 self.mode = Mode::FilterInput;
1889 self.log_groups_state.filter_mode = true;
1890 self.log_groups_state.input_focus = InputFocus::Filter;
1891 }
1892 }
1893 Action::NextFilterFocus => {
1894 if self.mode == Mode::FilterInput
1895 && self.current_service == Service::LambdaApplications
1896 {
1897 use crate::ui::lambda::FILTER_CONTROLS;
1898 if self.lambda_application_state.current_application.is_some() {
1899 if self.lambda_application_state.detail_tab
1900 == crate::ui::lambda::ApplicationDetailTab::Deployments
1901 {
1902 self.lambda_application_state.deployment_input_focus = self
1903 .lambda_application_state
1904 .deployment_input_focus
1905 .next(&FILTER_CONTROLS);
1906 } else {
1907 self.lambda_application_state.resource_input_focus = self
1908 .lambda_application_state
1909 .resource_input_focus
1910 .next(&FILTER_CONTROLS);
1911 }
1912 } else {
1913 self.lambda_application_state.input_focus = self
1914 .lambda_application_state
1915 .input_focus
1916 .next(&FILTER_CONTROLS);
1917 }
1918 } else if self.mode == Mode::FilterInput
1919 && self.current_service == Service::IamRoles
1920 && self.iam_state.current_role.is_some()
1921 {
1922 use crate::ui::iam::POLICY_FILTER_CONTROLS;
1923 self.iam_state.policy_input_focus = self
1924 .iam_state
1925 .policy_input_focus
1926 .next(&POLICY_FILTER_CONTROLS);
1927 } else if self.mode == Mode::FilterInput
1928 && self.current_service == Service::IamRoles
1929 && self.iam_state.current_role.is_none()
1930 {
1931 use crate::ui::iam::ROLE_FILTER_CONTROLS;
1932 self.iam_state.role_input_focus =
1933 self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
1934 } else if self.mode == Mode::InsightsInput {
1935 use crate::app::InsightsFocus;
1936 self.insights_state.insights.insights_focus =
1937 match self.insights_state.insights.insights_focus {
1938 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
1939 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
1940 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
1941 InsightsFocus::Query => InsightsFocus::QueryLanguage,
1942 };
1943 } else if self.mode == Mode::FilterInput
1944 && self.current_service == Service::CloudFormationStacks
1945 {
1946 self.cfn_state.input_focus = self
1947 .cfn_state
1948 .input_focus
1949 .next(&crate::ui::cfn::State::FILTER_CONTROLS);
1950 } else if self.mode == Mode::FilterInput
1951 && self.current_service == Service::CloudWatchLogGroups
1952 {
1953 use crate::ui::cw::logs::FILTER_CONTROLS;
1954 self.log_groups_state.input_focus =
1955 self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
1956 } else if self.mode == Mode::EventFilterInput {
1957 self.log_groups_state.event_input_focus =
1958 self.log_groups_state.event_input_focus.next();
1959 } else if self.mode == Mode::FilterInput
1960 && self.current_service == Service::CloudWatchAlarms
1961 {
1962 use crate::ui::cw::alarms::FILTER_CONTROLS;
1963 self.alarms_state.input_focus =
1964 self.alarms_state.input_focus.next(&FILTER_CONTROLS);
1965 } else if self.mode == Mode::FilterInput
1966 && self.current_service == Service::EcrRepositories
1967 && self.ecr_state.current_repository.is_none()
1968 {
1969 use crate::ui::ecr::FILTER_CONTROLS;
1970 self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
1971 } else if self.mode == Mode::FilterInput
1972 && self.current_service == Service::LambdaFunctions
1973 {
1974 use crate::ui::lambda::FILTER_CONTROLS;
1975 if self.lambda_state.current_version.is_some()
1976 && self.lambda_state.detail_tab
1977 == crate::app::LambdaDetailTab::Configuration
1978 {
1979 self.lambda_state.alias_input_focus =
1980 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
1981 } else if self.lambda_state.current_function.is_some()
1982 && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
1983 {
1984 self.lambda_state.version_input_focus =
1985 self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
1986 } else if self.lambda_state.current_function.is_some()
1987 && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Aliases
1988 {
1989 self.lambda_state.alias_input_focus =
1990 self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
1991 } else if self.lambda_state.current_function.is_none() {
1992 self.lambda_state.input_focus =
1993 self.lambda_state.input_focus.next(&FILTER_CONTROLS);
1994 }
1995 }
1996 }
1997 Action::PrevFilterFocus => {
1998 if self.mode == Mode::FilterInput
1999 && self.current_service == Service::LambdaApplications
2000 {
2001 use crate::ui::lambda::FILTER_CONTROLS;
2002 if self.lambda_application_state.current_application.is_some() {
2003 if self.lambda_application_state.detail_tab
2004 == crate::ui::lambda::ApplicationDetailTab::Deployments
2005 {
2006 self.lambda_application_state.deployment_input_focus = self
2007 .lambda_application_state
2008 .deployment_input_focus
2009 .prev(&FILTER_CONTROLS);
2010 } else {
2011 self.lambda_application_state.resource_input_focus = self
2012 .lambda_application_state
2013 .resource_input_focus
2014 .prev(&FILTER_CONTROLS);
2015 }
2016 } else {
2017 self.lambda_application_state.input_focus = self
2018 .lambda_application_state
2019 .input_focus
2020 .prev(&FILTER_CONTROLS);
2021 }
2022 } else if self.mode == Mode::FilterInput
2023 && self.current_service == Service::CloudFormationStacks
2024 {
2025 self.cfn_state.input_focus = self
2026 .cfn_state
2027 .input_focus
2028 .prev(&crate::ui::cfn::State::FILTER_CONTROLS);
2029 } else if self.mode == Mode::FilterInput
2030 && self.current_service == Service::IamRoles
2031 && self.iam_state.current_role.is_none()
2032 {
2033 use crate::ui::iam::ROLE_FILTER_CONTROLS;
2034 self.iam_state.role_input_focus =
2035 self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
2036 } else if self.mode == Mode::FilterInput
2037 && self.current_service == Service::CloudWatchLogGroups
2038 {
2039 use crate::ui::cw::logs::FILTER_CONTROLS;
2040 self.log_groups_state.input_focus =
2041 self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
2042 } else if self.mode == Mode::EventFilterInput {
2043 self.log_groups_state.event_input_focus =
2044 self.log_groups_state.event_input_focus.prev();
2045 } else if self.mode == Mode::FilterInput
2046 && self.current_service == Service::IamRoles
2047 && self.iam_state.current_role.is_some()
2048 {
2049 use crate::ui::iam::POLICY_FILTER_CONTROLS;
2050 self.iam_state.policy_input_focus = self
2051 .iam_state
2052 .policy_input_focus
2053 .prev(&POLICY_FILTER_CONTROLS);
2054 } else if self.mode == Mode::FilterInput
2055 && self.current_service == Service::CloudWatchAlarms
2056 {
2057 use crate::ui::cw::alarms::FILTER_CONTROLS;
2058 self.alarms_state.input_focus =
2059 self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
2060 } else if self.mode == Mode::FilterInput
2061 && self.current_service == Service::EcrRepositories
2062 && self.ecr_state.current_repository.is_none()
2063 {
2064 use crate::ui::ecr::FILTER_CONTROLS;
2065 self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
2066 } else if self.mode == Mode::FilterInput
2067 && self.current_service == Service::LambdaFunctions
2068 {
2069 use crate::ui::lambda::FILTER_CONTROLS;
2070 if self.lambda_state.current_version.is_some()
2071 && self.lambda_state.detail_tab
2072 == crate::app::LambdaDetailTab::Configuration
2073 {
2074 self.lambda_state.alias_input_focus =
2075 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2076 } else if self.lambda_state.current_function.is_some()
2077 && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
2078 {
2079 self.lambda_state.version_input_focus =
2080 self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
2081 } else if self.lambda_state.current_function.is_some()
2082 && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Aliases
2083 {
2084 self.lambda_state.alias_input_focus =
2085 self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2086 } else if self.lambda_state.current_function.is_none() {
2087 self.lambda_state.input_focus =
2088 self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
2089 }
2090 }
2091 }
2092 Action::ToggleFilterCheckbox => {
2093 if self.mode == Mode::InsightsInput {
2094 use crate::app::InsightsFocus;
2095 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2096 && self.insights_state.insights.show_dropdown
2097 && !self.insights_state.insights.log_group_matches.is_empty()
2098 {
2099 let selected_idx = self.insights_state.insights.dropdown_selected;
2100 if let Some(group_name) = self
2101 .insights_state
2102 .insights
2103 .log_group_matches
2104 .get(selected_idx)
2105 {
2106 let group_name = group_name.clone();
2107 if let Some(pos) = self
2108 .insights_state
2109 .insights
2110 .selected_log_groups
2111 .iter()
2112 .position(|g| g == &group_name)
2113 {
2114 self.insights_state.insights.selected_log_groups.remove(pos);
2115 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2116 self.insights_state
2117 .insights
2118 .selected_log_groups
2119 .push(group_name);
2120 }
2121 }
2122 }
2123 } else if self.mode == Mode::FilterInput
2124 && self.current_service == Service::CloudFormationStacks
2125 {
2126 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
2127 match self.cfn_state.input_focus {
2128 STATUS_FILTER => {
2129 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
2130 }
2131 VIEW_NESTED => {
2132 self.cfn_state.view_nested = !self.cfn_state.view_nested;
2133 }
2134 _ => {}
2135 }
2136 } else if self.mode == Mode::FilterInput
2137 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2138 {
2139 match self.log_groups_state.input_focus {
2140 InputFocus::Checkbox("ExactMatch") => {
2141 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2142 }
2143 InputFocus::Checkbox("ShowExpired") => {
2144 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2145 }
2146 _ => {}
2147 }
2148 } else if self.mode == Mode::EventFilterInput
2149 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2150 {
2151 self.log_groups_state.relative_unit =
2152 self.log_groups_state.relative_unit.next();
2153 }
2154 }
2155 Action::CycleSortColumn => {
2156 if self.view_mode == ViewMode::Detail
2157 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2158 {
2159 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2160 StreamSort::Name => StreamSort::CreationTime,
2161 StreamSort::CreationTime => StreamSort::LastEventTime,
2162 StreamSort::LastEventTime => StreamSort::Name,
2163 };
2164 }
2165 }
2166 Action::ToggleSortDirection => {
2167 if self.view_mode == ViewMode::Detail
2168 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2169 {
2170 self.log_groups_state.stream_sort_desc =
2171 !self.log_groups_state.stream_sort_desc;
2172 }
2173 }
2174 Action::ScrollUp => {
2175 if self.view_mode == ViewMode::PolicyView {
2176 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
2177 } else if self.current_service == Service::IamRoles
2178 && self.iam_state.current_role.is_some()
2179 && self.iam_state.role_tab == RoleTab::TrustRelationships
2180 {
2181 self.iam_state.trust_policy_scroll =
2182 self.iam_state.trust_policy_scroll.saturating_sub(10);
2183 } else if self.view_mode == ViewMode::Events {
2184 if self.log_groups_state.event_scroll_offset == 0
2185 && self.log_groups_state.has_older_events
2186 {
2187 self.log_groups_state.loading = true;
2188 } else {
2189 self.log_groups_state.event_scroll_offset =
2190 self.log_groups_state.event_scroll_offset.saturating_sub(1);
2191 }
2192 } else if self.view_mode == ViewMode::InsightsResults {
2193 self.insights_state.insights.results_selected = self
2194 .insights_state
2195 .insights
2196 .results_selected
2197 .saturating_sub(1);
2198 } else if self.view_mode == ViewMode::Detail {
2199 self.log_groups_state.selected_stream =
2200 self.log_groups_state.selected_stream.saturating_sub(1);
2201 self.log_groups_state.expanded_stream = None;
2202 } else if self.view_mode == ViewMode::List
2203 && self.current_service == Service::CloudWatchLogGroups
2204 {
2205 self.log_groups_state.log_groups.selected =
2206 self.log_groups_state.log_groups.selected.saturating_sub(1);
2207 self.log_groups_state.log_groups.snap_to_page();
2208 } else if self.current_service == Service::EcrRepositories {
2209 if self.ecr_state.current_repository.is_some() {
2210 self.ecr_state.images.page_up();
2211 } else {
2212 self.ecr_state.repositories.page_up();
2213 }
2214 }
2215 }
2216 Action::ScrollDown => {
2217 if self.view_mode == ViewMode::PolicyView {
2218 let lines = self.iam_state.policy_document.lines().count();
2219 let max_scroll = lines.saturating_sub(1);
2220 self.iam_state.policy_scroll =
2221 (self.iam_state.policy_scroll + 10).min(max_scroll);
2222 } else if self.current_service == Service::IamRoles
2223 && self.iam_state.current_role.is_some()
2224 && self.iam_state.role_tab == RoleTab::TrustRelationships
2225 {
2226 let lines = self.iam_state.trust_policy_document.lines().count();
2227 let max_scroll = lines.saturating_sub(1);
2228 self.iam_state.trust_policy_scroll =
2229 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
2230 } else if self.view_mode == ViewMode::Events {
2231 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
2232 if self.log_groups_state.event_scroll_offset >= max_scroll {
2233 } else {
2235 self.log_groups_state.event_scroll_offset =
2236 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
2237 }
2238 } else if self.view_mode == ViewMode::InsightsResults {
2239 let max = self
2240 .insights_state
2241 .insights
2242 .query_results
2243 .len()
2244 .saturating_sub(1);
2245 self.insights_state.insights.results_selected =
2246 (self.insights_state.insights.results_selected + 1).min(max);
2247 } else if self.view_mode == ViewMode::Detail {
2248 let filtered_streams = self.filtered_log_streams();
2249 let max = filtered_streams.len().saturating_sub(1);
2250 self.log_groups_state.selected_stream =
2251 (self.log_groups_state.selected_stream + 1).min(max);
2252 } else if self.view_mode == ViewMode::List
2253 && self.current_service == Service::CloudWatchLogGroups
2254 {
2255 let filtered_groups = self.filtered_log_groups();
2256 self.log_groups_state
2257 .log_groups
2258 .next_item(filtered_groups.len());
2259 } else if self.current_service == Service::EcrRepositories {
2260 if self.ecr_state.current_repository.is_some() {
2261 let filtered_images = self.filtered_ecr_images();
2262 self.ecr_state.images.page_down(filtered_images.len());
2263 } else {
2264 let filtered_repos = self.filtered_ecr_repositories();
2265 self.ecr_state.repositories.page_down(filtered_repos.len());
2266 }
2267 }
2268 }
2269
2270 Action::Refresh => {
2271 if self.mode == Mode::ProfilePicker {
2272 self.log_groups_state.loading = true;
2273 self.log_groups_state.loading_message = "Refreshing...".to_string();
2274 } else if self.mode == Mode::RegionPicker {
2275 self.measure_region_latencies();
2276 } else if self.mode == Mode::SessionPicker {
2277 self.sessions = crate::session::Session::list_all().unwrap_or_default();
2278 } else if self.current_service == Service::CloudWatchInsights
2279 && !self.insights_state.insights.selected_log_groups.is_empty()
2280 {
2281 self.log_groups_state.loading = true;
2282 self.insights_state.insights.query_completed = true;
2283 } else if self.current_service == Service::LambdaFunctions {
2284 self.lambda_state.table.loading = true;
2285 } else if self.current_service == Service::LambdaApplications {
2286 self.lambda_application_state.table.loading = true;
2287 } else if matches!(
2288 self.view_mode,
2289 ViewMode::Events | ViewMode::Detail | ViewMode::List
2290 ) {
2291 self.log_groups_state.loading = true;
2292 }
2293 }
2294 Action::Yank => {
2295 if self.mode == Mode::ErrorModal {
2296 if let Some(error) = &self.error_message {
2298 copy_to_clipboard(error);
2299 }
2300 } else if self.view_mode == ViewMode::Events {
2301 if let Some(event) = self
2302 .log_groups_state
2303 .log_events
2304 .get(self.log_groups_state.event_scroll_offset)
2305 {
2306 copy_to_clipboard(&event.message);
2307 }
2308 } else if self.current_service == Service::EcrRepositories {
2309 if self.ecr_state.current_repository.is_some() {
2310 let filtered_images = self.filtered_ecr_images();
2311 if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
2312 copy_to_clipboard(&image.uri);
2313 }
2314 } else {
2315 let filtered_repos = self.filtered_ecr_repositories();
2316 if let Some(repo) =
2317 self.ecr_state.repositories.get_selected(&filtered_repos)
2318 {
2319 copy_to_clipboard(&repo.uri);
2320 }
2321 }
2322 } else if self.current_service == Service::LambdaFunctions {
2323 let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
2324 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
2325 copy_to_clipboard(&func.arn);
2326 }
2327 } else if self.current_service == Service::CloudFormationStacks {
2328 if let Some(stack_name) = &self.cfn_state.current_stack {
2329 if let Some(stack) = self
2331 .cfn_state
2332 .table
2333 .items
2334 .iter()
2335 .find(|s| &s.name == stack_name)
2336 {
2337 copy_to_clipboard(&stack.stack_id);
2338 }
2339 } else {
2340 let filtered_stacks = self.filtered_cloudformation_stacks();
2342 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
2343 copy_to_clipboard(&stack.stack_id);
2344 }
2345 }
2346 } else if self.current_service == Service::IamUsers {
2347 if self.iam_state.current_user.is_some() {
2348 if let Some(user_name) = &self.iam_state.current_user {
2349 if let Some(user) = self
2350 .iam_state
2351 .users
2352 .items
2353 .iter()
2354 .find(|u| u.user_name == *user_name)
2355 {
2356 copy_to_clipboard(&user.arn);
2357 }
2358 }
2359 } else {
2360 let filtered_users = crate::ui::iam::filtered_iam_users(self);
2361 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
2362 copy_to_clipboard(&user.arn);
2363 }
2364 }
2365 } else if self.current_service == Service::IamRoles {
2366 if self.iam_state.current_role.is_some() {
2367 if let Some(role_name) = &self.iam_state.current_role {
2368 if let Some(role) = self
2369 .iam_state
2370 .roles
2371 .items
2372 .iter()
2373 .find(|r| r.role_name == *role_name)
2374 {
2375 copy_to_clipboard(&role.arn);
2376 }
2377 }
2378 } else {
2379 let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
2380 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
2381 copy_to_clipboard(&role.arn);
2382 }
2383 }
2384 } else if self.current_service == Service::IamUserGroups {
2385 if self.iam_state.current_group.is_some() {
2386 if let Some(group_name) = &self.iam_state.current_group {
2387 let arn = iam::format_arn(&self.config.account_id, "group", group_name);
2388 copy_to_clipboard(&arn);
2389 }
2390 } else {
2391 let filtered_groups: Vec<_> = self
2392 .iam_state
2393 .groups
2394 .items
2395 .iter()
2396 .filter(|g| {
2397 if self.iam_state.groups.filter.is_empty() {
2398 true
2399 } else {
2400 g.group_name
2401 .to_lowercase()
2402 .contains(&self.iam_state.groups.filter.to_lowercase())
2403 }
2404 })
2405 .collect();
2406 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
2407 let arn = iam::format_arn(
2408 &self.config.account_id,
2409 "group",
2410 &group.group_name,
2411 );
2412 copy_to_clipboard(&arn);
2413 }
2414 }
2415 }
2416 }
2417 Action::CopyToClipboard => {
2418 self.snapshot_requested = true;
2420 }
2421 Action::RetryLoad => {
2422 self.error_message = None;
2423 self.mode = Mode::Normal;
2424 self.log_groups_state.loading = true;
2425 }
2426 Action::ApplyFilter => {
2427 if self.mode == Mode::InsightsInput {
2428 use crate::app::InsightsFocus;
2429 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2430 && self.insights_state.insights.show_dropdown
2431 {
2432 self.insights_state.insights.show_dropdown = false;
2434 self.mode = Mode::Normal;
2435 if !self.insights_state.insights.selected_log_groups.is_empty() {
2436 self.log_groups_state.loading = true;
2437 self.insights_state.insights.query_completed = true;
2438 }
2439 }
2440 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
2441 if let Ok(page) = self.page_input.parse::<usize>() {
2442 self.go_to_page(page);
2443 }
2444 self.page_input.clear();
2445 } else {
2446 self.mode = Mode::Normal;
2447 self.log_groups_state.filter_mode = false;
2448 }
2449 }
2450 Action::ToggleExactMatch => {
2451 if self.view_mode == ViewMode::Detail
2452 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2453 {
2454 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
2455 }
2456 }
2457 Action::ToggleShowExpired => {
2458 if self.view_mode == ViewMode::Detail
2459 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2460 {
2461 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
2462 }
2463 }
2464 Action::GoBack => {
2465 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
2467 self.mode = Mode::Normal;
2468 self.service_picker.filter.clear();
2469 }
2470 else if self.current_service == Service::S3Buckets
2472 && self.s3_state.current_bucket.is_some()
2473 {
2474 if !self.s3_state.prefix_stack.is_empty() {
2475 self.s3_state.prefix_stack.pop();
2476 self.s3_state.buckets.loading = true;
2477 } else {
2478 self.s3_state.current_bucket = None;
2479 self.s3_state.objects.clear();
2480 }
2481 }
2482 else if self.current_service == Service::EcrRepositories
2484 && self.ecr_state.current_repository.is_some()
2485 {
2486 if self.ecr_state.images.has_expanded_item() {
2487 self.ecr_state.images.collapse();
2488 } else {
2489 self.ecr_state.current_repository = None;
2490 self.ecr_state.current_repository_uri = None;
2491 self.ecr_state.images.items.clear();
2492 self.ecr_state.images.reset();
2493 }
2494 }
2495 else if self.current_service == Service::IamUsers
2497 && self.iam_state.current_user.is_some()
2498 {
2499 self.iam_state.current_user = None;
2500 self.iam_state.policies.items.clear();
2501 self.iam_state.policies.reset();
2502 self.update_current_tab_breadcrumb();
2503 }
2504 else if self.current_service == Service::IamUserGroups
2506 && self.iam_state.current_group.is_some()
2507 {
2508 self.iam_state.current_group = None;
2509 self.update_current_tab_breadcrumb();
2510 }
2511 else if self.current_service == Service::IamRoles {
2513 if self.view_mode == ViewMode::PolicyView {
2514 self.view_mode = ViewMode::Detail;
2516 self.iam_state.current_policy = None;
2517 self.iam_state.policy_document.clear();
2518 self.iam_state.policy_scroll = 0;
2519 self.update_current_tab_breadcrumb();
2520 } else if self.iam_state.current_role.is_some() {
2521 self.iam_state.current_role = None;
2522 self.iam_state.policies.items.clear();
2523 self.iam_state.policies.reset();
2524 self.update_current_tab_breadcrumb();
2525 }
2526 }
2527 else if self.current_service == Service::LambdaFunctions
2529 && self.lambda_state.current_version.is_some()
2530 {
2531 self.lambda_state.current_version = None;
2532 self.lambda_state.detail_tab = LambdaDetailTab::Versions;
2533 }
2534 else if self.current_service == Service::LambdaFunctions
2536 && self.lambda_state.current_alias.is_some()
2537 {
2538 self.lambda_state.current_alias = None;
2539 self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
2540 }
2541 else if self.current_service == Service::LambdaFunctions
2543 && self.lambda_state.current_function.is_some()
2544 {
2545 self.lambda_state.current_function = None;
2546 self.update_current_tab_breadcrumb();
2547 }
2548 else if self.current_service == Service::LambdaApplications
2550 && self.lambda_application_state.current_application.is_some()
2551 {
2552 self.lambda_application_state.current_application = None;
2553 self.update_current_tab_breadcrumb();
2554 }
2555 else if self.current_service == Service::CloudFormationStacks
2557 && self.cfn_state.current_stack.is_some()
2558 {
2559 self.cfn_state.current_stack = None;
2560 self.update_current_tab_breadcrumb();
2561 }
2562 else if self.view_mode == ViewMode::InsightsResults {
2564 if self.insights_state.insights.expanded_result.is_some() {
2565 self.insights_state.insights.expanded_result = None;
2566 }
2567 }
2568 else if self.current_service == Service::CloudWatchAlarms {
2570 if self.alarms_state.table.has_expanded_item() {
2571 self.alarms_state.table.collapse();
2572 }
2573 }
2574 else if self.view_mode == ViewMode::Events {
2576 if self.log_groups_state.expanded_event.is_some() {
2577 self.log_groups_state.expanded_event = None;
2578 } else {
2579 self.view_mode = ViewMode::Detail;
2580 self.log_groups_state.event_filter.clear();
2581 }
2582 }
2583 else if self.view_mode == ViewMode::Detail {
2585 self.view_mode = ViewMode::List;
2586 self.log_groups_state.stream_filter.clear();
2587 self.log_groups_state.exact_match = false;
2588 self.log_groups_state.show_expired = false;
2589 }
2590 }
2591 Action::OpenInConsole | Action::OpenInBrowser => {
2592 let url = self.get_console_url();
2593 let _ = webbrowser::open(&url);
2594 }
2595 Action::ShowHelp => {
2596 self.mode = Mode::HelpModal;
2597 }
2598 Action::OpenRegionPicker => {
2599 self.region_filter.clear();
2600 self.region_picker_selected = 0;
2601 self.measure_region_latencies();
2602 self.mode = Mode::RegionPicker;
2603 }
2604 Action::OpenProfilePicker => {
2605 self.profile_filter.clear();
2606 self.profile_picker_selected = 0;
2607 self.available_profiles = Self::load_aws_profiles();
2608 self.mode = Mode::ProfilePicker;
2609 }
2610 Action::OpenCalendar => {
2611 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
2612 self.calendar_selecting = CalendarField::StartDate;
2613 self.mode = Mode::CalendarPicker;
2614 }
2615 Action::CloseCalendar => {
2616 self.mode = Mode::Normal;
2617 self.calendar_date = None;
2618 }
2619 Action::CalendarPrevDay => {
2620 if let Some(date) = self.calendar_date {
2621 self.calendar_date = date.checked_sub(time::Duration::days(1));
2622 }
2623 }
2624 Action::CalendarNextDay => {
2625 if let Some(date) = self.calendar_date {
2626 self.calendar_date = date.checked_add(time::Duration::days(1));
2627 }
2628 }
2629 Action::CalendarPrevWeek => {
2630 if let Some(date) = self.calendar_date {
2631 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
2632 }
2633 }
2634 Action::CalendarNextWeek => {
2635 if let Some(date) = self.calendar_date {
2636 self.calendar_date = date.checked_add(time::Duration::weeks(1));
2637 }
2638 }
2639 Action::CalendarPrevMonth => {
2640 if let Some(date) = self.calendar_date {
2641 self.calendar_date = Some(if date.month() == time::Month::January {
2642 date.replace_month(time::Month::December)
2643 .unwrap()
2644 .replace_year(date.year() - 1)
2645 .unwrap()
2646 } else {
2647 date.replace_month(date.month().previous()).unwrap()
2648 });
2649 }
2650 }
2651 Action::CalendarNextMonth => {
2652 if let Some(date) = self.calendar_date {
2653 self.calendar_date = Some(if date.month() == time::Month::December {
2654 date.replace_month(time::Month::January)
2655 .unwrap()
2656 .replace_year(date.year() + 1)
2657 .unwrap()
2658 } else {
2659 date.replace_month(date.month().next()).unwrap()
2660 });
2661 }
2662 }
2663 Action::CalendarSelect => {
2664 if let Some(date) = self.calendar_date {
2665 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
2666 .unix_timestamp()
2667 * 1000;
2668 match self.calendar_selecting {
2669 CalendarField::StartDate => {
2670 self.log_groups_state.start_time = Some(timestamp);
2671 self.calendar_selecting = CalendarField::EndDate;
2672 }
2673 CalendarField::EndDate => {
2674 self.log_groups_state.end_time = Some(timestamp);
2675 self.mode = Mode::Normal;
2676 self.calendar_date = None;
2677 }
2678 }
2679 }
2680 }
2681 }
2682 }
2683
2684 pub fn filtered_services(&self) -> Vec<&'static str> {
2685 let mut services = if self.service_picker.filter.is_empty() {
2686 self.service_picker.services.clone()
2687 } else {
2688 self.service_picker
2689 .services
2690 .iter()
2691 .filter(|s| {
2692 s.to_lowercase()
2693 .contains(&self.service_picker.filter.to_lowercase())
2694 })
2695 .copied()
2696 .collect()
2697 };
2698 services.sort();
2699 services
2700 }
2701
2702 pub fn selected_log_group(&self) -> Option<&LogGroup> {
2703 crate::ui::cw::logs::selected_log_group(self)
2704 }
2705
2706 pub fn filtered_log_streams(&self) -> Vec<&LogStream> {
2707 crate::ui::cw::logs::filtered_log_streams(self)
2708 }
2709
2710 pub fn filtered_log_events(&self) -> Vec<&LogEvent> {
2711 crate::ui::cw::logs::filtered_log_events(self)
2712 }
2713
2714 pub fn filtered_log_groups(&self) -> Vec<&LogGroup> {
2715 crate::ui::cw::logs::filtered_log_groups(self)
2716 }
2717
2718 pub fn filtered_ecr_repositories(&self) -> Vec<&EcrRepository> {
2719 crate::ui::ecr::filtered_ecr_repositories(self)
2720 }
2721
2722 pub fn filtered_ecr_images(&self) -> Vec<&EcrImage> {
2723 crate::ui::ecr::filtered_ecr_images(self)
2724 }
2725
2726 pub fn filtered_cloudformation_stacks(&self) -> Vec<&CfnStack> {
2727 crate::ui::cfn::filtered_cloudformation_stacks(self)
2728 }
2729
2730 pub fn breadcrumbs(&self) -> String {
2731 if !self.service_selected {
2732 return String::new();
2733 }
2734
2735 let mut parts = vec![];
2736
2737 match self.current_service {
2738 Service::CloudWatchLogGroups => {
2739 parts.push("CloudWatch".to_string());
2740 parts.push("Log groups".to_string());
2741
2742 if self.view_mode != ViewMode::List {
2743 if let Some(group) = self.selected_log_group() {
2744 parts.push(group.name.clone());
2745 }
2746 }
2747
2748 if self.view_mode == ViewMode::Events {
2749 if let Some(stream) = self
2750 .log_groups_state
2751 .log_streams
2752 .get(self.log_groups_state.selected_stream)
2753 {
2754 parts.push(stream.name.clone());
2755 }
2756 }
2757 }
2758 Service::CloudWatchInsights => {
2759 parts.push("CloudWatch".to_string());
2760 parts.push("Insights".to_string());
2761 }
2762 Service::CloudWatchAlarms => {
2763 parts.push("CloudWatch".to_string());
2764 parts.push("Alarms".to_string());
2765 }
2766 Service::S3Buckets => {
2767 parts.push("S3".to_string());
2768 if let Some(bucket) = &self.s3_state.current_bucket {
2769 parts.push(bucket.clone());
2770 if let Some(prefix) = self.s3_state.prefix_stack.last() {
2771 parts.push(prefix.trim_end_matches('/').to_string());
2772 }
2773 } else {
2774 parts.push("Buckets".to_string());
2775 }
2776 }
2777 Service::EcrRepositories => {
2778 parts.push("ECR".to_string());
2779 if let Some(repo) = &self.ecr_state.current_repository {
2780 parts.push(repo.clone());
2781 } else {
2782 parts.push("Repositories".to_string());
2783 }
2784 }
2785 Service::LambdaFunctions => {
2786 parts.push("Lambda".to_string());
2787 if let Some(func) = &self.lambda_state.current_function {
2788 parts.push(func.clone());
2789 } else {
2790 parts.push("Functions".to_string());
2791 }
2792 }
2793 Service::LambdaApplications => {
2794 parts.push("Lambda".to_string());
2795 parts.push("Applications".to_string());
2796 }
2797 Service::CloudFormationStacks => {
2798 parts.push("CloudFormation".to_string());
2799 if let Some(stack_name) = &self.cfn_state.current_stack {
2800 parts.push(stack_name.clone());
2801 } else {
2802 parts.push("Stacks".to_string());
2803 }
2804 }
2805 Service::IamUsers => {
2806 parts.push("IAM".to_string());
2807 parts.push("Users".to_string());
2808 }
2809 Service::IamRoles => {
2810 parts.push("IAM".to_string());
2811 parts.push("Roles".to_string());
2812 if let Some(role_name) = &self.iam_state.current_role {
2813 parts.push(role_name.clone());
2814 if let Some(policy_name) = &self.iam_state.current_policy {
2815 parts.push(policy_name.clone());
2816 }
2817 }
2818 }
2819 Service::IamUserGroups => {
2820 parts.push("IAM".to_string());
2821 parts.push("User Groups".to_string());
2822 if let Some(group_name) = &self.iam_state.current_group {
2823 parts.push(group_name.clone());
2824 }
2825 }
2826 }
2827
2828 parts.join(" > ")
2829 }
2830
2831 pub fn update_current_tab_breadcrumb(&mut self) {
2832 if !self.tabs.is_empty() {
2833 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
2834 }
2835 }
2836
2837 pub fn get_console_url(&self) -> String {
2838 use crate::{cfn, cw, ecr, iam, lambda, s3};
2839
2840 match self.current_service {
2841 Service::CloudWatchLogGroups => {
2842 if self.view_mode == ViewMode::Events {
2843 if let Some(group) = self.selected_log_group() {
2844 if let Some(stream) = self
2845 .log_groups_state
2846 .log_streams
2847 .get(self.log_groups_state.selected_stream)
2848 {
2849 return cw::logs::console_url_stream(
2850 &self.config.region,
2851 &group.name,
2852 &stream.name,
2853 );
2854 }
2855 }
2856 } else if self.view_mode == ViewMode::Detail {
2857 if let Some(group) = self.selected_log_group() {
2858 return cw::logs::console_url_detail(&self.config.region, &group.name);
2859 }
2860 }
2861 cw::logs::console_url_list(&self.config.region)
2862 }
2863 Service::CloudWatchInsights => cw::insights::console_url(
2864 &self.config.region,
2865 &self.config.account_id,
2866 &self.insights_state.insights.query_text,
2867 &self.insights_state.insights.selected_log_groups,
2868 ),
2869 Service::CloudWatchAlarms => {
2870 let view_type = match self.alarms_state.view_as {
2871 AlarmViewMode::Table | AlarmViewMode::Detail => "table",
2872 AlarmViewMode::Cards => "card",
2873 };
2874 cw::alarms::console_url(
2875 &self.config.region,
2876 view_type,
2877 self.alarms_state.table.page_size.value(),
2878 &self.alarms_state.sort_column,
2879 self.alarms_state.sort_direction.as_str(),
2880 )
2881 }
2882 Service::S3Buckets => {
2883 if let Some(bucket_name) = &self.s3_state.current_bucket {
2884 let prefix = self.s3_state.prefix_stack.join("");
2885 s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
2886 } else {
2887 s3::console_url_buckets(&self.config.region)
2888 }
2889 }
2890 Service::EcrRepositories => {
2891 if let Some(repo_name) = &self.ecr_state.current_repository {
2892 ecr::console_url_private_repository(
2893 &self.config.region,
2894 &self.config.account_id,
2895 repo_name,
2896 )
2897 } else {
2898 ecr::console_url_repositories(&self.config.region)
2899 }
2900 }
2901 Service::LambdaFunctions => {
2902 if let Some(func_name) = &self.lambda_state.current_function {
2903 if let Some(version) = &self.lambda_state.current_version {
2904 lambda::console_url_function_version(
2905 &self.config.region,
2906 func_name,
2907 version,
2908 &self.lambda_state.detail_tab,
2909 )
2910 } else {
2911 lambda::console_url_function_detail(&self.config.region, func_name)
2912 }
2913 } else {
2914 lambda::console_url_functions(&self.config.region)
2915 }
2916 }
2917 Service::LambdaApplications => {
2918 if let Some(app_name) = &self.lambda_application_state.current_application {
2919 lambda::console_url_application_detail(
2920 &self.config.region,
2921 app_name,
2922 &self.lambda_application_state.detail_tab,
2923 )
2924 } else {
2925 lambda::console_url_applications(&self.config.region)
2926 }
2927 }
2928 Service::CloudFormationStacks => {
2929 if let Some(stack_name) = &self.cfn_state.current_stack {
2930 if let Some(stack) = self
2931 .cfn_state
2932 .table
2933 .items
2934 .iter()
2935 .find(|s| &s.name == stack_name)
2936 {
2937 return cfn::console_url_stack_detail_with_tab(
2938 &self.config.region,
2939 &stack.stack_id,
2940 &self.cfn_state.detail_tab,
2941 );
2942 }
2943 }
2944 cfn::console_url_stacks(&self.config.region)
2945 }
2946 Service::IamUsers => {
2947 if let Some(user_name) = &self.iam_state.current_user {
2948 let section = match self.iam_state.user_tab {
2949 UserTab::Permissions => "permissions",
2950 UserTab::Groups => "groups",
2951 UserTab::Tags => "tags",
2952 UserTab::SecurityCredentials => "security_credentials",
2953 UserTab::LastAccessed => "access_advisor",
2954 };
2955 iam::console_url_user_detail(&self.config.region, user_name, section)
2956 } else {
2957 iam::console_url_users(&self.config.region)
2958 }
2959 }
2960 Service::IamRoles => {
2961 if let Some(policy_name) = &self.iam_state.current_policy {
2962 if let Some(role_name) = &self.iam_state.current_role {
2963 return iam::console_url_role_policy(
2964 &self.config.region,
2965 role_name,
2966 policy_name,
2967 );
2968 }
2969 }
2970 if let Some(role_name) = &self.iam_state.current_role {
2971 let section = match self.iam_state.role_tab {
2972 RoleTab::Permissions => "permissions",
2973 RoleTab::TrustRelationships => "trust_relationships",
2974 RoleTab::Tags => "tags",
2975 RoleTab::LastAccessed => "access_advisor",
2976 RoleTab::RevokeSessions => "revoke_sessions",
2977 };
2978 iam::console_url_role_detail(&self.config.region, role_name, section)
2979 } else {
2980 iam::console_url_roles(&self.config.region)
2981 }
2982 }
2983 Service::IamUserGroups => iam::console_url_groups(&self.config.region),
2984 }
2985 }
2986
2987 fn calculate_total_bucket_rows(&self) -> usize {
2988 crate::ui::s3::calculate_total_bucket_rows(self)
2989 }
2990
2991 fn calculate_total_object_rows(&self) -> usize {
2992 crate::ui::s3::calculate_total_object_rows(self)
2993 }
2994
2995 fn next_item(&mut self) {
2996 match self.mode {
2997 Mode::FilterInput => {
2998 if self.current_service == Service::CloudFormationStacks {
2999 use crate::ui::cfn::STATUS_FILTER;
3000 if self.cfn_state.input_focus == STATUS_FILTER {
3001 self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3002 }
3003 }
3004 }
3005 Mode::RegionPicker => {
3006 let filtered = self.get_filtered_regions();
3007 if !filtered.is_empty() {
3008 self.region_picker_selected =
3009 (self.region_picker_selected + 1).min(filtered.len() - 1);
3010 }
3011 }
3012 Mode::ProfilePicker => {
3013 let filtered = self.get_filtered_profiles();
3014 if !filtered.is_empty() {
3015 self.profile_picker_selected =
3016 (self.profile_picker_selected + 1).min(filtered.len() - 1);
3017 }
3018 }
3019 Mode::SessionPicker => {
3020 let filtered = self.get_filtered_sessions();
3021 if !filtered.is_empty() {
3022 self.session_picker_selected =
3023 (self.session_picker_selected + 1).min(filtered.len() - 1);
3024 }
3025 }
3026 Mode::InsightsInput => {
3027 use crate::app::InsightsFocus;
3028 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3029 && self.insights_state.insights.show_dropdown
3030 && !self.insights_state.insights.log_group_matches.is_empty()
3031 {
3032 let max = self.insights_state.insights.log_group_matches.len() - 1;
3033 self.insights_state.insights.dropdown_selected =
3034 (self.insights_state.insights.dropdown_selected + 1).min(max);
3035 }
3036 }
3037 Mode::ColumnSelector => {
3038 let max = if self.current_service == Service::S3Buckets
3039 && self.s3_state.current_bucket.is_none()
3040 {
3041 self.all_bucket_columns.len() - 1
3042 } else if self.view_mode == ViewMode::Events {
3043 self.all_event_columns.len() - 1
3044 } else if self.view_mode == ViewMode::Detail {
3045 self.all_stream_columns.len() - 1
3046 } else if self.current_service == Service::CloudWatchAlarms {
3047 29
3049 } else if self.current_service == Service::EcrRepositories {
3050 if self.ecr_state.current_repository.is_some() {
3051 self.all_ecr_image_columns.len() + 6
3053 } else {
3054 self.all_ecr_columns.len() - 1
3056 }
3057 } else if self.current_service == Service::LambdaFunctions {
3058 self.lambda_state.all_columns.len() + 6
3060 } else if self.current_service == Service::LambdaApplications {
3061 self.all_lambda_application_columns.len() + 5
3063 } else if self.current_service == Service::CloudFormationStacks {
3064 self.all_cfn_columns.len() + 6
3066 } else if self.current_service == Service::IamUsers {
3067 if self.iam_state.current_user.is_some() {
3068 self.all_policy_columns.len() + 5
3070 } else {
3071 self.all_iam_columns.len() + 5
3073 }
3074 } else if self.current_service == Service::IamRoles {
3075 if self.iam_state.current_role.is_some() {
3076 self.all_policy_columns.len() + 5
3078 } else {
3079 self.all_role_columns.len() + 5
3081 }
3082 } else {
3083 self.all_columns.len() - 1
3084 };
3085 self.column_selector_index = (self.column_selector_index + 1).min(max);
3086 }
3087 Mode::ServicePicker => {
3088 let filtered = self.filtered_services();
3089 if !filtered.is_empty() {
3090 self.service_picker.selected =
3091 (self.service_picker.selected + 1).min(filtered.len() - 1);
3092 }
3093 }
3094 Mode::TabPicker => {
3095 let filtered = self.get_filtered_tabs();
3096 if !filtered.is_empty() {
3097 self.tab_picker_selected =
3098 (self.tab_picker_selected + 1).min(filtered.len() - 1);
3099 }
3100 }
3101 Mode::Normal => {
3102 if !self.service_selected {
3103 let filtered = self.filtered_services();
3104 if !filtered.is_empty() {
3105 self.service_picker.selected =
3106 (self.service_picker.selected + 1).min(filtered.len() - 1);
3107 }
3108 } else if self.current_service == Service::S3Buckets {
3109 if self.s3_state.current_bucket.is_some() {
3110 if self.s3_state.object_tab == S3ObjectTab::Properties {
3111 self.s3_state.properties_scroll =
3113 self.s3_state.properties_scroll.saturating_add(1);
3114 } else {
3115 let total_rows = self.calculate_total_object_rows();
3117 let max = total_rows.saturating_sub(1);
3118 self.s3_state.selected_object =
3119 (self.s3_state.selected_object + 1).min(max);
3120
3121 let visible_rows = self.s3_state.object_visible_rows.get();
3123 if self.s3_state.selected_object
3124 >= self.s3_state.object_scroll_offset + visible_rows
3125 {
3126 self.s3_state.object_scroll_offset =
3127 self.s3_state.selected_object - visible_rows + 1;
3128 }
3129 }
3130 } else {
3131 let total_rows = self.calculate_total_bucket_rows();
3133 if total_rows > 0 {
3134 self.s3_state.selected_row =
3135 (self.s3_state.selected_row + 1).min(total_rows - 1);
3136
3137 let visible_rows = self.s3_state.bucket_visible_rows.get();
3139 if self.s3_state.selected_row
3140 >= self.s3_state.bucket_scroll_offset + visible_rows
3141 {
3142 self.s3_state.bucket_scroll_offset =
3143 self.s3_state.selected_row - visible_rows + 1;
3144 }
3145 }
3146 }
3147 } else if self.view_mode == ViewMode::InsightsResults {
3148 let max = self
3149 .insights_state
3150 .insights
3151 .query_results
3152 .len()
3153 .saturating_sub(1);
3154 if self.insights_state.insights.results_selected < max {
3155 self.insights_state.insights.results_selected += 1;
3156 }
3157 } else if self.view_mode == ViewMode::PolicyView {
3158 let lines = self.iam_state.policy_document.lines().count();
3159 let max_scroll = lines.saturating_sub(1);
3160 self.iam_state.policy_scroll =
3161 (self.iam_state.policy_scroll + 1).min(max_scroll);
3162 } else if self.view_mode == ViewMode::Events {
3163 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3164 if self.log_groups_state.event_scroll_offset >= max_scroll {
3165 } else {
3167 self.log_groups_state.event_scroll_offset =
3168 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3169 }
3170 } else if self.current_service == Service::CloudWatchLogGroups {
3171 if self.view_mode == ViewMode::List {
3172 let filtered_groups = self.filtered_log_groups();
3173 self.log_groups_state
3174 .log_groups
3175 .next_item(filtered_groups.len());
3176 } else if self.view_mode == ViewMode::Detail {
3177 let filtered_streams = self.filtered_log_streams();
3178 if !filtered_streams.is_empty() {
3179 let max = filtered_streams.len() - 1;
3180 if self.log_groups_state.selected_stream >= max {
3181 } else {
3183 self.log_groups_state.selected_stream =
3184 (self.log_groups_state.selected_stream + 1).min(max);
3185 }
3186 }
3187 }
3188 } else if self.current_service == Service::CloudWatchAlarms {
3189 let filtered_alarms = match self.alarms_state.alarm_tab {
3190 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
3191 AlarmTab::InAlarm => self
3192 .alarms_state
3193 .table
3194 .items
3195 .iter()
3196 .filter(|a| a.state.to_uppercase() == "ALARM")
3197 .count(),
3198 };
3199 if filtered_alarms > 0 {
3200 self.alarms_state.table.next_item(filtered_alarms);
3201 }
3202 } else if self.current_service == Service::EcrRepositories {
3203 if self.ecr_state.current_repository.is_some() {
3204 let filtered_images = self.filtered_ecr_images();
3205 if !filtered_images.is_empty() {
3206 self.ecr_state.images.next_item(filtered_images.len());
3207 }
3208 } else {
3209 let filtered_repos = self.filtered_ecr_repositories();
3210 if !filtered_repos.is_empty() {
3211 self.ecr_state.repositories.selected =
3212 (self.ecr_state.repositories.selected + 1)
3213 .min(filtered_repos.len() - 1);
3214 self.ecr_state.repositories.snap_to_page();
3215 }
3216 }
3217 } else if self.current_service == Service::LambdaFunctions {
3218 if self.lambda_state.current_function.is_some()
3219 && self.lambda_state.detail_tab == LambdaDetailTab::Code
3220 {
3221 if let Some(func_name) = &self.lambda_state.current_function {
3223 if let Some(func) = self
3224 .lambda_state
3225 .table
3226 .items
3227 .iter()
3228 .find(|f| f.name == *func_name)
3229 {
3230 let max = func.layers.len().saturating_sub(1);
3231 if !func.layers.is_empty() {
3232 self.lambda_state.layer_selected =
3233 (self.lambda_state.layer_selected + 1).min(max);
3234 }
3235 }
3236 }
3237 } else if self.lambda_state.current_function.is_some()
3238 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3239 {
3240 let filtered: Vec<_> = self
3242 .lambda_state
3243 .version_table
3244 .items
3245 .iter()
3246 .filter(|v| {
3247 self.lambda_state.version_table.filter.is_empty()
3248 || v.version.to_lowercase().contains(
3249 &self.lambda_state.version_table.filter.to_lowercase(),
3250 )
3251 || v.aliases.to_lowercase().contains(
3252 &self.lambda_state.version_table.filter.to_lowercase(),
3253 )
3254 || v.description.to_lowercase().contains(
3255 &self.lambda_state.version_table.filter.to_lowercase(),
3256 )
3257 })
3258 .collect();
3259 if !filtered.is_empty() {
3260 self.lambda_state.version_table.selected =
3261 (self.lambda_state.version_table.selected + 1)
3262 .min(filtered.len() - 1);
3263 self.lambda_state.version_table.snap_to_page();
3264 }
3265 } else if self.lambda_state.current_function.is_some()
3266 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3267 || (self.lambda_state.current_version.is_some()
3268 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3269 {
3270 let version_filter = self.lambda_state.current_version.clone();
3272 let filtered: Vec<_> = self
3273 .lambda_state
3274 .alias_table
3275 .items
3276 .iter()
3277 .filter(|a| {
3278 (version_filter.is_none()
3279 || a.versions.contains(version_filter.as_ref().unwrap()))
3280 && (self.lambda_state.alias_table.filter.is_empty()
3281 || a.name.to_lowercase().contains(
3282 &self.lambda_state.alias_table.filter.to_lowercase(),
3283 )
3284 || a.versions.to_lowercase().contains(
3285 &self.lambda_state.alias_table.filter.to_lowercase(),
3286 )
3287 || a.description.to_lowercase().contains(
3288 &self.lambda_state.alias_table.filter.to_lowercase(),
3289 ))
3290 })
3291 .collect();
3292 if !filtered.is_empty() {
3293 self.lambda_state.alias_table.selected =
3294 (self.lambda_state.alias_table.selected + 1)
3295 .min(filtered.len() - 1);
3296 self.lambda_state.alias_table.snap_to_page();
3297 }
3298 } else if self.lambda_state.current_function.is_none() {
3299 let filtered = crate::ui::lambda::filtered_lambda_functions(self);
3300 if !filtered.is_empty() {
3301 self.lambda_state.table.next_item(filtered.len());
3302 self.lambda_state.table.snap_to_page();
3303 }
3304 }
3305 } else if self.current_service == Service::LambdaApplications {
3306 if self.lambda_application_state.current_application.is_some() {
3307 if self.lambda_application_state.detail_tab
3308 == crate::ui::lambda::ApplicationDetailTab::Overview
3309 {
3310 let len = self.lambda_application_state.resources.items.len();
3311 if len > 0 {
3312 self.lambda_application_state.resources.next_item(len);
3313 }
3314 } else {
3315 let len = self.lambda_application_state.deployments.items.len();
3316 if len > 0 {
3317 self.lambda_application_state.deployments.next_item(len);
3318 }
3319 }
3320 } else {
3321 let filtered = crate::ui::lambda::filtered_lambda_applications(self);
3322 if !filtered.is_empty() {
3323 self.lambda_application_state.table.selected =
3324 (self.lambda_application_state.table.selected + 1)
3325 .min(filtered.len() - 1);
3326 self.lambda_application_state.table.snap_to_page();
3327 }
3328 }
3329 } else if self.current_service == Service::CloudFormationStacks {
3330 let filtered = self.filtered_cloudformation_stacks();
3331 self.cfn_state.table.next_item(filtered.len());
3332 } else if self.current_service == Service::IamUsers {
3333 if self.iam_state.current_user.is_some() {
3334 if self.iam_state.user_tab == UserTab::Tags {
3335 let filtered = crate::ui::iam::filtered_user_tags(self);
3336 if !filtered.is_empty() {
3337 self.iam_state.user_tags.next_item(filtered.len());
3338 }
3339 } else {
3340 let filtered = crate::ui::iam::filtered_iam_policies(self);
3341 if !filtered.is_empty() {
3342 self.iam_state.policies.next_item(filtered.len());
3343 }
3344 }
3345 } else {
3346 let filtered = crate::ui::iam::filtered_iam_users(self);
3347 if !filtered.is_empty() {
3348 self.iam_state.users.next_item(filtered.len());
3349 }
3350 }
3351 } else if self.current_service == Service::IamRoles {
3352 if self.iam_state.current_role.is_some() {
3353 if self.iam_state.role_tab == RoleTab::TrustRelationships {
3354 let lines = self.iam_state.trust_policy_document.lines().count();
3355 let max_scroll = lines.saturating_sub(1);
3356 self.iam_state.trust_policy_scroll =
3357 (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
3358 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
3359 self.iam_state.revoke_sessions_scroll =
3360 (self.iam_state.revoke_sessions_scroll + 1).min(19);
3361 } else if self.iam_state.role_tab == RoleTab::Tags {
3362 let filtered = crate::ui::iam::filtered_tags(self);
3363 if !filtered.is_empty() {
3364 self.iam_state.tags.next_item(filtered.len());
3365 }
3366 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
3367 let filtered = crate::ui::iam::filtered_last_accessed(self);
3368 if !filtered.is_empty() {
3369 self.iam_state
3370 .last_accessed_services
3371 .next_item(filtered.len());
3372 }
3373 } else {
3374 let filtered = crate::ui::iam::filtered_iam_policies(self);
3375 if !filtered.is_empty() {
3376 self.iam_state.policies.next_item(filtered.len());
3377 }
3378 }
3379 } else {
3380 let filtered = crate::ui::iam::filtered_iam_roles(self);
3381 if !filtered.is_empty() {
3382 self.iam_state.roles.next_item(filtered.len());
3383 }
3384 }
3385 } else if self.current_service == Service::IamUserGroups {
3386 if self.iam_state.current_group.is_some() {
3387 if self.iam_state.group_tab == GroupTab::Users {
3388 let filtered: Vec<_> = self
3389 .iam_state
3390 .group_users
3391 .items
3392 .iter()
3393 .filter(|u| {
3394 if self.iam_state.group_users.filter.is_empty() {
3395 true
3396 } else {
3397 u.user_name.to_lowercase().contains(
3398 &self.iam_state.group_users.filter.to_lowercase(),
3399 )
3400 }
3401 })
3402 .collect();
3403 if !filtered.is_empty() {
3404 self.iam_state.group_users.next_item(filtered.len());
3405 }
3406 } else if self.iam_state.group_tab == GroupTab::Permissions {
3407 let filtered = crate::ui::iam::filtered_iam_policies(self);
3408 if !filtered.is_empty() {
3409 self.iam_state.policies.next_item(filtered.len());
3410 }
3411 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3412 let filtered = crate::ui::iam::filtered_last_accessed(self);
3413 if !filtered.is_empty() {
3414 self.iam_state
3415 .last_accessed_services
3416 .next_item(filtered.len());
3417 }
3418 }
3419 } else {
3420 let filtered: Vec<_> = self
3421 .iam_state
3422 .groups
3423 .items
3424 .iter()
3425 .filter(|g| {
3426 if self.iam_state.groups.filter.is_empty() {
3427 true
3428 } else {
3429 g.group_name
3430 .to_lowercase()
3431 .contains(&self.iam_state.groups.filter.to_lowercase())
3432 }
3433 })
3434 .collect();
3435 if !filtered.is_empty() {
3436 self.iam_state.groups.next_item(filtered.len());
3437 }
3438 }
3439 }
3440 }
3441 _ => {}
3442 }
3443 }
3444
3445 fn prev_item(&mut self) {
3446 match self.mode {
3447 Mode::FilterInput => {
3448 if self.current_service == Service::CloudFormationStacks {
3449 use crate::ui::cfn::STATUS_FILTER;
3450 if self.cfn_state.input_focus == STATUS_FILTER {
3451 self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
3452 }
3453 }
3454 }
3455 Mode::RegionPicker => {
3456 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
3457 }
3458 Mode::ProfilePicker => {
3459 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
3460 }
3461 Mode::SessionPicker => {
3462 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
3463 }
3464 Mode::InsightsInput => {
3465 use crate::app::InsightsFocus;
3466 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3467 && self.insights_state.insights.show_dropdown
3468 && !self.insights_state.insights.log_group_matches.is_empty()
3469 {
3470 self.insights_state.insights.dropdown_selected = self
3471 .insights_state
3472 .insights
3473 .dropdown_selected
3474 .saturating_sub(1);
3475 }
3476 }
3477 Mode::ColumnSelector => {
3478 self.column_selector_index = self.column_selector_index.saturating_sub(1);
3479 }
3480 Mode::ServicePicker => {
3481 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3482 }
3483 Mode::TabPicker => {
3484 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
3485 }
3486 Mode::Normal => {
3487 if !self.service_selected {
3488 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3489 } else if self.current_service == Service::S3Buckets {
3490 if self.s3_state.current_bucket.is_some() {
3491 if self.s3_state.object_tab == S3ObjectTab::Properties {
3492 self.s3_state.properties_scroll =
3493 self.s3_state.properties_scroll.saturating_sub(1);
3494 } else {
3495 self.s3_state.selected_object =
3496 self.s3_state.selected_object.saturating_sub(1);
3497
3498 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
3500 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
3501 }
3502 }
3503 } else {
3504 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
3505
3506 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
3508 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
3509 }
3510 }
3511 } else if self.view_mode == ViewMode::InsightsResults {
3512 if self.insights_state.insights.results_selected > 0 {
3513 self.insights_state.insights.results_selected -= 1;
3514 }
3515 } else if self.view_mode == ViewMode::PolicyView {
3516 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
3517 } else if self.view_mode == ViewMode::Events {
3518 if self.log_groups_state.event_scroll_offset == 0 {
3519 if self.log_groups_state.has_older_events {
3520 self.log_groups_state.loading = true;
3521 }
3522 } else {
3524 self.log_groups_state.event_scroll_offset =
3525 self.log_groups_state.event_scroll_offset.saturating_sub(1);
3526 }
3527 } else if self.current_service == Service::CloudWatchLogGroups {
3528 if self.view_mode == ViewMode::List {
3529 self.log_groups_state.log_groups.prev_item();
3530 } else if self.view_mode == ViewMode::Detail
3531 && self.log_groups_state.selected_stream > 0
3532 {
3533 self.log_groups_state.selected_stream =
3534 self.log_groups_state.selected_stream.saturating_sub(1);
3535 self.log_groups_state.expanded_stream = None;
3536 }
3537 } else if self.current_service == Service::CloudWatchAlarms {
3538 self.alarms_state.table.prev_item();
3539 } else if self.current_service == Service::EcrRepositories {
3540 if self.ecr_state.current_repository.is_some() {
3541 self.ecr_state.images.prev_item();
3542 } else {
3543 self.ecr_state.repositories.prev_item();
3544 }
3545 } else if self.current_service == Service::LambdaFunctions {
3546 if self.lambda_state.current_function.is_some()
3547 && self.lambda_state.detail_tab == LambdaDetailTab::Code
3548 {
3549 self.lambda_state.layer_selected =
3551 self.lambda_state.layer_selected.saturating_sub(1);
3552 } else if self.lambda_state.current_function.is_some()
3553 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3554 {
3555 self.lambda_state.version_table.prev_item();
3556 } else if self.lambda_state.current_function.is_some()
3557 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3558 || (self.lambda_state.current_version.is_some()
3559 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3560 {
3561 self.lambda_state.alias_table.prev_item();
3562 } else if self.lambda_state.current_function.is_none() {
3563 self.lambda_state.table.prev_item();
3564 }
3565 } else if self.current_service == Service::LambdaApplications {
3566 if self.lambda_application_state.current_application.is_some()
3567 && self.lambda_application_state.detail_tab
3568 == crate::ui::lambda::ApplicationDetailTab::Overview
3569 {
3570 self.lambda_application_state.resources.selected = self
3571 .lambda_application_state
3572 .resources
3573 .selected
3574 .saturating_sub(1);
3575 } else if self.lambda_application_state.current_application.is_some()
3576 && self.lambda_application_state.detail_tab
3577 == crate::ui::lambda::ApplicationDetailTab::Deployments
3578 {
3579 self.lambda_application_state.deployments.selected = self
3580 .lambda_application_state
3581 .deployments
3582 .selected
3583 .saturating_sub(1);
3584 } else {
3585 self.lambda_application_state.table.selected = self
3586 .lambda_application_state
3587 .table
3588 .selected
3589 .saturating_sub(1);
3590 self.lambda_application_state.table.snap_to_page();
3591 }
3592 } else if self.current_service == Service::CloudFormationStacks {
3593 self.cfn_state.table.prev_item();
3594 } else if self.current_service == Service::IamUsers {
3595 self.iam_state.users.prev_item();
3596 } else if self.current_service == Service::IamRoles {
3597 if self.iam_state.current_role.is_some() {
3598 if self.iam_state.role_tab == RoleTab::TrustRelationships {
3599 self.iam_state.trust_policy_scroll =
3600 self.iam_state.trust_policy_scroll.saturating_sub(1);
3601 } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
3602 self.iam_state.revoke_sessions_scroll =
3603 self.iam_state.revoke_sessions_scroll.saturating_sub(1);
3604 } else if self.iam_state.role_tab == RoleTab::Tags {
3605 self.iam_state.tags.prev_item();
3606 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
3607 self.iam_state.last_accessed_services.prev_item();
3608 } else {
3609 self.iam_state.policies.prev_item();
3610 }
3611 } else {
3612 self.iam_state.roles.prev_item();
3613 }
3614 } else if self.current_service == Service::IamUserGroups {
3615 if self.iam_state.current_group.is_some() {
3616 if self.iam_state.group_tab == GroupTab::Users {
3617 self.iam_state.group_users.prev_item();
3618 } else if self.iam_state.group_tab == GroupTab::Permissions {
3619 self.iam_state.policies.prev_item();
3620 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3621 self.iam_state.last_accessed_services.prev_item();
3622 }
3623 } else {
3624 self.iam_state.groups.prev_item();
3625 }
3626 }
3627 }
3628 _ => {}
3629 }
3630 }
3631
3632 fn page_down(&mut self) {
3633 if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
3634 use crate::ui::cfn::filtered_cloudformation_stacks;
3635 let page_size = self.cfn_state.table.page_size.value();
3636 let filtered_count = filtered_cloudformation_stacks(self).len();
3637 self.cfn_state.input_focus.handle_page_down(
3638 &mut self.cfn_state.table.selected,
3639 &mut self.cfn_state.table.scroll_offset,
3640 page_size,
3641 filtered_count,
3642 );
3643 } else if self.mode == Mode::FilterInput
3644 && self.current_service == Service::IamRoles
3645 && self.iam_state.current_role.is_none()
3646 {
3647 let page_size = self.iam_state.roles.page_size.value();
3648 let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
3649 self.iam_state.role_input_focus.handle_page_down(
3650 &mut self.iam_state.roles.selected,
3651 &mut self.iam_state.roles.scroll_offset,
3652 page_size,
3653 filtered_count,
3654 );
3655 } else if self.mode == Mode::FilterInput
3656 && self.current_service == Service::CloudWatchAlarms
3657 {
3658 let page_size = self.alarms_state.table.page_size.value();
3659 let filtered_count = self.alarms_state.table.items.len();
3660 self.alarms_state.input_focus.handle_page_down(
3661 &mut self.alarms_state.table.selected,
3662 &mut self.alarms_state.table.scroll_offset,
3663 page_size,
3664 filtered_count,
3665 );
3666 } else if self.mode == Mode::FilterInput
3667 && self.current_service == Service::CloudWatchLogGroups
3668 {
3669 if self.view_mode == ViewMode::List {
3670 let filtered = self.filtered_log_groups();
3672 let page_size = self.log_groups_state.log_groups.page_size.value();
3673 let filtered_count = filtered.len();
3674 self.log_groups_state.input_focus.handle_page_down(
3675 &mut self.log_groups_state.log_groups.selected,
3676 &mut self.log_groups_state.log_groups.scroll_offset,
3677 page_size,
3678 filtered_count,
3679 );
3680 } else {
3681 let filtered = self.filtered_log_streams();
3683 let page_size = 20;
3684 let filtered_count = filtered.len();
3685 self.log_groups_state.input_focus.handle_page_down(
3686 &mut self.log_groups_state.selected_stream,
3687 &mut self.log_groups_state.stream_page,
3688 page_size,
3689 filtered_count,
3690 );
3691 self.log_groups_state.expanded_stream = None;
3692 }
3693 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3694 {
3695 if self.lambda_state.current_function.is_some()
3696 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3697 && self.lambda_state.version_input_focus == InputFocus::Pagination
3698 {
3699 let page_size = self.lambda_state.version_table.page_size.value();
3700 let filtered_count: usize = self
3701 .lambda_state
3702 .version_table
3703 .items
3704 .iter()
3705 .filter(|v| {
3706 self.lambda_state.version_table.filter.is_empty()
3707 || v.version
3708 .to_lowercase()
3709 .contains(&self.lambda_state.version_table.filter.to_lowercase())
3710 || v.aliases
3711 .to_lowercase()
3712 .contains(&self.lambda_state.version_table.filter.to_lowercase())
3713 || v.description
3714 .to_lowercase()
3715 .contains(&self.lambda_state.version_table.filter.to_lowercase())
3716 })
3717 .count();
3718 let target = self.lambda_state.version_table.selected + page_size;
3719 self.lambda_state.version_table.selected =
3720 target.min(filtered_count.saturating_sub(1));
3721 } else if self.lambda_state.current_function.is_some()
3722 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3723 || (self.lambda_state.current_version.is_some()
3724 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3725 && self.lambda_state.alias_input_focus == InputFocus::Pagination
3726 {
3727 let page_size = self.lambda_state.alias_table.page_size.value();
3728 let version_filter = self.lambda_state.current_version.clone();
3729 let filtered_count = self
3730 .lambda_state
3731 .alias_table
3732 .items
3733 .iter()
3734 .filter(|a| {
3735 (version_filter.is_none()
3736 || a.versions.contains(version_filter.as_ref().unwrap()))
3737 && (self.lambda_state.alias_table.filter.is_empty()
3738 || a.name
3739 .to_lowercase()
3740 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
3741 || a.versions
3742 .to_lowercase()
3743 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
3744 || a.description
3745 .to_lowercase()
3746 .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
3747 })
3748 .count();
3749 let target = self.lambda_state.alias_table.selected + page_size;
3750 self.lambda_state.alias_table.selected =
3751 target.min(filtered_count.saturating_sub(1));
3752 } else if self.lambda_state.current_function.is_none() {
3753 let page_size = self.lambda_state.table.page_size.value();
3754 let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
3755 self.lambda_state.input_focus.handle_page_down(
3756 &mut self.lambda_state.table.selected,
3757 &mut self.lambda_state.table.scroll_offset,
3758 page_size,
3759 filtered_count,
3760 );
3761 }
3762 } else if self.mode == Mode::FilterInput
3763 && self.current_service == Service::EcrRepositories
3764 && self.ecr_state.current_repository.is_none()
3765 && self.ecr_state.input_focus == InputFocus::Filter
3766 {
3767 let filtered = self.filtered_ecr_repositories();
3769 self.ecr_state.repositories.page_down(filtered.len());
3770 } else if self.mode == Mode::FilterInput
3771 && self.current_service == Service::EcrRepositories
3772 && self.ecr_state.current_repository.is_none()
3773 {
3774 let page_size = self.ecr_state.repositories.page_size.value();
3775 let filtered_count = self.filtered_ecr_repositories().len();
3776 self.ecr_state.input_focus.handle_page_down(
3777 &mut self.ecr_state.repositories.selected,
3778 &mut self.ecr_state.repositories.scroll_offset,
3779 page_size,
3780 filtered_count,
3781 );
3782 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3783 let page_size = self.iam_state.policies.page_size.value();
3784 let filtered_count = crate::ui::iam::filtered_iam_policies(self).len();
3785 self.iam_state.policy_input_focus.handle_page_down(
3786 &mut self.iam_state.policies.selected,
3787 &mut self.iam_state.policies.scroll_offset,
3788 page_size,
3789 filtered_count,
3790 );
3791 } else if self.view_mode == ViewMode::PolicyView {
3792 let lines = self.iam_state.policy_document.lines().count();
3793 let max_scroll = lines.saturating_sub(1);
3794 self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
3795 } else if self.current_service == Service::IamRoles
3796 && self.iam_state.current_role.is_some()
3797 && self.iam_state.role_tab == RoleTab::TrustRelationships
3798 {
3799 let lines = self.iam_state.trust_policy_document.lines().count();
3800 let max_scroll = lines.saturating_sub(1);
3801 self.iam_state.trust_policy_scroll =
3802 (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
3803 } else if self.current_service == Service::IamRoles
3804 && self.iam_state.current_role.is_some()
3805 && self.iam_state.role_tab == RoleTab::RevokeSessions
3806 {
3807 self.iam_state.revoke_sessions_scroll =
3808 (self.iam_state.revoke_sessions_scroll + 10).min(19);
3809 } else if self.mode == Mode::Normal {
3810 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
3811 {
3812 let total_rows = self.calculate_total_bucket_rows();
3813 self.s3_state.selected_row = self
3814 .s3_state
3815 .selected_row
3816 .saturating_add(10)
3817 .min(total_rows.saturating_sub(1));
3818
3819 let visible_rows = self.s3_state.bucket_visible_rows.get();
3821 if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
3822 self.s3_state.bucket_scroll_offset =
3823 self.s3_state.selected_row - visible_rows + 1;
3824 }
3825 } else if self.current_service == Service::S3Buckets
3826 && self.s3_state.current_bucket.is_some()
3827 {
3828 let total_rows = self.calculate_total_object_rows();
3829 self.s3_state.selected_object = self
3830 .s3_state
3831 .selected_object
3832 .saturating_add(10)
3833 .min(total_rows.saturating_sub(1));
3834
3835 let visible_rows = self.s3_state.object_visible_rows.get();
3837 if self.s3_state.selected_object
3838 >= self.s3_state.object_scroll_offset + visible_rows
3839 {
3840 self.s3_state.object_scroll_offset =
3841 self.s3_state.selected_object - visible_rows + 1;
3842 }
3843 } else if self.current_service == Service::CloudWatchLogGroups
3844 && self.view_mode == ViewMode::List
3845 {
3846 let filtered = self.filtered_log_groups();
3847 self.log_groups_state.log_groups.page_down(filtered.len());
3848 } else if self.current_service == Service::CloudWatchLogGroups
3849 && self.view_mode == ViewMode::Detail
3850 {
3851 let len = self.filtered_log_streams().len();
3852 nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
3853 } else if self.view_mode == ViewMode::Events {
3854 let max = self.log_groups_state.log_events.len();
3855 nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
3856 } else if self.view_mode == ViewMode::InsightsResults {
3857 let max = self.insights_state.insights.query_results.len();
3858 nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
3859 } else if self.current_service == Service::CloudWatchAlarms {
3860 let filtered = match self.alarms_state.alarm_tab {
3861 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
3862 AlarmTab::InAlarm => self
3863 .alarms_state
3864 .table
3865 .items
3866 .iter()
3867 .filter(|a| a.state.to_uppercase() == "ALARM")
3868 .count(),
3869 };
3870 if filtered > 0 {
3871 self.alarms_state.table.page_down(filtered);
3872 }
3873 } else if self.current_service == Service::EcrRepositories {
3874 if self.ecr_state.current_repository.is_some() {
3875 let filtered = self.filtered_ecr_images();
3876 self.ecr_state.images.page_down(filtered.len());
3877 } else {
3878 let filtered = self.filtered_ecr_repositories();
3879 self.ecr_state.repositories.page_down(filtered.len());
3880 }
3881 } else if self.current_service == Service::LambdaFunctions {
3882 let len = crate::ui::lambda::filtered_lambda_functions(self).len();
3883 self.lambda_state.table.page_down(len);
3884 } else if self.current_service == Service::LambdaApplications {
3885 let len = crate::ui::lambda::filtered_lambda_applications(self).len();
3886 self.lambda_application_state.table.page_down(len);
3887 } else if self.current_service == Service::CloudFormationStacks {
3888 let filtered = self.filtered_cloudformation_stacks();
3889 self.cfn_state.table.page_down(filtered.len());
3890 } else if self.current_service == Service::IamUsers {
3891 let len = crate::ui::iam::filtered_iam_users(self).len();
3892 nav_page_down(&mut self.iam_state.users.selected, len, 10);
3893 } else if self.current_service == Service::IamRoles {
3894 if self.iam_state.current_role.is_some() {
3895 let filtered = crate::ui::iam::filtered_iam_policies(self);
3896 if !filtered.is_empty() {
3897 self.iam_state.policies.page_down(filtered.len());
3898 }
3899 } else {
3900 let filtered = crate::ui::iam::filtered_iam_roles(self);
3901 self.iam_state.roles.page_down(filtered.len());
3902 }
3903 } else if self.current_service == Service::IamUserGroups {
3904 if self.iam_state.current_group.is_some() {
3905 if self.iam_state.group_tab == GroupTab::Users {
3906 let filtered: Vec<_> = self
3907 .iam_state
3908 .group_users
3909 .items
3910 .iter()
3911 .filter(|u| {
3912 if self.iam_state.group_users.filter.is_empty() {
3913 true
3914 } else {
3915 u.user_name
3916 .to_lowercase()
3917 .contains(&self.iam_state.group_users.filter.to_lowercase())
3918 }
3919 })
3920 .collect();
3921 if !filtered.is_empty() {
3922 self.iam_state.group_users.page_down(filtered.len());
3923 }
3924 } else if self.iam_state.group_tab == GroupTab::Permissions {
3925 let filtered = crate::ui::iam::filtered_iam_policies(self);
3926 if !filtered.is_empty() {
3927 self.iam_state.policies.page_down(filtered.len());
3928 }
3929 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3930 let filtered = crate::ui::iam::filtered_last_accessed(self);
3931 if !filtered.is_empty() {
3932 self.iam_state
3933 .last_accessed_services
3934 .page_down(filtered.len());
3935 }
3936 }
3937 } else {
3938 let filtered: Vec<_> = self
3939 .iam_state
3940 .groups
3941 .items
3942 .iter()
3943 .filter(|g| {
3944 if self.iam_state.groups.filter.is_empty() {
3945 true
3946 } else {
3947 g.group_name
3948 .to_lowercase()
3949 .contains(&self.iam_state.groups.filter.to_lowercase())
3950 }
3951 })
3952 .collect();
3953 if !filtered.is_empty() {
3954 self.iam_state.groups.page_down(filtered.len());
3955 }
3956 }
3957 }
3958 }
3959 }
3960
3961 fn page_up(&mut self) {
3962 if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
3963 let page_size = self.cfn_state.table.page_size.value();
3964 self.cfn_state.input_focus.handle_page_up(
3965 &mut self.cfn_state.table.selected,
3966 &mut self.cfn_state.table.scroll_offset,
3967 page_size,
3968 );
3969 } else if self.mode == Mode::FilterInput
3970 && self.current_service == Service::IamRoles
3971 && self.iam_state.current_role.is_none()
3972 {
3973 let page_size = self.iam_state.roles.page_size.value();
3974 self.iam_state.role_input_focus.handle_page_up(
3975 &mut self.iam_state.roles.selected,
3976 &mut self.iam_state.roles.scroll_offset,
3977 page_size,
3978 );
3979 } else if self.mode == Mode::FilterInput
3980 && self.current_service == Service::CloudWatchAlarms
3981 {
3982 let page_size = self.alarms_state.table.page_size.value();
3983 self.alarms_state.input_focus.handle_page_up(
3984 &mut self.alarms_state.table.selected,
3985 &mut self.alarms_state.table.scroll_offset,
3986 page_size,
3987 );
3988 } else if self.mode == Mode::FilterInput
3989 && self.current_service == Service::CloudWatchLogGroups
3990 {
3991 if self.view_mode == ViewMode::List {
3992 let page_size = self.log_groups_state.log_groups.page_size.value();
3994 self.log_groups_state.input_focus.handle_page_up(
3995 &mut self.log_groups_state.log_groups.selected,
3996 &mut self.log_groups_state.log_groups.scroll_offset,
3997 page_size,
3998 );
3999 } else {
4000 let page_size = 20;
4002 self.log_groups_state.input_focus.handle_page_up(
4003 &mut self.log_groups_state.selected_stream,
4004 &mut self.log_groups_state.stream_page,
4005 page_size,
4006 );
4007 self.log_groups_state.expanded_stream = None;
4008 }
4009 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4010 {
4011 if self.lambda_state.current_function.is_some()
4012 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4013 && self.lambda_state.version_input_focus == InputFocus::Pagination
4014 {
4015 let page_size = self.lambda_state.version_table.page_size.value();
4016 self.lambda_state.version_table.selected = self
4017 .lambda_state
4018 .version_table
4019 .selected
4020 .saturating_sub(page_size);
4021 } else if self.lambda_state.current_function.is_some()
4022 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4023 || (self.lambda_state.current_version.is_some()
4024 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4025 && self.lambda_state.alias_input_focus == InputFocus::Pagination
4026 {
4027 let page_size = self.lambda_state.alias_table.page_size.value();
4028 self.lambda_state.alias_table.selected = self
4029 .lambda_state
4030 .alias_table
4031 .selected
4032 .saturating_sub(page_size);
4033 } else if self.lambda_state.current_function.is_none() {
4034 let page_size = self.lambda_state.table.page_size.value();
4035 self.lambda_state.input_focus.handle_page_up(
4036 &mut self.lambda_state.table.selected,
4037 &mut self.lambda_state.table.scroll_offset,
4038 page_size,
4039 );
4040 }
4041 } else if self.mode == Mode::FilterInput
4042 && self.current_service == Service::EcrRepositories
4043 && self.ecr_state.current_repository.is_none()
4044 && self.ecr_state.input_focus == InputFocus::Filter
4045 {
4046 self.ecr_state.repositories.page_up();
4048 } else if self.mode == Mode::FilterInput
4049 && self.current_service == Service::EcrRepositories
4050 && self.ecr_state.current_repository.is_none()
4051 {
4052 let page_size = self.ecr_state.repositories.page_size.value();
4053 self.ecr_state.input_focus.handle_page_up(
4054 &mut self.ecr_state.repositories.selected,
4055 &mut self.ecr_state.repositories.scroll_offset,
4056 page_size,
4057 );
4058 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4059 let page_size = self.iam_state.policies.page_size.value();
4060 self.iam_state.policy_input_focus.handle_page_up(
4061 &mut self.iam_state.policies.selected,
4062 &mut self.iam_state.policies.scroll_offset,
4063 page_size,
4064 );
4065 } else if self.view_mode == ViewMode::PolicyView {
4066 self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
4067 } else if self.current_service == Service::IamRoles
4068 && self.iam_state.current_role.is_some()
4069 && self.iam_state.role_tab == RoleTab::TrustRelationships
4070 {
4071 self.iam_state.trust_policy_scroll =
4072 self.iam_state.trust_policy_scroll.saturating_sub(10);
4073 } else if self.current_service == Service::IamRoles
4074 && self.iam_state.current_role.is_some()
4075 && self.iam_state.role_tab == RoleTab::RevokeSessions
4076 {
4077 self.iam_state.revoke_sessions_scroll =
4078 self.iam_state.revoke_sessions_scroll.saturating_sub(10);
4079 } else if self.mode == Mode::Normal {
4080 if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4081 {
4082 self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
4083
4084 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4086 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4087 }
4088 } else if self.current_service == Service::S3Buckets
4089 && self.s3_state.current_bucket.is_some()
4090 {
4091 self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
4092
4093 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4095 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4096 }
4097 } else if self.current_service == Service::CloudWatchLogGroups
4098 && self.view_mode == ViewMode::List
4099 {
4100 self.log_groups_state.log_groups.page_up();
4101 } else if self.current_service == Service::CloudWatchLogGroups
4102 && self.view_mode == ViewMode::Detail
4103 {
4104 self.log_groups_state.selected_stream =
4105 self.log_groups_state.selected_stream.saturating_sub(10);
4106 } else if self.view_mode == ViewMode::Events {
4107 if self.log_groups_state.event_scroll_offset < 10
4108 && self.log_groups_state.has_older_events
4109 {
4110 self.log_groups_state.loading = true;
4111 }
4112 self.log_groups_state.event_scroll_offset =
4113 self.log_groups_state.event_scroll_offset.saturating_sub(10);
4114 } else if self.view_mode == ViewMode::InsightsResults {
4115 self.insights_state.insights.results_selected = self
4116 .insights_state
4117 .insights
4118 .results_selected
4119 .saturating_sub(10);
4120 } else if self.current_service == Service::CloudWatchAlarms {
4121 self.alarms_state.table.page_up();
4122 } else if self.current_service == Service::EcrRepositories {
4123 if self.ecr_state.current_repository.is_some() {
4124 self.ecr_state.images.page_up();
4125 } else {
4126 self.ecr_state.repositories.page_up();
4127 }
4128 } else if self.current_service == Service::LambdaFunctions {
4129 self.lambda_state.table.page_up();
4130 } else if self.current_service == Service::LambdaApplications {
4131 self.lambda_application_state.table.page_up();
4132 } else if self.current_service == Service::CloudFormationStacks {
4133 self.cfn_state.table.page_up();
4134 } else if self.current_service == Service::IamUsers {
4135 self.iam_state.users.page_up();
4136 } else if self.current_service == Service::IamRoles {
4137 if self.iam_state.current_role.is_some() {
4138 self.iam_state.policies.page_up();
4139 } else {
4140 self.iam_state.roles.page_up();
4141 }
4142 }
4143 }
4144 }
4145
4146 fn next_pane(&mut self) {
4147 if self.current_service == Service::S3Buckets {
4148 if self.s3_state.current_bucket.is_some() {
4149 let mut visual_idx = 0;
4152 let mut found_obj: Option<S3Object> = None;
4153
4154 fn check_nested(
4156 obj: &S3Object,
4157 visual_idx: &mut usize,
4158 target_idx: usize,
4159 expanded_prefixes: &std::collections::HashSet<String>,
4160 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
4161 found_obj: &mut Option<S3Object>,
4162 ) {
4163 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4164 if let Some(preview) = prefix_preview.get(&obj.key) {
4165 for nested_obj in preview {
4166 if *visual_idx == target_idx {
4167 *found_obj = Some(nested_obj.clone());
4168 return;
4169 }
4170 *visual_idx += 1;
4171
4172 check_nested(
4174 nested_obj,
4175 visual_idx,
4176 target_idx,
4177 expanded_prefixes,
4178 prefix_preview,
4179 found_obj,
4180 );
4181 if found_obj.is_some() {
4182 return;
4183 }
4184 }
4185 } else {
4186 *visual_idx += 1;
4188 }
4189 }
4190 }
4191
4192 for obj in &self.s3_state.objects {
4193 if visual_idx == self.s3_state.selected_object {
4194 found_obj = Some(obj.clone());
4195 break;
4196 }
4197 visual_idx += 1;
4198
4199 check_nested(
4201 obj,
4202 &mut visual_idx,
4203 self.s3_state.selected_object,
4204 &self.s3_state.expanded_prefixes,
4205 &self.s3_state.prefix_preview,
4206 &mut found_obj,
4207 );
4208 if found_obj.is_some() {
4209 break;
4210 }
4211 }
4212
4213 if let Some(obj) = found_obj {
4214 if obj.is_prefix {
4215 if !self.s3_state.expanded_prefixes.contains(&obj.key) {
4216 self.s3_state.expanded_prefixes.insert(obj.key.clone());
4217 if !self.s3_state.prefix_preview.contains_key(&obj.key) {
4219 self.s3_state.buckets.loading = true;
4220 }
4221 }
4222 if self.s3_state.expanded_prefixes.contains(&obj.key) {
4224 if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
4225 if !preview.is_empty() {
4226 self.s3_state.selected_object += 1;
4227 }
4228 }
4229 }
4230 }
4231 }
4232 } else {
4233 let mut row_idx = 0;
4235 let mut found = false;
4236 for bucket in &self.s3_state.buckets.items {
4237 if row_idx == self.s3_state.selected_row {
4238 if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
4240 self.s3_state.expanded_prefixes.insert(bucket.name.clone());
4241 if !self.s3_state.bucket_preview.contains_key(&bucket.name)
4242 && !self.s3_state.bucket_errors.contains_key(&bucket.name)
4243 {
4244 self.s3_state.buckets.loading = true;
4245 }
4246 }
4247 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4249 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4250 if !preview.is_empty() {
4251 self.s3_state.selected_row = row_idx + 1;
4252 }
4253 }
4254 }
4255 break;
4256 }
4257 row_idx += 1;
4258
4259 if self.s3_state.bucket_errors.contains_key(&bucket.name)
4261 && self.s3_state.expanded_prefixes.contains(&bucket.name)
4262 {
4263 continue;
4264 }
4265
4266 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4267 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4268 #[allow(clippy::too_many_arguments)]
4270 fn check_nested_expansion(
4271 objects: &[crate::s3::Object],
4272 row_idx: &mut usize,
4273 target_row: usize,
4274 expanded_prefixes: &mut std::collections::HashSet<String>,
4275 prefix_preview: &std::collections::HashMap<
4276 String,
4277 Vec<crate::s3::Object>,
4278 >,
4279 found: &mut bool,
4280 loading: &mut bool,
4281 selected_row: &mut usize,
4282 ) {
4283 for obj in objects {
4284 if *row_idx == target_row {
4285 if obj.is_prefix {
4287 if !expanded_prefixes.contains(&obj.key) {
4288 expanded_prefixes.insert(obj.key.clone());
4289 if !prefix_preview.contains_key(&obj.key) {
4290 *loading = true;
4291 }
4292 }
4293 if expanded_prefixes.contains(&obj.key) {
4295 if let Some(preview) = prefix_preview.get(&obj.key)
4296 {
4297 if !preview.is_empty() {
4298 *selected_row = *row_idx + 1;
4299 }
4300 }
4301 }
4302 }
4303 *found = true;
4304 return;
4305 }
4306 *row_idx += 1;
4307
4308 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4310 if let Some(nested) = prefix_preview.get(&obj.key) {
4311 check_nested_expansion(
4312 nested,
4313 row_idx,
4314 target_row,
4315 expanded_prefixes,
4316 prefix_preview,
4317 found,
4318 loading,
4319 selected_row,
4320 );
4321 if *found {
4322 return;
4323 }
4324 } else {
4325 *row_idx += 1; }
4327 }
4328 }
4329 }
4330
4331 check_nested_expansion(
4332 preview,
4333 &mut row_idx,
4334 self.s3_state.selected_row,
4335 &mut self.s3_state.expanded_prefixes,
4336 &self.s3_state.prefix_preview,
4337 &mut found,
4338 &mut self.s3_state.buckets.loading,
4339 &mut self.s3_state.selected_row,
4340 );
4341 if found || row_idx > self.s3_state.selected_row {
4342 break;
4343 }
4344 } else {
4345 row_idx += 1;
4346 if row_idx > self.s3_state.selected_row {
4347 break;
4348 }
4349 }
4350 }
4351 if found {
4352 break;
4353 }
4354 }
4355 }
4356 } else if self.view_mode == ViewMode::InsightsResults {
4357 let max_cols = self
4359 .insights_state
4360 .insights
4361 .query_results
4362 .first()
4363 .map(|r| r.len())
4364 .unwrap_or(0);
4365 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
4366 self.insights_state.insights.results_horizontal_scroll += 1;
4367 }
4368 } else if self.current_service == Service::CloudWatchLogGroups
4369 && self.view_mode == ViewMode::List
4370 {
4371 if self.log_groups_state.log_groups.expanded_item
4373 != Some(self.log_groups_state.log_groups.selected)
4374 {
4375 self.log_groups_state.log_groups.expanded_item =
4376 Some(self.log_groups_state.log_groups.selected);
4377 }
4378 } else if self.current_service == Service::CloudWatchLogGroups
4379 && self.view_mode == ViewMode::Detail
4380 {
4381 if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
4383 {
4384 self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
4385 }
4386 } else if self.view_mode == ViewMode::Events {
4387 if self.log_groups_state.expanded_event
4390 != Some(self.log_groups_state.event_scroll_offset)
4391 {
4392 self.log_groups_state.expanded_event =
4393 Some(self.log_groups_state.event_scroll_offset);
4394 }
4395 } else if self.current_service == Service::CloudWatchAlarms {
4396 if !self.alarms_state.table.is_expanded() {
4398 self.alarms_state.table.toggle_expand();
4399 }
4400 } else if self.current_service == Service::EcrRepositories {
4401 if self.ecr_state.current_repository.is_some() {
4402 self.ecr_state.images.toggle_expand();
4404 } else {
4405 self.ecr_state.repositories.toggle_expand();
4407 }
4408 } else if self.current_service == Service::LambdaFunctions {
4409 if self.lambda_state.current_function.is_some()
4410 && self.lambda_state.detail_tab == LambdaDetailTab::Code
4411 {
4412 if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
4414 self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
4415 } else {
4416 self.lambda_state.layer_expanded = None;
4417 }
4418 } else if self.lambda_state.current_function.is_some()
4419 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4420 {
4421 self.lambda_state.version_table.toggle_expand();
4423 } else if self.lambda_state.current_function.is_some()
4424 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4425 || (self.lambda_state.current_version.is_some()
4426 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4427 {
4428 self.lambda_state.alias_table.toggle_expand();
4430 } else if self.lambda_state.current_function.is_none() {
4431 self.lambda_state.table.toggle_expand();
4433 }
4434 } else if self.current_service == Service::LambdaApplications {
4435 if self.lambda_application_state.current_application.is_some() {
4436 if self.lambda_application_state.detail_tab
4438 == crate::ui::lambda::ApplicationDetailTab::Overview
4439 {
4440 self.lambda_application_state.resources.toggle_expand();
4441 } else {
4442 self.lambda_application_state.deployments.toggle_expand();
4443 }
4444 } else {
4445 if self.lambda_application_state.table.expanded_item
4447 != Some(self.lambda_application_state.table.selected)
4448 {
4449 self.lambda_application_state.table.expanded_item =
4450 Some(self.lambda_application_state.table.selected);
4451 }
4452 }
4453 } else if self.current_service == Service::CloudFormationStacks
4454 && self.cfn_state.current_stack.is_none()
4455 {
4456 self.cfn_state.table.toggle_expand();
4457 } else if self.current_service == Service::IamUsers {
4458 if self.iam_state.current_user.is_some() {
4459 if self.iam_state.user_tab == UserTab::Tags {
4460 if self.iam_state.user_tags.expanded_item
4461 != Some(self.iam_state.user_tags.selected)
4462 {
4463 self.iam_state.user_tags.expanded_item =
4464 Some(self.iam_state.user_tags.selected);
4465 }
4466 } else if self.iam_state.policies.expanded_item
4467 != Some(self.iam_state.policies.selected)
4468 {
4469 self.iam_state.policies.toggle_expand();
4470 }
4471 } else if !self.iam_state.users.is_expanded() {
4472 self.iam_state.users.toggle_expand();
4473 }
4474 } else if self.current_service == Service::IamRoles {
4475 if self.iam_state.current_role.is_some() {
4476 if self.iam_state.role_tab == RoleTab::Tags {
4478 if !self.iam_state.tags.is_expanded() {
4479 self.iam_state.tags.expand();
4480 }
4481 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4482 if !self.iam_state.last_accessed_services.is_expanded() {
4483 self.iam_state.last_accessed_services.expand();
4484 }
4485 } else if !self.iam_state.policies.is_expanded() {
4486 self.iam_state.policies.expand();
4487 }
4488 } else if !self.iam_state.roles.is_expanded() {
4489 self.iam_state.roles.expand();
4490 }
4491 } else if self.current_service == Service::IamUserGroups {
4492 if self.iam_state.current_group.is_some() {
4493 if self.iam_state.group_tab == GroupTab::Users {
4494 if !self.iam_state.group_users.is_expanded() {
4495 self.iam_state.group_users.expand();
4496 }
4497 } else if self.iam_state.group_tab == GroupTab::Permissions {
4498 if !self.iam_state.policies.is_expanded() {
4499 self.iam_state.policies.expand();
4500 }
4501 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
4502 && !self.iam_state.last_accessed_services.is_expanded()
4503 {
4504 self.iam_state.last_accessed_services.expand();
4505 }
4506 } else if !self.iam_state.groups.is_expanded() {
4507 self.iam_state.groups.expand();
4508 }
4509 }
4510 }
4511
4512 fn go_to_page(&mut self, page: usize) {
4513 if page == 0 {
4514 return;
4515 }
4516
4517 match self.current_service {
4518 Service::CloudWatchAlarms => {
4519 let alarm_page_size = self.alarms_state.table.page_size.value();
4520 let target = (page - 1) * alarm_page_size;
4521 let filtered_count = match self.alarms_state.alarm_tab {
4522 AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4523 AlarmTab::InAlarm => self
4524 .alarms_state
4525 .table
4526 .items
4527 .iter()
4528 .filter(|a| a.state.to_uppercase() == "ALARM")
4529 .count(),
4530 };
4531 let max_offset = filtered_count.saturating_sub(alarm_page_size);
4532 self.alarms_state.table.scroll_offset = target.min(max_offset);
4533 self.alarms_state.table.selected = self
4534 .alarms_state
4535 .table
4536 .scroll_offset
4537 .min(filtered_count.saturating_sub(1));
4538 }
4539 Service::CloudWatchLogGroups => match self.view_mode {
4540 ViewMode::Events => {
4541 let page_size = 20;
4542 let target = (page - 1) * page_size;
4543 let max = self.log_groups_state.log_events.len().saturating_sub(1);
4544 self.log_groups_state.event_scroll_offset = target.min(max);
4545 }
4546 ViewMode::Detail => {
4547 let page_size = 20;
4548 let target = (page - 1) * page_size;
4549 let max = self.log_groups_state.log_streams.len().saturating_sub(1);
4550 self.log_groups_state.selected_stream = target.min(max);
4551 }
4552 ViewMode::List => {
4553 let total = self.log_groups_state.log_groups.items.len();
4554 self.log_groups_state.log_groups.goto_page(page, total);
4555 }
4556 _ => {}
4557 },
4558 Service::EcrRepositories => {
4559 if self.ecr_state.current_repository.is_some() {
4560 let filtered_count = self
4561 .ecr_state
4562 .images
4563 .filtered(|img| {
4564 self.ecr_state.images.filter.is_empty()
4565 || img
4566 .tag
4567 .to_lowercase()
4568 .contains(&self.ecr_state.images.filter.to_lowercase())
4569 || img
4570 .digest
4571 .to_lowercase()
4572 .contains(&self.ecr_state.images.filter.to_lowercase())
4573 })
4574 .len();
4575 self.ecr_state.images.goto_page(page, filtered_count);
4576 } else {
4577 let filtered_count = self
4578 .ecr_state
4579 .repositories
4580 .filtered(|r| {
4581 self.ecr_state.repositories.filter.is_empty()
4582 || r.name
4583 .to_lowercase()
4584 .contains(&self.ecr_state.repositories.filter.to_lowercase())
4585 })
4586 .len();
4587 self.ecr_state.repositories.goto_page(page, filtered_count);
4588 }
4589 }
4590 Service::S3Buckets => {
4591 if self.s3_state.current_bucket.is_some() {
4592 let page_size = 50; let target = (page - 1) * page_size;
4594 let total_rows = self.calculate_total_object_rows();
4595 let max = total_rows.saturating_sub(1);
4596 self.s3_state.selected_object = target.min(max);
4597 } else {
4598 let page_size = 50; let target = (page - 1) * page_size;
4600 let total_rows = self.calculate_total_bucket_rows();
4601 let max = total_rows.saturating_sub(1);
4602 self.s3_state.selected_row = target.min(max);
4603 }
4604 }
4605 Service::LambdaFunctions => {
4606 if self.lambda_state.current_function.is_some()
4607 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4608 {
4609 let filtered_count = self
4610 .lambda_state
4611 .version_table
4612 .filtered(|v| {
4613 self.lambda_state.version_table.filter.is_empty()
4614 || v.version.to_lowercase().contains(
4615 &self.lambda_state.version_table.filter.to_lowercase(),
4616 )
4617 || v.aliases.to_lowercase().contains(
4618 &self.lambda_state.version_table.filter.to_lowercase(),
4619 )
4620 || v.description.to_lowercase().contains(
4621 &self.lambda_state.version_table.filter.to_lowercase(),
4622 )
4623 })
4624 .len();
4625 self.lambda_state
4626 .version_table
4627 .goto_page(page, filtered_count);
4628 } else {
4629 let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
4630 self.lambda_state.table.goto_page(page, filtered_count);
4631 }
4632 }
4633 Service::LambdaApplications => {
4634 let filtered_count = crate::ui::lambda::filtered_lambda_applications(self).len();
4635 self.lambda_application_state
4636 .table
4637 .goto_page(page, filtered_count);
4638 }
4639 Service::CloudFormationStacks => {
4640 let filtered_count = self.filtered_cloudformation_stacks().len();
4641 self.cfn_state.table.goto_page(page, filtered_count);
4642 }
4643 Service::IamUsers => {
4644 let filtered_count = crate::ui::iam::filtered_iam_users(self).len();
4645 self.iam_state.users.goto_page(page, filtered_count);
4646 }
4647 Service::IamRoles => {
4648 let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
4649 self.iam_state.roles.goto_page(page, filtered_count);
4650 }
4651 _ => {}
4652 }
4653 }
4654
4655 fn prev_pane(&mut self) {
4656 if self.current_service == Service::S3Buckets {
4657 if self.s3_state.current_bucket.is_some() {
4658 let mut visual_idx = 0;
4661 let mut found_obj: Option<S3Object> = None;
4662 let mut parent_idx: Option<usize> = None;
4663
4664 #[allow(clippy::too_many_arguments)]
4666 fn find_with_parent(
4667 objects: &[S3Object],
4668 visual_idx: &mut usize,
4669 target_idx: usize,
4670 expanded_prefixes: &std::collections::HashSet<String>,
4671 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
4672 found_obj: &mut Option<S3Object>,
4673 parent_idx: &mut Option<usize>,
4674 current_parent: Option<usize>,
4675 ) {
4676 for obj in objects {
4677 if *visual_idx == target_idx {
4678 *found_obj = Some(obj.clone());
4679 *parent_idx = current_parent;
4680 return;
4681 }
4682 let obj_idx = *visual_idx;
4683 *visual_idx += 1;
4684
4685 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4687 if let Some(preview) = prefix_preview.get(&obj.key) {
4688 find_with_parent(
4689 preview,
4690 visual_idx,
4691 target_idx,
4692 expanded_prefixes,
4693 prefix_preview,
4694 found_obj,
4695 parent_idx,
4696 Some(obj_idx),
4697 );
4698 if found_obj.is_some() {
4699 return;
4700 }
4701 }
4702 }
4703 }
4704 }
4705
4706 find_with_parent(
4707 &self.s3_state.objects,
4708 &mut visual_idx,
4709 self.s3_state.selected_object,
4710 &self.s3_state.expanded_prefixes,
4711 &self.s3_state.prefix_preview,
4712 &mut found_obj,
4713 &mut parent_idx,
4714 None,
4715 );
4716
4717 if let Some(obj) = found_obj {
4718 if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
4719 self.s3_state.expanded_prefixes.remove(&obj.key);
4721 } else if let Some(parent) = parent_idx {
4722 self.s3_state.selected_object = parent;
4724 }
4725 }
4726
4727 let visible_rows = self.s3_state.object_visible_rows.get();
4729 if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4730 self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4731 } else if self.s3_state.selected_object
4732 >= self.s3_state.object_scroll_offset + visible_rows
4733 {
4734 self.s3_state.object_scroll_offset = self
4735 .s3_state
4736 .selected_object
4737 .saturating_sub(visible_rows - 1);
4738 }
4739 } else {
4740 let mut row_idx = 0;
4742 for bucket in &self.s3_state.buckets.items {
4743 if row_idx == self.s3_state.selected_row {
4744 self.s3_state.expanded_prefixes.remove(&bucket.name);
4746 break;
4747 }
4748 row_idx += 1;
4749 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4750 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4751 #[allow(clippy::too_many_arguments)]
4753 fn check_nested_collapse(
4754 objects: &[crate::s3::Object],
4755 row_idx: &mut usize,
4756 target_row: usize,
4757 expanded_prefixes: &mut std::collections::HashSet<String>,
4758 prefix_preview: &std::collections::HashMap<
4759 String,
4760 Vec<crate::s3::Object>,
4761 >,
4762 found: &mut bool,
4763 selected_row: &mut usize,
4764 parent_row: usize,
4765 ) {
4766 for obj in objects {
4767 let current_row = *row_idx;
4768 if *row_idx == target_row {
4769 if obj.is_prefix {
4771 if expanded_prefixes.contains(&obj.key) {
4772 expanded_prefixes.remove(&obj.key);
4774 } else {
4775 *selected_row = parent_row;
4777 }
4778 } else {
4779 *selected_row = parent_row;
4781 }
4782 *found = true;
4783 return;
4784 }
4785 *row_idx += 1;
4786
4787 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4789 if let Some(nested) = prefix_preview.get(&obj.key) {
4790 check_nested_collapse(
4791 nested,
4792 row_idx,
4793 target_row,
4794 expanded_prefixes,
4795 prefix_preview,
4796 found,
4797 selected_row,
4798 current_row,
4799 );
4800 if *found {
4801 return;
4802 }
4803 } else {
4804 *row_idx += 1; }
4806 }
4807 }
4808 }
4809
4810 let mut found = false;
4811 let parent_row = row_idx - 1; check_nested_collapse(
4813 preview,
4814 &mut row_idx,
4815 self.s3_state.selected_row,
4816 &mut self.s3_state.expanded_prefixes,
4817 &self.s3_state.prefix_preview,
4818 &mut found,
4819 &mut self.s3_state.selected_row,
4820 parent_row,
4821 );
4822 if found {
4823 let visible_rows = self.s3_state.bucket_visible_rows.get();
4825 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4826 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4827 } else if self.s3_state.selected_row
4828 >= self.s3_state.bucket_scroll_offset + visible_rows
4829 {
4830 self.s3_state.bucket_scroll_offset =
4831 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
4832 }
4833 return;
4834 }
4835 } else {
4836 row_idx += 1;
4837 }
4838 }
4839 }
4840
4841 let visible_rows = self.s3_state.bucket_visible_rows.get();
4843 if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4844 self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4845 } else if self.s3_state.selected_row
4846 >= self.s3_state.bucket_scroll_offset + visible_rows
4847 {
4848 self.s3_state.bucket_scroll_offset =
4849 self.s3_state.selected_row.saturating_sub(visible_rows - 1);
4850 }
4851 }
4852 } else if self.view_mode == ViewMode::InsightsResults {
4853 self.insights_state.insights.results_horizontal_scroll = self
4855 .insights_state
4856 .insights
4857 .results_horizontal_scroll
4858 .saturating_sub(1);
4859 } else if self.current_service == Service::CloudWatchLogGroups
4860 && self.view_mode == ViewMode::List
4861 {
4862 if self.log_groups_state.log_groups.has_expanded_item() {
4864 self.log_groups_state.log_groups.collapse();
4865 }
4866 } else if self.current_service == Service::CloudWatchLogGroups
4867 && self.view_mode == ViewMode::Detail
4868 {
4869 if self.log_groups_state.expanded_stream.is_some() {
4871 self.log_groups_state.expanded_stream = None;
4872 }
4873 } else if self.view_mode == ViewMode::Events {
4874 if self.log_groups_state.expanded_event.is_some() {
4876 self.log_groups_state.expanded_event = None;
4877 }
4878 } else if self.current_service == Service::CloudWatchAlarms {
4879 self.alarms_state.table.collapse();
4881 } else if self.current_service == Service::EcrRepositories {
4882 if self.ecr_state.current_repository.is_some() {
4883 self.ecr_state.images.collapse();
4885 } else {
4886 self.ecr_state.repositories.collapse();
4888 }
4889 } else if self.current_service == Service::LambdaFunctions {
4890 if self.lambda_state.current_function.is_some()
4891 && self.lambda_state.detail_tab == LambdaDetailTab::Code
4892 {
4893 self.lambda_state.layer_expanded = None;
4895 } else if self.lambda_state.current_function.is_some()
4896 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4897 {
4898 self.lambda_state.version_table.collapse();
4900 } else if self.lambda_state.current_function.is_some()
4901 && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4902 || (self.lambda_state.current_version.is_some()
4903 && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4904 {
4905 self.lambda_state.alias_table.collapse();
4907 } else if self.lambda_state.current_function.is_none() {
4908 self.lambda_state.table.collapse();
4910 }
4911 } else if self.current_service == Service::LambdaApplications {
4912 if self.lambda_application_state.current_application.is_some() {
4913 if self.lambda_application_state.detail_tab
4915 == crate::ui::lambda::ApplicationDetailTab::Overview
4916 {
4917 self.lambda_application_state.resources.collapse();
4918 } else {
4919 self.lambda_application_state.deployments.collapse();
4920 }
4921 } else {
4922 if self.lambda_application_state.table.has_expanded_item() {
4924 self.lambda_application_state.table.collapse();
4925 }
4926 }
4927 } else if self.current_service == Service::CloudFormationStacks
4928 && self.cfn_state.current_stack.is_none()
4929 {
4930 self.cfn_state.table.collapse();
4931 } else if self.current_service == Service::IamUsers {
4932 if self.iam_state.users.has_expanded_item() {
4933 self.iam_state.users.collapse();
4934 }
4935 } else if self.current_service == Service::IamRoles {
4936 if self.view_mode == ViewMode::PolicyView {
4937 self.view_mode = ViewMode::Detail;
4939 self.iam_state.current_policy = None;
4940 self.iam_state.policy_document.clear();
4941 self.iam_state.policy_scroll = 0;
4942 } else if self.iam_state.current_role.is_some() {
4943 if self.iam_state.role_tab == RoleTab::Tags
4944 && self.iam_state.tags.has_expanded_item()
4945 {
4946 self.iam_state.tags.collapse();
4947 } else if self.iam_state.role_tab == RoleTab::LastAccessed
4948 && self
4949 .iam_state
4950 .last_accessed_services
4951 .expanded_item
4952 .is_some()
4953 {
4954 self.iam_state.last_accessed_services.collapse();
4955 } else if self.iam_state.policies.has_expanded_item() {
4956 self.iam_state.policies.collapse();
4957 }
4958 } else if self.iam_state.roles.has_expanded_item() {
4959 self.iam_state.roles.collapse();
4960 }
4961 } else if self.current_service == Service::IamUserGroups {
4962 if self.iam_state.current_group.is_some() {
4963 if self.iam_state.group_tab == GroupTab::Users
4964 && self.iam_state.group_users.has_expanded_item()
4965 {
4966 self.iam_state.group_users.collapse();
4967 } else if self.iam_state.group_tab == GroupTab::Permissions
4968 && self.iam_state.policies.has_expanded_item()
4969 {
4970 self.iam_state.policies.collapse();
4971 } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
4972 && self
4973 .iam_state
4974 .last_accessed_services
4975 .expanded_item
4976 .is_some()
4977 {
4978 self.iam_state.last_accessed_services.collapse();
4979 }
4980 } else if self.iam_state.groups.has_expanded_item() {
4981 self.iam_state.groups.collapse();
4982 }
4983 }
4984 }
4985
4986 fn select_item(&mut self) {
4987 if self.mode == Mode::RegionPicker {
4988 let filtered = self.get_filtered_regions();
4989 if let Some(region) = filtered.get(self.region_picker_selected) {
4990 if !self.tabs.is_empty() {
4992 let mut session = crate::session::Session::new(
4993 self.profile.clone(),
4994 self.region.clone(),
4995 self.config.account_id.clone(),
4996 self.config.role_arn.clone(),
4997 );
4998
4999 for tab in &self.tabs {
5000 session.tabs.push(crate::session::SessionTab {
5001 service: format!("{:?}", tab.service),
5002 title: tab.title.clone(),
5003 breadcrumb: tab.breadcrumb.clone(),
5004 filter: None,
5005 selected_item: None,
5006 });
5007 }
5008
5009 let _ = session.save();
5010 }
5011
5012 self.region = region.code.to_string();
5013 self.config.region = region.code.to_string();
5014
5015 self.tabs.clear();
5017 self.current_tab = 0;
5018 self.service_selected = false;
5019
5020 self.mode = Mode::Normal;
5021 }
5022 } else if self.mode == Mode::ProfilePicker {
5023 let filtered = self.get_filtered_profiles();
5024 if let Some(profile) = filtered.get(self.profile_picker_selected) {
5025 let profile_name = profile.name.clone();
5026 let profile_region = profile.region.clone();
5027
5028 self.profile = profile_name.clone();
5029 std::env::set_var("AWS_PROFILE", &profile_name);
5030
5031 if let Some(region) = profile_region {
5033 self.region = region;
5034 }
5035
5036 self.mode = Mode::Normal;
5037 }
5039 } else if self.mode == Mode::ServicePicker {
5040 let filtered = self.filtered_services();
5041 if let Some(&service) = filtered.get(self.service_picker.selected) {
5042 let new_service = match service {
5043 "CloudWatch > Log Groups" => Service::CloudWatchLogGroups,
5044 "CloudWatch > Logs Insights" => Service::CloudWatchInsights,
5045 "CloudWatch > Alarms" => Service::CloudWatchAlarms,
5046 "CloudFormation > Stacks" => Service::CloudFormationStacks,
5047 "ECR > Repositories" => Service::EcrRepositories,
5048 "IAM > Users" => Service::IamUsers,
5049 "IAM > Roles" => Service::IamRoles,
5050 "IAM > User Groups" => Service::IamUserGroups,
5051 "Lambda > Functions" => Service::LambdaFunctions,
5052 "Lambda > Applications" => Service::LambdaApplications,
5053 "S3 > Buckets" => Service::S3Buckets,
5054 _ => return,
5055 };
5056
5057 self.tabs.push(Tab {
5059 service: new_service,
5060 title: service.to_string(),
5061 breadcrumb: service.to_string(),
5062 });
5063 self.current_tab = self.tabs.len() - 1;
5064 self.current_service = new_service;
5065 self.view_mode = ViewMode::List;
5066 self.service_selected = true;
5067 self.mode = Mode::Normal;
5068 }
5069 } else if self.mode == Mode::TabPicker {
5070 let filtered = self.get_filtered_tabs();
5071 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
5072 self.current_tab = idx;
5073 self.current_service = self.tabs[idx].service;
5074 self.mode = Mode::Normal;
5075 self.tab_filter.clear();
5076 }
5077 } else if self.mode == Mode::SessionPicker {
5078 let filtered = self.get_filtered_sessions();
5079 if let Some(&session) = filtered.get(self.session_picker_selected) {
5080 let session = session.clone();
5081
5082 self.current_session = Some(session.clone());
5084 self.profile = session.profile.clone();
5085 self.region = session.region.clone();
5086 self.config.region = session.region.clone();
5087 self.config.account_id = session.account_id.clone();
5088 self.config.role_arn = session.role_arn.clone();
5089
5090 self.tabs = session
5092 .tabs
5093 .iter()
5094 .map(|st| Tab {
5095 service: match st.service.as_str() {
5096 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
5097 "CloudWatchInsights" => Service::CloudWatchInsights,
5098 "CloudWatchAlarms" => Service::CloudWatchAlarms,
5099 "S3Buckets" => Service::S3Buckets,
5100 _ => Service::CloudWatchLogGroups,
5101 },
5102 title: st.title.clone(),
5103 breadcrumb: st.breadcrumb.clone(),
5104 })
5105 .collect();
5106
5107 if !self.tabs.is_empty() {
5108 self.current_tab = 0;
5109 self.current_service = self.tabs[0].service;
5110 self.service_selected = true;
5111 }
5112
5113 self.mode = Mode::Normal;
5114 }
5115 } else if self.mode == Mode::InsightsInput {
5116 use crate::app::InsightsFocus;
5118 match self.insights_state.insights.insights_focus {
5119 InsightsFocus::Query => {
5120 self.insights_state.insights.query_text.push('\n');
5122 self.insights_state.insights.query_cursor_line += 1;
5123 self.insights_state.insights.query_cursor_col = 0;
5124 }
5125 InsightsFocus::LogGroupSearch => {
5126 self.insights_state.insights.show_dropdown =
5128 !self.insights_state.insights.show_dropdown;
5129 }
5130 _ => {}
5131 }
5132 } else if self.mode == Mode::Normal {
5133 if !self.service_selected {
5135 let filtered = self.filtered_services();
5136 if let Some(&service) = filtered.get(self.service_picker.selected) {
5137 match service {
5138 "CloudWatch > Log Groups" => {
5139 self.current_service = Service::CloudWatchLogGroups;
5140 self.view_mode = ViewMode::List;
5141 self.service_selected = true;
5142 }
5143 "CloudWatch > Logs Insights" => {
5144 self.current_service = Service::CloudWatchInsights;
5145 self.view_mode = ViewMode::InsightsResults;
5146 self.service_selected = true;
5147 }
5148 "CloudWatch > Alarms" => {
5149 self.current_service = Service::CloudWatchAlarms;
5150 self.view_mode = ViewMode::List;
5151 self.service_selected = true;
5152 }
5153 "S3 > Buckets" => {
5154 self.current_service = Service::S3Buckets;
5155 self.view_mode = ViewMode::List;
5156 self.service_selected = true;
5157 }
5158 "ECR > Repositories" => {
5159 self.current_service = Service::EcrRepositories;
5160 self.view_mode = ViewMode::List;
5161 self.service_selected = true;
5162 }
5163 "Lambda > Functions" => {
5164 self.current_service = Service::LambdaFunctions;
5165 self.view_mode = ViewMode::List;
5166 self.service_selected = true;
5167 }
5168 "Lambda > Applications" => {
5169 self.current_service = Service::LambdaApplications;
5170 self.view_mode = ViewMode::List;
5171 self.service_selected = true;
5172 }
5173 _ => {}
5174 }
5175 }
5176 return;
5177 }
5178
5179 if self.view_mode == ViewMode::InsightsResults {
5181 if self.insights_state.insights.expanded_result
5183 == Some(self.insights_state.insights.results_selected)
5184 {
5185 self.insights_state.insights.expanded_result = None;
5186 } else {
5187 self.insights_state.insights.expanded_result =
5188 Some(self.insights_state.insights.results_selected);
5189 }
5190 } else if self.current_service == Service::S3Buckets {
5191 if self.s3_state.current_bucket.is_none() {
5192 let mut row_idx = 0;
5194 for bucket in &self.s3_state.buckets.items {
5195 if row_idx == self.s3_state.selected_row {
5196 self.s3_state.current_bucket = Some(bucket.name.clone());
5198 self.s3_state.prefix_stack.clear();
5199 self.s3_state.buckets.loading = true;
5200 return;
5201 }
5202 row_idx += 1;
5203
5204 if self.s3_state.bucket_errors.contains_key(&bucket.name)
5206 && self.s3_state.expanded_prefixes.contains(&bucket.name)
5207 {
5208 continue;
5209 }
5210
5211 if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5212 if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5213 for obj in preview {
5214 if row_idx == self.s3_state.selected_row {
5215 if obj.is_prefix {
5217 self.s3_state.current_bucket =
5218 Some(bucket.name.clone());
5219 self.s3_state.prefix_stack = vec![obj.key.clone()];
5220 self.s3_state.buckets.loading = true;
5221 }
5222 return;
5223 }
5224 row_idx += 1;
5225
5226 if obj.is_prefix
5228 && self.s3_state.expanded_prefixes.contains(&obj.key)
5229 {
5230 if let Some(nested) =
5231 self.s3_state.prefix_preview.get(&obj.key)
5232 {
5233 for nested_obj in nested {
5234 if row_idx == self.s3_state.selected_row {
5235 if nested_obj.is_prefix {
5237 self.s3_state.current_bucket =
5238 Some(bucket.name.clone());
5239 self.s3_state.prefix_stack = vec![
5241 obj.key.clone(),
5242 nested_obj.key.clone(),
5243 ];
5244 self.s3_state.buckets.loading = true;
5245 }
5246 return;
5247 }
5248 row_idx += 1;
5249 }
5250 } else {
5251 row_idx += 1;
5252 }
5253 }
5254 }
5255 } else {
5256 row_idx += 1;
5257 }
5258 }
5259 }
5260 } else {
5261 let mut visual_idx = 0;
5263 let mut found_obj: Option<S3Object> = None;
5264
5265 fn check_nested_select(
5267 obj: &S3Object,
5268 visual_idx: &mut usize,
5269 target_idx: usize,
5270 expanded_prefixes: &std::collections::HashSet<String>,
5271 prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5272 found_obj: &mut Option<S3Object>,
5273 ) {
5274 if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5275 if let Some(preview) = prefix_preview.get(&obj.key) {
5276 for nested_obj in preview {
5277 if *visual_idx == target_idx {
5278 *found_obj = Some(nested_obj.clone());
5279 return;
5280 }
5281 *visual_idx += 1;
5282
5283 check_nested_select(
5285 nested_obj,
5286 visual_idx,
5287 target_idx,
5288 expanded_prefixes,
5289 prefix_preview,
5290 found_obj,
5291 );
5292 if found_obj.is_some() {
5293 return;
5294 }
5295 }
5296 } else {
5297 *visual_idx += 1;
5299 }
5300 }
5301 }
5302
5303 for obj in &self.s3_state.objects {
5304 if visual_idx == self.s3_state.selected_object {
5305 found_obj = Some(obj.clone());
5306 break;
5307 }
5308 visual_idx += 1;
5309
5310 check_nested_select(
5312 obj,
5313 &mut visual_idx,
5314 self.s3_state.selected_object,
5315 &self.s3_state.expanded_prefixes,
5316 &self.s3_state.prefix_preview,
5317 &mut found_obj,
5318 );
5319 if found_obj.is_some() {
5320 break;
5321 }
5322 }
5323
5324 if let Some(obj) = found_obj {
5325 if obj.is_prefix {
5326 self.s3_state.prefix_stack.push(obj.key.clone());
5328 self.s3_state.buckets.loading = true;
5329 }
5330 }
5331 }
5332 } else if self.current_service == Service::CloudFormationStacks {
5333 if self.cfn_state.current_stack.is_none() {
5334 let filtered_stacks = self.filtered_cloudformation_stacks();
5336 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
5337 self.cfn_state.current_stack = Some(stack.name.clone());
5338 self.update_current_tab_breadcrumb();
5339 }
5340 }
5341 } else if self.current_service == Service::EcrRepositories {
5342 if self.ecr_state.current_repository.is_none() {
5343 let filtered_repos = self.filtered_ecr_repositories();
5345 if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
5346 let repo_name = repo.name.clone();
5347 let repo_uri = repo.uri.clone();
5348 self.ecr_state.current_repository = Some(repo_name);
5349 self.ecr_state.current_repository_uri = Some(repo_uri);
5350 self.ecr_state.images.reset();
5351 self.ecr_state.repositories.loading = true;
5352 }
5353 }
5354 } else if self.current_service == Service::IamUsers {
5355 if self.iam_state.current_user.is_some() {
5356 if self.iam_state.user_tab != UserTab::Tags {
5358 let filtered = crate::ui::iam::filtered_iam_policies(self);
5359 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5360 self.iam_state.current_policy = Some(policy.policy_name.clone());
5361 self.iam_state.policy_scroll = 0;
5362 self.view_mode = ViewMode::PolicyView;
5363 self.iam_state.policies.loading = true;
5364 self.update_current_tab_breadcrumb();
5365 }
5366 }
5367 } else if self.iam_state.current_user.is_none() {
5368 let filtered_users = crate::ui::iam::filtered_iam_users(self);
5369 if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
5370 self.iam_state.current_user = Some(user.user_name.clone());
5371 self.iam_state.user_tab = UserTab::Permissions;
5372 self.iam_state.policies.reset();
5373 self.update_current_tab_breadcrumb();
5374 }
5375 }
5376 } else if self.current_service == Service::IamRoles {
5377 if self.iam_state.current_role.is_some() {
5378 if self.iam_state.role_tab != RoleTab::Tags {
5380 let filtered = crate::ui::iam::filtered_iam_policies(self);
5381 if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5382 self.iam_state.current_policy = Some(policy.policy_name.clone());
5383 self.iam_state.policy_scroll = 0;
5384 self.view_mode = ViewMode::PolicyView;
5385 self.iam_state.policies.loading = true;
5386 self.update_current_tab_breadcrumb();
5387 }
5388 }
5389 } else if self.iam_state.current_role.is_none() {
5390 let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
5391 if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
5392 self.iam_state.current_role = Some(role.role_name.clone());
5393 self.iam_state.role_tab = RoleTab::Permissions;
5394 self.iam_state.policies.reset();
5395 self.update_current_tab_breadcrumb();
5396 }
5397 }
5398 } else if self.current_service == Service::IamUserGroups {
5399 if self.iam_state.current_group.is_none() {
5400 let filtered_groups: Vec<_> = self
5401 .iam_state
5402 .groups
5403 .items
5404 .iter()
5405 .filter(|g| {
5406 if self.iam_state.groups.filter.is_empty() {
5407 true
5408 } else {
5409 g.group_name
5410 .to_lowercase()
5411 .contains(&self.iam_state.groups.filter.to_lowercase())
5412 }
5413 })
5414 .collect();
5415 if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
5416 self.iam_state.current_group = Some(group.group_name.clone());
5417 self.update_current_tab_breadcrumb();
5418 }
5419 }
5420 } else if self.current_service == Service::LambdaFunctions {
5421 if self.lambda_state.current_function.is_some()
5422 && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5423 {
5424 if self.mode == Mode::Normal {
5427 let page_size = self.lambda_state.version_table.page_size.value();
5428 let filtered: Vec<_> = self
5429 .lambda_state
5430 .version_table
5431 .items
5432 .iter()
5433 .filter(|v| {
5434 self.lambda_state.version_table.filter.is_empty()
5435 || v.version.to_lowercase().contains(
5436 &self.lambda_state.version_table.filter.to_lowercase(),
5437 )
5438 || v.aliases.to_lowercase().contains(
5439 &self.lambda_state.version_table.filter.to_lowercase(),
5440 )
5441 })
5442 .collect();
5443 let current_page = self.lambda_state.version_table.selected / page_size;
5444 let start_idx = current_page * page_size;
5445 let end_idx = (start_idx + page_size).min(filtered.len());
5446 let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
5447 let page_index = self.lambda_state.version_table.selected % page_size;
5448 if let Some(version) = paginated.get(page_index) {
5449 self.lambda_state.current_version = Some(version.version.clone());
5450 self.lambda_state.detail_tab = LambdaDetailTab::Code;
5451 }
5452 } else {
5453 if self.lambda_state.version_table.expanded_item
5455 == Some(self.lambda_state.version_table.selected)
5456 {
5457 self.lambda_state.version_table.collapse();
5458 } else {
5459 self.lambda_state.version_table.expanded_item =
5460 Some(self.lambda_state.version_table.selected);
5461 }
5462 }
5463 } else if self.lambda_state.current_function.is_some()
5464 && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5465 {
5466 let filtered: Vec<_> = self
5468 .lambda_state
5469 .alias_table
5470 .items
5471 .iter()
5472 .filter(|a| {
5473 self.lambda_state.alias_table.filter.is_empty()
5474 || a.name
5475 .to_lowercase()
5476 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
5477 || a.versions
5478 .to_lowercase()
5479 .contains(&self.lambda_state.alias_table.filter.to_lowercase())
5480 })
5481 .collect();
5482 if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
5483 self.lambda_state.current_alias = Some(alias.name.clone());
5484 }
5485 } else if self.lambda_state.current_function.is_none() {
5486 let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
5487 if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
5488 self.lambda_state.current_function = Some(func.name.clone());
5489 self.lambda_state.detail_tab = LambdaDetailTab::Code;
5490 self.update_current_tab_breadcrumb();
5491 }
5492 }
5493 } else if self.current_service == Service::LambdaApplications {
5494 let filtered = crate::ui::lambda::filtered_lambda_applications(self);
5495 if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
5496 let app_name = app.name.clone();
5497 self.lambda_application_state.current_application = Some(app_name.clone());
5498 self.lambda_application_state.detail_tab =
5499 crate::ui::lambda::ApplicationDetailTab::Overview;
5500
5501 use crate::lambda::Resource;
5503 self.lambda_application_state.resources.items = vec![
5504 Resource {
5505 logical_id: "ApiGatewayRestApi".to_string(),
5506 physical_id: "abc123xyz".to_string(),
5507 resource_type: "AWS::ApiGateway::RestApi".to_string(),
5508 last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
5509 },
5510 Resource {
5511 logical_id: "LambdaFunction".to_string(),
5512 physical_id: format!("{}-function", app_name),
5513 resource_type: "AWS::Lambda::Function".to_string(),
5514 last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
5515 },
5516 Resource {
5517 logical_id: "DynamoDBTable".to_string(),
5518 physical_id: format!("{}-table", app_name),
5519 resource_type: "AWS::DynamoDB::Table".to_string(),
5520 last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
5521 },
5522 ];
5523
5524 use crate::lambda::Deployment;
5526 self.lambda_application_state.deployments.items = vec![
5527 Deployment {
5528 deployment_id: "d-ABC123XYZ".to_string(),
5529 resource_type: "AWS::Serverless::Application".to_string(),
5530 last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
5531 status: "Succeeded".to_string(),
5532 },
5533 Deployment {
5534 deployment_id: "d-DEF456UVW".to_string(),
5535 resource_type: "AWS::Serverless::Application".to_string(),
5536 last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
5537 status: "Succeeded".to_string(),
5538 },
5539 ];
5540
5541 self.update_current_tab_breadcrumb();
5542 }
5543 } else if self.current_service == Service::CloudWatchLogGroups {
5544 if self.view_mode == ViewMode::List {
5545 let filtered_groups = self.filtered_log_groups();
5547 if let Some(selected_group) =
5548 filtered_groups.get(self.log_groups_state.log_groups.selected)
5549 {
5550 if let Some(actual_idx) = self
5551 .log_groups_state
5552 .log_groups
5553 .items
5554 .iter()
5555 .position(|g| g.name == selected_group.name)
5556 {
5557 self.log_groups_state.log_groups.selected = actual_idx;
5558 }
5559 }
5560 self.view_mode = ViewMode::Detail;
5561 self.log_groups_state.log_streams.clear();
5562 self.log_groups_state.selected_stream = 0;
5563 self.log_groups_state.loading = true;
5564 self.update_current_tab_breadcrumb();
5565 } else if self.view_mode == ViewMode::Detail {
5566 let filtered_streams = self.filtered_log_streams();
5568 if let Some(selected_stream) =
5569 filtered_streams.get(self.log_groups_state.selected_stream)
5570 {
5571 if let Some(actual_idx) = self
5572 .log_groups_state
5573 .log_streams
5574 .iter()
5575 .position(|s| s.name == selected_stream.name)
5576 {
5577 self.log_groups_state.selected_stream = actual_idx;
5578 }
5579 }
5580 self.view_mode = ViewMode::Events;
5581 self.update_current_tab_breadcrumb();
5582 self.log_groups_state.log_events.clear();
5583 self.log_groups_state.event_scroll_offset = 0;
5584 self.log_groups_state.next_backward_token = None;
5585 self.log_groups_state.loading = true;
5586 } else if self.view_mode == ViewMode::Events {
5587 if self.log_groups_state.expanded_event
5589 == Some(self.log_groups_state.event_scroll_offset)
5590 {
5591 self.log_groups_state.expanded_event = None;
5592 } else {
5593 self.log_groups_state.expanded_event =
5594 Some(self.log_groups_state.event_scroll_offset);
5595 }
5596 }
5597 } else if self.current_service == Service::CloudWatchAlarms {
5598 self.alarms_state.table.toggle_expand();
5600 } else if self.current_service == Service::CloudWatchInsights {
5601 if !self.insights_state.insights.selected_log_groups.is_empty() {
5603 self.log_groups_state.loading = true;
5604 self.insights_state.insights.query_completed = true;
5605 }
5606 }
5607 }
5608 }
5609
5610 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
5611 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
5612 Ok(())
5613 }
5614
5615 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
5616 let alarms = self.alarms_client.list_alarms().await?;
5617 self.alarms_state.table.items = alarms
5618 .into_iter()
5619 .map(
5620 |(
5621 name,
5622 state,
5623 state_updated,
5624 description,
5625 metric_name,
5626 namespace,
5627 statistic,
5628 period,
5629 comparison,
5630 threshold,
5631 actions_enabled,
5632 state_reason,
5633 resource,
5634 dimensions,
5635 expression,
5636 alarm_type,
5637 cross_account,
5638 )| Alarm {
5639 name,
5640 state,
5641 state_updated_timestamp: state_updated,
5642 description,
5643 metric_name,
5644 namespace,
5645 statistic,
5646 period,
5647 comparison_operator: comparison,
5648 threshold,
5649 actions_enabled,
5650 state_reason,
5651 resource,
5652 dimensions,
5653 expression,
5654 alarm_type,
5655 cross_account,
5656 },
5657 )
5658 .collect();
5659 Ok(())
5660 }
5661
5662 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
5663 if let Some(bucket_name) = &self.s3_state.current_bucket {
5664 let bucket_region = if let Some(bucket) = self
5666 .s3_state
5667 .buckets
5668 .items
5669 .iter_mut()
5670 .find(|b| &b.name == bucket_name)
5671 {
5672 if bucket.region.is_empty() {
5673 let region = self.s3_client.get_bucket_location(bucket_name).await?;
5675 bucket.region = region.clone();
5676 region
5677 } else {
5678 bucket.region.clone()
5679 }
5680 } else {
5681 self.config.region.clone()
5682 };
5683
5684 let prefix = self
5685 .s3_state
5686 .prefix_stack
5687 .last()
5688 .cloned()
5689 .unwrap_or_default();
5690 let objects = self
5691 .s3_client
5692 .list_objects(bucket_name, &bucket_region, &prefix)
5693 .await?;
5694 self.s3_state.objects = objects
5695 .into_iter()
5696 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
5697 key,
5698 size,
5699 last_modified: modified,
5700 is_prefix,
5701 storage_class,
5702 })
5703 .collect();
5704 self.s3_state.selected_object = 0;
5705 }
5706 Ok(())
5707 }
5708
5709 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
5710 let bucket_region = self
5711 .s3_state
5712 .buckets
5713 .items
5714 .iter()
5715 .find(|b| b.name == bucket_name)
5716 .and_then(|b| {
5717 if b.region.is_empty() {
5718 None
5719 } else {
5720 Some(b.region.as_str())
5721 }
5722 })
5723 .unwrap_or(self.config.region.as_str());
5724 let objects = self
5725 .s3_client
5726 .list_objects(&bucket_name, bucket_region, "")
5727 .await?;
5728 let preview: Vec<S3Object> = objects
5729 .into_iter()
5730 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
5731 key,
5732 size,
5733 last_modified: modified,
5734 is_prefix,
5735 storage_class,
5736 })
5737 .collect();
5738 self.s3_state.bucket_preview.insert(bucket_name, preview);
5739 Ok(())
5740 }
5741
5742 pub async fn load_prefix_preview(
5743 &mut self,
5744 bucket_name: String,
5745 prefix: String,
5746 ) -> anyhow::Result<()> {
5747 let bucket_region = self
5748 .s3_state
5749 .buckets
5750 .items
5751 .iter()
5752 .find(|b| b.name == bucket_name)
5753 .and_then(|b| {
5754 if b.region.is_empty() {
5755 None
5756 } else {
5757 Some(b.region.as_str())
5758 }
5759 })
5760 .unwrap_or(self.config.region.as_str());
5761 let objects = self
5762 .s3_client
5763 .list_objects(&bucket_name, bucket_region, &prefix)
5764 .await?;
5765 let preview: Vec<S3Object> = objects
5766 .into_iter()
5767 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
5768 key,
5769 size,
5770 last_modified: modified,
5771 is_prefix,
5772 storage_class,
5773 })
5774 .collect();
5775 self.s3_state.prefix_preview.insert(prefix, preview);
5776 Ok(())
5777 }
5778
5779 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
5780 let repos = match self.ecr_state.tab {
5781 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
5782 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
5783 };
5784
5785 self.ecr_state.repositories.items = repos
5786 .into_iter()
5787 .map(|r| EcrRepository {
5788 name: r.name,
5789 uri: r.uri,
5790 created_at: r.created_at,
5791 tag_immutability: r.tag_immutability,
5792 encryption_type: r.encryption_type,
5793 })
5794 .collect();
5795
5796 self.ecr_state
5797 .repositories
5798 .items
5799 .sort_by(|a, b| a.name.cmp(&b.name));
5800 Ok(())
5801 }
5802
5803 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
5804 if let Some(repo_name) = &self.ecr_state.current_repository {
5805 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
5806 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
5807
5808 self.ecr_state.images.items = images
5809 .into_iter()
5810 .map(|i| EcrImage {
5811 tag: i.tag,
5812 artifact_type: i.artifact_type,
5813 pushed_at: i.pushed_at,
5814 size_bytes: i.size_bytes,
5815 uri: i.uri,
5816 digest: i.digest,
5817 last_pull_time: i.last_pull_time,
5818 })
5819 .collect();
5820
5821 self.ecr_state
5822 .images
5823 .items
5824 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
5825 }
5826 }
5827 Ok(())
5828 }
5829
5830 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
5831 let stacks = self
5832 .cloudformation_client
5833 .list_stacks(self.cfn_state.view_nested)
5834 .await?;
5835
5836 let mut stacks: Vec<CfnStack> = stacks
5837 .into_iter()
5838 .map(|s| CfnStack {
5839 name: s.name,
5840 stack_id: s.stack_id,
5841 status: s.status,
5842 created_time: s.created_time,
5843 updated_time: s.updated_time,
5844 deleted_time: s.deleted_time,
5845 drift_status: s.drift_status,
5846 last_drift_check_time: s.last_drift_check_time,
5847 status_reason: s.status_reason,
5848 description: s.description,
5849 detailed_status: String::new(),
5850 root_stack: String::new(),
5851 parent_stack: String::new(),
5852 termination_protection: false,
5853 iam_role: String::new(),
5854 tags: Vec::new(),
5855 stack_policy: String::new(),
5856 rollback_monitoring_time: String::new(),
5857 rollback_alarms: Vec::new(),
5858 notification_arns: Vec::new(),
5859 })
5860 .collect();
5861
5862 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
5864
5865 self.cfn_state.table.items = stacks;
5866
5867 Ok(())
5868 }
5869
5870 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
5871 let attached_policies = self
5873 .iam_client
5874 .list_attached_role_policies(role_name)
5875 .await
5876 .map_err(|e| anyhow::anyhow!(e))?;
5877
5878 let mut policies: Vec<crate::iam::Policy> = attached_policies
5879 .into_iter()
5880 .map(|p| crate::iam::Policy {
5881 policy_name: p.policy_name().unwrap_or("").to_string(),
5882 policy_type: "Managed".to_string(),
5883 attached_via: "Direct".to_string(),
5884 attached_entities: "-".to_string(),
5885 description: "-".to_string(),
5886 creation_time: "-".to_string(),
5887 edited_time: "-".to_string(),
5888 policy_arn: p.policy_arn().map(|s| s.to_string()),
5889 })
5890 .collect();
5891
5892 let inline_policy_names = self
5894 .iam_client
5895 .list_role_policies(role_name)
5896 .await
5897 .map_err(|e| anyhow::anyhow!(e))?;
5898
5899 for policy_name in inline_policy_names {
5900 policies.push(crate::iam::Policy {
5901 policy_name,
5902 policy_type: "Inline".to_string(),
5903 attached_via: "Direct".to_string(),
5904 attached_entities: "-".to_string(),
5905 description: "-".to_string(),
5906 creation_time: "-".to_string(),
5907 edited_time: "-".to_string(),
5908 policy_arn: None,
5909 });
5910 }
5911
5912 self.iam_state.policies.items = policies;
5913
5914 Ok(())
5915 }
5916
5917 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
5918 let attached_policies = self
5919 .iam_client
5920 .list_attached_group_policies(group_name)
5921 .await
5922 .map_err(|e| anyhow::anyhow!(e))?;
5923
5924 let mut policies: Vec<crate::iam::Policy> = attached_policies
5925 .into_iter()
5926 .map(|p| crate::iam::Policy {
5927 policy_name: p.policy_name().unwrap_or("").to_string(),
5928 policy_type: "AWS managed".to_string(),
5929 attached_via: "Direct".to_string(),
5930 attached_entities: "-".to_string(),
5931 description: "-".to_string(),
5932 creation_time: "-".to_string(),
5933 edited_time: "-".to_string(),
5934 policy_arn: p.policy_arn().map(|s| s.to_string()),
5935 })
5936 .collect();
5937
5938 let inline_policy_names = self
5939 .iam_client
5940 .list_group_policies(group_name)
5941 .await
5942 .map_err(|e| anyhow::anyhow!(e))?;
5943
5944 for policy_name in inline_policy_names {
5945 policies.push(crate::iam::Policy {
5946 policy_name,
5947 policy_type: "Inline".to_string(),
5948 attached_via: "Direct".to_string(),
5949 attached_entities: "-".to_string(),
5950 description: "-".to_string(),
5951 creation_time: "-".to_string(),
5952 edited_time: "-".to_string(),
5953 policy_arn: None,
5954 });
5955 }
5956
5957 self.iam_state.policies.items = policies;
5958
5959 Ok(())
5960 }
5961
5962 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
5963 let users = self
5964 .iam_client
5965 .get_group_users(group_name)
5966 .await
5967 .map_err(|e| anyhow::anyhow!(e))?;
5968
5969 let group_users: Vec<crate::iam::GroupUser> = users
5970 .into_iter()
5971 .map(|u| {
5972 let creation_time = {
5973 let dt = u.create_date();
5974 let timestamp = dt.secs();
5975 let datetime =
5976 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
5977 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
5978 };
5979
5980 crate::iam::GroupUser {
5981 user_name: u.user_name().to_string(),
5982 groups: String::new(),
5983 last_activity: String::new(),
5984 creation_time,
5985 }
5986 })
5987 .collect();
5988
5989 self.iam_state.group_users.items = group_users;
5990
5991 Ok(())
5992 }
5993
5994 pub async fn load_policy_document(
5995 &mut self,
5996 role_name: &str,
5997 policy_name: &str,
5998 ) -> anyhow::Result<()> {
5999 let policy = self
6001 .iam_state
6002 .policies
6003 .items
6004 .iter()
6005 .find(|p| p.policy_name == policy_name)
6006 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
6007
6008 let document = if let Some(policy_arn) = &policy.policy_arn {
6009 self.iam_client
6011 .get_policy_version(policy_arn)
6012 .await
6013 .map_err(|e| anyhow::anyhow!(e))?
6014 } else {
6015 self.iam_client
6017 .get_role_policy(role_name, policy_name)
6018 .await
6019 .map_err(|e| anyhow::anyhow!(e))?
6020 };
6021
6022 self.iam_state.policy_document = document;
6023
6024 Ok(())
6025 }
6026
6027 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
6028 let document = self
6029 .iam_client
6030 .get_role(role_name)
6031 .await
6032 .map_err(|e| anyhow::anyhow!(e))?;
6033
6034 self.iam_state.trust_policy_document = document;
6035
6036 Ok(())
6037 }
6038
6039 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
6040 self.iam_state.last_accessed_services.items = vec![];
6042 self.iam_state.last_accessed_services.selected = 0;
6043
6044 Ok(())
6045 }
6046
6047 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
6048 let tags = self
6049 .iam_client
6050 .list_role_tags(role_name)
6051 .await
6052 .map_err(|e| anyhow::anyhow!(e))?;
6053 self.iam_state.tags.items = tags
6054 .into_iter()
6055 .map(|(k, v)| crate::iam::RoleTag { key: k, value: v })
6056 .collect();
6057 self.iam_state.tags.reset();
6058 Ok(())
6059 }
6060
6061 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
6062 let tags = self
6063 .iam_client
6064 .list_user_tags(user_name)
6065 .await
6066 .map_err(|e| anyhow::anyhow!(e))?;
6067 self.iam_state.user_tags.items = tags
6068 .into_iter()
6069 .map(|(k, v)| crate::iam::UserTag { key: k, value: v })
6070 .collect();
6071 self.iam_state.user_tags.reset();
6072 Ok(())
6073 }
6074
6075 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
6076 if let Some(group) = self
6077 .log_groups_state
6078 .log_groups
6079 .items
6080 .get(self.log_groups_state.log_groups.selected)
6081 {
6082 self.log_groups_state.log_streams =
6083 self.cloudwatch_client.list_log_streams(&group.name).await?;
6084 self.log_groups_state.selected_stream = 0;
6085 }
6086 Ok(())
6087 }
6088
6089 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
6090 if let Some(group) = self
6091 .log_groups_state
6092 .log_groups
6093 .items
6094 .get(self.log_groups_state.log_groups.selected)
6095 {
6096 if let Some(stream) = self
6097 .log_groups_state
6098 .log_streams
6099 .get(self.log_groups_state.selected_stream)
6100 {
6101 let (start_time, end_time) =
6103 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
6104 let now = chrono::Utc::now().timestamp_millis();
6105 let duration_ms = match self.log_groups_state.relative_unit {
6106 crate::app::TimeUnit::Minutes => amount * 60 * 1000,
6107 crate::app::TimeUnit::Hours => amount * 60 * 60 * 1000,
6108 crate::app::TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6109 crate::app::TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6110 };
6111 (Some(now - duration_ms), Some(now))
6112 } else {
6113 (None, None)
6114 };
6115
6116 let (mut events, has_more, token) = self
6117 .cloudwatch_client
6118 .get_log_events(
6119 &group.name,
6120 &stream.name,
6121 self.log_groups_state.next_backward_token.clone(),
6122 start_time,
6123 end_time,
6124 )
6125 .await?;
6126
6127 if self.log_groups_state.next_backward_token.is_some() {
6128 events.append(&mut self.log_groups_state.log_events);
6130 self.log_groups_state.event_scroll_offset = 0;
6131 } else {
6132 self.log_groups_state.event_scroll_offset = 0;
6134 }
6135
6136 self.log_groups_state.log_events = events;
6137 self.log_groups_state.has_older_events =
6138 has_more && self.log_groups_state.log_events.len() >= 25;
6139 self.log_groups_state.next_backward_token = token;
6140 self.log_groups_state.selected_event = 0;
6141 }
6142 }
6143 Ok(())
6144 }
6145
6146 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
6147 if self.insights_state.insights.selected_log_groups.is_empty() {
6148 return Err(anyhow::anyhow!(
6149 "No log groups selected. Please select at least one log group."
6150 ));
6151 }
6152
6153 let now = chrono::Utc::now().timestamp_millis();
6154 let amount = self
6155 .insights_state
6156 .insights
6157 .insights_relative_amount
6158 .parse::<i64>()
6159 .unwrap_or(1);
6160 let duration_ms = match self.insights_state.insights.insights_relative_unit {
6161 TimeUnit::Minutes => amount * 60 * 1000,
6162 TimeUnit::Hours => amount * 60 * 60 * 1000,
6163 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6164 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6165 };
6166 let start_time = now - duration_ms;
6167
6168 let query_id = self
6169 .cloudwatch_client
6170 .start_query(
6171 self.insights_state.insights.selected_log_groups.clone(),
6172 self.insights_state.insights.query_text.trim().to_string(),
6173 start_time,
6174 now,
6175 )
6176 .await?;
6177
6178 for _ in 0..60 {
6180 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
6181 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
6182
6183 if status == "Complete" {
6184 self.insights_state.insights.query_results = results;
6185 self.insights_state.insights.query_completed = true;
6186 self.insights_state.insights.results_selected = 0;
6187 self.insights_state.insights.expanded_result = None;
6188 self.view_mode = ViewMode::InsightsResults;
6189 return Ok(());
6190 } else if status == "Failed" || status == "Cancelled" {
6191 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
6192 }
6193 }
6194
6195 Err(anyhow::anyhow!("Query timeout"))
6196 }
6197}
6198
6199impl CloudWatchInsightsState {
6200 fn new() -> Self {
6201 Self {
6202 insights: InsightsState::default(),
6203 loading: false,
6204 }
6205 }
6206}
6207
6208impl CloudWatchAlarmsState {
6209 fn new() -> Self {
6210 Self {
6211 table: crate::table::TableState::new(),
6212 alarm_tab: AlarmTab::AllAlarms,
6213 view_as: AlarmViewMode::Table,
6214 wrap_lines: false,
6215 sort_column: "Last state update".to_string(),
6216 sort_direction: SortDirection::Asc,
6217 input_focus: InputFocus::Filter,
6218 }
6219 }
6220}
6221
6222impl ServicePickerState {
6223 fn new() -> Self {
6224 Self {
6225 filter: String::new(),
6226 selected: 0,
6227 services: vec![
6228 "CloudWatch > Log Groups",
6229 "CloudWatch > Logs Insights",
6230 "CloudWatch > Alarms",
6231 "CloudFormation > Stacks",
6232 "ECR > Repositories",
6233 "IAM > Users",
6234 "IAM > Roles",
6235 "IAM > User Groups",
6236 "Lambda > Functions",
6237 "Lambda > Applications",
6238 "S3 > Buckets",
6239 ],
6240 }
6241 }
6242}
6243
6244#[cfg(test)]
6245mod test_helpers {
6246 use super::*;
6247
6248 #[allow(dead_code)]
6250 pub fn test_app() -> App {
6251 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
6252 }
6253
6254 #[allow(dead_code)]
6255 pub fn test_app_no_region() -> App {
6256 App::new_without_client("test".to_string(), None)
6257 }
6258
6259 #[allow(dead_code)]
6260 pub fn test_tab(service: Service) -> Tab {
6261 Tab {
6262 service,
6263 title: service.name().to_string(),
6264 breadcrumb: service.name().to_string(),
6265 }
6266 }
6267
6268 #[allow(dead_code)]
6269 pub fn test_iam_role(name: &str) -> crate::iam::IamRole {
6270 crate::iam::IamRole {
6271 role_name: name.to_string(),
6272 path: "/".to_string(),
6273 description: format!("Test role {}", name),
6274 trusted_entities: "AWS Service: ec2.amazonaws.com".to_string(),
6275 creation_time: "2024-01-01 00:00:00".to_string(),
6276 arn: format!("arn:aws:iam::123456789012:role/{}", name),
6277 max_session_duration: "3600 seconds".to_string(),
6278 last_activity: "-".to_string(),
6279 }
6280 }
6281}
6282
6283#[cfg(test)]
6284mod tests {
6285 use super::*;
6286 use crate::keymap::Action;
6287 use test_helpers::*;
6288
6289 #[test]
6290 fn test_next_tab_cycles_forward() {
6291 let mut app = test_app();
6292 app.tabs = vec![
6293 Tab {
6294 service: Service::CloudWatchLogGroups,
6295 title: "CloudWatch > Log Groups".to_string(),
6296 breadcrumb: "CloudWatch > Log Groups".to_string(),
6297 },
6298 Tab {
6299 service: Service::CloudWatchInsights,
6300 title: "CloudWatch > Logs Insights".to_string(),
6301 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6302 },
6303 Tab {
6304 service: Service::CloudWatchAlarms,
6305 title: "CloudWatch > Alarms".to_string(),
6306 breadcrumb: "CloudWatch > Alarms".to_string(),
6307 },
6308 ];
6309 app.current_tab = 0;
6310
6311 app.handle_action(Action::NextTab);
6312 assert_eq!(app.current_tab, 1);
6313 assert_eq!(app.current_service, Service::CloudWatchInsights);
6314
6315 app.handle_action(Action::NextTab);
6316 assert_eq!(app.current_tab, 2);
6317 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6318
6319 app.handle_action(Action::NextTab);
6321 assert_eq!(app.current_tab, 0);
6322 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6323 }
6324
6325 #[test]
6326 fn test_prev_tab_cycles_backward() {
6327 let mut app = test_app();
6328 app.tabs = vec![
6329 Tab {
6330 service: Service::CloudWatchLogGroups,
6331 title: "CloudWatch > Log Groups".to_string(),
6332 breadcrumb: "CloudWatch > Log Groups".to_string(),
6333 },
6334 Tab {
6335 service: Service::CloudWatchInsights,
6336 title: "CloudWatch > Logs Insights".to_string(),
6337 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6338 },
6339 Tab {
6340 service: Service::CloudWatchAlarms,
6341 title: "CloudWatch > Alarms".to_string(),
6342 breadcrumb: "CloudWatch > Alarms".to_string(),
6343 },
6344 ];
6345 app.current_tab = 2;
6346
6347 app.handle_action(Action::PrevTab);
6348 assert_eq!(app.current_tab, 1);
6349 assert_eq!(app.current_service, Service::CloudWatchInsights);
6350
6351 app.handle_action(Action::PrevTab);
6352 assert_eq!(app.current_tab, 0);
6353 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6354
6355 app.handle_action(Action::PrevTab);
6357 assert_eq!(app.current_tab, 2);
6358 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6359 }
6360
6361 #[test]
6362 fn test_close_tab_removes_current() {
6363 let mut app = test_app();
6364 app.tabs = vec![
6365 Tab {
6366 service: Service::CloudWatchLogGroups,
6367 title: "CloudWatch > Log Groups".to_string(),
6368 breadcrumb: "CloudWatch > Log Groups".to_string(),
6369 },
6370 Tab {
6371 service: Service::CloudWatchInsights,
6372 title: "CloudWatch > Logs Insights".to_string(),
6373 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6374 },
6375 Tab {
6376 service: Service::CloudWatchAlarms,
6377 title: "CloudWatch > Alarms".to_string(),
6378 breadcrumb: "CloudWatch > Alarms".to_string(),
6379 },
6380 ];
6381 app.current_tab = 1;
6382 app.service_selected = true;
6383
6384 app.handle_action(Action::CloseTab);
6385 assert_eq!(app.tabs.len(), 2);
6386 assert_eq!(app.current_tab, 1);
6387 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6388 }
6389
6390 #[test]
6391 fn test_close_last_tab_exits_service() {
6392 let mut app = test_app();
6393 app.tabs = vec![Tab {
6394 service: Service::CloudWatchLogGroups,
6395 title: "CloudWatch > Log Groups".to_string(),
6396 breadcrumb: "CloudWatch > Log Groups".to_string(),
6397 }];
6398 app.current_tab = 0;
6399 app.service_selected = true;
6400
6401 app.handle_action(Action::CloseTab);
6402 assert_eq!(app.tabs.len(), 0);
6403 assert!(!app.service_selected);
6404 assert_eq!(app.current_tab, 0);
6405 }
6406
6407 #[test]
6408 fn test_close_service_removes_current_tab() {
6409 let mut app = test_app();
6410 app.tabs = vec![
6411 Tab {
6412 service: Service::CloudWatchLogGroups,
6413 title: "CloudWatch > Log Groups".to_string(),
6414 breadcrumb: "CloudWatch > Log Groups".to_string(),
6415 },
6416 Tab {
6417 service: Service::CloudWatchInsights,
6418 title: "CloudWatch > Logs Insights".to_string(),
6419 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6420 },
6421 Tab {
6422 service: Service::CloudWatchAlarms,
6423 title: "CloudWatch > Alarms".to_string(),
6424 breadcrumb: "CloudWatch > Alarms".to_string(),
6425 },
6426 ];
6427 app.current_tab = 1;
6428 app.service_selected = true;
6429
6430 app.handle_action(Action::CloseService);
6431
6432 assert_eq!(app.tabs.len(), 2);
6434 assert_eq!(app.current_tab, 1);
6436 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6437 assert!(app.service_selected);
6439 assert_eq!(app.mode, Mode::Normal);
6440 }
6441
6442 #[test]
6443 fn test_close_service_last_tab_shows_picker() {
6444 let mut app = test_app();
6445 app.tabs = vec![Tab {
6446 service: Service::CloudWatchLogGroups,
6447 title: "CloudWatch > Log Groups".to_string(),
6448 breadcrumb: "CloudWatch > Log Groups".to_string(),
6449 }];
6450 app.current_tab = 0;
6451 app.service_selected = true;
6452
6453 app.handle_action(Action::CloseService);
6454
6455 assert_eq!(app.tabs.len(), 0);
6457 assert!(!app.service_selected);
6459 assert_eq!(app.mode, Mode::ServicePicker);
6460 }
6461
6462 #[test]
6463 fn test_open_tab_picker_with_tabs() {
6464 let mut app = test_app();
6465 app.tabs = vec![
6466 Tab {
6467 service: Service::CloudWatchLogGroups,
6468 title: "CloudWatch > Log Groups".to_string(),
6469 breadcrumb: "CloudWatch > Log Groups".to_string(),
6470 },
6471 Tab {
6472 service: Service::CloudWatchInsights,
6473 title: "CloudWatch > Logs Insights".to_string(),
6474 breadcrumb: "CloudWatch > Logs Insights".to_string(),
6475 },
6476 ];
6477 app.current_tab = 1;
6478
6479 app.handle_action(Action::OpenTabPicker);
6480 assert_eq!(app.mode, Mode::TabPicker);
6481 assert_eq!(app.tab_picker_selected, 1);
6482 }
6483
6484 #[test]
6485 fn test_open_tab_picker_without_tabs() {
6486 let mut app = test_app();
6487 app.tabs = vec![];
6488
6489 app.handle_action(Action::OpenTabPicker);
6490 assert_eq!(app.mode, Mode::Normal);
6491 }
6492
6493 #[test]
6494 fn test_pending_key_state() {
6495 let mut app = test_app();
6496 assert_eq!(app.pending_key, None);
6497
6498 app.pending_key = Some('g');
6499 assert_eq!(app.pending_key, Some('g'));
6500 }
6501
6502 #[test]
6503 fn test_tab_breadcrumb_updates() {
6504 let mut app = test_app();
6505 app.tabs = vec![Tab {
6506 service: Service::CloudWatchLogGroups,
6507 title: "CloudWatch > Log Groups".to_string(),
6508 breadcrumb: "CloudWatch > Log groups".to_string(),
6509 }];
6510 app.current_tab = 0;
6511 app.service_selected = true;
6512 app.current_service = Service::CloudWatchLogGroups;
6513
6514 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
6516
6517 app.log_groups_state
6519 .log_groups
6520 .items
6521 .push(rusticity_core::LogGroup {
6522 name: "/aws/lambda/test".to_string(),
6523 creation_time: None,
6524 stored_bytes: Some(1024),
6525 retention_days: None,
6526 log_class: None,
6527 arn: None,
6528 });
6529 app.log_groups_state.log_groups.reset();
6530 app.view_mode = ViewMode::Detail;
6531 app.update_current_tab_breadcrumb();
6532
6533 assert_eq!(
6535 app.tabs[0].breadcrumb,
6536 "CloudWatch > Log groups > /aws/lambda/test"
6537 );
6538 }
6539
6540 #[test]
6541 fn test_s3_bucket_column_selector_navigation() {
6542 let mut app = test_app();
6543 app.current_service = Service::S3Buckets;
6544 app.mode = Mode::ColumnSelector;
6545 app.column_selector_index = 0;
6546
6547 app.handle_action(Action::NextItem);
6549 assert_eq!(app.column_selector_index, 1);
6550
6551 app.handle_action(Action::NextItem);
6552 assert_eq!(app.column_selector_index, 2);
6553
6554 app.handle_action(Action::NextItem);
6556 assert_eq!(app.column_selector_index, 2);
6557
6558 app.handle_action(Action::PrevItem);
6560 assert_eq!(app.column_selector_index, 1);
6561
6562 app.handle_action(Action::PrevItem);
6563 assert_eq!(app.column_selector_index, 0);
6564
6565 app.handle_action(Action::PrevItem);
6567 assert_eq!(app.column_selector_index, 0);
6568 }
6569
6570 #[test]
6571 fn test_cloudwatch_alarms_state_initialized() {
6572 let app = test_app();
6573
6574 assert_eq!(app.alarms_state.table.items.len(), 0);
6576 assert_eq!(app.alarms_state.table.selected, 0);
6577 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
6578 assert!(!app.alarms_state.table.loading);
6579 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
6580 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
6581 }
6582
6583 #[test]
6584 fn test_cloudwatch_alarms_service_selection() {
6585 let mut app = test_app();
6586
6587 app.current_service = Service::CloudWatchAlarms;
6589 app.service_selected = true;
6590
6591 assert_eq!(app.current_service, Service::CloudWatchAlarms);
6592 assert!(app.service_selected);
6593 }
6594
6595 #[test]
6596 fn test_cloudwatch_alarms_column_preferences() {
6597 let app = test_app();
6598
6599 assert!(!app.all_alarm_columns.is_empty());
6601 assert!(!app.visible_alarm_columns.is_empty());
6602
6603 assert!(app.visible_alarm_columns.contains(&AlarmColumn::Name));
6605 assert!(app.visible_alarm_columns.contains(&AlarmColumn::State));
6606 }
6607
6608 #[test]
6609 fn test_s3_bucket_navigation_without_expansion() {
6610 let mut app = test_app();
6611 app.current_service = Service::S3Buckets;
6612 app.service_selected = true;
6613 app.mode = Mode::Normal;
6614
6615 app.s3_state.buckets.items = vec![
6617 S3Bucket {
6618 name: "bucket1".to_string(),
6619 region: "us-east-1".to_string(),
6620 creation_date: "2024-01-01T00:00:00Z".to_string(),
6621 },
6622 S3Bucket {
6623 name: "bucket2".to_string(),
6624 region: "us-east-1".to_string(),
6625 creation_date: "2024-01-02T00:00:00Z".to_string(),
6626 },
6627 S3Bucket {
6628 name: "bucket3".to_string(),
6629 region: "us-east-1".to_string(),
6630 creation_date: "2024-01-03T00:00:00Z".to_string(),
6631 },
6632 ];
6633 app.s3_state.selected_row = 0;
6634
6635 app.handle_action(Action::NextItem);
6637 assert_eq!(app.s3_state.selected_row, 1);
6638
6639 app.handle_action(Action::NextItem);
6640 assert_eq!(app.s3_state.selected_row, 2);
6641
6642 app.handle_action(Action::NextItem);
6644 assert_eq!(app.s3_state.selected_row, 2);
6645
6646 app.handle_action(Action::PrevItem);
6648 assert_eq!(app.s3_state.selected_row, 1);
6649
6650 app.handle_action(Action::PrevItem);
6651 assert_eq!(app.s3_state.selected_row, 0);
6652
6653 app.handle_action(Action::PrevItem);
6655 assert_eq!(app.s3_state.selected_row, 0);
6656 }
6657
6658 #[test]
6659 fn test_s3_bucket_navigation_with_expansion() {
6660 let mut app = test_app();
6661 app.current_service = Service::S3Buckets;
6662 app.service_selected = true;
6663 app.mode = Mode::Normal;
6664
6665 app.s3_state.buckets.items = vec![
6667 S3Bucket {
6668 name: "bucket1".to_string(),
6669 region: "us-east-1".to_string(),
6670 creation_date: "2024-01-01T00:00:00Z".to_string(),
6671 },
6672 S3Bucket {
6673 name: "bucket2".to_string(),
6674 region: "us-east-1".to_string(),
6675 creation_date: "2024-01-02T00:00:00Z".to_string(),
6676 },
6677 ];
6678
6679 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6681 app.s3_state.bucket_preview.insert(
6682 "bucket1".to_string(),
6683 vec![
6684 S3Object {
6685 key: "file1.txt".to_string(),
6686 size: 100,
6687 last_modified: "2024-01-01T00:00:00Z".to_string(),
6688 is_prefix: false,
6689 storage_class: "STANDARD".to_string(),
6690 },
6691 S3Object {
6692 key: "folder/".to_string(),
6693 size: 0,
6694 last_modified: "2024-01-01T00:00:00Z".to_string(),
6695 is_prefix: true,
6696 storage_class: String::new(),
6697 },
6698 ],
6699 );
6700
6701 app.s3_state.selected_row = 0;
6702
6703 app.handle_action(Action::NextItem);
6706 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
6709 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
6712 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
6716 assert_eq!(app.s3_state.selected_row, 3);
6717 }
6718
6719 #[test]
6720 fn test_s3_bucket_navigation_with_nested_expansion() {
6721 let mut app = test_app();
6722 app.current_service = Service::S3Buckets;
6723 app.service_selected = true;
6724 app.mode = Mode::Normal;
6725
6726 app.s3_state.buckets.items = vec![S3Bucket {
6728 name: "bucket1".to_string(),
6729 region: "us-east-1".to_string(),
6730 creation_date: "2024-01-01T00:00:00Z".to_string(),
6731 }];
6732
6733 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6735 app.s3_state.bucket_preview.insert(
6736 "bucket1".to_string(),
6737 vec![S3Object {
6738 key: "folder/".to_string(),
6739 size: 0,
6740 last_modified: "2024-01-01T00:00:00Z".to_string(),
6741 is_prefix: true,
6742 storage_class: String::new(),
6743 }],
6744 );
6745
6746 app.s3_state.expanded_prefixes.insert("folder/".to_string());
6748 app.s3_state.prefix_preview.insert(
6749 "folder/".to_string(),
6750 vec![
6751 S3Object {
6752 key: "folder/file1.txt".to_string(),
6753 size: 100,
6754 last_modified: "2024-01-01T00:00:00Z".to_string(),
6755 is_prefix: false,
6756 storage_class: "STANDARD".to_string(),
6757 },
6758 S3Object {
6759 key: "folder/file2.txt".to_string(),
6760 size: 200,
6761 last_modified: "2024-01-01T00:00:00Z".to_string(),
6762 is_prefix: false,
6763 storage_class: "STANDARD".to_string(),
6764 },
6765 ],
6766 );
6767
6768 app.s3_state.selected_row = 0;
6769
6770 app.handle_action(Action::NextItem);
6772 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
6775 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
6778 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
6782 assert_eq!(app.s3_state.selected_row, 3);
6783 }
6784
6785 #[test]
6786 fn test_calculate_total_bucket_rows() {
6787 let mut app = test_app();
6788
6789 assert_eq!(app.calculate_total_bucket_rows(), 0);
6791
6792 app.s3_state.buckets.items = vec![
6794 S3Bucket {
6795 name: "bucket1".to_string(),
6796 region: "us-east-1".to_string(),
6797 creation_date: "2024-01-01T00:00:00Z".to_string(),
6798 },
6799 S3Bucket {
6800 name: "bucket2".to_string(),
6801 region: "us-east-1".to_string(),
6802 creation_date: "2024-01-02T00:00:00Z".to_string(),
6803 },
6804 ];
6805 assert_eq!(app.calculate_total_bucket_rows(), 2);
6806
6807 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6809 app.s3_state.bucket_preview.insert(
6810 "bucket1".to_string(),
6811 vec![
6812 S3Object {
6813 key: "file1.txt".to_string(),
6814 size: 100,
6815 last_modified: "2024-01-01T00:00:00Z".to_string(),
6816 is_prefix: false,
6817 storage_class: "STANDARD".to_string(),
6818 },
6819 S3Object {
6820 key: "file2.txt".to_string(),
6821 size: 200,
6822 last_modified: "2024-01-01T00:00:00Z".to_string(),
6823 is_prefix: false,
6824 storage_class: "STANDARD".to_string(),
6825 },
6826 S3Object {
6827 key: "folder/".to_string(),
6828 size: 0,
6829 last_modified: "2024-01-01T00:00:00Z".to_string(),
6830 is_prefix: true,
6831 storage_class: String::new(),
6832 },
6833 ],
6834 );
6835 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
6839 app.s3_state.prefix_preview.insert(
6840 "folder/".to_string(),
6841 vec![
6842 S3Object {
6843 key: "folder/nested1.txt".to_string(),
6844 size: 50,
6845 last_modified: "2024-01-01T00:00:00Z".to_string(),
6846 is_prefix: false,
6847 storage_class: "STANDARD".to_string(),
6848 },
6849 S3Object {
6850 key: "folder/nested2.txt".to_string(),
6851 size: 75,
6852 last_modified: "2024-01-01T00:00:00Z".to_string(),
6853 is_prefix: false,
6854 storage_class: "STANDARD".to_string(),
6855 },
6856 ],
6857 );
6858 assert_eq!(app.calculate_total_bucket_rows(), 7); }
6860
6861 #[test]
6862 fn test_calculate_total_object_rows() {
6863 let mut app = test_app();
6864 app.s3_state.current_bucket = Some("test-bucket".to_string());
6865
6866 assert_eq!(app.calculate_total_object_rows(), 0);
6868
6869 app.s3_state.objects = vec![
6871 S3Object {
6872 key: "file1.txt".to_string(),
6873 size: 100,
6874 last_modified: "2024-01-01T00:00:00Z".to_string(),
6875 is_prefix: false,
6876 storage_class: "STANDARD".to_string(),
6877 },
6878 S3Object {
6879 key: "folder/".to_string(),
6880 size: 0,
6881 last_modified: "2024-01-01T00:00:00Z".to_string(),
6882 is_prefix: true,
6883 storage_class: String::new(),
6884 },
6885 ];
6886 assert_eq!(app.calculate_total_object_rows(), 2);
6887
6888 app.s3_state.expanded_prefixes.insert("folder/".to_string());
6890 app.s3_state.prefix_preview.insert(
6891 "folder/".to_string(),
6892 vec![
6893 S3Object {
6894 key: "folder/file2.txt".to_string(),
6895 size: 200,
6896 last_modified: "2024-01-01T00:00:00Z".to_string(),
6897 is_prefix: false,
6898 storage_class: "STANDARD".to_string(),
6899 },
6900 S3Object {
6901 key: "folder/subfolder/".to_string(),
6902 size: 0,
6903 last_modified: "2024-01-01T00:00:00Z".to_string(),
6904 is_prefix: true,
6905 storage_class: String::new(),
6906 },
6907 ],
6908 );
6909 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
6913 .expanded_prefixes
6914 .insert("folder/subfolder/".to_string());
6915 app.s3_state.prefix_preview.insert(
6916 "folder/subfolder/".to_string(),
6917 vec![S3Object {
6918 key: "folder/subfolder/deep.txt".to_string(),
6919 size: 50,
6920 last_modified: "2024-01-01T00:00:00Z".to_string(),
6921 is_prefix: false,
6922 storage_class: "STANDARD".to_string(),
6923 }],
6924 );
6925 assert_eq!(app.calculate_total_object_rows(), 5); }
6927
6928 #[test]
6929 fn test_s3_object_navigation_with_deep_nesting() {
6930 let mut app = test_app();
6931 app.current_service = Service::S3Buckets;
6932 app.service_selected = true;
6933 app.mode = Mode::Normal;
6934 app.s3_state.current_bucket = Some("test-bucket".to_string());
6935
6936 app.s3_state.objects = vec![S3Object {
6938 key: "folder1/".to_string(),
6939 size: 0,
6940 last_modified: "2024-01-01T00:00:00Z".to_string(),
6941 is_prefix: true,
6942 storage_class: String::new(),
6943 }];
6944
6945 app.s3_state
6947 .expanded_prefixes
6948 .insert("folder1/".to_string());
6949 app.s3_state.prefix_preview.insert(
6950 "folder1/".to_string(),
6951 vec![S3Object {
6952 key: "folder1/folder2/".to_string(),
6953 size: 0,
6954 last_modified: "2024-01-01T00:00:00Z".to_string(),
6955 is_prefix: true,
6956 storage_class: String::new(),
6957 }],
6958 );
6959
6960 app.s3_state
6962 .expanded_prefixes
6963 .insert("folder1/folder2/".to_string());
6964 app.s3_state.prefix_preview.insert(
6965 "folder1/folder2/".to_string(),
6966 vec![S3Object {
6967 key: "folder1/folder2/file.txt".to_string(),
6968 size: 100,
6969 last_modified: "2024-01-01T00:00:00Z".to_string(),
6970 is_prefix: false,
6971 storage_class: "STANDARD".to_string(),
6972 }],
6973 );
6974
6975 app.s3_state.selected_object = 0;
6976
6977 app.handle_action(Action::NextItem);
6979 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
6982 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
6986 assert_eq!(app.s3_state.selected_object, 2);
6987 }
6988
6989 #[test]
6990 fn test_s3_expand_nested_folder_in_objects_view() {
6991 let mut app = test_app();
6992 app.current_service = Service::S3Buckets;
6993 app.service_selected = true;
6994 app.mode = Mode::Normal;
6995 app.s3_state.current_bucket = Some("test-bucket".to_string());
6996
6997 app.s3_state.objects = vec![S3Object {
6999 key: "parent/".to_string(),
7000 size: 0,
7001 last_modified: "2024-01-01T00:00:00Z".to_string(),
7002 is_prefix: true,
7003 storage_class: String::new(),
7004 }];
7005
7006 app.s3_state.expanded_prefixes.insert("parent/".to_string());
7008 app.s3_state.prefix_preview.insert(
7009 "parent/".to_string(),
7010 vec![S3Object {
7011 key: "parent/child/".to_string(),
7012 size: 0,
7013 last_modified: "2024-01-01T00:00:00Z".to_string(),
7014 is_prefix: true,
7015 storage_class: String::new(),
7016 }],
7017 );
7018
7019 app.s3_state.selected_object = 1;
7021
7022 app.handle_action(Action::NextPane);
7024
7025 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
7027 assert!(app.s3_state.buckets.loading); }
7029
7030 #[test]
7031 fn test_s3_drill_into_nested_folder() {
7032 let mut app = test_app();
7033 app.current_service = Service::S3Buckets;
7034 app.service_selected = true;
7035 app.mode = Mode::Normal;
7036 app.s3_state.current_bucket = Some("test-bucket".to_string());
7037
7038 app.s3_state.objects = vec![S3Object {
7040 key: "parent/".to_string(),
7041 size: 0,
7042 last_modified: "2024-01-01T00:00:00Z".to_string(),
7043 is_prefix: true,
7044 storage_class: String::new(),
7045 }];
7046
7047 app.s3_state.expanded_prefixes.insert("parent/".to_string());
7049 app.s3_state.prefix_preview.insert(
7050 "parent/".to_string(),
7051 vec![S3Object {
7052 key: "parent/child/".to_string(),
7053 size: 0,
7054 last_modified: "2024-01-01T00:00:00Z".to_string(),
7055 is_prefix: true,
7056 storage_class: String::new(),
7057 }],
7058 );
7059
7060 app.s3_state.selected_object = 1;
7062
7063 app.handle_action(Action::Select);
7065
7066 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
7068 assert!(app.s3_state.buckets.loading); }
7070
7071 #[test]
7072 fn test_s3_esc_pops_navigation_stack() {
7073 let mut app = test_app();
7074 app.current_service = Service::S3Buckets;
7075 app.s3_state.current_bucket = Some("test-bucket".to_string());
7076 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
7077
7078 app.handle_action(Action::GoBack);
7080 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
7081 assert!(app.s3_state.buckets.loading);
7082
7083 app.s3_state.buckets.loading = false;
7085 app.handle_action(Action::GoBack);
7086 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7087 assert!(app.s3_state.buckets.loading);
7088
7089 app.s3_state.buckets.loading = false;
7091 app.handle_action(Action::GoBack);
7092 assert_eq!(app.s3_state.current_bucket, None);
7093 }
7094
7095 #[test]
7096 fn test_s3_esc_from_bucket_root_exits() {
7097 let mut app = test_app();
7098 app.current_service = Service::S3Buckets;
7099 app.s3_state.current_bucket = Some("test-bucket".to_string());
7100 app.s3_state.prefix_stack = vec![];
7101
7102 app.handle_action(Action::GoBack);
7104 assert_eq!(app.s3_state.current_bucket, None);
7105 assert_eq!(app.s3_state.objects.len(), 0);
7106 }
7107
7108 #[test]
7109 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
7110 let mut app = test_app();
7111 app.current_service = Service::S3Buckets;
7112 app.service_selected = true;
7113 app.mode = Mode::Normal;
7114
7115 app.s3_state.buckets.items = vec![S3Bucket {
7117 name: "test-bucket".to_string(),
7118 region: "us-east-1".to_string(),
7119 creation_date: "2024-01-01".to_string(),
7120 }];
7121
7122 app.s3_state
7124 .expanded_prefixes
7125 .insert("test-bucket".to_string());
7126 app.s3_state.bucket_preview.insert(
7127 "test-bucket".to_string(),
7128 vec![S3Object {
7129 key: "parent/".to_string(),
7130 size: 0,
7131 last_modified: "2024-01-01".to_string(),
7132 is_prefix: true,
7133 storage_class: String::new(),
7134 }],
7135 );
7136
7137 app.s3_state.expanded_prefixes.insert("parent/".to_string());
7139 app.s3_state.prefix_preview.insert(
7140 "parent/".to_string(),
7141 vec![S3Object {
7142 key: "parent/child/".to_string(),
7143 size: 0,
7144 last_modified: "2024-01-01".to_string(),
7145 is_prefix: true,
7146 storage_class: String::new(),
7147 }],
7148 );
7149
7150 app.s3_state.selected_row = 2;
7152
7153 app.handle_action(Action::Select);
7155
7156 assert_eq!(
7158 app.s3_state.prefix_stack,
7159 vec!["parent/".to_string(), "parent/child/".to_string()]
7160 );
7161 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
7162 assert!(app.s3_state.buckets.loading);
7163
7164 app.s3_state.buckets.loading = false;
7166 app.handle_action(Action::GoBack);
7167 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
7168 assert!(app.s3_state.buckets.loading);
7169
7170 app.s3_state.buckets.loading = false;
7172 app.handle_action(Action::GoBack);
7173 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7174 assert!(app.s3_state.buckets.loading);
7175
7176 app.s3_state.buckets.loading = false;
7178 app.handle_action(Action::GoBack);
7179 assert_eq!(app.s3_state.current_bucket, None);
7180 }
7181
7182 #[test]
7183 fn test_region_picker_fuzzy_filter() {
7184 let mut app = test_app();
7185 app.region_latencies.insert("us-east-1".to_string(), 10);
7186 app.region_filter = "vir".to_string();
7187 let filtered = app.get_filtered_regions();
7188 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
7189 }
7190
7191 #[test]
7192 fn test_profile_picker_loads_profiles() {
7193 let profiles = App::load_aws_profiles();
7194 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
7196 }
7197
7198 #[test]
7199 fn test_profile_with_region_uses_it() {
7200 let mut app = test_app_no_region();
7201 app.available_profiles = vec![AwsProfile {
7202 name: "test-profile".to_string(),
7203 region: Some("eu-west-1".to_string()),
7204 account: Some("123456789".to_string()),
7205 role_arn: None,
7206 source_profile: None,
7207 }];
7208 app.profile_picker_selected = 0;
7209 app.mode = Mode::ProfilePicker;
7210
7211 let filtered = app.get_filtered_profiles();
7213 if let Some(profile) = filtered.first() {
7214 let profile_name = profile.name.clone();
7215 let profile_region = profile.region.clone();
7216
7217 app.profile = profile_name;
7218 if let Some(region) = profile_region {
7219 app.region = region;
7220 }
7221 }
7222
7223 assert_eq!(app.profile, "test-profile");
7224 assert_eq!(app.region, "eu-west-1");
7225 }
7226
7227 #[test]
7228 fn test_profile_without_region_keeps_unknown() {
7229 let mut app = test_app_no_region();
7230 let initial_region = app.region.clone();
7231
7232 app.available_profiles = vec![AwsProfile {
7233 name: "test-profile".to_string(),
7234 region: None,
7235 account: None,
7236 role_arn: None,
7237 source_profile: None,
7238 }];
7239 app.profile_picker_selected = 0;
7240 app.mode = Mode::ProfilePicker;
7241
7242 let filtered = app.get_filtered_profiles();
7243 if let Some(profile) = filtered.first() {
7244 let profile_name = profile.name.clone();
7245 let profile_region = profile.region.clone();
7246
7247 app.profile = profile_name;
7248 if let Some(region) = profile_region {
7249 app.region = region;
7250 }
7251 }
7252
7253 assert_eq!(app.profile, "test-profile");
7254 assert_eq!(app.region, initial_region); }
7256
7257 #[test]
7258 fn test_region_selection_closes_all_tabs() {
7259 let mut app = test_app();
7260
7261 app.tabs.push(Tab {
7263 service: Service::CloudWatchLogGroups,
7264 title: "CloudWatch".to_string(),
7265 breadcrumb: "CloudWatch".to_string(),
7266 });
7267 app.tabs.push(Tab {
7268 service: Service::S3Buckets,
7269 title: "S3".to_string(),
7270 breadcrumb: "S3".to_string(),
7271 });
7272 app.service_selected = true;
7273 app.current_tab = 1;
7274
7275 app.region_latencies.insert("eu-west-1".to_string(), 50);
7277
7278 app.mode = Mode::RegionPicker;
7280 app.region_picker_selected = 0;
7281
7282 let filtered = app.get_filtered_regions();
7283 if let Some(region) = filtered.first() {
7284 app.region = region.code.to_string();
7285 app.tabs.clear();
7286 app.current_tab = 0;
7287 app.service_selected = false;
7288 app.mode = Mode::Normal;
7289 }
7290
7291 assert_eq!(app.tabs.len(), 0);
7292 assert_eq!(app.current_tab, 0);
7293 assert!(!app.service_selected);
7294 assert_eq!(app.region, "eu-west-1");
7295 }
7296
7297 #[test]
7298 fn test_region_picker_can_be_closed_without_selection() {
7299 let mut app = test_app();
7300 let initial_region = app.region.clone();
7301
7302 app.mode = Mode::RegionPicker;
7303
7304 app.mode = Mode::Normal;
7306
7307 assert_eq!(app.region, initial_region);
7309 }
7310
7311 #[test]
7312 fn test_session_filter_works() {
7313 let mut app = test_app();
7314
7315 app.sessions = vec![
7316 crate::session::Session {
7317 id: "1".to_string(),
7318 timestamp: "2024-01-01".to_string(),
7319 profile: "prod-profile".to_string(),
7320 region: "us-east-1".to_string(),
7321 account_id: "123456789".to_string(),
7322 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7323 tabs: vec![],
7324 },
7325 crate::session::Session {
7326 id: "2".to_string(),
7327 timestamp: "2024-01-02".to_string(),
7328 profile: "dev-profile".to_string(),
7329 region: "eu-west-1".to_string(),
7330 account_id: "987654321".to_string(),
7331 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
7332 tabs: vec![],
7333 },
7334 ];
7335
7336 app.session_filter = "prod".to_string();
7338 let filtered = app.get_filtered_sessions();
7339 assert_eq!(filtered.len(), 1);
7340 assert_eq!(filtered[0].profile, "prod-profile");
7341
7342 app.session_filter = "eu".to_string();
7344 let filtered = app.get_filtered_sessions();
7345 assert_eq!(filtered.len(), 1);
7346 assert_eq!(filtered[0].region, "eu-west-1");
7347
7348 app.session_filter.clear();
7350 let filtered = app.get_filtered_sessions();
7351 assert_eq!(filtered.len(), 2);
7352 }
7353
7354 #[test]
7355 fn test_profile_picker_shows_account() {
7356 let mut app = test_app_no_region();
7357 app.available_profiles = vec![AwsProfile {
7358 name: "test-profile".to_string(),
7359 region: Some("us-east-1".to_string()),
7360 account: Some("123456789".to_string()),
7361 role_arn: None,
7362 source_profile: None,
7363 }];
7364
7365 let filtered = app.get_filtered_profiles();
7366 assert_eq!(filtered.len(), 1);
7367 assert_eq!(filtered[0].account, Some("123456789".to_string()));
7368 }
7369
7370 #[test]
7371 fn test_profile_without_account() {
7372 let mut app = test_app_no_region();
7373 app.available_profiles = vec![AwsProfile {
7374 name: "test-profile".to_string(),
7375 region: Some("us-east-1".to_string()),
7376 account: None,
7377 role_arn: None,
7378 source_profile: None,
7379 }];
7380
7381 let filtered = app.get_filtered_profiles();
7382 assert_eq!(filtered.len(), 1);
7383 assert_eq!(filtered[0].account, None);
7384 }
7385
7386 #[test]
7387 fn test_profile_with_all_fields() {
7388 let mut app = test_app_no_region();
7389 app.available_profiles = vec![AwsProfile {
7390 name: "prod-profile".to_string(),
7391 region: Some("us-west-2".to_string()),
7392 account: Some("123456789".to_string()),
7393 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
7394 source_profile: Some("base-profile".to_string()),
7395 }];
7396
7397 let filtered = app.get_filtered_profiles();
7398 assert_eq!(filtered.len(), 1);
7399 assert_eq!(filtered[0].name, "prod-profile");
7400 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
7401 assert_eq!(filtered[0].account, Some("123456789".to_string()));
7402 assert_eq!(
7403 filtered[0].role_arn,
7404 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
7405 );
7406 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
7407 }
7408
7409 #[test]
7410 fn test_profile_filter_by_source_profile() {
7411 let mut app = test_app_no_region();
7412 app.available_profiles = vec![
7413 AwsProfile {
7414 name: "profile1".to_string(),
7415 region: None,
7416 account: None,
7417 role_arn: None,
7418 source_profile: Some("base".to_string()),
7419 },
7420 AwsProfile {
7421 name: "profile2".to_string(),
7422 region: None,
7423 account: None,
7424 role_arn: None,
7425 source_profile: Some("other".to_string()),
7426 },
7427 ];
7428
7429 app.profile_filter = "base".to_string();
7430 let filtered = app.get_filtered_profiles();
7431 assert_eq!(filtered.len(), 1);
7432 assert_eq!(filtered[0].name, "profile1");
7433 }
7434
7435 #[test]
7436 fn test_profile_filter_by_role() {
7437 let mut app = test_app_no_region();
7438 app.available_profiles = vec![
7439 AwsProfile {
7440 name: "admin-profile".to_string(),
7441 region: None,
7442 account: None,
7443 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
7444 source_profile: None,
7445 },
7446 AwsProfile {
7447 name: "dev-profile".to_string(),
7448 region: None,
7449 account: None,
7450 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
7451 source_profile: None,
7452 },
7453 ];
7454
7455 app.profile_filter = "Admin".to_string();
7456 let filtered = app.get_filtered_profiles();
7457 assert_eq!(filtered.len(), 1);
7458 assert_eq!(filtered[0].name, "admin-profile");
7459 }
7460
7461 #[test]
7462 fn test_profiles_sorted_by_name() {
7463 let mut app = test_app_no_region();
7464 app.available_profiles = vec![
7465 AwsProfile {
7466 name: "zebra-profile".to_string(),
7467 region: None,
7468 account: None,
7469 role_arn: None,
7470 source_profile: None,
7471 },
7472 AwsProfile {
7473 name: "alpha-profile".to_string(),
7474 region: None,
7475 account: None,
7476 role_arn: None,
7477 source_profile: None,
7478 },
7479 AwsProfile {
7480 name: "beta-profile".to_string(),
7481 region: None,
7482 account: None,
7483 role_arn: None,
7484 source_profile: None,
7485 },
7486 ];
7487
7488 let filtered = app.get_filtered_profiles();
7489 assert_eq!(filtered.len(), 3);
7490 assert_eq!(filtered[0].name, "alpha-profile");
7491 assert_eq!(filtered[1].name, "beta-profile");
7492 assert_eq!(filtered[2].name, "zebra-profile");
7493 }
7494
7495 #[test]
7496 fn test_profile_with_role_arn() {
7497 let mut app = test_app_no_region();
7498 app.available_profiles = vec![AwsProfile {
7499 name: "role-profile".to_string(),
7500 region: Some("us-east-1".to_string()),
7501 account: Some("123456789".to_string()),
7502 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
7503 source_profile: None,
7504 }];
7505
7506 let filtered = app.get_filtered_profiles();
7507 assert_eq!(filtered.len(), 1);
7508 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
7509 }
7510
7511 #[test]
7512 fn test_profile_with_user_arn() {
7513 let mut app = test_app_no_region();
7514 app.available_profiles = vec![AwsProfile {
7515 name: "user-profile".to_string(),
7516 region: Some("us-east-1".to_string()),
7517 account: Some("123456789".to_string()),
7518 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
7519 source_profile: None,
7520 }];
7521
7522 let filtered = app.get_filtered_profiles();
7523 assert_eq!(filtered.len(), 1);
7524 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
7525 }
7526
7527 #[test]
7528 fn test_filtered_profiles_also_sorted() {
7529 let mut app = test_app_no_region();
7530 app.available_profiles = vec![
7531 AwsProfile {
7532 name: "prod-zebra".to_string(),
7533 region: Some("us-east-1".to_string()),
7534 account: None,
7535 role_arn: None,
7536 source_profile: None,
7537 },
7538 AwsProfile {
7539 name: "prod-alpha".to_string(),
7540 region: Some("us-east-1".to_string()),
7541 account: None,
7542 role_arn: None,
7543 source_profile: None,
7544 },
7545 AwsProfile {
7546 name: "dev-profile".to_string(),
7547 region: Some("us-west-2".to_string()),
7548 account: None,
7549 role_arn: None,
7550 source_profile: None,
7551 },
7552 ];
7553
7554 app.profile_filter = "prod".to_string();
7555 let filtered = app.get_filtered_profiles();
7556 assert_eq!(filtered.len(), 2);
7557 assert_eq!(filtered[0].name, "prod-alpha");
7558 assert_eq!(filtered[1].name, "prod-zebra");
7559 }
7560
7561 #[test]
7562 fn test_profile_picker_has_all_columns() {
7563 let mut app = test_app_no_region();
7564 app.available_profiles = vec![AwsProfile {
7565 name: "test".to_string(),
7566 region: Some("us-east-1".to_string()),
7567 account: Some("123456789".to_string()),
7568 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
7569 source_profile: Some("base".to_string()),
7570 }];
7571
7572 let filtered = app.get_filtered_profiles();
7573 assert_eq!(filtered.len(), 1);
7574 assert!(filtered[0].name == "test");
7575 assert!(filtered[0].region.is_some());
7576 assert!(filtered[0].account.is_some());
7577 assert!(filtered[0].role_arn.is_some());
7578 assert!(filtered[0].source_profile.is_some());
7579 }
7580
7581 #[test]
7582 fn test_session_picker_shows_tab_count() {
7583 let mut app = test_app_no_region();
7584 app.sessions = vec![crate::session::Session {
7585 id: "1".to_string(),
7586 timestamp: "2024-01-01".to_string(),
7587 profile: "test".to_string(),
7588 region: "us-east-1".to_string(),
7589 account_id: "123".to_string(),
7590 role_arn: String::new(),
7591 tabs: vec![
7592 crate::session::SessionTab {
7593 service: "CloudWatch".to_string(),
7594 title: "Logs".to_string(),
7595 breadcrumb: String::new(),
7596 filter: None,
7597 selected_item: None,
7598 },
7599 crate::session::SessionTab {
7600 service: "S3".to_string(),
7601 title: "Buckets".to_string(),
7602 breadcrumb: String::new(),
7603 filter: None,
7604 selected_item: None,
7605 },
7606 ],
7607 }];
7608
7609 let filtered = app.get_filtered_sessions();
7610 assert_eq!(filtered.len(), 1);
7611 assert_eq!(filtered[0].tabs.len(), 2);
7612 }
7613
7614 #[test]
7615 fn test_start_background_data_fetch_loads_profiles() {
7616 let mut app = test_app_no_region();
7617 assert!(app.available_profiles.is_empty());
7618
7619 app.available_profiles = App::load_aws_profiles();
7621
7622 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7624 }
7625
7626 #[test]
7627 fn test_refresh_in_profile_picker() {
7628 let mut app = test_app_no_region();
7629 app.mode = Mode::ProfilePicker;
7630 app.available_profiles = vec![AwsProfile {
7631 name: "test".to_string(),
7632 region: None,
7633 account: None,
7634 role_arn: None,
7635 source_profile: None,
7636 }];
7637
7638 app.handle_action(Action::Refresh);
7639
7640 assert!(app.log_groups_state.loading);
7642 assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
7643 }
7644
7645 #[test]
7646 fn test_refresh_sets_loading_for_profile_picker() {
7647 let mut app = test_app_no_region();
7648 app.mode = Mode::ProfilePicker;
7649
7650 assert!(!app.log_groups_state.loading);
7651
7652 app.handle_action(Action::Refresh);
7653
7654 assert!(app.log_groups_state.loading);
7655 }
7656
7657 #[test]
7658 fn test_profiles_loaded_on_demand() {
7659 let mut app = test_app_no_region();
7660
7661 assert!(app.available_profiles.is_empty());
7663
7664 app.available_profiles = App::load_aws_profiles();
7666
7667 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7669 }
7670
7671 #[test]
7672 fn test_profile_accounts_not_fetched_automatically() {
7673 let mut app = test_app_no_region();
7674 app.available_profiles = App::load_aws_profiles();
7675
7676 for profile in &app.available_profiles {
7678 assert!(profile.account.is_none() || profile.account.is_some());
7681 }
7682 }
7683
7684 #[test]
7685 fn test_ctrl_r_triggers_account_fetch() {
7686 let mut app = test_app_no_region();
7687 app.mode = Mode::ProfilePicker;
7688 app.available_profiles = vec![AwsProfile {
7689 name: "test".to_string(),
7690 region: Some("us-east-1".to_string()),
7691 account: None,
7692 role_arn: None,
7693 source_profile: None,
7694 }];
7695
7696 assert!(app.available_profiles[0].account.is_none());
7698
7699 app.handle_action(Action::Refresh);
7701
7702 assert!(app.log_groups_state.loading);
7704 }
7705
7706 #[test]
7707 fn test_refresh_in_region_picker() {
7708 let mut app = test_app_no_region();
7709 app.mode = Mode::RegionPicker;
7710
7711 let initial_latencies = app.region_latencies.len();
7712 app.handle_action(Action::Refresh);
7713
7714 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
7716 }
7717
7718 #[test]
7719 fn test_refresh_in_session_picker() {
7720 let mut app = test_app_no_region();
7721 app.mode = Mode::SessionPicker;
7722 app.sessions = vec![];
7723
7724 app.handle_action(Action::Refresh);
7725
7726 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
7728 }
7729
7730 #[test]
7731 fn test_session_picker_selection() {
7732 let mut app = test_app();
7733
7734 app.sessions = vec![crate::session::Session {
7735 id: "1".to_string(),
7736 timestamp: "2024-01-01".to_string(),
7737 profile: "prod-profile".to_string(),
7738 region: "us-west-2".to_string(),
7739 account_id: "123456789".to_string(),
7740 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7741 tabs: vec![crate::session::SessionTab {
7742 service: "CloudWatchLogGroups".to_string(),
7743 title: "Log Groups".to_string(),
7744 breadcrumb: "CloudWatch > Log Groups".to_string(),
7745 filter: Some("test".to_string()),
7746 selected_item: None,
7747 }],
7748 }];
7749
7750 app.mode = Mode::SessionPicker;
7751 app.session_picker_selected = 0;
7752
7753 app.handle_action(Action::Select);
7755
7756 assert_eq!(app.mode, Mode::Normal);
7757 assert_eq!(app.profile, "prod-profile");
7758 assert_eq!(app.region, "us-west-2");
7759 assert_eq!(app.config.account_id, "123456789");
7760 assert_eq!(app.tabs.len(), 1);
7761 assert_eq!(app.tabs[0].title, "Log Groups");
7762 }
7763
7764 #[test]
7765 fn test_save_session_creates_session() {
7766 let mut app =
7767 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7768 app.config.account_id = "123456789".to_string();
7769 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7770
7771 app.tabs.push(Tab {
7772 service: Service::CloudWatchLogGroups,
7773 title: "Log Groups".to_string(),
7774 breadcrumb: "CloudWatch > Log Groups".to_string(),
7775 });
7776
7777 app.save_current_session();
7778
7779 assert!(app.current_session.is_some());
7780 let session = app.current_session.clone().unwrap();
7781 assert_eq!(session.profile, "test-profile");
7782 assert_eq!(session.region, "us-east-1");
7783 assert_eq!(session.account_id, "123456789");
7784 assert_eq!(session.tabs.len(), 1);
7785
7786 let _ = session.delete();
7788 }
7789
7790 #[test]
7791 fn test_save_session_updates_existing() {
7792 let mut app =
7793 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7794 app.config.account_id = "123456789".to_string();
7795 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7796
7797 app.current_session = Some(crate::session::Session {
7798 id: "existing".to_string(),
7799 timestamp: "2024-01-01".to_string(),
7800 profile: "test-profile".to_string(),
7801 region: "us-east-1".to_string(),
7802 account_id: "123456789".to_string(),
7803 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7804 tabs: vec![],
7805 });
7806
7807 app.tabs.push(Tab {
7808 service: Service::CloudWatchLogGroups,
7809 title: "Log Groups".to_string(),
7810 breadcrumb: "CloudWatch > Log Groups".to_string(),
7811 });
7812
7813 app.save_current_session();
7814
7815 let session = app.current_session.clone().unwrap();
7816 assert_eq!(session.id, "existing");
7817 assert_eq!(session.tabs.len(), 1);
7818
7819 let _ = session.delete();
7821 }
7822
7823 #[test]
7824 fn test_save_session_skips_empty_tabs() {
7825 let mut app =
7826 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7827 app.config.account_id = "123456789".to_string();
7828
7829 app.save_current_session();
7830
7831 assert!(app.current_session.is_none());
7832 }
7833
7834 #[test]
7835 fn test_save_session_deletes_when_tabs_closed() {
7836 let mut app =
7837 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7838 app.config.account_id = "123456789".to_string();
7839 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7840
7841 app.current_session = Some(crate::session::Session {
7843 id: "test_delete".to_string(),
7844 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
7845 profile: "test-profile".to_string(),
7846 region: "us-east-1".to_string(),
7847 account_id: "123456789".to_string(),
7848 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7849 tabs: vec![],
7850 });
7851
7852 app.save_current_session();
7854
7855 assert!(app.current_session.is_none());
7856 }
7857
7858 #[test]
7859 fn test_closing_all_tabs_deletes_session() {
7860 let mut app =
7861 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7862 app.config.account_id = "123456789".to_string();
7863 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7864
7865 app.tabs.push(Tab {
7867 service: Service::CloudWatchLogGroups,
7868 title: "Log Groups".to_string(),
7869 breadcrumb: "CloudWatch > Log Groups".to_string(),
7870 });
7871
7872 app.save_current_session();
7874 assert!(app.current_session.is_some());
7875 let session_id = app.current_session.as_ref().unwrap().id.clone();
7876
7877 app.tabs.clear();
7879
7880 app.save_current_session();
7882 assert!(app.current_session.is_none());
7883
7884 let _ = crate::session::Session::load(&session_id).map(|s| s.delete());
7886 }
7887
7888 #[test]
7889 fn test_credential_error_opens_profile_picker() {
7890 let mut app = App::new_without_client("default".to_string(), None);
7892 let error_str = "Unable to load credentials from any source";
7893
7894 if error_str.contains("credentials") {
7895 app.available_profiles = App::load_aws_profiles();
7896 app.mode = Mode::ProfilePicker;
7897 }
7898
7899 assert_eq!(app.mode, Mode::ProfilePicker);
7900 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7902 }
7903
7904 #[test]
7905 fn test_non_credential_error_shows_error_modal() {
7906 let mut app = App::new_without_client("default".to_string(), None);
7907 let error_str = "Network timeout";
7908
7909 if !error_str.contains("credentials") {
7910 app.error_message = Some(error_str.to_string());
7911 app.mode = Mode::ErrorModal;
7912 }
7913
7914 assert_eq!(app.mode, Mode::ErrorModal);
7915 assert!(app.error_message.is_some());
7916 }
7917
7918 #[tokio::test]
7919 async fn test_profile_selection_loads_credentials() {
7920 std::env::set_var("AWS_PROFILE", "default");
7922
7923 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
7925
7926 if let Ok(app) = result {
7927 assert!(!app.config.account_id.is_empty());
7929 assert!(!app.config.role_arn.is_empty());
7930 assert_eq!(app.profile, "default");
7931 assert_eq!(app.config.region, "us-east-1");
7932 }
7933 }
7935
7936 #[test]
7937 fn test_new_app_shows_service_picker_with_no_tabs() {
7938 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
7939
7940 assert!(!app.service_selected);
7942 assert_eq!(app.mode, Mode::ServicePicker);
7944 assert!(app.tabs.is_empty());
7946 }
7947
7948 #[tokio::test]
7949 async fn test_aws_profile_env_var_read_before_config_load() {
7950 std::env::set_var("AWS_PROFILE", "test-profile");
7952
7953 let profile_name = None
7955 .or_else(|| std::env::var("AWS_PROFILE").ok())
7956 .unwrap_or_else(|| "default".to_string());
7957
7958 assert_eq!(profile_name, "test-profile");
7960
7961 std::env::set_var("AWS_PROFILE", &profile_name);
7963
7964 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
7966
7967 std::env::remove_var("AWS_PROFILE");
7968 }
7969
7970 #[test]
7971 fn test_next_preferences_cloudformation() {
7972 let mut app = test_app();
7973 app.current_service = Service::CloudFormationStacks;
7974 app.mode = Mode::ColumnSelector;
7975 app.column_selector_index = 0;
7976
7977 let page_size_idx = app.all_cfn_columns.len() + 2;
7979 app.handle_action(Action::NextPreferences);
7980 assert_eq!(app.column_selector_index, page_size_idx);
7981
7982 app.handle_action(Action::NextPreferences);
7984 assert_eq!(app.column_selector_index, 0);
7985 }
7986
7987 #[test]
7988 fn test_next_preferences_lambda_functions() {
7989 let mut app = test_app();
7990 app.current_service = Service::LambdaFunctions;
7991 app.mode = Mode::ColumnSelector;
7992 app.column_selector_index = 0;
7993
7994 let page_size_idx = app.lambda_state.all_columns.len() + 2;
7995 app.handle_action(Action::NextPreferences);
7996 assert_eq!(app.column_selector_index, page_size_idx);
7997
7998 app.handle_action(Action::NextPreferences);
7999 assert_eq!(app.column_selector_index, 0);
8000 }
8001
8002 #[test]
8003 fn test_next_preferences_lambda_applications() {
8004 let mut app = test_app();
8005 app.current_service = Service::LambdaApplications;
8006 app.mode = Mode::ColumnSelector;
8007 app.column_selector_index = 0;
8008
8009 let page_size_idx = app.all_lambda_application_columns.len() + 2;
8010 app.handle_action(Action::NextPreferences);
8011 assert_eq!(app.column_selector_index, page_size_idx);
8012
8013 app.handle_action(Action::NextPreferences);
8014 assert_eq!(app.column_selector_index, 0);
8015 }
8016
8017 #[test]
8018 fn test_next_preferences_ecr_images() {
8019 let mut app = test_app();
8020 app.current_service = Service::EcrRepositories;
8021 app.ecr_state.current_repository = Some("test-repo".to_string());
8022 app.mode = Mode::ColumnSelector;
8023 app.column_selector_index = 0;
8024
8025 let page_size_idx = app.all_ecr_image_columns.len() + 2;
8026 app.handle_action(Action::NextPreferences);
8027 assert_eq!(app.column_selector_index, page_size_idx);
8028
8029 app.handle_action(Action::NextPreferences);
8030 assert_eq!(app.column_selector_index, 0);
8031 }
8032
8033 #[test]
8034 fn test_cloudformation_next_item() {
8035 let mut app = test_app();
8036 app.current_service = Service::CloudFormationStacks;
8037 app.service_selected = true;
8038 app.mode = Mode::Normal;
8039 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8040 app.cfn_state.table.items = vec![
8041 CfnStack {
8042 name: "stack1".to_string(),
8043 stack_id: "id1".to_string(),
8044 status: "CREATE_COMPLETE".to_string(),
8045 created_time: "2024-01-01".to_string(),
8046 updated_time: String::new(),
8047 deleted_time: String::new(),
8048 drift_status: String::new(),
8049 last_drift_check_time: String::new(),
8050 status_reason: String::new(),
8051 description: String::new(),
8052 detailed_status: String::new(),
8053 root_stack: String::new(),
8054 parent_stack: String::new(),
8055 termination_protection: false,
8056 iam_role: String::new(),
8057 tags: Vec::new(),
8058 stack_policy: String::new(),
8059 rollback_monitoring_time: String::new(),
8060 rollback_alarms: Vec::new(),
8061 notification_arns: Vec::new(),
8062 },
8063 CfnStack {
8064 name: "stack2".to_string(),
8065 stack_id: "id2".to_string(),
8066 status: "UPDATE_COMPLETE".to_string(),
8067 created_time: "2024-01-02".to_string(),
8068 updated_time: String::new(),
8069 deleted_time: String::new(),
8070 drift_status: String::new(),
8071 last_drift_check_time: String::new(),
8072 status_reason: String::new(),
8073 description: String::new(),
8074 detailed_status: String::new(),
8075 root_stack: String::new(),
8076 parent_stack: String::new(),
8077 termination_protection: false,
8078 iam_role: String::new(),
8079 tags: Vec::new(),
8080 stack_policy: String::new(),
8081 rollback_monitoring_time: String::new(),
8082 rollback_alarms: Vec::new(),
8083 notification_arns: Vec::new(),
8084 },
8085 ];
8086 app.cfn_state.table.reset();
8087
8088 app.handle_action(Action::NextItem);
8089 assert_eq!(app.cfn_state.table.selected, 1);
8090
8091 app.handle_action(Action::NextItem);
8092 assert_eq!(app.cfn_state.table.selected, 1); }
8094
8095 #[test]
8096 fn test_cloudformation_prev_item() {
8097 let mut app = test_app();
8098 app.current_service = Service::CloudFormationStacks;
8099 app.service_selected = true;
8100 app.mode = Mode::Normal;
8101 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8102 app.cfn_state.table.items = vec![
8103 CfnStack {
8104 name: "stack1".to_string(),
8105 stack_id: "id1".to_string(),
8106 status: "CREATE_COMPLETE".to_string(),
8107 created_time: "2024-01-01".to_string(),
8108 updated_time: String::new(),
8109 deleted_time: String::new(),
8110 drift_status: String::new(),
8111 last_drift_check_time: String::new(),
8112 status_reason: String::new(),
8113 description: String::new(),
8114 detailed_status: String::new(),
8115 root_stack: String::new(),
8116 parent_stack: String::new(),
8117 termination_protection: false,
8118 iam_role: String::new(),
8119 tags: Vec::new(),
8120 stack_policy: String::new(),
8121 rollback_monitoring_time: String::new(),
8122 rollback_alarms: Vec::new(),
8123 notification_arns: Vec::new(),
8124 },
8125 CfnStack {
8126 name: "stack2".to_string(),
8127 stack_id: "id2".to_string(),
8128 status: "UPDATE_COMPLETE".to_string(),
8129 created_time: "2024-01-02".to_string(),
8130 updated_time: String::new(),
8131 deleted_time: String::new(),
8132 drift_status: String::new(),
8133 last_drift_check_time: String::new(),
8134 status_reason: String::new(),
8135 description: String::new(),
8136 detailed_status: String::new(),
8137 root_stack: String::new(),
8138 parent_stack: String::new(),
8139 termination_protection: false,
8140 iam_role: String::new(),
8141 tags: Vec::new(),
8142 stack_policy: String::new(),
8143 rollback_monitoring_time: String::new(),
8144 rollback_alarms: Vec::new(),
8145 notification_arns: Vec::new(),
8146 },
8147 ];
8148 app.cfn_state.table.selected = 1;
8149
8150 app.handle_action(Action::PrevItem);
8151 assert_eq!(app.cfn_state.table.selected, 0);
8152
8153 app.handle_action(Action::PrevItem);
8154 assert_eq!(app.cfn_state.table.selected, 0); }
8156
8157 #[test]
8158 fn test_cloudformation_page_down() {
8159 let mut app = test_app();
8160 app.current_service = Service::CloudFormationStacks;
8161 app.service_selected = true;
8162 app.mode = Mode::Normal;
8163 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8164
8165 for i in 0..20 {
8167 app.cfn_state.table.items.push(CfnStack {
8168 name: format!("stack{}", i),
8169 stack_id: format!("id{}", i),
8170 status: "CREATE_COMPLETE".to_string(),
8171 created_time: format!("2024-01-{:02}", i + 1),
8172 updated_time: String::new(),
8173 deleted_time: String::new(),
8174 drift_status: String::new(),
8175 last_drift_check_time: String::new(),
8176 status_reason: String::new(),
8177 description: String::new(),
8178 detailed_status: String::new(),
8179 root_stack: String::new(),
8180 parent_stack: String::new(),
8181 termination_protection: false,
8182 iam_role: String::new(),
8183 tags: Vec::new(),
8184 stack_policy: String::new(),
8185 rollback_monitoring_time: String::new(),
8186 rollback_alarms: Vec::new(),
8187 notification_arns: Vec::new(),
8188 });
8189 }
8190 app.cfn_state.table.reset();
8191
8192 app.handle_action(Action::PageDown);
8193 assert_eq!(app.cfn_state.table.selected, 10);
8194
8195 app.handle_action(Action::PageDown);
8196 assert_eq!(app.cfn_state.table.selected, 19); }
8198
8199 #[test]
8200 fn test_cloudformation_page_up() {
8201 let mut app = test_app();
8202 app.current_service = Service::CloudFormationStacks;
8203 app.service_selected = true;
8204 app.mode = Mode::Normal;
8205 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8206
8207 for i in 0..20 {
8209 app.cfn_state.table.items.push(CfnStack {
8210 name: format!("stack{}", i),
8211 stack_id: format!("id{}", i),
8212 status: "CREATE_COMPLETE".to_string(),
8213 created_time: format!("2024-01-{:02}", i + 1),
8214 updated_time: String::new(),
8215 deleted_time: String::new(),
8216 drift_status: String::new(),
8217 last_drift_check_time: String::new(),
8218 status_reason: String::new(),
8219 description: String::new(),
8220 detailed_status: String::new(),
8221 root_stack: String::new(),
8222 parent_stack: String::new(),
8223 termination_protection: false,
8224 iam_role: String::new(),
8225 tags: Vec::new(),
8226 stack_policy: String::new(),
8227 rollback_monitoring_time: String::new(),
8228 rollback_alarms: Vec::new(),
8229 notification_arns: Vec::new(),
8230 });
8231 }
8232 app.cfn_state.table.selected = 15;
8233
8234 app.handle_action(Action::PageUp);
8235 assert_eq!(app.cfn_state.table.selected, 5);
8236
8237 app.handle_action(Action::PageUp);
8238 assert_eq!(app.cfn_state.table.selected, 0); }
8240
8241 #[test]
8242 fn test_cloudformation_filter_input() {
8243 let mut app = test_app();
8244 app.current_service = Service::CloudFormationStacks;
8245 app.service_selected = true;
8246 app.mode = Mode::Normal;
8247
8248 app.handle_action(Action::StartFilter);
8249 assert_eq!(app.mode, Mode::FilterInput);
8250
8251 app.cfn_state.table.filter = "test".to_string();
8253 assert_eq!(app.cfn_state.table.filter, "test");
8254 }
8255
8256 #[test]
8257 fn test_cloudformation_filter_applies() {
8258 let mut app = test_app();
8259 app.current_service = Service::CloudFormationStacks;
8260 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8261 app.cfn_state.table.items = vec![
8262 CfnStack {
8263 name: "prod-stack".to_string(),
8264 stack_id: "id1".to_string(),
8265 status: "CREATE_COMPLETE".to_string(),
8266 created_time: "2024-01-01".to_string(),
8267 updated_time: String::new(),
8268 deleted_time: String::new(),
8269 drift_status: String::new(),
8270 last_drift_check_time: String::new(),
8271 status_reason: String::new(),
8272 description: "Production stack".to_string(),
8273 detailed_status: String::new(),
8274 root_stack: String::new(),
8275 parent_stack: String::new(),
8276 termination_protection: false,
8277 iam_role: String::new(),
8278 tags: Vec::new(),
8279 stack_policy: String::new(),
8280 rollback_monitoring_time: String::new(),
8281 rollback_alarms: Vec::new(),
8282 notification_arns: Vec::new(),
8283 },
8284 CfnStack {
8285 name: "dev-stack".to_string(),
8286 stack_id: "id2".to_string(),
8287 status: "UPDATE_COMPLETE".to_string(),
8288 created_time: "2024-01-02".to_string(),
8289 updated_time: String::new(),
8290 deleted_time: String::new(),
8291 drift_status: String::new(),
8292 last_drift_check_time: String::new(),
8293 status_reason: String::new(),
8294 description: "Development stack".to_string(),
8295 detailed_status: String::new(),
8296 root_stack: String::new(),
8297 parent_stack: String::new(),
8298 termination_protection: false,
8299 iam_role: String::new(),
8300 tags: Vec::new(),
8301 stack_policy: String::new(),
8302 rollback_monitoring_time: String::new(),
8303 rollback_alarms: Vec::new(),
8304 notification_arns: Vec::new(),
8305 },
8306 ];
8307 app.cfn_state.table.filter = "prod".to_string();
8308
8309 let filtered = app.filtered_cloudformation_stacks();
8310 assert_eq!(filtered.len(), 1);
8311 assert_eq!(filtered[0].name, "prod-stack");
8312 }
8313
8314 #[test]
8315 fn test_cloudformation_right_arrow_expands() {
8316 let mut app = test_app();
8317 app.current_service = Service::CloudFormationStacks;
8318 app.service_selected = true;
8319 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8320 app.cfn_state.table.items = vec![CfnStack {
8321 name: "test-stack".to_string(),
8322 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8323 .to_string(),
8324 status: "CREATE_COMPLETE".to_string(),
8325 created_time: "2024-01-01".to_string(),
8326 updated_time: String::new(),
8327 deleted_time: String::new(),
8328 drift_status: String::new(),
8329 last_drift_check_time: String::new(),
8330 status_reason: String::new(),
8331 description: "Test stack".to_string(),
8332 detailed_status: String::new(),
8333 root_stack: String::new(),
8334 parent_stack: String::new(),
8335 termination_protection: false,
8336 iam_role: String::new(),
8337 tags: Vec::new(),
8338 stack_policy: String::new(),
8339 rollback_monitoring_time: String::new(),
8340 rollback_alarms: Vec::new(),
8341 notification_arns: Vec::new(),
8342 }];
8343 app.cfn_state.table.reset();
8344
8345 assert_eq!(app.cfn_state.table.expanded_item, None);
8346
8347 app.handle_action(Action::NextPane);
8348 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
8349 }
8350
8351 #[test]
8352 fn test_cloudformation_left_arrow_collapses() {
8353 let mut app = test_app();
8354 app.current_service = Service::CloudFormationStacks;
8355 app.service_selected = true;
8356 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8357 app.cfn_state.table.items = vec![CfnStack {
8358 name: "test-stack".to_string(),
8359 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8360 .to_string(),
8361 status: "CREATE_COMPLETE".to_string(),
8362 created_time: "2024-01-01".to_string(),
8363 updated_time: String::new(),
8364 deleted_time: String::new(),
8365 drift_status: String::new(),
8366 last_drift_check_time: String::new(),
8367 status_reason: String::new(),
8368 description: "Test stack".to_string(),
8369 detailed_status: String::new(),
8370 root_stack: String::new(),
8371 parent_stack: String::new(),
8372 termination_protection: false,
8373 iam_role: String::new(),
8374 tags: Vec::new(),
8375 stack_policy: String::new(),
8376 rollback_monitoring_time: String::new(),
8377 rollback_alarms: Vec::new(),
8378 notification_arns: Vec::new(),
8379 }];
8380 app.cfn_state.table.reset();
8381 app.cfn_state.table.expanded_item = Some(0);
8382
8383 app.handle_action(Action::PrevPane);
8384 assert_eq!(app.cfn_state.table.expanded_item, None);
8385 }
8386
8387 #[test]
8388 fn test_cloudformation_enter_drills_into_stack() {
8389 let mut app = test_app();
8390 app.current_service = Service::CloudFormationStacks;
8391 app.service_selected = true;
8392 app.mode = Mode::Normal;
8393 app.tabs = vec![Tab {
8394 service: Service::CloudFormationStacks,
8395 title: "CloudFormation > Stacks".to_string(),
8396 breadcrumb: "CloudFormation > Stacks".to_string(),
8397 }];
8398 app.current_tab = 0;
8399 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8400 app.cfn_state.table.items = vec![CfnStack {
8401 name: "test-stack".to_string(),
8402 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8403 .to_string(),
8404 status: "CREATE_COMPLETE".to_string(),
8405 created_time: "2024-01-01".to_string(),
8406 updated_time: String::new(),
8407 deleted_time: String::new(),
8408 drift_status: String::new(),
8409 last_drift_check_time: String::new(),
8410 status_reason: String::new(),
8411 description: "Test stack".to_string(),
8412 detailed_status: String::new(),
8413 root_stack: String::new(),
8414 parent_stack: String::new(),
8415 termination_protection: false,
8416 iam_role: String::new(),
8417 tags: Vec::new(),
8418 stack_policy: String::new(),
8419 rollback_monitoring_time: String::new(),
8420 rollback_alarms: Vec::new(),
8421 notification_arns: Vec::new(),
8422 }];
8423 app.cfn_state.table.reset();
8424
8425 let filtered = app.filtered_cloudformation_stacks();
8427 assert_eq!(filtered.len(), 1);
8428 assert_eq!(filtered[0].name, "test-stack");
8429
8430 assert_eq!(app.cfn_state.current_stack, None);
8431
8432 app.handle_action(Action::Select);
8434 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
8435 }
8436
8437 #[test]
8438 fn test_cloudformation_copy_to_clipboard() {
8439 let mut app = test_app();
8440 app.current_service = Service::CloudFormationStacks;
8441 app.service_selected = true;
8442 app.mode = Mode::Normal;
8443 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8444 app.cfn_state.table.items = vec![
8445 CfnStack {
8446 name: "stack1".to_string(),
8447 stack_id: "id1".to_string(),
8448 status: "CREATE_COMPLETE".to_string(),
8449 created_time: "2024-01-01".to_string(),
8450 updated_time: String::new(),
8451 deleted_time: String::new(),
8452 drift_status: String::new(),
8453 last_drift_check_time: String::new(),
8454 status_reason: String::new(),
8455 description: String::new(),
8456 detailed_status: String::new(),
8457 root_stack: String::new(),
8458 parent_stack: String::new(),
8459 termination_protection: false,
8460 iam_role: String::new(),
8461 tags: Vec::new(),
8462 stack_policy: String::new(),
8463 rollback_monitoring_time: String::new(),
8464 rollback_alarms: Vec::new(),
8465 notification_arns: Vec::new(),
8466 },
8467 CfnStack {
8468 name: "stack2".to_string(),
8469 stack_id: "id2".to_string(),
8470 status: "UPDATE_COMPLETE".to_string(),
8471 created_time: "2024-01-02".to_string(),
8472 updated_time: String::new(),
8473 deleted_time: String::new(),
8474 drift_status: String::new(),
8475 last_drift_check_time: String::new(),
8476 status_reason: String::new(),
8477 description: String::new(),
8478 detailed_status: String::new(),
8479 root_stack: String::new(),
8480 parent_stack: String::new(),
8481 termination_protection: false,
8482 iam_role: String::new(),
8483 tags: Vec::new(),
8484 stack_policy: String::new(),
8485 rollback_monitoring_time: String::new(),
8486 rollback_alarms: Vec::new(),
8487 notification_arns: Vec::new(),
8488 },
8489 ];
8490
8491 assert!(!app.snapshot_requested);
8492 app.handle_action(Action::CopyToClipboard);
8493
8494 assert!(app.snapshot_requested);
8496 }
8497
8498 #[test]
8499 fn test_cloudformation_expansion_shows_all_visible_columns() {
8500 let mut app = test_app();
8501 app.current_service = Service::CloudFormationStacks;
8502 app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8503 app.cfn_state.table.items = vec![CfnStack {
8504 name: "test-stack".to_string(),
8505 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8506 .to_string(),
8507 status: "CREATE_COMPLETE".to_string(),
8508 created_time: "2024-01-01".to_string(),
8509 updated_time: "2024-01-02".to_string(),
8510 deleted_time: String::new(),
8511 drift_status: "IN_SYNC".to_string(),
8512 last_drift_check_time: "2024-01-03".to_string(),
8513 status_reason: String::new(),
8514 description: "Test description".to_string(),
8515 detailed_status: String::new(),
8516 root_stack: String::new(),
8517 parent_stack: String::new(),
8518 termination_protection: false,
8519 iam_role: String::new(),
8520 tags: Vec::new(),
8521 stack_policy: String::new(),
8522 rollback_monitoring_time: String::new(),
8523 rollback_alarms: Vec::new(),
8524 notification_arns: Vec::new(),
8525 }];
8526
8527 app.visible_cfn_columns = vec![
8529 CfnColumn::Name,
8530 CfnColumn::Status,
8531 CfnColumn::CreatedTime,
8532 CfnColumn::Description,
8533 ];
8534
8535 app.cfn_state.table.expanded_item = Some(0);
8536
8537 assert_eq!(app.visible_cfn_columns.len(), 4);
8540 assert!(app.cfn_state.table.has_expanded_item());
8541 }
8542
8543 #[test]
8544 fn test_cloudformation_empty_list_shows_page_1() {
8545 let mut app = test_app();
8546 app.current_service = Service::CloudFormationStacks;
8547 app.cfn_state.table.items = vec![];
8548
8549 let filtered = app.filtered_cloudformation_stacks();
8550 assert_eq!(filtered.len(), 0);
8551
8552 let page_size = app.cfn_state.table.page_size.value();
8554 let total_pages = filtered.len().div_ceil(page_size);
8555 assert_eq!(total_pages, 0);
8556
8557 }
8560}
8561
8562impl App {
8563 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
8564 let mut all = AwsRegion::all();
8565
8566 for region in &mut all {
8568 region.latency_ms = self.region_latencies.get(region.code).copied();
8569 }
8570
8571 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
8573 all
8574 } else {
8575 let filter_lower = self.region_filter.to_lowercase();
8576 all.into_iter()
8577 .filter(|r| {
8578 r.name.to_lowercase().contains(&filter_lower)
8579 || r.code.to_lowercase().contains(&filter_lower)
8580 || r.group.to_lowercase().contains(&filter_lower)
8581 })
8582 .collect()
8583 };
8584
8585 let mut sorted = filtered;
8587 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
8588 sorted
8589 }
8590
8591 pub fn measure_region_latencies(&mut self) {
8592 use std::time::Instant;
8593 self.region_latencies.clear();
8594
8595 let regions = AwsRegion::all();
8596 let start_all = Instant::now();
8597 tracing::info!("Starting latency measurement for {} regions", regions.len());
8598
8599 let handles: Vec<_> = regions
8600 .iter()
8601 .map(|region| {
8602 let code = region.code.to_string();
8603 std::thread::spawn(move || {
8604 let endpoint = format!("https://sts.{}.amazonaws.com", code);
8606 let start = Instant::now();
8607
8608 match ureq::get(&endpoint)
8609 .timeout(std::time::Duration::from_secs(2))
8610 .call()
8611 {
8612 Ok(_) => {
8613 let latency = start.elapsed().as_millis() as u64;
8614 Some((code, latency))
8615 }
8616 Err(e) => {
8617 tracing::debug!("Failed to measure {}: {}", code, e);
8618 Some((code, 9999))
8619 }
8620 }
8621 })
8622 })
8623 .collect();
8624
8625 for handle in handles {
8626 if let Ok(Some((code, latency))) = handle.join() {
8627 self.region_latencies.insert(code, latency);
8628 }
8629 }
8630
8631 tracing::info!(
8632 "Measured {} regions in {:?}",
8633 self.region_latencies.len(),
8634 start_all.elapsed()
8635 );
8636 }
8637
8638 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
8639 crate::aws::filter_profiles(&self.available_profiles, &self.profile_filter)
8640 }
8641
8642 pub fn get_filtered_sessions(&self) -> Vec<&crate::session::Session> {
8643 if self.session_filter.is_empty() {
8644 return self.sessions.iter().collect();
8645 }
8646 let filter_lower = self.session_filter.to_lowercase();
8647 self.sessions
8648 .iter()
8649 .filter(|s| {
8650 s.profile.to_lowercase().contains(&filter_lower)
8651 || s.region.to_lowercase().contains(&filter_lower)
8652 || s.account_id.to_lowercase().contains(&filter_lower)
8653 || s.role_arn.to_lowercase().contains(&filter_lower)
8654 })
8655 .collect()
8656 }
8657
8658 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
8659 if self.tab_filter.is_empty() {
8660 return self.tabs.iter().enumerate().collect();
8661 }
8662 let filter_lower = self.tab_filter.to_lowercase();
8663 self.tabs
8664 .iter()
8665 .enumerate()
8666 .filter(|(_, tab)| {
8667 tab.title.to_lowercase().contains(&filter_lower)
8668 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
8669 })
8670 .collect()
8671 }
8672
8673 pub fn load_aws_profiles() -> Vec<AwsProfile> {
8674 AwsProfile::load_all()
8675 }
8676
8677 pub async fn fetch_profile_accounts(&mut self) {
8678 for profile in &mut self.available_profiles {
8679 if profile.account.is_none() {
8680 let region = profile
8681 .region
8682 .clone()
8683 .unwrap_or_else(|| "us-east-1".to_string());
8684 if let Ok(account) =
8685 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
8686 {
8687 profile.account = Some(account);
8688 }
8689 }
8690 }
8691 }
8692
8693 fn save_current_session(&mut self) {
8694 if self.tabs.is_empty() {
8696 if let Some(ref session) = self.current_session {
8697 let _ = session.delete();
8698 self.current_session = None;
8699 }
8700 return;
8701 }
8702
8703 let session = if let Some(ref mut current) = self.current_session {
8704 current.tabs = self
8706 .tabs
8707 .iter()
8708 .map(|t| crate::session::SessionTab {
8709 service: format!("{:?}", t.service),
8710 title: t.title.clone(),
8711 breadcrumb: t.breadcrumb.clone(),
8712 filter: match t.service {
8713 Service::CloudWatchLogGroups => {
8714 Some(self.log_groups_state.log_groups.filter.clone())
8715 }
8716 _ => None,
8717 },
8718 selected_item: None,
8719 })
8720 .collect();
8721 current.clone()
8722 } else {
8723 let mut session = crate::session::Session::new(
8725 self.profile.clone(),
8726 self.region.clone(),
8727 self.config.account_id.clone(),
8728 self.config.role_arn.clone(),
8729 );
8730 session.tabs = self
8731 .tabs
8732 .iter()
8733 .map(|t| crate::session::SessionTab {
8734 service: format!("{:?}", t.service),
8735 title: t.title.clone(),
8736 breadcrumb: t.breadcrumb.clone(),
8737 filter: match t.service {
8738 Service::CloudWatchLogGroups => {
8739 Some(self.log_groups_state.log_groups.filter.clone())
8740 }
8741 _ => None,
8742 },
8743 selected_item: None,
8744 })
8745 .collect();
8746 self.current_session = Some(session.clone());
8747 session
8748 };
8749
8750 let _ = session.save();
8751 }
8752}
8753
8754#[cfg(test)]
8755mod iam_policy_view_tests {
8756 use super::*;
8757 use test_helpers::*;
8758
8759 #[test]
8760 fn test_enter_opens_policy_view() {
8761 let mut app = test_app();
8762 app.current_service = Service::IamRoles;
8763 app.service_selected = true;
8764 app.mode = Mode::Normal;
8765 app.view_mode = ViewMode::Detail;
8766 app.iam_state.current_role = Some("TestRole".to_string());
8767 app.iam_state.policies.items = vec![crate::iam::Policy {
8768 policy_name: "TestPolicy".to_string(),
8769 policy_type: "Inline".to_string(),
8770 attached_via: "Direct".to_string(),
8771 attached_entities: "1".to_string(),
8772 description: "Test".to_string(),
8773 creation_time: "2023-01-01".to_string(),
8774 edited_time: "2023-01-01".to_string(),
8775 policy_arn: None,
8776 }];
8777 app.iam_state.policies.reset();
8778
8779 app.handle_action(Action::Select);
8780
8781 assert_eq!(app.view_mode, ViewMode::PolicyView);
8782 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
8783 assert_eq!(app.iam_state.policy_scroll, 0);
8784 assert!(app.iam_state.policies.loading);
8785 }
8786
8787 #[test]
8788 fn test_escape_closes_policy_view() {
8789 let mut app = test_app();
8790 app.current_service = Service::IamRoles;
8791 app.service_selected = true;
8792 app.mode = Mode::Normal;
8793 app.view_mode = ViewMode::PolicyView;
8794 app.iam_state.current_role = Some("TestRole".to_string());
8795 app.iam_state.current_policy = Some("TestPolicy".to_string());
8796 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
8797 app.iam_state.policy_scroll = 5;
8798
8799 app.handle_action(Action::PrevPane);
8800
8801 assert_eq!(app.view_mode, ViewMode::Detail);
8802 assert_eq!(app.iam_state.current_policy, None);
8803 assert_eq!(app.iam_state.policy_document, "");
8804 assert_eq!(app.iam_state.policy_scroll, 0);
8805 }
8806
8807 #[test]
8808 fn test_ctrl_d_scrolls_down_in_policy_view() {
8809 let mut app = test_app();
8810 app.current_service = Service::IamRoles;
8811 app.service_selected = true;
8812 app.mode = Mode::Normal;
8813 app.view_mode = ViewMode::PolicyView;
8814 app.iam_state.current_role = Some("TestRole".to_string());
8815 app.iam_state.current_policy = Some("TestPolicy".to_string());
8816 app.iam_state.policy_document = (0..100)
8817 .map(|i| format!("line {}", i))
8818 .collect::<Vec<_>>()
8819 .join("\n");
8820 app.iam_state.policy_scroll = 0;
8821
8822 app.handle_action(Action::ScrollDown);
8823
8824 assert_eq!(app.iam_state.policy_scroll, 10);
8825
8826 app.handle_action(Action::ScrollDown);
8827
8828 assert_eq!(app.iam_state.policy_scroll, 20);
8829 }
8830
8831 #[test]
8832 fn test_ctrl_u_scrolls_up_in_policy_view() {
8833 let mut app = test_app();
8834 app.current_service = Service::IamRoles;
8835 app.service_selected = true;
8836 app.mode = Mode::Normal;
8837 app.view_mode = ViewMode::PolicyView;
8838 app.iam_state.current_role = Some("TestRole".to_string());
8839 app.iam_state.current_policy = Some("TestPolicy".to_string());
8840 app.iam_state.policy_document = (0..100)
8841 .map(|i| format!("line {}", i))
8842 .collect::<Vec<_>>()
8843 .join("\n");
8844 app.iam_state.policy_scroll = 30;
8845
8846 app.handle_action(Action::ScrollUp);
8847
8848 assert_eq!(app.iam_state.policy_scroll, 20);
8849
8850 app.handle_action(Action::ScrollUp);
8851
8852 assert_eq!(app.iam_state.policy_scroll, 10);
8853 }
8854
8855 #[test]
8856 fn test_scroll_does_not_go_negative() {
8857 let mut app = test_app();
8858 app.current_service = Service::IamRoles;
8859 app.service_selected = true;
8860 app.mode = Mode::Normal;
8861 app.view_mode = ViewMode::PolicyView;
8862 app.iam_state.current_role = Some("TestRole".to_string());
8863 app.iam_state.current_policy = Some("TestPolicy".to_string());
8864 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8865 app.iam_state.policy_scroll = 0;
8866
8867 app.handle_action(Action::ScrollUp);
8868
8869 assert_eq!(app.iam_state.policy_scroll, 0);
8870 }
8871
8872 #[test]
8873 fn test_scroll_does_not_exceed_max() {
8874 let mut app = test_app();
8875 app.current_service = Service::IamRoles;
8876 app.service_selected = true;
8877 app.mode = Mode::Normal;
8878 app.view_mode = ViewMode::PolicyView;
8879 app.iam_state.current_role = Some("TestRole".to_string());
8880 app.iam_state.current_policy = Some("TestPolicy".to_string());
8881 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8882 app.iam_state.policy_scroll = 0;
8883
8884 app.handle_action(Action::ScrollDown);
8885
8886 assert_eq!(app.iam_state.policy_scroll, 2); }
8888
8889 #[test]
8890 fn test_policy_view_console_url() {
8891 let mut app = test_app();
8892 app.current_service = Service::IamRoles;
8893 app.service_selected = true;
8894 app.view_mode = ViewMode::PolicyView;
8895 app.iam_state.current_role = Some("TestRole".to_string());
8896 app.iam_state.current_policy = Some("TestPolicy".to_string());
8897
8898 let url = app.get_console_url();
8899
8900 assert!(url.contains("us-east-1.console.aws.amazon.com"));
8901 assert!(url.contains("/roles/details/TestRole"));
8902 assert!(url.contains("/editPolicy/TestPolicy"));
8903 assert!(url.contains("step=addPermissions"));
8904 }
8905
8906 #[test]
8907 fn test_esc_from_policy_view_goes_to_role_detail() {
8908 let mut app = test_app();
8909 app.current_service = Service::IamRoles;
8910 app.service_selected = true;
8911 app.mode = Mode::Normal;
8912 app.view_mode = ViewMode::PolicyView;
8913 app.iam_state.current_role = Some("TestRole".to_string());
8914 app.iam_state.current_policy = Some("TestPolicy".to_string());
8915 app.iam_state.policy_document = "test".to_string();
8916 app.iam_state.policy_scroll = 5;
8917
8918 app.handle_action(Action::GoBack);
8919
8920 assert_eq!(app.view_mode, ViewMode::Detail);
8921 assert_eq!(app.iam_state.current_policy, None);
8922 assert_eq!(app.iam_state.policy_document, "");
8923 assert_eq!(app.iam_state.policy_scroll, 0);
8924 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
8925 }
8926
8927 #[test]
8928 fn test_esc_from_role_detail_goes_to_role_list() {
8929 let mut app = test_app();
8930 app.current_service = Service::IamRoles;
8931 app.service_selected = true;
8932 app.mode = Mode::Normal;
8933 app.view_mode = ViewMode::Detail;
8934 app.iam_state.current_role = Some("TestRole".to_string());
8935
8936 app.handle_action(Action::GoBack);
8937
8938 assert_eq!(app.iam_state.current_role, None);
8939 }
8940
8941 #[test]
8942 fn test_right_arrow_expands_policy_row() {
8943 let mut app = test_app();
8944 app.current_service = Service::IamRoles;
8945 app.service_selected = true;
8946 app.mode = Mode::Normal;
8947 app.view_mode = ViewMode::Detail;
8948 app.iam_state.current_role = Some("TestRole".to_string());
8949 app.iam_state.policies.items = vec![crate::iam::Policy {
8950 policy_name: "TestPolicy".to_string(),
8951 policy_type: "Inline".to_string(),
8952 attached_via: "Direct".to_string(),
8953 attached_entities: "1".to_string(),
8954 description: "Test".to_string(),
8955 creation_time: "2023-01-01".to_string(),
8956 edited_time: "2023-01-01".to_string(),
8957 policy_arn: None,
8958 }];
8959 app.iam_state.policies.reset();
8960
8961 app.handle_action(Action::NextPane);
8962
8963 assert_eq!(app.view_mode, ViewMode::Detail);
8965 assert_eq!(app.iam_state.current_policy, None);
8966 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
8967 }
8968}
8969
8970#[cfg(test)]
8971mod tab_filter_tests {
8972 use super::*;
8973 use test_helpers::*;
8974
8975 #[test]
8976 fn test_space_t_opens_tab_picker() {
8977 let mut app = test_app();
8978 app.tabs = vec![
8979 Tab {
8980 service: Service::CloudWatchLogGroups,
8981 title: "Tab 1".to_string(),
8982 breadcrumb: "CloudWatch > Log groups".to_string(),
8983 },
8984 Tab {
8985 service: Service::S3Buckets,
8986 title: "Tab 2".to_string(),
8987 breadcrumb: "S3 > Buckets".to_string(),
8988 },
8989 ];
8990 app.current_tab = 0;
8991
8992 app.handle_action(Action::OpenTabPicker);
8993
8994 assert_eq!(app.mode, Mode::TabPicker);
8995 assert_eq!(app.tab_picker_selected, 0);
8996 }
8997
8998 #[test]
8999 fn test_tab_filter_works() {
9000 let mut app = test_app();
9001 app.tabs = vec![
9002 Tab {
9003 service: Service::CloudWatchLogGroups,
9004 title: "CloudWatch Logs".to_string(),
9005 breadcrumb: "CloudWatch > Log groups".to_string(),
9006 },
9007 Tab {
9008 service: Service::S3Buckets,
9009 title: "S3 Buckets".to_string(),
9010 breadcrumb: "S3 > Buckets".to_string(),
9011 },
9012 Tab {
9013 service: Service::CloudWatchAlarms,
9014 title: "CloudWatch Alarms".to_string(),
9015 breadcrumb: "CloudWatch > Alarms".to_string(),
9016 },
9017 ];
9018 app.mode = Mode::TabPicker;
9019
9020 app.handle_action(Action::FilterInput('s'));
9022 app.handle_action(Action::FilterInput('3'));
9023
9024 let filtered = app.get_filtered_tabs();
9025 assert_eq!(filtered.len(), 1);
9026 assert_eq!(filtered[0].1.title, "S3 Buckets");
9027 }
9028
9029 #[test]
9030 fn test_tab_filter_by_breadcrumb() {
9031 let mut app = test_app();
9032 app.tabs = vec![
9033 Tab {
9034 service: Service::CloudWatchLogGroups,
9035 title: "Tab 1".to_string(),
9036 breadcrumb: "CloudWatch > Log groups".to_string(),
9037 },
9038 Tab {
9039 service: Service::S3Buckets,
9040 title: "Tab 2".to_string(),
9041 breadcrumb: "S3 > Buckets".to_string(),
9042 },
9043 ];
9044 app.mode = Mode::TabPicker;
9045
9046 app.handle_action(Action::FilterInput('c'));
9048 app.handle_action(Action::FilterInput('l'));
9049 app.handle_action(Action::FilterInput('o'));
9050 app.handle_action(Action::FilterInput('u'));
9051 app.handle_action(Action::FilterInput('d'));
9052
9053 let filtered = app.get_filtered_tabs();
9054 assert_eq!(filtered.len(), 1);
9055 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
9056 }
9057
9058 #[test]
9059 fn test_tab_filter_backspace() {
9060 let mut app = test_app();
9061 app.tabs = vec![
9062 Tab {
9063 service: Service::CloudWatchLogGroups,
9064 title: "CloudWatch Logs".to_string(),
9065 breadcrumb: "CloudWatch > Log groups".to_string(),
9066 },
9067 Tab {
9068 service: Service::S3Buckets,
9069 title: "S3 Buckets".to_string(),
9070 breadcrumb: "S3 > Buckets".to_string(),
9071 },
9072 ];
9073 app.mode = Mode::TabPicker;
9074
9075 app.handle_action(Action::FilterInput('s'));
9076 app.handle_action(Action::FilterInput('3'));
9077 assert_eq!(app.tab_filter, "s3");
9078
9079 app.handle_action(Action::FilterBackspace);
9080 assert_eq!(app.tab_filter, "s");
9081
9082 let filtered = app.get_filtered_tabs();
9083 assert_eq!(filtered.len(), 2); }
9085
9086 #[test]
9087 fn test_tab_selection_with_filter() {
9088 let mut app = test_app();
9089 app.tabs = vec![
9090 Tab {
9091 service: Service::CloudWatchLogGroups,
9092 title: "CloudWatch Logs".to_string(),
9093 breadcrumb: "CloudWatch > Log groups".to_string(),
9094 },
9095 Tab {
9096 service: Service::S3Buckets,
9097 title: "S3 Buckets".to_string(),
9098 breadcrumb: "S3 > Buckets".to_string(),
9099 },
9100 ];
9101 app.mode = Mode::TabPicker;
9102 app.current_tab = 0;
9103
9104 app.handle_action(Action::FilterInput('s'));
9106 app.handle_action(Action::FilterInput('3'));
9107
9108 app.handle_action(Action::Select);
9110
9111 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
9113 assert_eq!(app.tab_filter, ""); }
9115}
9116
9117#[cfg(test)]
9118mod region_latency_tests {
9119 use super::*;
9120 use test_helpers::*;
9121
9122 #[test]
9123 fn test_regions_sorted_by_latency() {
9124 let mut app = test_app();
9125
9126 app.region_latencies.insert("us-west-2".to_string(), 50);
9128 app.region_latencies.insert("us-east-1".to_string(), 10);
9129 app.region_latencies.insert("eu-west-1".to_string(), 100);
9130
9131 let filtered = app.get_filtered_regions();
9132
9133 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
9135
9136 assert!(with_latency.len() >= 3);
9137 assert_eq!(with_latency[0].code, "us-east-1");
9138 assert_eq!(with_latency[0].latency_ms, Some(10));
9139 assert_eq!(with_latency[1].code, "us-west-2");
9140 assert_eq!(with_latency[1].latency_ms, Some(50));
9141 assert_eq!(with_latency[2].code, "eu-west-1");
9142 assert_eq!(with_latency[2].latency_ms, Some(100));
9143 }
9144
9145 #[test]
9146 fn test_regions_with_latency_before_without() {
9147 let mut app = test_app();
9148
9149 app.region_latencies.insert("eu-west-1".to_string(), 100);
9151
9152 let filtered = app.get_filtered_regions();
9153
9154 assert_eq!(filtered[0].code, "eu-west-1");
9156 assert_eq!(filtered[0].latency_ms, Some(100));
9157
9158 for region in &filtered[1..] {
9160 assert!(region.latency_ms.is_none());
9161 }
9162 }
9163
9164 #[test]
9165 fn test_region_filter_with_latency() {
9166 let mut app = test_app();
9167
9168 app.region_latencies.insert("us-east-1".to_string(), 10);
9169 app.region_latencies.insert("us-west-2".to_string(), 50);
9170 app.region_filter = "us".to_string();
9171
9172 let filtered = app.get_filtered_regions();
9173
9174 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
9176 assert_eq!(filtered[0].code, "us-east-1");
9177 assert_eq!(filtered[1].code, "us-west-2");
9178 }
9179
9180 #[test]
9181 fn test_latency_persists_across_filters() {
9182 let mut app = test_app();
9183
9184 app.region_latencies.insert("us-east-1".to_string(), 10);
9185
9186 app.region_filter = "eu".to_string();
9188 let filtered = app.get_filtered_regions();
9189 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
9190
9191 app.region_filter.clear();
9193 let all = app.get_filtered_regions();
9194
9195 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
9197 assert_eq!(us_east.latency_ms, Some(10));
9198 }
9199
9200 #[test]
9201 fn test_measure_region_latencies_clears_previous() {
9202 let mut app = test_app();
9203
9204 app.region_latencies.insert("us-east-1".to_string(), 100);
9206 app.region_latencies.insert("eu-west-1".to_string(), 200);
9207
9208 app.measure_region_latencies();
9210
9211 assert!(
9213 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
9214 );
9215 }
9216
9217 #[test]
9218 fn test_regions_with_latency_sorted_first() {
9219 let mut app = test_app();
9220
9221 app.region_latencies.insert("us-east-1".to_string(), 50);
9223 app.region_latencies.insert("eu-west-1".to_string(), 500);
9224
9225 let filtered = app.get_filtered_regions();
9226
9227 assert!(filtered.len() > 2);
9229
9230 assert_eq!(filtered[0].code, "us-east-1");
9232 assert_eq!(filtered[0].latency_ms, Some(50));
9233 assert_eq!(filtered[1].code, "eu-west-1");
9234 assert_eq!(filtered[1].latency_ms, Some(500));
9235
9236 for region in &filtered[2..] {
9238 assert!(region.latency_ms.is_none());
9239 }
9240 }
9241
9242 #[test]
9243 fn test_regions_without_latency_sorted_as_1000ms() {
9244 let mut app = test_app();
9245
9246 app.region_latencies
9248 .insert("ap-southeast-2".to_string(), 1500);
9249 app.region_latencies.insert("us-east-1".to_string(), 50);
9251
9252 let filtered = app.get_filtered_regions();
9253
9254 assert_eq!(filtered[0].code, "us-east-1");
9256 assert_eq!(filtered[0].latency_ms, Some(50));
9257
9258 let slow_region_idx = filtered
9260 .iter()
9261 .position(|r| r.code == "ap-southeast-2")
9262 .unwrap();
9263 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
9267 assert!(region.latency_ms.is_none());
9268 }
9269 }
9270
9271 #[test]
9272 fn test_region_picker_opens_with_latencies() {
9273 let mut app = test_app();
9274
9275 app.region_filter.clear();
9277 app.region_picker_selected = 0;
9278 app.measure_region_latencies();
9279
9280 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
9283 }
9284
9285 #[test]
9286 fn test_ecr_tab_next() {
9287 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
9288 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
9289 }
9290
9291 #[test]
9292 fn test_ecr_tab_switching() {
9293 let mut app = test_app();
9294 app.current_service = Service::EcrRepositories;
9295 app.service_selected = true;
9296 app.ecr_state.tab = EcrTab::Private;
9297
9298 app.handle_action(Action::NextDetailTab);
9299 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9300 assert_eq!(app.ecr_state.repositories.selected, 0);
9301
9302 app.handle_action(Action::NextDetailTab);
9303 assert_eq!(app.ecr_state.tab, EcrTab::Private);
9304 }
9305
9306 #[test]
9307 fn test_ecr_navigation() {
9308 let mut app = test_app();
9309 app.current_service = Service::EcrRepositories;
9310 app.service_selected = true;
9311 app.mode = Mode::Normal;
9312 app.ecr_state.repositories.items = vec![
9313 EcrRepository {
9314 name: "repo1".to_string(),
9315 uri: "uri1".to_string(),
9316 created_at: "2023-01-01".to_string(),
9317 tag_immutability: "MUTABLE".to_string(),
9318 encryption_type: "AES256".to_string(),
9319 },
9320 EcrRepository {
9321 name: "repo2".to_string(),
9322 uri: "uri2".to_string(),
9323 created_at: "2023-01-02".to_string(),
9324 tag_immutability: "IMMUTABLE".to_string(),
9325 encryption_type: "KMS".to_string(),
9326 },
9327 ];
9328
9329 app.handle_action(Action::NextItem);
9330 assert_eq!(app.ecr_state.repositories.selected, 1);
9331
9332 app.handle_action(Action::PrevItem);
9333 assert_eq!(app.ecr_state.repositories.selected, 0);
9334 }
9335
9336 #[test]
9337 fn test_ecr_filter() {
9338 let mut app = test_app();
9339 app.current_service = Service::EcrRepositories;
9340 app.service_selected = true;
9341 app.ecr_state.repositories.items = vec![
9342 EcrRepository {
9343 name: "my-app".to_string(),
9344 uri: "uri1".to_string(),
9345 created_at: "2023-01-01".to_string(),
9346 tag_immutability: "MUTABLE".to_string(),
9347 encryption_type: "AES256".to_string(),
9348 },
9349 EcrRepository {
9350 name: "other-service".to_string(),
9351 uri: "uri2".to_string(),
9352 created_at: "2023-01-02".to_string(),
9353 tag_immutability: "IMMUTABLE".to_string(),
9354 encryption_type: "KMS".to_string(),
9355 },
9356 ];
9357
9358 app.ecr_state.repositories.filter = "app".to_string();
9359 let filtered = app.filtered_ecr_repositories();
9360 assert_eq!(filtered.len(), 1);
9361 assert_eq!(filtered[0].name, "my-app");
9362 }
9363
9364 #[test]
9365 fn test_ecr_filter_input() {
9366 let mut app = test_app();
9367 app.current_service = Service::EcrRepositories;
9368 app.service_selected = true;
9369 app.mode = Mode::FilterInput;
9370
9371 app.handle_action(Action::FilterInput('t'));
9372 app.handle_action(Action::FilterInput('e'));
9373 app.handle_action(Action::FilterInput('s'));
9374 app.handle_action(Action::FilterInput('t'));
9375 assert_eq!(app.ecr_state.repositories.filter, "test");
9376
9377 app.handle_action(Action::FilterBackspace);
9378 assert_eq!(app.ecr_state.repositories.filter, "tes");
9379 }
9380
9381 #[test]
9382 fn test_iam_users_filter_input() {
9383 let mut app = test_app();
9384 app.current_service = Service::IamUsers;
9385 app.service_selected = true;
9386 app.mode = Mode::FilterInput;
9387
9388 app.handle_action(Action::FilterInput('a'));
9389 app.handle_action(Action::FilterInput('d'));
9390 app.handle_action(Action::FilterInput('m'));
9391 app.handle_action(Action::FilterInput('i'));
9392 app.handle_action(Action::FilterInput('n'));
9393 assert_eq!(app.iam_state.users.filter, "admin");
9394
9395 app.handle_action(Action::FilterBackspace);
9396 assert_eq!(app.iam_state.users.filter, "admi");
9397 }
9398
9399 #[test]
9400 fn test_iam_policies_filter_input() {
9401 let mut app = test_app();
9402 app.current_service = Service::IamUsers;
9403 app.service_selected = true;
9404 app.iam_state.current_user = Some("testuser".to_string());
9405 app.mode = Mode::FilterInput;
9406
9407 app.handle_action(Action::FilterInput('r'));
9408 app.handle_action(Action::FilterInput('e'));
9409 app.handle_action(Action::FilterInput('a'));
9410 app.handle_action(Action::FilterInput('d'));
9411 assert_eq!(app.iam_state.policies.filter, "read");
9412
9413 app.handle_action(Action::FilterBackspace);
9414 assert_eq!(app.iam_state.policies.filter, "rea");
9415 }
9416
9417 #[test]
9418 fn test_iam_start_filter() {
9419 let mut app = test_app();
9420 app.current_service = Service::IamUsers;
9421 app.service_selected = true;
9422 app.mode = Mode::Normal;
9423
9424 app.handle_action(Action::StartFilter);
9425 assert_eq!(app.mode, Mode::FilterInput);
9426 }
9427
9428 #[test]
9429 fn test_iam_roles_filter_input() {
9430 let mut app = test_app();
9431 app.current_service = Service::IamRoles;
9432 app.service_selected = true;
9433 app.mode = Mode::FilterInput;
9434
9435 app.handle_action(Action::FilterInput('a'));
9436 app.handle_action(Action::FilterInput('d'));
9437 app.handle_action(Action::FilterInput('m'));
9438 app.handle_action(Action::FilterInput('i'));
9439 app.handle_action(Action::FilterInput('n'));
9440 assert_eq!(app.iam_state.roles.filter, "admin");
9441
9442 app.handle_action(Action::FilterBackspace);
9443 assert_eq!(app.iam_state.roles.filter, "admi");
9444 }
9445
9446 #[test]
9447 fn test_iam_roles_start_filter() {
9448 let mut app = test_app();
9449 app.current_service = Service::IamRoles;
9450 app.service_selected = true;
9451 app.mode = Mode::Normal;
9452
9453 app.handle_action(Action::StartFilter);
9454 assert_eq!(app.mode, Mode::FilterInput);
9455 }
9456
9457 #[test]
9458 fn test_iam_roles_navigation() {
9459 let mut app = test_app();
9460 app.current_service = Service::IamRoles;
9461 app.service_selected = true;
9462 app.mode = Mode::Normal;
9463 app.iam_state.roles.items = (0..10)
9464 .map(|i| crate::iam::IamRole {
9465 role_name: format!("role{}", i),
9466 path: "/".to_string(),
9467 trusted_entities: String::new(),
9468 last_activity: String::new(),
9469 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9470 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9471 description: String::new(),
9472 max_session_duration: "3600 seconds".to_string(),
9473 })
9474 .collect();
9475
9476 assert_eq!(app.iam_state.roles.selected, 0);
9477
9478 app.handle_action(Action::NextItem);
9479 assert_eq!(app.iam_state.roles.selected, 1);
9480
9481 app.handle_action(Action::NextItem);
9482 assert_eq!(app.iam_state.roles.selected, 2);
9483
9484 app.handle_action(Action::PrevItem);
9485 assert_eq!(app.iam_state.roles.selected, 1);
9486 }
9487
9488 #[test]
9489 fn test_iam_roles_page_hotkey() {
9490 let mut app = test_app();
9491 app.current_service = Service::IamRoles;
9492 app.service_selected = true;
9493 app.mode = Mode::Normal;
9494 app.iam_state.roles.page_size = PageSize::Ten;
9495 app.iam_state.roles.items = (0..100)
9496 .map(|i| crate::iam::IamRole {
9497 role_name: format!("role{}", i),
9498 path: "/".to_string(),
9499 trusted_entities: String::new(),
9500 last_activity: String::new(),
9501 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9502 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9503 description: String::new(),
9504 max_session_duration: "3600 seconds".to_string(),
9505 })
9506 .collect();
9507
9508 app.handle_action(Action::FilterInput('2'));
9509 app.handle_action(Action::OpenColumnSelector);
9510 assert_eq!(app.iam_state.roles.selected, 10); }
9512
9513 #[test]
9514 fn test_iam_users_page_hotkey() {
9515 let mut app = test_app();
9516 app.current_service = Service::IamUsers;
9517 app.service_selected = true;
9518 app.mode = Mode::Normal;
9519 app.iam_state.users.page_size = PageSize::Ten;
9520 app.iam_state.users.items = (0..100)
9521 .map(|i| crate::iam::IamUser {
9522 user_name: format!("user{}", i),
9523 path: "/".to_string(),
9524 groups: String::new(),
9525 last_activity: String::new(),
9526 mfa: String::new(),
9527 password_age: String::new(),
9528 console_last_sign_in: String::new(),
9529 access_key_id: String::new(),
9530 active_key_age: String::new(),
9531 access_key_last_used: String::new(),
9532 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
9533 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9534 console_access: String::new(),
9535 signing_certs: String::new(),
9536 })
9537 .collect();
9538
9539 app.handle_action(Action::FilterInput('3'));
9540 app.handle_action(Action::OpenColumnSelector);
9541 assert_eq!(app.iam_state.users.selected, 20); }
9543
9544 #[test]
9545 fn test_ecr_scroll_navigation() {
9546 let mut app = test_app();
9547 app.current_service = Service::EcrRepositories;
9548 app.service_selected = true;
9549 app.ecr_state.repositories.items = (0..20)
9550 .map(|i| EcrRepository {
9551 name: format!("repo{}", i),
9552 uri: format!("uri{}", i),
9553 created_at: "2023-01-01".to_string(),
9554 tag_immutability: "MUTABLE".to_string(),
9555 encryption_type: "AES256".to_string(),
9556 })
9557 .collect();
9558
9559 app.handle_action(Action::ScrollDown);
9560 assert_eq!(app.ecr_state.repositories.selected, 10);
9561
9562 app.handle_action(Action::ScrollUp);
9563 assert_eq!(app.ecr_state.repositories.selected, 0);
9564 }
9565
9566 #[test]
9567 fn test_ecr_tab_switching_triggers_reload() {
9568 let mut app = test_app();
9569 app.current_service = Service::EcrRepositories;
9570 app.service_selected = true;
9571 app.ecr_state.tab = EcrTab::Private;
9572 app.ecr_state.repositories.loading = false;
9573 app.ecr_state.repositories.items = vec![EcrRepository {
9574 name: "private-repo".to_string(),
9575 uri: "uri".to_string(),
9576 created_at: "2023-01-01".to_string(),
9577 tag_immutability: "MUTABLE".to_string(),
9578 encryption_type: "AES256".to_string(),
9579 }];
9580
9581 app.handle_action(Action::NextDetailTab);
9582 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9583 assert!(app.ecr_state.repositories.loading);
9584 assert_eq!(app.ecr_state.repositories.selected, 0);
9585 }
9586
9587 #[test]
9588 fn test_ecr_tab_cycles_between_private_and_public() {
9589 let mut app = test_app();
9590 app.current_service = Service::EcrRepositories;
9591 app.service_selected = true;
9592 app.ecr_state.tab = EcrTab::Private;
9593
9594 app.handle_action(Action::NextDetailTab);
9595 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9596
9597 app.handle_action(Action::NextDetailTab);
9598 assert_eq!(app.ecr_state.tab, EcrTab::Private);
9599 }
9600
9601 #[test]
9602 fn test_page_size_values() {
9603 assert_eq!(PageSize::Ten.value(), 10);
9604 assert_eq!(PageSize::TwentyFive.value(), 25);
9605 assert_eq!(PageSize::Fifty.value(), 50);
9606 assert_eq!(PageSize::OneHundred.value(), 100);
9607 }
9608
9609 #[test]
9610 fn test_page_size_next() {
9611 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
9612 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
9613 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
9614 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
9615 }
9616
9617 #[test]
9618 fn test_ecr_enter_drills_into_repository() {
9619 let mut app = test_app();
9620 app.current_service = Service::EcrRepositories;
9621 app.service_selected = true;
9622 app.mode = Mode::Normal;
9623 app.ecr_state.repositories.items = vec![EcrRepository {
9624 name: "my-repo".to_string(),
9625 uri: "uri".to_string(),
9626 created_at: "2023-01-01".to_string(),
9627 tag_immutability: "MUTABLE".to_string(),
9628 encryption_type: "AES256".to_string(),
9629 }];
9630
9631 app.handle_action(Action::Select);
9632 assert_eq!(
9633 app.ecr_state.current_repository,
9634 Some("my-repo".to_string())
9635 );
9636 assert!(app.ecr_state.repositories.loading);
9637 }
9638
9639 #[test]
9640 fn test_ecr_repository_expansion() {
9641 let mut app = test_app();
9642 app.current_service = Service::EcrRepositories;
9643 app.service_selected = true;
9644 app.ecr_state.repositories.items = vec![EcrRepository {
9645 name: "my-repo".to_string(),
9646 uri: "uri".to_string(),
9647 created_at: "2023-01-01".to_string(),
9648 tag_immutability: "MUTABLE".to_string(),
9649 encryption_type: "AES256".to_string(),
9650 }];
9651 app.ecr_state.repositories.selected = 0;
9652
9653 assert_eq!(app.ecr_state.repositories.expanded_item, None);
9654
9655 app.handle_action(Action::NextPane);
9656 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
9657
9658 app.handle_action(Action::PrevPane);
9659 assert_eq!(app.ecr_state.repositories.expanded_item, None);
9660 }
9661
9662 #[test]
9663 fn test_ecr_ctrl_d_scrolls_down() {
9664 let mut app = test_app();
9665 app.current_service = Service::EcrRepositories;
9666 app.service_selected = true;
9667 app.mode = Mode::Normal;
9668 app.ecr_state.repositories.items = (0..30)
9669 .map(|i| EcrRepository {
9670 name: format!("repo{}", i),
9671 uri: format!("uri{}", i),
9672 created_at: "2023-01-01".to_string(),
9673 tag_immutability: "MUTABLE".to_string(),
9674 encryption_type: "AES256".to_string(),
9675 })
9676 .collect();
9677 app.ecr_state.repositories.selected = 0;
9678
9679 app.handle_action(Action::PageDown);
9680 assert_eq!(app.ecr_state.repositories.selected, 10);
9681 }
9682
9683 #[test]
9684 fn test_ecr_ctrl_u_scrolls_up() {
9685 let mut app = test_app();
9686 app.current_service = Service::EcrRepositories;
9687 app.service_selected = true;
9688 app.mode = Mode::Normal;
9689 app.ecr_state.repositories.items = (0..30)
9690 .map(|i| EcrRepository {
9691 name: format!("repo{}", i),
9692 uri: format!("uri{}", i),
9693 created_at: "2023-01-01".to_string(),
9694 tag_immutability: "MUTABLE".to_string(),
9695 encryption_type: "AES256".to_string(),
9696 })
9697 .collect();
9698 app.ecr_state.repositories.selected = 15;
9699
9700 app.handle_action(Action::PageUp);
9701 assert_eq!(app.ecr_state.repositories.selected, 5);
9702 }
9703
9704 #[test]
9705 fn test_ecr_images_ctrl_d_scrolls_down() {
9706 let mut app = test_app();
9707 app.current_service = Service::EcrRepositories;
9708 app.service_selected = true;
9709 app.mode = Mode::Normal;
9710 app.ecr_state.current_repository = Some("repo".to_string());
9711 app.ecr_state.images.items = (0..30)
9712 .map(|i| EcrImage {
9713 tag: format!("tag{}", i),
9714 artifact_type: "container".to_string(),
9715 pushed_at: "2023-01-01T12:00:00Z".to_string(),
9716 size_bytes: 104857600,
9717 uri: format!("uri{}", i),
9718 digest: format!("sha256:{}", i),
9719 last_pull_time: String::new(),
9720 })
9721 .collect();
9722 app.ecr_state.images.selected = 0;
9723
9724 app.handle_action(Action::PageDown);
9725 assert_eq!(app.ecr_state.images.selected, 10);
9726 }
9727
9728 #[test]
9729 fn test_ecr_esc_goes_back_from_images_to_repos() {
9730 let mut app = test_app();
9731 app.current_service = Service::EcrRepositories;
9732 app.service_selected = true;
9733 app.mode = Mode::Normal;
9734 app.ecr_state.current_repository = Some("my-repo".to_string());
9735 app.ecr_state.images.items = vec![EcrImage {
9736 tag: "latest".to_string(),
9737 artifact_type: "container".to_string(),
9738 pushed_at: "2023-01-01T12:00:00Z".to_string(),
9739 size_bytes: 104857600,
9740 uri: "uri".to_string(),
9741 digest: "sha256:abc".to_string(),
9742 last_pull_time: String::new(),
9743 }];
9744
9745 app.handle_action(Action::GoBack);
9746 assert_eq!(app.ecr_state.current_repository, None);
9747 assert!(app.ecr_state.images.items.is_empty());
9748 }
9749
9750 #[test]
9751 fn test_ecr_esc_collapses_expanded_image_first() {
9752 let mut app = test_app();
9753 app.current_service = Service::EcrRepositories;
9754 app.service_selected = true;
9755 app.mode = Mode::Normal;
9756 app.ecr_state.current_repository = Some("my-repo".to_string());
9757 app.ecr_state.images.expanded_item = Some(0);
9758
9759 app.handle_action(Action::GoBack);
9760 assert_eq!(app.ecr_state.images.expanded_item, None);
9761 assert_eq!(
9762 app.ecr_state.current_repository,
9763 Some("my-repo".to_string())
9764 );
9765 }
9766
9767 #[test]
9768 fn test_pagination_with_lowercase_p() {
9769 let mut app = test_app();
9770 app.current_service = Service::EcrRepositories;
9771 app.service_selected = true;
9772 app.mode = Mode::Normal;
9773 app.ecr_state.repositories.items = (0..100)
9774 .map(|i| EcrRepository {
9775 name: format!("repo{}", i),
9776 uri: format!("uri{}", i),
9777 created_at: "2023-01-01".to_string(),
9778 tag_immutability: "MUTABLE".to_string(),
9779 encryption_type: "AES256".to_string(),
9780 })
9781 .collect();
9782
9783 app.handle_action(Action::FilterInput('2'));
9785 assert_eq!(app.page_input, "2");
9786
9787 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
9791
9792 #[test]
9793 fn test_lowercase_p_without_number_opens_preferences() {
9794 let mut app = test_app();
9795 app.current_service = Service::EcrRepositories;
9796 app.service_selected = true;
9797 app.mode = Mode::Normal;
9798
9799 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
9801 }
9802
9803 #[test]
9804 fn test_ctrl_o_generates_correct_console_url() {
9805 let mut app = test_app();
9806 app.current_service = Service::EcrRepositories;
9807 app.service_selected = true;
9808 app.mode = Mode::Normal;
9809 app.config.account_id = "123456789012".to_string();
9810
9811 let url = app.get_console_url();
9813 assert!(url.contains("ecr/private-registry/repositories"));
9814 assert!(url.contains("region=us-east-1"));
9815
9816 app.ecr_state.current_repository = Some("my-repo".to_string());
9818 let url = app.get_console_url();
9819 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
9820 assert!(url.contains("region=us-east-1"));
9821 }
9822
9823 #[test]
9824 fn test_page_input_display_and_reset() {
9825 let mut app = test_app();
9826 app.current_service = Service::EcrRepositories;
9827 app.service_selected = true;
9828 app.mode = Mode::Normal;
9829 app.ecr_state.repositories.items = (0..100)
9830 .map(|i| EcrRepository {
9831 name: format!("repo{}", i),
9832 uri: format!("uri{}", i),
9833 created_at: "2023-01-01".to_string(),
9834 tag_immutability: "MUTABLE".to_string(),
9835 encryption_type: "AES256".to_string(),
9836 })
9837 .collect();
9838
9839 app.handle_action(Action::FilterInput('2'));
9841 assert_eq!(app.page_input, "2");
9842
9843 app.handle_action(Action::OpenColumnSelector);
9845 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
9848
9849 #[test]
9850 fn test_page_navigation_updates_scroll_offset_for_cfn() {
9851 let mut app = test_app();
9852 app.current_service = Service::CloudFormationStacks;
9853 app.service_selected = true;
9854 app.mode = Mode::Normal;
9855 app.cfn_state.table.items = (0..100)
9856 .map(|i| crate::cfn::Stack {
9857 name: format!("stack-{}", i),
9858 stack_id: format!(
9859 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9860 i
9861 ),
9862 status: "CREATE_COMPLETE".to_string(),
9863 created_time: "2023-01-01T00:00:00Z".to_string(),
9864 updated_time: "2023-01-01T00:00:00Z".to_string(),
9865 deleted_time: String::new(),
9866 drift_status: "IN_SYNC".to_string(),
9867 last_drift_check_time: String::new(),
9868 status_reason: String::new(),
9869 description: String::new(),
9870 detailed_status: String::new(),
9871 root_stack: String::new(),
9872 parent_stack: String::new(),
9873 termination_protection: false,
9874 iam_role: String::new(),
9875 tags: vec![],
9876 stack_policy: String::new(),
9877 rollback_monitoring_time: String::new(),
9878 rollback_alarms: vec![],
9879 notification_arns: vec![],
9880 })
9881 .collect();
9882
9883 app.handle_action(Action::FilterInput('2'));
9885 assert_eq!(app.page_input, "2");
9886
9887 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
9892 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
9894 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
9895
9896 let current_page = app.cfn_state.table.scroll_offset / page_size;
9898 assert_eq!(
9899 current_page, 1,
9900 "2p should go to page 2 (0-indexed as 1), not page 3"
9901 ); }
9903
9904 #[test]
9905 fn test_3p_goes_to_page_3_not_page_5() {
9906 let mut app = test_app();
9907 app.current_service = Service::CloudFormationStacks;
9908 app.service_selected = true;
9909 app.mode = Mode::Normal;
9910 app.cfn_state.table.items = (0..200)
9911 .map(|i| crate::cfn::Stack {
9912 name: format!("stack-{}", i),
9913 stack_id: format!(
9914 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9915 i
9916 ),
9917 status: "CREATE_COMPLETE".to_string(),
9918 created_time: "2023-01-01T00:00:00Z".to_string(),
9919 updated_time: "2023-01-01T00:00:00Z".to_string(),
9920 deleted_time: String::new(),
9921 drift_status: "IN_SYNC".to_string(),
9922 last_drift_check_time: String::new(),
9923 status_reason: String::new(),
9924 description: String::new(),
9925 detailed_status: String::new(),
9926 root_stack: String::new(),
9927 parent_stack: String::new(),
9928 termination_protection: false,
9929 iam_role: String::new(),
9930 tags: vec![],
9931 stack_policy: String::new(),
9932 rollback_monitoring_time: String::new(),
9933 rollback_alarms: vec![],
9934 notification_arns: vec![],
9935 })
9936 .collect();
9937
9938 app.handle_action(Action::FilterInput('3'));
9940 app.handle_action(Action::OpenColumnSelector);
9941
9942 let page_size = app.cfn_state.table.page_size.value();
9943 let current_page = app.cfn_state.table.scroll_offset / page_size;
9944 assert_eq!(
9945 current_page, 2,
9946 "3p should go to page 3 (0-indexed as 2), not page 5"
9947 );
9948 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
9949 }
9950
9951 #[test]
9952 fn test_log_streams_page_navigation_uses_correct_page_size() {
9953 let mut app = test_app();
9954 app.current_service = Service::CloudWatchLogGroups;
9955 app.view_mode = ViewMode::Detail;
9956 app.service_selected = true;
9957 app.mode = Mode::Normal;
9958 app.log_groups_state.log_streams = (0..100)
9959 .map(|i| LogStream {
9960 name: format!("stream-{}", i),
9961 creation_time: None,
9962 last_event_time: None,
9963 })
9964 .collect();
9965
9966 app.handle_action(Action::FilterInput('2'));
9968 app.handle_action(Action::OpenColumnSelector);
9969
9970 assert_eq!(app.log_groups_state.selected_stream, 20);
9972
9973 let page_size = 20;
9975 let current_page = app.log_groups_state.selected_stream / page_size;
9976 assert_eq!(
9977 current_page, 1,
9978 "2p should go to page 2 (0-indexed as 1), not page 3"
9979 );
9980 }
9981
9982 #[test]
9983 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
9984 let mut app = test_app();
9985 app.current_service = Service::EcrRepositories;
9986 app.service_selected = true;
9987 app.mode = Mode::Normal;
9988 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
9990 .map(|i| EcrRepository {
9991 name: format!("repo{}", i),
9992 uri: format!("uri{}", i),
9993 created_at: "2023-01-01".to_string(),
9994 tag_immutability: "MUTABLE".to_string(),
9995 encryption_type: "AES256".to_string(),
9996 })
9997 .collect();
9998
9999 app.handle_action(Action::FilterInput('3'));
10001 app.handle_action(Action::OpenColumnSelector);
10002
10003 assert_eq!(app.ecr_state.repositories.selected, 50);
10005
10006 let page_size = app.ecr_state.repositories.page_size.value();
10007 let current_page = app.ecr_state.repositories.selected / page_size;
10008 assert_eq!(
10009 current_page, 2,
10010 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
10011 );
10012 }
10013
10014 #[test]
10015 fn test_page_navigation_updates_scroll_offset_for_alarms() {
10016 let mut app = test_app();
10017 app.current_service = Service::CloudWatchAlarms;
10018 app.service_selected = true;
10019 app.mode = Mode::Normal;
10020 app.alarms_state.table.items = (0..100)
10021 .map(|i| crate::cw::alarms::Alarm {
10022 name: format!("alarm-{}", i),
10023 state: "OK".to_string(),
10024 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
10025 description: String::new(),
10026 metric_name: "CPUUtilization".to_string(),
10027 namespace: "AWS/EC2".to_string(),
10028 statistic: "Average".to_string(),
10029 period: 300,
10030 comparison_operator: "GreaterThanThreshold".to_string(),
10031 threshold: 80.0,
10032 actions_enabled: true,
10033 state_reason: String::new(),
10034 resource: String::new(),
10035 dimensions: String::new(),
10036 expression: String::new(),
10037 alarm_type: "MetricAlarm".to_string(),
10038 cross_account: String::new(),
10039 })
10040 .collect();
10041
10042 app.handle_action(Action::FilterInput('2'));
10044 app.handle_action(Action::OpenColumnSelector);
10045
10046 let page_size = app.alarms_state.table.page_size.value();
10048 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
10050 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
10051 }
10052
10053 #[test]
10054 fn test_ecr_pagination_with_65_repos() {
10055 let mut app = test_app();
10056 app.current_service = Service::EcrRepositories;
10057 app.service_selected = true;
10058 app.mode = Mode::Normal;
10059 app.ecr_state.repositories.items = (0..65)
10060 .map(|i| EcrRepository {
10061 name: format!("repo{:02}", i),
10062 uri: format!("uri{}", i),
10063 created_at: "2023-01-01".to_string(),
10064 tag_immutability: "MUTABLE".to_string(),
10065 encryption_type: "AES256".to_string(),
10066 })
10067 .collect();
10068
10069 assert_eq!(app.ecr_state.repositories.selected, 0);
10071 let page_size = 50;
10072 let current_page = app.ecr_state.repositories.selected / page_size;
10073 assert_eq!(current_page, 0);
10074
10075 app.handle_action(Action::FilterInput('2'));
10077 app.handle_action(Action::OpenColumnSelector);
10078 assert_eq!(app.ecr_state.repositories.selected, 50);
10079
10080 let current_page = app.ecr_state.repositories.selected / page_size;
10082 assert_eq!(current_page, 1);
10083 }
10084
10085 #[test]
10086 fn test_ecr_repos_input_focus_tab_cycling() {
10087 let mut app = test_app();
10088 app.current_service = Service::EcrRepositories;
10089 app.service_selected = true;
10090 app.mode = Mode::FilterInput;
10091 app.ecr_state.input_focus = InputFocus::Filter;
10092
10093 app.handle_action(Action::NextFilterFocus);
10095 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10096
10097 app.handle_action(Action::NextFilterFocus);
10099 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10100
10101 app.handle_action(Action::PrevFilterFocus);
10103 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10104
10105 app.handle_action(Action::PrevFilterFocus);
10107 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10108 }
10109
10110 #[test]
10111 fn test_ecr_images_column_toggle_not_off_by_one() {
10112 use crate::ecr::image::Column as ImageColumn;
10113 let mut app = test_app();
10114 app.current_service = Service::EcrRepositories;
10115 app.service_selected = true;
10116 app.mode = Mode::ColumnSelector;
10117 app.ecr_state.current_repository = Some("test-repo".to_string());
10118
10119 app.visible_ecr_image_columns = ImageColumn::all();
10121 let initial_count = app.visible_ecr_image_columns.len();
10122
10123 app.column_selector_index = 0;
10125 app.handle_action(Action::ToggleColumn);
10126
10127 assert_eq!(app.visible_ecr_image_columns.len(), initial_count - 1);
10129 assert!(!app.visible_ecr_image_columns.contains(&ImageColumn::Tag));
10130
10131 app.handle_action(Action::ToggleColumn);
10133 assert_eq!(app.visible_ecr_image_columns.len(), initial_count);
10134 assert!(app.visible_ecr_image_columns.contains(&ImageColumn::Tag));
10135 }
10136
10137 #[test]
10138 fn test_ecr_repos_column_toggle_works() {
10139 use crate::ecr::repo::Column;
10140 let mut app = test_app();
10141 app.current_service = Service::EcrRepositories;
10142 app.service_selected = true;
10143 app.mode = Mode::ColumnSelector;
10144 app.ecr_state.current_repository = None;
10145
10146 app.visible_ecr_columns = Column::all();
10148 let initial_count = app.visible_ecr_columns.len();
10149
10150 app.column_selector_index = 0;
10152 app.handle_action(Action::ToggleColumn);
10153
10154 assert_eq!(app.visible_ecr_columns.len(), initial_count - 1);
10156 assert!(!app.visible_ecr_columns.contains(&Column::Name));
10157
10158 app.handle_action(Action::ToggleColumn);
10160 assert_eq!(app.visible_ecr_columns.len(), initial_count);
10161 assert!(app.visible_ecr_columns.contains(&Column::Name));
10162 }
10163
10164 #[test]
10165 fn test_ecr_repos_pagination_left_right_navigation() {
10166 use crate::ecr::repo::Repository as EcrRepository;
10167 let mut app = test_app();
10168 app.current_service = Service::EcrRepositories;
10169 app.service_selected = true;
10170 app.mode = Mode::FilterInput;
10171 app.ecr_state.input_focus = InputFocus::Pagination;
10172
10173 app.ecr_state.repositories.items = (0..150)
10175 .map(|i| EcrRepository {
10176 name: format!("repo{:03}", i),
10177 uri: format!("uri{}", i),
10178 created_at: "2023-01-01".to_string(),
10179 tag_immutability: "MUTABLE".to_string(),
10180 encryption_type: "AES256".to_string(),
10181 })
10182 .collect();
10183
10184 app.ecr_state.repositories.selected = 0;
10186 eprintln!(
10187 "Initial: selected={}, focus={:?}, mode={:?}",
10188 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
10189 );
10190
10191 app.handle_action(Action::PageDown);
10193 eprintln!(
10194 "After PageDown: selected={}",
10195 app.ecr_state.repositories.selected
10196 );
10197 assert_eq!(app.ecr_state.repositories.selected, 50);
10198
10199 app.handle_action(Action::PageDown);
10201 eprintln!(
10202 "After 2nd PageDown: selected={}",
10203 app.ecr_state.repositories.selected
10204 );
10205 assert_eq!(app.ecr_state.repositories.selected, 100);
10206
10207 app.handle_action(Action::PageDown);
10209 eprintln!(
10210 "After 3rd PageDown: selected={}",
10211 app.ecr_state.repositories.selected
10212 );
10213 assert_eq!(app.ecr_state.repositories.selected, 100);
10214
10215 app.handle_action(Action::PageUp);
10217 eprintln!(
10218 "After PageUp: selected={}",
10219 app.ecr_state.repositories.selected
10220 );
10221 assert_eq!(app.ecr_state.repositories.selected, 50);
10222
10223 app.handle_action(Action::PageUp);
10225 eprintln!(
10226 "After 2nd PageUp: selected={}",
10227 app.ecr_state.repositories.selected
10228 );
10229 assert_eq!(app.ecr_state.repositories.selected, 0);
10230
10231 app.handle_action(Action::PageUp);
10233 eprintln!(
10234 "After 3rd PageUp: selected={}",
10235 app.ecr_state.repositories.selected
10236 );
10237 assert_eq!(app.ecr_state.repositories.selected, 0);
10238 }
10239
10240 #[test]
10241 fn test_ecr_repos_filter_input_when_input_focused() {
10242 use crate::ecr::repo::Repository as EcrRepository;
10243 let mut app = test_app();
10244 app.current_service = Service::EcrRepositories;
10245 app.service_selected = true;
10246 app.mode = Mode::FilterInput;
10247 app.ecr_state.input_focus = InputFocus::Filter;
10248
10249 app.ecr_state.repositories.items = vec![
10251 EcrRepository {
10252 name: "test-repo".to_string(),
10253 uri: "uri1".to_string(),
10254 created_at: "2023-01-01".to_string(),
10255 tag_immutability: "MUTABLE".to_string(),
10256 encryption_type: "AES256".to_string(),
10257 },
10258 EcrRepository {
10259 name: "prod-repo".to_string(),
10260 uri: "uri2".to_string(),
10261 created_at: "2023-01-01".to_string(),
10262 tag_immutability: "MUTABLE".to_string(),
10263 encryption_type: "AES256".to_string(),
10264 },
10265 ];
10266
10267 assert_eq!(app.ecr_state.repositories.filter, "");
10269 app.handle_action(Action::FilterInput('t'));
10270 assert_eq!(app.ecr_state.repositories.filter, "t");
10271 app.handle_action(Action::FilterInput('e'));
10272 assert_eq!(app.ecr_state.repositories.filter, "te");
10273 app.handle_action(Action::FilterInput('s'));
10274 assert_eq!(app.ecr_state.repositories.filter, "tes");
10275 app.handle_action(Action::FilterInput('t'));
10276 assert_eq!(app.ecr_state.repositories.filter, "test");
10277 }
10278
10279 #[test]
10280 fn test_ecr_repos_digit_input_when_pagination_focused() {
10281 use crate::ecr::repo::Repository as EcrRepository;
10282 let mut app = test_app();
10283 app.current_service = Service::EcrRepositories;
10284 app.service_selected = true;
10285 app.mode = Mode::FilterInput;
10286 app.ecr_state.input_focus = InputFocus::Pagination;
10287
10288 app.ecr_state.repositories.items = vec![EcrRepository {
10290 name: "test-repo".to_string(),
10291 uri: "uri1".to_string(),
10292 created_at: "2023-01-01".to_string(),
10293 tag_immutability: "MUTABLE".to_string(),
10294 encryption_type: "AES256".to_string(),
10295 }];
10296
10297 assert_eq!(app.ecr_state.repositories.filter, "");
10299 assert_eq!(app.page_input, "");
10300 app.handle_action(Action::FilterInput('2'));
10301 assert_eq!(app.ecr_state.repositories.filter, "");
10302 assert_eq!(app.page_input, "2");
10303
10304 app.handle_action(Action::FilterInput('a'));
10306 assert_eq!(app.ecr_state.repositories.filter, "");
10307 assert_eq!(app.page_input, "2");
10308 }
10309
10310 #[test]
10311 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
10312 use crate::ecr::repo::Repository as EcrRepository;
10313 let mut app = test_app();
10314 app.current_service = Service::EcrRepositories;
10315 app.service_selected = true;
10316 app.mode = Mode::FilterInput;
10317 app.ecr_state.input_focus = InputFocus::Filter;
10318
10319 app.ecr_state.repositories.items = (0..150)
10321 .map(|i| EcrRepository {
10322 name: format!("repo{:03}", i),
10323 uri: format!("uri{}", i),
10324 created_at: "2023-01-01".to_string(),
10325 tag_immutability: "MUTABLE".to_string(),
10326 encryption_type: "AES256".to_string(),
10327 })
10328 .collect();
10329
10330 app.ecr_state.repositories.selected = 0;
10332
10333 app.handle_action(Action::PageDown);
10335 assert_eq!(
10336 app.ecr_state.repositories.selected, 10,
10337 "Should scroll down by 10"
10338 );
10339
10340 app.handle_action(Action::PageUp);
10341 assert_eq!(
10342 app.ecr_state.repositories.selected, 0,
10343 "Should scroll back up"
10344 );
10345 }
10346
10347 #[test]
10348 fn test_ecr_repos_pagination_control_actually_works() {
10349 use crate::ecr::repo::Repository as EcrRepository;
10350
10351 let mut app = test_app();
10353 app.current_service = Service::EcrRepositories;
10354 app.service_selected = true;
10355 app.mode = Mode::FilterInput;
10356 app.ecr_state.current_repository = None;
10357 app.ecr_state.input_focus = InputFocus::Pagination;
10358
10359 app.ecr_state.repositories.items = (0..100)
10361 .map(|i| EcrRepository {
10362 name: format!("repo{:03}", i),
10363 uri: format!("uri{}", i),
10364 created_at: "2023-01-01".to_string(),
10365 tag_immutability: "MUTABLE".to_string(),
10366 encryption_type: "AES256".to_string(),
10367 })
10368 .collect();
10369
10370 app.ecr_state.repositories.selected = 0;
10371
10372 assert_eq!(app.mode, Mode::FilterInput);
10374 assert_eq!(app.current_service, Service::EcrRepositories);
10375 assert_eq!(app.ecr_state.current_repository, None);
10376 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10377
10378 app.handle_action(Action::PageDown);
10380 assert_eq!(
10381 app.ecr_state.repositories.selected, 50,
10382 "PageDown should move to page 2"
10383 );
10384
10385 app.handle_action(Action::PageUp);
10386 assert_eq!(
10387 app.ecr_state.repositories.selected, 0,
10388 "PageUp should move back to page 1"
10389 );
10390 }
10391
10392 #[test]
10393 fn test_ecr_repos_start_filter_resets_focus_to_input() {
10394 let mut app = test_app();
10395 app.current_service = Service::EcrRepositories;
10396 app.service_selected = true;
10397 app.mode = Mode::Normal;
10398 app.ecr_state.current_repository = None;
10399
10400 app.ecr_state.input_focus = InputFocus::Pagination;
10402
10403 app.handle_action(Action::StartFilter);
10405
10406 assert_eq!(app.mode, Mode::FilterInput);
10408 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10409 }
10410
10411 #[test]
10412 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
10413 use crate::ecr::repo::Repository as EcrRepository;
10414
10415 let mut app = test_app();
10416 app.current_service = Service::EcrRepositories;
10417 app.service_selected = true;
10418 app.mode = Mode::Normal;
10419 app.ecr_state.current_repository = None;
10420
10421 app.ecr_state.repositories.items = (0..100)
10423 .map(|i| EcrRepository {
10424 name: format!("repo{:03}", i),
10425 uri: format!("uri{}", i),
10426 created_at: "2023-01-01".to_string(),
10427 tag_immutability: "MUTABLE".to_string(),
10428 encryption_type: "AES256".to_string(),
10429 })
10430 .collect();
10431
10432 app.ecr_state.repositories.selected = 0;
10433
10434 app.handle_action(Action::StartFilter);
10436 assert_eq!(app.mode, Mode::FilterInput);
10437 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10438
10439 app.handle_action(Action::NextFilterFocus);
10441 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10442
10443 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
10445 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
10446 app.handle_action(Action::PageDown);
10447 eprintln!(
10448 "After PageDown: selected={}",
10449 app.ecr_state.repositories.selected
10450 );
10451
10452 assert_eq!(
10454 app.ecr_state.repositories.selected, 50,
10455 "Right arrow should move to page 2"
10456 );
10457
10458 app.handle_action(Action::PageUp);
10460 assert_eq!(
10461 app.ecr_state.repositories.selected, 0,
10462 "Left arrow should move back to page 1"
10463 );
10464 }
10465
10466 #[test]
10467 fn test_service_picker_i_key_activates_filter() {
10468 let mut app = test_app();
10469
10470 assert_eq!(app.mode, Mode::ServicePicker);
10472 assert!(app.service_picker.filter.is_empty());
10473
10474 app.handle_action(Action::FilterInput('i'));
10476
10477 assert_eq!(app.mode, Mode::ServicePicker);
10479 assert_eq!(app.service_picker.filter, "i");
10480 }
10481
10482 #[test]
10483 fn test_service_picker_typing_filters_services() {
10484 let mut app = test_app();
10485
10486 assert_eq!(app.mode, Mode::ServicePicker);
10488
10489 app.handle_action(Action::FilterInput('s'));
10491 app.handle_action(Action::FilterInput('3'));
10492
10493 assert_eq!(app.service_picker.filter, "s3");
10494 assert_eq!(app.mode, Mode::ServicePicker);
10495 }
10496
10497 #[test]
10498 fn test_service_picker_resets_on_open() {
10499 let mut app = test_app();
10500
10501 app.service_selected = true;
10503 app.mode = Mode::Normal;
10504
10505 app.service_picker.filter = "previous".to_string();
10507 app.service_picker.selected = 5;
10508
10509 app.handle_action(Action::OpenSpaceMenu);
10511
10512 assert_eq!(app.mode, Mode::SpaceMenu);
10514 assert!(app.service_picker.filter.is_empty());
10515 assert_eq!(app.service_picker.selected, 0);
10516 }
10517
10518 #[test]
10519 fn test_no_pii_in_test_data() {
10520 let test_repo = EcrRepository {
10522 name: "test-repo".to_string(),
10523 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
10524 created_at: "2024-01-01".to_string(),
10525 tag_immutability: "MUTABLE".to_string(),
10526 encryption_type: "AES256".to_string(),
10527 };
10528
10529 assert!(test_repo.uri.starts_with("123456789012"));
10531 assert!(!test_repo.uri.contains("123456789013")); }
10533
10534 #[test]
10535 fn test_lambda_versions_tab_triggers_loading() {
10536 let mut app = test_app();
10537 app.current_service = Service::LambdaFunctions;
10538 app.service_selected = true;
10539
10540 app.lambda_state.current_function = Some("test-function".to_string());
10542 app.lambda_state.detail_tab = LambdaDetailTab::Code;
10543
10544 assert!(app.lambda_state.version_table.items.is_empty());
10546
10547 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10549
10550 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
10553 assert!(app.lambda_state.current_function.is_some());
10554 }
10555
10556 #[test]
10557 fn test_lambda_versions_navigation() {
10558 use crate::lambda::Version;
10559
10560 let mut app = test_app();
10561 app.current_service = Service::LambdaFunctions;
10562 app.service_selected = true;
10563 app.lambda_state.current_function = Some("test-function".to_string());
10564 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10565
10566 app.lambda_state.version_table.items = vec![
10568 Version {
10569 version: "3".to_string(),
10570 aliases: "prod".to_string(),
10571 description: "".to_string(),
10572 last_modified: "".to_string(),
10573 architecture: "X86_64".to_string(),
10574 },
10575 Version {
10576 version: "2".to_string(),
10577 aliases: "".to_string(),
10578 description: "".to_string(),
10579 last_modified: "".to_string(),
10580 architecture: "X86_64".to_string(),
10581 },
10582 Version {
10583 version: "1".to_string(),
10584 aliases: "".to_string(),
10585 description: "".to_string(),
10586 last_modified: "".to_string(),
10587 architecture: "X86_64".to_string(),
10588 },
10589 ];
10590
10591 assert_eq!(app.lambda_state.version_table.items.len(), 3);
10593 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
10594 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
10595
10596 app.lambda_state.version_table.selected = 1;
10598 assert_eq!(app.lambda_state.version_table.selected, 1);
10599 }
10600
10601 #[test]
10602 fn test_lambda_versions_with_aliases() {
10603 use crate::lambda::Version;
10604
10605 let version = Version {
10606 version: "35".to_string(),
10607 aliases: "prod, staging".to_string(),
10608 description: "Production version".to_string(),
10609 last_modified: "2024-01-01".to_string(),
10610 architecture: "X86_64".to_string(),
10611 };
10612
10613 assert_eq!(version.aliases, "prod, staging");
10614 assert!(!version.aliases.is_empty());
10615 }
10616
10617 #[test]
10618 fn test_lambda_versions_expansion() {
10619 use crate::lambda::Version;
10620
10621 let mut app = test_app();
10622 app.current_service = Service::LambdaFunctions;
10623 app.service_selected = true;
10624 app.lambda_state.current_function = Some("test-function".to_string());
10625 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10626
10627 app.lambda_state.version_table.items = vec![
10629 Version {
10630 version: "2".to_string(),
10631 aliases: "prod".to_string(),
10632 description: "Production".to_string(),
10633 last_modified: "2024-01-01".to_string(),
10634 architecture: "X86_64".to_string(),
10635 },
10636 Version {
10637 version: "1".to_string(),
10638 aliases: "".to_string(),
10639 description: "".to_string(),
10640 last_modified: "2024-01-01".to_string(),
10641 architecture: "Arm64".to_string(),
10642 },
10643 ];
10644
10645 app.lambda_state.version_table.selected = 0;
10646
10647 app.lambda_state.version_table.expanded_item = Some(0);
10649 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
10650
10651 app.lambda_state.version_table.selected = 1;
10653 app.lambda_state.version_table.expanded_item = Some(1);
10654 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
10655 }
10656
10657 #[test]
10658 fn test_lambda_versions_page_navigation() {
10659 use crate::lambda::Version;
10660
10661 let mut app = test_app();
10662 app.current_service = Service::LambdaFunctions;
10663 app.service_selected = true;
10664 app.lambda_state.current_function = Some("test-function".to_string());
10665 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10666
10667 app.lambda_state.version_table.items = (1..=30)
10669 .map(|i| Version {
10670 version: i.to_string(),
10671 aliases: "".to_string(),
10672 description: "".to_string(),
10673 last_modified: "".to_string(),
10674 architecture: "X86_64".to_string(),
10675 })
10676 .collect();
10677
10678 app.lambda_state.version_table.page_size = PageSize::Ten;
10679 app.lambda_state.version_table.selected = 0;
10680
10681 app.page_input = "2".to_string();
10683 app.handle_action(Action::OpenColumnSelector);
10684
10685 assert_eq!(app.lambda_state.version_table.selected, 10);
10687 }
10688
10689 #[test]
10690 fn test_lambda_versions_pagination_arrow_keys() {
10691 use crate::lambda::Version;
10692
10693 let mut app = test_app();
10694 app.current_service = Service::LambdaFunctions;
10695 app.service_selected = true;
10696 app.lambda_state.current_function = Some("test-function".to_string());
10697 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10698 app.mode = Mode::FilterInput;
10699 app.lambda_state.version_input_focus = InputFocus::Pagination;
10700
10701 app.lambda_state.version_table.items = (1..=30)
10703 .map(|i| Version {
10704 version: i.to_string(),
10705 aliases: "".to_string(),
10706 description: "".to_string(),
10707 last_modified: "".to_string(),
10708 architecture: "X86_64".to_string(),
10709 })
10710 .collect();
10711
10712 app.lambda_state.version_table.page_size = PageSize::Ten;
10713 app.lambda_state.version_table.selected = 0;
10714
10715 app.handle_action(Action::PageDown);
10717 assert_eq!(app.lambda_state.version_table.selected, 10);
10718
10719 app.handle_action(Action::PageUp);
10721 assert_eq!(app.lambda_state.version_table.selected, 0);
10722 }
10723
10724 #[test]
10725 fn test_lambda_versions_page_input_in_filter_mode() {
10726 use crate::lambda::Version;
10727
10728 let mut app = test_app();
10729 app.current_service = Service::LambdaFunctions;
10730 app.service_selected = true;
10731 app.lambda_state.current_function = Some("test-function".to_string());
10732 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10733 app.mode = Mode::FilterInput;
10734 app.lambda_state.version_input_focus = InputFocus::Pagination;
10735
10736 app.lambda_state.version_table.items = (1..=30)
10738 .map(|i| Version {
10739 version: i.to_string(),
10740 aliases: "".to_string(),
10741 description: "".to_string(),
10742 last_modified: "".to_string(),
10743 architecture: "X86_64".to_string(),
10744 })
10745 .collect();
10746
10747 app.lambda_state.version_table.page_size = PageSize::Ten;
10748 app.lambda_state.version_table.selected = 0;
10749
10750 app.handle_action(Action::FilterInput('2'));
10752 assert_eq!(app.page_input, "2");
10753 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
10757 assert_eq!(app.lambda_state.version_table.selected, 10);
10758 assert_eq!(app.page_input, ""); }
10760
10761 #[test]
10762 fn test_lambda_versions_filter_input() {
10763 use crate::lambda::Version;
10764
10765 let mut app = test_app();
10766 app.current_service = Service::LambdaFunctions;
10767 app.service_selected = true;
10768 app.lambda_state.current_function = Some("test-function".to_string());
10769 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10770 app.mode = Mode::FilterInput;
10771 app.lambda_state.version_input_focus = InputFocus::Filter;
10772
10773 app.lambda_state.version_table.items = vec![
10775 Version {
10776 version: "1".to_string(),
10777 aliases: "prod".to_string(),
10778 description: "Production".to_string(),
10779 last_modified: "".to_string(),
10780 architecture: "X86_64".to_string(),
10781 },
10782 Version {
10783 version: "2".to_string(),
10784 aliases: "staging".to_string(),
10785 description: "Staging".to_string(),
10786 last_modified: "".to_string(),
10787 architecture: "X86_64".to_string(),
10788 },
10789 ];
10790
10791 app.handle_action(Action::FilterInput('p'));
10793 app.handle_action(Action::FilterInput('r'));
10794 app.handle_action(Action::FilterInput('o'));
10795 app.handle_action(Action::FilterInput('d'));
10796 assert_eq!(app.lambda_state.version_table.filter, "prod");
10797
10798 app.handle_action(Action::FilterBackspace);
10800 assert_eq!(app.lambda_state.version_table.filter, "pro");
10801 }
10802
10803 #[test]
10804 fn test_lambda_aliases_table_expansion() {
10805 use crate::lambda::Alias;
10806
10807 let mut app = test_app();
10808 app.current_service = Service::LambdaFunctions;
10809 app.service_selected = true;
10810 app.lambda_state.current_function = Some("test-function".to_string());
10811 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
10812 app.mode = Mode::Normal;
10813
10814 app.lambda_state.alias_table.items = vec![
10815 Alias {
10816 name: "prod".to_string(),
10817 versions: "1".to_string(),
10818 description: "Production alias".to_string(),
10819 },
10820 Alias {
10821 name: "staging".to_string(),
10822 versions: "2".to_string(),
10823 description: "Staging alias".to_string(),
10824 },
10825 ];
10826
10827 app.lambda_state.alias_table.selected = 0;
10828
10829 app.handle_action(Action::Select);
10831 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
10832 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
10833
10834 app.handle_action(Action::GoBack);
10836 assert_eq!(app.lambda_state.current_alias, None);
10837 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
10838
10839 app.lambda_state.alias_table.selected = 1;
10841 app.handle_action(Action::Select);
10842 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
10843 }
10844
10845 #[test]
10846 fn test_lambda_versions_arrow_key_expansion() {
10847 use crate::lambda::Version;
10848
10849 let mut app = test_app();
10850 app.current_service = Service::LambdaFunctions;
10851 app.service_selected = true;
10852 app.lambda_state.current_function = Some("test-function".to_string());
10853 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10854 app.mode = Mode::Normal;
10855
10856 app.lambda_state.version_table.items = vec![Version {
10857 version: "1".to_string(),
10858 aliases: "prod".to_string(),
10859 description: "Production".to_string(),
10860 last_modified: "2024-01-01".to_string(),
10861 architecture: "X86_64".to_string(),
10862 }];
10863
10864 app.lambda_state.version_table.selected = 0;
10865
10866 app.handle_action(Action::NextPane);
10868 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
10869
10870 app.handle_action(Action::PrevPane);
10872 assert_eq!(app.lambda_state.version_table.expanded_item, None);
10873 }
10874
10875 #[test]
10876 fn test_lambda_version_detail_view() {
10877 use crate::lambda::Function;
10878
10879 let mut app = test_app();
10880 app.current_service = Service::LambdaFunctions;
10881 app.service_selected = true;
10882 app.lambda_state.current_function = Some("test-function".to_string());
10883 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10884 app.mode = Mode::Normal;
10885
10886 app.lambda_state.table.items = vec![Function {
10887 name: "test-function".to_string(),
10888 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
10889 application: None,
10890 description: "Test".to_string(),
10891 package_type: "Zip".to_string(),
10892 runtime: "python3.12".to_string(),
10893 architecture: "X86_64".to_string(),
10894 code_size: 1024,
10895 code_sha256: "hash".to_string(),
10896 memory_mb: 128,
10897 timeout_seconds: 30,
10898 last_modified: "2024-01-01".to_string(),
10899 layers: vec![],
10900 }];
10901
10902 app.lambda_state.version_table.items = vec![crate::lambda::Version {
10903 version: "1".to_string(),
10904 aliases: "prod".to_string(),
10905 description: "Production".to_string(),
10906 last_modified: "2024-01-01".to_string(),
10907 architecture: "X86_64".to_string(),
10908 }];
10909
10910 app.lambda_state.version_table.selected = 0;
10911
10912 app.handle_action(Action::Select);
10914 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
10915 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
10916
10917 app.handle_action(Action::GoBack);
10919 assert_eq!(app.lambda_state.current_version, None);
10920 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
10921 }
10922
10923 #[test]
10924 fn test_lambda_version_detail_tabs() {
10925 use crate::lambda::Function;
10926
10927 let mut app = test_app();
10928 app.current_service = Service::LambdaFunctions;
10929 app.service_selected = true;
10930 app.lambda_state.current_function = Some("test-function".to_string());
10931 app.lambda_state.current_version = Some("1".to_string());
10932 app.lambda_state.detail_tab = LambdaDetailTab::Code;
10933 app.mode = Mode::Normal;
10934
10935 app.lambda_state.table.items = vec![Function {
10936 name: "test-function".to_string(),
10937 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
10938 application: None,
10939 description: "Test".to_string(),
10940 package_type: "Zip".to_string(),
10941 runtime: "python3.12".to_string(),
10942 architecture: "X86_64".to_string(),
10943 code_size: 1024,
10944 code_sha256: "hash".to_string(),
10945 memory_mb: 128,
10946 timeout_seconds: 30,
10947 last_modified: "2024-01-01".to_string(),
10948 layers: vec![],
10949 }];
10950
10951 app.handle_action(Action::NextDetailTab);
10953 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
10954
10955 app.handle_action(Action::NextDetailTab);
10956 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
10957
10958 app.handle_action(Action::PrevDetailTab);
10960 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
10961 }
10962
10963 #[test]
10964 fn test_lambda_aliases_arrow_key_expansion() {
10965 use crate::lambda::Alias;
10966
10967 let mut app = test_app();
10968 app.current_service = Service::LambdaFunctions;
10969 app.service_selected = true;
10970 app.lambda_state.current_function = Some("test-function".to_string());
10971 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
10972 app.mode = Mode::Normal;
10973
10974 app.lambda_state.alias_table.items = vec![Alias {
10975 name: "prod".to_string(),
10976 versions: "1".to_string(),
10977 description: "Production alias".to_string(),
10978 }];
10979
10980 app.lambda_state.alias_table.selected = 0;
10981
10982 app.handle_action(Action::NextPane);
10984 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
10985
10986 app.handle_action(Action::PrevPane);
10988 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
10989 }
10990
10991 #[test]
10992 fn test_lambda_functions_arrow_key_expansion() {
10993 use crate::lambda::Function;
10994
10995 let mut app = test_app();
10996 app.current_service = Service::LambdaFunctions;
10997 app.service_selected = true;
10998 app.mode = Mode::Normal;
10999
11000 app.lambda_state.table.items = vec![Function {
11001 name: "test-function".to_string(),
11002 arn: "arn".to_string(),
11003 application: None,
11004 description: "Test".to_string(),
11005 package_type: "Zip".to_string(),
11006 runtime: "python3.12".to_string(),
11007 architecture: "X86_64".to_string(),
11008 code_size: 1024,
11009 code_sha256: "hash".to_string(),
11010 memory_mb: 128,
11011 timeout_seconds: 30,
11012 last_modified: "2024-01-01".to_string(),
11013 layers: vec![],
11014 }];
11015
11016 app.lambda_state.table.selected = 0;
11017
11018 app.handle_action(Action::NextPane);
11020 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
11021
11022 app.handle_action(Action::PrevPane);
11024 assert_eq!(app.lambda_state.table.expanded_item, None);
11025 }
11026
11027 #[test]
11028 fn test_lambda_version_detail_with_application() {
11029 use crate::lambda::Function;
11030
11031 let mut app = test_app();
11032 app.current_service = Service::LambdaFunctions;
11033 app.service_selected = true;
11034 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
11035 app.lambda_state.current_version = Some("1".to_string());
11036 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11037 app.mode = Mode::Normal;
11038
11039 app.lambda_state.table.items = vec![Function {
11040 name: "storefront-studio-beta-api".to_string(),
11041 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
11042 .to_string(),
11043 application: Some("storefront-studio-beta".to_string()),
11044 description: "API function".to_string(),
11045 package_type: "Zip".to_string(),
11046 runtime: "python3.12".to_string(),
11047 architecture: "X86_64".to_string(),
11048 code_size: 1024,
11049 code_sha256: "hash".to_string(),
11050 memory_mb: 128,
11051 timeout_seconds: 30,
11052 last_modified: "2024-01-01".to_string(),
11053 layers: vec![],
11054 }];
11055
11056 assert_eq!(
11058 app.lambda_state.table.items[0].application,
11059 Some("storefront-studio-beta".to_string())
11060 );
11061 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11062 }
11063
11064 #[test]
11065 fn test_lambda_layer_navigation() {
11066 use crate::lambda::{Function, Layer};
11067
11068 let mut app = test_app();
11069 app.current_service = Service::LambdaFunctions;
11070 app.service_selected = true;
11071 app.lambda_state.current_function = Some("test-function".to_string());
11072 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11073 app.mode = Mode::Normal;
11074
11075 app.lambda_state.table.items = vec![Function {
11076 name: "test-function".to_string(),
11077 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11078 application: None,
11079 description: "Test".to_string(),
11080 package_type: "Zip".to_string(),
11081 runtime: "python3.12".to_string(),
11082 architecture: "X86_64".to_string(),
11083 code_size: 1024,
11084 code_sha256: "hash".to_string(),
11085 memory_mb: 128,
11086 timeout_seconds: 30,
11087 last_modified: "2024-01-01".to_string(),
11088 layers: vec![
11089 Layer {
11090 arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11091 code_size: 1024,
11092 },
11093 Layer {
11094 arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11095 code_size: 2048,
11096 },
11097 Layer {
11098 arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
11099 code_size: 3072,
11100 },
11101 ],
11102 }];
11103
11104 assert_eq!(app.lambda_state.layer_selected, 0);
11105
11106 app.handle_action(Action::NextItem);
11107 assert_eq!(app.lambda_state.layer_selected, 1);
11108
11109 app.handle_action(Action::NextItem);
11110 assert_eq!(app.lambda_state.layer_selected, 2);
11111
11112 app.handle_action(Action::NextItem);
11113 assert_eq!(app.lambda_state.layer_selected, 2);
11114
11115 app.handle_action(Action::PrevItem);
11116 assert_eq!(app.lambda_state.layer_selected, 1);
11117
11118 app.handle_action(Action::PrevItem);
11119 assert_eq!(app.lambda_state.layer_selected, 0);
11120
11121 app.handle_action(Action::PrevItem);
11122 assert_eq!(app.lambda_state.layer_selected, 0);
11123 }
11124
11125 #[test]
11126 fn test_lambda_layer_expansion() {
11127 use crate::lambda::{Function, Layer};
11128
11129 let mut app = test_app();
11130 app.current_service = Service::LambdaFunctions;
11131 app.service_selected = true;
11132 app.lambda_state.current_function = Some("test-function".to_string());
11133 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11134 app.mode = Mode::Normal;
11135
11136 app.lambda_state.table.items = vec![Function {
11137 name: "test-function".to_string(),
11138 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11139 application: None,
11140 description: "Test".to_string(),
11141 package_type: "Zip".to_string(),
11142 runtime: "python3.12".to_string(),
11143 architecture: "X86_64".to_string(),
11144 code_size: 1024,
11145 code_sha256: "hash".to_string(),
11146 memory_mb: 128,
11147 timeout_seconds: 30,
11148 last_modified: "2024-01-01".to_string(),
11149 layers: vec![Layer {
11150 arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
11151 code_size: 1024,
11152 }],
11153 }];
11154
11155 assert_eq!(app.lambda_state.layer_expanded, None);
11156
11157 app.handle_action(Action::NextPane);
11158 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11159
11160 app.handle_action(Action::PrevPane);
11161 assert_eq!(app.lambda_state.layer_expanded, None);
11162
11163 app.handle_action(Action::NextPane);
11164 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11165
11166 app.handle_action(Action::NextPane);
11167 assert_eq!(app.lambda_state.layer_expanded, None);
11168 }
11169
11170 #[test]
11171 fn test_lambda_layer_selection_and_expansion_workflow() {
11172 use crate::lambda::{Function, Layer};
11173
11174 let mut app = test_app();
11175 app.current_service = Service::LambdaFunctions;
11176 app.service_selected = true;
11177 app.lambda_state.current_function = Some("test-function".to_string());
11178 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11179 app.mode = Mode::Normal;
11180
11181 app.lambda_state.table.items = vec![Function {
11182 name: "test-function".to_string(),
11183 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11184 application: None,
11185 description: "Test".to_string(),
11186 package_type: "Zip".to_string(),
11187 runtime: "python3.12".to_string(),
11188 architecture: "X86_64".to_string(),
11189 code_size: 1024,
11190 code_sha256: "hash".to_string(),
11191 memory_mb: 128,
11192 timeout_seconds: 30,
11193 last_modified: "2024-01-01".to_string(),
11194 layers: vec![
11195 Layer {
11196 arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11197 code_size: 1024,
11198 },
11199 Layer {
11200 arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11201 code_size: 2048,
11202 },
11203 ],
11204 }];
11205
11206 assert_eq!(app.lambda_state.layer_selected, 0);
11208 assert_eq!(app.lambda_state.layer_expanded, None);
11209
11210 app.handle_action(Action::NextPane);
11212 assert_eq!(app.lambda_state.layer_selected, 0);
11213 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11214
11215 app.handle_action(Action::NextItem);
11217 assert_eq!(app.lambda_state.layer_selected, 1);
11218 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
11222 assert_eq!(app.lambda_state.layer_selected, 1);
11223 assert_eq!(app.lambda_state.layer_expanded, Some(1));
11224
11225 app.handle_action(Action::PrevPane);
11227 assert_eq!(app.lambda_state.layer_selected, 1);
11228 assert_eq!(app.lambda_state.layer_expanded, None);
11229
11230 app.handle_action(Action::PrevItem);
11232 assert_eq!(app.lambda_state.layer_selected, 0);
11233 assert_eq!(app.lambda_state.layer_expanded, None);
11234 }
11235
11236 #[test]
11237 fn test_backtab_cycles_detail_tabs_backward() {
11238 let mut app = test_app();
11239 app.mode = Mode::Normal;
11240
11241 app.current_service = Service::LambdaFunctions;
11243 app.service_selected = true;
11244 app.lambda_state.current_function = Some("test-function".to_string());
11245 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11246
11247 app.handle_action(Action::PrevDetailTab);
11248 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11249
11250 app.handle_action(Action::PrevDetailTab);
11251 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11252
11253 app.current_service = Service::IamRoles;
11255 app.iam_state.current_role = Some("test-role".to_string());
11256 app.iam_state.role_tab = RoleTab::Permissions;
11257
11258 app.handle_action(Action::PrevDetailTab);
11259 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11260
11261 app.current_service = Service::IamUsers;
11263 app.iam_state.current_user = Some("test-user".to_string());
11264 app.iam_state.user_tab = UserTab::Permissions;
11265
11266 app.handle_action(Action::PrevDetailTab);
11267 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
11268
11269 app.current_service = Service::IamUserGroups;
11271 app.iam_state.current_group = Some("test-group".to_string());
11272 app.iam_state.group_tab = GroupTab::Permissions;
11273
11274 app.handle_action(Action::PrevDetailTab);
11275 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
11276
11277 app.current_service = Service::S3Buckets;
11279 app.s3_state.current_bucket = Some("test-bucket".to_string());
11280 app.s3_state.object_tab = S3ObjectTab::Properties;
11281
11282 app.handle_action(Action::PrevDetailTab);
11283 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
11284
11285 app.current_service = Service::EcrRepositories;
11287 app.ecr_state.current_repository = None;
11288 app.ecr_state.tab = EcrTab::Private;
11289
11290 app.handle_action(Action::PrevDetailTab);
11291 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11292
11293 app.current_service = Service::CloudFormationStacks;
11295 app.cfn_state.current_stack = Some("test-stack".to_string());
11296 app.cfn_state.detail_tab = CfnDetailTab::Resources;
11297 }
11298
11299 #[test]
11300 fn test_cloudformation_status_filter_active() {
11301 use crate::ui::cfn::StatusFilter;
11302 let filter = StatusFilter::Active;
11303 assert!(filter.matches("CREATE_IN_PROGRESS"));
11304 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11305 assert!(!filter.matches("CREATE_COMPLETE"));
11306 assert!(!filter.matches("DELETE_COMPLETE"));
11307 assert!(!filter.matches("CREATE_FAILED"));
11308 }
11309
11310 #[test]
11311 fn test_cloudformation_status_filter_complete() {
11312 use crate::ui::cfn::StatusFilter;
11313 let filter = StatusFilter::Complete;
11314 assert!(filter.matches("CREATE_COMPLETE"));
11315 assert!(filter.matches("UPDATE_COMPLETE"));
11316 assert!(!filter.matches("DELETE_COMPLETE"));
11317 assert!(!filter.matches("CREATE_IN_PROGRESS"));
11318 }
11319
11320 #[test]
11321 fn test_cloudformation_status_filter_failed() {
11322 use crate::ui::cfn::StatusFilter;
11323 let filter = StatusFilter::Failed;
11324 assert!(filter.matches("CREATE_FAILED"));
11325 assert!(filter.matches("UPDATE_FAILED"));
11326 assert!(!filter.matches("CREATE_COMPLETE"));
11327 }
11328
11329 #[test]
11330 fn test_cloudformation_status_filter_deleted() {
11331 use crate::ui::cfn::StatusFilter;
11332 let filter = StatusFilter::Deleted;
11333 assert!(filter.matches("DELETE_COMPLETE"));
11334 assert!(filter.matches("DELETE_IN_PROGRESS"));
11335 assert!(!filter.matches("CREATE_COMPLETE"));
11336 }
11337
11338 #[test]
11339 fn test_cloudformation_status_filter_in_progress() {
11340 use crate::ui::cfn::StatusFilter;
11341 let filter = StatusFilter::InProgress;
11342 assert!(filter.matches("CREATE_IN_PROGRESS"));
11343 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11344 assert!(filter.matches("DELETE_IN_PROGRESS"));
11345 assert!(!filter.matches("CREATE_COMPLETE"));
11346 }
11347
11348 #[test]
11349 fn test_cloudformation_status_filter_cycle() {
11350 use crate::ui::cfn::StatusFilter;
11351 let filter = StatusFilter::All;
11352 assert_eq!(filter.next(), StatusFilter::Active);
11353 assert_eq!(filter.next().next(), StatusFilter::Complete);
11354 assert_eq!(filter.next().next().next(), StatusFilter::Failed);
11355 assert_eq!(filter.next().next().next().next(), StatusFilter::Deleted);
11356 assert_eq!(
11357 filter.next().next().next().next().next(),
11358 StatusFilter::InProgress
11359 );
11360 assert_eq!(
11361 filter.next().next().next().next().next().next(),
11362 StatusFilter::All
11363 );
11364 }
11365
11366 #[test]
11367 fn test_cloudformation_default_columns() {
11368 let app = test_app();
11369 assert_eq!(app.visible_cfn_columns.len(), 4);
11370 assert!(app.visible_cfn_columns.contains(&CfnColumn::Name));
11371 assert!(app.visible_cfn_columns.contains(&CfnColumn::Status));
11372 assert!(app.visible_cfn_columns.contains(&CfnColumn::CreatedTime));
11373 assert!(app.visible_cfn_columns.contains(&CfnColumn::Description));
11374 }
11375
11376 #[test]
11377 fn test_cloudformation_all_columns() {
11378 let app = test_app();
11379 assert_eq!(app.all_cfn_columns.len(), 10);
11380 }
11381
11382 #[test]
11383 fn test_cloudformation_filter_by_name() {
11384 use crate::ui::cfn::StatusFilter;
11385 let mut app = test_app();
11386 app.cfn_state.status_filter = StatusFilter::Complete;
11387 app.cfn_state.table.items = vec![
11388 CfnStack {
11389 name: "my-stack".to_string(),
11390 stack_id: "id1".to_string(),
11391 status: "CREATE_COMPLETE".to_string(),
11392 created_time: "2024-01-01".to_string(),
11393 updated_time: String::new(),
11394 deleted_time: String::new(),
11395 drift_status: String::new(),
11396 last_drift_check_time: String::new(),
11397 status_reason: String::new(),
11398 description: String::new(),
11399 detailed_status: String::new(),
11400 root_stack: String::new(),
11401 parent_stack: String::new(),
11402 termination_protection: false,
11403 iam_role: String::new(),
11404 tags: Vec::new(),
11405 stack_policy: String::new(),
11406 rollback_monitoring_time: String::new(),
11407 rollback_alarms: Vec::new(),
11408 notification_arns: Vec::new(),
11409 },
11410 CfnStack {
11411 name: "other-stack".to_string(),
11412 stack_id: "id2".to_string(),
11413 status: "CREATE_COMPLETE".to_string(),
11414 created_time: "2024-01-02".to_string(),
11415 updated_time: String::new(),
11416 deleted_time: String::new(),
11417 drift_status: String::new(),
11418 last_drift_check_time: String::new(),
11419 status_reason: String::new(),
11420 description: String::new(),
11421 detailed_status: String::new(),
11422 root_stack: String::new(),
11423 parent_stack: String::new(),
11424 termination_protection: false,
11425 iam_role: String::new(),
11426 tags: Vec::new(),
11427 stack_policy: String::new(),
11428 rollback_monitoring_time: String::new(),
11429 rollback_alarms: Vec::new(),
11430 notification_arns: Vec::new(),
11431 },
11432 ];
11433
11434 app.cfn_state.table.filter = "my".to_string();
11435 let filtered = app.filtered_cloudformation_stacks();
11436 assert_eq!(filtered.len(), 1);
11437 assert_eq!(filtered[0].name, "my-stack");
11438 }
11439
11440 #[test]
11441 fn test_cloudformation_filter_by_description() {
11442 use crate::ui::cfn::StatusFilter;
11443 let mut app = test_app();
11444 app.cfn_state.status_filter = StatusFilter::Complete;
11445 app.cfn_state.table.items = vec![CfnStack {
11446 name: "stack1".to_string(),
11447 stack_id: "id1".to_string(),
11448 status: "CREATE_COMPLETE".to_string(),
11449 created_time: "2024-01-01".to_string(),
11450 updated_time: String::new(),
11451 deleted_time: String::new(),
11452 drift_status: String::new(),
11453 last_drift_check_time: String::new(),
11454 status_reason: String::new(),
11455 description: "production stack".to_string(),
11456 detailed_status: String::new(),
11457 root_stack: String::new(),
11458 parent_stack: String::new(),
11459 termination_protection: false,
11460 iam_role: String::new(),
11461 tags: Vec::new(),
11462 stack_policy: String::new(),
11463 rollback_monitoring_time: String::new(),
11464 rollback_alarms: Vec::new(),
11465 notification_arns: Vec::new(),
11466 }];
11467
11468 app.cfn_state.table.filter = "production".to_string();
11469 let filtered = app.filtered_cloudformation_stacks();
11470 assert_eq!(filtered.len(), 1);
11471 }
11472
11473 #[test]
11474 fn test_cloudformation_status_filter_applied() {
11475 use crate::ui::cfn::StatusFilter;
11476 let mut app = test_app();
11477 app.cfn_state.table.items = vec![
11478 CfnStack {
11479 name: "complete-stack".to_string(),
11480 stack_id: "id1".to_string(),
11481 status: "CREATE_COMPLETE".to_string(),
11482 created_time: "2024-01-01".to_string(),
11483 updated_time: String::new(),
11484 deleted_time: String::new(),
11485 drift_status: String::new(),
11486 last_drift_check_time: String::new(),
11487 status_reason: String::new(),
11488 description: String::new(),
11489 detailed_status: String::new(),
11490 root_stack: String::new(),
11491 parent_stack: String::new(),
11492 termination_protection: false,
11493 iam_role: String::new(),
11494 tags: Vec::new(),
11495 stack_policy: String::new(),
11496 rollback_monitoring_time: String::new(),
11497 rollback_alarms: Vec::new(),
11498 notification_arns: Vec::new(),
11499 },
11500 CfnStack {
11501 name: "failed-stack".to_string(),
11502 stack_id: "id2".to_string(),
11503 status: "CREATE_FAILED".to_string(),
11504 created_time: "2024-01-02".to_string(),
11505 updated_time: String::new(),
11506 deleted_time: String::new(),
11507 drift_status: String::new(),
11508 last_drift_check_time: String::new(),
11509 status_reason: String::new(),
11510 description: String::new(),
11511 detailed_status: String::new(),
11512 root_stack: String::new(),
11513 parent_stack: String::new(),
11514 termination_protection: false,
11515 iam_role: String::new(),
11516 tags: Vec::new(),
11517 stack_policy: String::new(),
11518 rollback_monitoring_time: String::new(),
11519 rollback_alarms: Vec::new(),
11520 notification_arns: Vec::new(),
11521 },
11522 ];
11523
11524 app.cfn_state.status_filter = StatusFilter::Complete;
11525 let filtered = app.filtered_cloudformation_stacks();
11526 assert_eq!(filtered.len(), 1);
11527 assert_eq!(filtered[0].name, "complete-stack");
11528
11529 app.cfn_state.status_filter = StatusFilter::Failed;
11530 let filtered = app.filtered_cloudformation_stacks();
11531 assert_eq!(filtered.len(), 1);
11532 assert_eq!(filtered[0].name, "failed-stack");
11533 }
11534
11535 #[test]
11536 fn test_cloudformation_default_page_size() {
11537 let app = test_app();
11538 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
11539 }
11540
11541 #[test]
11542 fn test_cloudformation_default_status_filter() {
11543 use crate::ui::cfn::StatusFilter;
11544 let app = test_app();
11545 assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
11546 }
11547
11548 #[test]
11549 fn test_cloudformation_view_nested_default_false() {
11550 let app = test_app();
11551 assert!(!app.cfn_state.view_nested);
11552 }
11553
11554 #[test]
11555 fn test_cloudformation_pagination_hotkeys() {
11556 use crate::ui::cfn::StatusFilter;
11557 let mut app = test_app();
11558 app.current_service = Service::CloudFormationStacks;
11559 app.service_selected = true;
11560 app.cfn_state.status_filter = StatusFilter::All;
11561
11562 for i in 0..150 {
11564 app.cfn_state.table.items.push(CfnStack {
11565 name: format!("stack-{}", i),
11566 stack_id: format!("id-{}", i),
11567 status: "CREATE_COMPLETE".to_string(),
11568 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11569 updated_time: String::new(),
11570 deleted_time: String::new(),
11571 drift_status: String::new(),
11572 last_drift_check_time: String::new(),
11573 status_reason: String::new(),
11574 description: String::new(),
11575 detailed_status: String::new(),
11576 root_stack: String::new(),
11577 parent_stack: String::new(),
11578 termination_protection: false,
11579 iam_role: String::new(),
11580 tags: vec![],
11581 stack_policy: String::new(),
11582 rollback_monitoring_time: String::new(),
11583 rollback_alarms: vec![],
11584 notification_arns: vec![],
11585 });
11586 }
11587
11588 app.go_to_page(2);
11590 assert_eq!(app.cfn_state.table.selected, 50);
11591
11592 app.go_to_page(3);
11594 assert_eq!(app.cfn_state.table.selected, 100);
11595
11596 app.go_to_page(1);
11598 assert_eq!(app.cfn_state.table.selected, 0);
11599 }
11600
11601 #[test]
11602 fn test_cloudformation_tab_cycling_in_filter_mode() {
11603 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
11604 let mut app = test_app();
11605 app.current_service = Service::CloudFormationStacks;
11606 app.service_selected = true;
11607 app.mode = Mode::FilterInput;
11608 app.cfn_state.input_focus = InputFocus::Filter;
11609
11610 app.handle_action(Action::NextFilterFocus);
11612 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
11613
11614 app.handle_action(Action::NextFilterFocus);
11616 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
11617
11618 app.handle_action(Action::NextFilterFocus);
11620 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
11621
11622 app.handle_action(Action::NextFilterFocus);
11624 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
11625 }
11626
11627 #[test]
11628 fn test_cloudformation_timestamp_format_includes_utc() {
11629 let stack = CfnStack {
11630 name: "test-stack".to_string(),
11631 stack_id: "id-123".to_string(),
11632 status: "CREATE_COMPLETE".to_string(),
11633 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
11634 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
11635 deleted_time: String::new(),
11636 drift_status: String::new(),
11637 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
11638 status_reason: String::new(),
11639 description: String::new(),
11640 detailed_status: String::new(),
11641 root_stack: String::new(),
11642 parent_stack: String::new(),
11643 termination_protection: false,
11644 iam_role: String::new(),
11645 tags: vec![],
11646 stack_policy: String::new(),
11647 rollback_monitoring_time: String::new(),
11648 rollback_alarms: vec![],
11649 notification_arns: vec![],
11650 };
11651
11652 assert!(stack.created_time.contains("(UTC)"));
11653 assert!(stack.updated_time.contains("(UTC)"));
11654 assert!(stack.last_drift_check_time.contains("(UTC)"));
11655 assert_eq!(stack.created_time.len(), 25);
11656 }
11657
11658 #[test]
11659 fn test_cloudformation_enter_drills_into_stack_view() {
11660 use crate::ui::cfn::StatusFilter;
11661 let mut app = test_app();
11662 app.current_service = Service::CloudFormationStacks;
11663 app.service_selected = true;
11664 app.mode = Mode::Normal;
11665 app.cfn_state.status_filter = StatusFilter::All;
11666 app.tabs = vec![Tab {
11667 service: Service::CloudFormationStacks,
11668 title: "CloudFormation > Stacks".to_string(),
11669 breadcrumb: "CloudFormation > Stacks".to_string(),
11670 }];
11671 app.current_tab = 0;
11672
11673 app.cfn_state.table.items.push(CfnStack {
11674 name: "test-stack".to_string(),
11675 stack_id: "id-123".to_string(),
11676 status: "CREATE_COMPLETE".to_string(),
11677 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11678 updated_time: String::new(),
11679 deleted_time: String::new(),
11680 drift_status: String::new(),
11681 last_drift_check_time: String::new(),
11682 status_reason: String::new(),
11683 description: String::new(),
11684 detailed_status: String::new(),
11685 root_stack: String::new(),
11686 parent_stack: String::new(),
11687 termination_protection: false,
11688 iam_role: String::new(),
11689 tags: vec![],
11690 stack_policy: String::new(),
11691 rollback_monitoring_time: String::new(),
11692 rollback_alarms: vec![],
11693 notification_arns: vec![],
11694 });
11695
11696 app.cfn_state.table.reset();
11697 assert_eq!(app.cfn_state.current_stack, None);
11698
11699 app.handle_action(Action::Select);
11701 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
11702 }
11703
11704 #[test]
11705 fn test_cloudformation_arrow_keys_expand_collapse() {
11706 use crate::ui::cfn::StatusFilter;
11707 let mut app = test_app();
11708 app.current_service = Service::CloudFormationStacks;
11709 app.service_selected = true;
11710 app.mode = Mode::Normal;
11711 app.cfn_state.status_filter = StatusFilter::All;
11712
11713 app.cfn_state.table.items.push(CfnStack {
11714 name: "test-stack".to_string(),
11715 stack_id: "id-123".to_string(),
11716 status: "CREATE_COMPLETE".to_string(),
11717 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11718 updated_time: String::new(),
11719 deleted_time: String::new(),
11720 drift_status: String::new(),
11721 last_drift_check_time: String::new(),
11722 status_reason: String::new(),
11723 description: String::new(),
11724 detailed_status: String::new(),
11725 root_stack: String::new(),
11726 parent_stack: String::new(),
11727 termination_protection: false,
11728 iam_role: String::new(),
11729 tags: vec![],
11730 stack_policy: String::new(),
11731 rollback_monitoring_time: String::new(),
11732 rollback_alarms: vec![],
11733 notification_arns: vec![],
11734 });
11735
11736 app.cfn_state.table.reset();
11737 assert_eq!(app.cfn_state.table.expanded_item, None);
11738
11739 app.handle_action(Action::NextPane);
11741 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
11742
11743 app.handle_action(Action::PrevPane);
11745 assert_eq!(app.cfn_state.table.expanded_item, None);
11746
11747 assert_eq!(app.cfn_state.current_stack, None);
11749 }
11750
11751 #[test]
11752 fn test_cloudformation_tab_cycling() {
11753 use crate::ui::cfn::{DetailTab, StatusFilter};
11754 let mut app = test_app();
11755 app.current_service = Service::CloudFormationStacks;
11756 app.service_selected = true;
11757 app.mode = Mode::Normal;
11758 app.cfn_state.status_filter = StatusFilter::All;
11759 app.cfn_state.current_stack = Some("test-stack".to_string());
11760
11761 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
11762 }
11763
11764 #[test]
11765 fn test_cloudformation_console_url() {
11766 use crate::ui::cfn::{DetailTab, StatusFilter};
11767 let mut app = test_app();
11768 app.current_service = Service::CloudFormationStacks;
11769 app.service_selected = true;
11770 app.cfn_state.status_filter = StatusFilter::All;
11771
11772 app.cfn_state.table.items.push(CfnStack {
11773 name: "test-stack".to_string(),
11774 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11775 .to_string(),
11776 status: "CREATE_COMPLETE".to_string(),
11777 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11778 updated_time: String::new(),
11779 deleted_time: String::new(),
11780 drift_status: String::new(),
11781 last_drift_check_time: String::new(),
11782 status_reason: String::new(),
11783 description: String::new(),
11784 detailed_status: String::new(),
11785 root_stack: String::new(),
11786 parent_stack: String::new(),
11787 termination_protection: false,
11788 iam_role: String::new(),
11789 tags: vec![],
11790 stack_policy: String::new(),
11791 rollback_monitoring_time: String::new(),
11792 rollback_alarms: vec![],
11793 notification_arns: vec![],
11794 });
11795
11796 app.cfn_state.current_stack = Some("test-stack".to_string());
11797
11798 app.cfn_state.detail_tab = DetailTab::StackInfo;
11800 let url = app.get_console_url();
11801 assert!(url.contains("stackinfo"));
11802 assert!(url.contains("arn%3Aaws%3Acloudformation"));
11803
11804 app.cfn_state.detail_tab = DetailTab::Events;
11806 let url = app.get_console_url();
11807 assert!(url.contains("events"));
11808 assert!(url.contains("arn%3Aaws%3Acloudformation"));
11809 }
11810
11811 #[test]
11812 fn test_iam_role_select() {
11813 let mut app = test_app();
11814 app.current_service = Service::IamRoles;
11815 app.service_selected = true;
11816 app.mode = Mode::Normal;
11817
11818 app.iam_state.roles.items = vec![
11819 crate::iam::IamRole {
11820 role_name: "role1".to_string(),
11821 path: "/".to_string(),
11822 trusted_entities: "AWS Service: ec2".to_string(),
11823 last_activity: "-".to_string(),
11824 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
11825 creation_time: "2025-01-01".to_string(),
11826 description: "Test role 1".to_string(),
11827 max_session_duration: "3600 seconds".to_string(),
11828 },
11829 crate::iam::IamRole {
11830 role_name: "role2".to_string(),
11831 path: "/".to_string(),
11832 trusted_entities: "AWS Service: lambda".to_string(),
11833 last_activity: "-".to_string(),
11834 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
11835 creation_time: "2025-01-02".to_string(),
11836 description: "Test role 2".to_string(),
11837 max_session_duration: "7200 seconds".to_string(),
11838 },
11839 ];
11840
11841 app.iam_state.roles.selected = 0;
11843 app.handle_action(Action::Select);
11844
11845 assert_eq!(
11846 app.iam_state.current_role,
11847 Some("role1".to_string()),
11848 "Should open role detail view"
11849 );
11850 assert_eq!(
11851 app.iam_state.role_tab,
11852 RoleTab::Permissions,
11853 "Should default to Permissions tab"
11854 );
11855 }
11856
11857 #[test]
11858 fn test_iam_role_back_navigation() {
11859 let mut app = test_app();
11860 app.current_service = Service::IamRoles;
11861 app.service_selected = true;
11862 app.iam_state.current_role = Some("test-role".to_string());
11863
11864 app.handle_action(Action::GoBack);
11865
11866 assert_eq!(
11867 app.iam_state.current_role, None,
11868 "Should return to roles list"
11869 );
11870 }
11871
11872 #[test]
11873 fn test_iam_role_tab_navigation() {
11874 let mut app = test_app();
11875 app.current_service = Service::IamRoles;
11876 app.service_selected = true;
11877 app.iam_state.current_role = Some("test-role".to_string());
11878 app.iam_state.role_tab = RoleTab::Permissions;
11879
11880 app.handle_action(Action::NextDetailTab);
11881
11882 assert_eq!(
11883 app.iam_state.role_tab,
11884 RoleTab::TrustRelationships,
11885 "Should move to next tab"
11886 );
11887 }
11888
11889 #[test]
11890 fn test_iam_role_tab_cycle_order() {
11891 let mut app = test_app();
11892 app.current_service = Service::IamRoles;
11893 app.service_selected = true;
11894 app.iam_state.current_role = Some("test-role".to_string());
11895 app.iam_state.role_tab = RoleTab::Permissions;
11896
11897 app.handle_action(Action::NextDetailTab);
11898 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
11899
11900 app.handle_action(Action::NextDetailTab);
11901 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
11902
11903 app.handle_action(Action::NextDetailTab);
11904 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
11905
11906 app.handle_action(Action::NextDetailTab);
11907 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11908
11909 app.handle_action(Action::NextDetailTab);
11910 assert_eq!(
11911 app.iam_state.role_tab,
11912 RoleTab::Permissions,
11913 "Should cycle back to first tab"
11914 );
11915 }
11916
11917 #[test]
11918 fn test_iam_role_pagination() {
11919 let mut app = test_app();
11920 app.current_service = Service::IamRoles;
11921 app.service_selected = true;
11922 app.iam_state.roles.page_size = crate::common::PageSize::Ten;
11923
11924 app.iam_state.roles.items = (0..25)
11925 .map(|i| crate::iam::IamRole {
11926 role_name: format!("role{}", i),
11927 path: "/".to_string(),
11928 trusted_entities: "AWS Service: ec2".to_string(),
11929 last_activity: "-".to_string(),
11930 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
11931 creation_time: "2025-01-01".to_string(),
11932 description: format!("Test role {}", i),
11933 max_session_duration: "3600 seconds".to_string(),
11934 })
11935 .collect();
11936
11937 app.go_to_page(2);
11939
11940 assert_eq!(
11941 app.iam_state.roles.selected, 10,
11942 "Should select first item of page 2"
11943 );
11944 assert_eq!(
11945 app.iam_state.roles.scroll_offset, 10,
11946 "Should update scroll offset"
11947 );
11948 }
11949
11950 #[test]
11951 fn test_tags_table_populated_on_role_detail() {
11952 let mut app = test_app();
11953 app.current_service = Service::IamRoles;
11954 app.service_selected = true;
11955 app.mode = Mode::Normal;
11956 app.iam_state.roles.items = vec![crate::iam::IamRole {
11957 role_name: "TestRole".to_string(),
11958 path: "/".to_string(),
11959 trusted_entities: String::new(),
11960 last_activity: String::new(),
11961 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
11962 creation_time: "2025-01-01".to_string(),
11963 description: String::new(),
11964 max_session_duration: "3600 seconds".to_string(),
11965 }];
11966
11967 app.iam_state.tags.items = vec![
11969 crate::iam::RoleTag {
11970 key: "Environment".to_string(),
11971 value: "Production".to_string(),
11972 },
11973 crate::iam::RoleTag {
11974 key: "Team".to_string(),
11975 value: "Platform".to_string(),
11976 },
11977 ];
11978
11979 assert_eq!(app.iam_state.tags.items.len(), 2);
11980 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
11981 assert_eq!(app.iam_state.tags.items[0].value, "Production");
11982 assert_eq!(app.iam_state.tags.selected, 0);
11983 }
11984
11985 #[test]
11986 fn test_tags_table_navigation() {
11987 let mut app = test_app();
11988 app.current_service = Service::IamRoles;
11989 app.service_selected = true;
11990 app.mode = Mode::Normal;
11991 app.iam_state.current_role = Some("TestRole".to_string());
11992 app.iam_state.role_tab = RoleTab::Tags;
11993 app.iam_state.tags.items = vec![
11994 crate::iam::RoleTag {
11995 key: "Tag1".to_string(),
11996 value: "Value1".to_string(),
11997 },
11998 crate::iam::RoleTag {
11999 key: "Tag2".to_string(),
12000 value: "Value2".to_string(),
12001 },
12002 ];
12003
12004 app.handle_action(Action::NextItem);
12005 assert_eq!(app.iam_state.tags.selected, 1);
12006
12007 app.handle_action(Action::PrevItem);
12008 assert_eq!(app.iam_state.tags.selected, 0);
12009 }
12010
12011 #[test]
12012 fn test_last_accessed_table_navigation() {
12013 let mut app = test_app();
12014 app.current_service = Service::IamRoles;
12015 app.service_selected = true;
12016 app.mode = Mode::Normal;
12017 app.iam_state.current_role = Some("TestRole".to_string());
12018 app.iam_state.role_tab = RoleTab::LastAccessed;
12019 app.iam_state.last_accessed_services.items = vec![
12020 crate::iam::LastAccessedService {
12021 service: "S3".to_string(),
12022 policies_granting: "Policy1".to_string(),
12023 last_accessed: "2025-01-01".to_string(),
12024 },
12025 crate::iam::LastAccessedService {
12026 service: "EC2".to_string(),
12027 policies_granting: "Policy2".to_string(),
12028 last_accessed: "2025-01-02".to_string(),
12029 },
12030 ];
12031
12032 app.handle_action(Action::NextItem);
12033 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
12034
12035 app.handle_action(Action::PrevItem);
12036 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
12037 }
12038
12039 #[test]
12040 fn test_cfn_input_focus_next() {
12041 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12042 let mut app = test_app();
12043 app.current_service = Service::CloudFormationStacks;
12044 app.mode = Mode::FilterInput;
12045 app.cfn_state.input_focus = InputFocus::Filter;
12046
12047 app.handle_action(Action::NextFilterFocus);
12048 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12049
12050 app.handle_action(Action::NextFilterFocus);
12051 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12052
12053 app.handle_action(Action::NextFilterFocus);
12054 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12055
12056 app.handle_action(Action::NextFilterFocus);
12057 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12058 }
12059
12060 #[test]
12061 fn test_cfn_input_focus_prev() {
12062 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12063 let mut app = test_app();
12064 app.current_service = Service::CloudFormationStacks;
12065 app.mode = Mode::FilterInput;
12066 app.cfn_state.input_focus = InputFocus::Filter;
12067
12068 app.handle_action(Action::PrevFilterFocus);
12069 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12070
12071 app.handle_action(Action::PrevFilterFocus);
12072 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12073
12074 app.handle_action(Action::PrevFilterFocus);
12075 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12076
12077 app.handle_action(Action::PrevFilterFocus);
12078 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12079 }
12080
12081 #[test]
12082 fn test_cw_logs_input_focus_prev() {
12083 let mut app = test_app();
12084 app.current_service = Service::CloudWatchLogGroups;
12085 app.mode = Mode::FilterInput;
12086 app.view_mode = ViewMode::Detail;
12087 app.log_groups_state.detail_tab = crate::ui::cw::logs::DetailTab::LogStreams;
12088 app.log_groups_state.input_focus = InputFocus::Filter;
12089
12090 app.handle_action(Action::PrevFilterFocus);
12091 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
12092
12093 app.handle_action(Action::PrevFilterFocus);
12094 assert_eq!(
12095 app.log_groups_state.input_focus,
12096 InputFocus::Checkbox("ShowExpired")
12097 );
12098
12099 app.handle_action(Action::PrevFilterFocus);
12100 assert_eq!(
12101 app.log_groups_state.input_focus,
12102 InputFocus::Checkbox("ExactMatch")
12103 );
12104
12105 app.handle_action(Action::PrevFilterFocus);
12106 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
12107 }
12108
12109 #[test]
12110 fn test_cw_events_input_focus_prev() {
12111 use crate::ui::cw::logs::EventFilterFocus;
12112 let mut app = test_app();
12113 app.mode = Mode::EventFilterInput;
12114 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
12115
12116 app.handle_action(Action::PrevFilterFocus);
12117 assert_eq!(
12118 app.log_groups_state.event_input_focus,
12119 EventFilterFocus::DateRange
12120 );
12121
12122 app.handle_action(Action::PrevFilterFocus);
12123 assert_eq!(
12124 app.log_groups_state.event_input_focus,
12125 EventFilterFocus::Filter
12126 );
12127 }
12128
12129 #[test]
12130 fn test_cfn_input_focus_cycle_complete() {
12131 let mut app = test_app();
12132 app.current_service = Service::CloudFormationStacks;
12133 app.mode = Mode::FilterInput;
12134 app.cfn_state.input_focus = InputFocus::Filter;
12135
12136 for _ in 0..4 {
12138 app.handle_action(Action::NextFilterFocus);
12139 }
12140 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12141
12142 for _ in 0..4 {
12144 app.handle_action(Action::PrevFilterFocus);
12145 }
12146 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12147 }
12148
12149 #[test]
12150 fn test_cfn_filter_status_arrow_keys() {
12151 use crate::ui::cfn::{StatusFilter, STATUS_FILTER};
12152 let mut app = test_app();
12153 app.current_service = Service::CloudFormationStacks;
12154 app.mode = Mode::FilterInput;
12155 app.cfn_state.input_focus = STATUS_FILTER;
12156 app.cfn_state.status_filter = StatusFilter::All;
12157
12158 app.handle_action(Action::NextItem);
12159 assert_eq!(app.cfn_state.status_filter, StatusFilter::Active);
12160
12161 app.handle_action(Action::PrevItem);
12162 assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
12163 }
12164
12165 #[test]
12166 fn test_cfn_filter_shift_tab_cycles_backward() {
12167 use crate::ui::cfn::STATUS_FILTER;
12168 let mut app = test_app();
12169 app.current_service = Service::CloudFormationStacks;
12170 app.mode = Mode::FilterInput;
12171 app.cfn_state.input_focus = STATUS_FILTER;
12172
12173 app.handle_action(Action::PrevFilterFocus);
12175 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12176
12177 app.handle_action(Action::PrevFilterFocus);
12179 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12180 }
12181
12182 #[test]
12183 fn test_cfn_pagination_arrow_keys() {
12184 let mut app = test_app();
12185 app.current_service = Service::CloudFormationStacks;
12186 app.mode = Mode::FilterInput;
12187 app.cfn_state.input_focus = InputFocus::Pagination;
12188 app.cfn_state.table.scroll_offset = 0;
12189 app.cfn_state.table.page_size = crate::common::PageSize::Ten;
12190
12191 app.cfn_state.table.items = (0..30)
12193 .map(|i| crate::cfn::Stack {
12194 name: format!("stack-{}", i),
12195 stack_id: format!("id-{}", i),
12196 status: "CREATE_COMPLETE".to_string(),
12197 created_time: "2024-01-01".to_string(),
12198 updated_time: String::new(),
12199 deleted_time: String::new(),
12200 drift_status: String::new(),
12201 last_drift_check_time: String::new(),
12202 status_reason: String::new(),
12203 description: String::new(),
12204 detailed_status: String::new(),
12205 root_stack: String::new(),
12206 parent_stack: String::new(),
12207 termination_protection: false,
12208 iam_role: String::new(),
12209 tags: Vec::new(),
12210 stack_policy: String::new(),
12211 rollback_monitoring_time: String::new(),
12212 rollback_alarms: Vec::new(),
12213 notification_arns: Vec::new(),
12214 })
12215 .collect();
12216
12217 app.handle_action(Action::PageDown);
12219 assert_eq!(app.cfn_state.table.scroll_offset, 10);
12220 let page_size = app.cfn_state.table.page_size.value();
12222 let current_page = app.cfn_state.table.scroll_offset / page_size;
12223 assert_eq!(current_page, 1);
12224
12225 app.handle_action(Action::PageUp);
12227 assert_eq!(app.cfn_state.table.scroll_offset, 0);
12228 let current_page = app.cfn_state.table.scroll_offset / page_size;
12229 assert_eq!(current_page, 0);
12230 }
12231
12232 #[test]
12233 fn test_cfn_page_navigation_updates_selection() {
12234 let mut app = test_app();
12235 app.current_service = Service::CloudFormationStacks;
12236 app.mode = Mode::Normal;
12237
12238 app.cfn_state.table.items = (0..30)
12240 .map(|i| crate::cfn::Stack {
12241 name: format!("stack-{}", i),
12242 stack_id: format!("id-{}", i),
12243 status: "CREATE_COMPLETE".to_string(),
12244 created_time: "2024-01-01".to_string(),
12245 updated_time: String::new(),
12246 deleted_time: String::new(),
12247 drift_status: String::new(),
12248 last_drift_check_time: String::new(),
12249 status_reason: String::new(),
12250 description: String::new(),
12251 detailed_status: String::new(),
12252 root_stack: String::new(),
12253 parent_stack: String::new(),
12254 termination_protection: false,
12255 iam_role: String::new(),
12256 tags: Vec::new(),
12257 stack_policy: String::new(),
12258 rollback_monitoring_time: String::new(),
12259 rollback_alarms: Vec::new(),
12260 notification_arns: Vec::new(),
12261 })
12262 .collect();
12263
12264 app.cfn_state.table.reset();
12265 app.cfn_state.table.scroll_offset = 0;
12266
12267 app.handle_action(Action::PageDown);
12269 assert_eq!(app.cfn_state.table.selected, 10);
12270
12271 app.handle_action(Action::PageDown);
12273 assert_eq!(app.cfn_state.table.selected, 20);
12274
12275 app.handle_action(Action::PageUp);
12277 assert_eq!(app.cfn_state.table.selected, 10);
12278 }
12279
12280 #[test]
12281 fn test_cfn_filter_input_only_when_focused() {
12282 use crate::ui::cfn::STATUS_FILTER;
12283 let mut app = test_app();
12284 app.current_service = Service::CloudFormationStacks;
12285 app.mode = Mode::FilterInput;
12286 app.cfn_state.input_focus = STATUS_FILTER;
12287 app.cfn_state.table.filter = String::new();
12288
12289 app.handle_action(Action::FilterInput('t'));
12291 app.handle_action(Action::FilterInput('e'));
12292 app.handle_action(Action::FilterInput('s'));
12293 app.handle_action(Action::FilterInput('t'));
12294 assert_eq!(app.cfn_state.table.filter, "");
12295
12296 app.cfn_state.input_focus = InputFocus::Filter;
12298 app.handle_action(Action::FilterInput('t'));
12299 app.handle_action(Action::FilterInput('e'));
12300 app.handle_action(Action::FilterInput('s'));
12301 app.handle_action(Action::FilterInput('t'));
12302 assert_eq!(app.cfn_state.table.filter, "test");
12303 }
12304
12305 #[test]
12306 fn test_cfn_input_focus_resets_on_start() {
12307 let mut app = test_app();
12308 app.current_service = Service::CloudFormationStacks;
12309 app.service_selected = true;
12310 app.mode = Mode::Normal;
12311 app.cfn_state.input_focus = InputFocus::Pagination;
12312
12313 app.handle_action(Action::StartFilter);
12315 assert_eq!(app.mode, Mode::FilterInput);
12316 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12317 }
12318
12319 #[test]
12320 fn test_iam_roles_input_focus_cycles_forward() {
12321 let mut app = test_app();
12322 app.current_service = Service::IamRoles;
12323 app.mode = Mode::FilterInput;
12324 app.iam_state.role_input_focus = InputFocus::Filter;
12325
12326 app.handle_action(Action::NextFilterFocus);
12327 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12328
12329 app.handle_action(Action::NextFilterFocus);
12330 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12331 }
12332
12333 #[test]
12334 fn test_iam_roles_input_focus_cycles_backward() {
12335 let mut app = test_app();
12336 app.current_service = Service::IamRoles;
12337 app.mode = Mode::FilterInput;
12338 app.iam_state.role_input_focus = InputFocus::Filter;
12339
12340 app.handle_action(Action::PrevFilterFocus);
12341 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12342
12343 app.handle_action(Action::PrevFilterFocus);
12344 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12345 }
12346
12347 #[test]
12348 fn test_iam_roles_filter_input_only_when_focused() {
12349 let mut app = test_app();
12350 app.current_service = Service::IamRoles;
12351 app.mode = Mode::FilterInput;
12352 app.iam_state.role_input_focus = InputFocus::Pagination;
12353 app.iam_state.roles.filter = String::new();
12354
12355 app.handle_action(Action::FilterInput('t'));
12357 app.handle_action(Action::FilterInput('e'));
12358 app.handle_action(Action::FilterInput('s'));
12359 app.handle_action(Action::FilterInput('t'));
12360 assert_eq!(app.iam_state.roles.filter, "");
12361
12362 app.iam_state.role_input_focus = InputFocus::Filter;
12364 app.handle_action(Action::FilterInput('t'));
12365 app.handle_action(Action::FilterInput('e'));
12366 app.handle_action(Action::FilterInput('s'));
12367 app.handle_action(Action::FilterInput('t'));
12368 assert_eq!(app.iam_state.roles.filter, "test");
12369 }
12370
12371 #[test]
12372 fn test_iam_roles_page_down_updates_scroll_offset() {
12373 let mut app = test_app();
12374 app.current_service = Service::IamRoles;
12375 app.mode = Mode::Normal;
12376 app.iam_state.roles.items = (0..50)
12377 .map(|i| crate::iam::IamRole {
12378 role_name: format!("role-{}", i),
12379 path: "/".to_string(),
12380 trusted_entities: "AWS Service".to_string(),
12381 last_activity: "N/A".to_string(),
12382 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
12383 creation_time: "2024-01-01".to_string(),
12384 description: String::new(),
12385 max_session_duration: "1 hour".to_string(),
12386 })
12387 .collect();
12388
12389 app.iam_state.roles.selected = 0;
12390 app.iam_state.roles.scroll_offset = 0;
12391
12392 app.handle_action(Action::PageDown);
12394 assert_eq!(app.iam_state.roles.selected, 10);
12395 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12397
12398 app.handle_action(Action::PageDown);
12400 assert_eq!(app.iam_state.roles.selected, 20);
12401 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12402 }
12403
12404 #[test]
12405 fn test_application_selection_and_deployments_tab() {
12406 use crate::lambda::Application as LambdaApplication;
12407 use crate::ui::lambda::ApplicationDetailTab;
12408
12409 let mut app = test_app();
12410 app.current_service = Service::LambdaApplications;
12411 app.service_selected = true;
12412 app.mode = Mode::Normal;
12413
12414 app.lambda_application_state.table.items = vec![LambdaApplication {
12415 name: "test-app".to_string(),
12416 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12417 description: "Test application".to_string(),
12418 status: "CREATE_COMPLETE".to_string(),
12419 last_modified: "2024-01-01".to_string(),
12420 }];
12421
12422 app.handle_action(Action::Select);
12424 assert_eq!(
12425 app.lambda_application_state.current_application,
12426 Some("test-app".to_string())
12427 );
12428 assert_eq!(
12429 app.lambda_application_state.detail_tab,
12430 ApplicationDetailTab::Overview
12431 );
12432
12433 app.handle_action(Action::NextDetailTab);
12435 assert_eq!(
12436 app.lambda_application_state.detail_tab,
12437 ApplicationDetailTab::Deployments
12438 );
12439
12440 app.handle_action(Action::GoBack);
12442 assert_eq!(app.lambda_application_state.current_application, None);
12443 }
12444
12445 #[test]
12446 fn test_application_resources_filter_and_pagination() {
12447 use crate::lambda::Application as LambdaApplication;
12448 use crate::ui::lambda::ApplicationDetailTab;
12449
12450 let mut app = test_app();
12451 app.current_service = Service::LambdaApplications;
12452 app.service_selected = true;
12453 app.mode = Mode::Normal;
12454
12455 app.lambda_application_state.table.items = vec![LambdaApplication {
12456 name: "test-app".to_string(),
12457 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12458 description: "Test application".to_string(),
12459 status: "CREATE_COMPLETE".to_string(),
12460 last_modified: "2024-01-01".to_string(),
12461 }];
12462
12463 app.handle_action(Action::Select);
12465 assert_eq!(
12466 app.lambda_application_state.detail_tab,
12467 ApplicationDetailTab::Overview
12468 );
12469
12470 assert!(!app.lambda_application_state.resources.items.is_empty());
12472
12473 app.mode = Mode::FilterInput;
12475 assert_eq!(
12476 app.lambda_application_state.resource_input_focus,
12477 InputFocus::Filter
12478 );
12479
12480 app.handle_action(Action::NextFilterFocus);
12481 assert_eq!(
12482 app.lambda_application_state.resource_input_focus,
12483 InputFocus::Pagination
12484 );
12485
12486 app.handle_action(Action::PrevFilterFocus);
12487 assert_eq!(
12488 app.lambda_application_state.resource_input_focus,
12489 InputFocus::Filter
12490 );
12491 }
12492
12493 #[test]
12494 fn test_application_deployments_filter_and_pagination() {
12495 use crate::lambda::Application as LambdaApplication;
12496 use crate::ui::lambda::ApplicationDetailTab;
12497
12498 let mut app = test_app();
12499 app.current_service = Service::LambdaApplications;
12500 app.service_selected = true;
12501 app.mode = Mode::Normal;
12502
12503 app.lambda_application_state.table.items = vec![LambdaApplication {
12504 name: "test-app".to_string(),
12505 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12506 description: "Test application".to_string(),
12507 status: "CREATE_COMPLETE".to_string(),
12508 last_modified: "2024-01-01".to_string(),
12509 }];
12510
12511 app.handle_action(Action::Select);
12513 app.handle_action(Action::NextDetailTab);
12514 assert_eq!(
12515 app.lambda_application_state.detail_tab,
12516 ApplicationDetailTab::Deployments
12517 );
12518
12519 assert!(!app.lambda_application_state.deployments.items.is_empty());
12521
12522 app.mode = Mode::FilterInput;
12524 assert_eq!(
12525 app.lambda_application_state.deployment_input_focus,
12526 InputFocus::Filter
12527 );
12528
12529 app.handle_action(Action::NextFilterFocus);
12530 assert_eq!(
12531 app.lambda_application_state.deployment_input_focus,
12532 InputFocus::Pagination
12533 );
12534
12535 app.handle_action(Action::PrevFilterFocus);
12536 assert_eq!(
12537 app.lambda_application_state.deployment_input_focus,
12538 InputFocus::Filter
12539 );
12540 }
12541
12542 #[test]
12543 fn test_application_resource_expansion() {
12544 use crate::lambda::Application as LambdaApplication;
12545 use crate::ui::lambda::ApplicationDetailTab;
12546
12547 let mut app = test_app();
12548 app.current_service = Service::LambdaApplications;
12549 app.service_selected = true;
12550 app.mode = Mode::Normal;
12551
12552 app.lambda_application_state.table.items = vec![LambdaApplication {
12553 name: "test-app".to_string(),
12554 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12555 description: "Test application".to_string(),
12556 status: "CREATE_COMPLETE".to_string(),
12557 last_modified: "2024-01-01".to_string(),
12558 }];
12559
12560 app.handle_action(Action::Select);
12562 assert_eq!(
12563 app.lambda_application_state.detail_tab,
12564 ApplicationDetailTab::Overview
12565 );
12566
12567 app.handle_action(Action::NextPane);
12569 assert_eq!(
12570 app.lambda_application_state.resources.expanded_item,
12571 Some(0)
12572 );
12573
12574 app.handle_action(Action::PrevPane);
12576 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
12577 }
12578
12579 #[test]
12580 fn test_application_deployment_expansion() {
12581 use crate::lambda::Application as LambdaApplication;
12582 use crate::ui::lambda::ApplicationDetailTab;
12583
12584 let mut app = test_app();
12585 app.current_service = Service::LambdaApplications;
12586 app.service_selected = true;
12587 app.mode = Mode::Normal;
12588
12589 app.lambda_application_state.table.items = vec![LambdaApplication {
12590 name: "test-app".to_string(),
12591 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12592 description: "Test application".to_string(),
12593 status: "CREATE_COMPLETE".to_string(),
12594 last_modified: "2024-01-01".to_string(),
12595 }];
12596
12597 app.handle_action(Action::Select);
12599 app.handle_action(Action::NextDetailTab);
12600 assert_eq!(
12601 app.lambda_application_state.detail_tab,
12602 ApplicationDetailTab::Deployments
12603 );
12604
12605 app.handle_action(Action::NextPane);
12607 assert_eq!(
12608 app.lambda_application_state.deployments.expanded_item,
12609 Some(0)
12610 );
12611
12612 app.handle_action(Action::PrevPane);
12614 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
12615 }
12616
12617 #[test]
12618 fn test_s3_nested_prefix_expansion() {
12619 use crate::s3::Bucket;
12620 use crate::s3::Object as S3Object;
12621
12622 let mut app = test_app();
12623 app.current_service = Service::S3Buckets;
12624 app.service_selected = true;
12625 app.mode = Mode::Normal;
12626
12627 app.s3_state.buckets.items = vec![Bucket {
12629 name: "test-bucket".to_string(),
12630 region: "us-east-1".to_string(),
12631 creation_date: "2024-01-01".to_string(),
12632 }];
12633
12634 app.s3_state.bucket_preview.insert(
12636 "test-bucket".to_string(),
12637 vec![S3Object {
12638 key: "level1/".to_string(),
12639 size: 0,
12640 last_modified: "".to_string(),
12641 is_prefix: true,
12642 storage_class: "".to_string(),
12643 }],
12644 );
12645
12646 app.s3_state.prefix_preview.insert(
12648 "level1/".to_string(),
12649 vec![S3Object {
12650 key: "level1/level2/".to_string(),
12651 size: 0,
12652 last_modified: "".to_string(),
12653 is_prefix: true,
12654 storage_class: "".to_string(),
12655 }],
12656 );
12657
12658 app.s3_state.selected_row = 0;
12660 app.handle_action(Action::NextPane);
12661 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
12662
12663 app.s3_state.selected_row = 1;
12665 app.handle_action(Action::NextPane);
12666 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
12667
12668 app.s3_state.selected_row = 2;
12670 app.handle_action(Action::NextPane);
12671 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
12672
12673 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
12675 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
12676 }
12677
12678 #[test]
12679 fn test_s3_nested_prefix_collapse() {
12680 use crate::s3::Bucket;
12681 use crate::s3::Object as S3Object;
12682
12683 let mut app = test_app();
12684 app.current_service = Service::S3Buckets;
12685 app.service_selected = true;
12686 app.mode = Mode::Normal;
12687
12688 app.s3_state.buckets.items = vec![Bucket {
12689 name: "test-bucket".to_string(),
12690 region: "us-east-1".to_string(),
12691 creation_date: "2024-01-01".to_string(),
12692 }];
12693
12694 app.s3_state.bucket_preview.insert(
12695 "test-bucket".to_string(),
12696 vec![S3Object {
12697 key: "level1/".to_string(),
12698 size: 0,
12699 last_modified: "".to_string(),
12700 is_prefix: true,
12701 storage_class: "".to_string(),
12702 }],
12703 );
12704
12705 app.s3_state.prefix_preview.insert(
12706 "level1/".to_string(),
12707 vec![S3Object {
12708 key: "level1/level2/".to_string(),
12709 size: 0,
12710 last_modified: "".to_string(),
12711 is_prefix: true,
12712 storage_class: "".to_string(),
12713 }],
12714 );
12715
12716 app.s3_state
12718 .expanded_prefixes
12719 .insert("test-bucket".to_string());
12720 app.s3_state.expanded_prefixes.insert("level1/".to_string());
12721 app.s3_state
12722 .expanded_prefixes
12723 .insert("level1/level2/".to_string());
12724
12725 app.s3_state.selected_row = 2;
12727 app.handle_action(Action::PrevPane);
12728 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
12729 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
12733 app.handle_action(Action::PrevPane);
12734 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
12735 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
12739 app.handle_action(Action::PrevPane);
12740 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
12741 }
12742}