fundaia 0.7.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use anyhow::Result;
use owo_colors::OwoColorize;

use crate::api::client::ApiClient;
use crate::render::table::{Cell, Table};
use crate::render::theme;

/// Spanish for the two roles, so the output reads like the web interface does.
fn role_label(role: &str) -> &str {
    match role {
        "admin" => "administrador",
        _ => "miembro",
    }
}

fn status_label(status: &str) -> &str {
    match status {
        "pending" => "pendiente",
        "accepted" => "aceptada",
        _ => "caducada",
    }
}

pub async fn list(client: &ApiClient) -> Result<()> {
    let workspaces = client.workspaces().await?;
    let mut table = Table::new(&["id", "espacio", "slug", "tu papel"]);

    for workspace in &workspaces {
        table.push(vec![
            Cell::styled(workspace.id.clone(), theme::muted()),
            Cell::styled(workspace.name.clone(), theme::strong()),
            Cell::plain(workspace.slug.clone()),
            Cell::plain(
                workspace
                    .role
                    .as_deref()
                    .map(role_label)
                    // The instance owner reaches every workspace and is in none.
                    .unwrap_or("")
                    .to_string(),
            ),
        ]);
    }

    if table.is_empty() {
        println!("Todavía no hay ningún espacio.");
        return Ok(());
    }

    table.print();
    Ok(())
}

/// Who shares a workspace, and who has been asked to.
///
/// Both lists in one command because they answer the same question — *who can
/// see my projects* — and a pending invitation is an answer to it too.
pub async fn members(client: &ApiClient, reference: Option<&str>) -> Result<()> {
    let workspace = client.find_workspace(reference).await?;

    println!();
    println!("  {}", workspace.name.style(theme::strong()));
    println!();

    let members = client.members(&workspace.id).await?;
    let mut table = Table::new(&["usuario", "github", "correo", "papel"]);

    for member in &members {
        table.push(vec![
            Cell::styled(
                member
                    .display_name
                    .clone()
                    .unwrap_or_else(|| member.user_id.clone()),
                theme::strong(),
            ),
            Cell::plain(
                member
                    .github_login
                    .as_ref()
                    .map(|login| format!("@{login}"))
                    .unwrap_or_else(|| "".to_string()),
            ),
            Cell::plain(member.email.clone().unwrap_or_else(|| "".to_string())),
            Cell::plain(role_label(&member.role).to_string()),
        ]);
    }

    table.print();

    let pending: Vec<_> = client
        .invitations(&workspace.id)
        .await?
        .into_iter()
        .filter(|invitation| invitation.status == "pending")
        .collect();

    if pending.is_empty() {
        return Ok(());
    }

    println!();
    println!("  {}", "Invitaciones pendientes".style(theme::muted()));
    println!();

    let mut invitations = Table::new(&["id", "correo", "papel", "estado"]);
    for invitation in &pending {
        invitations.push(vec![
            Cell::styled(invitation.id.clone(), theme::muted()),
            Cell::styled(invitation.email.clone(), theme::strong()),
            Cell::plain(role_label(&invitation.role).to_string()),
            Cell::plain(status_label(&invitation.status).to_string()),
        ]);
    }
    invitations.print();

    Ok(())
}

/// Sends an invitation and prints the link it sent.
///
/// The link is printed on purpose: whoever runs this is often on a machine with
/// no mail client, and delivery is the part most likely to be slow — or off
/// entirely on an instance with no provider configured. Copying it into a chat
/// window is then the fastest path, and it is the same credential either way.
pub async fn invite(
    client: &ApiClient,
    reference: Option<&str>,
    email: &str,
    admin: bool,
) -> Result<()> {
    let workspace = client.find_workspace(reference).await?;
    let role = if admin { "admin" } else { "member" };

    let sent = client.invite(&workspace.id, email, role).await?;

    println!();
    println!(
        "  {} Invitación enviada a {} como {}",
        theme::CHECK.style(theme::success()),
        email.style(theme::strong()),
        role_label(role),
    );
    println!("    {}", sent.accept_url.style(theme::muted()));
    println!();

    Ok(())
}

pub async fn revoke(
    client: &ApiClient,
    reference: Option<&str>,
    invitation_id: &str,
) -> Result<()> {
    let workspace = client.find_workspace(reference).await?;
    client
        .revoke_invitation(&workspace.id, invitation_id)
        .await?;

    println!(
        "  {} Invitación retirada",
        theme::CHECK.style(theme::success())
    );
    Ok(())
}

pub async fn remove(client: &ApiClient, reference: Option<&str>, user_id: &str) -> Result<()> {
    let workspace = client.find_workspace(reference).await?;
    client.remove_member(&workspace.id, user_id).await?;

    println!(
        "  {} {user_id} ya no está en {}",
        theme::CHECK.style(theme::success()),
        workspace.name.style(theme::strong()),
    );
    Ok(())
}