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 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}