use anyhow::Result;
use owo_colors::OwoColorize;
use crate::api::client::ApiClient;
use crate::api::models::{FeedDeployment, StatusPageAnswer, StatusPageChange};
use crate::output::Format;
use crate::render::pipeline;
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;
use crate::render::time;
pub fn feed_scope(active: bool, failed: bool) -> &'static str {
match (active, failed) {
(true, _) => "active",
(_, true) => "failed",
_ => "all",
}
}
fn duration_word(seconds: Option<u64>) -> String {
seconds.map_or_else(
|| "—".to_owned(),
|seconds| time::format_millis((seconds * 1_000) as i64),
)
}
fn where_of(deployment: &FeedDeployment) -> String {
if deployment.environment.is_default {
return format!("{}/{}", deployment.project.slug, deployment.service.name);
}
format!(
"{}/{} · {}",
deployment.project.slug, deployment.service.name, deployment.environment.slug
)
}
pub async fn deployments(
client: &ApiClient,
active: bool,
failed: bool,
limit: usize,
format: Format,
) -> Result<()> {
let scope = feed_scope(active, failed);
let answer = client.deployment_feed(scope, limit).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let mut table = Table::new(&[
"servicio",
"estado",
"etapas",
"motivo",
"autor",
"commit",
"duración",
"cuándo",
]);
for deployment in &answer.body.deployments {
table.push(vec![
Cell::styled(where_of(deployment), theme::strong()),
Cell::styled(
deployment.status.clone(),
theme::deployment_status_style(&deployment.status),
),
Cell::styled(
pipeline::track(&deployment.pipeline),
theme::deployment_status_style(&deployment.status),
),
Cell::styled(deployment.trigger.clone(), theme::muted()),
Cell::plain(
deployment
.author
.as_ref()
.map_or_else(|| "—".to_owned(), |author| author.display_name.clone()),
),
Cell::styled(
deployment
.commit_sha
.as_deref()
.map_or_else(|| "—".to_owned(), |sha| sha.chars().take(7).collect()),
theme::muted(),
),
Cell::plain(duration_word(deployment.duration_seconds)),
Cell::styled(time::when(&deployment.created_at), theme::muted()),
]);
}
let nothing = match scope {
"active" => "No hay ningún despliegue en curso.",
"failed" => "Ningún despliegue ha fallado en las últimas 24 horas.",
_ => "Nada en curso, y nada desplegado en las últimas 24 horas.",
};
table.print_or(nothing.style(theme::muted()));
Ok(())
}
pub struct StatusPageRequest<'a> {
pub project: &'a str,
pub switch: Option<bool>,
pub title: Option<String>,
pub services: Option<Vec<String>>,
pub rotate: bool,
}
impl StatusPageRequest<'_> {
fn change(&self) -> Option<StatusPageChange> {
let change = StatusPageChange {
enabled: self.switch,
title: self.title.clone(),
services: self.services.clone(),
rotate_token: self.rotate.then_some(true),
};
let asks = change.enabled.is_some()
|| change.title.is_some()
|| change.services.is_some()
|| change.rotate_token.is_some();
asks.then_some(change)
}
}
pub fn services_of(list: &str) -> Vec<String> {
list.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_owned)
.collect()
}
pub async fn status_page(
client: &ApiClient,
request: StatusPageRequest<'_>,
format: Format,
) -> Result<()> {
let project = client.find_project(request.project).await?;
let answer = match request.change() {
Some(change) => client.set_status_page(&project.id, &change).await?,
None => client.status_page(&project.id).await?,
};
if format.answered_in_json(&answer.raw) {
return Ok(());
}
print_status_page(&answer.body, &project.slug);
Ok(())
}
fn health_word(status: &str) -> &str {
match status {
"operational" => "operativo",
"degraded" => "degradado",
"down" => "caído",
other => other,
}
}
fn health_style(status: &str) -> owo_colors::Style {
match status {
"operational" => theme::success(),
"degraded" => theme::warning(),
_ => theme::danger(),
}
}
fn uptime_word(percent: Option<f64>) -> String {
percent.map_or_else(|| "sin datos".to_owned(), |value| format!("{value:.2} %"))
}
fn print_status_page(answer: &StatusPageAnswer, slug: &str) {
let page = &answer.status_page;
if !page.enabled {
println!(
" {}",
format!("{slug} no publica página de estado. fundaia status-page {slug} on")
.style(theme::muted())
);
return;
}
println!(
" {} {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&page.shown_title).style(theme::strong())
);
if let Some(url) = &page.url {
println!(" {}", sanitise::plain(url).style(theme::accent()));
}
let Some(view) = &answer.view else {
return;
};
println!(
" {} · {} en 30 días",
health_word(&view.status).style(health_style(&view.status)),
uptime_word(view.uptime_percent)
);
println!();
let mut table = Table::new(&["servicio", "estado", "disponibilidad"]);
for service in &view.services {
table.push(vec![
Cell::styled(service.name.clone(), theme::strong()),
Cell::styled(
health_word(&service.status).to_owned(),
health_style(&service.status),
),
Cell::plain(uptime_word(service.uptime_percent)),
]);
}
table.print_or("");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_ask_for_everything_without_a_flag() {
assert_eq!(feed_scope(false, false), "all");
}
#[test]
fn it_should_ask_for_failures_with_failed() {
assert_eq!(feed_scope(false, true), "failed");
}
#[test]
fn it_should_split_a_list_of_services_on_commas() {
assert_eq!(services_of(" web, api ,,"), vec!["web", "api"]);
}
#[test]
fn it_should_only_read_when_nothing_changes() {
let request = StatusPageRequest {
project: "tienda",
switch: None,
title: None,
services: None,
rotate: false,
};
assert!(request.change().is_none());
}
#[test]
fn it_should_say_a_down_service_is_down() {
assert_eq!(health_word("down"), "caído");
}
}