Skip to main content

rusticity_term/rds/
instance.rs

1use crate::common::{format_iso_timestamp, translate_column, ColumnId};
2use crate::ui::table::Column as TableColumn;
3use ratatui::prelude::*;
4use rusticity_core::rds::{InventoryOptions, RdsInstance};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Column {
8    Identifier,
9    Status,
10    ClusterIdentifier,
11    Role,
12    Engine,
13    EngineVersion,
14    UpgradeRolloutOrder,
15    ExtendedSupport,
16    RegionAz,
17    Class,
18    Recommendations,
19    Cpu,
20    CurrentActivity,
21    Maintenance,
22    Vpc,
23    MultiAz,
24    StorageType,
25    Storage,
26    Iops,
27    StorageThroughput,
28    SecurityGroups,
29    SubnetGroup,
30    CertificateExpiry,
31    PendingChanges,
32    CharacterSet,
33    OptionGroup,
34    CreatedTime,
35    Encrypted,
36    PriorityTier,
37    ReplicaLag,
38    ReplicaState,
39    ParameterGroup,
40    CloneGroupId,
41    Endpoint,
42    Port,
43    Arn,
44}
45
46impl Column {
47    pub const ALL: [Self; 36] = [
48        Self::Identifier,
49        Self::Status,
50        Self::ClusterIdentifier,
51        Self::Role,
52        Self::Engine,
53        Self::EngineVersion,
54        Self::UpgradeRolloutOrder,
55        Self::ExtendedSupport,
56        Self::RegionAz,
57        Self::Class,
58        Self::Recommendations,
59        Self::Cpu,
60        Self::CurrentActivity,
61        Self::Maintenance,
62        Self::Vpc,
63        Self::MultiAz,
64        Self::StorageType,
65        Self::Storage,
66        Self::Iops,
67        Self::StorageThroughput,
68        Self::SecurityGroups,
69        Self::SubnetGroup,
70        Self::CertificateExpiry,
71        Self::PendingChanges,
72        Self::CharacterSet,
73        Self::OptionGroup,
74        Self::CreatedTime,
75        Self::Encrypted,
76        Self::PriorityTier,
77        Self::ReplicaLag,
78        Self::ReplicaState,
79        Self::ParameterGroup,
80        Self::CloneGroupId,
81        Self::Endpoint,
82        Self::Port,
83        Self::Arn,
84    ];
85
86    fn definition(self) -> (ColumnId, &'static str, usize) {
87        match self {
88            Self::Identifier => ("column.rds.instance.identifier", "DB identifier", 26),
89            Self::Status => ("column.rds.instance.status", "Status", 18),
90            Self::ClusterIdentifier => (
91                "column.rds.instance.cluster_identifier",
92                "DB cluster identifier",
93                26,
94            ),
95            Self::Role => ("column.rds.instance.role", "Role", 14),
96            Self::Engine => ("column.rds.instance.engine", "Engine", 18),
97            Self::EngineVersion => ("column.rds.instance.engine_version", "Engine version", 14),
98            Self::UpgradeRolloutOrder => (
99                "column.rds.instance.upgrade_rollout_order",
100                "Upgrade rollout order",
101                21,
102            ),
103            Self::ExtendedSupport => (
104                "column.rds.instance.extended_support",
105                "RDS Extended Support",
106                20,
107            ),
108            Self::RegionAz => ("column.rds.instance.region_az", "Region & AZ", 25),
109            Self::Class => ("column.rds.instance.class", "Size", 18),
110            Self::Recommendations => ("column.rds.instance.recommendations", "Recommendations", 16),
111            Self::Cpu => ("column.rds.instance.cpu", "CPU", 10),
112            Self::CurrentActivity => (
113                "column.rds.instance.current_activity",
114                "Current activity",
115                18,
116            ),
117            Self::Maintenance => ("column.rds.instance.maintenance", "Maintenance", 22),
118            Self::Vpc => ("column.rds.instance.vpc", "VPC", 24),
119            Self::MultiAz => ("column.rds.instance.multi_az", "Multi-AZ", 10),
120            Self::StorageType => ("column.rds.instance.storage_type", "Storage type", 14),
121            Self::Storage => ("column.rds.instance.storage", "Storage", 14),
122            Self::Iops => ("column.rds.instance.iops", "Provisioned IOPS", 16),
123            Self::StorageThroughput => (
124                "column.rds.instance.storage_throughput",
125                "Storage throughput",
126                20,
127            ),
128            Self::SecurityGroups => ("column.rds.instance.security_groups", "Security groups", 24),
129            Self::SubnetGroup => (
130                "column.rds.instance.subnet_group",
131                "DB subnet group name",
132                24,
133            ),
134            Self::CertificateExpiry => (
135                "column.rds.instance.certificate_expiry",
136                "DB Certificate Expiry",
137                27,
138            ),
139            Self::PendingChanges => ("column.rds.instance.pending_changes", "Pending changes", 22),
140            Self::CharacterSet => ("column.rds.instance.character_set", "Character set", 14),
141            Self::OptionGroup => ("column.rds.instance.option_group", "Option group", 24),
142            Self::CreatedTime => ("column.rds.instance.created_time", "Created time", 27),
143            Self::Encrypted => ("column.rds.instance.encrypted", "Encrypted", 12),
144            Self::PriorityTier => ("column.rds.instance.priority_tier", "Priority tier", 14),
145            Self::ReplicaLag => ("column.rds.instance.replica_lag", "Replica lag", 14),
146            Self::ReplicaState => ("column.rds.instance.replica_state", "Replica State", 18),
147            Self::ParameterGroup => ("column.rds.instance.parameter_group", "Parameter group", 24),
148            Self::CloneGroupId => ("column.rds.instance.clone_group_id", "Clone group ID", 26),
149            Self::Endpoint => ("column.rds.instance.endpoint", "Endpoint", 48),
150            Self::Port => ("column.rds.instance.port", "Port", 7),
151            Self::Arn => ("column.rds.instance.arn", "ARN", 48),
152        }
153    }
154    pub fn id(self) -> ColumnId {
155        self.definition().0
156    }
157    pub fn default_name(self) -> &'static str {
158        self.definition().1
159    }
160    pub fn name(self) -> String {
161        translate_column(self.id(), self.default_name())
162    }
163    pub fn from_id(id: &str) -> Option<Self> {
164        match id {
165            "column.rds.instance.region" | "column.rds.instance.availability_zone" => {
166                Some(Self::RegionAz)
167            }
168            _ => Self::ALL.into_iter().find(|column| column.id() == id),
169        }
170    }
171    pub fn ids() -> Vec<ColumnId> {
172        Self::ALL.into_iter().map(Self::id).collect()
173    }
174    pub fn default_visible_ids() -> Vec<ColumnId> {
175        [
176            Self::Identifier,
177            Self::Status,
178            Self::Engine,
179            Self::EngineVersion,
180            Self::RegionAz,
181            Self::Class,
182            Self::Endpoint,
183        ]
184        .into_iter()
185        .map(Self::id)
186        .collect()
187    }
188
189    pub fn inventory_options(ids: &[ColumnId]) -> InventoryOptions {
190        let has = |column: Self| ids.contains(&column.id());
191        InventoryOptions {
192            clusters: has(Self::Role) || has(Self::CloneGroupId) || has(Self::ReplicaLag),
193            recommendations: has(Self::Recommendations),
194            maintenance: has(Self::Maintenance),
195            cpu: has(Self::Cpu),
196            current_activity: has(Self::CurrentActivity),
197            replica_lag: has(Self::ReplicaLag),
198        }
199    }
200
201    pub fn render(self, instance: &RdsInstance) -> (String, Style) {
202        if self == Self::Status {
203            let (value, color) = format_status(&instance.status);
204            return (value, Style::default().fg(color));
205        }
206        let value = match self {
207            Self::Identifier => instance.identifier.clone(),
208            Self::Status => instance.status.clone(),
209            Self::ClusterIdentifier => instance.cluster_identifier.clone(),
210            Self::Role => instance.role.clone(),
211            Self::Engine => instance.engine.clone(),
212            Self::EngineVersion => instance.engine_version.clone(),
213            Self::UpgradeRolloutOrder => instance.upgrade_rollout_order.clone(),
214            Self::ExtendedSupport => instance.extended_support.clone(),
215            Self::RegionAz => [
216                instance.region.as_str(),
217                instance.availability_zone.as_str(),
218            ]
219            .into_iter()
220            .filter(|s| !s.is_empty())
221            .collect::<Vec<_>>()
222            .join(" / "),
223            Self::Class => instance.instance_class.clone(),
224            Self::Recommendations => instance.recommendations.clone(),
225            Self::Cpu => instance.cpu.clone(),
226            Self::CurrentActivity => instance.current_activity.clone(),
227            Self::Maintenance => instance.maintenance.clone(),
228            Self::Vpc => instance.vpc.clone(),
229            Self::MultiAz => yes_no(instance.multi_az),
230            Self::StorageType => instance.storage_type.clone(),
231            Self::Storage => instance
232                .storage_gib
233                .map(|v| format!("{v} GiB"))
234                .unwrap_or_default(),
235            Self::Iops => number(instance.iops),
236            Self::StorageThroughput => instance
237                .storage_throughput
238                .map(|v| format!("{v} MiB/s"))
239                .unwrap_or_default(),
240            Self::SecurityGroups => instance.security_groups.clone(),
241            Self::SubnetGroup => instance.subnet_group.clone(),
242            Self::CertificateExpiry => format_iso_timestamp(&instance.certificate_expiry),
243            Self::PendingChanges => instance.pending_changes.clone(),
244            Self::CharacterSet => instance.character_set.clone(),
245            Self::OptionGroup => instance.option_group.clone(),
246            Self::CreatedTime => format_iso_timestamp(&instance.created_time),
247            Self::Encrypted => yes_no(instance.encrypted),
248            Self::PriorityTier => number(instance.priority_tier),
249            Self::ReplicaLag => instance.replica_lag.clone(),
250            Self::ReplicaState => instance.replica_state.clone(),
251            Self::ParameterGroup => instance.parameter_group.clone(),
252            Self::CloneGroupId => instance.clone_group_id.clone(),
253            Self::Endpoint => instance.endpoint.clone(),
254            Self::Port => number(instance.port),
255            Self::Arn => instance.arn.clone(),
256        };
257        let color = if value.is_empty() || value == "-" {
258            Color::DarkGray
259        } else if value == "Unavailable" {
260            Color::Yellow
261        } else if matches!(
262            self,
263            Self::Encrypted | Self::MultiAz | Self::ExtendedSupport
264        ) {
265            match value.as_str() {
266                "Yes" | "Enabled" => Color::Green,
267                "No" | "Disabled" => Color::Yellow,
268                _ => Color::White,
269            }
270        } else {
271            Color::White
272        };
273        (
274            if value.is_empty() { "-".into() } else { value },
275            Style::default().fg(color),
276        )
277    }
278
279    /// Own the translated label rather than leaking an allocation for name().
280    pub fn into_table_column(self) -> Box<dyn TableColumn<RdsInstance>> {
281        Box::new(InstanceColumn {
282            column: self,
283            label: self.name(),
284        })
285    }
286}
287
288fn number(value: Option<i32>) -> String {
289    value.map(|v| v.to_string()).unwrap_or_default()
290}
291fn yes_no(value: Option<bool>) -> String {
292    value
293        .map(|v| if v { "Yes" } else { "No" }.into())
294        .unwrap_or_default()
295}
296
297struct InstanceColumn {
298    column: Column,
299    label: String,
300}
301impl TableColumn<RdsInstance> for InstanceColumn {
302    fn name(&self) -> &str {
303        &self.label
304    }
305    fn width(&self) -> u16 {
306        self.label
307            .chars()
308            .count()
309            .max(self.column.definition().2)
310            .min(u16::MAX as usize) as u16
311    }
312    fn render(&self, item: &RdsInstance) -> (String, Style) {
313        self.column.render(item)
314    }
315}
316
317pub fn format_status(status: &str) -> (String, Color) {
318    let (label, color) = match status.to_ascii_lowercase().as_str() {
319        "available" => ("✅ Available", Color::Green),
320        "creating" => ("⏳ Creating", Color::Yellow),
321        "starting" => ("⏳ Starting", Color::Yellow),
322        "stopping" => ("⏳ Stopping", Color::Yellow),
323        "stopped" => ("Stopped", Color::DarkGray),
324        "modifying" => ("🔄 Modifying", Color::Yellow),
325        "rebooting" => ("🔄 Rebooting", Color::Yellow),
326        "backing-up" => ("Backing up", Color::Yellow),
327        "maintenance" => ("Maintenance", Color::Yellow),
328        "upgrading" => ("🔄 Upgrading", Color::Yellow),
329        "deleting" => ("🗑 Deleting", Color::Red),
330        "failed" => ("⚠ Failed", Color::Red),
331        "incompatible-restore"
332        | "incompatible-network"
333        | "inaccessible-encryption-credentials"
334        | "storage-full" => return (status.into(), Color::Red),
335        "" => ("-", Color::DarkGray),
336        _ => return (status.into(), Color::White),
337    };
338    (label.into(), color)
339}
340
341pub fn status_color(status: &str) -> Color {
342    format_status(status).1
343}