use crate::apig::api::Column as ApigColumn;
use crate::apig::resource::Column as ResourceColumn;
use crate::apig::resource::Resource as ApigResource;
use crate::apig::route::Column as RouteColumn;
use crate::apig::route::Route;
pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
use crate::cloudtrail::{CloudTrailEvent, CloudTrailEventColumn, EventResourceColumn};
use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
pub use crate::cw::insights::InsightsFocus;
use crate::cw::insights::InsightsState;
pub use crate::cw::{Alarm, AlarmColumn};
pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
use crate::iam::{
self, GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag,
UserColumn, UserTag as IamUserTag,
};
#[cfg(test)]
use crate::iam::{IamRole, IamUser, LastAccessedService};
use crate::keymap::{Action, Mode};
pub use crate::lambda::{
Alias as LambdaAlias, Application as LambdaApplication,
ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
ResourceColumn as LambdaResourceColumn, Version as LambdaVersion,
};
pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
use crate::session::{Session, SessionTab};
pub use crate::sqs::queue::Column as SqsColumn;
pub use crate::sqs::trigger::Column as SqsTriggerColumn;
use crate::sqs::{console_url_queue_detail, console_url_queues};
#[cfg(test)]
use crate::sqs::{
EventBridgePipe, LambdaTrigger, Queue as SqsQueue, QueueTag as SqsQueueTag, SnsSubscription,
};
use crate::table::TableState;
pub use crate::ui::apig::{
filtered_apis, State as ApigState, FILTER_CONTROLS as APIG_FILTER_CONTROLS,
};
pub use crate::ui::cfn::{
change_set_column_ids, change_set_visible_column_ids, event_column_ids,
event_visible_column_ids, filtered_change_sets, filtered_cloudformation_stacks,
filtered_events, filtered_outputs, filtered_parameters, filtered_resources, output_column_ids,
parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
EventsView as CfnEventsView, State as CfnState, StatusFilter as CfnStatusFilter,
};
pub use crate::ui::cw::alarms::{
AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
};
pub use crate::ui::cw::logs::{
filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
};
pub use crate::ui::ec2::{
DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
STATE_FILTER as EC2_STATE_FILTER,
};
pub use crate::ui::ecr::{
filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
FILTER_CONTROLS as ECR_FILTER_CONTROLS,
};
use crate::ui::iam::{
filtered_iam_policies, filtered_iam_roles, filtered_iam_users, filtered_last_accessed,
filtered_tags as filtered_iam_tags, filtered_user_tags, GroupTab, RoleTab, State as IamState,
UserTab,
};
pub use crate::ui::lambda::{
filtered_lambda_applications, filtered_lambda_functions,
ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
};
use crate::ui::monitoring::MonitoringState;
pub use crate::ui::s3::{
calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
ObjectTab as S3ObjectTab, State as S3State,
};
pub use crate::ui::sqs::{
extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
};
use crate::ui::tree::TreeItem;
pub use crate::ui::{
CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
StreamColumn, StreamSort, TimeUnit,
};
#[cfg(test)]
use rusticity_core::LogStream;
use rusticity_core::{
AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
CloudWatchClient, Ec2Client, EcrClient, IamClient, LambdaClient, S3Client, SqsClient,
};
use std::collections::HashMap;
#[derive(Clone)]
pub struct Tab {
pub service: Service,
pub title: String,
pub breadcrumb: String,
}
pub struct App {
pub running: bool,
pub mode: Mode,
pub config: AwsConfig,
pub cloudwatch_client: CloudWatchClient,
pub cloudtrail_client: CloudTrailClient,
pub s3_client: S3Client,
pub sqs_client: SqsClient,
pub alarms_client: AlarmsClient,
pub ec2_client: Ec2Client,
pub ecr_client: EcrClient,
pub apig_client: ApiGatewayClient,
pub iam_client: IamClient,
pub lambda_client: LambdaClient,
pub cloudformation_client: CloudFormationClient,
pub current_service: Service,
pub tabs: Vec<Tab>,
pub current_tab: usize,
pub tab_picker_selected: usize,
pub tab_filter: String,
pub pending_key: Option<char>,
pub log_groups_state: CloudWatchLogGroupsState,
pub insights_state: CloudWatchInsightsState,
pub alarms_state: CloudWatchAlarmsState,
pub cloudtrail_state: CloudTrailState,
pub s3_state: S3State,
pub sqs_state: SqsState,
pub ec2_state: Ec2State,
pub ecr_state: EcrState,
pub apig_state: ApigState,
pub lambda_state: LambdaState,
pub lambda_application_state: LambdaApplicationState,
pub cfn_state: CfnState,
pub iam_state: IamState,
pub service_picker: ServicePickerState,
pub service_selected: bool,
pub profile: String,
pub region: String,
pub region_selector_index: usize,
pub cw_log_group_visible_column_ids: Vec<ColumnId>,
pub cw_log_group_column_ids: Vec<ColumnId>,
pub column_selector_index: usize,
pub preference_section: Preferences,
pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
pub cw_log_stream_column_ids: Vec<ColumnId>,
pub cw_log_event_visible_column_ids: Vec<ColumnId>,
pub cw_log_event_column_ids: Vec<ColumnId>,
pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
pub cw_log_tag_column_ids: Vec<ColumnId>,
pub cw_alarm_visible_column_ids: Vec<ColumnId>,
pub cw_alarm_column_ids: Vec<ColumnId>,
pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
pub cloudtrail_event_column_ids: Vec<ColumnId>,
pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
pub cloudtrail_resource_column_ids: Vec<ColumnId>,
pub s3_bucket_visible_column_ids: Vec<ColumnId>,
pub s3_bucket_column_ids: Vec<ColumnId>,
pub sqs_visible_column_ids: Vec<ColumnId>,
pub sqs_column_ids: Vec<ColumnId>,
pub ec2_visible_column_ids: Vec<ColumnId>,
pub ec2_column_ids: Vec<ColumnId>,
pub ecr_repo_visible_column_ids: Vec<ColumnId>,
pub ecr_repo_column_ids: Vec<ColumnId>,
pub ecr_image_visible_column_ids: Vec<ColumnId>,
pub ecr_image_column_ids: Vec<ColumnId>,
pub apig_api_visible_column_ids: Vec<ColumnId>,
pub apig_api_column_ids: Vec<ColumnId>,
pub apig_route_visible_column_ids: Vec<ColumnId>,
pub apig_route_column_ids: Vec<ColumnId>,
pub apig_resource_visible_column_ids: Vec<ColumnId>,
pub apig_resource_column_ids: Vec<ColumnId>,
pub lambda_application_visible_column_ids: Vec<ColumnId>,
pub lambda_application_column_ids: Vec<ColumnId>,
pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
pub lambda_deployment_column_ids: Vec<ColumnId>,
pub lambda_resource_visible_column_ids: Vec<ColumnId>,
pub lambda_resource_column_ids: Vec<ColumnId>,
pub cfn_visible_column_ids: Vec<ColumnId>,
pub cfn_column_ids: Vec<ColumnId>,
pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
pub cfn_parameter_column_ids: Vec<ColumnId>,
pub cfn_output_visible_column_ids: Vec<ColumnId>,
pub cfn_output_column_ids: Vec<ColumnId>,
pub cfn_resource_visible_column_ids: Vec<ColumnId>,
pub cfn_resource_column_ids: Vec<ColumnId>,
pub cfn_event_visible_column_ids: Vec<ColumnId>,
pub cfn_event_column_ids: Vec<ColumnId>,
pub cfn_change_set_visible_column_ids: Vec<ColumnId>,
pub cfn_change_set_column_ids: Vec<ColumnId>,
pub iam_user_visible_column_ids: Vec<ColumnId>,
pub iam_user_column_ids: Vec<ColumnId>,
pub iam_role_visible_column_ids: Vec<ColumnId>,
pub iam_role_column_ids: Vec<ColumnId>,
pub iam_group_visible_column_ids: Vec<String>,
pub iam_group_column_ids: Vec<String>,
pub iam_policy_visible_column_ids: Vec<String>,
pub iam_policy_column_ids: Vec<String>,
pub view_mode: ViewMode,
pub error_message: Option<String>,
pub error_scroll: usize,
pub page_input: String,
pub calendar_date: Option<time::Date>,
pub calendar_selecting: CalendarField,
pub cursor_pos: usize,
pub current_session: Option<Session>,
pub sessions: Vec<Session>,
pub session_picker_selected: usize,
pub session_filter: String,
pub session_filter_active: bool,
pub region_filter: String,
pub region_picker_selected: usize,
pub region_filter_active: bool,
pub region_latencies: std::collections::HashMap<String, u64>,
pub profile_filter: String,
pub profile_picker_selected: usize,
pub profile_filter_active: bool,
pub available_profiles: Vec<AwsProfile>,
pub snapshot_requested: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CalendarField {
StartDate,
EndDate,
}
pub struct CloudWatchInsightsState {
pub insights: InsightsState,
pub loading: bool,
}
pub struct CloudWatchAlarmsState {
pub table: TableState<Alarm>,
pub current_alarm: Option<String>,
pub alarm_tab: AlarmTab,
pub view_as: AlarmViewMode,
pub wrap_lines: bool,
pub sort_column: String,
pub sort_direction: SortDirection,
pub input_focus: InputFocus,
pub metric_data: Vec<(i64, f64)>,
pub metrics_loading: bool,
}
#[derive(Debug, Clone)]
pub struct CloudTrailState {
pub table: TableState<CloudTrailEvent>,
pub input_focus: InputFocus,
pub current_event: Option<CloudTrailEvent>,
pub event_json_scroll: usize,
pub detail_focus: CloudTrailDetailFocus,
pub resources_expanded_index: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CloudTrailDetailFocus {
Resources,
EventRecord,
}
impl CyclicEnum for CloudTrailDetailFocus {
const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
}
impl PageSize {
pub fn value(&self) -> usize {
match self {
PageSize::Ten => 10,
PageSize::TwentyFive => 25,
PageSize::Fifty => 50,
PageSize::OneHundred => 100,
}
}
pub fn next(&self) -> Self {
match self {
PageSize::Ten => PageSize::TwentyFive,
PageSize::TwentyFive => PageSize::Fifty,
PageSize::Fifty => PageSize::OneHundred,
PageSize::OneHundred => PageSize::Ten,
}
}
}
pub struct ServicePickerState {
pub filter: String,
pub filter_active: bool,
pub selected: usize,
pub services: Vec<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ViewMode {
List,
Detail,
Events,
InsightsResults,
PolicyView,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Service {
ApiGatewayApis,
CloudWatchLogGroups,
CloudWatchInsights,
CloudWatchAlarms,
CloudTrailEvents,
S3Buckets,
SqsQueues,
Ec2Instances,
EcrRepositories,
LambdaFunctions,
LambdaApplications,
CloudFormationStacks,
IamUsers,
IamRoles,
IamUserGroups,
}
impl Service {
pub fn name(&self) -> &str {
match self {
Service::ApiGatewayApis => "API Gateway › APIs",
Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
Service::CloudWatchInsights => "CloudWatch › Logs Insights",
Service::CloudWatchAlarms => "CloudWatch › Alarms",
Service::CloudTrailEvents => "CloudTrail › Event History",
Service::S3Buckets => "S3 › Buckets",
Service::SqsQueues => "SQS › Queues",
Service::Ec2Instances => "EC2 › Instances",
Service::EcrRepositories => "ECR › Repositories",
Service::LambdaFunctions => "Lambda › Functions",
Service::LambdaApplications => "Lambda › Applications",
Service::CloudFormationStacks => "CloudFormation › Stacks",
Service::IamUsers => "IAM › Users",
Service::IamRoles => "IAM › Roles",
Service::IamUserGroups => "IAM › User Groups",
}
}
}
pub(crate) fn copy_to_clipboard(text: &str) {
use std::io::Write;
use std::process::{Command, Stdio};
if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
}
}
pub(crate) fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
if max > 0 {
*selected = (*selected + page_size).min(max - 1);
}
}
pub(crate) fn toggle_iam_preference(
idx: usize,
column_ids: &[String],
visible_column_ids: &mut Vec<String>,
page_size: &mut PageSize,
) {
if idx > 0 && idx <= column_ids.len() {
if let Some(col) = column_ids.get(idx - 1) {
if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
if visible_column_ids.len() > 1 {
visible_column_ids.remove(pos);
}
} else {
visible_column_ids.push(col.clone());
}
}
} else if idx == column_ids.len() + 3 {
*page_size = PageSize::Ten;
} else if idx == column_ids.len() + 4 {
*page_size = PageSize::TwentyFive;
} else if idx == column_ids.len() + 5 {
*page_size = PageSize::Fifty;
}
}
pub(crate) fn toggle_iam_preference_static(
idx: usize,
column_ids: &[ColumnId],
visible_column_ids: &mut Vec<ColumnId>,
page_size: &mut PageSize,
) {
if idx > 0 && idx <= column_ids.len() {
if let Some(col) = column_ids.get(idx - 1) {
if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
if visible_column_ids.len() > 1 {
visible_column_ids.remove(pos);
}
} else {
visible_column_ids.push(*col);
}
}
} else if idx == column_ids.len() + 3 {
*page_size = PageSize::Ten;
} else if idx == column_ids.len() + 4 {
*page_size = PageSize::TwentyFive;
} else if idx == column_ids.len() + 5 {
*page_size = PageSize::Fifty;
}
}
pub(crate) fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
if idx == base_idx {
*page_size = PageSize::Ten;
} else if idx == base_idx + 1 {
*page_size = PageSize::TwentyFive;
} else if idx == base_idx + 2 {
*page_size = PageSize::Fifty;
}
}
fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
let page_size_idx = num_columns + 2;
if *current_idx < page_size_idx {
*current_idx = page_size_idx;
} else {
*current_idx = 0;
}
}
fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
let page_size_idx = num_columns + 2;
if *current_idx >= page_size_idx {
*current_idx = 0;
} else {
*current_idx = page_size_idx;
}
}
impl App {
pub fn get_input_focus(&self) -> InputFocus {
InputFocus::Filter
}
fn get_active_filter_mut(&mut self) -> Option<&mut String> {
if self.current_service == Service::ApiGatewayApis {
if self.apig_state.current_api.is_some()
&& self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
{
Some(&mut self.apig_state.route_filter)
} else {
Some(&mut self.apig_state.apis.filter)
}
} else if self.current_service == Service::CloudWatchAlarms {
Some(&mut self.alarms_state.table.filter)
} else if self.current_service == Service::CloudTrailEvents {
Some(&mut self.cloudtrail_state.table.filter)
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::get_active_filter_mut(self)
} else if self.current_service == Service::S3Buckets {
if self.s3_state.current_bucket.is_some() {
Some(&mut self.s3_state.object_filter)
} else {
Some(&mut self.s3_state.buckets.filter)
}
} else if self.current_service == Service::EcrRepositories {
if self.ecr_state.current_repository.is_some() {
Some(&mut self.ecr_state.images.filter)
} else {
Some(&mut self.ecr_state.repositories.filter)
}
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
{
Some(&mut self.sqs_state.triggers.filter)
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
{
Some(&mut self.sqs_state.pipes.filter)
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
{
Some(&mut self.sqs_state.tags.filter)
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
Some(&mut self.sqs_state.subscriptions.filter)
} else {
Some(&mut self.sqs_state.queues.filter)
}
} else if self.current_service == Service::LambdaFunctions {
if self.lambda_state.current_version.is_some()
&& self.lambda_state.detail_tab == LambdaDetailTab::Configuration
{
Some(&mut self.lambda_state.alias_table.filter)
} else if self.lambda_state.current_function.is_some()
&& self.lambda_state.detail_tab == LambdaDetailTab::Versions
{
Some(&mut self.lambda_state.version_table.filter)
} else if self.lambda_state.current_function.is_some()
&& self.lambda_state.detail_tab == LambdaDetailTab::Aliases
{
Some(&mut self.lambda_state.alias_table.filter)
} else {
Some(&mut self.lambda_state.table.filter)
}
} else if self.current_service == Service::LambdaApplications {
if self.lambda_application_state.current_application.is_some() {
if self.lambda_application_state.detail_tab
== LambdaApplicationDetailTab::Deployments
{
Some(&mut self.lambda_application_state.deployments.filter)
} else {
Some(&mut self.lambda_application_state.resources.filter)
}
} else {
Some(&mut self.lambda_application_state.table.filter)
}
} else if self.current_service == Service::CloudFormationStacks {
if self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Resources
{
Some(&mut self.cfn_state.resources.filter)
} else {
Some(&mut self.cfn_state.table.filter)
}
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
if self.iam_state.user_tab == UserTab::Tags {
Some(&mut self.iam_state.user_tags.filter)
} else {
Some(&mut self.iam_state.policies.filter)
}
} else {
Some(&mut self.iam_state.users.filter)
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
if self.iam_state.role_tab == RoleTab::Tags {
Some(&mut self.iam_state.tags.filter)
} else if self.iam_state.role_tab == RoleTab::LastAccessed {
Some(&mut self.iam_state.last_accessed_filter)
} else {
Some(&mut self.iam_state.policies.filter)
}
} else {
Some(&mut self.iam_state.roles.filter)
}
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_some() {
if self.iam_state.group_tab == GroupTab::Permissions {
Some(&mut self.iam_state.policies.filter)
} else if self.iam_state.group_tab == GroupTab::Users {
Some(&mut self.iam_state.group_users.filter)
} else {
None
}
} else {
Some(&mut self.iam_state.groups.filter)
}
} else if self.view_mode == ViewMode::List {
Some(&mut self.log_groups_state.log_groups.filter)
} else if self.view_mode == ViewMode::Detail
&& self.log_groups_state.detail_tab == DetailTab::LogStreams
{
Some(&mut self.log_groups_state.stream_filter)
} else {
None
}
}
fn apply_filter_operation<F>(&mut self, op: F)
where
F: FnOnce(&mut String),
{
if let Some(filter) = self.get_active_filter_mut() {
op(filter);
if self.current_service == Service::CloudWatchAlarms {
self.alarms_state.table.reset();
} else if self.current_service == Service::CloudTrailEvents {
self.cloudtrail_state.table.reset();
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::apply_filter_reset(self);
} else if self.current_service == Service::S3Buckets {
crate::s3::actions::apply_filter_reset(self);
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::apply_filter_reset(self);
} else if self.current_service == Service::SqsQueues {
self.sqs_state.queues.reset();
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::apply_filter_reset(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::apply_filter_reset(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::apply_filter_reset(self);
} else if self.current_service == Service::IamUsers {
crate::iam::actions::apply_filter_reset_users(self);
} else if self.current_service == Service::IamRoles {
crate::iam::actions::apply_filter_reset_roles(self);
} else if self.current_service == Service::IamUserGroups {
crate::iam::actions::apply_filter_reset_groups(self);
} else if self.current_service == Service::CloudWatchLogGroups {
if self.view_mode == ViewMode::List {
self.log_groups_state.log_groups.reset();
} else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
self.log_groups_state.selected_stream = 0;
}
} else if self.current_service == Service::ApiGatewayApis {
self.apig_state.apis.reset();
}
}
}
pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
.ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
std::env::set_var("AWS_PROFILE", &profile_name);
let config = AwsConfig::new(region).await?;
let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
let cloudtrail_client = CloudTrailClient::new(config.clone());
let s3_client = S3Client::new(config.clone());
let sqs_client = SqsClient::new(config.clone());
let alarms_client = AlarmsClient::new(config.clone());
let ec2_client = Ec2Client::new(config.clone());
let ecr_client = EcrClient::new(config.clone());
let apig_client = ApiGatewayClient::new(config.clone());
let iam_client = IamClient::new(config.clone());
let lambda_client = LambdaClient::new(config.clone());
let cloudformation_client = CloudFormationClient::new(config.clone());
let region_name = config.region.clone();
Ok(Self {
running: true,
mode: Mode::ServicePicker,
config,
cloudwatch_client,
cloudtrail_client,
s3_client,
sqs_client,
alarms_client,
ec2_client,
ecr_client,
apig_client,
iam_client,
lambda_client,
cloudformation_client,
current_service: Service::CloudWatchLogGroups,
tabs: Vec::new(),
current_tab: 0,
tab_picker_selected: 0,
tab_filter: String::new(),
pending_key: None,
log_groups_state: CloudWatchLogGroupsState::new(),
insights_state: CloudWatchInsightsState::new(),
alarms_state: CloudWatchAlarmsState::new(),
cloudtrail_state: CloudTrailState {
table: TableState::new(),
input_focus: InputFocus::Filter,
current_event: None,
event_json_scroll: 0,
detail_focus: CloudTrailDetailFocus::Resources,
resources_expanded_index: None,
},
s3_state: S3State::new(),
sqs_state: SqsState::new(),
ec2_state: Ec2State::default(),
ecr_state: EcrState::new(),
apig_state: ApigState::new(),
lambda_state: LambdaState::new(),
lambda_application_state: LambdaApplicationState::new(),
cfn_state: CfnState::new(),
iam_state: IamState::new(),
service_picker: ServicePickerState::new(),
service_selected: false,
profile: profile_name,
region: region_name,
region_selector_index: 0,
cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
cw_log_group_column_ids: LogGroupColumn::ids(),
column_selector_index: 0,
cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
cw_log_stream_column_ids: StreamColumn::ids(),
cw_log_event_visible_column_ids: EventColumn::default_visible(),
cw_log_event_column_ids: EventColumn::ids(),
cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
cw_alarm_visible_column_ids: [
AlarmColumn::Name,
AlarmColumn::State,
AlarmColumn::LastStateUpdate,
AlarmColumn::Conditions,
AlarmColumn::Actions,
]
.iter()
.map(|c| c.id())
.collect(),
cw_alarm_column_ids: AlarmColumn::ids(),
cloudtrail_event_visible_column_ids: [
CloudTrailEventColumn::EventName,
CloudTrailEventColumn::EventTime,
CloudTrailEventColumn::Username,
CloudTrailEventColumn::EventSource,
CloudTrailEventColumn::ResourceType,
CloudTrailEventColumn::ResourceName,
]
.iter()
.map(|c| c.id())
.collect(),
cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
cloudtrail_resource_column_ids: EventResourceColumn::ids(),
s3_bucket_visible_column_ids: S3BucketColumn::ids(),
s3_bucket_column_ids: S3BucketColumn::ids(),
sqs_visible_column_ids: [
SqsColumn::Name,
SqsColumn::Type,
SqsColumn::Created,
SqsColumn::MessagesAvailable,
SqsColumn::MessagesInFlight,
SqsColumn::Encryption,
SqsColumn::ContentBasedDeduplication,
]
.iter()
.map(|c| c.id())
.collect(),
sqs_column_ids: SqsColumn::ids(),
ec2_visible_column_ids: [
Ec2Column::Name,
Ec2Column::InstanceId,
Ec2Column::InstanceState,
Ec2Column::InstanceType,
Ec2Column::StatusCheck,
Ec2Column::AlarmStatus,
Ec2Column::AvailabilityZone,
Ec2Column::PublicIpv4Dns,
Ec2Column::PublicIpv4Address,
Ec2Column::ElasticIp,
Ec2Column::Ipv6Ips,
Ec2Column::Monitoring,
Ec2Column::SecurityGroupName,
Ec2Column::KeyName,
Ec2Column::LaunchTime,
Ec2Column::PlatformDetails,
]
.iter()
.map(|c| c.id())
.collect(),
ec2_column_ids: Ec2Column::ids(),
ecr_repo_visible_column_ids: EcrColumn::ids(),
ecr_repo_column_ids: EcrColumn::ids(),
ecr_image_visible_column_ids: EcrImageColumn::ids(),
ecr_image_column_ids: EcrImageColumn::ids(),
apig_api_visible_column_ids: ApigColumn::ids(),
apig_api_column_ids: ApigColumn::ids(),
apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
apig_resource_visible_column_ids: ResourceColumn::all()
.iter()
.map(|c| c.id())
.collect(),
apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
lambda_application_column_ids: LambdaApplicationColumn::ids(),
lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
lambda_deployment_column_ids: DeploymentColumn::ids(),
lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
cfn_visible_column_ids: [
CfnColumn::Name,
CfnColumn::Status,
CfnColumn::CreatedTime,
CfnColumn::Description,
]
.iter()
.map(|c| c.id())
.collect(),
cfn_column_ids: CfnColumn::ids(),
cfn_parameter_visible_column_ids: parameter_column_ids(),
cfn_parameter_column_ids: parameter_column_ids(),
cfn_output_visible_column_ids: output_column_ids(),
cfn_output_column_ids: output_column_ids(),
cfn_resource_visible_column_ids: resource_column_ids(),
cfn_resource_column_ids: resource_column_ids(),
cfn_event_visible_column_ids: event_visible_column_ids(),
cfn_event_column_ids: event_column_ids(),
cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
cfn_change_set_column_ids: change_set_column_ids(),
iam_user_visible_column_ids: UserColumn::visible(),
iam_user_column_ids: UserColumn::ids(),
iam_role_visible_column_ids: RoleColumn::visible(),
iam_role_column_ids: RoleColumn::ids(),
iam_group_visible_column_ids: vec![
"Group name".to_string(),
"Users".to_string(),
"Permissions".to_string(),
"Creation time".to_string(),
],
iam_group_column_ids: vec![
"Group name".to_string(),
"Path".to_string(),
"Users".to_string(),
"Permissions".to_string(),
"Creation time".to_string(),
],
iam_policy_visible_column_ids: vec![
"Policy name".to_string(),
"Type".to_string(),
"Attached via".to_string(),
],
iam_policy_column_ids: vec![
"Policy name".to_string(),
"Type".to_string(),
"Attached via".to_string(),
"Attached entities".to_string(),
"Description".to_string(),
"Creation time".to_string(),
"Edited time".to_string(),
],
preference_section: Preferences::Columns,
view_mode: ViewMode::List,
error_message: None,
error_scroll: 0,
page_input: String::new(),
calendar_date: None,
calendar_selecting: CalendarField::StartDate,
cursor_pos: 0,
current_session: None,
sessions: Vec::new(),
session_picker_selected: 0,
session_filter: String::new(),
session_filter_active: false,
region_filter: String::new(),
region_filter_active: false,
region_picker_selected: 0,
region_latencies: std::collections::HashMap::new(),
profile_filter: String::new(),
profile_filter_active: false,
profile_picker_selected: 0,
available_profiles: Vec::new(),
snapshot_requested: false,
})
}
pub fn new_without_client(profile: String, region: Option<String>) -> Self {
let config = AwsConfig::dummy(region.clone());
Self {
running: true,
mode: Mode::ServicePicker,
config: config.clone(),
cloudwatch_client: CloudWatchClient::dummy(config.clone()),
cloudtrail_client: CloudTrailClient::new(config.clone()),
s3_client: S3Client::new(config.clone()),
sqs_client: SqsClient::new(config.clone()),
alarms_client: AlarmsClient::new(config.clone()),
ec2_client: Ec2Client::new(config.clone()),
ecr_client: EcrClient::new(config.clone()),
apig_client: ApiGatewayClient::new(config.clone()),
iam_client: IamClient::new(config.clone()),
lambda_client: LambdaClient::new(config.clone()),
cloudformation_client: CloudFormationClient::new(config.clone()),
current_service: Service::CloudWatchLogGroups,
tabs: Vec::new(),
current_tab: 0,
tab_picker_selected: 0,
tab_filter: String::new(),
pending_key: None,
log_groups_state: CloudWatchLogGroupsState::new(),
insights_state: CloudWatchInsightsState::new(),
alarms_state: CloudWatchAlarmsState::new(),
s3_state: S3State::new(),
cloudtrail_state: CloudTrailState {
table: TableState::new(),
input_focus: InputFocus::Filter,
current_event: None,
event_json_scroll: 0,
detail_focus: CloudTrailDetailFocus::Resources,
resources_expanded_index: None,
},
sqs_state: SqsState::new(),
ec2_state: Ec2State::default(),
ecr_state: EcrState::new(),
apig_state: ApigState::new(),
lambda_state: LambdaState::new(),
lambda_application_state: LambdaApplicationState::new(),
cfn_state: CfnState::new(),
iam_state: IamState::new(),
service_picker: ServicePickerState::new(),
service_selected: false,
profile,
region: region.unwrap_or_default(),
region_selector_index: 0,
cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
cw_log_group_column_ids: LogGroupColumn::ids(),
column_selector_index: 0,
preference_section: Preferences::Columns,
cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
cw_log_stream_column_ids: StreamColumn::ids(),
cw_log_event_visible_column_ids: EventColumn::default_visible(),
cw_log_event_column_ids: EventColumn::ids(),
cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
cw_alarm_visible_column_ids: [
AlarmColumn::Name,
AlarmColumn::State,
AlarmColumn::LastStateUpdate,
AlarmColumn::Conditions,
AlarmColumn::Actions,
]
.iter()
.map(|c| c.id())
.collect(),
cw_alarm_column_ids: AlarmColumn::ids(),
s3_bucket_visible_column_ids: S3BucketColumn::ids(),
cloudtrail_event_visible_column_ids: [
CloudTrailEventColumn::EventName,
CloudTrailEventColumn::EventTime,
CloudTrailEventColumn::Username,
CloudTrailEventColumn::EventSource,
CloudTrailEventColumn::ResourceType,
CloudTrailEventColumn::ResourceName,
]
.iter()
.map(|c| c.id())
.collect(),
cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
cloudtrail_resource_column_ids: EventResourceColumn::ids(),
s3_bucket_column_ids: S3BucketColumn::ids(),
sqs_visible_column_ids: [
SqsColumn::Name,
SqsColumn::Type,
SqsColumn::Created,
SqsColumn::MessagesAvailable,
SqsColumn::MessagesInFlight,
SqsColumn::Encryption,
SqsColumn::ContentBasedDeduplication,
]
.iter()
.map(|c| c.id())
.collect(),
sqs_column_ids: SqsColumn::ids(),
ec2_visible_column_ids: [
Ec2Column::Name,
Ec2Column::InstanceId,
Ec2Column::InstanceState,
Ec2Column::InstanceType,
Ec2Column::StatusCheck,
Ec2Column::AlarmStatus,
Ec2Column::AvailabilityZone,
Ec2Column::PublicIpv4Dns,
Ec2Column::PublicIpv4Address,
Ec2Column::ElasticIp,
Ec2Column::Ipv6Ips,
Ec2Column::Monitoring,
Ec2Column::SecurityGroupName,
Ec2Column::KeyName,
Ec2Column::LaunchTime,
Ec2Column::PlatformDetails,
]
.iter()
.map(|c| c.id())
.collect(),
ec2_column_ids: Ec2Column::ids(),
ecr_repo_visible_column_ids: EcrColumn::ids(),
ecr_repo_column_ids: EcrColumn::ids(),
ecr_image_visible_column_ids: EcrImageColumn::ids(),
ecr_image_column_ids: EcrImageColumn::ids(),
lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
lambda_application_column_ids: LambdaApplicationColumn::ids(),
apig_api_visible_column_ids: ApigColumn::ids(),
apig_api_column_ids: ApigColumn::ids(),
apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
apig_resource_visible_column_ids: ResourceColumn::all()
.iter()
.map(|c| c.id())
.collect(),
apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
lambda_deployment_column_ids: DeploymentColumn::ids(),
lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
cfn_visible_column_ids: [
CfnColumn::Name,
CfnColumn::Status,
CfnColumn::CreatedTime,
CfnColumn::Description,
]
.iter()
.map(|c| c.id())
.collect(),
cfn_column_ids: CfnColumn::ids(),
iam_user_visible_column_ids: UserColumn::visible(),
cfn_parameter_visible_column_ids: parameter_column_ids(),
cfn_parameter_column_ids: parameter_column_ids(),
cfn_output_visible_column_ids: output_column_ids(),
cfn_output_column_ids: output_column_ids(),
cfn_resource_visible_column_ids: resource_column_ids(),
cfn_resource_column_ids: resource_column_ids(),
cfn_event_visible_column_ids: event_visible_column_ids(),
cfn_event_column_ids: event_column_ids(),
cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
cfn_change_set_column_ids: change_set_column_ids(),
iam_user_column_ids: UserColumn::ids(),
iam_role_visible_column_ids: RoleColumn::visible(),
iam_role_column_ids: RoleColumn::ids(),
iam_group_visible_column_ids: vec![
"Group name".to_string(),
"Users".to_string(),
"Permissions".to_string(),
"Creation time".to_string(),
],
iam_group_column_ids: vec![
"Group name".to_string(),
"Path".to_string(),
"Users".to_string(),
"Permissions".to_string(),
"Creation time".to_string(),
],
iam_policy_visible_column_ids: vec![
"Policy name".to_string(),
"Type".to_string(),
"Attached via".to_string(),
],
iam_policy_column_ids: vec![
"Policy name".to_string(),
"Type".to_string(),
"Attached via".to_string(),
"Attached entities".to_string(),
"Description".to_string(),
"Creation time".to_string(),
"Edited time".to_string(),
],
view_mode: ViewMode::List,
error_message: None,
error_scroll: 0,
page_input: String::new(),
calendar_date: None,
calendar_selecting: CalendarField::StartDate,
cursor_pos: 0,
current_session: None,
sessions: Vec::new(),
session_picker_selected: 0,
session_filter: String::new(),
session_filter_active: false,
region_filter: String::new(),
region_filter_active: false,
region_picker_selected: 0,
region_latencies: std::collections::HashMap::new(),
profile_filter: String::new(),
profile_filter_active: false,
profile_picker_selected: 0,
available_profiles: Vec::new(),
snapshot_requested: false,
}
}
pub fn handle_action(&mut self, action: Action) {
match action {
Action::Noop => {}
Action::EnterFilterMode => match self.mode {
Mode::ServicePicker => self.service_picker.filter_active = true,
Mode::RegionPicker => self.region_filter_active = true,
Mode::SessionPicker => self.session_filter_active = true,
Mode::ProfilePicker => self.profile_filter_active = true,
_ => {}
},
Action::ExitFilterMode => match self.mode {
Mode::ServicePicker => {
if self.service_picker.filter_active {
self.service_picker.filter_active = false;
} else {
self.mode = Mode::Normal;
}
}
Mode::RegionPicker => {
if self.region_filter_active {
self.region_filter_active = false;
} else {
self.mode = Mode::Normal;
}
}
Mode::SessionPicker => {
if self.session_filter_active {
self.session_filter_active = false;
} else {
self.mode = Mode::Normal;
}
}
Mode::ProfilePicker => {
if self.profile_filter_active {
self.profile_filter_active = false;
} else {
self.mode = Mode::Normal;
}
}
_ => {}
},
Action::Quit => {
self.save_current_session();
self.running = false;
}
Action::CloseService => {
if !self.tabs.is_empty() {
self.tabs.remove(self.current_tab);
if self.tabs.is_empty() {
self.service_selected = false;
self.current_tab = 0;
self.mode = Mode::ServicePicker;
} else {
if self.current_tab >= self.tabs.len() {
self.current_tab = self.tabs.len() - 1;
}
self.current_service = self.tabs[self.current_tab].service;
self.service_selected = true;
self.mode = Mode::Normal;
}
} else {
self.service_selected = false;
self.mode = Mode::Normal;
}
self.service_picker.filter.clear();
self.service_picker.selected = 0;
}
Action::NextItem => {
let should_navigate = match self.mode {
Mode::ServicePicker => !self.service_picker.filter_active,
Mode::RegionPicker => !self.region_filter_active,
Mode::SessionPicker => !self.session_filter_active,
Mode::ProfilePicker => !self.profile_filter_active,
_ => true,
};
if should_navigate {
self.next_item();
} else if self.mode == Mode::RegionPicker && self.region_filter_active {
self.region_filter.push('j');
self.region_picker_selected = 0;
}
}
Action::PrevItem => {
let should_navigate = match self.mode {
Mode::ServicePicker => !self.service_picker.filter_active,
Mode::RegionPicker => !self.region_filter_active,
Mode::SessionPicker => !self.session_filter_active,
Mode::ProfilePicker => !self.profile_filter_active,
_ => true,
};
if should_navigate {
self.prev_item();
} else if self.mode == Mode::RegionPicker && self.region_filter_active {
self.region_filter.push('k');
self.region_picker_selected = 0;
}
}
Action::PageUp => self.page_up(),
Action::PageDown => self.page_down(),
Action::NextPane => self.next_pane(),
Action::PrevPane => self.prev_pane(),
Action::CollapseRow => self.collapse_row(),
Action::ExpandRow => self.expand_row(),
Action::Select => self.select_item(),
Action::OpenSpaceMenu => {
self.mode = Mode::SpaceMenu;
self.service_picker.filter.clear();
self.service_picker.filter_active = false;
self.service_picker.selected = 0;
}
Action::CloseMenu => {
self.mode = Mode::Normal;
self.service_picker.filter.clear();
match self.current_service {
Service::S3Buckets => {
crate::s3::actions::reset_on_service_select(self);
}
Service::CloudFormationStacks => {
crate::cfn::actions::apply_filter_reset(self);
}
Service::LambdaFunctions => {
crate::lambda::functions::apply_filter_reset(self);
}
Service::SqsQueues => {
self.sqs_state.queues.reset();
}
Service::IamRoles => {
crate::iam::actions::reset_on_service_select_roles(self);
}
Service::IamUsers => {
crate::iam::actions::reset_on_service_select_users(self);
}
Service::IamUserGroups => {
crate::iam::actions::reset_on_service_select_groups(self);
}
Service::CloudWatchAlarms => {
self.alarms_state.table.reset();
}
Service::Ec2Instances => {
crate::ec2::actions::apply_filter_reset(self);
}
Service::EcrRepositories => {
crate::ecr::actions::apply_filter_reset(self);
}
Service::ApiGatewayApis => {
self.apig_state.apis.reset();
}
Service::LambdaApplications => {
crate::lambda::applications::apply_filter_reset(self);
}
_ => {}
}
}
Action::NextTab => {
if !self.tabs.is_empty() {
self.current_tab = (self.current_tab + 1) % self.tabs.len();
self.current_service = self.tabs[self.current_tab].service;
}
}
Action::PrevTab => {
if !self.tabs.is_empty() {
self.current_tab = if self.current_tab == 0 {
self.tabs.len() - 1
} else {
self.current_tab - 1
};
self.current_service = self.tabs[self.current_tab].service;
}
}
Action::CloseTab => {
if !self.tabs.is_empty() {
self.tabs.remove(self.current_tab);
if self.tabs.is_empty() {
self.service_selected = false;
self.current_tab = 0;
self.service_picker.filter.clear();
self.service_picker.selected = 0;
self.mode = Mode::ServicePicker;
} else {
if self.current_tab >= self.tabs.len() {
self.current_tab = self.tabs.len() - 1;
}
self.current_service = self.tabs[self.current_tab].service;
self.service_selected = true;
self.mode = Mode::Normal;
}
}
}
Action::OpenTabPicker => {
if !self.tabs.is_empty() {
self.tab_picker_selected = self.current_tab;
self.mode = Mode::TabPicker;
} else {
self.mode = Mode::Normal;
}
}
Action::OpenSessionPicker => {
self.save_current_session();
self.sessions = Session::list_all().unwrap_or_default();
self.session_picker_selected = 0;
self.mode = Mode::SessionPicker;
}
Action::LoadSession => {
let filtered_sessions = self.get_filtered_sessions();
if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
let session = session.clone();
self.profile = session.profile.clone();
self.region = session.region.clone();
self.config.account_id = session.account_id.clone();
self.config.role_arn = session.role_arn.clone();
self.tabs.clear();
for session_tab in &session.tabs {
let service = match session_tab.service.as_str() {
"ApiGatewayApis" => Service::ApiGatewayApis,
"CloudWatchLogGroups" => Service::CloudWatchLogGroups,
"CloudWatchInsights" => Service::CloudWatchInsights,
"CloudWatchAlarms" => Service::CloudWatchAlarms,
"CloudTrailEvents" => Service::CloudTrailEvents,
"S3Buckets" => Service::S3Buckets,
"SqsQueues" => Service::SqsQueues,
"Ec2Instances" => Service::Ec2Instances,
"EcrRepositories" => Service::EcrRepositories,
"LambdaFunctions" => Service::LambdaFunctions,
"LambdaApplications" => Service::LambdaApplications,
"CloudFormationStacks" => Service::CloudFormationStacks,
"IamUsers" => Service::IamUsers,
"IamRoles" => Service::IamRoles,
"IamUserGroups" => Service::IamUserGroups,
_ => continue,
};
self.tabs.push(Tab {
service,
title: session_tab.title.clone(),
breadcrumb: session_tab.breadcrumb.clone(),
});
if let Some(filter) = &session_tab.filter {
if service == Service::CloudWatchLogGroups {
self.log_groups_state.log_groups.filter = filter.clone();
}
}
}
if !self.tabs.is_empty() {
self.current_tab = 0;
self.current_service = self.tabs[0].service;
self.service_selected = true;
self.current_session = Some(session.clone());
}
}
self.mode = Mode::Normal;
}
Action::SaveSession => {
}
Action::OpenServicePicker => {
if self.mode == Mode::ServicePicker {
self.tabs.push(Tab {
service: Service::S3Buckets,
title: "S3 › Buckets".to_string(),
breadcrumb: "S3 › Buckets".to_string(),
});
self.current_tab = self.tabs.len() - 1;
self.current_service = Service::S3Buckets;
self.view_mode = ViewMode::List;
self.service_selected = true;
self.mode = Mode::Normal;
} else {
self.mode = Mode::ServicePicker;
self.service_picker.filter.clear();
self.service_picker.selected = 0;
}
}
Action::OpenCloudWatch => {
self.current_service = Service::CloudWatchLogGroups;
self.view_mode = ViewMode::List;
self.service_selected = true;
self.mode = Mode::Normal;
}
Action::OpenCloudWatchSplit => {
self.current_service = Service::CloudWatchInsights;
self.view_mode = ViewMode::InsightsResults;
self.service_selected = true;
self.mode = Mode::Normal;
}
Action::OpenCloudWatchAlarms => {
self.current_service = Service::CloudWatchAlarms;
self.view_mode = ViewMode::List;
self.service_selected = true;
self.mode = Mode::Normal;
}
Action::FilterInput(c) => {
if self.mode == Mode::TabPicker {
self.tab_filter.push(c);
self.tab_picker_selected = 0;
} else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
self.service_picker.filter.push(c);
self.service_picker.selected = 0;
} else if self.mode == Mode::RegionPicker && self.region_filter_active {
self.region_filter.push(c);
self.region_picker_selected = 0;
} else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
self.profile_filter.push(c);
self.profile_picker_selected = 0;
} else if self.mode == Mode::SessionPicker && self.session_filter_active {
self.session_filter.push(c);
self.session_picker_selected = 0;
} else if self.mode == Mode::InsightsInput {
match self.insights_state.insights.insights_focus {
InsightsFocus::Query => {
self.insights_state.insights.query_text.push(c);
}
InsightsFocus::LogGroupSearch => {
self.insights_state.insights.log_group_search.push(c);
if !self.insights_state.insights.log_group_search.is_empty() {
self.insights_state.insights.log_group_matches = self
.log_groups_state
.log_groups
.items
.iter()
.filter(|g| {
g.name.to_lowercase().contains(
&self
.insights_state
.insights
.log_group_search
.to_lowercase(),
)
})
.take(50)
.map(|g| g.name.clone())
.collect();
self.insights_state.insights.show_dropdown = true;
} else {
self.insights_state.insights.log_group_matches.clear();
self.insights_state.insights.show_dropdown = false;
}
}
_ => {}
}
} else if self.mode == Mode::FilterInput {
let is_pagination_focused = if self.current_service
== Service::LambdaApplications
{
crate::lambda::applications::is_pagination_focused(self)
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::is_pagination_focused(self)
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_none()
{
self.iam_state.role_input_focus == InputFocus::Pagination
} else if self.view_mode == ViewMode::PolicyView {
self.iam_state.policy_input_focus == InputFocus::Pagination
} else if self.current_service == Service::CloudWatchAlarms {
self.alarms_state.input_focus == InputFocus::Pagination
} else if self.current_service == Service::CloudTrailEvents {
self.cloudtrail_state.input_focus == InputFocus::Pagination
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::is_pagination_focused(self)
} else if self.current_service == Service::CloudWatchLogGroups {
self.log_groups_state.input_focus == InputFocus::Pagination
} else if self.current_service == Service::ApiGatewayApis {
self.apig_state.input_focus == InputFocus::Pagination
} else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
crate::ecr::actions::is_pagination_focused(self)
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::is_pagination_focused(self)
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some()
&& (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
|| self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
|| self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
|| self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
|| self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
{
self.sqs_state.input_focus == InputFocus::Pagination
} else {
false
}
} else {
false
};
if is_pagination_focused && c.is_ascii_digit() {
self.page_input.push(c);
} else if self.current_service == Service::LambdaApplications {
if crate::lambda::applications::is_filter_focused(self) {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::filter_char_push(self, c);
} else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
if self.ecr_state.input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_none()
{
if self.iam_state.role_input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.view_mode == ViewMode::PolicyView {
if self.iam_state.policy_input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.current_service == Service::ApiGatewayApis
&& self.apig_state.current_api.is_some()
&& self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
{
if self.apig_state.input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.current_service == Service::LambdaFunctions {
if crate::lambda::functions::filter_char_allowed(self) {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
|| self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
|| self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
|| self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
|| self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
{
if self.sqs_state.input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.current_service == Service::Ec2Instances
&& self.ec2_state.current_instance.is_some()
&& self.ec2_state.detail_tab == Ec2DetailTab::Tags
{
crate::ec2::actions::filter_char_push(self, c);
} else if self.current_service == Service::CloudWatchLogGroups {
if self.log_groups_state.input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| f.push(c));
}
} else {
self.apply_filter_operation(|f| f.push(c));
}
} else if self.mode == Mode::EventFilterInput {
if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
self.log_groups_state.event_filter.push(c);
} else if c.is_ascii_digit() {
self.log_groups_state.relative_amount.push(c);
}
} else if self.mode == Mode::Normal && c.is_ascii_digit() {
self.page_input.push(c);
}
}
Action::FilterBackspace => {
if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
self.service_picker.filter.pop();
self.service_picker.selected = 0;
} else if self.mode == Mode::TabPicker {
self.tab_filter.pop();
self.tab_picker_selected = 0;
} else if self.mode == Mode::RegionPicker && self.region_filter_active {
self.region_filter.pop();
self.region_picker_selected = 0;
} else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
self.profile_filter.pop();
self.profile_picker_selected = 0;
} else if self.mode == Mode::SessionPicker && self.session_filter_active {
self.session_filter.pop();
self.session_picker_selected = 0;
} else if self.mode == Mode::InsightsInput {
match self.insights_state.insights.insights_focus {
InsightsFocus::Query => {
self.insights_state.insights.query_text.pop();
}
InsightsFocus::LogGroupSearch => {
self.insights_state.insights.log_group_search.pop();
if !self.insights_state.insights.log_group_search.is_empty() {
self.insights_state.insights.log_group_matches = self
.log_groups_state
.log_groups
.items
.iter()
.filter(|g| {
g.name.to_lowercase().contains(
&self
.insights_state
.insights
.log_group_search
.to_lowercase(),
)
})
.take(50)
.map(|g| g.name.clone())
.collect();
self.insights_state.insights.show_dropdown = true;
} else {
self.insights_state.insights.log_group_matches.clear();
self.insights_state.insights.show_dropdown = false;
}
}
_ => {}
}
} else if self.mode == Mode::FilterInput {
if self.current_service == Service::CloudFormationStacks {
if self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Parameters
{
if self.cfn_state.parameters_input_focus == InputFocus::Filter {
self.cfn_state.parameters.filter.pop();
self.cfn_state.parameters.selected = 0;
}
} else if self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Outputs
{
if self.cfn_state.outputs_input_focus == InputFocus::Filter {
self.cfn_state.outputs.filter.pop();
self.cfn_state.outputs.selected = 0;
}
} else if self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Resources
{
if self.cfn_state.resources_input_focus == InputFocus::Filter {
self.cfn_state.resources.filter.pop();
self.cfn_state.resources.selected = 0;
}
} else if self.cfn_state.input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| {
f.pop();
});
}
} else if self.current_service == Service::Ec2Instances
&& self.ec2_state.current_instance.is_some()
&& self.ec2_state.detail_tab == Ec2DetailTab::Tags
{
crate::ec2::actions::filter_char_pop(self);
} else if self.current_service == Service::CloudWatchLogGroups {
if self.log_groups_state.input_focus == InputFocus::Filter {
self.apply_filter_operation(|f| {
f.pop();
});
}
} else {
self.apply_filter_operation(|f| {
f.pop();
});
}
} else if self.mode == Mode::EventFilterInput {
if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
self.log_groups_state.event_filter.pop();
} else {
self.log_groups_state.relative_amount.pop();
}
}
}
Action::DeleteWord => {
let text = if self.mode == Mode::ServicePicker {
&mut self.service_picker.filter
} else if self.mode == Mode::InsightsInput {
use crate::app::InsightsFocus;
match self.insights_state.insights.insights_focus {
InsightsFocus::Query => &mut self.insights_state.insights.query_text,
InsightsFocus::LogGroupSearch => {
&mut self.insights_state.insights.log_group_search
}
_ => return,
}
} else if self.mode == Mode::FilterInput {
if let Some(filter) = self.get_active_filter_mut() {
filter
} else {
return;
}
} else if self.mode == Mode::EventFilterInput {
if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
&mut self.log_groups_state.event_filter
} else {
&mut self.log_groups_state.relative_amount
}
} else {
return;
};
if text.is_empty() {
return;
}
let mut chars: Vec<char> = text.chars().collect();
while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
chars.pop();
}
while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
chars.pop();
}
*text = chars.into_iter().collect();
}
Action::WordLeft => {
}
Action::WordRight => {
}
Action::OpenColumnSelector => {
if self.current_service == Service::CloudFormationStacks
&& crate::cfn::actions::block_column_selector(self)
{
return;
}
if self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
&& self.iam_state.user_tab == UserTab::SecurityCredentials
{
return;
}
if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& (self.iam_state.role_tab == RoleTab::TrustRelationships
|| self.iam_state.role_tab == RoleTab::RevokeSessions)
{
return;
}
if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& matches!(
self.sqs_state.detail_tab,
SqsQueueDetailTab::QueuePolicies
| SqsQueueDetailTab::Monitoring
| SqsQueueDetailTab::DeadLetterQueue
| SqsQueueDetailTab::Encryption
| SqsQueueDetailTab::DeadLetterQueueRedriveTasks
)
{
return;
}
if self.current_service == Service::Ec2Instances
&& crate::ec2::actions::block_column_selector(self)
{
return;
}
if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
{
return;
}
if !self.page_input.is_empty() {
if let Ok(page) = self.page_input.parse::<usize>() {
self.go_to_page(page);
}
self.page_input.clear();
} else {
self.mode = Mode::ColumnSelector;
self.column_selector_index = 0;
}
}
Action::ToggleColumn => {
if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_none()
{
crate::s3::actions::toggle_column(self);
} else if self.current_service == Service::CloudWatchAlarms {
let idx = self.column_selector_index;
if (1..=16).contains(&idx) {
if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
Self::toggle_column_visibility(
&mut self.cw_alarm_visible_column_ids,
&self.cw_alarm_column_ids,
*col,
);
}
} else if idx == 19 {
self.alarms_state.view_as = AlarmViewMode::Table;
} else if idx == 20 {
self.alarms_state.view_as = AlarmViewMode::Cards;
} else if idx == 23 {
self.alarms_state.table.page_size = PageSize::Ten;
} else if idx == 24 {
self.alarms_state.table.page_size = PageSize::TwentyFive;
} else if idx == 25 {
self.alarms_state.table.page_size = PageSize::Fifty;
} else if idx == 26 {
self.alarms_state.table.page_size = PageSize::OneHundred;
} else if idx == 29 {
self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
}
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
{
let idx = self.column_selector_index;
if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
Self::toggle_column_visibility(
&mut self.cloudtrail_resource_visible_column_ids,
&self.cloudtrail_resource_column_ids,
*col,
);
}
}
} else {
let idx = self.column_selector_index;
if (1..=14).contains(&idx) {
if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
Self::toggle_column_visibility(
&mut self.cloudtrail_event_visible_column_ids,
&self.cloudtrail_event_column_ids,
*col,
);
}
} else if idx == 17 {
self.cloudtrail_state.table.page_size = PageSize::Ten;
self.cloudtrail_state.table.snap_to_page();
} else if idx == 18 {
self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
self.cloudtrail_state.table.snap_to_page();
} else if idx == 19 {
self.cloudtrail_state.table.page_size = PageSize::Fifty;
self.cloudtrail_state.table.snap_to_page();
} else if idx == 20 {
self.cloudtrail_state.table.page_size = PageSize::OneHundred;
self.cloudtrail_state.table.snap_to_page();
}
}
} else if self.current_service == Service::ApiGatewayApis {
if let Some(api) = &self.apig_state.current_api {
use crate::ui::apig::ApiDetailTab;
if self.apig_state.detail_tab == ApiDetailTab::Routes {
if api.protocol_type.to_uppercase() == "REST" {
let idx = self.column_selector_index;
if idx > 1 && idx <= self.apig_resource_column_ids.len() {
if let Some(col) = self.apig_resource_column_ids.get(idx - 1) {
if let Some(pos) = self
.apig_resource_visible_column_ids
.iter()
.position(|c| c == col)
{
self.apig_resource_visible_column_ids.remove(pos);
} else {
self.apig_resource_visible_column_ids.push(*col);
}
}
}
} else {
let idx = self.column_selector_index;
if idx > 1 && idx <= self.apig_route_column_ids.len() {
if let Some(col) = self.apig_route_column_ids.get(idx - 1) {
if let Some(pos) = self
.apig_route_visible_column_ids
.iter()
.position(|c| c == col)
{
self.apig_route_visible_column_ids.remove(pos);
} else {
self.apig_route_visible_column_ids.push(*col);
}
}
}
}
}
} else {
let idx = self.column_selector_index;
if idx > 0 && idx <= self.apig_api_column_ids.len() {
if let Some(col) = self.apig_api_column_ids.get(idx - 1) {
if let Some(pos) = self
.apig_api_visible_column_ids
.iter()
.position(|c| c == col)
{
self.apig_api_visible_column_ids.remove(pos);
} else {
self.apig_api_visible_column_ids.push(*col);
}
}
} else if idx == self.apig_api_column_ids.len() + 3 {
self.apig_state.apis.page_size = PageSize::Ten;
} else if idx == self.apig_api_column_ids.len() + 4 {
self.apig_state.apis.page_size = PageSize::TwentyFive;
} else if idx == self.apig_api_column_ids.len() + 5 {
self.apig_state.apis.page_size = PageSize::Fifty;
} else if idx == self.apig_api_column_ids.len() + 6 {
self.apig_state.apis.page_size = PageSize::OneHundred;
}
}
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::toggle_column(self);
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::toggle_column(self);
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
{
let idx = self.column_selector_index;
if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
if let Some(pos) = self
.sqs_state
.trigger_visible_column_ids
.iter()
.position(|c| c == col)
{
self.sqs_state.trigger_visible_column_ids.remove(pos);
} else {
self.sqs_state.trigger_visible_column_ids.push(col.clone());
}
}
} else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
self.sqs_state.triggers.page_size = PageSize::Ten;
} else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
self.sqs_state.triggers.page_size = PageSize::TwentyFive;
} else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
self.sqs_state.triggers.page_size = PageSize::Fifty;
} else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
self.sqs_state.triggers.page_size = PageSize::OneHundred;
}
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
{
let idx = self.column_selector_index;
if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
if let Some(pos) = self
.sqs_state
.pipe_visible_column_ids
.iter()
.position(|c| c == col)
{
self.sqs_state.pipe_visible_column_ids.remove(pos);
} else {
self.sqs_state.pipe_visible_column_ids.push(col.clone());
}
}
} else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
self.sqs_state.pipes.page_size = PageSize::Ten;
} else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
self.sqs_state.pipes.page_size = PageSize::TwentyFive;
} else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
self.sqs_state.pipes.page_size = PageSize::Fifty;
} else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
self.sqs_state.pipes.page_size = PageSize::OneHundred;
}
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
{
let idx = self.column_selector_index;
if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
if let Some(pos) = self
.sqs_state
.tag_visible_column_ids
.iter()
.position(|c| c == col)
{
self.sqs_state.tag_visible_column_ids.remove(pos);
} else {
self.sqs_state.tag_visible_column_ids.push(col.clone());
}
}
} else if idx == self.sqs_state.tag_column_ids.len() + 3 {
self.sqs_state.tags.page_size = PageSize::Ten;
} else if idx == self.sqs_state.tag_column_ids.len() + 4 {
self.sqs_state.tags.page_size = PageSize::TwentyFive;
} else if idx == self.sqs_state.tag_column_ids.len() + 5 {
self.sqs_state.tags.page_size = PageSize::Fifty;
} else if idx == self.sqs_state.tag_column_ids.len() + 6 {
self.sqs_state.tags.page_size = PageSize::OneHundred;
}
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
let idx = self.column_selector_index;
if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
if let Some(pos) = self
.sqs_state
.subscription_visible_column_ids
.iter()
.position(|c| c == col)
{
self.sqs_state.subscription_visible_column_ids.remove(pos);
} else {
self.sqs_state
.subscription_visible_column_ids
.push(col.clone());
}
}
} else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
self.sqs_state.subscriptions.page_size = PageSize::Ten;
} else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
} else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
self.sqs_state.subscriptions.page_size = PageSize::Fifty;
} else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
}
} else {
let idx = self.column_selector_index;
if let Some(col) = self.sqs_column_ids.get(idx) {
if let Some(pos) =
self.sqs_visible_column_ids.iter().position(|c| c == col)
{
self.sqs_visible_column_ids.remove(pos);
} else {
self.sqs_visible_column_ids.push(*col);
}
} else if idx == self.sqs_column_ids.len() + 2 {
self.sqs_state.queues.page_size = PageSize::Ten;
} else if idx == self.sqs_column_ids.len() + 3 {
self.sqs_state.queues.page_size = PageSize::TwentyFive;
} else if idx == self.sqs_column_ids.len() + 4 {
self.sqs_state.queues.page_size = PageSize::Fifty;
} else if idx == self.sqs_column_ids.len() + 5 {
self.sqs_state.queues.page_size = PageSize::OneHundred;
}
}
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::toggle_column(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::toggle_column(self);
} else if self.view_mode == ViewMode::Events {
if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
{
if let Some(pos) = self
.cw_log_event_visible_column_ids
.iter()
.position(|c| c == col)
{
self.cw_log_event_visible_column_ids.remove(pos);
} else {
self.cw_log_event_visible_column_ids.push(*col);
}
}
} else if self.view_mode == ViewMode::Detail {
let idx = self.column_selector_index;
if self.log_groups_state.detail_tab == DetailTab::Tags {
if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
if let Some(pos) = self
.cw_log_tag_visible_column_ids
.iter()
.position(|c| c == col)
{
self.cw_log_tag_visible_column_ids.remove(pos);
} else {
self.cw_log_tag_visible_column_ids.push(*col);
}
}
} else if idx == self.cw_log_tag_column_ids.len() + 3 {
self.log_groups_state.tags.page_size = PageSize::Ten;
} else if idx == self.cw_log_tag_column_ids.len() + 4 {
self.log_groups_state.tags.page_size = PageSize::TwentyFive;
} else if idx == self.cw_log_tag_column_ids.len() + 5 {
self.log_groups_state.tags.page_size = PageSize::Fifty;
} else if idx == self.cw_log_tag_column_ids.len() + 6 {
self.log_groups_state.tags.page_size = PageSize::OneHundred;
}
} else {
if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
if let Some(pos) = self
.cw_log_stream_visible_column_ids
.iter()
.position(|c| c == col)
{
self.cw_log_stream_visible_column_ids.remove(pos);
} else {
self.cw_log_stream_visible_column_ids.push(*col);
}
}
} else if idx == self.cw_log_stream_column_ids.len() + 3 {
self.log_groups_state.stream_page_size = 10;
self.log_groups_state.stream_current_page = 0;
} else if idx == self.cw_log_stream_column_ids.len() + 4 {
self.log_groups_state.stream_page_size = 25;
self.log_groups_state.stream_current_page = 0;
} else if idx == self.cw_log_stream_column_ids.len() + 5 {
self.log_groups_state.stream_page_size = 50;
self.log_groups_state.stream_current_page = 0;
} else if idx == self.cw_log_stream_column_ids.len() + 6 {
self.log_groups_state.stream_page_size = 100;
self.log_groups_state.stream_current_page = 0;
}
}
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::toggle_column(self);
} else if self.current_service == Service::IamUsers {
let idx = self.column_selector_index;
if self.iam_state.current_user.is_some() {
match self.iam_state.user_tab {
UserTab::Permissions => {
if idx > 0 && idx <= self.iam_policy_column_ids.len() {
if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
if let Some(pos) = self
.iam_policy_visible_column_ids
.iter()
.position(|c| c == col)
{
self.iam_policy_visible_column_ids.remove(pos);
} else {
self.iam_policy_visible_column_ids.push(col.clone());
}
}
} else if idx == self.iam_policy_column_ids.len() + 3 {
self.iam_state.policies.page_size = PageSize::Ten;
} else if idx == self.iam_policy_column_ids.len() + 4 {
self.iam_state.policies.page_size = PageSize::TwentyFive;
} else if idx == self.iam_policy_column_ids.len() + 5 {
self.iam_state.policies.page_size = PageSize::Fifty;
}
}
UserTab::Groups => {
toggle_iam_page_size_only(
idx,
5,
&mut self.iam_state.user_group_memberships.page_size,
);
}
UserTab::Tags => {
toggle_iam_page_size_only(
idx,
5,
&mut self.iam_state.user_tags.page_size,
);
}
UserTab::LastAccessed => {
toggle_iam_page_size_only(
idx,
6,
&mut self.iam_state.last_accessed_services.page_size,
);
}
_ => {}
}
} else {
toggle_iam_preference_static(
idx,
&self.iam_user_column_ids,
&mut self.iam_user_visible_column_ids,
&mut self.iam_state.users.page_size,
);
}
} else if self.current_service == Service::IamRoles {
let idx = self.column_selector_index;
if self.iam_state.current_role.is_some() {
match self.iam_state.role_tab {
RoleTab::Permissions => {
toggle_iam_preference(
idx,
&self.iam_policy_column_ids,
&mut self.iam_policy_visible_column_ids,
&mut self.iam_state.policies.page_size,
);
}
RoleTab::LastAccessed => {
toggle_iam_page_size_only(
idx,
6,
&mut self.iam_state.last_accessed_services.page_size,
);
}
_ => {}
}
} else {
toggle_iam_preference_static(
idx,
&self.iam_role_column_ids,
&mut self.iam_role_visible_column_ids,
&mut self.iam_state.roles.page_size,
);
}
} else if self.current_service == Service::IamUserGroups {
toggle_iam_preference(
self.column_selector_index,
&self.iam_group_column_ids,
&mut self.iam_group_visible_column_ids,
&mut self.iam_state.groups.page_size,
);
} else {
let idx = self.column_selector_index;
if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
if let Some(pos) = self
.cw_log_group_visible_column_ids
.iter()
.position(|c| c == col)
{
self.cw_log_group_visible_column_ids.remove(pos);
} else {
self.cw_log_group_visible_column_ids.push(*col);
}
}
} else if idx == self.cw_log_group_column_ids.len() + 3 {
self.log_groups_state.log_groups.page_size = PageSize::Ten;
} else if idx == self.cw_log_group_column_ids.len() + 4 {
self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
} else if idx == self.cw_log_group_column_ids.len() + 5 {
self.log_groups_state.log_groups.page_size = PageSize::Fifty;
} else if idx == self.cw_log_group_column_ids.len() + 6 {
self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
}
}
}
Action::NextPreferences => {
if self.current_service == Service::ApiGatewayApis {
cycle_preference_next(
&mut self.column_selector_index,
self.apig_api_column_ids.len(),
);
} else if self.current_service == Service::CloudWatchAlarms {
if self.column_selector_index < 18 {
self.column_selector_index = 18; } else if self.column_selector_index < 22 {
self.column_selector_index = 22; } else if self.column_selector_index < 28 {
self.column_selector_index = 28; } else {
self.column_selector_index = 0; }
} else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_some()
{
crate::ecr::actions::next_preferences(self);
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::next_preferences(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::next_preferences(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::next_preferences(self);
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
{
self.column_selector_index = 0;
} else {
let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
}
} else if self.current_service == Service::Ec2Instances {
let page_size_idx = self.ec2_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
match self.iam_state.user_tab {
UserTab::Permissions => {
let page_size_idx = self.iam_policy_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
}
UserTab::Groups | UserTab::Tags => {
if self.column_selector_index < 4 {
self.column_selector_index = 4;
} else {
self.column_selector_index = 0;
}
}
UserTab::LastAccessed => {
if self.column_selector_index < 5 {
self.column_selector_index = 5;
} else {
self.column_selector_index = 0;
}
}
_ => {}
}
} else {
let page_size_idx = self.iam_user_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
match self.iam_state.role_tab {
RoleTab::Permissions => {
let page_size_idx = self.iam_policy_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
}
RoleTab::Tags => {
if self.column_selector_index < 4 {
self.column_selector_index = 4;
} else {
self.column_selector_index = 0;
}
}
RoleTab::LastAccessed => {
if self.column_selector_index < 5 {
self.column_selector_index = 5;
} else {
self.column_selector_index = 0;
}
}
_ => {}
}
} else {
let page_size_idx = self.iam_role_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
}
} else if self.current_service == Service::IamUserGroups {
let page_size_idx = self.iam_group_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
{
let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
{
let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
{
let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
if self.column_selector_index < page_size_idx {
self.column_selector_index = page_size_idx;
} else {
self.column_selector_index = 0;
}
} else if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_none()
{
crate::s3::actions::next_preferences(self);
} else if self.current_service == Service::CloudWatchLogGroups {
if self.view_mode == ViewMode::Events {
} else if self.view_mode == ViewMode::Detail {
if self.log_groups_state.detail_tab == DetailTab::Tags {
cycle_preference_next(
&mut self.column_selector_index,
self.cw_log_tag_column_ids.len(),
);
} else {
cycle_preference_next(
&mut self.column_selector_index,
self.cw_log_stream_column_ids.len(),
);
}
} else {
cycle_preference_next(
&mut self.column_selector_index,
self.cw_log_group_column_ids.len(),
);
}
}
}
Action::PrevPreferences => {
if self.current_service == Service::ApiGatewayApis {
cycle_preference_prev(
&mut self.column_selector_index,
self.apig_api_column_ids.len(),
);
} else if self.current_service == Service::CloudWatchAlarms {
if self.column_selector_index >= 28 {
self.column_selector_index = 22;
} else if self.column_selector_index >= 22 {
self.column_selector_index = 18;
} else if self.column_selector_index >= 18 {
self.column_selector_index = 0;
} else {
self.column_selector_index = 28;
}
} else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_some()
{
crate::ecr::actions::prev_preferences(self);
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::prev_preferences(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::prev_preferences(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::prev_preferences(self);
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
{
self.column_selector_index = 0;
} else {
let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
if self.column_selector_index >= page_size_idx {
self.column_selector_index = 0;
} else {
self.column_selector_index = page_size_idx;
}
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::next_preferences(self);
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
match self.iam_state.user_tab {
UserTab::Permissions => {
let page_size_idx = self.iam_policy_column_ids.len() + 2;
if self.column_selector_index >= page_size_idx {
self.column_selector_index = 0;
} else {
self.column_selector_index = page_size_idx;
}
}
UserTab::Groups | UserTab::Tags => {
if self.column_selector_index >= 4 {
self.column_selector_index = 0;
} else {
self.column_selector_index = 4;
}
}
UserTab::LastAccessed => {
if self.column_selector_index >= 5 {
self.column_selector_index = 0;
} else {
self.column_selector_index = 5;
}
}
_ => {}
}
} else {
let page_size_idx = self.iam_user_column_ids.len() + 2;
if self.column_selector_index >= page_size_idx {
self.column_selector_index = 0;
} else {
self.column_selector_index = page_size_idx;
}
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
match self.iam_state.role_tab {
RoleTab::Permissions => {
let page_size_idx = self.iam_policy_column_ids.len() + 2;
if self.column_selector_index >= page_size_idx {
self.column_selector_index = 0;
} else {
self.column_selector_index = page_size_idx;
}
}
RoleTab::Tags => {
if self.column_selector_index >= 4 {
self.column_selector_index = 0;
} else {
self.column_selector_index = 4;
}
}
RoleTab::LastAccessed => {
if self.column_selector_index >= 5 {
self.column_selector_index = 0;
} else {
self.column_selector_index = 5;
}
}
_ => {}
}
} else {
let page_size_idx = self.iam_role_column_ids.len() + 2;
if self.column_selector_index >= page_size_idx {
self.column_selector_index = 0;
} else {
self.column_selector_index = page_size_idx;
}
}
} else if self.current_service == Service::IamUserGroups {
let page_size_idx = self.iam_group_column_ids.len() + 2;
if self.column_selector_index >= page_size_idx {
self.column_selector_index = 0;
} else {
self.column_selector_index = page_size_idx;
}
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
{
let page_size_idx = match self.sqs_state.detail_tab {
SqsQueueDetailTab::LambdaTriggers => {
self.sqs_state.trigger_column_ids.len() + 2
}
SqsQueueDetailTab::EventBridgePipes => {
self.sqs_state.pipe_column_ids.len() + 2
}
SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
SqsQueueDetailTab::SnsSubscriptions => {
self.sqs_state.subscription_column_ids.len() + 2
}
_ => 0,
};
if page_size_idx > 0 {
if self.column_selector_index >= page_size_idx {
self.column_selector_index = 0;
} else {
self.column_selector_index = page_size_idx;
}
}
} else if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_none()
{
crate::s3::actions::prev_preferences(self);
} else if self.current_service == Service::CloudWatchLogGroups {
if self.view_mode == ViewMode::Events {
} else if self.view_mode == ViewMode::Detail {
if self.log_groups_state.detail_tab == DetailTab::Tags {
cycle_preference_prev(
&mut self.column_selector_index,
self.cw_log_tag_column_ids.len(),
);
} else {
cycle_preference_prev(
&mut self.column_selector_index,
self.cw_log_stream_column_ids.len(),
);
}
} else {
cycle_preference_prev(
&mut self.column_selector_index,
self.cw_log_group_column_ids.len(),
);
}
}
}
Action::CloseColumnSelector => {
self.mode = Mode::Normal;
self.preference_section = Preferences::Columns;
}
Action::NextDetailTab => {
if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
} else if self.current_service == Service::ApiGatewayApis
&& self.apig_state.current_api.is_some()
{
self.apig_state.detail_tab = self.apig_state.detail_tab.next();
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
{
self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
self.sqs_state.set_metrics_loading(true);
self.sqs_state.set_monitoring_scroll(0);
self.sqs_state.clear_metrics();
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
self.sqs_state.triggers.loading = true;
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
self.sqs_state.pipes.loading = true;
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
self.sqs_state.tags.loading = true;
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
self.sqs_state.subscriptions.loading = true;
}
} else if self.current_service == Service::Ec2Instances
&& self.ec2_state.current_instance.is_some()
{
crate::ec2::actions::next_detail_tab(self);
} else if self.current_service == Service::LambdaApplications
&& self.lambda_application_state.current_application.is_some()
{
crate::lambda::applications::next_detail_tab(self);
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
{
crate::iam::actions::next_detail_tab_roles(self);
} else if self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
{
crate::iam::actions::next_detail_tab_users(self);
} else if self.current_service == Service::IamUserGroups
&& self.iam_state.current_group.is_some()
{
crate::iam::actions::next_detail_tab_groups(self);
} else if self.view_mode == ViewMode::Detail {
self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
if self.log_groups_state.detail_tab == DetailTab::Tags {
self.log_groups_state.tags.loading = true;
}
} else if self.current_service == Service::S3Buckets {
crate::s3::actions::next_detail_tab(self);
} else if self.current_service == Service::CloudWatchAlarms {
self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
AlarmTab::AllAlarms => AlarmTab::InAlarm,
AlarmTab::InAlarm => AlarmTab::AllAlarms,
};
self.alarms_state.table.reset();
} else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
crate::ecr::actions::next_detail_tab(self);
} else if self.current_service == Service::LambdaFunctions
&& self.lambda_state.current_function.is_some()
{
crate::lambda::functions::next_detail_tab(self);
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
{
crate::cfn::actions::next_detail_tab(self);
}
}
Action::PrevDetailTab => {
if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
} else if self.current_service == Service::ApiGatewayApis
&& self.apig_state.current_api.is_some()
{
self.apig_state.detail_tab = self.apig_state.detail_tab.prev();
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
{
self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
self.sqs_state.set_metrics_loading(true);
self.sqs_state.set_monitoring_scroll(0);
self.sqs_state.clear_metrics();
}
} else if self.current_service == Service::Ec2Instances
&& self.ec2_state.current_instance.is_some()
{
crate::ec2::actions::prev_detail_tab(self);
} else if self.current_service == Service::LambdaApplications
&& self.lambda_application_state.current_application.is_some()
{
crate::lambda::applications::prev_detail_tab(self);
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
{
crate::iam::actions::prev_detail_tab_roles(self);
} else if self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
{
crate::iam::actions::prev_detail_tab_users(self);
} else if self.current_service == Service::IamUserGroups
&& self.iam_state.current_group.is_some()
{
crate::iam::actions::prev_detail_tab_groups(self);
} else if self.view_mode == ViewMode::Detail {
self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
} else if self.current_service == Service::S3Buckets {
crate::s3::actions::prev_detail_tab(self);
} else if self.current_service == Service::CloudWatchAlarms {
self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
AlarmTab::AllAlarms => AlarmTab::InAlarm,
AlarmTab::InAlarm => AlarmTab::AllAlarms,
};
} else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
crate::ecr::actions::prev_detail_tab(self);
} else if self.current_service == Service::LambdaFunctions
&& self.lambda_state.current_function.is_some()
{
crate::lambda::functions::prev_detail_tab(self);
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
{
crate::cfn::actions::prev_detail_tab(self);
}
}
Action::StartFilter => {
if !self.service_selected && self.tabs.is_empty() {
return;
}
if self.current_service == Service::CloudWatchInsights {
self.mode = Mode::InsightsInput;
} else if self.current_service == Service::CloudWatchAlarms {
self.mode = Mode::FilterInput;
} else if self.current_service == Service::CloudTrailEvents {
self.mode = Mode::FilterInput;
self.cloudtrail_state.input_focus = InputFocus::Filter;
} else if self.current_service == Service::S3Buckets {
self.mode = Mode::FilterInput;
crate::s3::actions::start_filter(self);
} else if self.current_service == Service::ApiGatewayApis
|| self.current_service == Service::EcrRepositories
|| self.current_service == Service::IamUsers
|| self.current_service == Service::IamUserGroups
{
self.mode = Mode::FilterInput;
if self.current_service == Service::ApiGatewayApis {
self.apig_state.input_focus = InputFocus::Filter;
} else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
self.ecr_state.input_focus = InputFocus::Filter;
}
} else if self.current_service == Service::LambdaFunctions {
self.mode = Mode::FilterInput;
crate::lambda::functions::start_filter(self);
} else if self.current_service == Service::LambdaApplications {
self.mode = Mode::FilterInput;
crate::lambda::applications::start_filter(self);
} else if self.current_service == Service::IamRoles {
self.mode = Mode::FilterInput;
} else if self.current_service == Service::CloudFormationStacks {
self.mode = Mode::FilterInput;
crate::cfn::actions::start_filter(self);
} else if self.current_service == Service::SqsQueues {
self.mode = Mode::FilterInput;
self.sqs_state.input_focus = InputFocus::Filter;
} else if self.view_mode == ViewMode::List
|| (self.view_mode == ViewMode::Detail
&& (self.log_groups_state.detail_tab == DetailTab::LogStreams
|| self.log_groups_state.detail_tab == DetailTab::Tags))
{
self.mode = Mode::FilterInput;
self.log_groups_state.filter_mode = true;
self.log_groups_state.input_focus = InputFocus::Filter;
} else if self.view_mode == ViewMode::Events {
self.mode = Mode::EventFilterInput;
}
}
Action::StartEventFilter => {
if self.current_service == Service::CloudWatchInsights {
self.mode = Mode::InsightsInput;
} else if self.view_mode == ViewMode::List {
self.mode = Mode::FilterInput;
self.log_groups_state.filter_mode = true;
self.log_groups_state.input_focus = InputFocus::Filter;
} else if self.view_mode == ViewMode::Events {
self.mode = Mode::EventFilterInput;
} else if self.view_mode == ViewMode::Detail
&& self.log_groups_state.detail_tab == DetailTab::LogStreams
{
self.mode = Mode::FilterInput;
self.log_groups_state.filter_mode = true;
self.log_groups_state.input_focus = InputFocus::Filter;
}
}
Action::NextFilterFocus => {
if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::S3Buckets
{
crate::s3::actions::next_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::Ec2Instances
{
crate::ec2::actions::next_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::LambdaApplications
{
crate::lambda::applications::next_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
{
use crate::ui::iam::POLICY_FILTER_CONTROLS;
self.iam_state.policy_input_focus = self
.iam_state
.policy_input_focus
.next(&POLICY_FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_none()
{
use crate::ui::iam::ROLE_FILTER_CONTROLS;
self.iam_state.role_input_focus =
self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
{
use crate::ui::iam::{
POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
USER_SIMPLE_FILTER_CONTROLS,
};
if self.iam_state.user_tab == UserTab::Permissions {
self.iam_state.policy_input_focus = self
.iam_state
.policy_input_focus
.next(&POLICY_FILTER_CONTROLS);
} else if self.iam_state.user_tab == UserTab::LastAccessed {
self.iam_state.last_accessed_input_focus = self
.iam_state
.last_accessed_input_focus
.next(&USER_LAST_ACCESSED_FILTER_CONTROLS);
} else {
self.iam_state.user_input_focus = self
.iam_state
.user_input_focus
.next(&USER_SIMPLE_FILTER_CONTROLS);
}
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamUserGroups
{
use crate::ui::iam::GROUP_FILTER_CONTROLS;
self.iam_state.group_input_focus = self
.iam_state
.group_input_focus
.next(&GROUP_FILTER_CONTROLS);
} else if self.mode == Mode::InsightsInput {
use crate::app::InsightsFocus;
self.insights_state.insights.insights_focus =
match self.insights_state.insights.insights_focus {
InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
InsightsFocus::LogGroupSearch => InsightsFocus::Query,
InsightsFocus::Query => InsightsFocus::QueryLanguage,
};
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudFormationStacks
{
crate::cfn::actions::next_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::SqsQueues
{
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
self.sqs_state.input_focus = self
.sqs_state
.input_focus
.next(SUBSCRIPTION_FILTER_CONTROLS);
} else {
use crate::ui::sqs::FILTER_CONTROLS;
self.sqs_state.input_focus =
self.sqs_state.input_focus.next(FILTER_CONTROLS);
}
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchLogGroups
{
use crate::ui::cw::logs::FILTER_CONTROLS;
self.log_groups_state.input_focus =
self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
} else if self.mode == Mode::EventFilterInput {
self.log_groups_state.event_input_focus =
self.log_groups_state.event_input_focus.next();
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchAlarms
{
use crate::ui::cw::alarms::FILTER_CONTROLS;
self.alarms_state.input_focus =
self.alarms_state.input_focus.next(&FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudTrailEvents
{
const FILTER_CONTROLS: [InputFocus; 2] =
[InputFocus::Filter, InputFocus::Pagination];
self.cloudtrail_state.input_focus =
self.cloudtrail_state.input_focus.next(&FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::ApiGatewayApis
{
use crate::ui::apig::FILTER_CONTROLS;
self.apig_state.input_focus =
self.apig_state.input_focus.next(&FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
crate::ecr::actions::next_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::LambdaFunctions
{
crate::lambda::functions::next_filter_focus(self);
}
}
Action::PrevFilterFocus => {
if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::ApiGatewayApis
{
use crate::ui::apig::FILTER_CONTROLS;
self.apig_state.input_focus =
self.apig_state.input_focus.prev(&FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::S3Buckets
{
crate::s3::actions::prev_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::Ec2Instances
{
crate::ec2::actions::prev_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::LambdaApplications
{
crate::lambda::applications::prev_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudFormationStacks
{
crate::cfn::actions::prev_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::SqsQueues
{
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
self.sqs_state.input_focus = self
.sqs_state
.input_focus
.prev(SUBSCRIPTION_FILTER_CONTROLS);
} else {
use crate::ui::sqs::FILTER_CONTROLS;
self.sqs_state.input_focus =
self.sqs_state.input_focus.prev(FILTER_CONTROLS);
}
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_none()
{
use crate::ui::iam::ROLE_FILTER_CONTROLS;
self.iam_state.role_input_focus =
self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
{
use crate::ui::iam::{
POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
USER_SIMPLE_FILTER_CONTROLS,
};
if self.iam_state.user_tab == UserTab::Permissions {
self.iam_state.policy_input_focus = self
.iam_state
.policy_input_focus
.prev(&POLICY_FILTER_CONTROLS);
} else if self.iam_state.user_tab == UserTab::LastAccessed {
self.iam_state.last_accessed_input_focus = self
.iam_state
.last_accessed_input_focus
.prev(&USER_LAST_ACCESSED_FILTER_CONTROLS);
} else {
self.iam_state.user_input_focus = self
.iam_state
.user_input_focus
.prev(&USER_SIMPLE_FILTER_CONTROLS);
}
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamUserGroups
{
use crate::ui::iam::GROUP_FILTER_CONTROLS;
self.iam_state.group_input_focus = self
.iam_state
.group_input_focus
.prev(&GROUP_FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchLogGroups
{
use crate::ui::cw::logs::FILTER_CONTROLS;
self.log_groups_state.input_focus =
self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
} else if self.mode == Mode::EventFilterInput {
self.log_groups_state.event_input_focus =
self.log_groups_state.event_input_focus.prev();
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
{
use crate::ui::iam::POLICY_FILTER_CONTROLS;
self.iam_state.policy_input_focus = self
.iam_state
.policy_input_focus
.prev(&POLICY_FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchAlarms
{
use crate::ui::cw::alarms::FILTER_CONTROLS;
self.alarms_state.input_focus =
self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudTrailEvents
{
const FILTER_CONTROLS: [InputFocus; 2] =
[InputFocus::Filter, InputFocus::Pagination];
self.cloudtrail_state.input_focus =
self.cloudtrail_state.input_focus.prev(&FILTER_CONTROLS);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
crate::ecr::actions::prev_filter_focus(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::LambdaFunctions
{
crate::lambda::functions::prev_filter_focus(self);
}
}
Action::ToggleFilterCheckbox => {
if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
crate::ec2::actions::toggle_filter_checkbox(self);
} else if self.mode == Mode::InsightsInput {
use crate::app::InsightsFocus;
if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
&& self.insights_state.insights.show_dropdown
&& !self.insights_state.insights.log_group_matches.is_empty()
{
let selected_idx = self.insights_state.insights.dropdown_selected;
if let Some(group_name) = self
.insights_state
.insights
.log_group_matches
.get(selected_idx)
{
let group_name = group_name.clone();
if let Some(pos) = self
.insights_state
.insights
.selected_log_groups
.iter()
.position(|g| g == &group_name)
{
self.insights_state.insights.selected_log_groups.remove(pos);
} else if self.insights_state.insights.selected_log_groups.len() < 50 {
self.insights_state
.insights
.selected_log_groups
.push(group_name);
}
}
}
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudFormationStacks
{
crate::cfn::actions::toggle_filter_checkbox(self);
} else if self.mode == Mode::FilterInput
&& self.log_groups_state.detail_tab == DetailTab::LogStreams
{
match self.log_groups_state.input_focus {
InputFocus::Checkbox("ExactMatch") => {
self.log_groups_state.exact_match = !self.log_groups_state.exact_match
}
InputFocus::Checkbox("ShowExpired") => {
self.log_groups_state.show_expired = !self.log_groups_state.show_expired
}
_ => {}
}
} else if self.mode == Mode::EventFilterInput
&& self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
{
self.log_groups_state.relative_unit =
self.log_groups_state.relative_unit.next();
}
}
Action::CycleSortColumn => {
if self.view_mode == ViewMode::Detail
&& self.log_groups_state.detail_tab == DetailTab::LogStreams
{
self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
StreamSort::Name => StreamSort::CreationTime,
StreamSort::CreationTime => StreamSort::LastEventTime,
StreamSort::LastEventTime => StreamSort::Name,
};
}
}
Action::ToggleSortDirection => {
if self.view_mode == ViewMode::Detail
&& self.log_groups_state.detail_tab == DetailTab::LogStreams
{
self.log_groups_state.stream_sort_desc =
!self.log_groups_state.stream_sort_desc;
}
}
Action::ScrollUp => {
if self.mode == Mode::ErrorModal {
self.error_scroll = self.error_scroll.saturating_sub(1);
} else if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
self.cloudtrail_state.event_json_scroll =
self.cloudtrail_state.event_json_scroll.saturating_sub(10);
} else if self.current_service == Service::LambdaFunctions
&& self.lambda_state.current_function.is_some()
&& self.lambda_state.detail_tab == LambdaDetailTab::Monitor
&& !self.lambda_state.is_metrics_loading()
{
self.lambda_state.set_monitoring_scroll(
self.lambda_state.monitoring_scroll().saturating_sub(1),
);
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::scroll_up(self);
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
&& !self.sqs_state.is_metrics_loading()
{
self.sqs_state.set_monitoring_scroll(
self.sqs_state.monitoring_scroll().saturating_sub(1),
);
} else if self.view_mode == ViewMode::PolicyView {
self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::TrustRelationships
{
self.iam_state.trust_policy_scroll =
self.iam_state.trust_policy_scroll.saturating_sub(10);
} else if self.view_mode == ViewMode::Events {
if self.log_groups_state.event_scroll_offset == 0
&& self.log_groups_state.has_older_events
{
self.log_groups_state.loading = true;
} else {
self.log_groups_state.event_scroll_offset =
self.log_groups_state.event_scroll_offset.saturating_sub(1);
}
} else if self.view_mode == ViewMode::InsightsResults {
self.insights_state.insights.results_selected = self
.insights_state
.insights
.results_selected
.saturating_sub(1);
} else if self.view_mode == ViewMode::Detail {
self.log_groups_state.selected_stream =
self.log_groups_state.selected_stream.saturating_sub(1);
self.log_groups_state.expanded_stream = None;
} else if self.view_mode == ViewMode::List
&& self.current_service == Service::CloudWatchLogGroups
{
self.log_groups_state.log_groups.selected =
self.log_groups_state.log_groups.selected.saturating_sub(1);
self.log_groups_state.log_groups.snap_to_page();
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::scroll_up(self);
}
}
Action::ScrollDown => {
if self.mode == Mode::ErrorModal {
if let Some(error_msg) = &self.error_message {
let lines = error_msg.lines().count();
let max_scroll = lines.saturating_sub(1);
self.error_scroll = (self.error_scroll + 1).min(max_scroll);
}
} else if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
if let Some(event) = &self.cloudtrail_state.current_event {
let lines = event.cloud_trail_event_json.lines().count();
let max_scroll = lines.saturating_sub(1);
self.cloudtrail_state.event_json_scroll =
(self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
}
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
{
self.sqs_state
.set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
} else if self.view_mode == ViewMode::PolicyView {
let lines = self.iam_state.policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.iam_state.policy_scroll =
(self.iam_state.policy_scroll + 10).min(max_scroll);
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::TrustRelationships
{
let lines = self.iam_state.trust_policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.iam_state.trust_policy_scroll =
(self.iam_state.trust_policy_scroll + 10).min(max_scroll);
} else if self.view_mode == ViewMode::Events {
let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
if self.log_groups_state.event_scroll_offset >= max_scroll {
} else {
self.log_groups_state.event_scroll_offset =
(self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
}
} else if self.view_mode == ViewMode::InsightsResults {
let max = self
.insights_state
.insights
.query_results
.len()
.saturating_sub(1);
self.insights_state.insights.results_selected =
(self.insights_state.insights.results_selected + 1).min(max);
} else if self.view_mode == ViewMode::Detail {
let filtered_streams = filtered_log_streams(self);
let max = filtered_streams.len().saturating_sub(1);
self.log_groups_state.selected_stream =
(self.log_groups_state.selected_stream + 1).min(max);
} else if self.view_mode == ViewMode::List
&& self.current_service == Service::CloudWatchLogGroups
{
let filtered_groups = filtered_log_groups(self);
self.log_groups_state
.log_groups
.next_item(filtered_groups.len());
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::scroll_down(self);
}
}
Action::MeasureLatency => {
if self.mode == Mode::RegionPicker {
self.measure_region_latencies();
}
}
Action::Refresh => {
if self.mode == Mode::ProfilePicker {
self.log_groups_state.loading = true;
self.log_groups_state.loading_message = "Refreshing...".to_string();
} else if self.mode == Mode::RegionPicker {
} else if self.mode == Mode::SessionPicker {
self.sessions = Session::list_all().unwrap_or_default();
} else if self.current_service == Service::CloudWatchAlarms
&& self.alarms_state.current_alarm.is_some()
{
self.alarms_state.metrics_loading = true;
} else if self.current_service == Service::CloudWatchInsights
&& !self.insights_state.insights.selected_log_groups.is_empty()
{
self.log_groups_state.loading = true;
self.insights_state.insights.query_completed = true;
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::refresh(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::refresh(self);
} else if matches!(
self.view_mode,
ViewMode::Events | ViewMode::Detail | ViewMode::List
) {
self.log_groups_state.loading = true;
}
}
Action::Yank => {
if self.mode == Mode::ErrorModal {
if let Some(error) = &self.error_message {
copy_to_clipboard(error);
}
} else if self.view_mode == ViewMode::Events {
if let Some(event) = self
.log_groups_state
.log_events
.get(self.log_groups_state.event_scroll_offset)
{
copy_to_clipboard(&event.message);
}
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::yank(self);
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::yank(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::yank(self);
} else if self.current_service == Service::IamUsers {
crate::iam::actions::yank_users(self);
} else if self.current_service == Service::IamRoles {
crate::iam::actions::yank_roles(self);
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_some() {
if let Some(group_name) = &self.iam_state.current_group {
let arn = iam::format_arn(&self.config.account_id, "group", group_name);
copy_to_clipboard(&arn);
}
} else {
let filtered_groups: Vec<_> = self
.iam_state
.groups
.items
.iter()
.filter(|g| {
if self.iam_state.groups.filter.is_empty() {
true
} else {
g.group_name
.to_lowercase()
.contains(&self.iam_state.groups.filter.to_lowercase())
}
})
.collect();
if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
let arn = iam::format_arn(
&self.config.account_id,
"group",
&group.group_name,
);
copy_to_clipboard(&arn);
}
}
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some() {
if let Some(queue) = self
.sqs_state
.queues
.items
.iter()
.find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
{
let arn = format!(
"arn:aws:sqs:{}:{}:{}",
extract_region(&queue.url),
extract_account_id(&queue.url),
queue.name
);
copy_to_clipboard(&arn);
}
} else {
let filtered_queues = filtered_queues(
&self.sqs_state.queues.items,
&self.sqs_state.queues.filter,
);
if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
let arn = format!(
"arn:aws:sqs:{}:{}:{}",
extract_region(&queue.url),
extract_account_id(&queue.url),
queue.name
);
copy_to_clipboard(&arn);
}
}
} else if self.current_service == Service::ApiGatewayApis {
if let Some(_api) = &self.apig_state.current_api {
use crate::ui::apig::ApiDetailTab;
if self.apig_state.detail_tab == ApiDetailTab::Routes {
let (filtered_routes, _) = crate::ui::apig::filter_tree_items(
&self.apig_state.routes.items,
&self.apig_state.route_children,
&self.apig_state.route_filter,
);
let filtered_refs: Vec<&Route> = filtered_routes.iter().collect();
if let Some(route) = self.apig_state.routes.get_selected(&filtered_refs)
{
if !route.arn.is_empty() {
copy_to_clipboard(&route.arn);
}
}
}
}
}
}
Action::CopyToClipboard => {
self.snapshot_requested = true;
}
Action::RetryLoad => {
self.error_message = None;
self.mode = Mode::Normal;
self.log_groups_state.loading = true;
}
Action::ApplyFilter => {
if self.mode == Mode::FilterInput
&& self.current_service == Service::SqsQueues
&& self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
{
let regions = AwsRegion::all();
if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
self.sqs_state.subscription_region_filter = region.code.to_string();
}
self.mode = Mode::Normal;
} else if self.mode == Mode::InsightsInput {
use crate::app::InsightsFocus;
if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
&& self.insights_state.insights.show_dropdown
{
self.insights_state.insights.show_dropdown = false;
self.mode = Mode::Normal;
if !self.insights_state.insights.selected_log_groups.is_empty() {
self.log_groups_state.loading = true;
self.insights_state.insights.query_completed = true;
}
}
} else if self.mode == Mode::Normal && !self.page_input.is_empty() {
if let Ok(page) = self.page_input.parse::<usize>() {
self.go_to_page(page);
}
self.page_input.clear();
} else {
self.mode = Mode::Normal;
self.log_groups_state.filter_mode = false;
}
}
Action::ToggleExactMatch => {
if self.view_mode == ViewMode::Detail
&& self.log_groups_state.detail_tab == DetailTab::LogStreams
{
self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
}
}
Action::ToggleShowExpired => {
if self.view_mode == ViewMode::Detail
&& self.log_groups_state.detail_tab == DetailTab::LogStreams
{
self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
}
}
Action::GoBack => {
if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
self.mode = Mode::Normal;
self.service_picker.filter.clear();
}
else if self.current_service == Service::ApiGatewayApis
&& self.apig_state.current_api.is_some()
{
self.apig_state.current_api = None;
self.apig_state.routes.items.clear();
self.apig_state.detail_tab = crate::ui::apig::ApiDetailTab::Routes;
self.update_current_tab_breadcrumb();
}
else if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_some()
{
crate::s3::actions::go_back(self);
}
else if self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_some()
{
crate::ecr::actions::go_back(self);
}
else if self.current_service == Service::Ec2Instances
&& self.ec2_state.current_instance.is_some()
{
crate::ec2::actions::go_back(self);
}
else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
{
self.sqs_state.current_queue = None;
}
else if self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
{
crate::iam::actions::go_back_users(self);
} else if self.current_service == Service::IamUserGroups
&& self.iam_state.current_group.is_some()
{
crate::iam::actions::go_back_groups(self);
} else if self.current_service == Service::IamRoles {
crate::iam::actions::go_back_roles(self);
}
else if self.current_service == Service::LambdaFunctions
&& (self.lambda_state.current_version.is_some()
|| self.lambda_state.current_alias.is_some()
|| self.lambda_state.current_function.is_some())
{
crate::lambda::functions::go_back(self);
}
else if self.current_service == Service::CloudWatchAlarms
&& self.alarms_state.current_alarm.is_some()
{
self.alarms_state.current_alarm = None;
self.alarms_state.metric_data.clear();
self.view_mode = ViewMode::List;
self.update_current_tab_breadcrumb();
}
else if self.current_service == Service::LambdaApplications
&& self.lambda_application_state.current_application.is_some()
{
crate::lambda::applications::go_back(self);
}
else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
{
crate::cfn::actions::go_back(self);
}
else if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
self.cloudtrail_state.current_event = None;
self.update_current_tab_breadcrumb();
}
else if self.view_mode == ViewMode::InsightsResults {
if self.insights_state.insights.expanded_result.is_some() {
self.insights_state.insights.expanded_result = None;
}
}
else if self.current_service == Service::CloudWatchAlarms {
if self.alarms_state.table.has_expanded_item() {
self.alarms_state.table.collapse();
}
}
else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::collapse_row(self);
}
else if self.view_mode == ViewMode::Events {
if self.log_groups_state.expanded_event.is_some() {
self.log_groups_state.expanded_event = None;
} else {
self.view_mode = ViewMode::Detail;
self.log_groups_state.event_filter.clear();
}
}
else if self.view_mode == ViewMode::Detail {
self.view_mode = ViewMode::List;
self.log_groups_state.stream_filter.clear();
self.log_groups_state.exact_match = false;
self.log_groups_state.show_expired = false;
}
}
Action::OpenInConsole | Action::OpenInBrowser => {
let url = self.get_console_url();
let _ = webbrowser::open(&url);
}
Action::ShowHelp => {
self.mode = Mode::HelpModal;
}
Action::OpenRegionPicker => {
self.region_filter.clear();
self.region_picker_selected = 0;
self.mode = Mode::RegionPicker;
}
Action::OpenProfilePicker => {
self.profile_filter.clear();
self.profile_picker_selected = 0;
self.available_profiles = Self::load_aws_profiles();
self.mode = Mode::ProfilePicker;
}
Action::OpenCalendar => {
self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
self.calendar_selecting = CalendarField::StartDate;
self.mode = Mode::CalendarPicker;
}
Action::CloseCalendar => {
self.mode = Mode::Normal;
self.calendar_date = None;
}
Action::CalendarPrevDay => {
if let Some(date) = self.calendar_date {
self.calendar_date = date.checked_sub(time::Duration::days(1));
}
}
Action::CalendarNextDay => {
if let Some(date) = self.calendar_date {
self.calendar_date = date.checked_add(time::Duration::days(1));
}
}
Action::CalendarPrevWeek => {
if let Some(date) = self.calendar_date {
self.calendar_date = date.checked_sub(time::Duration::weeks(1));
}
}
Action::CalendarNextWeek => {
if let Some(date) = self.calendar_date {
self.calendar_date = date.checked_add(time::Duration::weeks(1));
}
}
Action::CalendarPrevMonth => {
if let Some(date) = self.calendar_date {
self.calendar_date = Some(if date.month() == time::Month::January {
date.replace_month(time::Month::December)
.unwrap()
.replace_year(date.year() - 1)
.unwrap()
} else {
date.replace_month(date.month().previous()).unwrap()
});
}
}
Action::CalendarNextMonth => {
if let Some(date) = self.calendar_date {
self.calendar_date = Some(if date.month() == time::Month::December {
date.replace_month(time::Month::January)
.unwrap()
.replace_year(date.year() + 1)
.unwrap()
} else {
date.replace_month(date.month().next()).unwrap()
});
}
}
Action::CalendarSelect => {
if let Some(date) = self.calendar_date {
let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
.unix_timestamp()
* 1000;
match self.calendar_selecting {
CalendarField::StartDate => {
self.log_groups_state.start_time = Some(timestamp);
self.calendar_selecting = CalendarField::EndDate;
}
CalendarField::EndDate => {
self.log_groups_state.end_time = Some(timestamp);
self.mode = Mode::Normal;
self.calendar_date = None;
}
}
}
}
}
}
pub fn filtered_services(&self) -> Vec<&'static str> {
let mut services = if self.service_picker.filter.is_empty() {
self.service_picker.services.clone()
} else {
self.service_picker
.services
.iter()
.filter(|s| {
s.to_lowercase()
.contains(&self.service_picker.filter.to_lowercase())
})
.copied()
.collect()
};
services.sort();
services
}
pub fn breadcrumbs(&self) -> String {
if !self.service_selected {
return String::new();
}
let mut parts = vec![];
match self.current_service {
Service::CloudWatchLogGroups => {
parts.push("CloudWatch".to_string());
parts.push("Log groups".to_string());
if self.view_mode != ViewMode::List {
if let Some(group) = selected_log_group(self) {
parts.push(group.name.clone());
}
}
if self.view_mode == ViewMode::Events {
if let Some(stream) = self
.log_groups_state
.log_streams
.get(self.log_groups_state.selected_stream)
{
parts.push(stream.name.clone());
}
}
}
Service::CloudWatchInsights => {
parts.push("CloudWatch".to_string());
parts.push("Insights".to_string());
}
Service::CloudWatchAlarms => {
parts.push("CloudWatch".to_string());
parts.push("Alarms".to_string());
}
Service::CloudTrailEvents => {
parts.push("CloudTrail".to_string());
parts.push("Event History".to_string());
}
Service::S3Buckets => {
parts.extend(crate::s3::actions::breadcrumb(self));
}
Service::SqsQueues => {
parts.push("SQS".to_string());
parts.push("Queues".to_string());
}
Service::EcrRepositories => {
parts.extend(crate::ecr::actions::breadcrumb(self));
}
Service::LambdaFunctions => {
parts.extend(crate::lambda::functions::breadcrumb(self));
}
Service::LambdaApplications => {
parts.extend(crate::lambda::applications::breadcrumb());
}
Service::CloudFormationStacks => {
parts.extend(crate::cfn::actions::breadcrumb(self));
}
Service::IamUsers => {
parts.extend(crate::iam::actions::breadcrumb_users());
}
Service::IamRoles => {
parts.extend(crate::iam::actions::breadcrumb_roles(self));
}
Service::IamUserGroups => {
parts.extend(crate::iam::actions::breadcrumb_groups(self));
}
Service::Ec2Instances => {
parts.extend(crate::ec2::actions::breadcrumb(self));
}
Service::ApiGatewayApis => {
parts.push("API Gateway".to_string());
if let Some(api) = &self.apig_state.current_api {
parts.push(api.name.clone());
} else {
parts.push("APIs".to_string());
}
}
}
parts.join(" > ")
}
pub fn update_current_tab_breadcrumb(&mut self) {
if !self.tabs.is_empty() {
self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
}
}
pub fn get_console_url(&self) -> String {
use crate::cw;
match self.current_service {
Service::CloudWatchLogGroups => {
if self.view_mode == ViewMode::Events {
if let Some(group) = selected_log_group(self) {
if let Some(stream) = self
.log_groups_state
.log_streams
.get(self.log_groups_state.selected_stream)
{
return cw::logs::console_url_stream(
&self.config.region,
&group.name,
&stream.name,
);
}
}
} else if self.view_mode == ViewMode::Detail {
if let Some(group) = selected_log_group(self) {
return cw::logs::console_url_detail(&self.config.region, &group.name);
}
}
cw::logs::console_url_list(&self.config.region)
}
Service::CloudWatchInsights => cw::insights::console_url(
&self.config.region,
&self.config.account_id,
&self.insights_state.insights.query_text,
&self.insights_state.insights.selected_log_groups,
),
Service::CloudWatchAlarms => {
let view_type = match self.alarms_state.view_as {
AlarmViewMode::Table | AlarmViewMode::Detail => "table",
AlarmViewMode::Cards => "card",
};
cw::alarms::console_url(
&self.config.region,
view_type,
self.alarms_state.table.page_size.value(),
&self.alarms_state.sort_column,
self.alarms_state.sort_direction.as_str(),
)
}
Service::CloudTrailEvents => {
if let Some(event) = &self.cloudtrail_state.current_event {
format!(
"https://{}.console.aws.amazon.com/cloudtrailv2/home?region={}#/events/{}",
self.config.region, self.config.region, event.event_id
)
} else {
format!(
"https://{}.console.aws.amazon.com/cloudtrail/home?region={}#/events",
self.config.region, self.config.region
)
}
}
Service::S3Buckets => crate::s3::actions::console_url(self),
Service::SqsQueues => {
if let Some(queue_url) = &self.sqs_state.current_queue {
console_url_queue_detail(&self.config.region, queue_url)
} else {
console_url_queues(&self.config.region)
}
}
Service::EcrRepositories => crate::ecr::actions::console_url(self),
Service::LambdaFunctions => crate::lambda::functions::console_url(self),
Service::LambdaApplications => crate::lambda::applications::console_url(self),
Service::CloudFormationStacks => crate::cfn::actions::console_url(self),
Service::IamUsers => crate::iam::actions::console_url_users(self),
Service::IamRoles => crate::iam::actions::console_url_roles(self),
Service::IamUserGroups => crate::iam::actions::console_url_groups(self),
Service::Ec2Instances => crate::ec2::actions::console_url(self),
Service::ApiGatewayApis => {
use crate::apig;
if let Some(api) = &self.apig_state.current_api {
if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
let protocol = api.protocol_type.to_uppercase();
if protocol == "REST" {
let (filtered_items, _) = crate::ui::apig::filter_tree_items(
&self.apig_state.resources.items,
&self.apig_state.resource_children,
&self.apig_state.route_filter,
);
let resource_id =
if self.apig_state.resources.selected < filtered_items.len() {
let mut current_row = 0;
self.find_resource_at_row(
&filtered_items,
self.apig_state.resources.selected,
&mut current_row,
)
} else {
None
};
apig::console_url_resources(
&self.config.region,
&api.id,
resource_id.as_deref(),
)
} else {
let (filtered_items, filtered_children) =
crate::ui::apig::filter_tree_items(
&self.apig_state.routes.items,
&self.apig_state.route_children,
&self.apig_state.route_filter,
);
let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
&filtered_items,
&self.apig_state.expanded_routes,
&filtered_children,
);
let route_id = if self.apig_state.routes.selected < total_rows {
let mut current_row = 0;
self.find_route_id_at_row_with_children(
&filtered_items,
&filtered_children,
self.apig_state.routes.selected,
&mut current_row,
)
} else {
None
};
apig::console_url_routes(
&self.config.region,
&api.id,
route_id.as_deref(),
)
}
} else {
apig::console_url_api(&self.config.region, &api.id)
}
} else {
apig::console_url_apis(&self.config.region)
}
}
}
}
pub fn calculate_total_bucket_rows(&self) -> usize {
calculate_total_bucket_rows(self)
}
fn calculate_total_object_rows(&self) -> usize {
calculate_total_object_rows(self)
}
fn get_column_selector_max(&self) -> usize {
if self.current_service == Service::ApiGatewayApis {
self.apig_api_column_ids.len() + 6
} else if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_none()
{
crate::s3::actions::column_selector_max(self)
} else if self.view_mode == ViewMode::Events {
self.cw_log_event_column_ids.len() - 1
} else if self.view_mode == ViewMode::Detail {
self.cw_log_stream_column_ids.len() + 6
} else if self.current_service == Service::CloudWatchAlarms {
29
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
{
self.cloudtrail_resource_column_ids.len()
} else {
self.cloudtrail_event_column_ids.len() + 6
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::column_selector_max(self)
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::column_selector_max(self)
} else if self.current_service == Service::SqsQueues {
self.sqs_column_ids.len() - 1
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::column_selector_max(self)
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::column_selector_max(self)
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::column_selector_max(self)
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
self.iam_policy_column_ids.len() + 5
} else {
self.iam_user_column_ids.len() + 5
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
self.iam_policy_column_ids.len() + 5
} else {
self.iam_role_column_ids.len() + 5
}
} else {
self.cw_log_group_column_ids.len() + 6
}
}
fn get_column_count(&self) -> usize {
if self.current_service == Service::ApiGatewayApis {
self.apig_api_column_ids.len()
} else if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_none()
{
crate::s3::actions::column_count(self)
} else if self.view_mode == ViewMode::Events {
self.cw_log_event_column_ids.len()
} else if self.view_mode == ViewMode::Detail {
self.cw_log_stream_column_ids.len()
} else if self.current_service == Service::CloudWatchAlarms {
14
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
{
self.cloudtrail_resource_column_ids.len()
} else {
self.cloudtrail_event_column_ids.len()
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::column_count(self)
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::column_count(self)
} else if self.current_service == Service::SqsQueues {
self.sqs_column_ids.len()
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::column_count(self)
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::column_count(self)
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::column_count(self)
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
self.iam_policy_column_ids.len()
} else {
self.iam_user_column_ids.len()
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
self.iam_policy_column_ids.len()
} else {
self.iam_role_column_ids.len()
}
} else {
self.cw_log_group_column_ids.len()
}
}
fn is_blank_row_index(&self, idx: usize) -> bool {
let column_count = self.get_column_count();
idx == column_count + 1
}
fn next_item(&mut self) {
match self.mode {
Mode::FilterInput => {
if self.current_service == Service::S3Buckets
&& crate::s3::actions::is_pagination_focused(self)
{
crate::s3::actions::page_down_filter_input(self);
} else if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.input_focus == InputFocus::Pagination
{
let page_size = self.cloudtrail_state.table.page_size.value();
let total_items = self.cloudtrail_state.table.items.len();
let current_page = self.cloudtrail_state.table.selected / page_size;
let total_pages = total_items.div_ceil(page_size);
if current_page + 1 < total_pages {
self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
}
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::next_item_filter_input(self);
} else if self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
{
use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
if self.iam_state.user_tab == UserTab::Permissions
&& self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
{
self.cycle_policy_type_next();
} else if self.iam_state.user_tab == UserTab::LastAccessed
&& self.iam_state.last_accessed_input_focus == HISTORY_FILTER
{
self.iam_state.last_accessed_history_filter =
self.iam_state.last_accessed_history_filter.next();
self.iam_state.last_accessed_services.reset();
}
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::Permissions
{
use crate::ui::iam::POLICY_TYPE_DROPDOWN;
if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
self.cycle_policy_type_next();
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::toggle_state_filter_next(self);
} else if self.current_service == Service::SqsQueues {
use crate::ui::sqs::SUBSCRIPTION_REGION;
if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
let regions = AwsRegion::all();
self.sqs_state.subscription_region_selected =
(self.sqs_state.subscription_region_selected + 1)
.min(regions.len() - 1);
self.sqs_state.subscriptions.reset();
}
}
}
Mode::RegionPicker => {
let filtered = self.get_filtered_regions();
if !filtered.is_empty() {
self.region_picker_selected =
(self.region_picker_selected + 1).min(filtered.len() - 1);
}
}
Mode::ProfilePicker => {
let filtered = self.get_filtered_profiles();
if !filtered.is_empty() {
self.profile_picker_selected =
(self.profile_picker_selected + 1).min(filtered.len() - 1);
}
}
Mode::SessionPicker => {
let filtered = self.get_filtered_sessions();
if !filtered.is_empty() {
self.session_picker_selected =
(self.session_picker_selected + 1).min(filtered.len() - 1);
}
}
Mode::InsightsInput => {
use crate::app::InsightsFocus;
if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
&& self.insights_state.insights.show_dropdown
&& !self.insights_state.insights.log_group_matches.is_empty()
{
let max = self.insights_state.insights.log_group_matches.len() - 1;
self.insights_state.insights.dropdown_selected =
(self.insights_state.insights.dropdown_selected + 1).min(max);
}
}
Mode::ColumnSelector => {
let max = self.get_column_selector_max();
let mut next_idx = (self.column_selector_index + 1).min(max);
if self.is_blank_row_index(next_idx) {
next_idx = (next_idx + 1).min(max);
}
self.column_selector_index = next_idx;
}
Mode::ServicePicker => {
let filtered = self.filtered_services();
if !filtered.is_empty() {
self.service_picker.selected =
(self.service_picker.selected + 1).min(filtered.len() - 1);
}
}
Mode::TabPicker => {
let filtered = self.get_filtered_tabs();
if !filtered.is_empty() {
self.tab_picker_selected =
(self.tab_picker_selected + 1).min(filtered.len() - 1);
}
}
Mode::Normal => {
if !self.service_selected {
let filtered = self.filtered_services();
if !filtered.is_empty() {
self.service_picker.selected =
(self.service_picker.selected + 1).min(filtered.len() - 1);
}
} else if self.current_service == Service::S3Buckets {
if self.s3_state.current_bucket.is_some() {
if self.s3_state.object_tab == S3ObjectTab::Properties {
self.s3_state.properties_scroll =
self.s3_state.properties_scroll.saturating_add(1);
} else {
let total_rows = self.calculate_total_object_rows();
let max = total_rows.saturating_sub(1);
self.s3_state.selected_object =
(self.s3_state.selected_object + 1).min(max);
let visible_rows = self.s3_state.object_visible_rows.get();
if self.s3_state.selected_object
>= self.s3_state.object_scroll_offset + visible_rows
{
self.s3_state.object_scroll_offset =
self.s3_state.selected_object - visible_rows + 1;
}
}
} else {
let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
if total_rows > 0 {
self.s3_state.selected_row =
(self.s3_state.selected_row + 1).min(total_rows - 1);
let visible_rows = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible_rows
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row - visible_rows + 1;
}
}
}
} else if self.view_mode == ViewMode::InsightsResults {
let max = self
.insights_state
.insights
.query_results
.len()
.saturating_sub(1);
if self.insights_state.insights.results_selected < max {
self.insights_state.insights.results_selected += 1;
}
} else if self.current_service == Service::ApiGatewayApis {
if let Some(api) = &self.apig_state.current_api {
if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
let protocol = api.protocol_type.to_uppercase();
if protocol == "REST" {
let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
&self.apig_state.resources.items,
&self.apig_state.expanded_resources,
&self.apig_state.resource_children,
);
if total_rows > 0 {
self.apig_state.resources.selected =
(self.apig_state.resources.selected + 1)
.min(total_rows - 1);
}
} else {
let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
&self.apig_state.routes.items,
&self.apig_state.expanded_routes,
&self.apig_state.route_children,
);
if total_rows > 0 {
self.apig_state.routes.selected =
(self.apig_state.routes.selected + 1).min(total_rows - 1);
}
}
}
} else {
let filtered = crate::ui::apig::filtered_apis(self);
if !filtered.is_empty() {
self.apig_state.apis.next_item(filtered.len());
}
}
} else if self.view_mode == ViewMode::PolicyView {
let lines = self.iam_state.policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.iam_state.policy_scroll =
(self.iam_state.policy_scroll + 1).min(max_scroll);
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
{
let lines = self.sqs_state.policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.sqs_state.policy_scroll =
(self.sqs_state.policy_scroll + 1).min(max_scroll);
} else if self.current_service == Service::LambdaFunctions
&& self.lambda_state.current_function.is_some()
&& self.lambda_state.detail_tab == LambdaDetailTab::Monitor
&& !self.lambda_state.is_metrics_loading()
{
self.lambda_state
.set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
&& !self.sqs_state.is_metrics_loading()
{
self.sqs_state
.set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
} else if self.view_mode == ViewMode::Events {
let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
if self.log_groups_state.event_scroll_offset >= max_scroll {
} else {
self.log_groups_state.event_scroll_offset =
(self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
}
} else if self.current_service == Service::CloudWatchLogGroups {
if self.view_mode == ViewMode::List {
let filtered_groups = filtered_log_groups(self);
self.log_groups_state
.log_groups
.next_item(filtered_groups.len());
} else if self.view_mode == ViewMode::Detail {
let filtered_streams = filtered_log_streams(self);
if !filtered_streams.is_empty() {
let max = filtered_streams.len() - 1;
if self.log_groups_state.selected_stream >= max {
} else {
self.log_groups_state.selected_stream =
(self.log_groups_state.selected_stream + 1).min(max);
self.log_groups_state.expanded_stream = None;
}
}
}
} else if self.current_service == Service::CloudWatchAlarms {
let filtered_alarms = match self.alarms_state.alarm_tab {
AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
AlarmTab::InAlarm => self
.alarms_state
.table
.items
.iter()
.filter(|a| a.state.to_uppercase() == "ALARM")
.count(),
};
if filtered_alarms > 0 {
self.alarms_state.table.next_item(filtered_alarms);
}
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
{
if let Some(event) = &self.cloudtrail_state.current_event {
let line_count = event.cloud_trail_event_json.lines().count();
let max_scroll = line_count.saturating_sub(1);
self.cloudtrail_state.event_json_scroll =
(self.cloudtrail_state.event_json_scroll + 1).min(max_scroll);
}
} else {
let page_size = self.cloudtrail_state.table.page_size.value();
let current_page = self.cloudtrail_state.table.selected / page_size;
let page_start = current_page * page_size;
let page_end = page_start + page_size;
let filtered_count = self.cloudtrail_state.table.items.len();
if self.cloudtrail_state.table.selected < filtered_count.saturating_sub(1) {
self.cloudtrail_state.table.selected =
(self.cloudtrail_state.table.selected + 1)
.min(page_end - 1)
.min(filtered_count - 1);
}
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::next_item(self);
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::next_item(self);
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
{
let filtered = filtered_lambda_triggers(self);
if !filtered.is_empty() {
self.sqs_state.triggers.next_item(filtered.len());
}
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
{
let filtered = filtered_eventbridge_pipes(self);
if !filtered.is_empty() {
self.sqs_state.pipes.next_item(filtered.len());
}
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
{
let filtered = filtered_tags(self);
if !filtered.is_empty() {
self.sqs_state.tags.next_item(filtered.len());
}
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
let filtered = filtered_subscriptions(self);
if !filtered.is_empty() {
self.sqs_state.subscriptions.next_item(filtered.len());
}
} else {
let filtered_queues = filtered_queues(
&self.sqs_state.queues.items,
&self.sqs_state.queues.filter,
);
if !filtered_queues.is_empty() {
self.sqs_state.queues.next_item(filtered_queues.len());
}
}
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::next_item(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::next_item(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::next_item(self);
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
if self.iam_state.user_tab == UserTab::Tags {
let filtered = filtered_user_tags(self);
if !filtered.is_empty() {
self.iam_state.user_tags.next_item(filtered.len());
}
} else {
let filtered = filtered_iam_policies(self);
if !filtered.is_empty() {
self.iam_state.policies.next_item(filtered.len());
}
}
} else {
let filtered = filtered_iam_users(self);
if !filtered.is_empty() {
self.iam_state.users.next_item(filtered.len());
}
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
if self.iam_state.role_tab == RoleTab::TrustRelationships {
let lines = self.iam_state.trust_policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.iam_state.trust_policy_scroll =
(self.iam_state.trust_policy_scroll + 1).min(max_scroll);
} else if self.iam_state.role_tab == RoleTab::RevokeSessions {
self.iam_state.revoke_sessions_scroll =
(self.iam_state.revoke_sessions_scroll + 1).min(19);
} else if self.iam_state.role_tab == RoleTab::Tags {
let filtered = filtered_iam_tags(self);
if !filtered.is_empty() {
self.iam_state.tags.next_item(filtered.len());
}
} else if self.iam_state.role_tab == RoleTab::LastAccessed {
let filtered = filtered_last_accessed(self);
if !filtered.is_empty() {
self.iam_state
.last_accessed_services
.next_item(filtered.len());
}
} else {
let filtered = filtered_iam_policies(self);
if !filtered.is_empty() {
self.iam_state.policies.next_item(filtered.len());
}
}
} else {
let filtered = filtered_iam_roles(self);
if !filtered.is_empty() {
self.iam_state.roles.next_item(filtered.len());
}
}
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_some() {
if self.iam_state.group_tab == GroupTab::Users {
let filtered: Vec<_> = self
.iam_state
.group_users
.items
.iter()
.filter(|u| {
if self.iam_state.group_users.filter.is_empty() {
true
} else {
u.user_name.to_lowercase().contains(
&self.iam_state.group_users.filter.to_lowercase(),
)
}
})
.collect();
if !filtered.is_empty() {
self.iam_state.group_users.next_item(filtered.len());
}
} else if self.iam_state.group_tab == GroupTab::Permissions {
let filtered = filtered_iam_policies(self);
if !filtered.is_empty() {
self.iam_state.policies.next_item(filtered.len());
}
} else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
let filtered = filtered_last_accessed(self);
if !filtered.is_empty() {
self.iam_state
.last_accessed_services
.next_item(filtered.len());
}
}
} else {
let filtered: Vec<_> = self
.iam_state
.groups
.items
.iter()
.filter(|g| {
if self.iam_state.groups.filter.is_empty() {
true
} else {
g.group_name
.to_lowercase()
.contains(&self.iam_state.groups.filter.to_lowercase())
}
})
.collect();
if !filtered.is_empty() {
self.iam_state.groups.next_item(filtered.len());
}
}
}
}
_ => {}
}
}
fn prev_item(&mut self) {
match self.mode {
Mode::FilterInput => {
if self.current_service == Service::S3Buckets
&& crate::s3::actions::is_pagination_focused(self)
{
crate::s3::actions::page_up_filter_input(self);
} else if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.input_focus == InputFocus::Pagination
{
let page_size = self.cloudtrail_state.table.page_size.value();
let current_page = self.cloudtrail_state.table.selected / page_size;
if current_page > 0 {
self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
}
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::prev_item_filter_input(self);
} else if self.current_service == Service::IamUsers
&& self.iam_state.current_user.is_some()
{
use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
if self.iam_state.user_tab == UserTab::Permissions
&& self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
{
self.cycle_policy_type_prev();
} else if self.iam_state.user_tab == UserTab::LastAccessed
&& self.iam_state.last_accessed_input_focus == HISTORY_FILTER
{
self.iam_state.last_accessed_history_filter =
self.iam_state.last_accessed_history_filter.prev();
self.iam_state.last_accessed_services.reset();
}
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::Permissions
{
use crate::ui::iam::POLICY_TYPE_DROPDOWN;
if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
self.cycle_policy_type_prev();
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::toggle_state_filter_prev(self);
} else if self.current_service == Service::SqsQueues {
use crate::ui::sqs::SUBSCRIPTION_REGION;
if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
self.sqs_state.subscription_region_selected = self
.sqs_state
.subscription_region_selected
.saturating_sub(1);
self.sqs_state.subscriptions.reset();
}
}
}
Mode::RegionPicker => {
self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
}
Mode::ProfilePicker => {
self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
}
Mode::SessionPicker => {
self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
}
Mode::InsightsInput => {
use crate::app::InsightsFocus;
if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
&& self.insights_state.insights.show_dropdown
&& !self.insights_state.insights.log_group_matches.is_empty()
{
self.insights_state.insights.dropdown_selected = self
.insights_state
.insights
.dropdown_selected
.saturating_sub(1);
}
}
Mode::ColumnSelector => {
let mut prev_idx = self.column_selector_index.saturating_sub(1);
if self.is_blank_row_index(prev_idx) {
prev_idx = prev_idx.saturating_sub(1);
}
self.column_selector_index = prev_idx;
}
Mode::ServicePicker => {
self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
}
Mode::TabPicker => {
self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
}
Mode::Normal => {
if !self.service_selected {
self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
} else if self.current_service == Service::S3Buckets {
if self.s3_state.current_bucket.is_some() {
if self.s3_state.object_tab == S3ObjectTab::Properties {
self.s3_state.properties_scroll =
self.s3_state.properties_scroll.saturating_sub(1);
} else {
self.s3_state.selected_object =
self.s3_state.selected_object.saturating_sub(1);
if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
self.s3_state.object_scroll_offset = self.s3_state.selected_object;
}
}
} else {
self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
}
}
} else if self.current_service == Service::ApiGatewayApis {
if let Some(api) = &self.apig_state.current_api {
if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
let protocol = api.protocol_type.to_uppercase();
if protocol == "REST" {
if self.apig_state.resources.selected > 0 {
self.apig_state.resources.selected -= 1;
}
} else {
if self.apig_state.routes.selected > 0 {
self.apig_state.routes.selected -= 1;
}
}
}
} else {
self.apig_state.apis.prev_item();
}
} else if self.view_mode == ViewMode::InsightsResults {
if self.insights_state.insights.results_selected > 0 {
self.insights_state.insights.results_selected -= 1;
}
} else if self.view_mode == ViewMode::PolicyView {
self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
{
} else if self.current_service == Service::LambdaFunctions
&& self.lambda_state.current_function.is_some()
&& self.lambda_state.detail_tab == LambdaDetailTab::Monitor
&& !self.lambda_state.is_metrics_loading()
{
crate::lambda::functions::scroll_up(self);
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
&& !self.sqs_state.is_metrics_loading()
{
self.sqs_state.set_monitoring_scroll(
self.sqs_state.monitoring_scroll().saturating_sub(1),
);
} else if self.view_mode == ViewMode::Events {
if self.log_groups_state.event_scroll_offset == 0 {
if self.log_groups_state.has_older_events {
self.log_groups_state.loading = true;
}
} else {
self.log_groups_state.event_scroll_offset =
self.log_groups_state.event_scroll_offset.saturating_sub(1);
}
} else if self.current_service == Service::CloudWatchLogGroups {
if self.view_mode == ViewMode::List {
self.log_groups_state.log_groups.prev_item();
} else if self.view_mode == ViewMode::Detail
&& self.log_groups_state.selected_stream > 0
{
self.log_groups_state.selected_stream =
self.log_groups_state.selected_stream.saturating_sub(1);
self.log_groups_state.expanded_stream = None;
}
} else if self.current_service == Service::CloudWatchAlarms {
self.alarms_state.table.prev_item();
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
{
self.cloudtrail_state.event_json_scroll =
self.cloudtrail_state.event_json_scroll.saturating_sub(1);
} else {
let page_size = self.cloudtrail_state.table.page_size.value();
let current_page = self.cloudtrail_state.table.selected / page_size;
let page_start = current_page * page_size;
if self.cloudtrail_state.table.selected > page_start {
self.cloudtrail_state.table.selected -= 1;
}
}
} else if self.current_service == Service::Ec2Instances {
if self.ec2_state.current_instance.is_some()
&& self.ec2_state.detail_tab == Ec2DetailTab::Tags
{
self.ec2_state.tags.prev_item();
} else {
self.ec2_state.table.prev_item();
}
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::prev_item(self);
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
{
self.sqs_state.triggers.prev_item();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
{
self.sqs_state.pipes.prev_item();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
{
self.sqs_state.tags.prev_item();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
self.sqs_state.subscriptions.prev_item();
} else {
self.sqs_state.queues.prev_item();
}
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::prev_item(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::prev_item(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::prev_item(self);
} else if self.current_service == Service::IamUsers {
self.iam_state.users.prev_item();
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
if self.iam_state.role_tab == RoleTab::TrustRelationships {
self.iam_state.trust_policy_scroll =
self.iam_state.trust_policy_scroll.saturating_sub(1);
} else if self.iam_state.role_tab == RoleTab::RevokeSessions {
self.iam_state.revoke_sessions_scroll =
self.iam_state.revoke_sessions_scroll.saturating_sub(1);
} else if self.iam_state.role_tab == RoleTab::Tags {
self.iam_state.tags.prev_item();
} else if self.iam_state.role_tab == RoleTab::LastAccessed {
self.iam_state.last_accessed_services.prev_item();
} else {
self.iam_state.policies.prev_item();
}
} else {
self.iam_state.roles.prev_item();
}
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_some() {
if self.iam_state.group_tab == GroupTab::Users {
self.iam_state.group_users.prev_item();
} else if self.iam_state.group_tab == GroupTab::Permissions {
self.iam_state.policies.prev_item();
} else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
self.iam_state.last_accessed_services.prev_item();
}
} else {
self.iam_state.groups.prev_item();
}
}
}
_ => {}
}
}
fn page_down(&mut self) {
if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
if let Some(event) = &self.cloudtrail_state.current_event {
let lines = event.cloud_trail_event_json.lines().count();
let max_scroll = lines.saturating_sub(1);
self.cloudtrail_state.event_json_scroll =
(self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
}
} else if self.mode == Mode::ColumnSelector {
let max = self.get_column_selector_max();
let mut next_idx = (self.column_selector_index + 10).min(max);
if self.is_blank_row_index(next_idx) {
next_idx = (next_idx + 1).min(max);
}
self.column_selector_index = next_idx;
} else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
crate::s3::actions::page_down_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudFormationStacks
{
crate::cfn::actions::page_down_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_none()
{
let page_size = self.iam_state.roles.page_size.value();
let filtered_count = filtered_iam_roles(self).len();
self.iam_state.role_input_focus.handle_page_down(
&mut self.iam_state.roles.selected,
&mut self.iam_state.roles.scroll_offset,
page_size,
filtered_count,
);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchAlarms
{
let page_size = self.alarms_state.table.page_size.value();
let filtered_count = self.alarms_state.table.items.len();
self.alarms_state.input_focus.handle_page_down(
&mut self.alarms_state.table.selected,
&mut self.alarms_state.table.scroll_offset,
page_size,
filtered_count,
);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudTrailEvents
{
let page_size = self.cloudtrail_state.table.page_size.value();
let filtered_count = self.cloudtrail_state.table.items.len();
self.cloudtrail_state.input_focus.handle_page_down(
&mut self.cloudtrail_state.table.selected,
&mut self.cloudtrail_state.table.scroll_offset,
page_size,
filtered_count,
);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchLogGroups
{
if self.view_mode == ViewMode::List {
let filtered = filtered_log_groups(self);
let page_size = self.log_groups_state.log_groups.page_size.value();
let filtered_count = filtered.len();
self.log_groups_state.input_focus.handle_page_down(
&mut self.log_groups_state.log_groups.selected,
&mut self.log_groups_state.log_groups.scroll_offset,
page_size,
filtered_count,
);
} else {
let filtered = filtered_log_streams(self);
let page_size = self.log_groups_state.stream_page_size;
let filtered_count = filtered.len();
self.log_groups_state.input_focus.handle_page_down(
&mut self.log_groups_state.selected_stream,
&mut self.log_groups_state.stream_current_page,
page_size,
filtered_count,
);
self.log_groups_state.expanded_stream = None;
}
} else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
{
crate::lambda::functions::page_down_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::LambdaApplications
{
crate::lambda::applications::page_down_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
crate::ecr::actions::page_down_filter_input(self);
} else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
let page_size = self.iam_state.policies.page_size.value();
let filtered_count = filtered_iam_policies(self).len();
self.iam_state.policy_input_focus.handle_page_down(
&mut self.iam_state.policies.selected,
&mut self.iam_state.policies.scroll_offset,
page_size,
filtered_count,
);
} else if self.view_mode == ViewMode::PolicyView {
let lines = self.iam_state.policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Template
{
crate::cfn::actions::scroll_down_template_fast(self);
} else if self.current_service == Service::LambdaFunctions
&& self.lambda_state.current_function.is_some()
&& self.lambda_state.detail_tab == LambdaDetailTab::Monitor
&& !self.lambda_state.is_metrics_loading()
{
crate::lambda::functions::scroll_down(self);
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::scroll_down(self);
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
{
if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
self.sqs_state
.set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
} else {
let lines = self.sqs_state.policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
}
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::TrustRelationships
{
let lines = self.iam_state.trust_policy_document.lines().count();
let max_scroll = lines.saturating_sub(1);
self.iam_state.trust_policy_scroll =
(self.iam_state.trust_policy_scroll + 10).min(max_scroll);
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::RevokeSessions
{
self.iam_state.revoke_sessions_scroll =
(self.iam_state.revoke_sessions_scroll + 10).min(19);
} else if self.mode == Mode::Normal {
if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
{
let total_rows = self.calculate_total_bucket_rows();
self.s3_state.selected_row = self
.s3_state
.selected_row
.saturating_add(10)
.min(total_rows.saturating_sub(1));
let visible_rows = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row - visible_rows + 1;
}
} else if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_some()
{
let total_rows = self.calculate_total_object_rows();
self.s3_state.selected_object = self
.s3_state
.selected_object
.saturating_add(10)
.min(total_rows.saturating_sub(1));
let visible_rows = self.s3_state.object_visible_rows.get();
if self.s3_state.selected_object
>= self.s3_state.object_scroll_offset + visible_rows
{
self.s3_state.object_scroll_offset =
self.s3_state.selected_object - visible_rows + 1;
}
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::List
{
let filtered = filtered_log_groups(self);
self.log_groups_state.log_groups.page_down(filtered.len());
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::Detail
{
let len = filtered_log_streams(self).len();
nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
} else if self.view_mode == ViewMode::Events {
let max = self.log_groups_state.log_events.len();
nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
} else if self.view_mode == ViewMode::InsightsResults {
let max = self.insights_state.insights.query_results.len();
nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
} else if self.current_service == Service::CloudWatchAlarms {
let filtered = match self.alarms_state.alarm_tab {
AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
AlarmTab::InAlarm => self
.alarms_state
.table
.items
.iter()
.filter(|a| a.state.to_uppercase() == "ALARM")
.count(),
};
if filtered > 0 {
self.alarms_state.table.page_down(filtered);
}
} else if self.current_service == Service::CloudTrailEvents {
let filtered_count = self.cloudtrail_state.table.items.len();
if filtered_count > 0 {
self.cloudtrail_state.table.page_down(filtered_count);
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::page_down_normal(self);
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::page_down_normal(self);
} else if self.current_service == Service::SqsQueues {
let filtered =
filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter);
self.sqs_state.queues.page_down(filtered.len());
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::page_down_normal(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::page_down_normal(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::page_down_normal(self);
} else if self.current_service == Service::IamUsers {
let len = filtered_iam_users(self).len();
nav_page_down(&mut self.iam_state.users.selected, len, 10);
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
let filtered = filtered_iam_policies(self);
if !filtered.is_empty() {
self.iam_state.policies.page_down(filtered.len());
}
} else {
let filtered = filtered_iam_roles(self);
self.iam_state.roles.page_down(filtered.len());
}
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_some() {
if self.iam_state.group_tab == GroupTab::Users {
let filtered: Vec<_> = self
.iam_state
.group_users
.items
.iter()
.filter(|u| {
if self.iam_state.group_users.filter.is_empty() {
true
} else {
u.user_name
.to_lowercase()
.contains(&self.iam_state.group_users.filter.to_lowercase())
}
})
.collect();
if !filtered.is_empty() {
self.iam_state.group_users.page_down(filtered.len());
}
} else if self.iam_state.group_tab == GroupTab::Permissions {
let filtered = filtered_iam_policies(self);
if !filtered.is_empty() {
self.iam_state.policies.page_down(filtered.len());
}
} else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
let filtered = filtered_last_accessed(self);
if !filtered.is_empty() {
self.iam_state
.last_accessed_services
.page_down(filtered.len());
}
}
} else {
let filtered: Vec<_> = self
.iam_state
.groups
.items
.iter()
.filter(|g| {
if self.iam_state.groups.filter.is_empty() {
true
} else {
g.group_name
.to_lowercase()
.contains(&self.iam_state.groups.filter.to_lowercase())
}
})
.collect();
if !filtered.is_empty() {
self.iam_state.groups.page_down(filtered.len());
}
}
}
}
}
fn cycle_policy_type_next(&mut self) {
let types = ["All types", "AWS managed", "Customer managed"];
let current_idx = types
.iter()
.position(|&t| t == self.iam_state.policy_type_filter)
.unwrap_or(0);
let next_idx = (current_idx + 1) % types.len();
self.iam_state.policy_type_filter = types[next_idx].to_string();
self.iam_state.policies.reset();
}
fn cycle_policy_type_prev(&mut self) {
let types = ["All types", "AWS managed", "Customer managed"];
let current_idx = types
.iter()
.position(|&t| t == self.iam_state.policy_type_filter)
.unwrap_or(0);
let prev_idx = if current_idx == 0 {
types.len() - 1
} else {
current_idx - 1
};
self.iam_state.policy_type_filter = types[prev_idx].to_string();
self.iam_state.policies.reset();
}
fn page_up(&mut self) {
if self.current_service == Service::CloudTrailEvents
&& self.cloudtrail_state.current_event.is_some()
{
self.cloudtrail_state.event_json_scroll =
self.cloudtrail_state.event_json_scroll.saturating_sub(10);
} else if self.mode == Mode::ColumnSelector {
let mut prev_idx = self.column_selector_index.saturating_sub(10);
if self.is_blank_row_index(prev_idx) {
prev_idx = prev_idx.saturating_sub(1);
}
self.column_selector_index = prev_idx;
} else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
crate::s3::actions::page_up_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudFormationStacks
{
crate::cfn::actions::page_up_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_none()
{
let page_size = self.iam_state.roles.page_size.value();
self.iam_state.role_input_focus.handle_page_up(
&mut self.iam_state.roles.selected,
&mut self.iam_state.roles.scroll_offset,
page_size,
);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchAlarms
{
let page_size = self.alarms_state.table.page_size.value();
self.alarms_state.input_focus.handle_page_up(
&mut self.alarms_state.table.selected,
&mut self.alarms_state.table.scroll_offset,
page_size,
);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudTrailEvents
{
let page_size = self.cloudtrail_state.table.page_size.value();
self.cloudtrail_state.input_focus.handle_page_up(
&mut self.cloudtrail_state.table.selected,
&mut self.cloudtrail_state.table.scroll_offset,
page_size,
);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::CloudWatchLogGroups
{
if self.view_mode == ViewMode::List {
let page_size = self.log_groups_state.log_groups.page_size.value();
self.log_groups_state.input_focus.handle_page_up(
&mut self.log_groups_state.log_groups.selected,
&mut self.log_groups_state.log_groups.scroll_offset,
page_size,
);
} else {
let page_size = self.log_groups_state.stream_page_size;
self.log_groups_state.input_focus.handle_page_up(
&mut self.log_groups_state.selected_stream,
&mut self.log_groups_state.stream_current_page,
page_size,
);
self.log_groups_state.expanded_stream = None;
}
} else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
{
crate::lambda::functions::page_up_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::LambdaApplications
{
crate::lambda::applications::page_up_filter_input(self);
} else if self.mode == Mode::FilterInput
&& self.current_service == Service::EcrRepositories
&& self.ecr_state.current_repository.is_none()
{
crate::ecr::actions::page_up_filter_input(self);
} else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
let page_size = self.iam_state.policies.page_size.value();
self.iam_state.policy_input_focus.handle_page_up(
&mut self.iam_state.policies.selected,
&mut self.iam_state.policies.scroll_offset,
page_size,
);
} else if self.view_mode == ViewMode::PolicyView {
self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Template
{
crate::cfn::actions::scroll_up_template_fast(self);
} else if self.current_service == Service::SqsQueues
&& self.sqs_state.current_queue.is_some()
{
if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
self.sqs_state
.set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
} else {
self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
}
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::TrustRelationships
{
self.iam_state.trust_policy_scroll =
self.iam_state.trust_policy_scroll.saturating_sub(10);
} else if self.current_service == Service::IamRoles
&& self.iam_state.current_role.is_some()
&& self.iam_state.role_tab == RoleTab::RevokeSessions
{
self.iam_state.revoke_sessions_scroll =
self.iam_state.revoke_sessions_scroll.saturating_sub(10);
} else if self.mode == Mode::Normal {
if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
{
self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
}
} else if self.current_service == Service::S3Buckets
&& self.s3_state.current_bucket.is_some()
{
self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
self.s3_state.object_scroll_offset = self.s3_state.selected_object;
}
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::List
{
self.log_groups_state.log_groups.page_up();
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::Detail
{
self.log_groups_state.selected_stream =
self.log_groups_state.selected_stream.saturating_sub(10);
} else if self.view_mode == ViewMode::Events {
if self.log_groups_state.event_scroll_offset < 10
&& self.log_groups_state.has_older_events
{
self.log_groups_state.loading = true;
}
self.log_groups_state.event_scroll_offset =
self.log_groups_state.event_scroll_offset.saturating_sub(10);
} else if self.view_mode == ViewMode::InsightsResults {
self.insights_state.insights.results_selected = self
.insights_state
.insights
.results_selected
.saturating_sub(10);
} else if self.current_service == Service::CloudWatchAlarms {
self.alarms_state.table.page_up();
} else if self.current_service == Service::CloudTrailEvents {
self.cloudtrail_state.table.page_up();
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::page_up_normal(self);
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::page_up_normal(self);
} else if self.current_service == Service::SqsQueues {
self.sqs_state.queues.page_up();
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::page_up_normal(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::page_up_normal(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::page_up_normal(self);
} else if self.current_service == Service::IamUsers {
self.iam_state.users.page_up();
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
self.iam_state.policies.page_up();
} else {
self.iam_state.roles.page_up();
}
}
}
}
fn next_pane(&mut self) {
if self.current_service == Service::S3Buckets {
if self.s3_state.current_bucket.is_some() {
let mut visual_idx = 0;
let mut found_obj: Option<S3Object> = None;
fn check_nested(
obj: &S3Object,
visual_idx: &mut usize,
target_idx: usize,
expanded_prefixes: &std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
found_obj: &mut Option<S3Object>,
) {
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(preview) = prefix_preview.get(&obj.key) {
for nested_obj in preview {
if *visual_idx == target_idx {
*found_obj = Some(nested_obj.clone());
return;
}
*visual_idx += 1;
check_nested(
nested_obj,
visual_idx,
target_idx,
expanded_prefixes,
prefix_preview,
found_obj,
);
if found_obj.is_some() {
return;
}
}
} else {
*visual_idx += 1;
}
}
}
for obj in &self.s3_state.objects {
if visual_idx == self.s3_state.selected_object {
found_obj = Some(obj.clone());
break;
}
visual_idx += 1;
check_nested(
obj,
&mut visual_idx,
self.s3_state.selected_object,
&self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
&mut found_obj,
);
if found_obj.is_some() {
break;
}
}
if let Some(obj) = found_obj {
if obj.is_prefix {
if !self.s3_state.expanded_prefixes.contains(&obj.key) {
self.s3_state.expanded_prefixes.insert(obj.key.clone());
if !self.s3_state.prefix_preview.contains_key(&obj.key) {
self.s3_state.buckets.loading = true;
}
}
if self.s3_state.expanded_prefixes.contains(&obj.key) {
if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
if !preview.is_empty() {
self.s3_state.selected_object += 1;
}
}
}
}
}
} else {
let mut row_idx = 0;
let mut found = false;
for bucket in &self.s3_state.buckets.items {
if row_idx == self.s3_state.selected_row {
if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
self.s3_state.expanded_prefixes.insert(bucket.name.clone());
if !self.s3_state.bucket_preview.contains_key(&bucket.name)
&& !self.s3_state.bucket_errors.contains_key(&bucket.name)
{
self.s3_state.buckets.loading = true;
}
}
if self.s3_state.expanded_prefixes.contains(&bucket.name) {
if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
if !preview.is_empty() {
self.s3_state.selected_row = row_idx + 1;
let visible = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row.saturating_sub(visible - 1);
}
}
}
}
break;
}
row_idx += 1;
if self.s3_state.bucket_errors.contains_key(&bucket.name)
&& self.s3_state.expanded_prefixes.contains(&bucket.name)
{
if let Some(err) = self.s3_state.bucket_errors.get(&bucket.name) {
let max_width = 120;
let error_rows = if err.len() > max_width {
err.len().div_ceil(max_width)
} else {
1
};
row_idx += error_rows;
}
continue;
}
if self.s3_state.expanded_prefixes.contains(&bucket.name) {
if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
#[allow(clippy::too_many_arguments)]
fn check_nested_expansion(
objects: &[S3Object],
row_idx: &mut usize,
target_row: usize,
expanded_prefixes: &mut std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
found: &mut bool,
loading: &mut bool,
selected_row: &mut usize,
) {
for obj in objects {
if *row_idx == target_row {
if obj.is_prefix {
if !expanded_prefixes.contains(&obj.key) {
expanded_prefixes.insert(obj.key.clone());
if !prefix_preview.contains_key(&obj.key) {
*loading = true;
}
}
if expanded_prefixes.contains(&obj.key) {
if let Some(preview) = prefix_preview.get(&obj.key)
{
if !preview.is_empty() {
*selected_row = *row_idx + 1;
}
}
}
}
*found = true;
return;
}
*row_idx += 1;
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(nested) = prefix_preview.get(&obj.key) {
check_nested_expansion(
nested,
row_idx,
target_row,
expanded_prefixes,
prefix_preview,
found,
loading,
selected_row,
);
if *found {
return;
}
} else {
*row_idx += 1; }
}
}
}
check_nested_expansion(
preview,
&mut row_idx,
self.s3_state.selected_row,
&mut self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
&mut found,
&mut self.s3_state.buckets.loading,
&mut self.s3_state.selected_row,
);
if found || row_idx > self.s3_state.selected_row {
break;
}
} else {
if row_idx > self.s3_state.selected_row {
break;
}
}
}
if found {
break;
}
}
}
} else if self.view_mode == ViewMode::InsightsResults {
let max_cols = self
.insights_state
.insights
.query_results
.first()
.map(|r| r.len())
.unwrap_or(0);
if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
self.insights_state.insights.results_horizontal_scroll += 1;
}
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::List
{
if self.log_groups_state.log_groups.expanded_item
!= Some(self.log_groups_state.log_groups.selected)
{
self.log_groups_state.log_groups.expanded_item =
Some(self.log_groups_state.log_groups.selected);
}
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::Detail
{
if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
{
self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
}
} else if self.view_mode == ViewMode::Events {
if self.log_groups_state.expanded_event
!= Some(self.log_groups_state.event_scroll_offset)
{
self.log_groups_state.expanded_event =
Some(self.log_groups_state.event_scroll_offset);
}
} else if self.current_service == Service::CloudWatchAlarms {
if !self.alarms_state.table.is_expanded() {
self.alarms_state.table.toggle_expand();
}
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::expand_row(self);
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::next_pane(self);
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
{
self.sqs_state.triggers.toggle_expand();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
{
self.sqs_state.pipes.toggle_expand();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
{
self.sqs_state.tags.toggle_expand();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
self.sqs_state.subscriptions.toggle_expand();
} else {
self.sqs_state.queues.expand();
}
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::expand_row(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::expand_row(self);
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_none()
{
self.cfn_state.table.toggle_expand();
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Parameters
{
self.cfn_state.parameters.toggle_expand();
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Outputs
{
self.cfn_state.outputs.toggle_expand();
} else if self.current_service == Service::CloudFormationStacks
&& self.cfn_state.current_stack.is_some()
&& self.cfn_state.detail_tab == CfnDetailTab::Resources
{
self.cfn_state.resources.toggle_expand();
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
if self.iam_state.user_tab == UserTab::Tags {
if self.iam_state.user_tags.expanded_item
!= Some(self.iam_state.user_tags.selected)
{
self.iam_state.user_tags.expanded_item =
Some(self.iam_state.user_tags.selected);
}
} else if self.iam_state.policies.expanded_item
!= Some(self.iam_state.policies.selected)
{
self.iam_state.policies.toggle_expand();
}
} else if !self.iam_state.users.is_expanded() {
self.iam_state.users.toggle_expand();
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
if self.iam_state.role_tab == RoleTab::Tags {
if !self.iam_state.tags.is_expanded() {
self.iam_state.tags.expand();
}
} else if self.iam_state.role_tab == RoleTab::LastAccessed {
if !self.iam_state.last_accessed_services.is_expanded() {
self.iam_state.last_accessed_services.expand();
}
} else if !self.iam_state.policies.is_expanded() {
self.iam_state.policies.expand();
}
} else if !self.iam_state.roles.is_expanded() {
self.iam_state.roles.expand();
}
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_some() {
if self.iam_state.group_tab == GroupTab::Users {
if !self.iam_state.group_users.is_expanded() {
self.iam_state.group_users.expand();
}
} else if self.iam_state.group_tab == GroupTab::Permissions {
if !self.iam_state.policies.is_expanded() {
self.iam_state.policies.expand();
}
} else if self.iam_state.group_tab == GroupTab::AccessAdvisor
&& !self.iam_state.last_accessed_services.is_expanded()
{
self.iam_state.last_accessed_services.expand();
}
} else if !self.iam_state.groups.is_expanded() {
self.iam_state.groups.expand();
}
}
}
fn go_to_page(&mut self, page: usize) {
if page == 0 {
return;
}
match self.current_service {
Service::CloudWatchAlarms => {
let alarm_page_size = self.alarms_state.table.page_size.value();
let target = (page - 1) * alarm_page_size;
let filtered_count = match self.alarms_state.alarm_tab {
AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
AlarmTab::InAlarm => self
.alarms_state
.table
.items
.iter()
.filter(|a| a.state.to_uppercase() == "ALARM")
.count(),
};
let max_offset = filtered_count.saturating_sub(alarm_page_size);
self.alarms_state.table.scroll_offset = target.min(max_offset);
self.alarms_state.table.selected = self
.alarms_state
.table
.scroll_offset
.min(filtered_count.saturating_sub(1));
}
Service::CloudTrailEvents => {
let page_size = self.cloudtrail_state.table.page_size.value();
let filtered_count = self.cloudtrail_state.table.items.len();
let max_page = (filtered_count / page_size) + 1;
if page <= max_page {
let target = (page - 1) * page_size;
self.cloudtrail_state.table.scroll_offset = target;
self.cloudtrail_state.table.selected = target;
self.cloudtrail_state.table.expanded_item = None; }
}
Service::CloudWatchLogGroups => match self.view_mode {
ViewMode::Events => {
let page_size = 20;
let target = (page - 1) * page_size;
let max = self.log_groups_state.log_events.len().saturating_sub(1);
self.log_groups_state.event_scroll_offset = target.min(max);
}
ViewMode::Detail => {
let page_size = self.log_groups_state.stream_page_size;
self.log_groups_state.stream_current_page = (page - 1).min(
self.log_groups_state
.log_streams
.len()
.div_ceil(page_size)
.saturating_sub(1),
);
self.log_groups_state.selected_stream = 0;
}
ViewMode::List => {
let total = self.log_groups_state.log_groups.items.len();
self.log_groups_state.log_groups.goto_page(page, total);
}
_ => {}
},
Service::EcrRepositories => {
crate::ecr::actions::go_to_page(self, page);
}
Service::SqsQueues => {
let filtered_count =
filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter)
.len();
self.sqs_state.queues.goto_page(page, filtered_count);
}
Service::S3Buckets => {
if self.s3_state.current_bucket.is_some() {
let page_size = 50; let target = (page - 1) * page_size;
let total_rows = self.calculate_total_object_rows();
let max = total_rows.saturating_sub(1);
self.s3_state.selected_object = target.min(max);
} else {
let page_size = self.s3_state.buckets.page_size.value();
let target = (page - 1) * page_size;
let total_rows = self.calculate_total_bucket_rows();
let max = total_rows.saturating_sub(1);
self.s3_state.selected_row = target.min(max);
self.s3_state.bucket_scroll_offset =
target.min(total_rows.saturating_sub(page_size));
}
}
Service::LambdaFunctions => {
crate::lambda::functions::go_to_page(self, page);
}
Service::LambdaApplications => {
crate::lambda::applications::go_to_page(self, page);
}
Service::CloudFormationStacks => {
crate::cfn::actions::go_to_page(self, page);
}
Service::IamUsers => {
let filtered_count = filtered_iam_users(self).len();
self.iam_state.users.goto_page(page, filtered_count);
}
Service::IamRoles => {
let filtered_count = filtered_iam_roles(self).len();
self.iam_state.roles.goto_page(page, filtered_count);
}
_ => {}
}
}
fn prev_pane(&mut self) {
if self.current_service == Service::S3Buckets {
if self.s3_state.current_bucket.is_some() {
let mut visual_idx = 0;
let mut found_obj: Option<S3Object> = None;
let mut parent_idx: Option<usize> = None;
#[allow(clippy::too_many_arguments)]
fn find_with_parent(
objects: &[S3Object],
visual_idx: &mut usize,
target_idx: usize,
expanded_prefixes: &std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
found_obj: &mut Option<S3Object>,
parent_idx: &mut Option<usize>,
current_parent: Option<usize>,
) {
for obj in objects {
if *visual_idx == target_idx {
*found_obj = Some(obj.clone());
*parent_idx = current_parent;
return;
}
let obj_idx = *visual_idx;
*visual_idx += 1;
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(preview) = prefix_preview.get(&obj.key) {
find_with_parent(
preview,
visual_idx,
target_idx,
expanded_prefixes,
prefix_preview,
found_obj,
parent_idx,
Some(obj_idx),
);
if found_obj.is_some() {
return;
}
}
}
}
}
find_with_parent(
&self.s3_state.objects,
&mut visual_idx,
self.s3_state.selected_object,
&self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
&mut found_obj,
&mut parent_idx,
None,
);
if let Some(obj) = found_obj {
if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
self.s3_state.expanded_prefixes.remove(&obj.key);
if let Some(parent) = parent_idx {
self.s3_state.selected_object = parent;
}
} else if let Some(parent) = parent_idx {
self.s3_state.selected_object = parent;
}
}
let visible_rows = self.s3_state.object_visible_rows.get();
if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
self.s3_state.object_scroll_offset = self.s3_state.selected_object;
} else if self.s3_state.selected_object
>= self.s3_state.object_scroll_offset + visible_rows
{
self.s3_state.object_scroll_offset = self
.s3_state
.selected_object
.saturating_sub(visible_rows - 1);
}
} else {
let mut row_idx = 0;
for bucket in &self.s3_state.buckets.items {
if row_idx == self.s3_state.selected_row {
self.s3_state.expanded_prefixes.remove(&bucket.name);
break;
}
row_idx += 1;
if self.s3_state.expanded_prefixes.contains(&bucket.name) {
if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
#[allow(clippy::too_many_arguments)]
fn check_nested_collapse(
objects: &[S3Object],
row_idx: &mut usize,
target_row: usize,
expanded_prefixes: &mut std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
found: &mut bool,
selected_row: &mut usize,
parent_row: usize,
) {
for obj in objects {
let current_row = *row_idx;
if *row_idx == target_row {
if obj.is_prefix {
if expanded_prefixes.contains(&obj.key) {
expanded_prefixes.remove(&obj.key);
*selected_row = parent_row;
} else {
*selected_row = parent_row;
}
} else {
*selected_row = parent_row;
}
*found = true;
return;
}
*row_idx += 1;
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(nested) = prefix_preview.get(&obj.key) {
check_nested_collapse(
nested,
row_idx,
target_row,
expanded_prefixes,
prefix_preview,
found,
selected_row,
current_row,
);
if *found {
return;
}
} else {
*row_idx += 1; }
}
}
}
let mut found = false;
let parent_row = row_idx - 1; check_nested_collapse(
preview,
&mut row_idx,
self.s3_state.selected_row,
&mut self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
&mut found,
&mut self.s3_state.selected_row,
parent_row,
);
if found {
let visible_rows = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
} else if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible_rows
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row.saturating_sub(visible_rows - 1);
}
return;
}
} else {
row_idx += 1;
}
}
}
let visible_rows = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
} else if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible_rows
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row.saturating_sub(visible_rows - 1);
}
}
} else if self.view_mode == ViewMode::InsightsResults {
self.insights_state.insights.results_horizontal_scroll = self
.insights_state
.insights
.results_horizontal_scroll
.saturating_sub(1);
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::List
{
if self.log_groups_state.log_groups.has_expanded_item() {
self.log_groups_state.log_groups.collapse();
}
} else if self.current_service == Service::CloudWatchLogGroups
&& self.view_mode == ViewMode::Detail
{
if self.log_groups_state.expanded_stream.is_some() {
self.log_groups_state.expanded_stream = None;
}
} else if self.view_mode == ViewMode::Events {
if self.log_groups_state.expanded_event.is_some() {
self.log_groups_state.expanded_event = None;
}
} else if self.current_service == Service::CloudWatchAlarms {
self.alarms_state.table.collapse();
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::prev_pane(self);
} else if self.current_service == Service::ApiGatewayApis {
self.apig_state.apis.collapse();
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::prev_pane(self);
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
{
self.sqs_state.triggers.collapse();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
{
self.sqs_state.pipes.collapse();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
{
self.sqs_state.tags.collapse();
} else if self.sqs_state.current_queue.is_some()
&& self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
{
self.sqs_state.subscriptions.collapse();
} else {
self.sqs_state.queues.collapse();
}
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::prev_pane(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::prev_pane(self);
} else if self.current_service == Service::CloudFormationStacks {
crate::cfn::actions::prev_pane(self);
} else if self.current_service == Service::IamUsers {
if self.iam_state.users.has_expanded_item() {
self.iam_state.users.collapse();
}
} else if self.current_service == Service::IamRoles {
if self.view_mode == ViewMode::PolicyView {
self.view_mode = ViewMode::Detail;
self.iam_state.current_policy = None;
self.iam_state.policy_document.clear();
self.iam_state.policy_scroll = 0;
} else if self.iam_state.current_role.is_some() {
if self.iam_state.role_tab == RoleTab::Tags
&& self.iam_state.tags.has_expanded_item()
{
self.iam_state.tags.collapse();
} else if self.iam_state.role_tab == RoleTab::LastAccessed
&& self
.iam_state
.last_accessed_services
.expanded_item
.is_some()
{
self.iam_state.last_accessed_services.collapse();
} else if self.iam_state.policies.has_expanded_item() {
self.iam_state.policies.collapse();
}
} else if self.iam_state.roles.has_expanded_item() {
self.iam_state.roles.collapse();
}
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_some() {
if self.iam_state.group_tab == GroupTab::Users
&& self.iam_state.group_users.has_expanded_item()
{
self.iam_state.group_users.collapse();
} else if self.iam_state.group_tab == GroupTab::Permissions
&& self.iam_state.policies.has_expanded_item()
{
self.iam_state.policies.collapse();
} else if self.iam_state.group_tab == GroupTab::AccessAdvisor
&& self
.iam_state
.last_accessed_services
.expanded_item
.is_some()
{
self.iam_state.last_accessed_services.collapse();
}
} else if self.iam_state.groups.has_expanded_item() {
self.iam_state.groups.collapse();
}
}
}
fn collapse_row(&mut self) {
match self.current_service {
Service::S3Buckets => {
if self.s3_state.current_bucket.is_none() {
let filtered_buckets: Vec<_> = self
.s3_state
.buckets
.items
.iter()
.filter(|b| {
if self.s3_state.buckets.filter.is_empty() {
true
} else {
b.name
.to_lowercase()
.contains(&self.s3_state.buckets.filter.to_lowercase())
}
})
.collect();
let mut row_idx = 0;
for bucket in filtered_buckets {
if row_idx == self.s3_state.selected_row {
self.s3_state.expanded_prefixes.remove(&bucket.name);
break;
}
row_idx += 1;
if self.s3_state.expanded_prefixes.contains(&bucket.name) {
if self.s3_state.bucket_errors.contains_key(&bucket.name) {
continue;
}
if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
#[allow(clippy::too_many_arguments)]
fn check_nested_collapse(
objects: &[S3Object],
row_idx: &mut usize,
target_row: usize,
expanded_prefixes: &mut std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<
String,
Vec<S3Object>,
>,
found: &mut bool,
selected_row: &mut usize,
parent_row: usize,
) {
for obj in objects {
let current_row = *row_idx;
if *row_idx == target_row {
if obj.is_prefix {
if expanded_prefixes.contains(&obj.key) {
expanded_prefixes.remove(&obj.key);
*selected_row = parent_row;
} else {
*selected_row = parent_row;
}
} else {
*selected_row = parent_row;
}
*found = true;
return;
}
*row_idx += 1;
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(nested) = prefix_preview.get(&obj.key) {
check_nested_collapse(
nested,
row_idx,
target_row,
expanded_prefixes,
prefix_preview,
found,
selected_row,
current_row,
);
if *found {
return;
}
} else {
*row_idx += 1; }
}
}
}
let mut found = false;
let parent_row = row_idx - 1; check_nested_collapse(
preview,
&mut row_idx,
self.s3_state.selected_row,
&mut self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
&mut found,
&mut self.s3_state.selected_row,
parent_row,
);
if found {
let visible_rows = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row
< self.s3_state.bucket_scroll_offset
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row;
} else if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible_rows
{
self.s3_state.bucket_scroll_offset = self
.s3_state
.selected_row
.saturating_sub(visible_rows - 1);
}
return;
}
} else {
row_idx += 1;
}
}
}
let visible_rows = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
} else if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible_rows
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row.saturating_sub(visible_rows - 1);
}
}
}
Service::CloudWatchLogGroups => {
if self.view_mode == ViewMode::Events {
if let Some(idx) = self.log_groups_state.expanded_event {
self.log_groups_state.expanded_event = None;
self.log_groups_state.selected_event = idx;
}
} else if self.view_mode == ViewMode::Detail {
if let Some(idx) = self.log_groups_state.expanded_stream {
self.log_groups_state.expanded_stream = None;
self.log_groups_state.selected_stream = idx;
}
} else {
self.log_groups_state.log_groups.collapse();
}
}
Service::CloudWatchAlarms => self.alarms_state.table.collapse(),
Service::Ec2Instances => {
crate::ec2::actions::collapse_row(self);
}
Service::EcrRepositories => {
crate::ecr::actions::collapse_row(self);
}
Service::LambdaFunctions => crate::lambda::functions::collapse_row(self),
Service::LambdaApplications => crate::lambda::applications::prev_pane(self),
Service::SqsQueues => self.sqs_state.queues.collapse(),
Service::CloudFormationStacks => {
crate::cfn::actions::collapse_row(self);
}
Service::IamUsers => {
if self.iam_state.current_user.is_some() {
match self.iam_state.user_tab {
crate::ui::iam::UserTab::Permissions => {
self.iam_state.policies.collapse();
}
crate::ui::iam::UserTab::Groups => {
self.iam_state.user_group_memberships.collapse();
}
crate::ui::iam::UserTab::Tags => {
self.iam_state.user_tags.collapse();
}
_ => {}
}
} else {
self.iam_state.users.collapse();
}
}
Service::IamRoles => {
if self.iam_state.current_role.is_some() {
match self.iam_state.role_tab {
crate::ui::iam::RoleTab::Permissions => {
self.iam_state.policies.collapse();
}
crate::ui::iam::RoleTab::Tags => {
self.iam_state.tags.collapse();
}
_ => {}
}
} else {
self.iam_state.roles.collapse();
}
}
Service::IamUserGroups => self.iam_state.groups.collapse(),
Service::ApiGatewayApis => {
if let Some(api) = &self.apig_state.current_api {
if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
let protocol = api.protocol_type.to_uppercase();
if protocol == "REST" {
let (filtered_items, _filtered_children) =
crate::ui::apig::filter_tree_items(
&self.apig_state.resources.items,
&self.apig_state.resource_children,
&self.apig_state.route_filter,
);
let selected_row = self.apig_state.resources.selected;
let mut current_row = 0;
if let Some(resource_id) = self.find_resource_at_row(
&filtered_items,
selected_row,
&mut current_row,
) {
if self.apig_state.expanded_resources.contains(&resource_id) {
self.apig_state.expanded_resources.remove(&resource_id);
} else if let Some(parent_row) =
self.find_resource_parent_row(&filtered_items, &resource_id)
{
self.apig_state.resources.selected = parent_row;
}
}
} else {
let (filtered_items, _filtered_children) =
crate::ui::apig::filter_tree_items(
&self.apig_state.routes.items,
&self.apig_state.route_children,
&self.apig_state.route_filter,
);
let selected_row = self.apig_state.routes.selected;
let mut current_row = 0;
if let Some(route_key) = self.find_route_at_row(
&filtered_items,
selected_row,
&mut current_row,
) {
if self.apig_state.expanded_routes.contains(&route_key) {
self.apig_state.expanded_routes.remove(&route_key);
} else if let Some(parent_row) =
self.find_parent_row(&filtered_items, &route_key)
{
self.apig_state.routes.selected = parent_row;
}
}
}
}
} else {
self.apig_state.apis.collapse();
}
}
Service::CloudTrailEvents => {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
{
self.cloudtrail_state.resources_expanded_index = None;
} else {
self.cloudtrail_state.table.collapse();
}
}
_ => {}
}
}
fn expand_row(&mut self) {
match self.current_service {
Service::S3Buckets => {
if self.s3_state.current_bucket.is_none() {
let filtered_buckets: Vec<_> = self
.s3_state
.buckets
.items
.iter()
.filter(|b| {
if self.s3_state.buckets.filter.is_empty() {
true
} else {
b.name
.to_lowercase()
.contains(&self.s3_state.buckets.filter.to_lowercase())
}
})
.collect();
fn check_nested_expand(
objects: &[S3Object],
row_idx: &mut usize,
target_row: usize,
expanded_prefixes: &mut std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
) -> Option<(bool, usize)> {
for obj in objects {
if *row_idx == target_row {
if obj.is_prefix {
if expanded_prefixes.contains(&obj.key) {
expanded_prefixes.remove(&obj.key);
return Some((true, *row_idx));
} else {
expanded_prefixes.insert(obj.key.clone());
return Some((true, *row_idx + 1)); }
}
return Some((false, *row_idx));
}
*row_idx += 1;
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(nested) = prefix_preview.get(&obj.key) {
if let Some(result) = check_nested_expand(
nested,
row_idx,
target_row,
expanded_prefixes,
prefix_preview,
) {
return Some(result);
}
}
}
}
None
}
let mut row_idx = 0;
for bucket in filtered_buckets {
if row_idx == self.s3_state.selected_row {
if self.s3_state.expanded_prefixes.contains(&bucket.name) {
if let Some(preview) =
self.s3_state.bucket_preview.get(&bucket.name)
{
if !preview.is_empty() {
self.s3_state.selected_row = row_idx + 1;
let visible = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible
{
self.s3_state.bucket_scroll_offset = self
.s3_state
.selected_row
.saturating_sub(visible - 1);
}
}
} else {
}
} else {
self.s3_state.expanded_prefixes.insert(bucket.name.clone());
self.s3_state.buckets.loading = true;
if let Some(preview) =
self.s3_state.bucket_preview.get(&bucket.name)
{
if !preview.is_empty() {
self.s3_state.selected_row = row_idx + 1;
let visible = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible
{
self.s3_state.bucket_scroll_offset = self
.s3_state
.selected_row
.saturating_sub(visible - 1);
}
}
}
}
return;
}
row_idx += 1;
if self.s3_state.expanded_prefixes.contains(&bucket.name) {
if self.s3_state.bucket_errors.contains_key(&bucket.name) {
continue;
}
if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
if let Some((loading, new_row)) = check_nested_expand(
preview,
&mut row_idx,
self.s3_state.selected_row,
&mut self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
) {
self.s3_state.selected_row = new_row;
if loading {
self.s3_state.buckets.loading = true;
}
let visible = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row.saturating_sub(visible - 1);
} else if self.s3_state.selected_row
< self.s3_state.bucket_scroll_offset
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row;
}
return;
}
}
}
}
} else {
fn check_object_expand(
objects: &[S3Object],
row_idx: &mut usize,
target_row: usize,
expanded_prefixes: &mut std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
) -> Option<(bool, usize)> {
for obj in objects {
if *row_idx == target_row {
if obj.is_prefix {
if expanded_prefixes.contains(&obj.key) {
expanded_prefixes.remove(&obj.key);
return Some((true, *row_idx));
} else {
expanded_prefixes.insert(obj.key.clone());
return Some((true, *row_idx + 1));
}
}
return Some((false, *row_idx));
}
*row_idx += 1;
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(nested) = prefix_preview.get(&obj.key) {
if let Some(result) = check_object_expand(
nested,
row_idx,
target_row,
expanded_prefixes,
prefix_preview,
) {
return Some(result);
}
}
}
}
None
}
let mut row_idx = 0;
if let Some((loading, new_row)) = check_object_expand(
&self.s3_state.objects,
&mut row_idx,
self.s3_state.selected_object,
&mut self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
) {
self.s3_state.selected_object = new_row;
if loading {
self.s3_state.buckets.loading = true;
}
}
}
}
Service::ApiGatewayApis => {
if let Some(api) = &self.apig_state.current_api {
if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
let protocol = api.protocol_type.to_uppercase();
if protocol == "REST" {
let (filtered_items, filtered_children) =
crate::ui::apig::filter_tree_items(
&self.apig_state.resources.items,
&self.apig_state.resource_children,
&self.apig_state.route_filter,
);
let selected_row = self.apig_state.resources.selected;
let mut current_row = 0;
if let Some(resource_id) = self.find_resource_at_row(
&filtered_items,
selected_row,
&mut current_row,
) {
if self.apig_state.expanded_resources.contains(&resource_id) {
let total_rows =
crate::ui::tree::TreeRenderer::count_visible_rows(
&filtered_items,
&self.apig_state.expanded_resources,
&filtered_children,
);
if selected_row + 1 < total_rows {
self.apig_state.resources.selected = selected_row + 1;
}
} else {
self.apig_state.expanded_resources.insert(resource_id);
}
}
} else {
let (filtered_items, filtered_children) =
crate::ui::apig::filter_tree_items(
&self.apig_state.routes.items,
&self.apig_state.route_children,
&self.apig_state.route_filter,
);
let selected_row = self.apig_state.routes.selected;
let mut current_row = 0;
if let Some(route_key) = self.find_route_at_row(
&filtered_items,
selected_row,
&mut current_row,
) {
if self.apig_state.expanded_routes.contains(&route_key) {
let total_rows =
crate::ui::tree::TreeRenderer::count_visible_rows(
&filtered_items,
&self.apig_state.expanded_routes,
&filtered_children,
);
if selected_row + 1 < total_rows {
self.apig_state.routes.selected = selected_row + 1;
}
} else {
self.apig_state.expanded_routes.insert(route_key);
}
}
}
}
} else {
self.apig_state.apis.expand();
}
}
Service::CloudTrailEvents => {
if self.cloudtrail_state.current_event.is_some()
&& self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
{
self.cloudtrail_state.resources_expanded_index = Some(0);
} else {
self.cloudtrail_state.table.expand();
}
}
Service::CloudFormationStacks => {
crate::cfn::actions::expand_row(self);
}
_ => {
self.next_pane();
}
}
}
fn find_route_at_row(
&self,
routes: &[Route],
target_row: usize,
current_row: &mut usize,
) -> Option<String> {
for route in routes {
if *current_row == target_row {
return Some(route.route_key.clone());
}
*current_row += 1;
if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
if let Some(key) = self.find_route_at_row(children, target_row, current_row) {
return Some(key);
}
}
}
}
None
}
fn find_route_id_at_row_with_children(
&self,
routes: &[Route],
children_map: &HashMap<String, Vec<Route>>,
target_row: usize,
current_row: &mut usize,
) -> Option<String> {
for route in routes {
if *current_row == target_row {
if !route.target.is_empty() {
return Some(route.route_id.clone());
} else {
return None;
}
}
*current_row += 1;
if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
if let Some(children) = children_map.get(&route.route_key) {
if let Some(id) = self.find_route_id_at_row_with_children(
children,
children_map,
target_row,
current_row,
) {
return Some(id);
}
}
}
}
None
}
fn find_parent_row(&self, routes: &[Route], child_key: &str) -> Option<usize> {
let mut current_row = 0;
self.find_parent_row_recursive(routes, child_key, &mut current_row)
}
fn find_parent_row_recursive(
&self,
routes: &[Route],
child_key: &str,
current_row: &mut usize,
) -> Option<usize> {
for route in routes {
let parent_row = *current_row;
*current_row += 1;
if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
for child in children {
if child.route_key == child_key {
return Some(parent_row);
}
}
if let Some(row) =
self.find_parent_row_recursive(children, child_key, current_row)
{
return Some(row);
}
}
}
}
None
}
fn find_resource_at_row(
&self,
resources: &[ApigResource],
target_row: usize,
current_row: &mut usize,
) -> Option<String> {
for resource in resources {
if *current_row == target_row {
return Some(resource.id.clone());
}
*current_row += 1;
if self.apig_state.expanded_resources.contains(&resource.id) {
if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
if let Some(id) = self.find_resource_at_row(children, target_row, current_row) {
return Some(id);
}
}
}
}
None
}
fn find_resource_parent_row(
&self,
resources: &[ApigResource],
child_id: &str,
) -> Option<usize> {
let mut current_row = 0;
self.find_resource_parent_row_recursive(resources, child_id, &mut current_row)
}
fn find_resource_parent_row_recursive(
&self,
resources: &[ApigResource],
child_id: &str,
current_row: &mut usize,
) -> Option<usize> {
for resource in resources {
let parent_row = *current_row;
*current_row += 1;
if self.apig_state.expanded_resources.contains(&resource.id) {
if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
for child in children {
if child.id == child_id {
return Some(parent_row);
}
}
if let Some(row) =
self.find_resource_parent_row_recursive(children, child_id, current_row)
{
return Some(row);
}
}
}
}
None
}
fn select_item(&mut self) {
if self.mode == Mode::RegionPicker {
if self.region_filter_active {
self.region_filter_active = false;
return;
}
let filtered = self.get_filtered_regions();
if let Some(region) = filtered.get(self.region_picker_selected) {
if !self.tabs.is_empty() {
let mut session = Session::new(
self.profile.clone(),
self.region.clone(),
self.config.account_id.clone(),
self.config.role_arn.clone(),
);
for tab in &self.tabs {
session.tabs.push(SessionTab {
service: format!("{:?}", tab.service),
title: tab.title.clone(),
breadcrumb: tab.breadcrumb.clone(),
filter: None,
selected_item: None,
});
}
let _ = session.save();
}
self.region = region.code.to_string();
self.config.region = region.code.to_string();
self.tabs.clear();
self.current_tab = 0;
self.service_selected = false;
self.mode = Mode::Normal;
}
} else if self.mode == Mode::ProfilePicker {
let filtered = self.get_filtered_profiles();
if let Some(profile) = filtered.get(self.profile_picker_selected) {
let profile_name = profile.name.clone();
let profile_region = profile.region.clone();
self.profile = profile_name.clone();
std::env::set_var("AWS_PROFILE", &profile_name);
if let Some(region) = profile_region {
self.region = region;
}
self.mode = Mode::Normal;
}
} else if self.mode == Mode::ServicePicker {
if self.service_picker.filter_active {
self.service_picker.filter_active = false;
return;
}
let filtered = self.filtered_services();
if let Some(&service) = filtered.get(self.service_picker.selected) {
let new_service = match service {
"API Gateway › APIs" => Service::ApiGatewayApis,
"CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
"CloudWatch › Logs Insights" => Service::CloudWatchInsights,
"CloudWatch › Alarms" => Service::CloudWatchAlarms,
"CloudTrail › Event History" => Service::CloudTrailEvents,
"CloudFormation › Stacks" => Service::CloudFormationStacks,
"EC2 › Instances" => Service::Ec2Instances,
"ECR › Repositories" => Service::EcrRepositories,
"IAM › Users" => Service::IamUsers,
"IAM › Roles" => Service::IamRoles,
"IAM › User Groups" => Service::IamUserGroups,
"Lambda › Functions" => Service::LambdaFunctions,
"Lambda › Applications" => Service::LambdaApplications,
"S3 › Buckets" => Service::S3Buckets,
"SQS › Queues" => Service::SqsQueues,
_ => return,
};
self.tabs.push(Tab {
service: new_service,
title: service.to_string(),
breadcrumb: service.to_string(),
});
self.current_tab = self.tabs.len() - 1;
self.current_service = new_service;
self.view_mode = ViewMode::List;
self.service_selected = true;
self.mode = Mode::Normal;
}
} else if self.mode == Mode::TabPicker {
let filtered = self.get_filtered_tabs();
if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
self.current_tab = idx;
self.current_service = self.tabs[idx].service;
self.mode = Mode::Normal;
self.tab_filter.clear();
}
} else if self.mode == Mode::SessionPicker {
let filtered = self.get_filtered_sessions();
if let Some(&session) = filtered.get(self.session_picker_selected) {
let session = session.clone();
self.current_session = Some(session.clone());
self.profile = session.profile.clone();
self.region = session.region.clone();
self.config.region = session.region.clone();
self.config.account_id = session.account_id.clone();
self.config.role_arn = session.role_arn.clone();
self.tabs = session
.tabs
.iter()
.map(|st| Tab {
service: match st.service.as_str() {
"CloudWatchLogGroups" => Service::CloudWatchLogGroups,
"CloudWatchInsights" => Service::CloudWatchInsights,
"CloudWatchAlarms" => Service::CloudWatchAlarms,
"S3Buckets" => Service::S3Buckets,
"CloudTrailEvents" => Service::CloudTrailEvents,
"SqsQueues" => Service::SqsQueues,
_ => Service::CloudWatchLogGroups,
},
title: st.title.clone(),
breadcrumb: st.breadcrumb.clone(),
})
.collect();
if !self.tabs.is_empty() {
self.current_tab = 0;
self.current_service = self.tabs[0].service;
self.service_selected = true;
}
self.mode = Mode::Normal;
}
} else if self.mode == Mode::InsightsInput {
use crate::app::InsightsFocus;
match self.insights_state.insights.insights_focus {
InsightsFocus::Query => {
self.insights_state.insights.query_text.push('\n');
self.insights_state.insights.query_cursor_line += 1;
self.insights_state.insights.query_cursor_col = 0;
}
InsightsFocus::LogGroupSearch => {
self.insights_state.insights.show_dropdown =
!self.insights_state.insights.show_dropdown;
}
_ => {}
}
} else if self.mode == Mode::Normal {
if !self.service_selected {
let filtered = self.filtered_services();
if let Some(&service) = filtered.get(self.service_picker.selected) {
match service {
"CloudWatch › Log Groups" => {
self.current_service = Service::CloudWatchLogGroups;
self.view_mode = ViewMode::List;
self.service_selected = true;
}
"CloudWatch › Logs Insights" => {
self.current_service = Service::CloudWatchInsights;
self.view_mode = ViewMode::InsightsResults;
self.service_selected = true;
}
"CloudWatch › Alarms" => {
self.current_service = Service::CloudWatchAlarms;
self.view_mode = ViewMode::List;
self.service_selected = true;
}
"S3 › Buckets" => {
self.current_service = Service::S3Buckets;
self.view_mode = ViewMode::List;
self.service_selected = true;
}
"EC2 › Instances" => {
self.current_service = Service::Ec2Instances;
self.view_mode = ViewMode::List;
self.service_selected = true;
}
"ECR › Repositories" => {
self.current_service = Service::EcrRepositories;
self.view_mode = ViewMode::List;
self.service_selected = true;
}
"Lambda › Functions" => {
self.current_service = Service::LambdaFunctions;
self.view_mode = ViewMode::List;
self.service_selected = true;
}
"Lambda › Applications" => {
self.current_service = Service::LambdaApplications;
self.view_mode = ViewMode::List;
self.service_selected = true;
}
_ => {}
}
}
return;
}
if self.view_mode == ViewMode::InsightsResults {
if self.insights_state.insights.expanded_result
== Some(self.insights_state.insights.results_selected)
{
self.insights_state.insights.expanded_result = None;
} else {
self.insights_state.insights.expanded_result =
Some(self.insights_state.insights.results_selected);
}
} else if self.current_service == Service::S3Buckets {
if self.s3_state.current_bucket.is_none() {
let filtered_buckets: Vec<_> = self
.s3_state
.buckets
.items
.iter()
.filter(|b| {
if self.s3_state.buckets.filter.is_empty() {
true
} else {
b.name
.to_lowercase()
.contains(&self.s3_state.buckets.filter.to_lowercase())
}
})
.collect();
let mut row_idx = 0;
for bucket in filtered_buckets {
if row_idx == self.s3_state.selected_row {
self.s3_state.current_bucket = Some(bucket.name.clone());
self.s3_state.prefix_stack.clear();
self.s3_state.buckets.loading = true;
return;
}
row_idx += 1;
if self.s3_state.bucket_errors.contains_key(&bucket.name)
&& self.s3_state.expanded_prefixes.contains(&bucket.name)
{
continue;
}
if self.s3_state.expanded_prefixes.contains(&bucket.name) {
if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
for obj in preview {
if row_idx == self.s3_state.selected_row {
if obj.is_prefix {
self.s3_state.current_bucket =
Some(bucket.name.clone());
self.s3_state.prefix_stack = vec![obj.key.clone()];
self.s3_state.buckets.loading = true;
}
return;
}
row_idx += 1;
if obj.is_prefix
&& self.s3_state.expanded_prefixes.contains(&obj.key)
{
if let Some(nested) =
self.s3_state.prefix_preview.get(&obj.key)
{
for nested_obj in nested {
if row_idx == self.s3_state.selected_row {
if nested_obj.is_prefix {
self.s3_state.current_bucket =
Some(bucket.name.clone());
self.s3_state.prefix_stack = vec![
obj.key.clone(),
nested_obj.key.clone(),
];
self.s3_state.buckets.loading = true;
}
return;
}
row_idx += 1;
}
} else {
row_idx += 1;
}
}
}
} else {
row_idx += 1;
}
}
}
} else {
let mut visual_idx = 0;
let mut found_obj: Option<S3Object> = None;
fn check_nested_select(
obj: &S3Object,
visual_idx: &mut usize,
target_idx: usize,
expanded_prefixes: &std::collections::HashSet<String>,
prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
found_obj: &mut Option<S3Object>,
) {
if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
if let Some(preview) = prefix_preview.get(&obj.key) {
for nested_obj in preview {
if *visual_idx == target_idx {
*found_obj = Some(nested_obj.clone());
return;
}
*visual_idx += 1;
check_nested_select(
nested_obj,
visual_idx,
target_idx,
expanded_prefixes,
prefix_preview,
found_obj,
);
if found_obj.is_some() {
return;
}
}
} else {
*visual_idx += 1;
}
}
}
for obj in &self.s3_state.objects {
if visual_idx == self.s3_state.selected_object {
found_obj = Some(obj.clone());
break;
}
visual_idx += 1;
check_nested_select(
obj,
&mut visual_idx,
self.s3_state.selected_object,
&self.s3_state.expanded_prefixes,
&self.s3_state.prefix_preview,
&mut found_obj,
);
if found_obj.is_some() {
break;
}
}
if let Some(obj) = found_obj {
if obj.is_prefix {
self.s3_state.prefix_stack.push(obj.key.clone());
self.s3_state.buckets.loading = true;
}
}
}
} else if self.current_service == Service::ApiGatewayApis {
if self.apig_state.current_api.is_none() {
let filtered_apis = crate::ui::apig::filtered_apis(self);
if let Some(api) = self.apig_state.apis.get_selected(&filtered_apis) {
let protocol = api.protocol_type.to_uppercase();
self.apig_state.current_api = Some((*api).clone());
if protocol == "REST" {
self.apig_state.resources.loading = true;
} else {
self.apig_state.routes.loading = true;
}
self.update_current_tab_breadcrumb();
}
}
} else if self.current_service == Service::CloudFormationStacks {
if self.cfn_state.current_stack.is_none() {
let filtered_stacks = filtered_cloudformation_stacks(self);
if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
let stack_name = stack.name.clone();
let mut tags = stack.tags.clone();
tags.sort_by(|a, b| a.0.cmp(&b.0));
self.cfn_state.current_stack = Some(stack_name);
self.cfn_state.tags.items = tags;
self.cfn_state.tags.reset();
self.cfn_state.table.loading = true;
self.update_current_tab_breadcrumb();
}
}
} else if self.current_service == Service::CloudWatchAlarms {
if self.alarms_state.current_alarm.is_none() {
let filtered_alarms: Vec<_> = self.alarms_state.table.items.iter().collect();
if let Some(alarm) = self.alarms_state.table.get_selected(&filtered_alarms) {
self.alarms_state.current_alarm = Some(alarm.name.clone());
self.alarms_state.metrics_loading = true;
self.view_mode = ViewMode::Detail;
self.update_current_tab_breadcrumb();
}
}
} else if self.current_service == Service::CloudTrailEvents {
if self.cloudtrail_state.current_event.is_none() {
let filtered_events: Vec<_> =
self.cloudtrail_state.table.items.iter().collect();
if let Some(event) = self.cloudtrail_state.table.get_selected(&filtered_events)
{
self.cloudtrail_state.current_event = Some((*event).clone());
self.cloudtrail_state.event_json_scroll = 0;
self.update_current_tab_breadcrumb();
}
}
} else if self.current_service == Service::EcrRepositories {
crate::ecr::actions::select_item(self);
} else if self.current_service == Service::Ec2Instances {
crate::ec2::actions::select_item(self);
} else if self.current_service == Service::SqsQueues {
if self.sqs_state.current_queue.is_none() {
let filtered_queues = filtered_queues(
&self.sqs_state.queues.items,
&self.sqs_state.queues.filter,
);
if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
self.sqs_state.current_queue = Some(queue.url.clone());
if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
self.sqs_state.metrics_loading = true;
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
self.sqs_state.triggers.loading = true;
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
self.sqs_state.pipes.loading = true;
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
self.sqs_state.tags.loading = true;
} else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
self.sqs_state.subscriptions.loading = true;
}
}
}
} else if self.current_service == Service::IamUsers {
if self.iam_state.current_user.is_some() {
if self.iam_state.user_tab == UserTab::Permissions {
let filtered = filtered_iam_policies(self);
if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
self.iam_state.current_policy = Some(policy.policy_name.clone());
self.iam_state.policy_scroll = 0;
self.view_mode = ViewMode::PolicyView;
self.iam_state.policies.loading = true;
self.update_current_tab_breadcrumb();
}
}
} else if self.iam_state.current_user.is_none() {
let filtered_users = filtered_iam_users(self);
if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
self.iam_state.current_user = Some(user.user_name.clone());
self.iam_state.user_tab = UserTab::Permissions;
self.iam_state.policies.reset();
self.update_current_tab_breadcrumb();
}
}
} else if self.current_service == Service::IamRoles {
if self.iam_state.current_role.is_some() {
if self.iam_state.role_tab == RoleTab::Permissions {
let filtered = filtered_iam_policies(self);
if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
self.iam_state.current_policy = Some(policy.policy_name.clone());
self.iam_state.policy_scroll = 0;
self.view_mode = ViewMode::PolicyView;
self.iam_state.policies.loading = true;
self.update_current_tab_breadcrumb();
}
}
} else if self.iam_state.current_role.is_none() {
let filtered_roles = filtered_iam_roles(self);
if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
self.iam_state.current_role = Some(role.role_name.clone());
self.iam_state.role_tab = RoleTab::Permissions;
self.iam_state.policies.reset();
self.update_current_tab_breadcrumb();
}
}
} else if self.current_service == Service::IamUserGroups {
if self.iam_state.current_group.is_none() {
let filtered_groups: Vec<_> = self
.iam_state
.groups
.items
.iter()
.filter(|g| {
if self.iam_state.groups.filter.is_empty() {
true
} else {
g.group_name
.to_lowercase()
.contains(&self.iam_state.groups.filter.to_lowercase())
}
})
.collect();
if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
self.iam_state.current_group = Some(group.group_name.clone());
self.update_current_tab_breadcrumb();
}
}
} else if self.current_service == Service::LambdaFunctions {
crate::lambda::functions::select_item(self);
} else if self.current_service == Service::LambdaApplications {
crate::lambda::applications::select_item(self);
} else if self.current_service == Service::CloudWatchLogGroups {
if self.view_mode == ViewMode::List {
let filtered_groups = filtered_log_groups(self);
if let Some(selected_group) =
filtered_groups.get(self.log_groups_state.log_groups.selected)
{
if let Some(actual_idx) = self
.log_groups_state
.log_groups
.items
.iter()
.position(|g| g.name == selected_group.name)
{
self.log_groups_state.log_groups.selected = actual_idx;
}
}
self.view_mode = ViewMode::Detail;
self.log_groups_state.log_streams.clear();
self.log_groups_state.tags.items.clear();
self.log_groups_state.tags.reset();
self.log_groups_state.selected_stream = 0;
self.log_groups_state.loading = true;
self.column_selector_index = 0;
self.update_current_tab_breadcrumb();
} else if self.view_mode == ViewMode::Detail {
let filtered_streams = filtered_log_streams(self);
if let Some(selected_stream) =
filtered_streams.get(self.log_groups_state.selected_stream)
{
if let Some(actual_idx) = self
.log_groups_state
.log_streams
.iter()
.position(|s| s.name == selected_stream.name)
{
self.log_groups_state.selected_stream = actual_idx;
}
}
self.view_mode = ViewMode::Events;
self.update_current_tab_breadcrumb();
self.log_groups_state.log_events.clear();
self.log_groups_state.event_scroll_offset = 0;
self.log_groups_state.next_backward_token = None;
self.log_groups_state.loading = true;
} else if self.view_mode == ViewMode::Events {
if self.log_groups_state.expanded_event
== Some(self.log_groups_state.event_scroll_offset)
{
self.log_groups_state.expanded_event = None;
} else {
self.log_groups_state.expanded_event =
Some(self.log_groups_state.event_scroll_offset);
}
}
} else if self.current_service == Service::CloudWatchAlarms
&& self.view_mode != ViewMode::Detail
{
self.alarms_state.table.toggle_expand();
} else if self.current_service == Service::CloudWatchInsights {
if !self.insights_state.insights.selected_log_groups.is_empty() {
self.log_groups_state.loading = true;
self.insights_state.insights.query_completed = true;
}
}
}
}
pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
Ok(())
}
pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
let alarms = self.alarms_client.list_alarms().await?;
self.alarms_state.table.items = alarms
.into_iter()
.map(
|(
name,
state,
state_updated,
description,
metric_name,
namespace,
statistic,
period,
comparison,
threshold,
actions_enabled,
state_reason,
resource,
dimensions,
expression,
alarm_type,
cross_account,
)| Alarm {
name,
state,
state_updated_timestamp: state_updated,
description,
metric_name,
namespace,
statistic,
period,
comparison_operator: comparison,
threshold,
actions_enabled,
state_reason,
resource,
dimensions,
expression,
alarm_type,
cross_account,
},
)
.collect();
Ok(())
}
pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
let (events, next_token) = self.cloudtrail_client.lookup_events(None, None).await?;
self.cloudtrail_state.table.items = events
.into_iter()
.map(
|(
event_name,
event_time,
username,
event_source,
resource_type,
resource_name,
read_only,
aws_region,
event_id,
access_key_id,
source_ip_address,
error_code,
request_id,
event_type,
cloud_trail_event_json,
)| CloudTrailEvent {
event_name,
event_time,
username,
event_source,
resource_type,
resource_name,
read_only,
aws_region,
event_id,
access_key_id,
source_ip_address,
error_code,
request_id,
event_type,
cloud_trail_event_json,
},
)
.collect();
self.cloudtrail_state.table.next_token = next_token;
Ok(())
}
pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
let (events, next_token) = self
.cloudtrail_client
.lookup_events(None, Some(token))
.await?;
self.cloudtrail_state
.table
.items
.extend(events.into_iter().map(
|(
event_name,
event_time,
username,
event_source,
resource_type,
resource_name,
read_only,
aws_region,
event_id,
access_key_id,
source_ip_address,
error_code,
request_id,
event_type,
cloud_trail_event_json,
)| CloudTrailEvent {
event_name,
event_time,
username,
event_source,
resource_type,
resource_name,
read_only,
aws_region,
event_id,
access_key_id,
source_ip_address,
error_code,
request_id,
event_type,
cloud_trail_event_json,
},
));
self.cloudtrail_state.table.next_token = next_token;
}
Ok(())
}
pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
if let Some(bucket_name) = &self.s3_state.current_bucket {
let bucket_region = if let Some(bucket) = self
.s3_state
.buckets
.items
.iter_mut()
.find(|b| &b.name == bucket_name)
{
if bucket.region.is_empty() {
let region = self.s3_client.get_bucket_location(bucket_name).await?;
bucket.region = region.clone();
region
} else {
bucket.region.clone()
}
} else {
self.config.region.clone()
};
let prefix = self
.s3_state
.prefix_stack
.last()
.cloned()
.unwrap_or_default();
let objects = self
.s3_client
.list_objects(bucket_name, &bucket_region, &prefix)
.await?;
self.s3_state.objects = objects
.into_iter()
.map(|(key, size, modified, is_prefix, storage_class)| S3Object {
key,
size,
last_modified: modified,
is_prefix,
storage_class,
})
.collect();
self.s3_state.selected_object = 0;
}
Ok(())
}
pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
let stored_region = self
.s3_state
.buckets
.items
.iter()
.find(|b| b.name == bucket_name)
.and_then(|b| {
if b.region.is_empty() {
None
} else {
Some(b.region.clone())
}
});
let bucket_region = match stored_region {
Some(r) => r,
None => {
let r = self.s3_client.get_bucket_location(&bucket_name).await?;
if let Some(b) = self
.s3_state
.buckets
.items
.iter_mut()
.find(|b| b.name == bucket_name)
{
b.region = r.clone();
}
r
}
};
let objects = self
.s3_client
.list_objects(&bucket_name, &bucket_region, "")
.await?;
let preview: Vec<S3Object> = objects
.into_iter()
.map(|(key, size, modified, is_prefix, storage_class)| S3Object {
key,
size,
last_modified: modified,
is_prefix,
storage_class,
})
.collect();
self.s3_state
.bucket_preview
.insert(bucket_name.clone(), preview);
self.after_bucket_preview_loaded(&bucket_name);
Ok(())
}
pub fn after_bucket_preview_loaded(&mut self, bucket_name: &str) {
if self.s3_state.current_bucket.is_some() {
return; }
let mut row_idx = 0usize;
for b in &self.s3_state.buckets.items {
if b.name == bucket_name {
if self.s3_state.selected_row == row_idx {
if let Some(preview) = self.s3_state.bucket_preview.get(bucket_name) {
if !preview.is_empty() {
self.s3_state.selected_row = row_idx + 1;
let visible = self.s3_state.bucket_visible_rows.get();
if self.s3_state.selected_row
>= self.s3_state.bucket_scroll_offset + visible
{
self.s3_state.bucket_scroll_offset =
self.s3_state.selected_row.saturating_sub(visible - 1);
}
}
}
}
return;
}
row_idx += 1;
if self.s3_state.expanded_prefixes.contains(&b.name) {
if let Some(p) = self.s3_state.bucket_preview.get(&b.name) {
row_idx += p.len();
}
}
}
}
pub async fn load_prefix_preview(
&mut self,
bucket_name: String,
prefix: String,
) -> anyhow::Result<()> {
let stored_region = self
.s3_state
.buckets
.items
.iter()
.find(|b| b.name == bucket_name)
.and_then(|b| {
if b.region.is_empty() {
None
} else {
Some(b.region.clone())
}
});
let bucket_region = match stored_region {
Some(r) => r,
None => {
let r = self.s3_client.get_bucket_location(&bucket_name).await?;
if let Some(b) = self
.s3_state
.buckets
.items
.iter_mut()
.find(|b| b.name == bucket_name)
{
b.region = r.clone();
}
r
}
};
let objects = self
.s3_client
.list_objects(&bucket_name, &bucket_region, &prefix)
.await?;
let preview: Vec<S3Object> = objects
.into_iter()
.map(|(key, size, modified, is_prefix, storage_class)| S3Object {
key,
size,
last_modified: modified,
is_prefix,
storage_class,
})
.collect();
self.s3_state.prefix_preview.insert(prefix, preview);
Ok(())
}
pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
let repos = match self.ecr_state.tab {
EcrTab::Private => self.ecr_client.list_private_repositories().await?,
EcrTab::Public => self.ecr_client.list_public_repositories().await?,
};
self.ecr_state.repositories.items = repos
.into_iter()
.map(|r| EcrRepository {
name: r.name,
uri: r.uri,
created_at: r.created_at,
tag_immutability: r.tag_immutability,
encryption_type: r.encryption_type,
})
.collect();
self.ecr_state
.repositories
.items
.sort_by(|a, b| a.name.cmp(&b.name));
Ok(())
}
pub async fn load_apis(&mut self) -> anyhow::Result<()> {
let apis = self.apig_client.list_rest_apis().await?;
self.apig_state.apis.items = apis
.into_iter()
.map(|a| crate::apig::api::RestApi {
id: a.id,
name: a.name,
description: a.description,
created_date: a.created_date,
api_key_source: a.api_key_source,
endpoint_configuration: a.endpoint_configuration,
protocol_type: a.protocol_type,
disable_execute_api_endpoint: a.disable_execute_api_endpoint,
status: a.status,
})
.collect();
self.apig_state
.apis
.items
.sort_by(|a, b| a.name.cmp(&b.name));
Ok(())
}
pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
let instances = self.ec2_client.list_instances().await?;
self.ec2_state.table.items = instances
.into_iter()
.map(|i| Ec2Instance {
instance_id: i.instance_id,
name: i.name,
state: i.state,
instance_type: i.instance_type,
availability_zone: i.availability_zone,
public_ipv4_dns: i.public_ipv4_dns,
public_ipv4_address: i.public_ipv4_address,
elastic_ip: i.elastic_ip,
ipv6_ips: i.ipv6_ips,
monitoring: i.monitoring,
security_groups: i.security_groups,
key_name: i.key_name,
launch_time: i.launch_time,
platform_details: i.platform_details,
status_checks: i.status_checks,
alarm_status: i.alarm_status,
private_dns_name: i.private_dns_name,
private_ip_address: i.private_ip_address,
security_group_ids: String::new(),
owner_id: String::new(),
volume_id: String::new(),
root_device_name: String::new(),
root_device_type: String::new(),
ebs_optimized: String::new(),
image_id: String::new(),
kernel_id: String::new(),
ramdisk_id: String::new(),
ami_launch_index: String::new(),
reservation_id: String::new(),
vpc_id: String::new(),
subnet_ids: String::new(),
instance_lifecycle: String::new(),
architecture: String::new(),
virtualization_type: String::new(),
platform: String::new(),
iam_instance_profile_arn: String::new(),
tenancy: String::new(),
affinity: String::new(),
host_id: String::new(),
placement_group: String::new(),
partition_number: String::new(),
capacity_reservation_id: String::new(),
state_transition_reason_code: String::new(),
state_transition_reason_message: String::new(),
stop_hibernation_behavior: String::new(),
outpost_arn: String::new(),
product_codes: String::new(),
availability_zone_id: String::new(),
imdsv2: String::new(),
usage_operation: String::new(),
managed: String::new(),
operator: String::new(),
})
.collect();
self.ec2_state
.table
.items
.sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
Ok(())
}
pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
if let Some(repo_name) = &self.ecr_state.current_repository {
if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
self.ecr_state.images.items = images
.into_iter()
.map(|i| EcrImage {
tag: i.tag,
artifact_type: i.artifact_type,
pushed_at: i.pushed_at,
size_bytes: i.size_bytes,
uri: i.uri,
digest: i.digest,
last_pull_time: i.last_pull_time,
})
.collect();
self.ecr_state
.images
.items
.sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
}
}
Ok(())
}
pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
let stacks = self
.cloudformation_client
.list_stacks(self.cfn_state.view_nested)
.await?;
let mut stacks: Vec<CfnStack> = stacks
.into_iter()
.map(|s| CfnStack {
name: s.name,
stack_id: s.stack_id,
status: s.status,
created_time: s.created_time,
updated_time: s.updated_time,
deleted_time: s.deleted_time,
drift_status: s.drift_status,
last_drift_check_time: s.last_drift_check_time,
status_reason: s.status_reason,
description: s.description,
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
})
.collect();
stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
self.cfn_state.table.items = stacks;
Ok(())
}
pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
let template = self.cloudformation_client.get_template(stack_name).await?;
self.cfn_state.template_body = template;
self.cfn_state.template_scroll = 0;
Ok(())
}
pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
let mut parameters = self
.cloudformation_client
.get_stack_parameters(stack_name)
.await?;
parameters.sort_by(|a, b| a.key.cmp(&b.key));
self.cfn_state.parameters.items = parameters;
self.cfn_state.parameters.reset();
Ok(())
}
pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
let outputs = self
.cloudformation_client
.get_stack_outputs(stack_name)
.await?;
self.cfn_state.outputs.items = outputs;
self.cfn_state.outputs.reset();
Ok(())
}
pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
let resources = self
.cloudformation_client
.get_stack_resources(stack_name)
.await?;
self.cfn_state.resources.items = resources;
self.cfn_state.resources.reset();
Ok(())
}
pub async fn load_cfn_events(&mut self, stack_name: &str) -> anyhow::Result<()> {
let events = self
.cloudformation_client
.list_stack_events(stack_name)
.await?;
self.cfn_state.events.items = events;
self.cfn_state.events.reset();
Ok(())
}
pub async fn load_cfn_change_sets(&mut self, stack_name: &str) -> anyhow::Result<()> {
let change_sets = self
.cloudformation_client
.list_change_sets(stack_name)
.await?;
self.cfn_state.change_sets.items = change_sets;
self.cfn_state.change_sets.reset();
Ok(())
}
pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
let attached_policies = self
.iam_client
.list_attached_role_policies(role_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let mut policies: Vec<IamPolicy> = Vec::new();
for p in attached_policies {
let (creation_time, edited_time) = if let Some(arn) = p.policy_arn() {
self.iam_client
.get_policy_metadata(arn)
.await
.unwrap_or_else(|_| ("-".to_string(), "-".to_string()))
} else {
("-".to_string(), "-".to_string())
};
policies.push(IamPolicy {
policy_name: p.policy_name().unwrap_or("").to_string(),
policy_type: "Managed".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "-".to_string(),
description: "-".to_string(),
creation_time,
edited_time,
policy_arn: p.policy_arn().map(|s| s.to_string()),
});
}
let inline_policy_names = self
.iam_client
.list_role_policies(role_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
for policy_name in inline_policy_names {
policies.push(IamPolicy {
policy_name,
policy_type: "Inline".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "-".to_string(),
description: "-".to_string(),
creation_time: "-".to_string(),
edited_time: "-".to_string(),
policy_arn: None,
});
}
self.iam_state.policies.items = policies;
Ok(())
}
pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
let attached_policies = self
.iam_client
.list_attached_group_policies(group_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let mut policies: Vec<IamPolicy> = attached_policies
.into_iter()
.map(|p| IamPolicy {
policy_name: p.policy_name().unwrap_or("").to_string(),
policy_type: "AWS managed".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "-".to_string(),
description: "-".to_string(),
creation_time: "-".to_string(),
edited_time: "-".to_string(),
policy_arn: p.policy_arn().map(|s| s.to_string()),
})
.collect();
let inline_policy_names = self
.iam_client
.list_group_policies(group_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
for policy_name in inline_policy_names {
policies.push(IamPolicy {
policy_name,
policy_type: "Inline".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "-".to_string(),
description: "-".to_string(),
creation_time: "-".to_string(),
edited_time: "-".to_string(),
policy_arn: None,
});
}
self.iam_state.policies.items = policies;
Ok(())
}
pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
let users = self
.iam_client
.get_group_users(group_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
let group_users: Vec<IamGroupUser> = users
.into_iter()
.map(|u| {
let creation_time = {
let dt = u.create_date();
let timestamp = dt.secs();
let datetime =
chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
};
IamGroupUser {
user_name: u.user_name().to_string(),
groups: String::new(),
last_activity: String::new(),
creation_time,
}
})
.collect();
self.iam_state.group_users.items = group_users;
Ok(())
}
pub async fn load_policy_document(
&mut self,
role_name: &str,
policy_name: &str,
) -> anyhow::Result<()> {
let policy = self
.iam_state
.policies
.items
.iter()
.find(|p| p.policy_name == policy_name)
.ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
let document = if let Some(policy_arn) = &policy.policy_arn {
self.iam_client
.get_policy_version(policy_arn)
.await
.map_err(|e| anyhow::anyhow!(e))?
} else {
self.iam_client
.get_role_policy(role_name, policy_name)
.await
.map_err(|e| anyhow::anyhow!(e))?
};
self.iam_state.policy_document = document;
Ok(())
}
pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
let document = self
.iam_client
.get_role(role_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
self.iam_state.trust_policy_document = document;
Ok(())
}
pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
self.iam_state.last_accessed_services.items = vec![];
self.iam_state.last_accessed_services.selected = 0;
Ok(())
}
pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
let tags = self
.iam_client
.list_role_tags(role_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
self.iam_state.tags.items = tags
.into_iter()
.map(|(k, v)| IamRoleTag { key: k, value: v })
.collect();
self.iam_state.tags.reset();
Ok(())
}
pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
let tags = self
.iam_client
.list_user_tags(user_name)
.await
.map_err(|e| anyhow::anyhow!(e))?;
self.iam_state.user_tags.items = tags
.into_iter()
.map(|(k, v)| IamUserTag { key: k, value: v })
.collect();
self.iam_state.user_tags.reset();
Ok(())
}
pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
if let Some(group) = self
.log_groups_state
.log_groups
.items
.get(self.log_groups_state.log_groups.selected)
{
self.log_groups_state.log_streams =
self.cloudwatch_client.list_log_streams(&group.name).await?;
self.log_groups_state.selected_stream = 0;
}
Ok(())
}
pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
if let Some(group) = self
.log_groups_state
.log_groups
.items
.get(self.log_groups_state.log_groups.selected)
{
let arn = if let Some(arn) = &group.log_group_arn {
arn.clone()
} else if let Some(arn) = &group.arn {
arn.clone()
} else {
let account_id = if self.config.account_id.is_empty() {
"*"
} else {
&self.config.account_id
};
format!(
"arn:aws:logs:{}:{}:log-group:{}",
self.config.region, account_id, group.name
)
};
let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
self.log_groups_state.tags.items = tags;
self.log_groups_state.tags.selected = 0;
self.log_groups_state.tags.scroll_offset = 0;
}
Ok(())
}
pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
if let Some(group) = self
.log_groups_state
.log_groups
.items
.get(self.log_groups_state.log_groups.selected)
{
if let Some(stream) = self
.log_groups_state
.log_streams
.get(self.log_groups_state.selected_stream)
{
let (start_time, end_time) =
if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
let now = chrono::Utc::now().timestamp_millis();
let duration_ms = match self.log_groups_state.relative_unit {
TimeUnit::Minutes => amount * 60 * 1000,
TimeUnit::Hours => amount * 60 * 60 * 1000,
TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
};
(Some(now - duration_ms), Some(now))
} else {
(None, None)
};
let (mut events, has_more, token) = self
.cloudwatch_client
.get_log_events(
&group.name,
&stream.name,
self.log_groups_state.next_backward_token.clone(),
start_time,
end_time,
)
.await?;
if self.log_groups_state.next_backward_token.is_some() {
events.append(&mut self.log_groups_state.log_events);
self.log_groups_state.event_scroll_offset = 0;
} else {
self.log_groups_state.event_scroll_offset = 0;
}
self.log_groups_state.log_events = events;
self.log_groups_state.has_older_events =
has_more && self.log_groups_state.log_events.len() >= 25;
self.log_groups_state.next_backward_token = token;
self.log_groups_state.selected_event = 0;
}
}
Ok(())
}
pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
if self.insights_state.insights.selected_log_groups.is_empty() {
return Err(anyhow::anyhow!(
"No log groups selected. Please select at least one log group."
));
}
let now = chrono::Utc::now().timestamp_millis();
let amount = self
.insights_state
.insights
.insights_relative_amount
.parse::<i64>()
.unwrap_or(1);
let duration_ms = match self.insights_state.insights.insights_relative_unit {
TimeUnit::Minutes => amount * 60 * 1000,
TimeUnit::Hours => amount * 60 * 60 * 1000,
TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
};
let start_time = now - duration_ms;
let query_id = self
.cloudwatch_client
.start_query(
self.insights_state.insights.selected_log_groups.clone(),
self.insights_state.insights.query_text.trim().to_string(),
start_time,
now,
)
.await?;
for _ in 0..60 {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
if status == "Complete" {
self.insights_state.insights.query_results = results;
self.insights_state.insights.query_completed = true;
self.insights_state.insights.results_selected = 0;
self.insights_state.insights.expanded_result = None;
self.view_mode = ViewMode::InsightsResults;
return Ok(());
} else if status == "Failed" || status == "Cancelled" {
return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
}
}
Err(anyhow::anyhow!("Query timeout"))
}
pub(crate) fn toggle_column_visibility<T: PartialEq + Copy>(
visible_columns: &mut Vec<T>,
_all_columns: &[T],
column_to_toggle: T,
) {
if let Some(pos) = visible_columns.iter().position(|c| c == &column_to_toggle) {
if visible_columns.len() > 1 {
visible_columns.remove(pos);
}
} else {
visible_columns.push(column_to_toggle);
}
}
}
impl CloudWatchInsightsState {
fn new() -> Self {
Self {
insights: InsightsState::default(),
loading: false,
}
}
}
impl CloudWatchAlarmsState {
fn new() -> Self {
Self {
table: TableState::new(),
current_alarm: None,
alarm_tab: AlarmTab::AllAlarms,
view_as: AlarmViewMode::Table,
wrap_lines: false,
sort_column: "Last state update".to_string(),
sort_direction: SortDirection::Asc,
input_focus: InputFocus::Filter,
metric_data: Vec::new(),
metrics_loading: false,
}
}
}
impl ServicePickerState {
fn new() -> Self {
Self {
filter: String::new(),
filter_active: false,
selected: 0,
services: vec![
"API Gateway › APIs",
"CloudWatch › Log Groups",
"CloudWatch › Logs Insights",
"CloudWatch › Alarms",
"CloudTrail › Event History",
"CloudFormation › Stacks",
"EC2 › Instances",
"ECR › Repositories",
"IAM › Users",
"IAM › Roles",
"IAM › User Groups",
"Lambda › Functions",
"Lambda › Applications",
"S3 › Buckets",
"SQS › Queues",
],
}
}
}
#[cfg(test)]
mod test_helpers {
use super::*;
pub fn test_app() -> App {
App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
}
pub fn test_app_no_region() -> App {
App::new_without_client("test".to_string(), None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keymap::Action;
use test_helpers::*;
#[test]
fn test_next_tab_cycles_forward() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
},
Tab {
service: Service::CloudWatchInsights,
title: "CloudWatch › Logs Insights".to_string(),
breadcrumb: "CloudWatch › Logs Insights".to_string(),
},
Tab {
service: Service::CloudWatchAlarms,
title: "CloudWatch › Alarms".to_string(),
breadcrumb: "CloudWatch › Alarms".to_string(),
},
];
app.current_tab = 0;
app.handle_action(Action::NextTab);
assert_eq!(app.current_tab, 1);
assert_eq!(app.current_service, Service::CloudWatchInsights);
app.handle_action(Action::NextTab);
assert_eq!(app.current_tab, 2);
assert_eq!(app.current_service, Service::CloudWatchAlarms);
app.handle_action(Action::NextTab);
assert_eq!(app.current_tab, 0);
assert_eq!(app.current_service, Service::CloudWatchLogGroups);
}
#[test]
fn test_prev_tab_cycles_backward() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
},
Tab {
service: Service::CloudWatchInsights,
title: "CloudWatch › Logs Insights".to_string(),
breadcrumb: "CloudWatch › Logs Insights".to_string(),
},
Tab {
service: Service::CloudWatchAlarms,
title: "CloudWatch › Alarms".to_string(),
breadcrumb: "CloudWatch › Alarms".to_string(),
},
];
app.current_tab = 2;
app.handle_action(Action::PrevTab);
assert_eq!(app.current_tab, 1);
assert_eq!(app.current_service, Service::CloudWatchInsights);
app.handle_action(Action::PrevTab);
assert_eq!(app.current_tab, 0);
assert_eq!(app.current_service, Service::CloudWatchLogGroups);
app.handle_action(Action::PrevTab);
assert_eq!(app.current_tab, 2);
assert_eq!(app.current_service, Service::CloudWatchAlarms);
}
#[test]
fn test_close_tab_removes_current() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
},
Tab {
service: Service::CloudWatchInsights,
title: "CloudWatch › Logs Insights".to_string(),
breadcrumb: "CloudWatch › Logs Insights".to_string(),
},
Tab {
service: Service::CloudWatchAlarms,
title: "CloudWatch › Alarms".to_string(),
breadcrumb: "CloudWatch › Alarms".to_string(),
},
];
app.current_tab = 1;
app.service_selected = true;
app.handle_action(Action::CloseTab);
assert_eq!(app.tabs.len(), 2);
assert_eq!(app.current_tab, 1);
assert_eq!(app.current_service, Service::CloudWatchAlarms);
}
#[test]
fn test_close_last_tab_exits_service() {
let mut app = test_app();
app.tabs = vec![Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
}];
app.current_tab = 0;
app.service_selected = true;
app.handle_action(Action::CloseTab);
assert_eq!(app.tabs.len(), 0);
assert!(!app.service_selected);
assert_eq!(app.current_tab, 0);
}
#[test]
fn test_close_service_removes_current_tab() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
},
Tab {
service: Service::CloudWatchInsights,
title: "CloudWatch › Logs Insights".to_string(),
breadcrumb: "CloudWatch › Logs Insights".to_string(),
},
Tab {
service: Service::CloudWatchAlarms,
title: "CloudWatch › Alarms".to_string(),
breadcrumb: "CloudWatch › Alarms".to_string(),
},
];
app.current_tab = 1;
app.service_selected = true;
app.handle_action(Action::CloseService);
assert_eq!(app.tabs.len(), 2);
assert_eq!(app.current_tab, 1);
assert_eq!(app.current_service, Service::CloudWatchAlarms);
assert!(app.service_selected);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_close_service_last_tab_shows_picker() {
let mut app = test_app();
app.tabs = vec![Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
}];
app.current_tab = 0;
app.service_selected = true;
app.handle_action(Action::CloseService);
assert_eq!(app.tabs.len(), 0);
assert!(!app.service_selected);
assert_eq!(app.mode, Mode::ServicePicker);
}
#[test]
fn test_open_tab_picker_with_tabs() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
},
Tab {
service: Service::CloudWatchInsights,
title: "CloudWatch › Logs Insights".to_string(),
breadcrumb: "CloudWatch › Logs Insights".to_string(),
},
];
app.current_tab = 1;
app.handle_action(Action::OpenTabPicker);
assert_eq!(app.mode, Mode::TabPicker);
assert_eq!(app.tab_picker_selected, 1);
}
#[test]
fn test_open_tab_picker_without_tabs() {
let mut app = test_app();
app.tabs = vec![];
app.handle_action(Action::OpenTabPicker);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_pending_key_state() {
let mut app = test_app();
assert_eq!(app.pending_key, None);
app.pending_key = Some('g');
assert_eq!(app.pending_key, Some('g'));
}
#[test]
fn test_tab_breadcrumb_updates() {
let mut app = test_app();
app.tabs = vec![Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch › Log Groups".to_string(),
breadcrumb: "CloudWatch > Log groups".to_string(),
}];
app.current_tab = 0;
app.service_selected = true;
app.current_service = Service::CloudWatchLogGroups;
assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
app.log_groups_state
.log_groups
.items
.push(rusticity_core::LogGroup {
name: "/aws/lambda/test".to_string(),
creation_time: None,
stored_bytes: Some(1024),
retention_days: None,
log_class: None,
arn: None,
log_group_arn: None,
deletion_protection_enabled: None,
});
app.log_groups_state.log_groups.reset();
app.view_mode = ViewMode::Detail;
app.update_current_tab_breadcrumb();
assert_eq!(
app.tabs[0].breadcrumb,
"CloudWatch > Log groups > /aws/lambda/test"
);
}
#[test]
fn test_s3_bucket_column_selector_navigation() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
app.handle_action(Action::NextItem);
assert_eq!(app.column_selector_index, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.column_selector_index, 2);
app.handle_action(Action::NextItem);
assert_eq!(app.column_selector_index, 3);
for _ in 0..10 {
app.handle_action(Action::NextItem);
}
assert_eq!(app.column_selector_index, 9);
app.handle_action(Action::PrevItem);
assert_eq!(app.column_selector_index, 8);
app.handle_action(Action::PrevItem);
assert_eq!(app.column_selector_index, 7);
for _ in 0..10 {
app.handle_action(Action::PrevItem);
}
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_cloudwatch_alarms_state_initialized() {
let app = test_app();
assert_eq!(app.alarms_state.table.items.len(), 0);
assert_eq!(app.alarms_state.table.selected, 0);
assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
assert!(!app.alarms_state.table.loading);
assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
}
#[test]
fn test_cloudwatch_alarms_service_selection() {
let mut app = test_app();
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
assert_eq!(app.current_service, Service::CloudWatchAlarms);
assert!(app.service_selected);
}
#[test]
fn test_cloudwatch_alarms_column_preferences() {
let app = test_app();
assert!(!app.cw_alarm_column_ids.is_empty());
assert!(!app.cw_alarm_visible_column_ids.is_empty());
assert!(app
.cw_alarm_visible_column_ids
.contains(&AlarmColumn::Name.id()));
assert!(app
.cw_alarm_visible_column_ids
.contains(&AlarmColumn::State.id()));
}
#[test]
fn test_s3_bucket_navigation_without_expansion() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01T00:00:00Z".to_string(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-02T00:00:00Z".to_string(),
},
S3Bucket {
name: "bucket3".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-03T00:00:00Z".to_string(),
},
];
app.s3_state.selected_row = 0;
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 2);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 2);
app.handle_action(Action::PrevItem);
assert_eq!(app.s3_state.selected_row, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.s3_state.selected_row, 0);
app.handle_action(Action::PrevItem);
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_s3_bucket_navigation_with_expansion() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01T00:00:00Z".to_string(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-02T00:00:00Z".to_string(),
},
];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![
S3Object {
key: "file1.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
S3Object {
key: "folder/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
},
],
);
app.s3_state.selected_row = 0;
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 2);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 3);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 3);
}
#[test]
fn test_s3_bucket_navigation_with_nested_expansion() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01T00:00:00Z".to_string(),
}];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![S3Object {
key: "folder/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.expanded_prefixes.insert("folder/".to_string());
app.s3_state.prefix_preview.insert(
"folder/".to_string(),
vec![
S3Object {
key: "folder/file1.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
S3Object {
key: "folder/file2.txt".to_string(),
size: 200,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
],
);
app.s3_state.selected_row = 0;
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 2);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 3);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 3);
}
#[test]
fn test_calculate_total_bucket_rows() {
let mut app = test_app();
assert_eq!(app.calculate_total_bucket_rows(), 0);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01T00:00:00Z".to_string(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-02T00:00:00Z".to_string(),
},
];
assert_eq!(app.calculate_total_bucket_rows(), 2);
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![
S3Object {
key: "file1.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
S3Object {
key: "file2.txt".to_string(),
size: 200,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
S3Object {
key: "folder/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
},
],
);
assert_eq!(app.calculate_total_bucket_rows(), 5);
app.s3_state.expanded_prefixes.insert("folder/".to_string());
app.s3_state.prefix_preview.insert(
"folder/".to_string(),
vec![
S3Object {
key: "folder/nested1.txt".to_string(),
size: 50,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
S3Object {
key: "folder/nested2.txt".to_string(),
size: 75,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
],
);
assert_eq!(app.calculate_total_bucket_rows(), 7); }
#[test]
fn test_calculate_total_object_rows() {
let mut app = test_app();
app.s3_state.current_bucket = Some("test-bucket".to_string());
assert_eq!(app.calculate_total_object_rows(), 0);
app.s3_state.objects = vec![
S3Object {
key: "file1.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
S3Object {
key: "folder/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
},
];
assert_eq!(app.calculate_total_object_rows(), 2);
app.s3_state.expanded_prefixes.insert("folder/".to_string());
app.s3_state.prefix_preview.insert(
"folder/".to_string(),
vec![
S3Object {
key: "folder/file2.txt".to_string(),
size: 200,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
},
S3Object {
key: "folder/subfolder/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
},
],
);
assert_eq!(app.calculate_total_object_rows(), 4);
app.s3_state
.expanded_prefixes
.insert("folder/subfolder/".to_string());
app.s3_state.prefix_preview.insert(
"folder/subfolder/".to_string(),
vec![S3Object {
key: "folder/subfolder/deep.txt".to_string(),
size: 50,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
}],
);
assert_eq!(app.calculate_total_object_rows(), 5); }
#[test]
fn test_s3_object_navigation_with_deep_nesting() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.objects = vec![S3Object {
key: "folder1/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}];
app.s3_state
.expanded_prefixes
.insert("folder1/".to_string());
app.s3_state.prefix_preview.insert(
"folder1/".to_string(),
vec![S3Object {
key: "folder1/folder2/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state
.expanded_prefixes
.insert("folder1/folder2/".to_string());
app.s3_state.prefix_preview.insert(
"folder1/folder2/".to_string(),
vec![S3Object {
key: "folder1/folder2/file.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: "STANDARD".to_string(),
}],
);
app.s3_state.selected_object = 0;
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_object, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_object, 2);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_object, 2);
}
#[test]
fn test_s3_expand_nested_folder_in_objects_view() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.objects = vec![S3Object {
key: "parent/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}];
app.s3_state.expanded_prefixes.insert("parent/".to_string());
app.s3_state.prefix_preview.insert(
"parent/".to_string(),
vec![S3Object {
key: "parent/child/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.selected_object = 1;
app.handle_action(Action::NextPane);
assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
assert!(app.s3_state.buckets.loading); }
#[test]
fn test_s3_drill_into_nested_folder() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.objects = vec![S3Object {
key: "parent/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}];
app.s3_state.expanded_prefixes.insert("parent/".to_string());
app.s3_state.prefix_preview.insert(
"parent/".to_string(),
vec![S3Object {
key: "parent/child/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.selected_object = 1;
app.handle_action(Action::Select);
assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
assert!(app.s3_state.buckets.loading); }
#[test]
fn test_s3_esc_pops_navigation_stack() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
app.handle_action(Action::GoBack);
assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
assert!(app.s3_state.buckets.loading);
app.s3_state.buckets.loading = false;
app.handle_action(Action::GoBack);
assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
assert!(app.s3_state.buckets.loading);
app.s3_state.buckets.loading = false;
app.handle_action(Action::GoBack);
assert_eq!(app.s3_state.current_bucket, None);
}
#[test]
fn test_s3_esc_from_bucket_root_exits() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.prefix_stack = vec![];
app.handle_action(Action::GoBack);
assert_eq!(app.s3_state.current_bucket, None);
assert_eq!(app.s3_state.objects.len(), 0);
}
#[test]
fn test_s3_drill_into_nested_prefix_from_bucket_list() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "test-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01".to_string(),
}];
app.s3_state
.expanded_prefixes
.insert("test-bucket".to_string());
app.s3_state.bucket_preview.insert(
"test-bucket".to_string(),
vec![S3Object {
key: "parent/".to_string(),
size: 0,
last_modified: "2024-01-01".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.expanded_prefixes.insert("parent/".to_string());
app.s3_state.prefix_preview.insert(
"parent/".to_string(),
vec![S3Object {
key: "parent/child/".to_string(),
size: 0,
last_modified: "2024-01-01".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 2;
app.handle_action(Action::Select);
assert_eq!(
app.s3_state.prefix_stack,
vec!["parent/".to_string(), "parent/child/".to_string()]
);
assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
assert!(app.s3_state.buckets.loading);
app.s3_state.buckets.loading = false;
app.handle_action(Action::GoBack);
assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
assert!(app.s3_state.buckets.loading);
app.s3_state.buckets.loading = false;
app.handle_action(Action::GoBack);
assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
assert!(app.s3_state.buckets.loading);
app.s3_state.buckets.loading = false;
app.handle_action(Action::GoBack);
assert_eq!(app.s3_state.current_bucket, None);
}
#[test]
fn test_region_picker_fuzzy_filter() {
let mut app = test_app();
app.region_latencies.insert("us-east-1".to_string(), 10);
app.region_filter = "vir".to_string();
let filtered = app.get_filtered_regions();
assert!(filtered.iter().any(|r| r.code == "us-east-1"));
}
#[test]
fn test_profile_picker_loads_profiles() {
let profiles = App::load_aws_profiles();
assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
}
#[test]
fn test_profile_with_region_uses_it() {
let mut app = test_app_no_region();
app.available_profiles = vec![AwsProfile {
name: "test-profile".to_string(),
region: Some("eu-west-1".to_string()),
account: Some("123456789".to_string()),
role_arn: None,
source_profile: None,
}];
app.profile_picker_selected = 0;
app.mode = Mode::ProfilePicker;
let filtered = app.get_filtered_profiles();
if let Some(profile) = filtered.first() {
let profile_name = profile.name.clone();
let profile_region = profile.region.clone();
app.profile = profile_name;
if let Some(region) = profile_region {
app.region = region;
}
}
assert_eq!(app.profile, "test-profile");
assert_eq!(app.region, "eu-west-1");
}
#[test]
fn test_profile_without_region_keeps_unknown() {
let mut app = test_app_no_region();
let initial_region = app.region.clone();
app.available_profiles = vec![AwsProfile {
name: "test-profile".to_string(),
region: None,
account: None,
role_arn: None,
source_profile: None,
}];
app.profile_picker_selected = 0;
app.mode = Mode::ProfilePicker;
let filtered = app.get_filtered_profiles();
if let Some(profile) = filtered.first() {
let profile_name = profile.name.clone();
let profile_region = profile.region.clone();
app.profile = profile_name;
if let Some(region) = profile_region {
app.region = region;
}
}
assert_eq!(app.profile, "test-profile");
assert_eq!(app.region, initial_region); }
#[test]
fn test_region_selection_closes_all_tabs() {
let mut app = test_app();
app.tabs.push(Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch".to_string(),
breadcrumb: "CloudWatch".to_string(),
});
app.tabs.push(Tab {
service: Service::S3Buckets,
title: "S3".to_string(),
breadcrumb: "S3".to_string(),
});
app.service_selected = true;
app.current_tab = 1;
app.region_latencies.insert("eu-west-1".to_string(), 50);
app.mode = Mode::RegionPicker;
app.region_picker_selected = 0;
let filtered = app.get_filtered_regions();
if let Some(region) = filtered.first() {
app.region = region.code.to_string();
app.tabs.clear();
app.current_tab = 0;
app.service_selected = false;
app.mode = Mode::Normal;
}
assert_eq!(app.tabs.len(), 0);
assert_eq!(app.current_tab, 0);
assert!(!app.service_selected);
assert_eq!(app.region, "eu-west-1");
}
#[test]
fn test_region_picker_can_be_closed_without_selection() {
let mut app = test_app();
let initial_region = app.region.clone();
app.mode = Mode::RegionPicker;
app.mode = Mode::Normal;
assert_eq!(app.region, initial_region);
}
#[test]
fn test_session_filter_works() {
let mut app = test_app();
app.sessions = vec![
Session {
id: "1".to_string(),
timestamp: "2024-01-01".to_string(),
profile: "prod-profile".to_string(),
region: "us-east-1".to_string(),
account_id: "123456789".to_string(),
role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
tabs: vec![],
},
Session {
id: "2".to_string(),
timestamp: "2024-01-02".to_string(),
profile: "dev-profile".to_string(),
region: "eu-west-1".to_string(),
account_id: "987654321".to_string(),
role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
tabs: vec![],
},
];
app.session_filter = "prod".to_string();
let filtered = app.get_filtered_sessions();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].profile, "prod-profile");
app.session_filter = "eu".to_string();
let filtered = app.get_filtered_sessions();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].region, "eu-west-1");
app.session_filter.clear();
let filtered = app.get_filtered_sessions();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_profile_picker_shows_account() {
let mut app = test_app_no_region();
app.available_profiles = vec![AwsProfile {
name: "test-profile".to_string(),
region: Some("us-east-1".to_string()),
account: Some("123456789".to_string()),
role_arn: None,
source_profile: None,
}];
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].account, Some("123456789".to_string()));
}
#[test]
fn test_profile_without_account() {
let mut app = test_app_no_region();
app.available_profiles = vec![AwsProfile {
name: "test-profile".to_string(),
region: Some("us-east-1".to_string()),
account: None,
role_arn: None,
source_profile: None,
}];
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].account, None);
}
#[test]
fn test_profile_with_all_fields() {
let mut app = test_app_no_region();
app.available_profiles = vec![AwsProfile {
name: "prod-profile".to_string(),
region: Some("us-west-2".to_string()),
account: Some("123456789".to_string()),
role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
source_profile: Some("base-profile".to_string()),
}];
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "prod-profile");
assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
assert_eq!(filtered[0].account, Some("123456789".to_string()));
assert_eq!(
filtered[0].role_arn,
Some("arn:aws:iam::123456789:role/AdminRole".to_string())
);
assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
}
#[test]
fn test_profile_filter_by_source_profile() {
let mut app = test_app_no_region();
app.available_profiles = vec![
AwsProfile {
name: "profile1".to_string(),
region: None,
account: None,
role_arn: None,
source_profile: Some("base".to_string()),
},
AwsProfile {
name: "profile2".to_string(),
region: None,
account: None,
role_arn: None,
source_profile: Some("other".to_string()),
},
];
app.profile_filter = "base".to_string();
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "profile1");
}
#[test]
fn test_profile_filter_by_role() {
let mut app = test_app_no_region();
app.available_profiles = vec![
AwsProfile {
name: "admin-profile".to_string(),
region: None,
account: None,
role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
source_profile: None,
},
AwsProfile {
name: "dev-profile".to_string(),
region: None,
account: None,
role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
source_profile: None,
},
];
app.profile_filter = "Admin".to_string();
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "admin-profile");
}
#[test]
fn test_profiles_sorted_by_name() {
let mut app = test_app_no_region();
app.available_profiles = vec![
AwsProfile {
name: "zebra-profile".to_string(),
region: None,
account: None,
role_arn: None,
source_profile: None,
},
AwsProfile {
name: "alpha-profile".to_string(),
region: None,
account: None,
role_arn: None,
source_profile: None,
},
AwsProfile {
name: "beta-profile".to_string(),
region: None,
account: None,
role_arn: None,
source_profile: None,
},
];
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 3);
assert_eq!(filtered[0].name, "alpha-profile");
assert_eq!(filtered[1].name, "beta-profile");
assert_eq!(filtered[2].name, "zebra-profile");
}
#[test]
fn test_profile_with_role_arn() {
let mut app = test_app_no_region();
app.available_profiles = vec![AwsProfile {
name: "role-profile".to_string(),
region: Some("us-east-1".to_string()),
account: Some("123456789".to_string()),
role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
source_profile: None,
}];
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
}
#[test]
fn test_profile_with_user_arn() {
let mut app = test_app_no_region();
app.available_profiles = vec![AwsProfile {
name: "user-profile".to_string(),
region: Some("us-east-1".to_string()),
account: Some("123456789".to_string()),
role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
source_profile: None,
}];
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
}
#[test]
fn test_filtered_profiles_also_sorted() {
let mut app = test_app_no_region();
app.available_profiles = vec![
AwsProfile {
name: "prod-zebra".to_string(),
region: Some("us-east-1".to_string()),
account: None,
role_arn: None,
source_profile: None,
},
AwsProfile {
name: "prod-alpha".to_string(),
region: Some("us-east-1".to_string()),
account: None,
role_arn: None,
source_profile: None,
},
AwsProfile {
name: "dev-profile".to_string(),
region: Some("us-west-2".to_string()),
account: None,
role_arn: None,
source_profile: None,
},
];
app.profile_filter = "prod".to_string();
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].name, "prod-alpha");
assert_eq!(filtered[1].name, "prod-zebra");
}
#[test]
fn test_profile_picker_has_all_columns() {
let mut app = test_app_no_region();
app.available_profiles = vec![AwsProfile {
name: "test".to_string(),
region: Some("us-east-1".to_string()),
account: Some("123456789".to_string()),
role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
source_profile: Some("base".to_string()),
}];
let filtered = app.get_filtered_profiles();
assert_eq!(filtered.len(), 1);
assert!(filtered[0].name == "test");
assert!(filtered[0].region.is_some());
assert!(filtered[0].account.is_some());
assert!(filtered[0].role_arn.is_some());
assert!(filtered[0].source_profile.is_some());
}
#[test]
fn test_session_picker_shows_tab_count() {
let mut app = test_app_no_region();
app.sessions = vec![Session {
id: "1".to_string(),
timestamp: "2024-01-01".to_string(),
profile: "test".to_string(),
region: "us-east-1".to_string(),
account_id: "123".to_string(),
role_arn: String::new(),
tabs: vec![
SessionTab {
service: "CloudWatch".to_string(),
title: "Logs".to_string(),
breadcrumb: String::new(),
filter: None,
selected_item: None,
},
SessionTab {
service: "S3".to_string(),
title: "Buckets".to_string(),
breadcrumb: String::new(),
filter: None,
selected_item: None,
},
],
}];
let filtered = app.get_filtered_sessions();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].tabs.len(), 2);
}
#[test]
fn test_start_background_data_fetch_loads_profiles() {
let mut app = test_app_no_region();
assert!(app.available_profiles.is_empty());
app.available_profiles = App::load_aws_profiles();
assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
}
#[test]
fn test_refresh_in_profile_picker() {
let mut app = test_app_no_region();
app.mode = Mode::ProfilePicker;
app.available_profiles = vec![AwsProfile {
name: "test".to_string(),
region: None,
account: None,
role_arn: None,
source_profile: None,
}];
app.handle_action(Action::Refresh);
assert!(app.log_groups_state.loading);
assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
}
#[test]
fn test_refresh_sets_loading_for_profile_picker() {
let mut app = test_app_no_region();
app.mode = Mode::ProfilePicker;
assert!(!app.log_groups_state.loading);
app.handle_action(Action::Refresh);
assert!(app.log_groups_state.loading);
}
#[test]
fn test_profiles_loaded_on_demand() {
let mut app = test_app_no_region();
assert!(app.available_profiles.is_empty());
app.available_profiles = App::load_aws_profiles();
assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
}
#[test]
fn test_profile_accounts_not_fetched_automatically() {
let mut app = test_app_no_region();
app.available_profiles = App::load_aws_profiles();
for profile in &app.available_profiles {
assert!(profile.account.is_none() || profile.account.is_some());
}
}
#[test]
fn test_ctrl_r_triggers_account_fetch() {
let mut app = test_app_no_region();
app.mode = Mode::ProfilePicker;
app.available_profiles = vec![AwsProfile {
name: "test".to_string(),
region: Some("us-east-1".to_string()),
account: None,
role_arn: None,
source_profile: None,
}];
assert!(app.available_profiles[0].account.is_none());
app.handle_action(Action::Refresh);
assert!(app.log_groups_state.loading);
}
#[test]
fn test_refresh_in_region_picker() {
let mut app = test_app_no_region();
app.mode = Mode::RegionPicker;
let initial_latencies = app.region_latencies.len();
app.handle_action(Action::Refresh);
assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
}
#[test]
fn test_refresh_in_session_picker() {
let mut app = test_app_no_region();
app.mode = Mode::SessionPicker;
app.sessions = vec![];
app.handle_action(Action::Refresh);
assert!(app.sessions.is_empty() || !app.sessions.is_empty());
}
#[test]
fn test_session_picker_selection() {
let mut app = test_app();
app.sessions = vec![Session {
id: "1".to_string(),
timestamp: "2024-01-01".to_string(),
profile: "prod-profile".to_string(),
region: "us-west-2".to_string(),
account_id: "123456789".to_string(),
role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
tabs: vec![SessionTab {
service: "CloudWatchLogGroups".to_string(),
title: "Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
filter: Some("test".to_string()),
selected_item: None,
}],
}];
app.mode = Mode::SessionPicker;
app.session_picker_selected = 0;
app.handle_action(Action::Select);
assert_eq!(app.mode, Mode::Normal);
assert_eq!(app.profile, "prod-profile");
assert_eq!(app.region, "us-west-2");
assert_eq!(app.config.account_id, "123456789");
assert_eq!(app.tabs.len(), 1);
assert_eq!(app.tabs[0].title, "Log Groups");
}
#[test]
fn test_save_session_creates_session() {
let mut app =
App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
app.config.account_id = "123456789".to_string();
app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
app.tabs.push(Tab {
service: Service::CloudWatchLogGroups,
title: "Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
});
app.save_current_session();
assert!(app.current_session.is_some());
let session = app.current_session.clone().unwrap();
assert_eq!(session.profile, "test-profile");
assert_eq!(session.region, "us-east-1");
assert_eq!(session.account_id, "123456789");
assert_eq!(session.tabs.len(), 1);
let _ = session.delete();
}
#[test]
fn test_save_session_updates_existing() {
let mut app =
App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
app.config.account_id = "123456789".to_string();
app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
app.current_session = Some(Session {
id: "existing".to_string(),
timestamp: "2024-01-01".to_string(),
profile: "test-profile".to_string(),
region: "us-east-1".to_string(),
account_id: "123456789".to_string(),
role_arn: "arn:aws:iam::123456789:role/test".to_string(),
tabs: vec![],
});
app.tabs.push(Tab {
service: Service::CloudWatchLogGroups,
title: "Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
});
app.save_current_session();
let session = app.current_session.clone().unwrap();
assert_eq!(session.id, "existing");
assert_eq!(session.tabs.len(), 1);
let _ = session.delete();
}
#[test]
fn test_save_session_skips_empty_tabs() {
let mut app =
App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
app.config.account_id = "123456789".to_string();
app.save_current_session();
assert!(app.current_session.is_none());
}
#[test]
fn test_save_session_deletes_when_tabs_closed() {
let mut app =
App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
app.config.account_id = "123456789".to_string();
app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
app.current_session = Some(Session {
id: "test_delete".to_string(),
timestamp: "2024-01-01 10:00:00 UTC".to_string(),
profile: "test-profile".to_string(),
region: "us-east-1".to_string(),
account_id: "123456789".to_string(),
role_arn: "arn:aws:iam::123456789:role/test".to_string(),
tabs: vec![],
});
app.save_current_session();
assert!(app.current_session.is_none());
}
#[test]
fn test_closing_all_tabs_deletes_session() {
let mut app =
App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
app.config.account_id = "123456789".to_string();
app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
app.tabs.push(Tab {
service: Service::CloudWatchLogGroups,
title: "Log Groups".to_string(),
breadcrumb: "CloudWatch › Log Groups".to_string(),
});
app.save_current_session();
assert!(app.current_session.is_some());
let session_id = app.current_session.as_ref().unwrap().id.clone();
app.tabs.clear();
app.save_current_session();
assert!(app.current_session.is_none());
let _ = Session::load(&session_id).map(|s| s.delete());
}
#[test]
fn test_credential_error_opens_profile_picker() {
let mut app = App::new_without_client("default".to_string(), None);
let error_str = "Unable to load credentials from any source";
if error_str.contains("credentials") {
app.available_profiles = App::load_aws_profiles();
app.mode = Mode::ProfilePicker;
}
assert_eq!(app.mode, Mode::ProfilePicker);
assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
}
#[test]
fn test_non_credential_error_shows_error_modal() {
let mut app = App::new_without_client("default".to_string(), None);
let error_str = "Network timeout";
if !error_str.contains("credentials") {
app.error_message = Some(error_str.to_string());
app.mode = Mode::ErrorModal;
}
assert_eq!(app.mode, Mode::ErrorModal);
assert!(app.error_message.is_some());
}
#[tokio::test]
async fn test_profile_selection_loads_credentials() {
std::env::set_var("AWS_PROFILE", "default");
let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
if let Ok(app) = result {
assert!(!app.config.account_id.is_empty());
assert!(!app.config.role_arn.is_empty());
assert_eq!(app.profile, "default");
assert_eq!(app.config.region, "us-east-1");
}
}
#[test]
fn test_new_app_shows_service_picker_with_no_tabs() {
let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
assert!(!app.service_selected);
assert_eq!(app.mode, Mode::ServicePicker);
assert!(app.tabs.is_empty());
}
#[tokio::test]
async fn test_aws_profile_env_var_read_before_config_load() {
std::env::set_var("AWS_PROFILE", "test-profile");
let profile_name = None
.or_else(|| std::env::var("AWS_PROFILE").ok())
.unwrap_or_else(|| "default".to_string());
assert_eq!(profile_name, "test-profile");
std::env::set_var("AWS_PROFILE", &profile_name);
assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
std::env::remove_var("AWS_PROFILE");
}
#[test]
fn test_next_preferences_cloudformation() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.cfn_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_s3_preferences_tab_cycling() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.s3_bucket_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_s3_filter_resets_selection() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket-1".to_string(),
region: "us-east-1".to_string(),
creation_date: "2023-01-01".to_string(),
},
S3Bucket {
name: "bucket-2".to_string(),
region: "us-east-1".to_string(),
creation_date: "2023-01-02".to_string(),
},
S3Bucket {
name: "other-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: "2023-01-03".to_string(),
},
];
app.s3_state.selected_row = 1;
app.s3_state.bucket_scroll_offset = 1;
app.mode = Mode::FilterInput;
app.apply_filter_operation(|f| f.push_str("bucket-"));
assert_eq!(app.s3_state.selected_row, 0);
assert_eq!(app.s3_state.bucket_scroll_offset, 0);
assert_eq!(app.s3_state.buckets.filter, "bucket-");
}
#[test]
fn test_s3_navigation_respects_filter() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "prod-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: "2023-01-01".to_string(),
},
S3Bucket {
name: "dev-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: "2023-01-02".to_string(),
},
S3Bucket {
name: "prod-logs".to_string(),
region: "us-east-1".to_string(),
creation_date: "2023-01-03".to_string(),
},
];
app.s3_state.buckets.filter = "prod".to_string();
assert_eq!(app.s3_state.selected_row, 0);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_next_preferences_lambda_functions() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_next_preferences_lambda_applications() {
let mut app = test_app();
app.current_service = Service::LambdaApplications;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.lambda_application_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_next_preferences_ecr_images() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.ecr_state.current_repository = Some("test-repo".to_string());
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.ecr_image_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_next_preferences_cloudwatch_log_groups() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.view_mode = ViewMode::List;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.cw_log_group_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_next_preferences_cloudwatch_log_streams() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.view_mode = ViewMode::Detail;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_cloudformation_next_item() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![
CfnStack {
name: "stack1".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
CfnStack {
name: "stack2".to_string(),
stack_id: "id2".to_string(),
status: "UPDATE_COMPLETE".to_string(),
created_time: "2024-01-02".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
];
app.cfn_state.table.reset();
app.handle_action(Action::NextItem);
assert_eq!(app.cfn_state.table.selected, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.cfn_state.table.selected, 1); }
#[test]
fn test_cloudformation_prev_item() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![
CfnStack {
name: "stack1".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
CfnStack {
name: "stack2".to_string(),
stack_id: "id2".to_string(),
status: "UPDATE_COMPLETE".to_string(),
created_time: "2024-01-02".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
];
app.cfn_state.table.selected = 1;
app.handle_action(Action::PrevItem);
assert_eq!(app.cfn_state.table.selected, 0);
app.handle_action(Action::PrevItem);
assert_eq!(app.cfn_state.table.selected, 0); }
#[test]
fn test_cloudformation_page_down() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
for i in 0..20 {
app.cfn_state.table.items.push(CfnStack {
name: format!("stack{}", i),
stack_id: format!("id{}", i),
status: "CREATE_COMPLETE".to_string(),
created_time: format!("2024-01-{:02}", i + 1),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
});
}
app.cfn_state.table.reset();
app.handle_action(Action::PageDown);
assert_eq!(app.cfn_state.table.selected, 10);
app.handle_action(Action::PageDown);
assert_eq!(app.cfn_state.table.selected, 19); }
#[test]
fn test_cloudformation_page_up() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
for i in 0..20 {
app.cfn_state.table.items.push(CfnStack {
name: format!("stack{}", i),
stack_id: format!("id{}", i),
status: "CREATE_COMPLETE".to_string(),
created_time: format!("2024-01-{:02}", i + 1),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
});
}
app.cfn_state.table.selected = 15;
app.handle_action(Action::PageUp);
assert_eq!(app.cfn_state.table.selected, 5);
app.handle_action(Action::PageUp);
assert_eq!(app.cfn_state.table.selected, 0); }
#[test]
fn test_cloudformation_filter_input() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
app.cfn_state.table.filter = "test".to_string();
assert_eq!(app.cfn_state.table.filter, "test");
}
#[test]
fn test_cloudformation_filter_applies() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![
CfnStack {
name: "prod-stack".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: "Production stack".to_string(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
CfnStack {
name: "dev-stack".to_string(),
stack_id: "id2".to_string(),
status: "UPDATE_COMPLETE".to_string(),
created_time: "2024-01-02".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: "Development stack".to_string(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
];
app.cfn_state.table.filter = "prod".to_string();
let filtered = filtered_cloudformation_stacks(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "prod-stack");
}
#[test]
fn test_cloudformation_right_arrow_expands() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![CfnStack {
name: "test-stack".to_string(),
stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
.to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: "Test stack".to_string(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
}];
app.cfn_state.table.reset();
assert_eq!(app.cfn_state.table.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.cfn_state.table.expanded_item, Some(0));
}
#[test]
fn test_cloudformation_left_arrow_collapses() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![CfnStack {
name: "test-stack".to_string(),
stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
.to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: "Test stack".to_string(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
}];
app.cfn_state.table.reset();
app.cfn_state.table.expanded_item = Some(0);
app.handle_action(Action::PrevPane);
assert_eq!(app.cfn_state.table.expanded_item, None);
}
#[test]
fn test_cloudformation_enter_drills_into_stack() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.tabs = vec![Tab {
service: Service::CloudFormationStacks,
title: "CloudFormation › Stacks".to_string(),
breadcrumb: "CloudFormation › Stacks".to_string(),
}];
app.current_tab = 0;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![CfnStack {
name: "test-stack".to_string(),
stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
.to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: "Test stack".to_string(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
}];
app.cfn_state.table.reset();
let filtered = filtered_cloudformation_stacks(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "test-stack");
assert_eq!(app.cfn_state.current_stack, None);
app.handle_action(Action::Select);
assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
}
#[test]
fn test_cloudformation_copy_to_clipboard() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![
CfnStack {
name: "stack1".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
CfnStack {
name: "stack2".to_string(),
stack_id: "id2".to_string(),
status: "UPDATE_COMPLETE".to_string(),
created_time: "2024-01-02".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
];
assert!(!app.snapshot_requested);
app.handle_action(Action::CopyToClipboard);
assert!(app.snapshot_requested);
}
#[test]
fn test_cloudformation_expansion_shows_all_visible_columns() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![CfnStack {
name: "test-stack".to_string(),
stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
.to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: "2024-01-02".to_string(),
deleted_time: String::new(),
drift_status: "IN_SYNC".to_string(),
last_drift_check_time: "2024-01-03".to_string(),
status_reason: String::new(),
description: "Test description".to_string(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
}];
app.cfn_visible_column_ids = [
CfnColumn::Name,
CfnColumn::Status,
CfnColumn::CreatedTime,
CfnColumn::Description,
]
.iter()
.map(|c| c.id())
.collect();
app.cfn_state.table.expanded_item = Some(0);
assert_eq!(app.cfn_visible_column_ids.len(), 4);
assert!(app.cfn_state.table.has_expanded_item());
}
#[test]
fn test_cloudformation_empty_list_shows_page_1() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.cfn_state.table.items = vec![];
let filtered = filtered_cloudformation_stacks(&app);
assert_eq!(filtered.len(), 0);
let page_size = app.cfn_state.table.page_size.value();
let total_pages = filtered.len().div_ceil(page_size);
assert_eq!(total_pages, 0);
}
}
impl App {
pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
let mut all = AwsRegion::all();
for region in &mut all {
region.latency_ms = self.region_latencies.get(region.code).copied();
}
let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
all
} else {
let filter_lower = self.region_filter.to_lowercase();
all.into_iter()
.filter(|r| {
r.name.to_lowercase().contains(&filter_lower)
|| r.code.to_lowercase().contains(&filter_lower)
|| r.group.to_lowercase().contains(&filter_lower)
})
.collect()
};
let mut sorted = filtered;
sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
sorted
}
pub fn measure_region_latencies(&mut self) {
use std::time::Instant;
self.region_latencies.clear();
let regions = AwsRegion::all();
let start_all = Instant::now();
tracing::info!("Starting latency measurement for {} regions", regions.len());
let handles: Vec<_> = regions
.iter()
.map(|region| {
let code = region.code.to_string();
std::thread::spawn(move || {
let endpoint = format!("https://sts.{}.amazonaws.com", code);
let start = Instant::now();
match ureq::get(&endpoint)
.timeout(std::time::Duration::from_secs(2))
.call()
{
Ok(_) => {
let latency = start.elapsed().as_millis() as u64;
Some((code, latency))
}
Err(e) => {
tracing::debug!("Failed to measure {}: {}", code, e);
Some((code, 9999))
}
}
})
})
.collect();
for handle in handles {
if let Ok(Some((code, latency))) = handle.join() {
self.region_latencies.insert(code, latency);
}
}
tracing::info!(
"Measured {} regions in {:?}",
self.region_latencies.len(),
start_all.elapsed()
);
}
pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
filter_profiles(&self.available_profiles, &self.profile_filter)
}
pub fn get_filtered_sessions(&self) -> Vec<&Session> {
if self.session_filter.is_empty() {
return self.sessions.iter().collect();
}
let filter_lower = self.session_filter.to_lowercase();
self.sessions
.iter()
.filter(|s| {
s.profile.to_lowercase().contains(&filter_lower)
|| s.region.to_lowercase().contains(&filter_lower)
|| s.account_id.to_lowercase().contains(&filter_lower)
|| s.role_arn.to_lowercase().contains(&filter_lower)
})
.collect()
}
pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
if self.tab_filter.is_empty() {
return self.tabs.iter().enumerate().collect();
}
let filter_lower = self.tab_filter.to_lowercase();
self.tabs
.iter()
.enumerate()
.filter(|(_, tab)| {
tab.title.to_lowercase().contains(&filter_lower)
|| tab.breadcrumb.to_lowercase().contains(&filter_lower)
})
.collect()
}
pub fn load_aws_profiles() -> Vec<AwsProfile> {
AwsProfile::load_all()
}
pub async fn fetch_profile_accounts(&mut self) {
for profile in &mut self.available_profiles {
if profile.account.is_none() {
let region = profile
.region
.clone()
.unwrap_or_else(|| "us-east-1".to_string());
if let Ok(account) =
rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
{
profile.account = Some(account);
}
}
}
}
fn save_current_session(&mut self) {
if self.tabs.is_empty() {
if let Some(ref session) = self.current_session {
let _ = session.delete();
self.current_session = None;
}
return;
}
let session = if let Some(ref mut current) = self.current_session {
current.tabs = self
.tabs
.iter()
.map(|t| SessionTab {
service: format!("{:?}", t.service),
title: t.title.clone(),
breadcrumb: t.breadcrumb.clone(),
filter: match t.service {
Service::CloudWatchLogGroups => {
Some(self.log_groups_state.log_groups.filter.clone())
}
_ => None,
},
selected_item: None,
})
.collect();
current.clone()
} else {
let mut session = Session::new(
self.profile.clone(),
self.region.clone(),
self.config.account_id.clone(),
self.config.role_arn.clone(),
);
session.tabs = self
.tabs
.iter()
.map(|t| SessionTab {
service: format!("{:?}", t.service),
title: t.title.clone(),
breadcrumb: t.breadcrumb.clone(),
filter: match t.service {
Service::CloudWatchLogGroups => {
Some(self.log_groups_state.log_groups.filter.clone())
}
_ => None,
},
selected_item: None,
})
.collect();
self.current_session = Some(session.clone());
session
};
let _ = session.save();
}
}
#[cfg(test)]
mod iam_policy_view_tests {
use super::*;
use test_helpers::*;
#[test]
fn test_enter_opens_policy_view() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::Detail;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.policies.items = vec![IamPolicy {
policy_name: "TestPolicy".to_string(),
policy_type: "Inline".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "1".to_string(),
description: "Test".to_string(),
creation_time: "2023-01-01".to_string(),
edited_time: "2023-01-01".to_string(),
policy_arn: None,
}];
app.iam_state.policies.reset();
app.handle_action(Action::Select);
assert_eq!(app.view_mode, ViewMode::PolicyView);
assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
assert_eq!(app.iam_state.policy_scroll, 0);
assert!(app.iam_state.policies.loading);
}
#[test]
fn test_escape_closes_policy_view() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::PolicyView;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.current_policy = Some("TestPolicy".to_string());
app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
app.iam_state.policy_scroll = 5;
app.handle_action(Action::PrevPane);
assert_eq!(app.view_mode, ViewMode::Detail);
assert_eq!(app.iam_state.current_policy, None);
assert_eq!(app.iam_state.policy_document, "");
assert_eq!(app.iam_state.policy_scroll, 0);
}
#[test]
fn test_ctrl_d_scrolls_down_in_policy_view() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::PolicyView;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.current_policy = Some("TestPolicy".to_string());
app.iam_state.policy_document = (0..100)
.map(|i| format!("line {}", i))
.collect::<Vec<_>>()
.join("\n");
app.iam_state.policy_scroll = 0;
app.handle_action(Action::ScrollDown);
assert_eq!(app.iam_state.policy_scroll, 10);
app.handle_action(Action::ScrollDown);
assert_eq!(app.iam_state.policy_scroll, 20);
}
#[test]
fn test_ctrl_u_scrolls_up_in_policy_view() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::PolicyView;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.current_policy = Some("TestPolicy".to_string());
app.iam_state.policy_document = (0..100)
.map(|i| format!("line {}", i))
.collect::<Vec<_>>()
.join("\n");
app.iam_state.policy_scroll = 30;
app.handle_action(Action::ScrollUp);
assert_eq!(app.iam_state.policy_scroll, 20);
app.handle_action(Action::ScrollUp);
assert_eq!(app.iam_state.policy_scroll, 10);
}
#[test]
fn test_scroll_does_not_go_negative() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::PolicyView;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.current_policy = Some("TestPolicy".to_string());
app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
app.iam_state.policy_scroll = 0;
app.handle_action(Action::ScrollUp);
assert_eq!(app.iam_state.policy_scroll, 0);
}
#[test]
fn test_scroll_does_not_exceed_max() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::PolicyView;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.current_policy = Some("TestPolicy".to_string());
app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
app.iam_state.policy_scroll = 0;
app.handle_action(Action::ScrollDown);
assert_eq!(app.iam_state.policy_scroll, 2); }
#[test]
fn test_policy_view_console_url() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.view_mode = ViewMode::PolicyView;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.current_policy = Some("TestPolicy".to_string());
let url = app.get_console_url();
assert!(url.contains("us-east-1.console.aws.amazon.com"));
assert!(url.contains("/roles/details/TestRole"));
assert!(url.contains("/editPolicy/TestPolicy"));
assert!(url.contains("step=addPermissions"));
}
#[test]
fn test_esc_from_policy_view_goes_to_role_detail() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::PolicyView;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.current_policy = Some("TestPolicy".to_string());
app.iam_state.policy_document = "test".to_string();
app.iam_state.policy_scroll = 5;
app.handle_action(Action::GoBack);
assert_eq!(app.view_mode, ViewMode::Detail);
assert_eq!(app.iam_state.current_policy, None);
assert_eq!(app.iam_state.policy_document, "");
assert_eq!(app.iam_state.policy_scroll, 0);
assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
}
#[test]
fn test_esc_from_role_detail_goes_to_role_list() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::Detail;
app.iam_state.current_role = Some("TestRole".to_string());
app.handle_action(Action::GoBack);
assert_eq!(app.iam_state.current_role, None);
}
#[test]
fn test_right_arrow_expands_policy_row() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.view_mode = ViewMode::Detail;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.policies.items = vec![IamPolicy {
policy_name: "TestPolicy".to_string(),
policy_type: "Inline".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "1".to_string(),
description: "Test".to_string(),
creation_time: "2023-01-01".to_string(),
edited_time: "2023-01-01".to_string(),
policy_arn: None,
}];
app.iam_state.policies.reset();
app.handle_action(Action::NextPane);
assert_eq!(app.view_mode, ViewMode::Detail);
assert_eq!(app.iam_state.current_policy, None);
assert_eq!(app.iam_state.policies.expanded_item, Some(0));
}
#[test]
fn test_managed_policy_has_creation_and_edit_time() {
use crate::iam::Policy as IamPolicy;
let managed = IamPolicy {
policy_name: "MyManagedPolicy".to_string(),
policy_type: "Managed".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "-".to_string(),
description: "-".to_string(),
creation_time: "2024-01-01 00:00:00 (UTC)".to_string(),
edited_time: "2024-06-01 00:00:00 (UTC)".to_string(),
policy_arn: Some("arn:aws:iam::123:policy/MyManagedPolicy".to_string()),
};
assert_ne!(
managed.creation_time, "-",
"Managed policy must have a real creation time, not '-'"
);
assert_ne!(
managed.edited_time, "-",
"Managed policy must have a real edit time, not '-'"
);
let inline = IamPolicy {
policy_name: "InlinePolicy".to_string(),
policy_type: "Inline".to_string(),
attached_via: "Direct".to_string(),
attached_entities: "-".to_string(),
description: "-".to_string(),
creation_time: "-".to_string(),
edited_time: "-".to_string(),
policy_arn: None,
};
assert_eq!(
inline.creation_time, "-",
"Inline policies have no creation time"
);
}
}
#[cfg(test)]
mod tab_filter_tests {
use super::*;
use test_helpers::*;
#[test]
fn test_space_t_opens_tab_picker() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "Tab 1".to_string(),
breadcrumb: "CloudWatch > Log groups".to_string(),
},
Tab {
service: Service::S3Buckets,
title: "Tab 2".to_string(),
breadcrumb: "S3 › Buckets".to_string(),
},
];
app.current_tab = 0;
app.handle_action(Action::OpenTabPicker);
assert_eq!(app.mode, Mode::TabPicker);
assert_eq!(app.tab_picker_selected, 0);
}
#[test]
fn test_tab_filter_works() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch Logs".to_string(),
breadcrumb: "CloudWatch > Log groups".to_string(),
},
Tab {
service: Service::S3Buckets,
title: "S3 Buckets".to_string(),
breadcrumb: "S3 › Buckets".to_string(),
},
Tab {
service: Service::CloudWatchAlarms,
title: "CloudWatch Alarms".to_string(),
breadcrumb: "CloudWatch › Alarms".to_string(),
},
];
app.mode = Mode::TabPicker;
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('3'));
let filtered = app.get_filtered_tabs();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].1.title, "S3 Buckets");
}
#[test]
fn test_tab_filter_by_breadcrumb() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "Tab 1".to_string(),
breadcrumb: "CloudWatch > Log groups".to_string(),
},
Tab {
service: Service::S3Buckets,
title: "Tab 2".to_string(),
breadcrumb: "S3 › Buckets".to_string(),
},
];
app.mode = Mode::TabPicker;
app.handle_action(Action::FilterInput('c'));
app.handle_action(Action::FilterInput('l'));
app.handle_action(Action::FilterInput('o'));
app.handle_action(Action::FilterInput('u'));
app.handle_action(Action::FilterInput('d'));
let filtered = app.get_filtered_tabs();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
}
#[test]
fn test_tab_filter_backspace() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch Logs".to_string(),
breadcrumb: "CloudWatch > Log groups".to_string(),
},
Tab {
service: Service::S3Buckets,
title: "S3 Buckets".to_string(),
breadcrumb: "S3 › Buckets".to_string(),
},
];
app.mode = Mode::TabPicker;
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('3'));
assert_eq!(app.tab_filter, "s3");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.tab_filter, "s");
let filtered = app.get_filtered_tabs();
assert_eq!(filtered.len(), 2); }
#[test]
fn test_tab_selection_with_filter() {
let mut app = test_app();
app.tabs = vec![
Tab {
service: Service::CloudWatchLogGroups,
title: "CloudWatch Logs".to_string(),
breadcrumb: "CloudWatch > Log groups".to_string(),
},
Tab {
service: Service::S3Buckets,
title: "S3 Buckets".to_string(),
breadcrumb: "S3 › Buckets".to_string(),
},
];
app.mode = Mode::TabPicker;
app.current_tab = 0;
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('3'));
app.handle_action(Action::Select);
assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
assert_eq!(app.tab_filter, ""); }
}
#[cfg(test)]
mod region_latency_tests {
use super::*;
use test_helpers::*;
#[test]
fn test_regions_sorted_by_latency() {
let mut app = test_app();
app.region_latencies.insert("us-west-2".to_string(), 50);
app.region_latencies.insert("us-east-1".to_string(), 10);
app.region_latencies.insert("eu-west-1".to_string(), 100);
let filtered = app.get_filtered_regions();
let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
assert!(with_latency.len() >= 3);
assert_eq!(with_latency[0].code, "us-east-1");
assert_eq!(with_latency[0].latency_ms, Some(10));
assert_eq!(with_latency[1].code, "us-west-2");
assert_eq!(with_latency[1].latency_ms, Some(50));
assert_eq!(with_latency[2].code, "eu-west-1");
assert_eq!(with_latency[2].latency_ms, Some(100));
}
#[test]
fn test_regions_with_latency_before_without() {
let mut app = test_app();
app.region_latencies.insert("eu-west-1".to_string(), 100);
let filtered = app.get_filtered_regions();
assert_eq!(filtered[0].code, "eu-west-1");
assert_eq!(filtered[0].latency_ms, Some(100));
for region in &filtered[1..] {
assert!(region.latency_ms.is_none());
}
}
#[test]
fn test_region_filter_with_latency() {
let mut app = test_app();
app.region_latencies.insert("us-east-1".to_string(), 10);
app.region_latencies.insert("us-west-2".to_string(), 50);
app.region_filter = "us".to_string();
let filtered = app.get_filtered_regions();
assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
assert_eq!(filtered[0].code, "us-east-1");
assert_eq!(filtered[1].code, "us-west-2");
}
#[test]
fn test_latency_persists_across_filters() {
let mut app = test_app();
app.region_latencies.insert("us-east-1".to_string(), 10);
app.region_filter = "eu".to_string();
let filtered = app.get_filtered_regions();
assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
app.region_filter.clear();
let all = app.get_filtered_regions();
let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
assert_eq!(us_east.latency_ms, Some(10));
}
#[test]
fn test_measure_region_latencies_clears_previous() {
let mut app = test_app();
app.region_latencies.insert("us-east-1".to_string(), 100);
app.region_latencies.insert("eu-west-1".to_string(), 200);
app.measure_region_latencies();
assert!(
app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
);
}
#[test]
fn test_regions_with_latency_sorted_first() {
let mut app = test_app();
app.region_latencies.insert("us-east-1".to_string(), 50);
app.region_latencies.insert("eu-west-1".to_string(), 500);
let filtered = app.get_filtered_regions();
assert!(filtered.len() > 2);
assert_eq!(filtered[0].code, "us-east-1");
assert_eq!(filtered[0].latency_ms, Some(50));
assert_eq!(filtered[1].code, "eu-west-1");
assert_eq!(filtered[1].latency_ms, Some(500));
for region in &filtered[2..] {
assert!(region.latency_ms.is_none());
}
}
#[test]
fn test_regions_without_latency_sorted_as_1000ms() {
let mut app = test_app();
app.region_latencies
.insert("ap-southeast-2".to_string(), 1500);
app.region_latencies.insert("us-east-1".to_string(), 50);
let filtered = app.get_filtered_regions();
assert_eq!(filtered[0].code, "us-east-1");
assert_eq!(filtered[0].latency_ms, Some(50));
let slow_region_idx = filtered
.iter()
.position(|r| r.code == "ap-southeast-2")
.unwrap();
assert!(slow_region_idx > 1);
for region in filtered.iter().take(slow_region_idx).skip(1) {
assert!(region.latency_ms.is_none());
}
}
#[test]
fn test_region_picker_opens_with_latencies() {
let mut app = test_app();
app.region_filter.clear();
app.region_picker_selected = 0;
app.measure_region_latencies();
assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
}
#[test]
fn test_region_picker_enter_in_filter_mode_exits_insert_not_selects() {
let mut app = test_app();
app.mode = Mode::RegionPicker;
app.region_filter_active = true; app.region_filter = "us".to_string();
let original_region = app.region.clone();
app.handle_action(Action::Select);
assert!(
!app.region_filter_active,
"Filter must be deactivated (switch to NORMAL/table focus)"
);
assert_eq!(
app.mode,
Mode::RegionPicker,
"Must stay in RegionPicker, not close it"
);
assert_eq!(
app.region, original_region,
"Region must NOT change when Enter is pressed in INSERT mode"
);
}
#[test]
fn test_region_picker_enter_in_normal_mode_selects_region() {
let mut app = test_app();
app.mode = Mode::RegionPicker;
app.region_filter_active = false; app.region_filter.clear();
app.handle_action(Action::Select);
assert_eq!(
app.mode,
Mode::Normal,
"Must close picker after selecting region in NORMAL mode"
);
}
#[test]
fn test_region_filter_can_type_any_character_in_insert_mode() {
use crate::keymap::{handle_key, Mode as KMode};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let mut app = test_app();
app.mode = Mode::RegionPicker;
app.region_filter_active = true;
app.region_filter.clear();
let t_key = KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE);
let o_key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
let k_key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
if let Some(action) = handle_key(t_key, KMode::RegionPicker) {
app.handle_action(action);
}
if let Some(action) = handle_key(o_key, KMode::RegionPicker) {
app.handle_action(action);
}
if let Some(action) = handle_key(k_key, KMode::RegionPicker) {
app.handle_action(action);
}
assert_eq!(
app.region_filter, "tok",
"'k' must be typed into filter (not navigate up) when filter is active"
);
}
#[test]
fn test_ecr_tab_next() {
assert_eq!(EcrTab::Private.next(), EcrTab::Public);
assert_eq!(EcrTab::Public.next(), EcrTab::Private);
}
#[test]
fn test_ecr_tab_switching() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.ecr_state.tab = EcrTab::Private;
app.handle_action(Action::NextDetailTab);
assert_eq!(app.ecr_state.tab, EcrTab::Public);
assert_eq!(app.ecr_state.repositories.selected, 0);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.ecr_state.tab, EcrTab::Private);
}
#[test]
fn test_ecr_navigation() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.items = vec![
EcrRepository {
name: "repo1".to_string(),
uri: "uri1".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
},
EcrRepository {
name: "repo2".to_string(),
uri: "uri2".to_string(),
created_at: "2023-01-02".to_string(),
tag_immutability: "IMMUTABLE".to_string(),
encryption_type: "KMS".to_string(),
},
];
app.handle_action(Action::NextItem);
assert_eq!(app.ecr_state.repositories.selected, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.ecr_state.repositories.selected, 0);
}
#[test]
fn test_ecr_filter() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.ecr_state.repositories.items = vec![
EcrRepository {
name: "my-app".to_string(),
uri: "uri1".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
},
EcrRepository {
name: "other-service".to_string(),
uri: "uri2".to_string(),
created_at: "2023-01-02".to_string(),
tag_immutability: "IMMUTABLE".to_string(),
encryption_type: "KMS".to_string(),
},
];
app.ecr_state.repositories.filter = "app".to_string();
let filtered = filtered_ecr_repositories(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "my-app");
}
#[test]
fn test_ecr_filter_input() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.ecr_state.repositories.filter, "test");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.ecr_state.repositories.filter, "tes");
}
#[test]
fn test_ecr_filter_resets_selection() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.repositories.items = vec![
EcrRepository {
name: "repo1".to_string(),
uri: "uri1".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
},
EcrRepository {
name: "repo2".to_string(),
uri: "uri2".to_string(),
created_at: "2023-01-02".to_string(),
tag_immutability: "IMMUTABLE".to_string(),
encryption_type: "KMS".to_string(),
},
EcrRepository {
name: "repo3".to_string(),
uri: "uri3".to_string(),
created_at: "2023-01-03".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
},
];
app.ecr_state.repositories.selected = 2;
assert_eq!(app.ecr_state.repositories.selected, 2);
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.ecr_state.repositories.filter, "t");
assert_eq!(app.ecr_state.repositories.selected, 0);
app.ecr_state.repositories.selected = 1;
app.handle_action(Action::FilterBackspace);
assert_eq!(app.ecr_state.repositories.filter, "");
assert_eq!(app.ecr_state.repositories.selected, 0);
}
#[test]
fn test_ecr_images_filter_resets_selection() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.current_repository = Some("test-repo".to_string());
app.ecr_state.images.items = vec![
EcrImage {
tag: "v1.0.0".to_string(),
artifact_type: "container".to_string(),
digest: "sha256:abc123".to_string(),
pushed_at: "2023-01-01".to_string(),
size_bytes: 1000,
uri: "uri1".to_string(),
last_pull_time: "".to_string(),
},
EcrImage {
tag: "v2.0.0".to_string(),
artifact_type: "container".to_string(),
digest: "sha256:def456".to_string(),
pushed_at: "2023-01-02".to_string(),
size_bytes: 2000,
uri: "uri2".to_string(),
last_pull_time: "".to_string(),
},
];
app.ecr_state.images.selected = 1;
assert_eq!(app.ecr_state.images.selected, 1);
app.handle_action(Action::FilterInput('v'));
assert_eq!(app.ecr_state.images.filter, "v");
assert_eq!(app.ecr_state.images.selected, 0);
}
#[test]
fn test_iam_users_filter_input() {
let mut app = test_app();
app.current_service = Service::IamUsers;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.handle_action(Action::FilterInput('a'));
app.handle_action(Action::FilterInput('d'));
app.handle_action(Action::FilterInput('m'));
app.handle_action(Action::FilterInput('i'));
app.handle_action(Action::FilterInput('n'));
assert_eq!(app.iam_state.users.filter, "admin");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.iam_state.users.filter, "admi");
}
#[test]
fn test_iam_policies_filter_input() {
let mut app = test_app();
app.current_service = Service::IamUsers;
app.service_selected = true;
app.iam_state.current_user = Some("testuser".to_string());
app.mode = Mode::FilterInput;
app.handle_action(Action::FilterInput('r'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('a'));
app.handle_action(Action::FilterInput('d'));
assert_eq!(app.iam_state.policies.filter, "read");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.iam_state.policies.filter, "rea");
}
#[test]
fn test_iam_start_filter() {
let mut app = test_app();
app.current_service = Service::IamUsers;
app.service_selected = true;
app.mode = Mode::Normal;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
}
#[test]
fn test_iam_roles_filter_input() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.handle_action(Action::FilterInput('a'));
app.handle_action(Action::FilterInput('d'));
app.handle_action(Action::FilterInput('m'));
app.handle_action(Action::FilterInput('i'));
app.handle_action(Action::FilterInput('n'));
assert_eq!(app.iam_state.roles.filter, "admin");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.iam_state.roles.filter, "admi");
}
#[test]
fn test_iam_roles_start_filter() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
}
#[test]
fn test_iam_roles_navigation() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.roles.items = (0..10)
.map(|i| IamRole {
role_name: format!("role{}", i),
path: "/".to_string(),
trusted_entities: String::new(),
last_activity: String::new(),
arn: format!("arn:aws:iam::123456789012:role/role{}", i),
creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
description: String::new(),
max_session_duration: Some(3600),
})
.collect();
assert_eq!(app.iam_state.roles.selected, 0);
app.handle_action(Action::NextItem);
assert_eq!(app.iam_state.roles.selected, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.iam_state.roles.selected, 2);
app.handle_action(Action::PrevItem);
assert_eq!(app.iam_state.roles.selected, 1);
}
#[test]
fn test_iam_roles_page_hotkey() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.roles.page_size = PageSize::Ten;
app.iam_state.roles.items = (0..100)
.map(|i| IamRole {
role_name: format!("role{}", i),
path: "/".to_string(),
trusted_entities: String::new(),
last_activity: String::new(),
arn: format!("arn:aws:iam::123456789012:role/role{}", i),
creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
description: String::new(),
max_session_duration: Some(3600),
})
.collect();
app.handle_action(Action::FilterInput('2'));
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.iam_state.roles.selected, 10); }
#[test]
fn test_iam_users_page_hotkey() {
let mut app = test_app();
app.current_service = Service::IamUsers;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.users.page_size = PageSize::Ten;
app.iam_state.users.items = (0..100)
.map(|i| IamUser {
user_name: format!("user{}", i),
path: "/".to_string(),
groups: String::new(),
last_activity: String::new(),
mfa: String::new(),
password_age: String::new(),
console_last_sign_in: String::new(),
access_key_id: String::new(),
active_key_age: String::new(),
access_key_last_used: String::new(),
arn: format!("arn:aws:iam::123456789012:user/user{}", i),
creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
console_access: String::new(),
signing_certs: String::new(),
})
.collect();
app.handle_action(Action::FilterInput('3'));
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.iam_state.users.selected, 20); }
#[test]
fn test_ecr_scroll_navigation() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.ecr_state.repositories.items = (0..20)
.map(|i| EcrRepository {
name: format!("repo{}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.handle_action(Action::ScrollDown);
assert_eq!(app.ecr_state.repositories.selected, 10);
app.handle_action(Action::ScrollUp);
assert_eq!(app.ecr_state.repositories.selected, 0);
}
#[test]
fn test_ecr_tab_switching_triggers_reload() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.ecr_state.tab = EcrTab::Private;
app.ecr_state.repositories.loading = false;
app.ecr_state.repositories.items = vec![EcrRepository {
name: "private-repo".to_string(),
uri: "uri".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
}];
app.handle_action(Action::NextDetailTab);
assert_eq!(app.ecr_state.tab, EcrTab::Public);
assert!(app.ecr_state.repositories.loading);
assert_eq!(app.ecr_state.repositories.selected, 0);
}
#[test]
fn test_ecr_tab_cycles_between_private_and_public() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.ecr_state.tab = EcrTab::Private;
app.handle_action(Action::NextDetailTab);
assert_eq!(app.ecr_state.tab, EcrTab::Public);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.ecr_state.tab, EcrTab::Private);
}
#[test]
fn test_page_size_values() {
assert_eq!(PageSize::Ten.value(), 10);
assert_eq!(PageSize::TwentyFive.value(), 25);
assert_eq!(PageSize::Fifty.value(), 50);
assert_eq!(PageSize::OneHundred.value(), 100);
}
#[test]
fn test_page_size_next() {
assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
}
#[test]
fn test_ecr_enter_drills_into_repository() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.items = vec![EcrRepository {
name: "my-repo".to_string(),
uri: "uri".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
}];
app.handle_action(Action::Select);
assert_eq!(
app.ecr_state.current_repository,
Some("my-repo".to_string())
);
assert!(app.ecr_state.repositories.loading);
}
#[test]
fn test_ecr_repository_expansion() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.ecr_state.repositories.items = vec![EcrRepository {
name: "my-repo".to_string(),
uri: "uri".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
}];
app.ecr_state.repositories.selected = 0;
assert_eq!(app.ecr_state.repositories.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.ecr_state.repositories.expanded_item, None);
}
#[test]
fn test_ecr_ctrl_d_scrolls_down() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.items = (0..30)
.map(|i| EcrRepository {
name: format!("repo{}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.ecr_state.repositories.selected = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.ecr_state.repositories.selected, 10);
}
#[test]
fn test_ecr_ctrl_u_scrolls_up() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.items = (0..30)
.map(|i| EcrRepository {
name: format!("repo{}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.ecr_state.repositories.selected = 15;
app.handle_action(Action::PageUp);
assert_eq!(app.ecr_state.repositories.selected, 5);
}
#[test]
fn test_ecr_images_ctrl_d_scrolls_down() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.current_repository = Some("repo".to_string());
app.ecr_state.images.items = (0..30)
.map(|i| EcrImage {
tag: format!("tag{}", i),
artifact_type: "container".to_string(),
pushed_at: "2023-01-01T12:00:00Z".to_string(),
size_bytes: 104857600,
uri: format!("uri{}", i),
digest: format!("sha256:{}", i),
last_pull_time: String::new(),
})
.collect();
app.ecr_state.images.selected = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.ecr_state.images.selected, 10);
}
#[test]
fn test_ecr_esc_goes_back_from_images_to_repos() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.current_repository = Some("my-repo".to_string());
app.ecr_state.images.items = vec![EcrImage {
tag: "latest".to_string(),
artifact_type: "container".to_string(),
pushed_at: "2023-01-01T12:00:00Z".to_string(),
size_bytes: 104857600,
uri: "uri".to_string(),
digest: "sha256:abc".to_string(),
last_pull_time: String::new(),
}];
app.handle_action(Action::GoBack);
assert_eq!(app.ecr_state.current_repository, None);
assert!(app.ecr_state.images.items.is_empty());
}
#[test]
fn test_ecr_esc_collapses_expanded_image_first() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.current_repository = Some("my-repo".to_string());
app.ecr_state.images.expanded_item = Some(0);
app.handle_action(Action::GoBack);
assert_eq!(app.ecr_state.images.expanded_item, None);
assert_eq!(
app.ecr_state.current_repository,
Some("my-repo".to_string())
);
}
#[test]
fn test_pagination_with_lowercase_p() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.items = (0..100)
.map(|i| EcrRepository {
name: format!("repo{}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.handle_action(Action::FilterInput('2'));
assert_eq!(app.page_input, "2");
app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
#[test]
fn test_lowercase_p_without_number_opens_preferences() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_ctrl_o_generates_correct_console_url() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.config.account_id = "123456789012".to_string();
let url = app.get_console_url();
assert!(url.contains("ecr/private-registry/repositories"));
assert!(url.contains("region=us-east-1"));
app.ecr_state.current_repository = Some("my-repo".to_string());
let url = app.get_console_url();
assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
assert!(url.contains("region=us-east-1"));
}
#[test]
fn test_page_input_display_and_reset() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.items = (0..100)
.map(|i| EcrRepository {
name: format!("repo{}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.handle_action(Action::FilterInput('2'));
assert_eq!(app.page_input, "2");
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
#[test]
fn test_page_navigation_updates_scroll_offset_for_cfn() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.table.items = (0..100)
.map(|i| CfnStack {
name: format!("stack-{}", i),
stack_id: format!(
"arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
i
),
status: "CREATE_COMPLETE".to_string(),
created_time: "2023-01-01T00:00:00Z".to_string(),
updated_time: "2023-01-01T00:00:00Z".to_string(),
deleted_time: String::new(),
drift_status: "IN_SYNC".to_string(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: vec![],
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: vec![],
notification_arns: vec![],
})
.collect();
app.handle_action(Action::FilterInput('2'));
assert_eq!(app.page_input, "2");
app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, "");
let page_size = app.cfn_state.table.page_size.value();
let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
let current_page = app.cfn_state.table.scroll_offset / page_size;
assert_eq!(
current_page, 1,
"2p should go to page 2 (0-indexed as 1), not page 3"
); }
#[test]
fn test_3p_goes_to_page_3_not_page_5() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.table.items = (0..200)
.map(|i| CfnStack {
name: format!("stack-{}", i),
stack_id: format!(
"arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
i
),
status: "CREATE_COMPLETE".to_string(),
created_time: "2023-01-01T00:00:00Z".to_string(),
updated_time: "2023-01-01T00:00:00Z".to_string(),
deleted_time: String::new(),
drift_status: "IN_SYNC".to_string(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: vec![],
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: vec![],
notification_arns: vec![],
})
.collect();
app.handle_action(Action::FilterInput('3'));
app.handle_action(Action::OpenColumnSelector);
let page_size = app.cfn_state.table.page_size.value();
let current_page = app.cfn_state.table.scroll_offset / page_size;
assert_eq!(
current_page, 2,
"3p should go to page 3 (0-indexed as 2), not page 5"
);
assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
}
#[test]
fn test_log_streams_page_navigation_uses_correct_page_size() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.view_mode = ViewMode::Detail;
app.service_selected = true;
app.mode = Mode::Normal;
app.log_groups_state.log_streams = (0..100)
.map(|i| LogStream {
name: format!("stream-{}", i),
creation_time: None,
last_event_time: None,
})
.collect();
app.handle_action(Action::FilterInput('2'));
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.log_groups_state.stream_current_page, 1);
assert_eq!(app.log_groups_state.selected_stream, 0);
assert_eq!(
app.log_groups_state.stream_current_page, 1,
"2p should go to page 2 (0-indexed as 1), not page 3"
);
}
#[test]
fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
.map(|i| EcrRepository {
name: format!("repo{}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.handle_action(Action::FilterInput('3'));
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.ecr_state.repositories.selected, 50);
let page_size = app.ecr_state.repositories.page_size.value();
let current_page = app.ecr_state.repositories.selected / page_size;
assert_eq!(
current_page, 2,
"3p with page_size=25 should go to page 3 (0-indexed as 2)"
);
}
#[test]
fn test_page_navigation_updates_scroll_offset_for_alarms() {
let mut app = test_app();
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
app.mode = Mode::Normal;
app.alarms_state.table.items = (0..100)
.map(|i| Alarm {
name: format!("alarm-{}", i),
state: "OK".to_string(),
state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
description: String::new(),
metric_name: "CPUUtilization".to_string(),
namespace: "AWS/EC2".to_string(),
statistic: "Average".to_string(),
period: 300,
comparison_operator: "GreaterThanThreshold".to_string(),
threshold: 80.0,
actions_enabled: true,
state_reason: String::new(),
resource: String::new(),
dimensions: String::new(),
expression: String::new(),
alarm_type: "MetricAlarm".to_string(),
cross_account: String::new(),
})
.collect();
app.handle_action(Action::FilterInput('2'));
app.handle_action(Action::OpenColumnSelector);
let page_size = app.alarms_state.table.page_size.value();
let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
}
#[test]
fn test_ecr_pagination_with_65_repos() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.repositories.items = (0..65)
.map(|i| EcrRepository {
name: format!("repo{:02}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
assert_eq!(app.ecr_state.repositories.selected, 0);
let page_size = 50;
let current_page = app.ecr_state.repositories.selected / page_size;
assert_eq!(current_page, 0);
app.handle_action(Action::FilterInput('2'));
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.ecr_state.repositories.selected, 50);
let current_page = app.ecr_state.repositories.selected / page_size;
assert_eq!(current_page, 1);
}
#[test]
fn test_ecr_repos_input_focus_tab_cycling() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.input_focus = InputFocus::Filter;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_ecr_images_column_toggle_not_off_by_one() {
use crate::ecr::image::Column as ImageColumn;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.ecr_state.current_repository = Some("test-repo".to_string());
app.ecr_image_visible_column_ids = ImageColumn::ids();
let initial_count = app.ecr_image_visible_column_ids.len();
app.column_selector_index = 0;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
assert!(!app
.ecr_image_visible_column_ids
.contains(&ImageColumn::Tag.id()));
app.handle_action(Action::ToggleColumn);
assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
assert!(app
.ecr_image_visible_column_ids
.contains(&ImageColumn::Tag.id()));
}
#[test]
fn test_ecr_repos_column_toggle_works() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.ecr_state.current_repository = None;
app.ecr_repo_visible_column_ids = EcrColumn::ids();
let initial_count = app.ecr_repo_visible_column_ids.len();
app.column_selector_index = 1;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
assert!(!app
.ecr_repo_visible_column_ids
.contains(&EcrColumn::Name.id()));
app.handle_action(Action::ToggleColumn);
assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
assert!(app
.ecr_repo_visible_column_ids
.contains(&EcrColumn::Name.id()));
}
#[test]
fn test_ecr_repos_pagination_left_right_navigation() {
use crate::ecr::repo::Repository as EcrRepository;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.input_focus = InputFocus::Pagination;
app.ecr_state.repositories.items = (0..150)
.map(|i| EcrRepository {
name: format!("repo{:03}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.ecr_state.repositories.selected = 0;
eprintln!(
"Initial: selected={}, focus={:?}, mode={:?}",
app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
);
app.handle_action(Action::PageDown);
eprintln!(
"After PageDown: selected={}",
app.ecr_state.repositories.selected
);
assert_eq!(app.ecr_state.repositories.selected, 50);
app.handle_action(Action::PageDown);
eprintln!(
"After 2nd PageDown: selected={}",
app.ecr_state.repositories.selected
);
assert_eq!(app.ecr_state.repositories.selected, 100);
app.handle_action(Action::PageDown);
eprintln!(
"After 3rd PageDown: selected={}",
app.ecr_state.repositories.selected
);
assert_eq!(app.ecr_state.repositories.selected, 100);
app.handle_action(Action::PageUp);
eprintln!(
"After PageUp: selected={}",
app.ecr_state.repositories.selected
);
assert_eq!(app.ecr_state.repositories.selected, 50);
app.handle_action(Action::PageUp);
eprintln!(
"After 2nd PageUp: selected={}",
app.ecr_state.repositories.selected
);
assert_eq!(app.ecr_state.repositories.selected, 0);
app.handle_action(Action::PageUp);
eprintln!(
"After 3rd PageUp: selected={}",
app.ecr_state.repositories.selected
);
assert_eq!(app.ecr_state.repositories.selected, 0);
}
#[test]
fn test_ecr_repos_filter_input_when_input_focused() {
use crate::ecr::repo::Repository as EcrRepository;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.input_focus = InputFocus::Filter;
app.ecr_state.repositories.items = vec![
EcrRepository {
name: "test-repo".to_string(),
uri: "uri1".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
},
EcrRepository {
name: "prod-repo".to_string(),
uri: "uri2".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
},
];
assert_eq!(app.ecr_state.repositories.filter, "");
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.ecr_state.repositories.filter, "t");
app.handle_action(Action::FilterInput('e'));
assert_eq!(app.ecr_state.repositories.filter, "te");
app.handle_action(Action::FilterInput('s'));
assert_eq!(app.ecr_state.repositories.filter, "tes");
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.ecr_state.repositories.filter, "test");
}
#[test]
fn test_ecr_repos_digit_input_when_pagination_focused() {
use crate::ecr::repo::Repository as EcrRepository;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.input_focus = InputFocus::Pagination;
app.ecr_state.repositories.items = vec![EcrRepository {
name: "test-repo".to_string(),
uri: "uri1".to_string(),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
}];
assert_eq!(app.ecr_state.repositories.filter, "");
assert_eq!(app.page_input, "");
app.handle_action(Action::FilterInput('2'));
assert_eq!(app.ecr_state.repositories.filter, "");
assert_eq!(app.page_input, "2");
app.handle_action(Action::FilterInput('a'));
assert_eq!(app.ecr_state.repositories.filter, "");
assert_eq!(app.page_input, "2");
}
#[test]
fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
use crate::ecr::repo::Repository as EcrRepository;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.input_focus = InputFocus::Filter;
app.ecr_state.repositories.items = (0..150)
.map(|i| EcrRepository {
name: format!("repo{:03}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.ecr_state.repositories.selected = 0;
app.handle_action(Action::PageDown);
assert_eq!(
app.ecr_state.repositories.selected, 10,
"Should scroll down by 10"
);
app.handle_action(Action::PageUp);
assert_eq!(
app.ecr_state.repositories.selected, 0,
"Should scroll back up"
);
}
#[test]
fn test_ecr_repos_pagination_control_actually_works() {
use crate::ecr::repo::Repository as EcrRepository;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.current_repository = None;
app.ecr_state.input_focus = InputFocus::Pagination;
app.ecr_state.repositories.items = (0..100)
.map(|i| EcrRepository {
name: format!("repo{:03}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.ecr_state.repositories.selected = 0;
assert_eq!(app.mode, Mode::FilterInput);
assert_eq!(app.current_service, Service::EcrRepositories);
assert_eq!(app.ecr_state.current_repository, None);
assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::PageDown);
assert_eq!(
app.ecr_state.repositories.selected, 50,
"PageDown should move to page 2"
);
app.handle_action(Action::PageUp);
assert_eq!(
app.ecr_state.repositories.selected, 0,
"PageUp should move back to page 1"
);
}
#[test]
fn test_ecr_repos_can_reach_last_page_when_not_multiple_of_page_size() {
use crate::ecr::repo::Repository as EcrRepository;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ecr_state.input_focus = InputFocus::Pagination;
app.ecr_state.repositories.items = (0..754)
.map(|i| EcrRepository {
name: format!("repo{:03}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.ecr_state.repositories.selected = 700;
app.handle_action(Action::PageDown);
assert_eq!(
app.ecr_state.repositories.selected, 750,
"Should reach last page (750) but got {}",
app.ecr_state.repositories.selected
);
app.handle_action(Action::PageDown);
assert_eq!(app.ecr_state.repositories.selected, 750);
}
#[test]
fn test_ecr_repos_start_filter_resets_focus_to_input() {
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.current_repository = None;
app.ecr_state.input_focus = InputFocus::Pagination;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
use crate::ecr::repo::Repository as EcrRepository;
let mut app = test_app();
app.current_service = Service::EcrRepositories;
app.service_selected = true;
app.mode = Mode::Normal;
app.ecr_state.current_repository = None;
app.ecr_state.repositories.items = (0..100)
.map(|i| EcrRepository {
name: format!("repo{:03}", i),
uri: format!("uri{}", i),
created_at: "2023-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
})
.collect();
app.ecr_state.repositories.selected = 0;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
app.handle_action(Action::PageDown);
eprintln!(
"After PageDown: selected={}",
app.ecr_state.repositories.selected
);
assert_eq!(
app.ecr_state.repositories.selected, 50,
"Right arrow should move to page 2"
);
app.handle_action(Action::PageUp);
assert_eq!(
app.ecr_state.repositories.selected, 0,
"Left arrow should move back to page 1"
);
}
#[test]
fn test_apig_filter_input() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.apig_state.apis.filter, "test");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.apig_state.apis.filter, "tes");
}
#[test]
fn test_apig_start_filter_enters_filter_mode() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::Normal;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_apig_input_focus_cycles_with_tab() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.apig_state.input_focus = InputFocus::Filter;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_apig_input_focus_cycles_with_shift_tab() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.apig_state.input_focus = InputFocus::Filter;
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_apig_exact_user_flow_i_tab_filter() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::Normal;
app.apig_state.apis.items = vec![
crate::apig::api::RestApi {
id: "api1".to_string(),
name: "test-api".to_string(),
description: "Test API".to_string(),
created_date: "2023-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
},
crate::apig::api::RestApi {
id: "api2".to_string(),
name: "prod-api".to_string(),
description: "Production API".to_string(),
created_date: "2023-01-02".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
},
];
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.apig_state.apis.filter, "test");
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_apig_row_expansion() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.apig_state.apis.items = vec![crate::apig::api::RestApi {
id: "api1".to_string(),
name: "test-api".to_string(),
description: "Test API".to_string(),
created_date: "2023-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
}];
app.apig_state.apis.selected = 0;
assert_eq!(app.apig_state.apis.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.apig_state.apis.expanded_item, None);
app.handle_action(Action::ExpandRow);
assert_eq!(app.apig_state.apis.expanded_item, Some(0));
app.handle_action(Action::NextPane);
assert_eq!(app.apig_state.apis.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.apig_state.apis.expanded_item, None);
}
#[test]
fn test_apig_column_preferences() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
let initial_count = app.apig_api_visible_column_ids.len();
assert_eq!(initial_count, app.apig_api_column_ids.len());
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.column_selector_index = 1;
let first_col = app.apig_api_column_ids[0];
assert!(app.apig_api_visible_column_ids.contains(&first_col));
app.handle_action(Action::ToggleColumn);
assert!(!app.apig_api_visible_column_ids.contains(&first_col));
assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
app.handle_action(Action::ToggleColumn);
assert!(app.apig_api_visible_column_ids.contains(&first_col));
assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
}
#[test]
fn test_apig_page_size_preferences() {
use crate::common::PageSize;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
app.handle_action(Action::OpenColumnSelector);
app.column_selector_index = app.apig_api_column_ids.len() + 3;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
app.column_selector_index = app.apig_api_column_ids.len() + 6;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
}
#[test]
fn test_apig_preferences_skip_blank_row() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 8;
app.handle_action(Action::NextItem);
assert_eq!(app.column_selector_index, 10);
app.handle_action(Action::PrevItem);
assert_eq!(app.column_selector_index, 8);
}
#[test]
fn test_apig_preferences_ctrl_d_skip_blank_row() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.column_selector_index, 10);
app.handle_action(Action::PageUp);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_apig_preferences_ctrl_u_skip_blank_row() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 10;
app.handle_action(Action::PageUp);
assert_eq!(app.column_selector_index, 0);
app.column_selector_index = 14;
app.handle_action(Action::PageUp);
assert_eq!(app.column_selector_index, 4);
}
#[test]
fn test_apig_preferences_tab_cycles_sections() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 10);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_apig_preferences_shift_tab_cycles_sections() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 10);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_apig_arrow_navigation() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::Normal; app.apig_state.apis.filter = String::new(); app.apig_state.apis.items = vec![
crate::apig::api::RestApi {
id: "api1".to_string(),
name: "test-api-1".to_string(),
description: "Test API 1".to_string(),
created_date: "2023-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
},
crate::apig::api::RestApi {
id: "api2".to_string(),
name: "test-api-2".to_string(),
description: "Test API 2".to_string(),
created_date: "2023-01-02".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
},
];
app.apig_state.apis.selected = 0;
app.handle_action(Action::NextItem);
assert_eq!(app.apig_state.apis.selected, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.apig_state.apis.selected, 0);
}
#[test]
fn test_apig_collapse_row() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.apig_state.apis.items = vec![crate::apig::api::RestApi {
id: "api1".to_string(),
name: "test-api".to_string(),
description: "Test API".to_string(),
created_date: "2023-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
}];
app.apig_state.apis.selected = 0;
app.apig_state.apis.expanded_item = Some(0);
app.handle_action(Action::CollapseRow);
assert_eq!(app.apig_state.apis.expanded_item, None);
}
#[test]
fn test_apig_filter_resets_selection() {
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.apig_state.input_focus = InputFocus::Filter;
app.apig_state.apis.items = vec![
crate::apig::api::RestApi {
id: "api1".to_string(),
name: "alpha-api".to_string(),
description: "Alpha API".to_string(),
created_date: "2023-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
},
crate::apig::api::RestApi {
id: "api2".to_string(),
name: "beta-api".to_string(),
description: "Beta API".to_string(),
created_date: "2023-01-02".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
},
];
app.apig_state.apis.selected = 1;
app.apig_state.apis.expanded_item = Some(1);
app.handle_action(Action::FilterInput('a'));
assert_eq!(app.apig_state.apis.selected, 0);
assert_eq!(app.apig_state.apis.expanded_item, None);
assert_eq!(app.apig_state.apis.filter, "a");
}
#[test]
fn test_service_picker_starts_in_normal_mode() {
let app = test_app();
assert_eq!(app.mode, Mode::ServicePicker);
assert!(!app.service_picker.filter_active);
}
#[test]
fn test_service_picker_i_key_activates_filter() {
let mut app = test_app();
assert_eq!(app.mode, Mode::ServicePicker);
assert!(!app.service_picker.filter_active);
assert!(app.service_picker.filter.is_empty());
app.handle_action(Action::EnterFilterMode);
assert_eq!(app.mode, Mode::ServicePicker);
assert!(app.service_picker.filter_active);
assert!(app.service_picker.filter.is_empty());
app.handle_action(Action::FilterInput('s'));
assert_eq!(app.service_picker.filter, "s");
}
#[test]
fn test_service_picker_esc_exits_filter_mode() {
let mut app = test_app();
assert_eq!(app.mode, Mode::ServicePicker);
assert!(!app.service_picker.filter_active);
app.handle_action(Action::EnterFilterMode);
assert!(app.service_picker.filter_active);
assert_eq!(app.mode, Mode::ServicePicker);
app.handle_action(Action::FilterInput('s'));
assert_eq!(app.service_picker.filter, "s");
app.handle_action(Action::ExitFilterMode);
assert!(!app.service_picker.filter_active);
assert_eq!(app.mode, Mode::ServicePicker);
app.handle_action(Action::ExitFilterMode);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_service_picker_navigation_only_works_in_normal_mode() {
let mut app = test_app();
app.service_picker.selected = 0;
assert!(!app.service_picker.filter_active);
app.handle_action(Action::NextItem);
assert_eq!(app.service_picker.selected, 1);
app.handle_action(Action::EnterFilterMode);
assert!(app.service_picker.filter_active);
let prev_selected = app.service_picker.selected;
app.handle_action(Action::NextItem);
assert_eq!(app.service_picker.selected, prev_selected);
app.handle_action(Action::ExitFilterMode);
assert!(!app.service_picker.filter_active);
app.handle_action(Action::NextItem);
assert_eq!(app.service_picker.selected, prev_selected + 1);
}
#[test]
fn test_service_picker_typing_filters_services() {
let mut app = test_app();
assert_eq!(app.mode, Mode::ServicePicker);
assert!(!app.service_picker.filter_active);
app.handle_action(Action::EnterFilterMode);
assert!(app.service_picker.filter_active);
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('3'));
assert_eq!(app.service_picker.filter, "s3");
assert_eq!(app.mode, Mode::ServicePicker);
}
#[test]
fn test_service_picker_enter_exits_insert_mode() {
let mut app = test_app();
assert_eq!(app.mode, Mode::ServicePicker);
assert!(!app.service_selected);
app.handle_action(Action::EnterFilterMode);
assert!(app.service_picker.filter_active);
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('3'));
app.handle_action(Action::Select);
assert!(!app.service_selected, "Should not select service");
assert!(!app.service_picker.filter_active, "Should exit INSERT mode");
assert_eq!(app.mode, Mode::ServicePicker);
app.handle_action(Action::Select);
assert!(app.service_selected, "Should select service in NORMAL mode");
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_service_picker_resets_on_open() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::Normal;
app.service_picker.filter = "previous".to_string();
app.service_picker.filter_active = true;
app.service_picker.selected = 5;
app.handle_action(Action::OpenSpaceMenu);
assert_eq!(app.mode, Mode::SpaceMenu);
assert!(app.service_picker.filter.is_empty());
assert!(!app.service_picker.filter_active);
assert_eq!(app.service_picker.selected, 0);
}
#[test]
fn test_no_pii_in_test_data() {
let test_repo = EcrRepository {
name: "test-repo".to_string(),
uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
created_at: "2024-01-01".to_string(),
tag_immutability: "MUTABLE".to_string(),
encryption_type: "AES256".to_string(),
};
assert!(test_repo.uri.starts_with("123456789012"));
assert!(!test_repo.uri.contains("123456789013")); }
#[test]
fn test_lambda_versions_tab_triggers_loading() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
assert!(app.lambda_state.version_table.items.is_empty());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
assert!(app.lambda_state.current_function.is_some());
}
#[test]
fn test_lambda_versions_navigation() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.lambda_state.version_table.items = vec![
LambdaVersion {
version: "3".to_string(),
aliases: "prod".to_string(),
description: "".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
},
LambdaVersion {
version: "2".to_string(),
aliases: "".to_string(),
description: "".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
},
LambdaVersion {
version: "1".to_string(),
aliases: "".to_string(),
description: "".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
},
];
assert_eq!(app.lambda_state.version_table.items.len(), 3);
assert_eq!(app.lambda_state.version_table.items[0].version, "3");
assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
app.lambda_state.version_table.selected = 1;
assert_eq!(app.lambda_state.version_table.selected, 1);
}
#[test]
fn test_lambda_versions_with_aliases() {
let version = LambdaVersion {
version: "35".to_string(),
aliases: "prod, staging".to_string(),
description: "Production version".to_string(),
last_modified: "2024-01-01".to_string(),
architecture: "X86_64".to_string(),
};
assert_eq!(version.aliases, "prod, staging");
assert!(!version.aliases.is_empty());
}
#[test]
fn test_lambda_versions_expansion() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.lambda_state.version_table.items = vec![
LambdaVersion {
version: "2".to_string(),
aliases: "prod".to_string(),
description: "Production".to_string(),
last_modified: "2024-01-01".to_string(),
architecture: "X86_64".to_string(),
},
LambdaVersion {
version: "1".to_string(),
aliases: "".to_string(),
description: "".to_string(),
last_modified: "2024-01-01".to_string(),
architecture: "Arm64".to_string(),
},
];
app.lambda_state.version_table.selected = 0;
app.lambda_state.version_table.expanded_item = Some(0);
assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
app.lambda_state.version_table.selected = 1;
app.lambda_state.version_table.expanded_item = Some(1);
assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
}
#[test]
fn test_lambda_versions_page_navigation() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.lambda_state.version_table.items = (1..=30)
.map(|i| LambdaVersion {
version: i.to_string(),
aliases: "".to_string(),
description: "".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
})
.collect();
app.lambda_state.version_table.page_size = PageSize::Ten;
app.lambda_state.version_table.selected = 0;
app.page_input = "2".to_string();
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.lambda_state.version_table.selected, 10);
}
#[test]
fn test_lambda_versions_pagination_arrow_keys() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.mode = Mode::FilterInput;
app.lambda_state.version_input_focus = InputFocus::Pagination;
app.lambda_state.version_table.items = (1..=30)
.map(|i| LambdaVersion {
version: i.to_string(),
aliases: "".to_string(),
description: "".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
})
.collect();
app.lambda_state.version_table.page_size = PageSize::Ten;
app.lambda_state.version_table.selected = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.lambda_state.version_table.selected, 10);
app.handle_action(Action::PageUp);
assert_eq!(app.lambda_state.version_table.selected, 0);
}
#[test]
fn test_lambda_versions_page_input_in_filter_mode() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.mode = Mode::FilterInput;
app.lambda_state.version_input_focus = InputFocus::Pagination;
app.lambda_state.version_table.items = (1..=30)
.map(|i| LambdaVersion {
version: i.to_string(),
aliases: "".to_string(),
description: "".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
})
.collect();
app.lambda_state.version_table.page_size = PageSize::Ten;
app.lambda_state.version_table.selected = 0;
app.handle_action(Action::FilterInput('2'));
assert_eq!(app.page_input, "2");
assert_eq!(app.lambda_state.version_table.filter, "");
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.lambda_state.version_table.selected, 10);
assert_eq!(app.page_input, ""); }
#[test]
fn test_lambda_versions_filter_input() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.mode = Mode::FilterInput;
app.lambda_state.version_input_focus = InputFocus::Filter;
app.lambda_state.version_table.items = vec![
LambdaVersion {
version: "1".to_string(),
aliases: "prod".to_string(),
description: "Production".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
},
LambdaVersion {
version: "2".to_string(),
aliases: "staging".to_string(),
description: "Staging".to_string(),
last_modified: "".to_string(),
architecture: "X86_64".to_string(),
},
];
app.handle_action(Action::FilterInput('p'));
app.handle_action(Action::FilterInput('r'));
app.handle_action(Action::FilterInput('o'));
app.handle_action(Action::FilterInput('d'));
assert_eq!(app.lambda_state.version_table.filter, "prod");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.lambda_state.version_table.filter, "pro");
}
#[test]
fn test_lambda_aliases_table_expansion() {
use crate::lambda::Alias;
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
app.mode = Mode::Normal;
app.lambda_state.alias_table.items = vec![
Alias {
name: "prod".to_string(),
versions: "1".to_string(),
description: "Production alias".to_string(),
},
Alias {
name: "staging".to_string(),
versions: "2".to_string(),
description: "Staging alias".to_string(),
},
];
app.lambda_state.alias_table.selected = 0;
app.handle_action(Action::Select);
assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
app.handle_action(Action::GoBack);
assert_eq!(app.lambda_state.current_alias, None);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
app.lambda_state.alias_table.selected = 1;
app.handle_action(Action::Select);
assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
}
#[test]
fn test_lambda_versions_arrow_key_expansion() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.mode = Mode::Normal;
app.lambda_state.version_table.items = vec![LambdaVersion {
version: "1".to_string(),
aliases: "prod".to_string(),
description: "Production".to_string(),
last_modified: "2024-01-01".to_string(),
architecture: "X86_64".to_string(),
}];
app.lambda_state.version_table.selected = 0;
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.lambda_state.version_table.expanded_item, None);
}
#[test]
fn test_lambda_version_detail_view() {
use crate::lambda::Function;
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Versions;
app.mode = Mode::Normal;
app.lambda_state.table.items = vec![Function {
name: "test-function".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
application: None,
description: "Test".to_string(),
package_type: "Zip".to_string(),
runtime: "python3.12".to_string(),
architecture: "X86_64".to_string(),
code_size: 1024,
code_sha256: "hash".to_string(),
memory_mb: 128,
timeout_seconds: 30,
last_modified: "2024-01-01".to_string(),
layers: vec![],
}];
app.lambda_state.version_table.items = vec![LambdaVersion {
version: "1".to_string(),
aliases: "prod".to_string(),
description: "Production".to_string(),
last_modified: "2024-01-01".to_string(),
architecture: "X86_64".to_string(),
}];
app.lambda_state.version_table.selected = 0;
app.handle_action(Action::Select);
assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
app.handle_action(Action::GoBack);
assert_eq!(app.lambda_state.current_version, None);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
}
#[test]
fn test_lambda_version_detail_tabs() {
use crate::lambda::Function;
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.current_version = Some("1".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.mode = Mode::Normal;
app.lambda_state.table.items = vec![Function {
name: "test-function".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
application: None,
description: "Test".to_string(),
package_type: "Zip".to_string(),
runtime: "python3.12".to_string(),
architecture: "X86_64".to_string(),
code_size: 1024,
code_sha256: "hash".to_string(),
memory_mb: 128,
timeout_seconds: 30,
last_modified: "2024-01-01".to_string(),
layers: vec![],
}];
app.handle_action(Action::NextDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
}
#[test]
fn test_lambda_aliases_arrow_key_expansion() {
use crate::lambda::Alias;
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
app.mode = Mode::Normal;
app.lambda_state.alias_table.items = vec![Alias {
name: "prod".to_string(),
versions: "1".to_string(),
description: "Production alias".to_string(),
}];
app.lambda_state.alias_table.selected = 0;
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.lambda_state.alias_table.expanded_item, None);
}
#[test]
fn test_lambda_functions_arrow_key_expansion() {
use crate::lambda::Function;
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.mode = Mode::Normal;
app.lambda_state.table.items = vec![Function {
name: "test-function".to_string(),
arn: "arn".to_string(),
application: None,
description: "Test".to_string(),
package_type: "Zip".to_string(),
runtime: "python3.12".to_string(),
architecture: "X86_64".to_string(),
code_size: 1024,
code_sha256: "hash".to_string(),
memory_mb: 128,
timeout_seconds: 30,
last_modified: "2024-01-01".to_string(),
layers: vec![],
}];
app.lambda_state.table.selected = 0;
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.table.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.lambda_state.table.expanded_item, None);
}
#[test]
fn test_lambda_version_detail_with_application() {
use crate::lambda::Function;
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
app.lambda_state.current_version = Some("1".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.mode = Mode::Normal;
app.lambda_state.table.items = vec![Function {
name: "storefront-studio-beta-api".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
.to_string(),
application: Some("storefront-studio-beta".to_string()),
description: "API function".to_string(),
package_type: "Zip".to_string(),
runtime: "python3.12".to_string(),
architecture: "X86_64".to_string(),
code_size: 1024,
code_sha256: "hash".to_string(),
memory_mb: 128,
timeout_seconds: 30,
last_modified: "2024-01-01".to_string(),
layers: vec![],
}];
assert_eq!(
app.lambda_state.table.items[0].application,
Some("storefront-studio-beta".to_string())
);
assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
}
#[test]
fn test_lambda_layer_navigation() {
use crate::lambda::{Function, Layer};
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.mode = Mode::Normal;
app.lambda_state.table.items = vec![Function {
name: "test-function".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
application: None,
description: "Test".to_string(),
package_type: "Zip".to_string(),
runtime: "python3.12".to_string(),
architecture: "X86_64".to_string(),
code_size: 1024,
code_sha256: "hash".to_string(),
memory_mb: 128,
timeout_seconds: 30,
last_modified: "2024-01-01".to_string(),
layers: vec![
Layer {
merge_order: "1".to_string(),
name: "layer1".to_string(),
layer_version: "1".to_string(),
compatible_runtimes: "python3.9".to_string(),
compatible_architectures: "x86_64".to_string(),
version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
},
Layer {
merge_order: "2".to_string(),
name: "layer2".to_string(),
layer_version: "2".to_string(),
compatible_runtimes: "python3.9".to_string(),
compatible_architectures: "x86_64".to_string(),
version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
},
Layer {
merge_order: "3".to_string(),
name: "layer3".to_string(),
layer_version: "3".to_string(),
compatible_runtimes: "python3.9".to_string(),
compatible_architectures: "x86_64".to_string(),
version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
},
],
}];
assert_eq!(app.lambda_state.layer_selected, 0);
app.handle_action(Action::NextItem);
assert_eq!(app.lambda_state.layer_selected, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.lambda_state.layer_selected, 2);
app.handle_action(Action::NextItem);
assert_eq!(app.lambda_state.layer_selected, 2);
app.handle_action(Action::PrevItem);
assert_eq!(app.lambda_state.layer_selected, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.lambda_state.layer_selected, 0);
app.handle_action(Action::PrevItem);
assert_eq!(app.lambda_state.layer_selected, 0);
}
#[test]
fn test_lambda_layer_expansion() {
use crate::lambda::{Function, Layer};
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.mode = Mode::Normal;
app.lambda_state.table.items = vec![Function {
name: "test-function".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
application: None,
description: "Test".to_string(),
package_type: "Zip".to_string(),
runtime: "python3.12".to_string(),
architecture: "X86_64".to_string(),
code_size: 1024,
code_sha256: "hash".to_string(),
memory_mb: 128,
timeout_seconds: 30,
last_modified: "2024-01-01".to_string(),
layers: vec![Layer {
merge_order: "1".to_string(),
name: "test-layer".to_string(),
layer_version: "1".to_string(),
compatible_runtimes: "python3.9".to_string(),
compatible_architectures: "x86_64".to_string(),
version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
}],
}];
assert_eq!(app.lambda_state.layer_expanded, None);
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.layer_expanded, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.lambda_state.layer_expanded, None);
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.layer_expanded, Some(0));
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.layer_expanded, None);
}
#[test]
fn test_lambda_layer_selection_and_expansion_workflow() {
use crate::lambda::{Function, Layer};
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.mode = Mode::Normal;
app.lambda_state.table.items = vec![Function {
name: "test-function".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
application: None,
description: "Test".to_string(),
package_type: "Zip".to_string(),
runtime: "python3.12".to_string(),
architecture: "X86_64".to_string(),
code_size: 1024,
code_sha256: "hash".to_string(),
memory_mb: 128,
timeout_seconds: 30,
last_modified: "2024-01-01".to_string(),
layers: vec![
Layer {
merge_order: "1".to_string(),
name: "layer1".to_string(),
layer_version: "1".to_string(),
compatible_runtimes: "python3.9".to_string(),
compatible_architectures: "x86_64".to_string(),
version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
},
Layer {
merge_order: "2".to_string(),
name: "layer2".to_string(),
layer_version: "2".to_string(),
compatible_runtimes: "python3.9".to_string(),
compatible_architectures: "x86_64".to_string(),
version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
},
],
}];
assert_eq!(app.lambda_state.layer_selected, 0);
assert_eq!(app.lambda_state.layer_expanded, None);
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.layer_selected, 0);
assert_eq!(app.lambda_state.layer_expanded, Some(0));
app.handle_action(Action::NextItem);
assert_eq!(app.lambda_state.layer_selected, 1);
assert_eq!(app.lambda_state.layer_expanded, Some(0));
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.layer_selected, 1);
assert_eq!(app.lambda_state.layer_expanded, Some(1));
app.handle_action(Action::PrevPane);
assert_eq!(app.lambda_state.layer_selected, 1);
assert_eq!(app.lambda_state.layer_expanded, None);
app.handle_action(Action::PrevItem);
assert_eq!(app.lambda_state.layer_selected, 0);
assert_eq!(app.lambda_state.layer_expanded, None);
}
#[test]
fn test_backtab_cycles_detail_tabs_backward() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
app.current_service = Service::IamRoles;
app.iam_state.current_role = Some("test-role".to_string());
app.iam_state.role_tab = RoleTab::Permissions;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
app.current_service = Service::IamUsers;
app.iam_state.current_user = Some("test-user".to_string());
app.iam_state.user_tab = UserTab::Permissions;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
app.current_service = Service::IamUserGroups;
app.iam_state.current_group = Some("test-group".to_string());
app.iam_state.group_tab = GroupTab::Permissions;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.iam_state.group_tab, GroupTab::Users);
app.current_service = Service::S3Buckets;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.object_tab = S3ObjectTab::Properties;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
app.current_service = Service::EcrRepositories;
app.ecr_state.current_repository = None;
app.ecr_state.tab = EcrTab::Private;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.ecr_state.tab, EcrTab::Public);
app.current_service = Service::CloudFormationStacks;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Resources;
}
#[test]
fn test_cloudformation_status_filter_active() {
let filter = CfnStatusFilter::Active;
assert!(filter.matches("CREATE_IN_PROGRESS"));
assert!(filter.matches("UPDATE_IN_PROGRESS"));
assert!(!filter.matches("CREATE_COMPLETE"));
assert!(!filter.matches("DELETE_COMPLETE"));
assert!(!filter.matches("CREATE_FAILED"));
}
#[test]
fn test_cloudformation_status_filter_complete() {
let filter = CfnStatusFilter::Complete;
assert!(filter.matches("CREATE_COMPLETE"));
assert!(filter.matches("UPDATE_COMPLETE"));
assert!(!filter.matches("DELETE_COMPLETE"));
assert!(!filter.matches("CREATE_IN_PROGRESS"));
}
#[test]
fn test_cloudformation_status_filter_failed() {
let filter = CfnStatusFilter::Failed;
assert!(filter.matches("CREATE_FAILED"));
assert!(filter.matches("UPDATE_FAILED"));
assert!(!filter.matches("CREATE_COMPLETE"));
}
#[test]
fn test_cloudformation_status_filter_deleted() {
let filter = CfnStatusFilter::Deleted;
assert!(filter.matches("DELETE_COMPLETE"));
assert!(filter.matches("DELETE_IN_PROGRESS"));
assert!(!filter.matches("CREATE_COMPLETE"));
}
#[test]
fn test_cloudformation_status_filter_in_progress() {
let filter = CfnStatusFilter::InProgress;
assert!(filter.matches("CREATE_IN_PROGRESS"));
assert!(filter.matches("UPDATE_IN_PROGRESS"));
assert!(filter.matches("DELETE_IN_PROGRESS"));
assert!(!filter.matches("CREATE_COMPLETE"));
}
#[test]
fn test_cloudformation_status_filter_cycle() {
let filter = CfnStatusFilter::All;
assert_eq!(filter.next(), CfnStatusFilter::Active);
assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
assert_eq!(
filter.next().next().next().next().next(),
CfnStatusFilter::InProgress
);
assert_eq!(
filter.next().next().next().next().next().next(),
CfnStatusFilter::All
);
}
#[test]
fn test_cloudformation_default_columns() {
let app = test_app();
assert_eq!(app.cfn_visible_column_ids.len(), 4);
assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
assert!(app
.cfn_visible_column_ids
.contains(&CfnColumn::CreatedTime.id()));
assert!(app
.cfn_visible_column_ids
.contains(&CfnColumn::Description.id()));
}
#[test]
fn test_cloudformation_all_columns() {
let app = test_app();
assert_eq!(app.cfn_column_ids.len(), 10);
}
#[test]
fn test_cloudformation_filter_by_name() {
let mut app = test_app();
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![
CfnStack {
name: "my-stack".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
CfnStack {
name: "other-stack".to_string(),
stack_id: "id2".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-02".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
];
app.cfn_state.table.filter = "my".to_string();
let filtered = filtered_cloudformation_stacks(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "my-stack");
}
#[test]
fn test_cloudformation_filter_by_description() {
let mut app = test_app();
app.cfn_state.status_filter = CfnStatusFilter::Complete;
app.cfn_state.table.items = vec![CfnStack {
name: "stack1".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: "production stack".to_string(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
}];
app.cfn_state.table.filter = "production".to_string();
let filtered = filtered_cloudformation_stacks(&app);
assert_eq!(filtered.len(), 1);
}
#[test]
fn test_cloudformation_status_filter_applied() {
let mut app = test_app();
app.cfn_state.table.items = vec![
CfnStack {
name: "complete-stack".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
CfnStack {
name: "failed-stack".to_string(),
stack_id: "id2".to_string(),
status: "CREATE_FAILED".to_string(),
created_time: "2024-01-02".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
];
app.cfn_state.status_filter = CfnStatusFilter::Complete;
let filtered = filtered_cloudformation_stacks(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "complete-stack");
app.cfn_state.status_filter = CfnStatusFilter::Failed;
let filtered = filtered_cloudformation_stacks(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "failed-stack");
}
#[test]
fn test_cloudformation_default_page_size() {
let app = test_app();
assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
}
#[test]
fn test_cloudformation_default_status_filter() {
let app = test_app();
assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
}
#[test]
fn test_cloudformation_view_nested_default_false() {
let app = test_app();
assert!(!app.cfn_state.view_nested);
}
#[test]
fn test_cloudformation_pagination_hotkeys() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.status_filter = CfnStatusFilter::All;
for i in 0..150 {
app.cfn_state.table.items.push(CfnStack {
name: format!("stack-{}", i),
stack_id: format!("id-{}", i),
status: "CREATE_COMPLETE".to_string(),
created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: vec![],
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: vec![],
notification_arns: vec![],
});
}
app.go_to_page(2);
assert_eq!(app.cfn_state.table.selected, 50);
app.go_to_page(3);
assert_eq!(app.cfn_state.table.selected, 100);
app.go_to_page(1);
assert_eq!(app.cfn_state.table.selected, 0);
}
#[test]
fn test_cloudformation_tab_cycling_in_filter_mode() {
use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = InputFocus::Filter;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_cloudformation_timestamp_format_includes_utc() {
let stack = CfnStack {
name: "test-stack".to_string(),
stack_id: "id-123".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: vec![],
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: vec![],
notification_arns: vec![],
};
assert!(stack.created_time.contains("(UTC)"));
assert!(stack.updated_time.contains("(UTC)"));
assert!(stack.last_drift_check_time.contains("(UTC)"));
assert_eq!(stack.created_time.len(), 25);
}
#[test]
fn test_cloudformation_enter_drills_into_stack_view() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::All;
app.tabs = vec![Tab {
service: Service::CloudFormationStacks,
title: "CloudFormation › Stacks".to_string(),
breadcrumb: "CloudFormation › Stacks".to_string(),
}];
app.current_tab = 0;
app.cfn_state.table.items.push(CfnStack {
name: "test-stack".to_string(),
stack_id: "id-123".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: vec![],
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: vec![],
notification_arns: vec![],
});
app.cfn_state.table.reset();
assert_eq!(app.cfn_state.current_stack, None);
app.handle_action(Action::Select);
assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
}
#[test]
fn test_cloudformation_arrow_keys_expand_collapse() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::All;
app.cfn_state.table.items.push(CfnStack {
name: "test-stack".to_string(),
stack_id: "id-123".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: vec![],
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: vec![],
notification_arns: vec![],
});
app.cfn_state.table.reset();
assert_eq!(app.cfn_state.table.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.cfn_state.table.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.cfn_state.table.expanded_item, None);
assert_eq!(app.cfn_state.current_stack, None);
}
#[test]
fn test_cloudformation_tab_cycling() {
use crate::ui::cfn::DetailTab;
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.status_filter = CfnStatusFilter::All;
app.cfn_state.current_stack = Some("test-stack".to_string());
assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
}
#[test]
fn test_cloudformation_console_url() {
use crate::ui::cfn::DetailTab;
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.status_filter = CfnStatusFilter::All;
app.cfn_state.table.items.push(CfnStack {
name: "test-stack".to_string(),
stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
.to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: vec![],
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: vec![],
notification_arns: vec![],
});
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = DetailTab::StackInfo;
let url = app.get_console_url();
assert!(url.contains("stackinfo"));
assert!(url.contains("arn%3Aaws%3Acloudformation"));
app.cfn_state.detail_tab = DetailTab::Events;
let url = app.get_console_url();
assert!(url.contains("events"));
assert!(url.contains("arn%3Aaws%3Acloudformation"));
}
#[test]
fn test_iam_role_select() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.roles.items = vec![
IamRole {
role_name: "role1".to_string(),
path: "/".to_string(),
trusted_entities: "AWS Service: ec2".to_string(),
last_activity: "-".to_string(),
arn: "arn:aws:iam::123456789012:role/role1".to_string(),
creation_time: "2025-01-01".to_string(),
description: "Test role 1".to_string(),
max_session_duration: Some(3600),
},
IamRole {
role_name: "role2".to_string(),
path: "/".to_string(),
trusted_entities: "AWS Service: lambda".to_string(),
last_activity: "-".to_string(),
arn: "arn:aws:iam::123456789012:role/role2".to_string(),
creation_time: "2025-01-02".to_string(),
description: "Test role 2".to_string(),
max_session_duration: Some(7200),
},
];
app.iam_state.roles.selected = 0;
app.handle_action(Action::Select);
assert_eq!(
app.iam_state.current_role,
Some("role1".to_string()),
"Should open role detail view"
);
assert_eq!(
app.iam_state.role_tab,
RoleTab::Permissions,
"Should default to Permissions tab"
);
}
#[test]
fn test_iam_role_back_navigation() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.iam_state.current_role = Some("test-role".to_string());
app.handle_action(Action::GoBack);
assert_eq!(
app.iam_state.current_role, None,
"Should return to roles list"
);
}
#[test]
fn test_iam_role_tab_navigation() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.iam_state.current_role = Some("test-role".to_string());
app.iam_state.role_tab = RoleTab::Permissions;
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.iam_state.role_tab,
RoleTab::TrustRelationships,
"Should move to next tab"
);
}
#[test]
fn test_iam_role_tab_cycle_order() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.iam_state.current_role = Some("test-role".to_string());
app.iam_state.role_tab = RoleTab::Permissions;
app.handle_action(Action::NextDetailTab);
assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.iam_state.role_tab,
RoleTab::Permissions,
"Should cycle back to first tab"
);
}
#[test]
fn test_iam_role_pagination() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.iam_state.roles.page_size = PageSize::Ten;
app.iam_state.roles.items = (0..25)
.map(|i| IamRole {
role_name: format!("role{}", i),
path: "/".to_string(),
trusted_entities: "AWS Service: ec2".to_string(),
last_activity: "-".to_string(),
arn: format!("arn:aws:iam::123456789012:role/role{}", i),
creation_time: "2025-01-01".to_string(),
description: format!("Test role {}", i),
max_session_duration: Some(3600),
})
.collect();
app.go_to_page(2);
assert_eq!(
app.iam_state.roles.selected, 10,
"Should select first item of page 2"
);
assert_eq!(
app.iam_state.roles.scroll_offset, 10,
"Should update scroll offset"
);
}
#[test]
fn test_tags_table_populated_on_role_detail() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.roles.items = vec![IamRole {
role_name: "TestRole".to_string(),
path: "/".to_string(),
trusted_entities: String::new(),
last_activity: String::new(),
arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
creation_time: "2025-01-01".to_string(),
description: String::new(),
max_session_duration: Some(3600),
}];
app.iam_state.tags.items = vec![
IamRoleTag {
key: "Environment".to_string(),
value: "Production".to_string(),
},
IamRoleTag {
key: "Team".to_string(),
value: "Platform".to_string(),
},
];
assert_eq!(app.iam_state.tags.items.len(), 2);
assert_eq!(app.iam_state.tags.items[0].key, "Environment");
assert_eq!(app.iam_state.tags.items[0].value, "Production");
assert_eq!(app.iam_state.tags.selected, 0);
}
#[test]
fn test_tags_table_navigation() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.role_tab = RoleTab::Tags;
app.iam_state.tags.items = vec![
IamRoleTag {
key: "Tag1".to_string(),
value: "Value1".to_string(),
},
IamRoleTag {
key: "Tag2".to_string(),
value: "Value2".to_string(),
},
];
app.handle_action(Action::NextItem);
assert_eq!(app.iam_state.tags.selected, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.iam_state.tags.selected, 0);
}
#[test]
fn test_last_accessed_table_navigation() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.current_role = Some("TestRole".to_string());
app.iam_state.role_tab = RoleTab::LastAccessed;
app.iam_state.last_accessed_services.items = vec![
LastAccessedService {
service: "S3".to_string(),
policies_granting: "Policy1".to_string(),
last_accessed: "2025-01-01".to_string(),
},
LastAccessedService {
service: "EC2".to_string(),
policies_granting: "Policy2".to_string(),
last_accessed: "2025-01-02".to_string(),
},
];
app.handle_action(Action::NextItem);
assert_eq!(app.iam_state.last_accessed_services.selected, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.iam_state.last_accessed_services.selected, 0);
}
#[test]
fn test_cfn_input_focus_next() {
use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = InputFocus::Filter;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_cfn_input_focus_prev() {
use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = InputFocus::Filter;
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_cw_logs_input_focus_prev() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.mode = Mode::FilterInput;
app.view_mode = ViewMode::Detail;
app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
app.log_groups_state.input_focus = InputFocus::Filter;
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(
app.log_groups_state.input_focus,
InputFocus::Checkbox("ShowExpired")
);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(
app.log_groups_state.input_focus,
InputFocus::Checkbox("ExactMatch")
);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_cw_events_input_focus_prev() {
use crate::ui::cw::logs::EventFilterFocus;
let mut app = test_app();
app.mode = Mode::EventFilterInput;
app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
app.handle_action(Action::PrevFilterFocus);
assert_eq!(
app.log_groups_state.event_input_focus,
EventFilterFocus::DateRange
);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(
app.log_groups_state.event_input_focus,
EventFilterFocus::Filter
);
}
#[test]
fn test_cfn_input_focus_cycle_complete() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = InputFocus::Filter;
for _ in 0..4 {
app.handle_action(Action::NextFilterFocus);
}
assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
for _ in 0..4 {
app.handle_action(Action::PrevFilterFocus);
}
assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_cfn_resources_pagination_left_right_arrows() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Resources;
app.cfn_state.resources_input_focus = InputFocus::Pagination;
app.cfn_state.resources.items = (0..60)
.map(|i| rusticity_core::cfn::StackResource {
logical_id: format!("Resource{}", i),
physical_id: format!("phys{}", i),
resource_type: "AWS::S3::Bucket".to_string(),
status: String::new(),
module_info: String::new(),
})
.collect();
app.cfn_state.resources.page_size = crate::common::PageSize::Ten;
app.cfn_state.resources.selected = 0;
app.handle_action(Action::PageDown);
assert_eq!(
app.cfn_state.resources.selected, 10,
"Right arrow with pagination focus must move to page 2 of Resources"
);
app.handle_action(Action::PageUp);
assert_eq!(
app.cfn_state.resources.selected, 0,
"Left arrow with pagination focus must move back to page 1 of Resources"
);
}
#[test]
fn test_cfn_filter_status_arrow_keys() {
use crate::ui::cfn::STATUS_FILTER;
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = STATUS_FILTER;
app.cfn_state.status_filter = CfnStatusFilter::All;
app.handle_action(Action::NextItem);
assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
app.handle_action(Action::PrevItem);
assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
}
#[test]
fn test_cfn_filter_shift_tab_cycles_backward() {
use crate::ui::cfn::STATUS_FILTER;
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = STATUS_FILTER;
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
}
#[test]
fn test_cfn_pagination_arrow_keys() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = InputFocus::Pagination;
app.cfn_state.table.scroll_offset = 0;
app.cfn_state.table.page_size = PageSize::Ten;
app.cfn_state.table.items = (0..30)
.map(|i| CfnStack {
name: format!("stack-{}", i),
stack_id: format!("id-{}", i),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
})
.collect();
app.handle_action(Action::PageDown);
assert_eq!(app.cfn_state.table.scroll_offset, 10);
let page_size = app.cfn_state.table.page_size.value();
let current_page = app.cfn_state.table.scroll_offset / page_size;
assert_eq!(current_page, 1);
app.handle_action(Action::PageUp);
assert_eq!(app.cfn_state.table.scroll_offset, 0);
let current_page = app.cfn_state.table.scroll_offset / page_size;
assert_eq!(current_page, 0);
}
#[test]
fn test_cfn_page_navigation_updates_selection() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::Normal;
app.cfn_state.table.items = (0..30)
.map(|i| CfnStack {
name: format!("stack-{}", i),
stack_id: format!("id-{}", i),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
})
.collect();
app.cfn_state.table.reset();
app.cfn_state.table.scroll_offset = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.cfn_state.table.selected, 10);
app.handle_action(Action::PageDown);
assert_eq!(app.cfn_state.table.selected, 20);
app.handle_action(Action::PageUp);
assert_eq!(app.cfn_state.table.selected, 10);
}
#[test]
fn test_cfn_filter_input_only_when_focused() {
use crate::ui::cfn::STATUS_FILTER;
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = STATUS_FILTER;
app.cfn_state.table.filter = String::new();
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.cfn_state.table.filter, "");
app.cfn_state.input_focus = InputFocus::Filter;
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.cfn_state.table.filter, "test");
}
#[test]
fn test_cfn_input_focus_resets_on_start() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.mode = Mode::Normal;
app.cfn_state.input_focus = InputFocus::Pagination;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_iam_roles_input_focus_cycles_forward() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.mode = Mode::FilterInput;
app.iam_state.role_input_focus = InputFocus::Filter;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
}
#[test]
fn test_iam_roles_input_focus_cycles_backward() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.mode = Mode::FilterInput;
app.iam_state.role_input_focus = InputFocus::Filter;
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
}
#[test]
fn test_iam_roles_filter_input_only_when_focused() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.mode = Mode::FilterInput;
app.iam_state.role_input_focus = InputFocus::Pagination;
app.iam_state.roles.filter = String::new();
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.iam_state.roles.filter, "");
app.iam_state.role_input_focus = InputFocus::Filter;
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.iam_state.roles.filter, "test");
}
#[test]
fn test_iam_roles_page_down_updates_scroll_offset() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.mode = Mode::Normal;
app.iam_state.roles.items = (0..50)
.map(|i| IamRole {
role_name: format!("role-{}", i),
path: "/".to_string(),
trusted_entities: "AWS Service".to_string(),
last_activity: "N/A".to_string(),
arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
creation_time: "2024-01-01".to_string(),
description: String::new(),
max_session_duration: Some(3600),
})
.collect();
app.iam_state.roles.selected = 0;
app.iam_state.roles.scroll_offset = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.iam_state.roles.selected, 10);
assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
app.handle_action(Action::PageDown);
assert_eq!(app.iam_state.roles.selected, 20);
assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
}
#[test]
fn test_application_selection_and_deployments_tab() {
use crate::lambda::Application as LambdaApplication;
use LambdaApplicationDetailTab;
let mut app = test_app();
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.mode = Mode::Normal;
app.lambda_application_state.table.items = vec![LambdaApplication {
name: "test-app".to_string(),
arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
description: "Test application".to_string(),
status: "CREATE_COMPLETE".to_string(),
last_modified: "2024-01-01".to_string(),
}];
app.handle_action(Action::Select);
assert_eq!(
app.lambda_application_state.current_application,
Some("test-app".to_string())
);
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Overview
);
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Deployments
);
app.handle_action(Action::GoBack);
assert_eq!(app.lambda_application_state.current_application, None);
}
#[test]
fn test_application_resources_filter_and_pagination() {
use crate::lambda::Application as LambdaApplication;
use LambdaApplicationDetailTab;
let mut app = test_app();
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.mode = Mode::Normal;
app.lambda_application_state.table.items = vec![LambdaApplication {
name: "test-app".to_string(),
arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
description: "Test application".to_string(),
status: "CREATE_COMPLETE".to_string(),
last_modified: "2024-01-01".to_string(),
}];
app.handle_action(Action::Select);
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Overview
);
assert!(!app.lambda_application_state.resources.items.is_empty());
app.mode = Mode::FilterInput;
assert_eq!(
app.lambda_application_state.resource_input_focus,
InputFocus::Filter
);
app.handle_action(Action::NextFilterFocus);
assert_eq!(
app.lambda_application_state.resource_input_focus,
InputFocus::Pagination
);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(
app.lambda_application_state.resource_input_focus,
InputFocus::Filter
);
}
#[test]
fn test_application_deployments_filter_and_pagination() {
use crate::lambda::Application as LambdaApplication;
use LambdaApplicationDetailTab;
let mut app = test_app();
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.mode = Mode::Normal;
app.lambda_application_state.table.items = vec![LambdaApplication {
name: "test-app".to_string(),
arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
description: "Test application".to_string(),
status: "CREATE_COMPLETE".to_string(),
last_modified: "2024-01-01".to_string(),
}];
app.handle_action(Action::Select);
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Deployments
);
assert!(!app.lambda_application_state.deployments.items.is_empty());
app.mode = Mode::FilterInput;
assert_eq!(
app.lambda_application_state.deployment_input_focus,
InputFocus::Filter
);
app.handle_action(Action::NextFilterFocus);
assert_eq!(
app.lambda_application_state.deployment_input_focus,
InputFocus::Pagination
);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(
app.lambda_application_state.deployment_input_focus,
InputFocus::Filter
);
}
#[test]
fn test_application_resource_expansion() {
use crate::lambda::Application as LambdaApplication;
use LambdaApplicationDetailTab;
let mut app = test_app();
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.mode = Mode::Normal;
app.lambda_application_state.table.items = vec![LambdaApplication {
name: "test-app".to_string(),
arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
description: "Test application".to_string(),
status: "CREATE_COMPLETE".to_string(),
last_modified: "2024-01-01".to_string(),
}];
app.handle_action(Action::Select);
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Overview
);
app.handle_action(Action::NextPane);
assert_eq!(
app.lambda_application_state.resources.expanded_item,
Some(0)
);
app.handle_action(Action::PrevPane);
assert_eq!(app.lambda_application_state.resources.expanded_item, None);
}
#[test]
fn test_application_deployment_expansion() {
use crate::lambda::Application as LambdaApplication;
use LambdaApplicationDetailTab;
let mut app = test_app();
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.mode = Mode::Normal;
app.lambda_application_state.table.items = vec![LambdaApplication {
name: "test-app".to_string(),
arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
description: "Test application".to_string(),
status: "CREATE_COMPLETE".to_string(),
last_modified: "2024-01-01".to_string(),
}];
app.handle_action(Action::Select);
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Deployments
);
app.handle_action(Action::NextPane);
assert_eq!(
app.lambda_application_state.deployments.expanded_item,
Some(0)
);
app.handle_action(Action::PrevPane);
assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
}
#[test]
fn test_s3_nested_prefix_expansion() {
use crate::s3::Bucket;
use S3Object;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![Bucket {
name: "test-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01".to_string(),
}];
app.s3_state.bucket_preview.insert(
"test-bucket".to_string(),
vec![S3Object {
key: "level1/".to_string(),
size: 0,
last_modified: "".to_string(),
is_prefix: true,
storage_class: "".to_string(),
}],
);
app.s3_state.prefix_preview.insert(
"level1/".to_string(),
vec![S3Object {
key: "level1/level2/".to_string(),
size: 0,
last_modified: "".to_string(),
is_prefix: true,
storage_class: "".to_string(),
}],
);
app.s3_state.selected_row = 0;
app.handle_action(Action::NextPane);
assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
app.s3_state.selected_row = 1;
app.handle_action(Action::NextPane);
assert!(app.s3_state.expanded_prefixes.contains("level1/"));
app.s3_state.selected_row = 2;
app.handle_action(Action::NextPane);
assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
assert!(app.s3_state.expanded_prefixes.contains("level1/"));
}
#[test]
fn test_s3_nested_prefix_collapse() {
use crate::s3::Bucket;
use S3Object;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![Bucket {
name: "test-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01".to_string(),
}];
app.s3_state.bucket_preview.insert(
"test-bucket".to_string(),
vec![S3Object {
key: "level1/".to_string(),
size: 0,
last_modified: "".to_string(),
is_prefix: true,
storage_class: "".to_string(),
}],
);
app.s3_state.prefix_preview.insert(
"level1/".to_string(),
vec![S3Object {
key: "level1/level2/".to_string(),
size: 0,
last_modified: "".to_string(),
is_prefix: true,
storage_class: "".to_string(),
}],
);
app.s3_state
.expanded_prefixes
.insert("test-bucket".to_string());
app.s3_state.expanded_prefixes.insert("level1/".to_string());
app.s3_state
.expanded_prefixes
.insert("level1/level2/".to_string());
app.s3_state.selected_row = 2;
app.handle_action(Action::PrevPane);
assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
assert!(app.s3_state.expanded_prefixes.contains("level1/"));
app.s3_state.selected_row = 1;
app.handle_action(Action::PrevPane);
assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
app.s3_state.selected_row = 0;
app.handle_action(Action::PrevPane);
assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
}
}
#[cfg(test)]
mod sqs_tests {
use super::*;
use test_helpers::*;
#[test]
fn test_sqs_filter_input() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.sqs_state.queues.filter, "test");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.sqs_state.queues.filter, "tes");
}
#[test]
fn test_sqs_start_filter() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::Normal;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_sqs_filter_focus_cycling() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.sqs_state.input_focus = InputFocus::Filter;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
}
#[test]
fn test_sqs_navigation() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::Normal;
app.sqs_state.queues.items = (0..10)
.map(|i| SqsQueue {
name: format!("queue{}", i),
url: String::new(),
queue_type: "Standard".to_string(),
created_timestamp: String::new(),
messages_available: "0".to_string(),
messages_in_flight: "0".to_string(),
encryption: "Disabled".to_string(),
content_based_deduplication: "Disabled".to_string(),
last_modified_timestamp: String::new(),
visibility_timeout: String::new(),
message_retention_period: String::new(),
maximum_message_size: String::new(),
delivery_delay: String::new(),
receive_message_wait_time: String::new(),
high_throughput_fifo: "N/A".to_string(),
deduplication_scope: "N/A".to_string(),
fifo_throughput_limit: "N/A".to_string(),
dead_letter_queue: "-".to_string(),
messages_delayed: "0".to_string(),
redrive_allow_policy: "-".to_string(),
redrive_policy: "".to_string(),
redrive_task_id: "-".to_string(),
redrive_task_start_time: "-".to_string(),
redrive_task_status: "-".to_string(),
redrive_task_percent: "-".to_string(),
redrive_task_destination: "-".to_string(),
})
.collect();
app.handle_action(Action::NextItem);
assert_eq!(app.sqs_state.queues.selected, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.sqs_state.queues.selected, 0);
}
#[test]
fn test_sqs_page_navigation() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::Normal;
app.sqs_state.queues.items = (0..100)
.map(|i| SqsQueue {
name: format!("queue{}", i),
url: String::new(),
queue_type: "Standard".to_string(),
created_timestamp: String::new(),
messages_available: "0".to_string(),
messages_in_flight: "0".to_string(),
encryption: "Disabled".to_string(),
content_based_deduplication: "Disabled".to_string(),
last_modified_timestamp: String::new(),
visibility_timeout: String::new(),
message_retention_period: String::new(),
maximum_message_size: String::new(),
delivery_delay: String::new(),
receive_message_wait_time: String::new(),
high_throughput_fifo: "N/A".to_string(),
deduplication_scope: "N/A".to_string(),
fifo_throughput_limit: "N/A".to_string(),
dead_letter_queue: "-".to_string(),
messages_delayed: "0".to_string(),
redrive_allow_policy: "-".to_string(),
redrive_policy: "".to_string(),
redrive_task_id: "-".to_string(),
redrive_task_start_time: "-".to_string(),
redrive_task_status: "-".to_string(),
redrive_task_percent: "-".to_string(),
redrive_task_destination: "-".to_string(),
})
.collect();
app.handle_action(Action::PageDown);
assert_eq!(app.sqs_state.queues.selected, 10);
app.handle_action(Action::PageUp);
assert_eq!(app.sqs_state.queues.selected, 0);
}
#[test]
fn test_sqs_queue_expansion() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.sqs_state.queues.items = vec![SqsQueue {
name: "my-queue".to_string(),
url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
queue_type: "Standard".to_string(),
created_timestamp: "2023-01-01".to_string(),
messages_available: "5".to_string(),
messages_in_flight: "2".to_string(),
encryption: "Enabled".to_string(),
content_based_deduplication: "Disabled".to_string(),
last_modified_timestamp: "2023-01-02".to_string(),
visibility_timeout: "30".to_string(),
message_retention_period: "345600".to_string(),
maximum_message_size: "262144".to_string(),
delivery_delay: "0".to_string(),
receive_message_wait_time: "0".to_string(),
high_throughput_fifo: "N/A".to_string(),
deduplication_scope: "N/A".to_string(),
fifo_throughput_limit: "N/A".to_string(),
dead_letter_queue: "-".to_string(),
messages_delayed: "0".to_string(),
redrive_allow_policy: "-".to_string(),
redrive_policy: "".to_string(),
redrive_task_id: "-".to_string(),
redrive_task_start_time: "-".to_string(),
redrive_task_status: "-".to_string(),
redrive_task_percent: "-".to_string(),
redrive_task_destination: "-".to_string(),
}];
app.sqs_state.queues.selected = 0;
assert_eq!(app.sqs_state.queues.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
app.handle_action(Action::NextPane);
assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.sqs_state.queues.expanded_item, None);
app.handle_action(Action::PrevPane);
assert_eq!(app.sqs_state.queues.expanded_item, None);
}
#[test]
fn test_sqs_column_toggle() {
use crate::sqs::queue::Column as SqsColumn;
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.sqs_visible_column_ids = SqsColumn::ids();
let initial_count = app.sqs_visible_column_ids.len();
app.column_selector_index = 0;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
app.handle_action(Action::ToggleColumn);
assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
}
#[test]
fn test_sqs_column_selector_navigation() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let max_index = app.sqs_column_ids.len() - 1;
for _ in 0..max_index {
app.handle_action(Action::NextItem);
}
assert_eq!(app.column_selector_index, max_index);
for _ in 0..max_index {
app.handle_action(Action::PrevItem);
}
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_sqs_queue_selection() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::Normal;
app.sqs_state.queues.items = vec![SqsQueue {
name: "my-queue".to_string(),
url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
queue_type: "Standard".to_string(),
created_timestamp: "2023-01-01".to_string(),
messages_available: "5".to_string(),
messages_in_flight: "2".to_string(),
encryption: "Enabled".to_string(),
content_based_deduplication: "Disabled".to_string(),
last_modified_timestamp: "2023-01-02".to_string(),
visibility_timeout: "30".to_string(),
message_retention_period: "345600".to_string(),
maximum_message_size: "262144".to_string(),
delivery_delay: "0".to_string(),
receive_message_wait_time: "0".to_string(),
high_throughput_fifo: "N/A".to_string(),
deduplication_scope: "N/A".to_string(),
fifo_throughput_limit: "N/A".to_string(),
dead_letter_queue: "-".to_string(),
messages_delayed: "0".to_string(),
redrive_allow_policy: "-".to_string(),
redrive_policy: "".to_string(),
redrive_task_id: "-".to_string(),
redrive_task_start_time: "-".to_string(),
redrive_task_status: "-".to_string(),
redrive_task_percent: "-".to_string(),
redrive_task_destination: "-".to_string(),
}];
app.sqs_state.queues.selected = 0;
assert_eq!(app.sqs_state.current_queue, None);
app.handle_action(Action::Select);
assert_eq!(
app.sqs_state.current_queue,
Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
);
app.handle_action(Action::GoBack);
assert_eq!(app.sqs_state.current_queue, None);
}
#[test]
fn test_sqs_lambda_triggers_expand_collapse() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.sqs_state.current_queue =
Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
app.sqs_state.triggers.items = vec![LambdaTrigger {
uuid: "test-uuid".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-01-01T00:00:00Z".to_string(),
}];
app.sqs_state.triggers.selected = 0;
assert_eq!(app.sqs_state.triggers.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.sqs_state.triggers.expanded_item, None);
}
#[test]
fn test_sqs_lambda_triggers_expand_toggle() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.sqs_state.current_queue =
Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
app.sqs_state.triggers.items = vec![LambdaTrigger {
uuid: "test-uuid".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-01-01T00:00:00Z".to_string(),
}];
app.sqs_state.triggers.selected = 0;
app.handle_action(Action::NextPane);
assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
app.handle_action(Action::NextPane);
assert_eq!(app.sqs_state.triggers.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
}
#[test]
fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
use crate::ui::sqs::filtered_lambda_triggers;
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.sqs_state.current_queue =
Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
app.sqs_state.triggers.items = vec![
LambdaTrigger {
uuid: "uuid-3".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-03-01T00:00:00Z".to_string(),
},
LambdaTrigger {
uuid: "uuid-1".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-01-01T00:00:00Z".to_string(),
},
LambdaTrigger {
uuid: "uuid-2".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-02-01T00:00:00Z".to_string(),
},
];
let sorted = filtered_lambda_triggers(&app);
assert_eq!(sorted.len(), 3);
assert_eq!(sorted[0].uuid, "uuid-1");
assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
assert_eq!(sorted[1].uuid, "uuid-2");
assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
assert_eq!(sorted[2].uuid, "uuid-3");
assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
}
#[test]
fn test_sqs_lambda_triggers_filter_input() {
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.sqs_state.current_queue =
Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
app.sqs_state.input_focus = InputFocus::Filter;
assert_eq!(app.sqs_state.triggers.filter, "");
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.sqs_state.triggers.filter, "t");
app.handle_action(Action::FilterInput('e'));
assert_eq!(app.sqs_state.triggers.filter, "te");
app.handle_action(Action::FilterInput('s'));
assert_eq!(app.sqs_state.triggers.filter, "tes");
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.sqs_state.triggers.filter, "test");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.sqs_state.triggers.filter, "tes");
}
#[test]
fn test_sqs_lambda_triggers_filter_applied() {
use crate::ui::sqs::filtered_lambda_triggers;
let mut app = test_app();
app.current_service = Service::SqsQueues;
app.service_selected = true;
app.sqs_state.current_queue =
Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
app.sqs_state.triggers.items = vec![
LambdaTrigger {
uuid: "uuid-1".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-01-01T00:00:00Z".to_string(),
},
LambdaTrigger {
uuid: "uuid-2".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-02-01T00:00:00Z".to_string(),
},
LambdaTrigger {
uuid: "uuid-3".to_string(),
arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-03-01T00:00:00Z".to_string(),
},
];
let filtered = filtered_lambda_triggers(&app);
assert_eq!(filtered.len(), 3);
app.sqs_state.triggers.filter = "alpha".to_string();
let filtered = filtered_lambda_triggers(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(
filtered[0].arn,
"arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
);
app.sqs_state.triggers.filter = "test".to_string();
let filtered = filtered_lambda_triggers(&app);
assert_eq!(filtered.len(), 2);
assert_eq!(
filtered[0].arn,
"arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
);
assert_eq!(
filtered[1].arn,
"arn:aws:lambda:us-east-1:123456789012:function:test-beta"
);
app.sqs_state.triggers.filter = "uuid-3".to_string();
let filtered = filtered_lambda_triggers(&app);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].uuid, "uuid-3");
}
#[test]
fn test_sqs_triggers_navigation() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::Normal;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
app.sqs_state.triggers.items = vec![
LambdaTrigger {
uuid: "1".to_string(),
arn: "arn1".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-01-01".to_string(),
},
LambdaTrigger {
uuid: "2".to_string(),
arn: "arn2".to_string(),
status: "Enabled".to_string(),
last_modified: "2024-01-02".to_string(),
},
];
assert_eq!(app.sqs_state.triggers.selected, 0);
app.next_item();
assert_eq!(app.sqs_state.triggers.selected, 1);
app.prev_item();
assert_eq!(app.sqs_state.triggers.selected, 0);
}
#[test]
fn test_sqs_pipes_navigation() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::Normal;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
app.sqs_state.pipes.items = vec![
EventBridgePipe {
name: "pipe1".to_string(),
status: "RUNNING".to_string(),
target: "target1".to_string(),
last_modified: "2024-01-01".to_string(),
},
EventBridgePipe {
name: "pipe2".to_string(),
status: "RUNNING".to_string(),
target: "target2".to_string(),
last_modified: "2024-01-02".to_string(),
},
];
assert_eq!(app.sqs_state.pipes.selected, 0);
app.next_item();
assert_eq!(app.sqs_state.pipes.selected, 1);
app.prev_item();
assert_eq!(app.sqs_state.pipes.selected, 0);
}
#[test]
fn test_sqs_tags_navigation() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::Normal;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
app.sqs_state.tags.items = vec![
SqsQueueTag {
key: "Env".to_string(),
value: "prod".to_string(),
},
SqsQueueTag {
key: "Team".to_string(),
value: "backend".to_string(),
},
];
assert_eq!(app.sqs_state.tags.selected, 0);
app.next_item();
assert_eq!(app.sqs_state.tags.selected, 1);
app.prev_item();
assert_eq!(app.sqs_state.tags.selected, 0);
}
#[test]
fn test_sqs_queues_navigation() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::Normal;
app.current_service = Service::SqsQueues;
app.sqs_state.queues.items = vec![
SqsQueue {
name: "queue1".to_string(),
url: "url1".to_string(),
queue_type: "Standard".to_string(),
created_timestamp: "".to_string(),
messages_available: "0".to_string(),
messages_in_flight: "0".to_string(),
encryption: "Disabled".to_string(),
content_based_deduplication: "Disabled".to_string(),
last_modified_timestamp: "".to_string(),
visibility_timeout: "".to_string(),
message_retention_period: "".to_string(),
maximum_message_size: "".to_string(),
delivery_delay: "".to_string(),
receive_message_wait_time: "".to_string(),
high_throughput_fifo: "-".to_string(),
deduplication_scope: "-".to_string(),
fifo_throughput_limit: "-".to_string(),
dead_letter_queue: "-".to_string(),
messages_delayed: "0".to_string(),
redrive_allow_policy: "-".to_string(),
redrive_policy: "".to_string(),
redrive_task_id: "-".to_string(),
redrive_task_start_time: "-".to_string(),
redrive_task_status: "-".to_string(),
redrive_task_percent: "-".to_string(),
redrive_task_destination: "-".to_string(),
},
SqsQueue {
name: "queue2".to_string(),
url: "url2".to_string(),
queue_type: "Standard".to_string(),
created_timestamp: "".to_string(),
messages_available: "0".to_string(),
messages_in_flight: "0".to_string(),
encryption: "Disabled".to_string(),
content_based_deduplication: "Disabled".to_string(),
last_modified_timestamp: "".to_string(),
visibility_timeout: "".to_string(),
message_retention_period: "".to_string(),
maximum_message_size: "".to_string(),
delivery_delay: "".to_string(),
receive_message_wait_time: "".to_string(),
high_throughput_fifo: "-".to_string(),
deduplication_scope: "-".to_string(),
fifo_throughput_limit: "-".to_string(),
dead_letter_queue: "-".to_string(),
messages_delayed: "0".to_string(),
redrive_allow_policy: "-".to_string(),
redrive_policy: "".to_string(),
redrive_task_id: "-".to_string(),
redrive_task_start_time: "-".to_string(),
redrive_task_status: "-".to_string(),
redrive_task_percent: "-".to_string(),
redrive_task_destination: "-".to_string(),
},
];
assert_eq!(app.sqs_state.queues.selected, 0);
app.next_item();
assert_eq!(app.sqs_state.queues.selected, 1);
app.prev_item();
assert_eq!(app.sqs_state.queues.selected, 0);
}
#[test]
fn test_sqs_subscriptions_navigation() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::Normal;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
app.sqs_state.subscriptions.items = vec![
SnsSubscription {
subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
},
SnsSubscription {
subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
},
];
assert_eq!(app.sqs_state.subscriptions.selected, 0);
app.next_item();
assert_eq!(app.sqs_state.subscriptions.selected, 1);
app.prev_item();
assert_eq!(app.sqs_state.subscriptions.selected, 0);
}
#[test]
fn test_sqs_subscription_region_dropdown_navigation() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::FilterInput;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
assert_eq!(app.sqs_state.subscription_region_selected, 0);
app.next_item();
assert_eq!(app.sqs_state.subscription_region_selected, 1);
app.next_item();
assert_eq!(app.sqs_state.subscription_region_selected, 2);
app.prev_item();
assert_eq!(app.sqs_state.subscription_region_selected, 1);
app.prev_item();
assert_eq!(app.sqs_state.subscription_region_selected, 0);
}
#[test]
fn test_sqs_subscription_region_selection() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::FilterInput;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
app.sqs_state.subscription_region_selected = 2;
assert_eq!(app.sqs_state.subscription_region_filter, "");
app.handle_action(Action::ApplyFilter);
assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_sqs_subscription_region_change_resets_selection() {
let mut app = test_app();
app.service_selected = true;
app.mode = Mode::FilterInput;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
app.sqs_state.subscription_region_selected = 0;
app.sqs_state.subscriptions.selected = 5;
app.handle_action(Action::NextItem);
assert_eq!(app.sqs_state.subscription_region_selected, 1);
assert_eq!(app.sqs_state.subscriptions.selected, 0);
}
#[test]
fn test_s3_object_filter_resets_selection() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::S3Buckets;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.selected_row = 5;
app.mode = Mode::FilterInput;
app.handle_action(Action::CloseMenu);
assert_eq!(app.s3_state.selected_row, 0);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_s3_bucket_filter_resets_selection() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::S3Buckets;
app.s3_state.selected_row = 10;
app.mode = Mode::FilterInput;
app.handle_action(Action::CloseMenu);
assert_eq!(app.s3_state.selected_row, 0);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_s3_selection_stays_in_bounds() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::S3Buckets;
app.s3_state.selected_row = 0;
app.s3_state.selected_object = 0;
app.prev_item();
assert_eq!(app.s3_state.selected_row, 0);
assert_eq!(app.s3_state.selected_object, 0);
}
#[test]
fn test_cfn_filter_resets_selection() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.cfn_state.table.selected = 10;
app.mode = Mode::FilterInput;
app.handle_action(Action::CloseMenu);
assert_eq!(app.cfn_state.table.selected, 0);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_lambda_filter_resets_selection() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::LambdaFunctions;
app.lambda_state.table.selected = 8;
app.mode = Mode::FilterInput;
app.handle_action(Action::CloseMenu);
assert_eq!(app.lambda_state.table.selected, 0);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_sqs_filter_resets_selection() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::SqsQueues;
app.sqs_state.queues.selected = 7;
app.mode = Mode::FilterInput;
app.handle_action(Action::CloseMenu);
assert_eq!(app.sqs_state.queues.selected, 0);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_sqs_queues_list_shows_preferences() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::SqsQueues;
app.mode = Mode::Normal;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_sqs_queue_policies_tab_no_preferences() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
app.mode = Mode::Normal;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_sqs_sns_subscriptions_tab_shows_preferences() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
app.mode = Mode::Normal;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_sqs_monitoring_tab_no_preferences() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::SqsQueues;
app.sqs_state.current_queue = Some("test-queue".to_string());
app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
app.mode = Mode::Normal;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_cfn_status_filter_change_resets_selection() {
use crate::ui::cfn::STATUS_FILTER;
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = STATUS_FILTER;
app.cfn_state.status_filter = CfnStatusFilter::All;
app.cfn_state.table.items = vec![
CfnStack {
name: "stack1".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
CfnStack {
name: "stack2".to_string(),
stack_id: "id2".to_string(),
status: "UPDATE_IN_PROGRESS".to_string(),
created_time: "2024-01-02".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
},
];
app.cfn_state.table.selected = 1;
app.handle_action(Action::NextItem);
assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
assert_eq!(app.cfn_state.table.selected, 0);
}
#[test]
fn test_cfn_view_nested_toggle_resets_selection() {
use crate::ui::cfn::VIEW_NESTED;
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::FilterInput;
app.cfn_state.input_focus = VIEW_NESTED;
app.cfn_state.view_nested = false;
app.cfn_state.table.items = vec![CfnStack {
name: "stack1".to_string(),
stack_id: "id1".to_string(),
status: "CREATE_COMPLETE".to_string(),
created_time: "2024-01-01".to_string(),
updated_time: String::new(),
deleted_time: String::new(),
drift_status: String::new(),
last_drift_check_time: String::new(),
status_reason: String::new(),
description: String::new(),
detailed_status: String::new(),
root_stack: String::new(),
parent_stack: String::new(),
termination_protection: false,
iam_role: String::new(),
tags: Vec::new(),
stack_policy: String::new(),
rollback_monitoring_time: String::new(),
rollback_alarms: Vec::new(),
notification_arns: Vec::new(),
}];
app.cfn_state.table.selected = 5;
app.handle_action(Action::ToggleFilterCheckbox);
assert!(app.cfn_state.view_nested);
assert_eq!(app.cfn_state.table.selected, 0);
}
#[test]
fn test_cfn_template_scroll_up() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.cfn_state.template_scroll = 20;
app.page_up();
assert_eq!(app.cfn_state.template_scroll, 10);
}
#[test]
fn test_cfn_template_scroll_down() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
app.cfn_state.template_scroll = 0;
app.page_down();
assert_eq!(app.cfn_state.template_scroll, 10);
}
#[test]
fn test_cfn_template_scroll_down_respects_max() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.cfn_state.template_body = "line1\nline2\nline3".to_string();
app.cfn_state.template_scroll = 0;
app.page_down();
assert_eq!(app.cfn_state.template_scroll, 2);
}
#[test]
fn test_cfn_template_arrow_up() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::Normal;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.cfn_state.template_scroll = 5;
app.prev_item();
assert_eq!(app.cfn_state.template_scroll, 4);
}
#[test]
fn test_cfn_template_arrow_down() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::Normal;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
app.cfn_state.template_scroll = 2;
app.next_item();
assert_eq!(app.cfn_state.template_scroll, 3);
}
#[test]
fn test_cfn_template_arrow_down_respects_max() {
let mut app = test_app();
app.service_selected = true;
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::Normal;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.cfn_state.template_body = "line1\nline2".to_string();
app.cfn_state.template_scroll = 1;
app.next_item();
assert_eq!(app.cfn_state.template_scroll, 1);
}
#[test]
fn test_cfn_yank_on_template_tab_copies_template_body() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.cfn_state.current_stack = Some("my-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.cfn_state.template_body = "AWSTemplateFormatVersion: '2010-09-09'".to_string();
assert!(!app.cfn_state.template_body.is_empty());
crate::cfn::actions::yank(&app); }
#[test]
fn test_cfn_change_sets_console_url_includes_events_view_graph() {
use crate::cfn::console_url_stack_detail_with_tab;
let url = console_url_stack_detail_with_tab(
"us-east-1",
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
&CfnDetailTab::ChangeSets,
);
assert!(
url.contains("eventsView=graph"),
"ChangeSets URL must include eventsView=graph, got: {url}"
);
assert!(url.contains("changesets"), "URL must contain 'changesets'");
}
#[test]
fn test_cfn_other_tabs_console_url_no_events_view_graph() {
use crate::cfn::console_url_stack_detail_with_tab;
for tab in [
CfnDetailTab::StackInfo,
CfnDetailTab::Events,
CfnDetailTab::Resources,
] {
let url = console_url_stack_detail_with_tab(
"us-east-1",
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
&tab,
);
assert!(
!url.contains("eventsView=graph"),
"{tab:?} URL must NOT include eventsView=graph, got: {url}"
);
}
}
#[test]
fn test_cfn_change_sets_expand_collapse() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.current_stack = Some("my-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::ChangeSets;
app.cfn_state.change_sets.items = vec![rusticity_core::cfn::StackChangeSet {
name: "cs-001".to_string(),
change_set_id: "id-001".to_string(),
created_time: String::new(),
status: "CREATE_COMPLETE".to_string(),
description: String::new(),
root_change_set_id: String::new(),
parent_change_set_id: String::new(),
}];
app.cfn_state.change_sets.selected = 0;
assert_eq!(app.cfn_state.change_sets.expanded_item, None);
app.handle_action(Action::ExpandRow);
assert_eq!(
app.cfn_state.change_sets.expanded_item,
Some(0),
"Enter must expand change set row"
);
app.handle_action(Action::CollapseRow);
assert_eq!(
app.cfn_state.change_sets.expanded_item, None,
"Left arrow must collapse change set row"
);
}
#[test]
fn test_cfn_events_toggle_column_hides_visible_column() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::ColumnSelector;
app.cfn_state.current_stack = Some("my-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Events;
let first_col = app.cfn_event_column_ids[0];
assert!(
app.cfn_event_visible_column_ids.contains(&first_col),
"First column must be visible before toggle"
);
app.column_selector_index = 1; app.handle_action(Action::ToggleColumn);
assert!(
!app.cfn_event_visible_column_ids.contains(&first_col),
"ToggleColumn must hide the first Events column"
);
app.handle_action(Action::ToggleColumn);
assert!(
app.cfn_event_visible_column_ids.contains(&first_col),
"ToggleColumn again must re-show the column"
);
}
#[test]
fn test_cfn_events_toggle_column_keeps_at_least_one_visible() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::ColumnSelector;
app.cfn_state.current_stack = Some("my-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Events;
app.cfn_event_visible_column_ids = vec![app.cfn_event_column_ids[0]];
app.column_selector_index = 1;
app.handle_action(Action::ToggleColumn);
assert!(
!app.cfn_event_visible_column_ids.is_empty(),
"At least one column must remain visible"
);
}
#[test]
fn test_space_menu_o_opens_service_picker() {
let mut app = test_app();
app.handle_action(Action::OpenSpaceMenu);
assert_eq!(app.mode, Mode::SpaceMenu);
app.handle_action(Action::OpenServicePicker);
assert_eq!(
app.mode,
Mode::ServicePicker,
"'o' from SpaceMenu must open ServicePicker"
);
}
#[test]
fn test_space_menu_c_closes_service() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.tabs.push(Tab {
service: Service::CloudFormationStacks,
title: "CFN".to_string(),
breadcrumb: "CloudFormation".to_string(),
});
app.current_tab = 0;
app.handle_action(Action::OpenSpaceMenu);
app.handle_action(Action::CloseService);
assert_ne!(
app.mode,
Mode::SpaceMenu,
"CloseService must exit SpaceMenu"
);
}
}
#[cfg(test)]
mod lambda_version_tab_tests {
use super::*;
use crate::ui::iam::POLICY_TYPE_DROPDOWN;
use test_helpers::*;
#[test]
fn test_lambda_version_tab_cycling_next() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.current_version = Some("1".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.handle_action(Action::NextDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
assert!(app.lambda_state.metrics_loading);
app.lambda_state.metrics_loading = false;
app.handle_action(Action::NextDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
}
#[test]
fn test_lambda_version_tab_cycling_prev() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.current_version = Some("1".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
assert!(app.lambda_state.metrics_loading);
app.lambda_state.metrics_loading = false;
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
}
#[test]
fn test_lambda_version_monitor_clears_metrics() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.lambda_state.current_function = Some("test-function".to_string());
app.lambda_state.current_version = Some("1".to_string());
app.lambda_state.detail_tab = LambdaDetailTab::Code;
app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
app.lambda_state.monitoring_scroll = 5;
app.handle_action(Action::NextDetailTab);
assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
assert!(app.lambda_state.metrics_loading);
assert_eq!(app.lambda_state.monitoring_scroll, 0);
assert!(app.lambda_state.metric_data_invocations.is_empty());
}
#[test]
fn test_cfn_parameters_expand_collapse() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Parameters;
app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
key: "Param1".to_string(),
value: "Value1".to_string(),
resolved_value: "Resolved1".to_string(),
}];
app.cfn_state.parameters.reset();
assert_eq!(app.cfn_state.parameters.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.cfn_state.parameters.expanded_item, None);
}
#[test]
fn test_cfn_parameters_filter_resets_selection() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Parameters;
app.cfn_state.parameters.items = vec![
rusticity_core::cfn::StackParameter {
key: "DatabaseName".to_string(),
value: "mydb".to_string(),
resolved_value: "mydb".to_string(),
},
rusticity_core::cfn::StackParameter {
key: "InstanceType".to_string(),
value: "t2.micro".to_string(),
resolved_value: "t2.micro".to_string(),
},
rusticity_core::cfn::StackParameter {
key: "Environment".to_string(),
value: "production".to_string(),
resolved_value: "production".to_string(),
},
];
app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
app.cfn_state.parameters_input_focus = InputFocus::Filter;
app.handle_action(Action::FilterInput('D'));
assert_eq!(app.cfn_state.parameters.selected, 0);
assert_eq!(app.cfn_state.parameters.filter, "D");
app.cfn_state.parameters.selected = 1;
app.handle_action(Action::FilterInput('a'));
assert_eq!(app.cfn_state.parameters.selected, 0);
assert_eq!(app.cfn_state.parameters.filter, "Da");
app.cfn_state.parameters.selected = 1;
app.handle_action(Action::FilterBackspace);
assert_eq!(app.cfn_state.parameters.selected, 0);
assert_eq!(app.cfn_state.parameters.filter, "D");
}
#[test]
fn test_cfn_template_tab_no_preferences() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Template;
app.mode = Mode::Normal;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.cfn_state.detail_tab = CfnDetailTab::GitSync;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.cfn_state.detail_tab = CfnDetailTab::Parameters;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.cfn_state.detail_tab = CfnDetailTab::Outputs;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector); }
#[test]
fn test_iam_user_groups_tab_shows_preferences() {
let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
app.current_service = Service::IamUsers;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.current_user = Some("test-user".to_string());
app.iam_state.user_tab = UserTab::Groups;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_iam_user_tags_tab_shows_preferences() {
let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
app.current_service = Service::IamUsers;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.current_user = Some("test-user".to_string());
app.iam_state.user_tab = UserTab::Tags;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_iam_user_last_accessed_tab_shows_preferences() {
let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
app.current_service = Service::IamUsers;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.current_user = Some("test-user".to_string());
app.iam_state.user_tab = UserTab::LastAccessed;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_iam_user_security_credentials_tab_no_preferences() {
let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
app.current_service = Service::IamUsers;
app.service_selected = true;
app.mode = Mode::Normal;
app.iam_state.current_user = Some("test-user".to_string());
app.iam_state.user_tab = UserTab::SecurityCredentials;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_iam_user_tabs_without_column_preferences() {
let mut app = test_app();
app.current_service = Service::IamUsers;
app.service_selected = true;
app.iam_state.current_user = Some("test-user".to_string());
app.mode = Mode::Normal;
app.iam_state.user_tab = UserTab::Groups;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.iam_state.user_tab = UserTab::Tags;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.iam_state.user_tab = UserTab::SecurityCredentials;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.iam_state.user_tab = UserTab::LastAccessed;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.iam_state.user_tab = UserTab::Permissions;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.iam_state.current_user = None;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_iam_role_policies_dropdown_cycling() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.iam_state.current_role = Some("test-role".to_string());
app.iam_state.role_tab = RoleTab::Permissions;
app.mode = Mode::FilterInput;
app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
app.iam_state.policy_type_filter = "All types".to_string();
app.next_item();
assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
app.next_item();
assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
app.next_item();
assert_eq!(app.iam_state.policy_type_filter, "All types");
app.prev_item();
assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
app.prev_item();
assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
app.prev_item();
assert_eq!(app.iam_state.policy_type_filter, "All types");
}
#[test]
fn test_iam_user_policies_dropdown_cycling() {
let mut app = test_app();
app.current_service = Service::IamUsers;
app.service_selected = true;
app.iam_state.current_user = Some("test-user".to_string());
app.iam_state.user_tab = UserTab::Permissions;
app.mode = Mode::FilterInput;
app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
app.iam_state.policy_type_filter = "All types".to_string();
app.next_item();
assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
app.next_item();
assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
app.next_item();
assert_eq!(app.iam_state.policy_type_filter, "All types");
app.prev_item();
assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
app.prev_item();
assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
app.prev_item();
assert_eq!(app.iam_state.policy_type_filter, "All types");
}
#[test]
fn test_iam_role_tabs_without_column_preferences() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.iam_state.current_role = Some("test-role".to_string());
app.mode = Mode::Normal;
app.iam_state.role_tab = RoleTab::TrustRelationships;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.iam_state.role_tab = RoleTab::RevokeSessions;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.iam_state.role_tab = RoleTab::LastAccessed;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.iam_state.role_tab = RoleTab::Permissions;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.iam_state.role_tab = RoleTab::Tags;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.mode = Mode::Normal;
app.iam_state.current_role = None;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_iam_role_tags_tab_cycling() {
let mut app = test_app();
app.current_service = Service::IamRoles;
app.service_selected = true;
app.iam_state.current_role = Some("test-role".to_string());
app.iam_state.role_tab = RoleTab::Tags;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 4);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 4);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_cfn_outputs_expand_collapse() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Outputs;
app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
key: "Output1".to_string(),
value: "Value1".to_string(),
description: "Description1".to_string(),
export_name: "Export1".to_string(),
}];
app.cfn_state.outputs.reset();
assert_eq!(app.cfn_state.outputs.expanded_item, None);
app.handle_action(Action::NextPane);
assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
app.handle_action(Action::PrevPane);
assert_eq!(app.cfn_state.outputs.expanded_item, None);
}
#[test]
fn test_cfn_outputs_filter_resets_selection() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.service_selected = true;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = CfnDetailTab::Outputs;
app.cfn_state.outputs.items = vec![
rusticity_core::cfn::StackOutput {
key: "ApiUrl".to_string(),
value: "https://api.example.com".to_string(),
description: "API endpoint".to_string(),
export_name: "MyApiUrl".to_string(),
},
rusticity_core::cfn::StackOutput {
key: "BucketName".to_string(),
value: "my-bucket".to_string(),
description: "S3 bucket".to_string(),
export_name: "MyBucket".to_string(),
},
];
app.cfn_state.outputs.reset();
app.cfn_state.outputs.selected = 1;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
app.handle_action(Action::FilterInput('A'));
assert_eq!(app.cfn_state.outputs.selected, 0);
assert_eq!(app.cfn_state.outputs.filter, "A");
app.cfn_state.outputs.selected = 1;
app.handle_action(Action::FilterInput('p'));
assert_eq!(app.cfn_state.outputs.selected, 0);
app.cfn_state.outputs.selected = 1;
app.handle_action(Action::FilterBackspace);
assert_eq!(app.cfn_state.outputs.selected, 0);
}
#[test]
fn test_ec2_service_in_picker() {
let app = test_app();
assert!(app.service_picker.services.contains(&"EC2 › Instances"));
}
#[test]
fn test_ec2_state_filter_cycles() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ec2_state.input_focus = EC2_STATE_FILTER;
let initial = app.ec2_state.state_filter;
assert_eq!(initial, Ec2StateFilter::AllStates);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
}
#[test]
fn test_ec2_filter_resets_table() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.ec2_state.input_focus = EC2_STATE_FILTER;
app.ec2_state.table.selected = 5;
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.table.selected, 0);
}
#[test]
fn test_ec2_columns_visible() {
let app = test_app();
assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
#[test]
fn test_ec2_breadcrumbs() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
let breadcrumb = app.breadcrumbs();
assert_eq!(breadcrumb, "EC2 > Instances");
}
#[test]
fn test_ec2_console_url() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
let url = app.get_console_url();
assert!(url.contains("ec2"));
assert!(url.contains("Instances"));
}
#[test]
fn test_ec2_filter_handling() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.handle_action(Action::FilterInput('t'));
app.handle_action(Action::FilterInput('e'));
app.handle_action(Action::FilterInput('s'));
app.handle_action(Action::FilterInput('t'));
assert_eq!(app.ec2_state.table.filter, "test");
app.handle_action(Action::FilterBackspace);
assert_eq!(app.ec2_state.table.filter, "tes");
}
#[test]
fn test_column_selector_page_down_ec2() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.column_selector_index, 10);
app.handle_action(Action::PageDown);
assert_eq!(app.column_selector_index, 20);
}
#[test]
fn test_column_selector_page_up_ec2() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 30;
app.handle_action(Action::PageUp);
assert_eq!(app.column_selector_index, 20);
app.handle_action(Action::PageUp);
assert_eq!(app.column_selector_index, 10);
}
#[test]
fn test_ec2_state_filter_dropdown_focus() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
app.handle_action(Action::ToggleFilterCheckbox);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
}
#[test]
fn test_column_selector_ctrl_d_scrolling() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
app.handle_action(Action::PageDown);
assert_eq!(app.column_selector_index, 11);
let max = app.get_column_selector_max();
app.handle_action(Action::PageDown);
assert_eq!(app.column_selector_index, max);
}
#[test]
fn test_column_selector_ctrl_u_scrolling() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 25;
app.handle_action(Action::PageUp);
assert_eq!(app.column_selector_index, 15);
app.handle_action(Action::PageUp);
assert_eq!(app.column_selector_index, 5);
}
#[test]
fn test_prev_preferences_lambda() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.mode = Mode::ColumnSelector;
let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
app.column_selector_index = page_size_idx;
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
}
#[test]
fn test_prev_preferences_cloudformation() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.mode = Mode::ColumnSelector;
let page_size_idx = app.cfn_column_ids.len() + 2;
app.column_selector_index = page_size_idx;
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
}
#[test]
fn test_prev_preferences_alarms() {
let mut app = test_app();
app.current_service = Service::CloudWatchAlarms;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 28;
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 22);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 18);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 28); }
#[test]
fn test_ec2_page_size_in_preferences() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.mode = Mode::ColumnSelector;
app.ec2_state.table.page_size = PageSize::Fifty;
let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
}
#[test]
fn test_ec2_next_preferences_with_page_size() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 0;
let page_size_idx = app.ec2_column_ids.len() + 2;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, page_size_idx);
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_ec2_dropdown_next_item() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.mode = Mode::FilterInput;
app.ec2_state.input_focus = EC2_STATE_FILTER;
app.ec2_state.state_filter = Ec2StateFilter::AllStates;
app.handle_action(Action::NextItem);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
app.handle_action(Action::NextItem);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
}
#[test]
fn test_ec2_dropdown_prev_item() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.mode = Mode::FilterInput;
app.ec2_state.input_focus = EC2_STATE_FILTER;
app.ec2_state.state_filter = Ec2StateFilter::Stopped;
app.handle_action(Action::PrevItem);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
app.handle_action(Action::PrevItem);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
}
#[test]
fn test_ec2_dropdown_cycles_with_arrows() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.mode = Mode::FilterInput;
app.ec2_state.input_focus = EC2_STATE_FILTER;
app.ec2_state.state_filter = Ec2StateFilter::Stopping;
app.handle_action(Action::NextItem);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
app.handle_action(Action::PrevItem);
assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
}
#[test]
fn test_collapse_row_ec2_instances() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.ec2_state.table.expanded_item = Some(0);
app.handle_action(Action::CollapseRow);
assert_eq!(app.ec2_state.table.expanded_item, None);
}
#[test]
fn test_collapse_row_ec2_tags() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.ec2_state.current_instance = Some("i-123".to_string());
app.ec2_state.detail_tab = Ec2DetailTab::Tags;
app.ec2_state.tags.expanded_item = Some(1);
app.handle_action(Action::CollapseRow);
assert_eq!(app.ec2_state.tags.expanded_item, None);
}
#[test]
fn test_collapse_row_cloudwatch_log_groups() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.log_groups_state.log_groups.expanded_item = Some(2);
app.handle_action(Action::CollapseRow);
assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
}
#[test]
fn test_collapse_row_cloudwatch_alarms() {
let mut app = test_app();
app.current_service = Service::CloudWatchAlarms;
app.alarms_state.table.expanded_item = Some(0);
app.handle_action(Action::CollapseRow);
assert_eq!(app.alarms_state.table.expanded_item, None);
}
#[test]
fn test_collapse_row_lambda_functions() {
let mut app = test_app();
app.current_service = Service::LambdaFunctions;
app.lambda_state.table.expanded_item = Some(1);
app.handle_action(Action::CollapseRow);
assert_eq!(app.lambda_state.table.expanded_item, None);
}
#[test]
fn test_collapse_row_cfn_stacks() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.cfn_state.table.expanded_item = Some(0);
app.handle_action(Action::CollapseRow);
assert_eq!(app.cfn_state.table.expanded_item, None);
}
#[test]
fn test_collapse_row_cfn_resources() {
let mut app = test_app();
app.current_service = Service::CloudFormationStacks;
app.cfn_state.current_stack = Some("test-stack".to_string());
app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
app.cfn_state.resources.expanded_item = Some(2);
app.handle_action(Action::CollapseRow);
assert_eq!(app.cfn_state.resources.expanded_item, None);
}
#[test]
fn test_collapse_row_iam_users() {
let mut app = test_app();
app.current_service = Service::IamUsers;
app.iam_state.users.expanded_item = Some(1);
app.handle_action(Action::CollapseRow);
assert_eq!(app.iam_state.users.expanded_item, None);
}
#[test]
fn test_collapse_row_does_nothing_when_not_expanded() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.ec2_state.table.expanded_item = None;
app.handle_action(Action::CollapseRow);
assert_eq!(app.ec2_state.table.expanded_item, None);
}
#[test]
fn test_s3_collapse_expanded_folder_moves_to_parent() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01T00:00:00Z".to_string(),
}];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![S3Object {
key: "folder1/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state
.expanded_prefixes
.insert("folder1/".to_string());
app.s3_state.prefix_preview.insert(
"folder1/".to_string(),
vec![S3Object {
key: "folder1/file.txt".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 1;
app.handle_action(Action::PrevPane);
assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_s3_collapse_hierarchy_level_by_level() {
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: "2024-01-01T00:00:00Z".to_string(),
}];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![S3Object {
key: "level1/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.expanded_prefixes.insert("level1/".to_string());
app.s3_state.prefix_preview.insert(
"level1/".to_string(),
vec![S3Object {
key: "level1/level2/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state
.expanded_prefixes
.insert("level1/level2/".to_string());
app.s3_state.prefix_preview.insert(
"level1/level2/".to_string(),
vec![S3Object {
key: "level1/level2/file.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 3;
app.handle_action(Action::PrevPane);
assert_eq!(app.s3_state.selected_row, 2);
app.handle_action(Action::PrevPane);
assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
assert_eq!(app.s3_state.selected_row, 1);
app.handle_action(Action::PrevPane);
assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
assert_eq!(app.s3_state.selected_row, 0);
app.handle_action(Action::PrevPane);
assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_ec2_instance_detail_tabs_no_preferences() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.ec2_state.table.expanded_item = Some(0);
app.mode = Mode::Normal;
app.ec2_state.detail_tab = Ec2DetailTab::Details;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.ec2_state.detail_tab = Ec2DetailTab::Security;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.ec2_state.detail_tab = Ec2DetailTab::Networking;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.ec2_state.detail_tab = Ec2DetailTab::Storage;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.ec2_state.detail_tab = Ec2DetailTab::Tags;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_ec2_monitoring_ctrl_u_scrolls_up() {
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.mode = Mode::Normal;
app.ec2_state.current_instance = Some("i-abc".to_string());
app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
app.ec2_state.set_monitoring_scroll(3);
assert_eq!(app.ec2_state.monitoring_scroll(), 3);
app.handle_action(Action::PageUp);
assert_eq!(
app.ec2_state.monitoring_scroll(),
2,
"Ctrl+U must scroll monitoring up by 1"
);
app.handle_action(Action::PageDown);
assert_eq!(
app.ec2_state.monitoring_scroll(),
3,
"Ctrl+D must scroll monitoring down by 1"
);
}
#[test]
fn test_ec2_instance_always_shows_when_state_filter_is_all_states() {
use crate::app::Ec2Instance;
use crate::ui::ec2::{filtered_ec2_instances, StateFilter};
let mut app = test_app();
app.current_service = Service::Ec2Instances;
app.service_selected = true;
app.ec2_state.table.filter.clear();
app.ec2_state.state_filter = StateFilter::AllStates;
app.ec2_state.table.items = vec![Ec2Instance {
instance_id: "i-05f181d4aaabadf5a".to_string(),
state: "running".to_string(),
..Default::default()
}];
let filtered = filtered_ec2_instances(&app);
assert_eq!(
filtered.len(),
1,
"Instance must appear with AllStates filter and no text filter"
);
assert_eq!(filtered[0].instance_id, "i-05f181d4aaabadf5a");
}
#[test]
fn test_ec2_instance_private_ip_populated_from_load() {
use crate::app::Ec2Instance;
let instance = Ec2Instance {
instance_id: "i-abc".to_string(),
name: "my-server".to_string(),
state: "running".to_string(),
private_dns_name: "ip-10-0-1-5.ec2.internal".to_string(),
private_ip_address: "10.0.1.5".to_string(),
security_group_ids: "sg-abc123".to_string(),
..Default::default()
};
assert!(
!instance.private_ip_address.is_empty(),
"private_ip_address must be populated"
);
assert!(
!instance.private_dns_name.is_empty(),
"private_dns_name must be populated"
);
}
#[test]
fn test_log_streams_filter_only_updates_when_focused() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.service_selected = true;
app.view_mode = ViewMode::Detail;
app.mode = Mode::FilterInput;
app.log_groups_state.stream_filter = "test".to_string();
app.log_groups_state.input_focus = InputFocus::Filter;
app.handle_action(Action::FilterInput('x'));
assert_eq!(app.log_groups_state.stream_filter, "testx");
app.log_groups_state.input_focus = InputFocus::Pagination;
app.handle_action(Action::FilterInput('y'));
assert_eq!(app.log_groups_state.stream_filter, "testx"); }
#[test]
fn test_log_streams_backspace_only_updates_when_focused() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.service_selected = true;
app.view_mode = ViewMode::Detail;
app.mode = Mode::FilterInput;
app.log_groups_state.stream_filter = "test".to_string();
app.log_groups_state.input_focus = InputFocus::Filter;
app.handle_action(Action::FilterBackspace);
assert_eq!(app.log_groups_state.stream_filter, "tes");
app.log_groups_state.input_focus = InputFocus::Pagination;
app.handle_action(Action::FilterBackspace);
assert_eq!(app.log_groups_state.stream_filter, "tes"); }
#[test]
fn test_log_groups_filter_only_updates_when_focused() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.service_selected = true;
app.view_mode = ViewMode::List;
app.mode = Mode::FilterInput;
app.log_groups_state.log_groups.filter = "test".to_string();
app.log_groups_state.input_focus = InputFocus::Filter;
app.handle_action(Action::FilterInput('x'));
assert_eq!(app.log_groups_state.log_groups.filter, "testx");
app.log_groups_state.input_focus = InputFocus::Pagination;
app.handle_action(Action::FilterInput('y'));
assert_eq!(app.log_groups_state.log_groups.filter, "testx"); }
#[test]
fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
use S3Bucket;
use S3Object;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.s3_state.buckets.items = vec![S3Bucket {
name: "test-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
}];
app.s3_state
.expanded_prefixes
.insert("test-bucket".to_string());
app.s3_state.bucket_preview.insert(
"test-bucket".to_string(),
vec![S3Object {
key: "folder1/".to_string(),
is_prefix: true,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state
.expanded_prefixes
.insert("folder1/".to_string());
app.s3_state.prefix_preview.insert(
"folder1/".to_string(),
vec![S3Object {
key: "folder1/folder2/".to_string(),
is_prefix: true,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 2;
app.handle_action(Action::CollapseRow);
assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
assert_eq!(app.s3_state.selected_row, 1);
}
#[test]
fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
use S3Bucket;
use S3Object;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.s3_state.buckets.items = vec![S3Bucket {
name: "test-bucket".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
}];
app.s3_state
.expanded_prefixes
.insert("test-bucket".to_string());
app.s3_state.bucket_preview.insert(
"test-bucket".to_string(),
vec![S3Object {
key: "folder1/".to_string(),
is_prefix: true,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state
.expanded_prefixes
.insert("folder1/".to_string());
app.s3_state.prefix_preview.insert(
"folder1/".to_string(),
vec![S3Object {
key: "folder1/file.txt".to_string(),
is_prefix: false,
size: 100,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 1;
app.handle_action(Action::CollapseRow);
assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_log_streams_pagination_limits_table_content() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.service_selected = true;
app.view_mode = ViewMode::Detail;
app.log_groups_state.log_streams = (0..50)
.map(|i| rusticity_core::LogStream {
name: format!("stream-{}", i),
creation_time: None,
last_event_time: None,
})
.collect();
app.log_groups_state.stream_page_size = 10;
app.log_groups_state.stream_current_page = 0;
assert_eq!(app.log_groups_state.stream_page_size, 10);
assert_eq!(app.log_groups_state.stream_current_page, 0);
app.log_groups_state.stream_current_page = 1;
assert_eq!(app.log_groups_state.stream_current_page, 1);
}
#[test]
fn test_log_streams_page_size_change_resets_page() {
let mut app = test_app();
app.current_service = Service::CloudWatchLogGroups;
app.service_selected = true;
app.view_mode = ViewMode::Detail;
app.mode = Mode::ColumnSelector;
app.log_groups_state.stream_page_size = 10;
app.log_groups_state.stream_current_page = 3;
app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; app.handle_action(Action::ToggleColumn);
assert_eq!(app.log_groups_state.stream_page_size, 25);
assert_eq!(app.log_groups_state.stream_current_page, 0);
}
#[test]
fn test_s3_objects_expanded_rows_stay_visible() {
use S3Object;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.current_bucket = Some("test-bucket".to_string());
app.s3_state.objects = vec![S3Object {
key: "folder1/".to_string(),
is_prefix: true,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}];
app.s3_state
.expanded_prefixes
.insert("folder1/".to_string());
app.s3_state.prefix_preview.insert(
"folder1/".to_string(),
(0..20)
.map(|i| S3Object {
key: format!("folder1/file{}.txt", i),
is_prefix: false,
size: 100,
last_modified: String::new(),
storage_class: String::new(),
})
.collect(),
);
app.s3_state.object_visible_rows.set(10);
app.s3_state.object_scroll_offset = 0;
app.s3_state.selected_object = 0;
for i in 1..=20 {
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_object, i);
let visible_start = app.s3_state.object_scroll_offset;
let visible_end = visible_start + app.s3_state.object_visible_rows.get();
assert!(
app.s3_state.selected_object >= visible_start
&& app.s3_state.selected_object < visible_end,
"Selection {} should be visible in range [{}, {})",
app.s3_state.selected_object,
visible_start,
visible_end
);
}
}
#[test]
fn test_s3_bucket_error_rows_counted_in_total() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG, s3_extended_request_id: CGSwddO9ummjFYFHKyqNEU= }".to_string();
app.s3_state
.bucket_errors
.insert("bucket1".to_string(), long_error.clone());
let total = app.calculate_total_bucket_rows();
let error_rows = long_error.len().div_ceil(120);
assert_eq!(total, 2 + error_rows);
}
#[test]
fn test_s3_bucket_with_error_can_be_collapsed() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
}];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
let error = "service error: PermanentRedirect".to_string();
app.s3_state
.bucket_errors
.insert("bucket1".to_string(), error);
app.s3_state.selected_row = 0;
app.handle_action(Action::CollapseRow);
assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_s3_bucket_collapse_on_bucket_row() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
}];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
let error = "service error: PermanentRedirect".to_string();
app.s3_state
.bucket_errors
.insert("bucket1".to_string(), error);
app.s3_state.selected_row = 0;
app.handle_action(Action::CollapseRow);
assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_s3_bucket_collapse_adjusts_scroll_offset() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = (0..20)
.map(|i| S3Bucket {
name: format!("bucket{}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state
.expanded_prefixes
.insert("bucket10".to_string());
let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG }".to_string();
app.s3_state
.bucket_errors
.insert("bucket10".to_string(), long_error.clone());
app.s3_state.bucket_visible_rows.set(10);
app.s3_state.bucket_scroll_offset = 10;
app.s3_state.selected_row = 10;
app.handle_action(Action::CollapseRow);
assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
assert_eq!(app.s3_state.selected_row, 10);
assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
assert!(
app.s3_state.selected_row
< app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
);
}
#[test]
fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket3".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.expanded_prefixes.insert("bucket2".to_string());
app.s3_state.bucket_preview.insert(
"bucket2".to_string(),
vec![
S3Object {
key: "folder1/".to_string(),
is_prefix: true,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
},
S3Object {
key: "file1.txt".to_string(),
is_prefix: false,
size: 100,
last_modified: String::new(),
storage_class: String::new(),
},
],
);
app.s3_state.expanded_prefixes.insert("bucket3".to_string());
let error = "service error: PermanentRedirect".to_string();
app.s3_state
.bucket_errors
.insert("bucket3".to_string(), error);
app.s3_state.bucket_visible_rows.set(10);
app.s3_state.bucket_scroll_offset = 0;
app.s3_state.selected_row = 3;
app.handle_action(Action::CollapseRow);
assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
assert_eq!(app.s3_state.selected_row, 1);
assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
assert!(
app.s3_state.selected_row
< app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
);
}
#[test]
fn test_s3_collapse_bucket_with_error() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
let error = "service error: unhandled error (PermanentRedirect)".to_string();
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state
.bucket_errors
.insert("bucket1".to_string(), error);
app.s3_state.bucket_visible_rows.set(10);
app.s3_state.bucket_scroll_offset = 0;
app.s3_state.selected_row = 0;
app.handle_action(Action::CollapseRow);
assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_s3_collapse_row_with_multiple_error_buckets() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket3".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
let error = "service error: unhandled error (PermanentRedirect)".to_string();
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state
.bucket_errors
.insert("bucket1".to_string(), error.clone());
app.s3_state.expanded_prefixes.insert("bucket3".to_string());
app.s3_state
.bucket_errors
.insert("bucket3".to_string(), error.clone());
app.s3_state.bucket_visible_rows.set(30);
app.s3_state.bucket_scroll_offset = 0;
app.s3_state.selected_row = 2;
app.handle_action(Action::CollapseRow);
assert!(
!app.s3_state.expanded_prefixes.contains("bucket3"),
"bucket3 should be collapsed"
);
assert!(
app.s3_state.expanded_prefixes.contains("bucket1"),
"bucket1 should still be expanded"
);
assert_eq!(app.s3_state.selected_row, 2);
}
#[test]
fn test_s3_collapse_row_nested_only_collapses_one_level() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
}];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![S3Object {
key: "level1/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.expanded_prefixes.insert("level1/".to_string());
app.s3_state.prefix_preview.insert(
"level1/".to_string(),
vec![S3Object {
key: "level1/level2/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state
.expanded_prefixes
.insert("level1/level2/".to_string());
app.s3_state.prefix_preview.insert(
"level1/level2/".to_string(),
vec![S3Object {
key: "level1/level2/file.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: String::new(),
}],
);
app.s3_state.bucket_visible_rows.set(10);
app.s3_state.selected_row = 2;
app.handle_action(Action::CollapseRow);
assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
assert!(app.s3_state.expanded_prefixes.contains("level1/"));
assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
assert_eq!(app.s3_state.selected_row, 1);
}
#[test]
fn test_s3_collapse_row_deeply_nested_file() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
}];
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![S3Object {
key: "level1/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state.expanded_prefixes.insert("level1/".to_string());
app.s3_state.prefix_preview.insert(
"level1/".to_string(),
vec![S3Object {
key: "level1/level2/".to_string(),
size: 0,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: true,
storage_class: String::new(),
}],
);
app.s3_state
.expanded_prefixes
.insert("level1/level2/".to_string());
app.s3_state.prefix_preview.insert(
"level1/level2/".to_string(),
vec![S3Object {
key: "level1/level2/file.txt".to_string(),
size: 100,
last_modified: "2024-01-01T00:00:00Z".to_string(),
is_prefix: false,
storage_class: String::new(),
}],
);
app.s3_state.bucket_visible_rows.set(10);
app.s3_state.selected_row = 3;
app.handle_action(Action::CollapseRow);
assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
assert!(app.s3_state.expanded_prefixes.contains("level1/"));
assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
assert_eq!(app.s3_state.selected_row, 2);
}
#[test]
fn test_s3_bucket_pagination_adjusts_scroll() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = (0..150)
.map(|i| S3Bucket {
name: format!("bucket{:03}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state.bucket_visible_rows.set(20);
app.s3_state.selected_row = 0;
app.s3_state.bucket_scroll_offset = 0;
app.go_to_page(2);
assert_eq!(app.s3_state.selected_row, 50);
assert_eq!(app.s3_state.bucket_scroll_offset, 50);
app.go_to_page(3);
assert_eq!(app.s3_state.selected_row, 100);
assert_eq!(app.s3_state.bucket_scroll_offset, 100);
app.go_to_page(1);
assert_eq!(app.s3_state.selected_row, 0);
assert_eq!(app.s3_state.bucket_scroll_offset, 0);
}
#[test]
fn test_s3_bucket_pagination_uses_page_size() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = (0..100)
.map(|i| S3Bucket {
name: format!("bucket{:03}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state.bucket_visible_rows.set(20);
app.s3_state.selected_row = 0;
assert_eq!(app.s3_state.buckets.page_size.value(), 50);
app.go_to_page(2);
assert_eq!(app.s3_state.selected_row, 50);
assert_eq!(app.s3_state.bucket_scroll_offset, 50);
app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
assert_eq!(app.s3_state.buckets.page_size.value(), 25);
app.go_to_page(2);
assert_eq!(app.s3_state.selected_row, 25);
assert_eq!(app.s3_state.bucket_scroll_offset, 25);
}
#[test]
fn test_s3_bucket_page_size_limits_visible_rows() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = (0..100)
.map(|i| S3Bucket {
name: format!("bucket{:03}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
assert_eq!(app.s3_state.buckets.page_size.value(), 10);
let total_rows = app.calculate_total_bucket_rows();
assert!(total_rows >= 10, "Should have at least 10 rows");
}
#[test]
fn test_s3_bucket_tab_cycling_in_filter() {
use crate::common::InputFocus;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.mode = Mode::FilterInput;
assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_s3_bucket_pagination_navigation_with_arrows() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.mode = Mode::FilterInput;
app.s3_state.buckets.items = (0..100)
.map(|i| S3Bucket {
name: format!("bucket{:03}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
app.s3_state.selected_row = 0;
app.s3_state.input_focus = crate::common::InputFocus::Pagination;
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 10);
app.handle_action(Action::NextItem);
assert_eq!(app.s3_state.selected_row, 20);
app.handle_action(Action::PrevItem);
assert_eq!(app.s3_state.selected_row, 10);
}
#[test]
fn test_s3_bucket_go_to_page_shows_correct_buckets() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = (0..100)
.map(|i| S3Bucket {
name: format!("bucket{:03}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
app.go_to_page(2);
assert_eq!(app.s3_state.selected_row, 10);
app.go_to_page(5);
assert_eq!(app.s3_state.selected_row, 40);
}
#[test]
fn test_s3_bucket_left_right_arrows_change_pages() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.mode = Mode::FilterInput;
app.s3_state.buckets.items = (0..100)
.map(|i| S3Bucket {
name: format!("bucket{:03}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
app.s3_state.selected_row = 0;
app.s3_state.input_focus = crate::common::InputFocus::Pagination;
app.handle_action(Action::PageDown);
assert_eq!(app.s3_state.selected_row, 10);
app.handle_action(Action::PageDown);
assert_eq!(app.s3_state.selected_row, 20);
app.handle_action(Action::PageUp);
assert_eq!(app.s3_state.selected_row, 10);
}
#[test]
fn test_s3_bucket_preview_uses_bucket_region_not_config_region() {
use S3Bucket;
let mut app = test_app();
app.config.region = "ap-southeast-3".to_string();
app.s3_state.buckets.items = vec![S3Bucket {
name: "my-bucket".to_string(),
region: "us-east-1".to_string(), creation_date: String::new(),
}];
let bucket = app
.s3_state
.buckets
.items
.iter()
.find(|b| b.name == "my-bucket")
.unwrap();
let effective_region = if bucket.region.is_empty() {
app.config.region.as_str()
} else {
bucket.region.as_str()
};
assert_eq!(
effective_region, "us-east-1",
"load_bucket_preview must use bucket's region, not app config region"
);
assert_ne!(
effective_region, "ap-southeast-3",
"Must NOT fall back to configured region for cross-region buckets"
);
}
#[test]
fn test_s3_scroll_follows_selection_after_bucket_expand() {
use S3Bucket;
use S3Object;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = (0..5)
.map(|i| S3Bucket {
name: format!("bucket{}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
app.s3_state.bucket_visible_rows = std::cell::Cell::new(3);
app.s3_state.selected_row = 2;
app.s3_state.bucket_scroll_offset = 0;
app.s3_state.bucket_preview.insert(
"bucket2".to_string(),
vec![
S3Object {
key: "file1.txt".to_string(),
is_prefix: false,
size: 100,
last_modified: String::new(),
storage_class: String::new(),
},
S3Object {
key: "file2.txt".to_string(),
is_prefix: false,
size: 200,
last_modified: String::new(),
storage_class: String::new(),
},
],
);
app.handle_action(Action::CollapseRow); app.handle_action(Action::NextPane);
let visible = app.s3_state.bucket_visible_rows.get();
let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
&& app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
assert!(
in_view,
"selected_row {} must be visible in [{}, {})",
app.s3_state.selected_row,
app.s3_state.bucket_scroll_offset,
app.s3_state.bucket_scroll_offset + visible
);
}
#[test]
fn test_s3_expand_without_preview_does_not_move_selection() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.selected_row = 0;
app.handle_action(Action::NextPane);
assert_eq!(
app.s3_state.selected_row, 0,
"Selection must not move before preview is loaded"
);
assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
assert!(app.s3_state.buckets.loading);
}
#[test]
fn test_s3_expand_with_preview_moves_to_first_child() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.selected_row = 0;
app.s3_state.bucket_preview.insert(
"bucket0".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.handle_action(Action::NextPane);
assert_eq!(app.s3_state.selected_row, 1, "Should move to first child");
}
#[test]
fn test_s3_expand_then_load_then_right_goes_to_first_child_not_sibling() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.selected_row = 0;
app.handle_action(Action::NextPane);
assert_eq!(
app.s3_state.selected_row, 0,
"Must stay on bucket0 while loading"
);
assert!(app.s3_state.buckets.loading);
app.s3_state.bucket_preview.insert(
"bucket0".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.buckets.loading = false;
app.handle_action(Action::NextPane);
assert_eq!(
app.s3_state.selected_row, 1,
"After preview loads, right arrow must select first child (row 1), not sibling (row 2)"
);
}
#[test]
fn test_s3_first_expand_never_moves_selection_before_children_visible() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.selected_row = 0;
app.handle_action(Action::ExpandRow);
assert_eq!(
app.s3_state.selected_row, 0,
"Selection must stay on bucket0 until children are visible"
);
assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
assert!(app.s3_state.buckets.loading);
}
#[test]
fn test_s3_after_preview_loads_selection_moves_to_first_child() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "b0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "b1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.selected_row = 0;
app.handle_action(Action::ExpandRow);
assert_eq!(
app.s3_state.selected_row, 0,
"Must stay on bucket while loading"
);
app.s3_state.bucket_preview.insert(
"b0".to_string(),
vec![S3Object {
key: "f.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.after_bucket_preview_loaded("b0");
assert_eq!(
app.s3_state.selected_row, 1,
"After preview loads, selection must advance to first child (row 1)"
);
}
#[test]
fn test_s3_right_arrow_on_expanded_loaded_bucket_enters_first_child() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "b0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "b1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.expanded_prefixes.insert("b0".to_string());
app.s3_state.bucket_preview.insert(
"b0".to_string(),
vec![S3Object {
key: "f.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 0;
app.handle_action(Action::ExpandRow);
assert!(
app.s3_state.expanded_prefixes.contains("b0"),
"Must stay expanded"
);
assert_eq!(app.s3_state.selected_row, 1, "Must move to first child");
}
#[test]
fn test_s3_second_expand_press_collapses_when_already_expanded_with_children() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
}];
app.s3_state.expanded_prefixes.insert("bucket0".to_string());
app.s3_state.bucket_preview.insert(
"bucket0".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 0;
app.handle_action(Action::CollapseRow);
assert!(
!app.s3_state.expanded_prefixes.contains("bucket0"),
"Left arrow must collapse the bucket"
);
assert_eq!(app.s3_state.selected_row, 0);
}
#[test]
fn test_s3_expand_row_index_correct_when_prior_bucket_has_error() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.expanded_prefixes.insert("bucket0".to_string());
app.s3_state
.bucket_errors
.insert("bucket0".to_string(), "AccessDenied".to_string());
let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
assert_eq!(
total, 4,
"Should be 4 rows: bucket0 + error + bucket1 + bucket2"
);
app.s3_state.selected_row = 3;
use crate::s3::Object as S3Object;
app.s3_state.bucket_preview.insert(
"bucket2".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.handle_action(Action::NextPane);
assert!(
app.s3_state.expanded_prefixes.contains("bucket2"),
"Must expand bucket2, not bucket1"
);
assert_eq!(
app.s3_state.selected_row, 4,
"First child of bucket2 must be row 4"
);
}
#[test]
fn test_s3_next_pane_row_count_matches_calculate_filtered_bucket_rows() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.expanded_prefixes.insert("bucket0".to_string());
app.s3_state.bucket_preview.insert(
"bucket0".to_string(),
vec![
S3Object {
key: "a.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
},
S3Object {
key: "b.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
},
S3Object {
key: "c.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
},
],
);
app.s3_state.expanded_prefixes.insert("bucket1".to_string());
let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
assert_eq!(
total, 6,
"Total rows: bucket0(1)+3children+bucket1(1)+bucket2(1)=6"
);
app.s3_state.selected_row = 5;
app.s3_state.bucket_preview.insert(
"bucket2".to_string(),
vec![S3Object {
key: "x.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.handle_action(Action::NextPane);
assert!(
app.s3_state.expanded_prefixes.contains("bucket2"),
"Must expand bucket2, not bucket1"
);
assert_eq!(
app.s3_state.selected_row, 6,
"First child of bucket2 must be row 6"
);
}
#[test]
fn test_s3_expand_row_index_correct_when_prior_bucket_has_loaded_children() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.expanded_prefixes.insert("bucket0".to_string());
app.s3_state.bucket_preview.insert(
"bucket0".to_string(),
vec![
S3Object {
key: "a.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
},
S3Object {
key: "b.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
},
],
);
app.s3_state.selected_row = 4;
app.s3_state.bucket_preview.insert(
"bucket2".to_string(),
vec![S3Object {
key: "c.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.handle_action(Action::NextPane);
assert!(
app.s3_state.expanded_prefixes.contains("bucket2"),
"bucket2 must be expanded"
);
assert_eq!(
app.s3_state.selected_row, 5,
"Must select first child of bucket2 (row 5), not some wrong row"
);
assert!(
!app.s3_state.expanded_prefixes.contains("bucket1"),
"bucket1 must NOT be expanded"
);
}
#[test]
fn test_s3_expand_with_empty_preview_stays_on_bucket() {
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state
.bucket_preview
.insert("bucket1".to_string(), vec![]);
app.s3_state.selected_row = 1;
app.handle_action(Action::NextPane);
assert_eq!(
app.s3_state.selected_row, 1,
"Empty bucket: selection must stay on bucket row, not advance to next sibling"
);
assert!(
app.s3_state.expanded_prefixes.contains("bucket1"),
"Bucket must be expanded (showing ▼)"
);
}
#[test]
fn test_s3_expand_with_preceding_loading_buckets_on_prior_page() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = (0..60)
.map(|i| S3Bucket {
name: format!("bucket{:02}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
for i in 0..3usize {
app.s3_state
.expanded_prefixes
.insert(format!("bucket{:02}", i));
}
app.s3_state.bucket_scroll_offset = 50;
app.s3_state.bucket_preview.insert(
"bucket55".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 55;
app.handle_action(Action::NextPane);
assert!(
app.s3_state.expanded_prefixes.contains("bucket55"),
"bucket55 must be expanded"
);
assert_eq!(
app.s3_state.selected_row, 56,
"Must select first child of bucket55 (row 56)"
);
}
#[test]
fn test_s3_expand_with_multiple_preceding_loading_buckets() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = (0..5)
.map(|i| S3Bucket {
name: format!("bucket{}", i),
region: "us-east-1".to_string(),
creation_date: String::new(),
})
.collect();
for i in 0..3 {
app.s3_state
.expanded_prefixes
.insert(format!("bucket{}", i));
}
app.s3_state.bucket_preview.insert(
"bucket3".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 3;
app.handle_action(Action::NextPane);
assert!(
app.s3_state.expanded_prefixes.contains("bucket3"),
"bucket3 must be expanded"
);
assert_eq!(
app.s3_state.selected_row, 4,
"Must select first child of bucket3 (row 4)"
);
}
#[test]
fn test_s3_expand_with_preceding_loading_bucket_selects_correct_row() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.expanded_prefixes.insert("bucket0".to_string());
app.s3_state.buckets.loading = true;
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.s3_state.selected_row = 1;
app.handle_action(Action::NextPane);
assert_eq!(
app.s3_state.selected_row, 2,
"Must expand bucket1's first child (row 2), not bucket2 (row 3)"
);
assert!(
app.s3_state.expanded_prefixes.contains("bucket1"),
"bucket1 must be in expanded_prefixes"
);
}
#[test]
fn test_s3_expand_last_row_on_page_shows_first_child_not_next_page() {
use crate::s3::Object as S3Object;
use S3Bucket;
let mut app = test_app();
app.current_service = Service::S3Buckets;
app.service_selected = true;
app.mode = Mode::Normal;
app.s3_state.buckets.items = vec![
S3Bucket {
name: "bucket0".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket1".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
S3Bucket {
name: "bucket2".to_string(),
region: "us-east-1".to_string(),
creation_date: String::new(),
},
];
app.s3_state.bucket_visible_rows = std::cell::Cell::new(2);
app.s3_state.selected_row = 1;
app.s3_state.bucket_scroll_offset = 0;
app.s3_state.bucket_preview.insert(
"bucket1".to_string(),
vec![S3Object {
key: "file.txt".to_string(),
is_prefix: false,
size: 0,
last_modified: String::new(),
storage_class: String::new(),
}],
);
app.handle_action(Action::NextPane);
assert_eq!(app.s3_state.selected_row, 2, "Must select first child");
let visible = app.s3_state.bucket_visible_rows.get();
let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
&& app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
assert!(
in_view,
"First child row {} must be visible in [{}, {})",
app.s3_state.selected_row,
app.s3_state.bucket_scroll_offset,
app.s3_state.bucket_scroll_offset + visible
);
}
#[test]
fn test_apig_detail_tab_navigation() {
use crate::apig::api::RestApi;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
}
#[test]
fn test_apig_routes_expand_collapse() {
use crate::apig::api::RestApi;
use crate::apig::route::Route;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
let virtual_parent = Route {
route_id: "virtual_/api".to_string(),
route_key: "/api".to_string(),
target: String::new(), authorization_type: String::new(),
api_key_required: false,
display_name: String::new(),
arn: String::new(),
};
let child_route = Route {
route_id: "1".to_string(),
route_key: "/api/users".to_string(),
target: "integration1".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: String::new(),
arn: String::new(),
};
app.apig_state.routes.items = vec![virtual_parent];
app.apig_state
.route_children
.insert("/api".to_string(), vec![child_route]);
assert!(app.apig_state.expanded_routes.is_empty());
app.apig_state.routes.selected = 0;
app.handle_action(Action::ExpandRow);
assert!(app.apig_state.expanded_routes.contains("/api"));
app.handle_action(Action::CollapseRow);
assert!(!app.apig_state.expanded_routes.contains("/api"));
app.handle_action(Action::ExpandRow);
assert!(app.apig_state.expanded_routes.contains("/api"));
}
#[test]
fn test_apig_routes_navigation() {
use crate::apig::api::RestApi;
use crate::apig::route::Route;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.mode = Mode::Normal;
app.service_selected = true;
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.routes.items = vec![
Route {
route_id: "1".to_string(),
route_key: "/api/users".to_string(),
target: "integration1".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: String::new(),
arn: String::new(),
},
Route {
route_id: "2".to_string(),
route_key: "/health".to_string(),
target: "integration2".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: String::new(),
arn: String::new(),
},
];
assert_eq!(app.apig_state.routes.selected, 0);
app.handle_action(Action::NextItem);
assert_eq!(app.apig_state.routes.selected, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.apig_state.routes.selected, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.apig_state.routes.selected, 0);
app.handle_action(Action::PrevItem);
assert_eq!(app.apig_state.routes.selected, 0);
}
#[test]
fn test_apig_routes_expand_jumps_to_child() {
use crate::apig::api::RestApi;
use crate::apig::route::Route;
use crate::ui::apig::ApiDetailTab;
use std::collections::HashMap;
let mut app = test_app();
app.mode = Mode::Normal;
app.service_selected = true;
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.routes.items = vec![Route {
route_id: "virtual_/api".to_string(),
route_key: "/api".to_string(),
target: String::new(),
authorization_type: String::new(),
api_key_required: false,
display_name: String::new(),
arn: String::new(),
}];
let mut children = HashMap::new();
children.insert(
"/api".to_string(),
vec![Route {
route_id: "1".to_string(),
route_key: "/api/users".to_string(),
target: "integration1".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: String::new(),
arn: String::new(),
}],
);
app.apig_state.route_children = children;
assert_eq!(app.apig_state.routes.selected, 0);
assert!(!app.apig_state.expanded_routes.contains("/api"));
app.handle_action(Action::ExpandRow);
assert!(app.apig_state.expanded_routes.contains("/api"));
assert_eq!(app.apig_state.routes.selected, 0);
app.handle_action(Action::ExpandRow);
assert_eq!(app.apig_state.routes.selected, 1);
}
#[test]
fn test_apig_filter_only_when_focused() {
use crate::apig::api::RestApi;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.mode = Mode::FilterInput;
app.apig_state.input_focus = InputFocus::Pagination;
app.handle_action(Action::FilterInput('x'));
assert_eq!(app.apig_state.route_filter, "");
app.apig_state.input_focus = InputFocus::Filter;
app.handle_action(Action::FilterInput('x'));
assert_eq!(app.apig_state.route_filter, "x");
}
#[test]
fn test_apig_routes_and_resources_use_same_render_function() {
let source = include_str!("ui/apig.rs");
let render_calls: Vec<_> = source
.match_indices("crate::ui::table::render_tree_table")
.collect();
assert_eq!(
render_calls.len(),
2,
"Both routes and resources must use render_tree_table"
);
}
#[test]
fn test_s3_uses_same_render_function() {
let source = include_str!("ui/s3.rs");
let render_calls: Vec<_> = source
.match_indices("crate::ui::table::render_tree_table")
.collect();
assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
}
#[test]
fn test_search_icon_has_proper_border_spacing() {
use crate::ui::SEARCH_ICON;
assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
assert!(
SEARCH_ICON.ends_with("─"),
"Should end with dash for proper border spacing"
);
assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
}
#[test]
fn test_apig_expand_with_filter() {
use crate::apig::api::RestApi;
use crate::apig::route::Route;
use crate::ui::apig::ApiDetailTab;
use std::collections::HashMap;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.routes.items = vec![Route {
route_id: "0".to_string(),
route_key: "/api".to_string(),
target: "".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: "/api".to_string(),
arn: String::new(),
}];
let mut children = HashMap::new();
children.insert(
"/api".to_string(),
vec![Route {
route_id: "1".to_string(),
route_key: "GET".to_string(),
target: "integration1".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: "GET".to_string(),
arn: String::new(),
}],
);
app.apig_state.route_children = children;
app.apig_state.route_filter = "GET".to_string();
assert_eq!(app.apig_state.routes.selected, 0);
assert!(!app.apig_state.expanded_routes.contains("/api"));
app.handle_action(Action::ExpandRow);
assert!(app.apig_state.expanded_routes.contains("/api"));
}
#[test]
fn test_apig_console_url_routes() {
use crate::apig::api::RestApi;
use crate::apig::route::Route;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.config.region = "us-east-1".to_string();
let url = app.get_console_url();
assert!(url.contains("apigateway/main/apis"));
assert!(url.contains("region=us-east-1"));
app.apig_state.current_api = Some(RestApi {
id: "2todvod3n0".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.routes.items = vec![Route {
route_id: "eizmisr".to_string(),
route_key: "GET /test".to_string(),
target: "integration1".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: "GET /test".to_string(),
arn: String::new(),
}];
app.apig_state.routes.selected = 0;
let url = app.get_console_url();
assert!(url.contains("apigateway/main/develop/routes"));
assert!(url.contains("api=2todvod3n0"));
assert!(url.contains("routes=eizmisr"));
assert!(url.contains("region=us-east-1"));
}
#[test]
fn test_apig_console_url_resources() {
use crate::apig::api::RestApi;
use crate::apig::resource::Resource;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.config.region = "us-east-1".to_string();
app.apig_state.current_api = Some(RestApi {
id: "2j9j50ze47".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.resources.items = vec![Resource {
id: "abc123".to_string(),
path: "/test".to_string(),
parent_id: None,
methods: vec![],
display_name: "/test".to_string(),
arn: String::new(),
}];
app.apig_state.resources.selected = 0;
let url = app.get_console_url();
assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
assert!(url.contains("api=2j9j50ze47"));
assert!(url.contains("#abc123"));
assert!(url.contains("region=us-east-1"));
}
#[test]
fn test_apig_console_url_routes_parent_vs_leaf() {
use crate::apig::api::RestApi;
use crate::apig::route::Route;
use crate::ui::apig::ApiDetailTab;
use std::collections::HashMap;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.config.region = "us-east-1".to_string();
app.apig_state.current_api = Some(RestApi {
id: "2todvod3n0".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.routes.items = vec![Route {
route_id: "parent".to_string(),
route_key: "/v1/get/jobs".to_string(),
target: "".to_string(), authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: "/v1/get/jobs".to_string(),
arn: String::new(),
}];
let mut children = HashMap::new();
children.insert(
"/v1/get/jobs".to_string(),
vec![Route {
route_id: "1iz9vtl".to_string(),
route_key: "GET".to_string(),
target: "integration1".to_string(), authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: "GET".to_string(),
arn: String::new(),
}],
);
app.apig_state.route_children = children;
app.apig_state.routes.selected = 0;
let url = app.get_console_url();
assert!(url.contains("apigateway/main/develop/routes"));
assert!(url.contains("api=2todvod3n0"));
assert!(
!url.contains("routes="),
"Parent node should not include routes parameter"
);
app.apig_state
.expanded_routes
.insert("/v1/get/jobs".to_string());
app.apig_state.routes.selected = 1;
let url = app.get_console_url();
assert!(
url.contains("routes=1iz9vtl"),
"Leaf node should include routes parameter: {}",
url
);
}
#[test]
fn test_apig_preferences_context() {
use crate::apig::api::RestApi;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
assert!(
app.apig_state.current_api.is_none(),
"Should be in list view"
);
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
assert!(
app.apig_state.current_api.is_some(),
"Should be in detail view"
);
}
#[test]
fn test_apig_route_columns() {
use crate::apig::route::Column as RouteColumn;
let cols = RouteColumn::all();
assert_eq!(cols.len(), 5);
assert_eq!(RouteColumn::RouteKey.id(), "route_key");
assert_eq!(RouteColumn::RouteId.id(), "route_id");
assert_eq!(RouteColumn::Arn.id(), "arn");
assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
assert_eq!(RouteColumn::Target.id(), "target");
assert_eq!(
RouteColumn::from_id("route_key"),
Some(RouteColumn::RouteKey)
);
assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
assert_eq!(RouteColumn::from_id("invalid"), None);
}
#[test]
fn test_apig_yank_copies_route_arn() {
use crate::apig::api::RestApi;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.routes.items = vec![
Route {
route_id: "route1".to_string(),
route_key: "GET /users".to_string(),
target: "integrations/abc".to_string(),
authorization_type: "NONE".to_string(),
api_key_required: false,
display_name: "GET /users".to_string(),
arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
},
Route {
route_id: "route2".to_string(),
route_key: "POST /users".to_string(),
target: "integrations/def".to_string(),
authorization_type: "AWS_IAM".to_string(),
api_key_required: true,
display_name: "POST /users".to_string(),
arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
},
];
app.apig_state.routes.selected = 0;
assert_eq!(
app.apig_state.routes.items[0].arn,
"arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
);
}
#[test]
fn test_apig_yank_ignores_empty_arn() {
use crate::apig::api::RestApi;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
app.apig_state.routes.items = vec![Route {
route_id: String::new(),
route_key: "/users".to_string(),
target: String::new(), authorization_type: String::new(),
api_key_required: false,
display_name: "/users".to_string(),
arn: String::new(), }];
app.apig_state.routes.selected = 0;
assert!(
app.apig_state.routes.items[0].arn.is_empty(),
"Virtual parent should have empty ARN"
);
}
#[test]
fn test_apig_route_column_toggle() {
use crate::apig::api::RestApi;
use crate::apig::route::Column as RouteColumn;
use crate::keymap::Action;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.mode = Mode::ColumnSelector;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "HTTP".to_string(),
disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
assert_eq!(app.apig_route_visible_column_ids.len(), 5);
app.column_selector_index = 1;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_route_visible_column_ids.len(), 5);
assert!(app
.apig_route_visible_column_ids
.contains(&RouteColumn::RouteKey.id()));
app.column_selector_index = 3;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_route_visible_column_ids.len(), 4);
assert!(!app
.apig_route_visible_column_ids
.contains(&RouteColumn::Arn.id()));
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_route_visible_column_ids.len(), 5);
assert!(app
.apig_route_visible_column_ids
.contains(&RouteColumn::Arn.id()));
}
#[test]
fn test_apig_resource_column_toggle() {
use crate::apig::api::RestApi;
use crate::apig::resource::Column as ResourceColumn;
use crate::keymap::Action;
use crate::ui::apig::ApiDetailTab;
let mut app = test_app();
app.current_service = Service::ApiGatewayApis;
app.mode = Mode::ColumnSelector;
app.apig_state.current_api = Some(RestApi {
id: "test123".to_string(),
name: "Test API".to_string(),
description: "Test".to_string(),
created_date: "2024-01-01".to_string(),
api_key_source: "HEADER".to_string(),
endpoint_configuration: "REGIONAL".to_string(),
protocol_type: "REST".to_string(), disable_execute_api_endpoint: false,
status: "AVAILABLE".to_string(),
});
app.apig_state.detail_tab = ApiDetailTab::Routes;
assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
app.column_selector_index = 1;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
assert!(app
.apig_resource_visible_column_ids
.contains(&ResourceColumn::Path.id()));
app.column_selector_index = 3;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
assert!(!app
.apig_resource_visible_column_ids
.contains(&ResourceColumn::Arn.id()));
app.handle_action(Action::ToggleColumn);
assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
assert!(app
.apig_resource_visible_column_ids
.contains(&ResourceColumn::Arn.id()));
}
#[test]
fn test_cloudtrail_filter_input() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.handle_action(Action::StartFilter);
assert_eq!(app.mode, Mode::FilterInput);
app.cloudtrail_state.table.filter = "test".to_string();
assert_eq!(app.cloudtrail_state.table.filter, "test");
}
#[test]
fn test_cloudtrail_row_expansion() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.cloudtrail_state.table.items = vec![CloudTrailEvent {
event_name: "Event1".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user1".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "abc123".to_string(),
access_key_id: "AKIA...".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
}];
assert_eq!(app.cloudtrail_state.table.expanded_item, None);
app.expand_row();
assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
app.collapse_row();
assert_eq!(app.cloudtrail_state.table.expanded_item, None);
}
#[test]
fn test_cloudtrail_service_initialization() {
let app = App::new_without_client("default".to_string(), None);
assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
}
#[test]
fn test_cloudtrail_service_name() {
assert_eq!(
Service::CloudTrailEvents.name(),
"CloudTrail › Event History"
);
}
#[test]
fn test_cloudtrail_in_service_picker() {
let app = App::new_without_client("default".to_string(), None);
assert!(app
.service_picker
.services
.contains(&"CloudTrail › Event History"));
}
#[test]
fn test_cloudtrail_service_selection() {
let mut app = App::new_without_client("default".to_string(), None);
app.current_service = Service::CloudTrailEvents;
app.service_selected = true;
assert_eq!(app.current_service, Service::CloudTrailEvents);
assert!(app.service_selected);
}
#[test]
fn test_cloudtrail_filter_resets_selection() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.cloudtrail_state.table.selected = 5;
app.cloudtrail_state.table.expanded_item = Some(3);
app.handle_action(Action::StartFilter);
app.apply_filter_operation(|_| {});
assert_eq!(app.cloudtrail_state.table.selected, 0);
assert_eq!(app.cloudtrail_state.table.expanded_item, None);
}
#[test]
fn test_cloudtrail_column_toggle() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
app.column_selector_index = 7;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
}
#[test]
fn test_cloudtrail_tab_cycles_filter_focus() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::FilterInput;
app.cloudtrail_state.input_focus = InputFocus::Filter;
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::NextFilterFocus);
assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_cloudtrail_shift_tab_cycles_filter_focus() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::FilterInput;
app.cloudtrail_state.input_focus = InputFocus::Filter;
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
app.handle_action(Action::PrevFilterFocus);
assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
}
#[test]
fn test_cloudtrail_detail_view_tab_cycles_focus() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "AWS::S3::Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
});
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::Resources
);
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::EventRecord
);
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::Resources
);
}
#[test]
fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "AWS::S3::Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
});
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::Resources
);
app.handle_action(Action::PrevDetailTab);
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::EventRecord
);
app.handle_action(Action::PrevDetailTab);
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::Resources
);
}
#[test]
fn test_cloudtrail_json_scroll_with_arrow_keys() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "AWS::S3::Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: (0..50)
.map(|i| format!("line {}", i))
.collect::<Vec<_>>()
.join("\n"),
});
app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
app.cloudtrail_state.event_json_scroll = 0;
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
}
#[test]
fn test_cloudtrail_tab_works_with_no_resources() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "".to_string(), resource_name: "".to_string(), read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
});
app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::Resources
);
app.handle_action(Action::NextDetailTab);
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::EventRecord
);
}
#[test]
fn test_cloudtrail_resources_expand_collapse() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "AWS::S3::Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
});
app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
app.cloudtrail_state.resources_expanded_index = None;
app.handle_action(Action::ExpandRow);
assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
app.handle_action(Action::CollapseRow);
assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
}
#[test]
fn test_cloudtrail_event_json_no_column_selector() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "AWS::S3::Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
});
app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::Normal);
app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
}
#[test]
fn test_cloudtrail_resources_preferences_show_only_3_columns() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "AWS::S3::Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
});
app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
assert_eq!(app.get_column_count(), 3);
assert_eq!(app.get_column_selector_max(), 3);
assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
app.column_selector_index = 1;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
app.column_selector_index = 2;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
app.column_selector_index = 3;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
app.column_selector_index = 1;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
}
#[test]
fn test_cloudtrail_resources_preferences_tab_cycles() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
app.cloudtrail_state.current_event = Some(CloudTrailEvent {
event_name: "PutObject".to_string(),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "AWS::S3::Bucket".to_string(),
resource_name: "my-bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
source_ip_address: "192.0.2.1".to_string(),
error_code: "".to_string(),
request_id: "req-123".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
});
app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
app.column_selector_index = 1;
app.handle_action(Action::NextPreferences);
assert_eq!(app.column_selector_index, 0);
app.column_selector_index = 1;
app.handle_action(Action::PrevPreferences);
assert_eq!(app.column_selector_index, 0);
}
#[test]
fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
let visible_cols_3 = 3;
let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
assert_eq!(height_3, 7);
let visible_cols_2 = 2;
let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
assert_eq!(height_2, 6);
let visible_cols_1 = 1;
let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
assert_eq!(height_1, 5);
}
#[test]
fn test_cloudtrail_default_focus_is_resources() {
let app = App::new_without_client("default".to_string(), None);
assert_eq!(
app.cloudtrail_state.detail_focus,
CloudTrailDetailFocus::Resources
);
}
fn setup_cloudtrail_pagination_test() -> App {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::FilterInput;
app.cloudtrail_state.input_focus = InputFocus::Pagination;
app.cloudtrail_state.table.page_size = PageSize::Ten;
app.cloudtrail_state.table.items = (0..25)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
app
}
#[test]
fn test_cloudtrail_pagination_navigation() {
let mut app = setup_cloudtrail_pagination_test();
assert_eq!(app.cloudtrail_state.table.selected, 0);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 10);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 20);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 20);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.table.selected, 10);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.table.selected, 0);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.table.selected, 0);
}
#[test]
fn test_cloudtrail_pagination_navigation_right_arrow() {
let mut app = setup_cloudtrail_pagination_test();
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::FilterInput;
app.cloudtrail_state.input_focus = InputFocus::Pagination;
app.cloudtrail_state.table.page_size = PageSize::Ten;
app.cloudtrail_state.table.items = (0..25)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
assert_eq!(app.cloudtrail_state.table.selected, 0);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 10);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 20);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 20);
}
#[test]
fn test_cloudtrail_pagination_navigation_left_arrow() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::FilterInput;
app.cloudtrail_state.input_focus = InputFocus::Pagination;
app.cloudtrail_state.table.page_size = PageSize::Ten;
app.cloudtrail_state.table.items = (0..25)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
app.cloudtrail_state.table.selected = 20;
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.table.selected, 10);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.table.selected, 0);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.table.selected, 0);
}
#[test]
fn test_cloudtrail_arrow_navigation() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.table.items = (0..5)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 1);
app.handle_action(Action::NextItem);
assert_eq!(app.cloudtrail_state.table.selected, 2);
app.handle_action(Action::PrevItem);
assert_eq!(app.cloudtrail_state.table.selected, 1);
}
#[test]
fn test_cloudtrail_page_down_navigation() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.table.page_size = PageSize::Ten;
app.cloudtrail_state.table.items = (0..25)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
app.handle_action(Action::PageDown);
assert_eq!(app.cloudtrail_state.table.selected, 10);
app.handle_action(Action::PageDown);
assert_eq!(app.cloudtrail_state.table.selected, 20);
}
#[test]
fn test_cloudtrail_page_up_navigation() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::Normal;
app.cloudtrail_state.table.page_size = PageSize::Ten;
app.cloudtrail_state.table.items = (0..25)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
app.cloudtrail_state.table.selected = 20;
app.handle_action(Action::PageUp);
assert_eq!(app.cloudtrail_state.table.selected, 10);
app.handle_action(Action::PageUp);
assert_eq!(app.cloudtrail_state.table.selected, 0);
}
#[test]
fn test_cloudtrail_page_size_change_updates_display() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
app.cloudtrail_state.table.items = (0..50)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
app.column_selector_index = 17;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
}
#[test]
fn test_cloudtrail_all_columns_toggleable() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
for idx in 1..=14 {
app.column_selector_index = idx;
let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
let initial_count = initial_visible.len();
let col = app.cloudtrail_event_column_ids.get(idx - 1).unwrap();
let is_visible = initial_visible.contains(col);
app.handle_action(Action::ToggleColumn);
if is_visible && initial_count == 1 {
assert_eq!(
app.cloudtrail_event_visible_column_ids, initial_visible,
"Last visible column at index {} should not be toggleable",
idx
);
} else {
assert_ne!(
app.cloudtrail_event_visible_column_ids, initial_visible,
"Column at index {} should be toggleable when not the last one",
idx
);
}
}
}
#[test]
fn test_cloudtrail_readonly_column_toggleable() {
use crate::cloudtrail::events::CloudTrailEventColumn;
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
let readonly_id = CloudTrailEventColumn::ReadOnly.id();
assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
assert!(!app
.cloudtrail_event_visible_column_ids
.contains(&readonly_id));
app.column_selector_index = 7;
app.handle_action(Action::ToggleColumn);
assert!(
app.cloudtrail_event_visible_column_ids
.contains(&readonly_id),
"ReadOnly column should be toggleable at index 7"
);
app.handle_action(Action::ToggleColumn);
assert!(!app
.cloudtrail_event_visible_column_ids
.contains(&readonly_id));
}
#[test]
fn test_cloudtrail_pagination_limits_displayed_items() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.cloudtrail_state.table.items = (0..50)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
app.cloudtrail_state.table.page_size = PageSize::Ten;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let backend = TestBackend::new(100, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| {
let area = frame.area();
crate::ui::cloudtrail::render_events(frame, &app, area);
})
.unwrap();
assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
}
#[test]
fn test_cloudtrail_readonly_column_selectable_in_preferences() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
app.column_selector_index = 7;
let max_index = app.get_column_selector_max();
assert!(
app.column_selector_index <= max_index,
"Read-only column index {} should be <= max {}",
app.column_selector_index,
max_index
);
let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
let col = CloudTrailEventColumn::from_id(col_id);
assert!(col.is_some(), "Column should exist at index 7");
assert_eq!(
col.unwrap().default_name(),
"Read-only",
"Column at index 7 should be Read-only"
);
}
#[test]
fn test_cloudtrail_navigate_to_readonly_column() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.mode = Mode::ColumnSelector;
app.column_selector_index = 1;
println!(
"cloudtrail_event_column_ids.len() = {}",
app.cloudtrail_event_column_ids.len()
);
assert_eq!(
app.cloudtrail_event_column_ids.len(),
14,
"Should have 14 columns"
);
let column_count = app.get_column_count();
println!("Column count from get_column_count(): {}", column_count);
assert_eq!(column_count, 14, "Column count should be 14");
assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
assert!(app.is_blank_row_index(15), "Index 15 should be blank");
for _ in 0..6 {
app.handle_action(Action::NextItem);
}
assert_eq!(
app.column_selector_index, 7,
"Should navigate to Read-only column at index 7"
);
let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
let col = CloudTrailEventColumn::from_id(col_id).unwrap();
assert_eq!(col.default_name(), "Read-only");
}
#[test]
fn test_cloudtrail_navigate_beyond_loaded_items() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.cloudtrail_state.table.items = (0..50)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
let initial_selected = app.cloudtrail_state.table.selected;
app.page_input = "10".to_string();
app.go_to_page(10);
assert_eq!(
app.cloudtrail_state.table.selected, initial_selected,
"Should ignore navigation to page 10 when only 1 page is loaded"
);
app.go_to_page(2);
let page_size = app.cloudtrail_state.table.page_size.value();
assert_eq!(
app.cloudtrail_state.table.selected, page_size,
"Should allow navigation to page 2 (loaded + 1)"
);
}
#[test]
fn test_cloudtrail_page_change_resets_expansion() {
let mut app = App::new_without_client("default".to_string(), None);
app.service_selected = true;
app.current_service = Service::CloudTrailEvents;
app.cloudtrail_state.table.items = (0..100)
.map(|i| CloudTrailEvent {
event_name: format!("Event{}", i),
event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
username: "user".to_string(),
event_source: "s3.amazonaws.com".to_string(),
resource_type: "Bucket".to_string(),
resource_name: "bucket".to_string(),
read_only: "false".to_string(),
aws_region: "us-east-1".to_string(),
event_id: "id".to_string(),
access_key_id: "key".to_string(),
source_ip_address: "1.2.3.4".to_string(),
error_code: "".to_string(),
request_id: "req".to_string(),
event_type: "AwsApiCall".to_string(),
cloud_trail_event_json: "{}".to_string(),
})
.collect();
app.cloudtrail_state.table.expanded_item = Some(5);
assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
app.go_to_page(2);
assert_eq!(
app.cloudtrail_state.table.expanded_item, None,
"Page change should reset expanded_item"
);
}
#[test]
fn test_lambda_application_resources_have_columns() {
let app = test_app();
assert_eq!(app.lambda_resource_visible_column_ids.len(), 4);
assert_eq!(app.lambda_resource_column_ids.len(), 4);
assert!(app
.lambda_resource_visible_column_ids
.contains(&"column.lambda.resource.logical_id"));
assert!(app
.lambda_resource_column_ids
.contains(&"column.lambda.resource.logical_id"));
}
#[test]
fn test_lambda_functions_list_right_expands_left_collapses() {
use crate::lambda::Function;
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::LambdaFunctions;
app.service_selected = true;
app.lambda_state.table.items = vec![Function {
name: "fn1".to_string(),
arn: "arn::fn1".to_string(),
application: None,
description: String::new(),
package_type: String::new(),
runtime: "python3.12".to_string(),
architecture: String::new(),
code_size: 0,
code_sha256: String::new(),
memory_mb: 128,
timeout_seconds: 3,
last_modified: String::new(),
layers: vec![],
}];
app.lambda_state.table.selected = 0;
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.table.expanded_item, Some(0));
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_state.table.expanded_item, Some(0));
app.handle_action(Action::CollapseRow);
assert_eq!(app.lambda_state.table.expanded_item, None);
}
#[test]
fn test_lambda_applications_list_right_expands_left_collapses() {
use crate::lambda::Application;
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.lambda_application_state.table.items = vec![Application {
name: "my-app".to_string(),
arn: String::new(),
description: String::new(),
status: "ACTIVE".to_string(),
last_modified: String::new(),
}];
app.lambda_application_state.table.selected = 0;
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
app.handle_action(Action::NextPane);
assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
app.handle_action(Action::CollapseRow);
assert_eq!(app.lambda_application_state.table.expanded_item, None);
}
#[test]
fn test_lambda_application_enter_on_deployments_tab_does_not_switch_to_overview() {
use crate::lambda::Application;
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.lambda_application_state.table.items = vec![Application {
name: "my-app".to_string(),
arn: String::new(),
description: String::new(),
status: "ACTIVE".to_string(),
last_modified: String::new(),
}];
app.handle_action(Action::Select);
assert!(app.lambda_application_state.current_application.is_some());
use crate::ui::lambda::ApplicationDetailTab as LambdaApplicationDetailTab;
app.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Deployments;
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Deployments
);
app.handle_action(Action::Select);
assert_eq!(
app.lambda_application_state.detail_tab,
LambdaApplicationDetailTab::Deployments
);
}
#[test]
fn test_lambda_applications_pagination_focus_left_right_jump_pages() {
use crate::lambda::Application;
let mut app = test_app();
app.current_service = Service::LambdaApplications;
app.service_selected = true;
app.mode = Mode::FilterInput;
app.lambda_application_state.input_focus = InputFocus::Pagination;
app.lambda_application_state.table.items = (0..100)
.map(|i| Application {
name: format!("app{:03}", i),
arn: String::new(),
description: String::new(),
status: "ACTIVE".to_string(),
last_modified: String::new(),
})
.collect();
app.lambda_application_state.table.selected = 0;
app.handle_action(Action::PageDown);
assert_eq!(
app.lambda_application_state.table.selected, 50,
"Right arrow with pagination focus should jump to page 2"
);
app.handle_action(Action::PageUp);
assert_eq!(
app.lambda_application_state.table.selected, 0,
"Left arrow with pagination focus should jump back to page 1"
);
app.handle_action(Action::NextItem);
assert_eq!(
app.lambda_application_state.table.selected, 0,
"Down arrow must not move table when pagination is focused"
);
app.lambda_application_state.table.selected = 5;
app.handle_action(Action::PrevItem);
assert_eq!(
app.lambda_application_state.table.selected, 5,
"Up arrow must not move table when pagination is focused"
);
}
#[test]
fn test_cloudwatch_alarms_enter_drills_down() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
app.view_mode = ViewMode::List;
app.alarms_state.table.items = vec![Alarm {
name: "test-alarm".to_string(),
state: "ALARM".to_string(),
state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
description: "Test alarm".to_string(),
metric_name: "CPUUtilization".to_string(),
namespace: "AWS/EC2".to_string(),
statistic: "Average".to_string(),
period: 300,
comparison_operator: "GreaterThanThreshold".to_string(),
threshold: 80.0,
actions_enabled: true,
state_reason: "Threshold crossed".to_string(),
resource: "".to_string(),
dimensions: "".to_string(),
expression: "".to_string(),
alarm_type: "MetricAlarm".to_string(),
cross_account: "".to_string(),
}];
assert!(app.alarms_state.current_alarm.is_none());
assert_eq!(app.view_mode, ViewMode::List);
assert!(!app.alarms_state.metrics_loading);
app.handle_action(Action::Select);
assert_eq!(
app.alarms_state.current_alarm,
Some("test-alarm".to_string())
);
assert_eq!(app.view_mode, ViewMode::Detail);
assert!(
app.alarms_state.metrics_loading,
"Should trigger metrics loading"
);
}
#[test]
fn test_cloudwatch_alarms_metric_data_renders() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
app.view_mode = ViewMode::Detail;
app.alarms_state.table.items = vec![Alarm {
name: "test-alarm".to_string(),
state: "ALARM".to_string(),
state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
description: "Test alarm".to_string(),
metric_name: "CPUUtilization".to_string(),
namespace: "AWS/EC2".to_string(),
statistic: "Average".to_string(),
period: 300,
comparison_operator: "GreaterThanThreshold".to_string(),
threshold: 80.0,
actions_enabled: true,
state_reason: "Threshold crossed".to_string(),
resource: "".to_string(),
dimensions: "".to_string(),
expression: "".to_string(),
alarm_type: "MetricAlarm".to_string(),
cross_account: "".to_string(),
}];
app.alarms_state.current_alarm = Some("test-alarm".to_string());
app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0), (3000, 70.0)];
app.alarms_state.metrics_loading = false;
assert!(!app.alarms_state.metric_data.is_empty());
assert_eq!(app.alarms_state.metric_data.len(), 3);
assert!(!app.alarms_state.metrics_loading);
}
#[test]
fn test_cloudwatch_alarms_back_clears_metrics() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
app.view_mode = ViewMode::Detail;
app.alarms_state.current_alarm = Some("test-alarm".to_string());
app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
assert!(!app.alarms_state.metric_data.is_empty());
app.handle_action(Action::GoBack);
assert!(app.alarms_state.current_alarm.is_none());
assert!(app.alarms_state.metric_data.is_empty());
assert_eq!(app.view_mode, ViewMode::List);
}
#[test]
fn test_cloudwatch_alarms_refresh_reloads_metrics() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
app.view_mode = ViewMode::Detail;
app.alarms_state.current_alarm = Some("test-alarm".to_string());
app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
app.alarms_state.metrics_loading = false;
assert!(!app.alarms_state.metric_data.is_empty());
assert!(!app.alarms_state.metrics_loading);
app.handle_action(Action::Refresh);
assert!(!app.alarms_state.metric_data.is_empty());
assert!(app.alarms_state.metrics_loading);
}
#[test]
fn test_cloudwatch_alarms_right_arrow_expands() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
app.view_mode = ViewMode::List;
app.alarms_state.table.items = vec![Alarm {
name: "test-alarm".to_string(),
state: "ALARM".to_string(),
state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
description: "Test alarm".to_string(),
metric_name: "CPUUtilization".to_string(),
namespace: "AWS/EC2".to_string(),
statistic: "Average".to_string(),
period: 300,
comparison_operator: "GreaterThanThreshold".to_string(),
threshold: 80.0,
actions_enabled: true,
state_reason: "Threshold crossed".to_string(),
resource: "".to_string(),
dimensions: "".to_string(),
expression: "".to_string(),
alarm_type: "MetricAlarm".to_string(),
cross_account: "".to_string(),
}];
assert!(!app.alarms_state.table.is_expanded());
app.handle_action(Action::NextPane);
assert!(app.alarms_state.table.is_expanded());
app.handle_action(Action::PrevPane);
assert!(!app.alarms_state.table.is_expanded());
}
#[test]
fn test_cloudwatch_alarms_tab_switches_tabs() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
app.handle_action(Action::NextDetailTab);
assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
app.handle_action(Action::PrevDetailTab);
assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
}
#[test]
fn test_column_toggle_prevents_hiding_last_column() {
let mut app = test_app();
app.mode = Mode::Normal;
app.current_service = Service::CloudWatchAlarms;
app.service_selected = true;
app.cw_alarm_visible_column_ids = vec!["column.cw.alarm.name"];
app.handle_action(Action::OpenColumnSelector);
assert_eq!(app.mode, Mode::ColumnSelector);
app.column_selector_index = 1;
app.handle_action(Action::ToggleColumn);
assert_eq!(app.cw_alarm_visible_column_ids.len(), 1);
assert_eq!(app.cw_alarm_visible_column_ids[0], "column.cw.alarm.name");
}
}