use chrono::{DateTime, Local, SecondsFormat, Utc};
use comfy_table::{
modifiers::UTF8_ROUND_CORNERS,
presets::{NOTHING, UTF8_BORDERS_ONLY, UTF8_FULL},
Attribute, Cell, CellAlignment, Color, ContentArrangement, Table,
};
use crossterm::style::Stylize;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display, str::FromStr};
use uuid::Uuid;
use crate::deployment::{DeploymentStateBeta, State};
pub const GIT_STRINGS_MAX_LENGTH: usize = 80;
pub const CREATE_SERVICE_BODY_LIMIT: usize = 50_000_000;
const GIT_OPTION_NONE_TEXT: &str = "N/A";
#[derive(Deserialize, Serialize, Debug)]
pub struct Response {
pub id: Uuid,
pub service_id: String,
pub state: State,
pub last_update: DateTime<Utc>,
pub git_commit_id: Option<String>,
pub git_commit_msg: Option<String>,
pub git_branch: Option<String>,
pub git_dirty: Option<bool>,
}
#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentListResponseBeta {
pub deployments: Vec<DeploymentResponseBeta>,
}
#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentResponseBeta {
pub id: String,
pub state: DeploymentStateBeta,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub uris: Vec<String>,
pub build_id: Option<String>,
pub build_meta: Option<BuildMetaBeta>,
}
impl Display for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} deployment '{}' is {}",
self.last_update
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string()
.dim(),
self.id,
self.state
.to_string()
.with(crossterm::style::Color::from_str(self.state.get_color()).unwrap())
)
}
}
impl DeploymentResponseBeta {
pub fn to_string_summary_colored(&self) -> String {
format!(
"Deployment {} - {}",
self.id.as_str().bold(),
self.state.to_string_colored(),
)
}
pub fn to_string_colored(&self) -> String {
format!(
"Deployment {} - {}\n{}",
self.id.as_str().bold(),
self.state.to_string_colored(),
self.uris.join("\n"),
)
}
}
impl State {
pub fn get_color(&self) -> &str {
match self {
State::Queued | State::Building | State::Built | State::Loading => "cyan",
State::Running => "green",
State::Completed | State::Stopped => "blue",
State::Crashed => "red",
State::Unknown => "yellow",
}
}
}
pub fn deployments_table_beta(deployments: &[DeploymentResponseBeta], raw: bool) -> String {
let mut table = Table::new();
table
.load_preset(if raw { NOTHING } else { UTF8_BORDERS_ONLY })
.set_content_arrangement(ContentArrangement::Disabled)
.set_header(vec!["Deployment ID", "Status", "Date", "Git revision"]);
for deploy in deployments.iter() {
let datetime: DateTime<Local> = DateTime::from(deploy.created_at);
table.add_row(vec![
Cell::new(&deploy.id).add_attribute(Attribute::Bold),
Cell::new(&deploy.state)
.fg(Color::from_str(deploy.state.get_color()).unwrap()),
Cell::new(datetime.to_rfc3339_opts(SecondsFormat::Secs, false)),
Cell::new(
deploy
.build_meta
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
),
]);
}
table.to_string()
}
pub fn get_deployments_table(
deployments: &[Response],
service_name: &str,
page: u32,
raw: bool,
page_hint: bool,
) -> String {
if deployments.is_empty() {
let mut s = if page <= 1 {
"No deployments are linked to this service\n".to_string()
} else {
"No more deployments are linked to this service\n".to_string()
};
if !raw {
s = s.yellow().bold().to_string();
}
s
} else {
let mut table = Table::new();
if raw {
table
.load_preset(NOTHING)
.set_content_arrangement(ContentArrangement::Disabled)
.set_header(vec![
Cell::new("Deployment ID").set_alignment(CellAlignment::Left),
Cell::new("Status").set_alignment(CellAlignment::Left),
Cell::new("Last updated").set_alignment(CellAlignment::Left),
Cell::new("Commit ID").set_alignment(CellAlignment::Left),
Cell::new("Commit Message").set_alignment(CellAlignment::Left),
Cell::new("Branch").set_alignment(CellAlignment::Left),
Cell::new("Dirty").set_alignment(CellAlignment::Left),
]);
} else {
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(vec![
Cell::new("Deployment ID")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
Cell::new("Status")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
Cell::new("Last updated")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
Cell::new("Commit ID")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
Cell::new("Commit Message")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
Cell::new("Branch")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
Cell::new("Dirty")
.set_alignment(CellAlignment::Center)
.add_attribute(Attribute::Bold),
]);
}
for deploy in deployments.iter() {
let truncated_commit_id = deploy
.git_commit_id
.as_ref()
.map_or(String::from(GIT_OPTION_NONE_TEXT), |val| {
val.chars().take(7).collect()
});
let truncated_commit_msg = deploy
.git_commit_msg
.as_ref()
.map_or(String::from(GIT_OPTION_NONE_TEXT), |val| {
val.chars().take(24).collect::<String>()
});
if raw {
table.add_row(vec![
Cell::new(deploy.id),
Cell::new(&deploy.state),
Cell::new(deploy.last_update.format("%Y-%m-%dT%H:%M:%SZ")),
Cell::new(truncated_commit_id),
Cell::new(truncated_commit_msg),
Cell::new(
deploy
.git_branch
.as_ref()
.map_or(GIT_OPTION_NONE_TEXT, |val| val as &str),
),
Cell::new(
deploy
.git_dirty
.map_or(String::from(GIT_OPTION_NONE_TEXT), |val| val.to_string()),
),
]);
} else {
table.add_row(vec![
Cell::new(deploy.id),
Cell::new(&deploy.state)
.fg(Color::from_str(deploy.state.get_color()).unwrap())
.set_alignment(CellAlignment::Center),
Cell::new(deploy.last_update.format("%Y-%m-%dT%H:%M:%SZ"))
.set_alignment(CellAlignment::Center),
Cell::new(truncated_commit_id),
Cell::new(truncated_commit_msg),
Cell::new(
deploy
.git_branch
.as_ref()
.map_or(GIT_OPTION_NONE_TEXT, |val| val as &str),
),
Cell::new(
deploy
.git_dirty
.map_or(String::from(GIT_OPTION_NONE_TEXT), |val| val.to_string()),
)
.set_alignment(CellAlignment::Center),
]);
}
}
let formatted_table = format!("\nMost recent deployments for {service_name}\n{table}\n");
if page_hint {
format!(
"{formatted_table}More deployments are available on the next page using `--page {}`\n",
page + 1
)
} else {
formatted_table
}
}
}
#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct UploadArchiveResponseBeta {
pub archive_version_id: String,
}
#[derive(Default, Deserialize, Serialize)]
pub struct DeploymentRequest {
pub data: Vec<u8>,
pub no_test: bool,
pub git_commit_id: Option<String>,
pub git_commit_msg: Option<String>,
pub git_branch: Option<String>,
pub git_dirty: Option<bool>,
}
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", content = "content")]
#[typeshare::typeshare]
pub enum DeploymentRequestBeta {
BuildArchive(DeploymentRequestBuildArchiveBeta),
Image(DeploymentRequestImageBeta),
}
#[derive(Default, Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentRequestBuildArchiveBeta {
pub archive_version_id: String,
pub build_args: Option<BuildArgsBeta>,
pub secrets: Option<HashMap<String, String>>,
pub build_meta: Option<BuildMetaBeta>,
}
#[derive(Deserialize, Serialize, Default)]
#[serde(tag = "type", content = "content")]
#[typeshare::typeshare]
pub enum BuildArgsBeta {
Rust(BuildArgsRustBeta),
#[default]
Unknown,
}
#[derive(Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct BuildArgsRustBeta {
pub shuttle_runtime_version: Option<String>,
pub cargo_chef: bool,
pub cargo_build: bool,
pub package_name: Option<String>,
pub binary_name: Option<String>,
pub features: Option<String>,
pub no_default_features: bool,
pub mold: bool,
}
impl Default for BuildArgsRustBeta {
fn default() -> Self {
Self {
shuttle_runtime_version: Default::default(),
cargo_chef: true,
cargo_build: true,
package_name: Default::default(),
binary_name: Default::default(),
features: Default::default(),
no_default_features: Default::default(),
mold: Default::default(),
}
}
}
#[derive(Default, Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct BuildMetaBeta {
pub git_commit_id: Option<String>,
pub git_commit_msg: Option<String>,
pub git_branch: Option<String>,
pub git_dirty: Option<bool>,
}
impl std::fmt::Display for BuildMetaBeta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(true) = self.git_dirty {
write!(f, "(dirty) ")?;
}
if let Some(ref c) = self.git_commit_id {
write!(f, "[{}] ", c.chars().take(7).collect::<String>())?;
}
if let Some(ref m) = self.git_commit_msg {
write!(f, "{m}")?;
}
Ok(())
}
}
#[derive(Default, Deserialize, Serialize)]
#[typeshare::typeshare]
pub struct DeploymentRequestImageBeta {
pub image: String,
pub secrets: Option<HashMap<String, String>>,
}