Skip to content

Commit

Permalink
feat: create subcommand to list all projects of calling account (#553)
Browse files Browse the repository at this point in the history
* feat: create subcommand to list all projects of calling account

* fix: filter query by account name

* tests: iter_user_projects_detailed

Co-authored-by: chesedo <[email protected]>
  • Loading branch information
emmakuen and chesedo authored Dec 30, 2022
1 parent 45eadce commit cb342fd
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 1 deletion.
2 changes: 2 additions & 0 deletions cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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
Expand Down
6 changes: 6 additions & 0 deletions cargo-shuttle/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ impl Client {
self.get(path).await
}

pub async fn get_projects_list(&self) -> Result<Vec<project::Response>> {
let path = "/projects".to_string();

self.get(path).await
}

pub async fn delete_project(&self, project: &ProjectName) -> Result<project::Response> {
let path = format!("/projects/{}", project.as_str());

Expand Down
10 changes: 10 additions & 0 deletions cargo-shuttle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,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")
Expand Down Expand Up @@ -508,6 +509,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 => {
Expand Down
41 changes: 40 additions & 1 deletion common/src/models/project.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -52,3 +55,39 @@ pub struct AdminResponse {
pub project_name: String,
pub account_name: String,
}

pub fn get_table(projects: &Vec<Response>) -> 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,
)
}
}
18 changes: 18 additions & 0 deletions gateway/src/api/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,23 @@ async fn get_project(
Ok(AxumJson(response))
}

async fn get_projects_list(
State(RouterState { service, .. }): State<RouterState>,
User { name, .. }: User,
) -> Result<AxumJson<Vec<project::Response>>, Error> {
let projects = service
.iter_user_projects_detailed(name.clone())
.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 {
Expand Down Expand Up @@ -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),
Expand Down
27 changes: 27 additions & 0 deletions gateway/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,25 @@ impl GatewayService {
.ok_or_else(|| Error::from_kind(ErrorKind::ProjectNotFound))
}

pub async fn iter_user_projects_detailed(
&self,
account_name: AccountName,
) -> Result<impl Iterator<Item = (ProjectName, Project)>, Error> {
let iter =
query("SELECT project_name, project_state FROM projects WHERE account_name = ?1")
.bind(account_name)
.fetch_all(&self.db)
.await?
.into_iter()
.map(|row| {
(
row.get("project_name"),
row.get::<SqlxJson<Project>, _>("project_state").0,
)
});
Ok(iter)
}

pub async fn update_project(
&self,
project_name: &ProjectName,
Expand Down Expand Up @@ -687,6 +706,14 @@ pub mod tests {
account_name: neo.clone(),
}
);
assert_eq!(
svc.iter_user_projects_detailed(neo.clone())
.await
.unwrap()
.map(|item| item.0)
.collect::<Vec<_>>(),
vec![matrix.clone()]
);

let mut work = svc
.new_task()
Expand Down

0 comments on commit cb342fd

Please sign in to comment.