use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Status {
Running,
Success,
Stopped,
Pending,
Failed,
Unknown(String),
}
impl FromStr for Status {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_uppercase().as_str() {
"RUNNING" => Status::Running,
"SUCCESS" | "COMPLETED" | "FINISHED" => Status::Success,
"IDLE" | "STOPPED" | "TERMINATED" | "DELETED" | "SKIPPED" | "CANCELED" => {
Status::Stopped
}
"PENDING"
| "STARTING"
| "RESTARTING"
| "DELETING"
| "TERMINATING"
| "QUEUED"
| "WAITING_FOR_RETRY"
| "BLOCKED"
| "CREATED"
| "INITIALIZING"
| "RESETTING"
| "SETTING_UP_TABLES"
| "WAITING_FOR_RESOURCES"
| "STOPPING" => Status::Pending,
"FAILED" | "ERROR" | "TIMEDOUT" | "TIMED_OUT" | "INTERNAL_ERROR" => Status::Failed,
other => Status::Unknown(other.to_string()),
})
}
}
impl Status {
pub fn rank(&self) -> u8 {
match self {
Status::Running => 0,
Status::Pending => 1,
Status::Failed => 2,
Status::Success => 3,
Status::Stopped => 4,
Status::Unknown(_) => 5,
}
}
pub fn label(&self) -> &str {
match self {
Status::Running => "RUNNING",
Status::Success => "SUCCESS",
Status::Stopped => "IDLE",
Status::Pending => "PENDING",
Status::Failed => "FAILED",
Status::Unknown(s) => s.as_str(),
}
}
}
pub fn relative_time(epoch_ms: u64) -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let secs = now.saturating_sub(epoch_ms) / 1000;
match secs {
0..=59 => "just now".to_string(),
60..=3599 => format!("{}m ago", secs / 60),
3600..=86_399 => format!("{}h ago", secs / 3600),
_ => format!("{}d ago", secs / 86_400),
}
}
pub fn fmt_duration_ms(ms: u64) -> String {
let secs = ms / 1000;
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m {}s", secs / 60, secs % 60)
} else {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
}
}
#[derive(Debug, Clone)]
pub enum Shape {
List(Vec<ListItem>),
Table(TableData),
Badge(BadgeData),
Text(String),
}
#[derive(Debug, Clone, Default)]
pub struct ListItem {
pub name: String,
pub status: Status,
pub detail: Option<String>,
pub id: Option<String>,
pub history: Vec<Status>,
pub alert: Option<String>,
}
impl Default for Status {
fn default() -> Self {
Status::Unknown(String::new())
}
}
pub fn item_matches(item: &ListItem, query: &str) -> bool {
if query.is_empty() {
return true;
}
let q = query.to_lowercase();
item.name.to_lowercase().contains(&q)
|| item
.detail
.as_deref()
.is_some_and(|d| d.to_lowercase().contains(&q))
|| item.status.label().to_lowercase().contains(&q)
}
#[derive(Debug, Clone)]
pub struct DetailData {
pub summary: Vec<(String, String)>,
pub activity: Vec<(Status, String)>,
pub raw: String,
}
#[derive(Debug, Clone)]
pub struct TableData {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
}
impl TableData {
pub fn to_csv(&self) -> String {
fn field(s: &str) -> String {
if s.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
let mut out = String::new();
out.push_str(
&self
.headers
.iter()
.map(|h| field(h))
.collect::<Vec<_>>()
.join(","),
);
out.push('\n');
for row in &self.rows {
out.push_str(&row.iter().map(|c| field(c)).collect::<Vec<_>>().join(","));
out.push('\n');
}
out
}
}
#[derive(Debug, Clone)]
pub struct BadgeData {
pub label: String,
pub value: String,
}