1use std::str::FromStr;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub enum Status {
7 Running,
8 Success,
9 Stopped,
10 Pending,
11 Failed,
12 Unknown(String),
13}
14
15impl FromStr for Status {
16 type Err = std::convert::Infallible;
17
18 fn from_str(s: &str) -> Result<Self, Self::Err> {
19 Ok(match s.to_uppercase().as_str() {
20 "RUNNING" => Status::Running,
21 "SUCCESS" | "COMPLETED" | "FINISHED" => Status::Success,
22 "IDLE" | "STOPPED" | "TERMINATED" | "DELETED" | "SKIPPED" | "CANCELED" => {
23 Status::Stopped
24 }
25 "PENDING"
26 | "STARTING"
27 | "RESTARTING"
28 | "DELETING"
29 | "TERMINATING"
30 | "QUEUED"
31 | "WAITING_FOR_RETRY"
32 | "BLOCKED"
33 | "CREATED"
34 | "INITIALIZING"
35 | "RESETTING"
36 | "SETTING_UP_TABLES"
37 | "WAITING_FOR_RESOURCES"
38 | "STOPPING" => Status::Pending,
39 "FAILED" | "ERROR" | "TIMEDOUT" | "TIMED_OUT" | "INTERNAL_ERROR" => Status::Failed,
40 other => Status::Unknown(other.to_string()),
41 })
42 }
43}
44
45impl Status {
46 pub fn rank(&self) -> u8 {
49 match self {
50 Status::Running => 0,
51 Status::Pending => 1,
52 Status::Failed => 2,
53 Status::Success => 3,
54 Status::Stopped => 4,
55 Status::Unknown(_) => 5,
56 }
57 }
58
59 pub fn label(&self) -> &str {
60 match self {
61 Status::Running => "RUNNING",
62 Status::Success => "SUCCESS",
63 Status::Stopped => "IDLE",
64 Status::Pending => "PENDING",
65 Status::Failed => "FAILED",
66 Status::Unknown(s) => s.as_str(),
67 }
68 }
69}
70
71pub fn relative_time(epoch_ms: u64) -> String {
73 let now = std::time::SystemTime::now()
74 .duration_since(std::time::UNIX_EPOCH)
75 .map(|d| d.as_millis() as u64)
76 .unwrap_or(0);
77 let secs = now.saturating_sub(epoch_ms) / 1000;
78 match secs {
79 0..=59 => "just now".to_string(),
80 60..=3599 => format!("{}m ago", secs / 60),
81 3600..=86_399 => format!("{}h ago", secs / 3600),
82 _ => format!("{}d ago", secs / 86_400),
83 }
84}
85
86pub fn fmt_duration_ms(ms: u64) -> String {
88 let secs = ms / 1000;
89 if secs < 60 {
90 format!("{}s", secs)
91 } else if secs < 3600 {
92 format!("{}m {}s", secs / 60, secs % 60)
93 } else {
94 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
95 }
96}
97
98#[derive(Debug, Clone)]
99pub enum Shape {
100 List(Vec<ListItem>),
101 Table(TableData),
102 Badge(BadgeData),
103 Text(String),
104}
105
106#[derive(Debug, Clone, Default)]
107pub struct ListItem {
108 pub name: String,
109 pub status: Status,
110 pub detail: Option<String>,
111 pub id: Option<String>,
113 pub history: Vec<Status>,
115 pub alert: Option<String>,
118}
119
120impl Default for Status {
121 fn default() -> Self {
122 Status::Unknown(String::new())
123 }
124}
125
126pub fn fav_key(item: &ListItem) -> String {
129 item.id.clone().unwrap_or_else(|| item.name.clone())
130}
131
132pub fn item_matches(item: &ListItem, query: &str) -> bool {
135 if query.is_empty() {
136 return true;
137 }
138 let q = query.to_lowercase();
139 item.name.to_lowercase().contains(&q)
140 || item
141 .detail
142 .as_deref()
143 .is_some_and(|d| d.to_lowercase().contains(&q))
144 || item.status.label().to_lowercase().contains(&q)
145}
146
147#[derive(Debug, Clone)]
149pub struct DetailData {
150 pub summary: Vec<(String, String)>,
152 pub activity: Vec<(Status, String)>,
154 pub raw: String,
156}
157
158#[derive(Debug, Clone)]
159pub struct TableData {
160 pub headers: Vec<String>,
161 pub rows: Vec<Vec<String>>,
162}
163
164impl TableData {
165 pub fn to_csv(&self) -> String {
168 fn field(s: &str) -> String {
169 if s.contains([',', '"', '\n', '\r']) {
170 format!("\"{}\"", s.replace('"', "\"\""))
171 } else {
172 s.to_string()
173 }
174 }
175 let mut out = String::new();
176 out.push_str(
177 &self
178 .headers
179 .iter()
180 .map(|h| field(h))
181 .collect::<Vec<_>>()
182 .join(","),
183 );
184 out.push('\n');
185 for row in &self.rows {
186 out.push_str(&row.iter().map(|c| field(c)).collect::<Vec<_>>().join(","));
187 out.push('\n');
188 }
189 out
190 }
191}
192
193#[derive(Debug, Clone)]
194pub struct BadgeData {
195 pub label: String,
196 pub value: String,
197}