1use crate::common::{format_bytes, format_timestamp, ColumnId, UTC_TIMESTAMP_WIDTH};
2use crate::ui::table;
3use ratatui::prelude::*;
4use rusticity_core::{LogEvent, LogGroup, LogStream};
5use std::collections::HashMap;
6
7pub fn init(i18n: &mut HashMap<String, String>) {
8 for col in [
9 StreamColumn::LogStream,
10 StreamColumn::ARN,
11 StreamColumn::CreationTime,
12 StreamColumn::FirstEventTime,
13 StreamColumn::LastEventTime,
14 StreamColumn::LastIngestionTime,
15 StreamColumn::UploadSequenceToken,
16 ] {
17 i18n.entry(col.id().to_string())
18 .or_insert_with(|| col.default_name().to_string());
19 }
20
21 for col in [EventColumn::Timestamp, EventColumn::Message] {
22 i18n.entry(col.id().to_string())
23 .or_insert_with(|| col.default_name().to_string());
24 }
25
26 for col in [
27 LogGroupColumn::LogGroup,
28 LogGroupColumn::LogClass,
29 LogGroupColumn::Retention,
30 LogGroupColumn::StoredBytes,
31 LogGroupColumn::CreationTime,
32 LogGroupColumn::ARN,
33 ] {
34 i18n.entry(col.id().to_string())
35 .or_insert_with(|| col.default_name().to_string());
36 }
37}
38
39pub fn console_url_list(region: &str) -> String {
40 format!(
41 "https://{}.console.aws.amazon.com/cloudwatch/home?region={}#logsV2:log-groups",
42 region, region
43 )
44}
45
46pub fn console_url_detail(region: &str, group_name: &str) -> String {
47 let encoded_group = urlencoding::encode(group_name);
48 format!(
49 "https://{}.console.aws.amazon.com/cloudwatch/home?region={}#logsV2:log-groups/log-group/{}",
50 region, region, encoded_group
51 )
52}
53
54pub fn console_url_stream(region: &str, group_name: &str, stream_name: &str) -> String {
55 let encoded_group = urlencoding::encode(group_name);
56 let encoded_stream = urlencoding::encode(stream_name);
57 format!(
58 "https://{}.console.aws.amazon.com/cloudwatch/home?region={}#logsV2:log-groups/log-group/{}/log-events/{}",
59 region, region, encoded_group, encoded_stream
60 )
61}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
65pub enum LogGroupColumn {
66 LogGroup,
67 LogClass,
68 Retention,
69 StoredBytes,
70 CreationTime,
71 ARN,
72}
73
74impl LogGroupColumn {
75 pub fn id(&self) -> &'static str {
76 match self {
77 LogGroupColumn::LogGroup => "column.cw.group.log_group",
78 LogGroupColumn::LogClass => "column.cw.group.log_class",
79 LogGroupColumn::Retention => "column.cw.group.retention",
80 LogGroupColumn::StoredBytes => "column.cw.group.stored_bytes",
81 LogGroupColumn::CreationTime => "column.cw.group.creation_time",
82 LogGroupColumn::ARN => "column.cw.group.arn",
83 }
84 }
85
86 pub fn default_name(&self) -> &'static str {
87 match self {
88 LogGroupColumn::LogGroup => "Log group",
89 LogGroupColumn::LogClass => "Log class",
90 LogGroupColumn::Retention => "Retention",
91 LogGroupColumn::StoredBytes => "Stored bytes",
92 LogGroupColumn::CreationTime => "Creation time",
93 LogGroupColumn::ARN => "ARN",
94 }
95 }
96
97 pub fn from_id(id: ColumnId) -> Option<Self> {
98 Self::try_from(id).ok()
99 }
100
101 pub fn all() -> [LogGroupColumn; 6] {
102 [
103 LogGroupColumn::LogGroup,
104 LogGroupColumn::LogClass,
105 LogGroupColumn::Retention,
106 LogGroupColumn::StoredBytes,
107 LogGroupColumn::CreationTime,
108 LogGroupColumn::ARN,
109 ]
110 }
111
112 pub fn ids() -> Vec<ColumnId> {
113 Self::all().iter().map(|c| c.id()).collect()
114 }
115
116 pub fn default_visible() -> Vec<ColumnId> {
117 vec![
118 LogGroupColumn::LogGroup.id(),
119 LogGroupColumn::StoredBytes.id(),
120 ]
121 }
122}
123
124impl TryFrom<ColumnId> for LogGroupColumn {
125 type Error = ();
126
127 fn try_from(id: ColumnId) -> Result<Self, Self::Error> {
128 Self::all().into_iter().find(|c| c.id() == id).ok_or(())
129 }
130}
131
132impl table::Column<LogGroup> for LogGroupColumn {
133 fn id(&self) -> &'static str {
134 Self::id(self)
135 }
136
137 fn default_name(&self) -> &'static str {
138 Self::default_name(self)
139 }
140
141 fn width(&self) -> u16 {
142 match self {
143 Self::LogGroup => 50,
144 Self::LogClass => 15,
145 Self::Retention => 10,
146 Self::StoredBytes => 15,
147 Self::CreationTime => UTC_TIMESTAMP_WIDTH,
148 Self::ARN => 80,
149 }
150 }
151
152 fn render(&self, item: &LogGroup) -> (String, Style) {
153 let text = match self {
154 Self::LogGroup => item.name.clone(),
155 Self::LogClass => item.log_class.clone().unwrap_or_else(|| "-".to_string()),
156 Self::Retention => item
157 .retention_days
158 .map(|d| d.to_string())
159 .unwrap_or_else(|| "Never".to_string()),
160 Self::StoredBytes => format_bytes(item.stored_bytes.unwrap_or(0)),
161 Self::CreationTime => item
162 .creation_time
163 .map(|t| format_timestamp(&t))
164 .unwrap_or_else(|| "-".to_string()),
165 Self::ARN => item.arn.clone().unwrap_or_else(|| "-".to_string()),
166 };
167 (text, Style::default())
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub enum StreamColumn {
173 LogStream,
174 ARN,
175 CreationTime,
176 FirstEventTime,
177 LastEventTime,
178 LastIngestionTime,
179 UploadSequenceToken,
180}
181
182impl StreamColumn {
183 pub fn id(&self) -> &'static str {
184 match self {
185 StreamColumn::LogStream => "column.cw.stream.log_stream",
186 StreamColumn::ARN => "column.cw.stream.arn",
187 StreamColumn::CreationTime => "column.cw.stream.creation_time",
188 StreamColumn::FirstEventTime => "column.cw.stream.first_event_time",
189 StreamColumn::LastEventTime => "column.cw.stream.last_event_time",
190 StreamColumn::LastIngestionTime => "column.cw.stream.last_ingestion_time",
191 StreamColumn::UploadSequenceToken => "column.cw.stream.upload_sequence_token",
192 }
193 }
194
195 pub fn default_name(&self) -> &'static str {
196 match self {
197 StreamColumn::LogStream => "Log stream",
198 StreamColumn::ARN => "ARN",
199 StreamColumn::CreationTime => "Creation time",
200 StreamColumn::FirstEventTime => "First event time",
201 StreamColumn::LastEventTime => "Last event time",
202 StreamColumn::LastIngestionTime => "Last ingestion time",
203 StreamColumn::UploadSequenceToken => "Upload sequence token",
204 }
205 }
206
207 pub fn from_id(id: ColumnId) -> Option<Self> {
208 Self::try_from(id).ok()
209 }
210
211 pub fn all() -> [StreamColumn; 7] {
212 [
213 StreamColumn::LogStream,
214 StreamColumn::ARN,
215 StreamColumn::CreationTime,
216 StreamColumn::FirstEventTime,
217 StreamColumn::LastEventTime,
218 StreamColumn::LastIngestionTime,
219 StreamColumn::UploadSequenceToken,
220 ]
221 }
222
223 pub fn ids() -> Vec<ColumnId> {
224 Self::all().iter().map(|c| c.id()).collect()
225 }
226
227 pub fn default_visible() -> Vec<ColumnId> {
228 [
229 StreamColumn::LogStream,
230 StreamColumn::CreationTime,
231 StreamColumn::LastEventTime,
232 ]
233 .iter()
234 .map(|c| c.id())
235 .collect()
236 }
237}
238
239impl TryFrom<ColumnId> for StreamColumn {
240 type Error = ();
241
242 fn try_from(id: ColumnId) -> Result<Self, Self::Error> {
243 Self::all().into_iter().find(|c| c.id() == id).ok_or(())
244 }
245}
246
247impl table::Column<LogStream> for StreamColumn {
248 fn id(&self) -> &'static str {
249 Self::id(self)
250 }
251
252 fn default_name(&self) -> &'static str {
253 Self::default_name(self)
254 }
255
256 fn width(&self) -> u16 {
257 match self {
258 StreamColumn::LogStream => 50,
259 StreamColumn::ARN => 80,
260 StreamColumn::CreationTime => UTC_TIMESTAMP_WIDTH,
261 StreamColumn::FirstEventTime => UTC_TIMESTAMP_WIDTH,
262 StreamColumn::LastEventTime => UTC_TIMESTAMP_WIDTH,
263 StreamColumn::LastIngestionTime => UTC_TIMESTAMP_WIDTH,
264 StreamColumn::UploadSequenceToken => 30,
265 }
266 }
267
268 fn render(&self, item: &LogStream) -> (String, Style) {
269 let text = match self {
270 StreamColumn::LogStream => item.name.clone(),
271 StreamColumn::ARN => "-".to_string(),
272 StreamColumn::CreationTime => item
273 .creation_time
274 .map(|t| format_timestamp(&t))
275 .unwrap_or_else(|| "-".to_string()),
276 StreamColumn::FirstEventTime => "-".to_string(),
277 StreamColumn::LastEventTime => item
278 .last_event_time
279 .map(|t| format_timestamp(&t))
280 .unwrap_or_else(|| "-".to_string()),
281 StreamColumn::LastIngestionTime => "-".to_string(),
282 StreamColumn::UploadSequenceToken => "-".to_string(),
283 };
284 (text, Style::default())
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq)]
289pub enum EventColumn {
290 Timestamp,
291 IngestionTime,
292 Message,
293 EventId,
294 LogStreamName,
295}
296
297impl EventColumn {
298 pub fn id(&self) -> &'static str {
299 match self {
300 EventColumn::Timestamp => "column.cw.event.timestamp",
301 EventColumn::IngestionTime => "column.cw.event.ingestion_time",
302 EventColumn::Message => "column.cw.event.message",
303 EventColumn::EventId => "column.cw.event.event_id",
304 EventColumn::LogStreamName => "column.cw.event.log_stream_name",
305 }
306 }
307
308 pub fn default_name(&self) -> &'static str {
309 match self {
310 EventColumn::Timestamp => "Timestamp",
311 EventColumn::IngestionTime => "Ingestion time",
312 EventColumn::Message => "Message",
313 EventColumn::EventId => "Event ID",
314 EventColumn::LogStreamName => "Log stream name",
315 }
316 }
317
318 pub fn from_id(id: ColumnId) -> Option<Self> {
319 Self::try_from(id).ok()
320 }
321
322 pub fn all() -> [EventColumn; 5] {
323 [
324 EventColumn::Timestamp,
325 EventColumn::IngestionTime,
326 EventColumn::Message,
327 EventColumn::EventId,
328 EventColumn::LogStreamName,
329 ]
330 }
331
332 pub fn ids() -> Vec<ColumnId> {
333 Self::all().iter().map(|c| c.id()).collect()
334 }
335
336 pub fn default_visible() -> Vec<ColumnId> {
337 [EventColumn::Timestamp, EventColumn::Message]
338 .iter()
339 .map(|c| c.id())
340 .collect()
341 }
342}
343
344impl TryFrom<ColumnId> for EventColumn {
345 type Error = ();
346
347 fn try_from(id: ColumnId) -> Result<Self, Self::Error> {
348 Self::all().into_iter().find(|c| c.id() == id).ok_or(())
349 }
350}
351
352impl table::Column<LogEvent> for EventColumn {
353 fn id(&self) -> &'static str {
354 Self::id(self)
355 }
356
357 fn default_name(&self) -> &'static str {
358 Self::default_name(self)
359 }
360
361 fn width(&self) -> u16 {
362 match self {
363 EventColumn::Timestamp => UTC_TIMESTAMP_WIDTH,
364 EventColumn::IngestionTime => UTC_TIMESTAMP_WIDTH,
365 EventColumn::Message => 100,
366 EventColumn::EventId => 30,
367 EventColumn::LogStreamName => 50,
368 }
369 }
370
371 fn render(&self, item: &LogEvent) -> (String, Style) {
372 let text = match self {
373 EventColumn::Timestamp => format_timestamp(&item.timestamp),
374 EventColumn::IngestionTime => "-".to_string(),
375 EventColumn::Message => item.message.clone(),
376 EventColumn::EventId => "-".to_string(),
377 EventColumn::LogStreamName => "-".to_string(),
378 };
379 (text, Style::default())
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_stream_column_all_returns_seven_columns() {
389 let columns = StreamColumn::all();
390 assert_eq!(columns.len(), 7);
391 }
392
393 #[test]
394 fn test_event_column_all_returns_five_columns() {
395 let columns = EventColumn::all();
396 assert_eq!(columns.len(), 5);
397 }
398
399 #[test]
400 fn test_log_group_column_id_returns_full_key() {
401 let id = LogGroupColumn::LogGroup.id();
402 assert_eq!(id, "column.cw.group.log_group");
403 assert!(id.starts_with("column."));
404 }
405
406 #[test]
407 fn test_stream_column_id_returns_full_key() {
408 let id = StreamColumn::LogStream.id();
409 assert_eq!(id, "column.cw.stream.log_stream");
410 assert!(id.starts_with("column."));
411 }
412
413 #[test]
414 fn test_event_column_id_returns_full_key() {
415 let id = EventColumn::Timestamp.id();
416 assert_eq!(id, "column.cw.event.timestamp");
417 assert!(id.starts_with("column."));
418 }
419}