From a2892a9265462a15bb822b9a35cff92d2fe180f4 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 27 Dec 2022 18:58:42 +0900 Subject: [PATCH] feat: create subcommand to list all projects of calling account --- cargo-shuttle/src/args.rs | 2 ++ cargo-shuttle/src/client.rs | 6 ++++++ cargo-shuttle/src/lib.rs | 10 +++++++++ common/src/models/project.rs | 41 +++++++++++++++++++++++++++++++++++- gateway/src/api/latest.rs | 18 ++++++++++++++++ gateway/src/service.rs | 16 ++++++++++++++ 6 files changed, 92 insertions(+), 1 deletion(-) diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs index 9976d3f6da..0ee9f896d7 100644 --- a/cargo-shuttle/src/args.rs +++ b/cargo-shuttle/src/args.rs @@ -111,6 +111,8 @@ pub enum DeploymentCommand { pub enum ProjectCommand { /// create an environment for this project on shuttle New, + /// list all projects belonging to the calling account + List, /// remove this project environment from shuttle Rm, /// show the status of this project's environment on shuttle diff --git a/cargo-shuttle/src/client.rs b/cargo-shuttle/src/client.rs index 4f3c4c7780..514c7c15a9 100644 --- a/cargo-shuttle/src/client.rs +++ b/cargo-shuttle/src/client.rs @@ -122,6 +122,12 @@ impl Client { self.get(path).await } + pub async fn get_projects_list(&self) -> Result> { + let path = ("/projects").to_string(); + + self.get(path).await + } + pub async fn delete_project(&self, project: &ProjectName) -> Result { let path = format!("/projects/{}", project.as_str()); diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index f6e7333e9e..affec7a2a7 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -99,6 +99,7 @@ impl Shuttle { Command::Project(ProjectCommand::Status { follow }) => { self.project_status(&client, follow).await } + Command::Project(ProjectCommand::List) => self.projects_list(&client).await, Command::Project(ProjectCommand::Rm) => self.project_delete(&client).await, _ => { unreachable!("commands that don't need a client have already been matched") @@ -521,6 +522,15 @@ impl Shuttle { Ok(()) } + async fn projects_list(&self, client: &Client) -> Result<()> { + let projects = client.get_projects_list().await?; + let projects_table = project::get_table(&projects); + + println!("{projects_table}"); + + Ok(()) + } + async fn project_status(&self, client: &Client, follow: bool) -> Result<()> { match follow { true => { diff --git a/common/src/models/project.rs b/common/src/models/project.rs index 171c4e46aa..015bc23028 100644 --- a/common/src/models/project.rs +++ b/common/src/models/project.rs @@ -1,4 +1,7 @@ -use comfy_table::Color; +use comfy_table::{ + modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Cell, CellAlignment, Color, + ContentArrangement, Table, +}; use crossterm::style::Stylize; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; @@ -52,3 +55,39 @@ pub struct AdminResponse { pub project_name: String, pub account_name: String, } + +pub fn get_table(projects: &Vec) -> String { + if projects.is_empty() { + format!( + "{}\n", + "No projects are linked to this account".yellow().bold() + ) + } else { + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .set_header(vec![ + Cell::new("Project Name").set_alignment(CellAlignment::Center), + Cell::new("Status").set_alignment(CellAlignment::Center), + ]); + + for project in projects.iter() { + table.add_row(vec![ + Cell::new(&project.name), + Cell::new(&project.state) + .fg(project.state.get_color()) + .set_alignment(CellAlignment::Center), + ]); + } + + format!( + r#" +These projects are linked to this account +{} +"#, + table, + ) + } +} diff --git a/gateway/src/api/latest.rs b/gateway/src/api/latest.rs index 9b7093f1ae..accc193309 100644 --- a/gateway/src/api/latest.rs +++ b/gateway/src/api/latest.rs @@ -104,6 +104,23 @@ async fn get_project( Ok(AxumJson(response)) } +async fn get_projects_list( + State(RouterState { service, .. }): State, + _: User, +) -> Result>, Error> { + let projects = service + .iter_projects_list() + .await? + .into_iter() + .map(|project| project::Response { + name: project.0.to_string(), + state: project.1.into(), + }) + .collect(); + + Ok(AxumJson(projects)) +} + #[instrument(skip_all, fields(%project))] async fn post_project( State(RouterState { @@ -457,6 +474,7 @@ impl ApiBuilder { self.router = self .router .route("/", get(get_status)) + .route("/projects", get(get_projects_list)) .route( "/projects/:project_name", get(get_project).delete(delete_project).post(post_project), diff --git a/gateway/src/service.rs b/gateway/src/service.rs index 20ca92a05b..5b5960f4a1 100644 --- a/gateway/src/service.rs +++ b/gateway/src/service.rs @@ -276,6 +276,22 @@ impl GatewayService { .ok_or_else(|| Error::from_kind(ErrorKind::ProjectNotFound)) } + pub async fn iter_projects_list( + &self, + ) -> Result, Error> { + let iter = query("SELECT project_name, project_state FROM projects") + .fetch_all(&self.db) + .await? + .into_iter() + .map(|row| { + ( + row.get("project_name"), + row.get::, _>("project_state").0, + ) + }); + Ok(iter) + } + pub async fn update_project( &self, project_name: &ProjectName,