Skip to content

Commit

Permalink
feat: Allow filtering projects by project status (#613)
Browse files Browse the repository at this point in the history
* feat: add filtering for project list

* Adds a flag for dynamically filtering projects by deployment status

* fix: incorrect logic

* refactor: filter rows at db query

* feat: allow rejection of non-valid project status

* refactor: use more relevant variable name

* tests: add additional assertions

* fix: add proper assertions for this feature

* fix: satisfy CI
  • Loading branch information
joshua-mo-143 authored Feb 21, 2023
1 parent 28a8abe commit 94a0708
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 6 deletions.
6 changes: 5 additions & 1 deletion cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ pub enum ProjectCommand {
/// create an environment for this project on shuttle
New,
/// list all projects belonging to the calling account
List,
List {
#[arg(long)]
/// Return projects filtered by a given project status
filter: Option<String>,
},
/// remove this project environment from shuttle
Rm,
/// show the status of this project's environment on shuttle
Expand Down
9 changes: 9 additions & 0 deletions cargo-shuttle/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ impl Client {
self.get(path).await
}

pub async fn get_projects_list_filtered(
&self,
filter: String,
) -> Result<Vec<project::Response>> {
let path = format!("/projects/{filter}");

self.get(path).await
}

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

Expand Down
23 changes: 20 additions & 3 deletions cargo-shuttle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ mod factory;
mod init;

use indicatif::ProgressBar;
use shuttle_common::models::project::State;
use shuttle_common::project::ProjectName;

use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs::{read_to_string, File};
use std::io::stdout;
use std::net::{Ipv4Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::str::FromStr;

use anyhow::{anyhow, bail, Context, Result};
pub use args::{Args, Command, DeployArgs, InitArgs, LoginArgs, ProjectArgs, RunArgs};
Expand Down Expand Up @@ -100,7 +103,9 @@ 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::List { filter }) => {
self.projects_list(&client, filter).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 @@ -560,8 +565,20 @@ impl Shuttle {
Ok(())
}

async fn projects_list(&self, client: &Client) -> Result<()> {
let projects = client.get_projects_list().await?;
async fn projects_list(&self, client: &Client, filter: Option<String>) -> Result<()> {
let projects = match filter {
Some(filter) => {
if let Ok(filter) = State::from_str(filter.trim()) {
client
.get_projects_list_filtered(filter.to_string())
.await?
} else {
return Err(anyhow!("That's not a valid project status!"));
}
}
None => client.get_projects_list().await?,
};

let projects_table = project::get_table(&projects);

println!("{projects_table}");
Expand Down
5 changes: 3 additions & 2 deletions common/src/models/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ use comfy_table::{
use crossterm::style::Stylize;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use strum::EnumString;

#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, Clone)]
pub struct Response {
pub name: String,
pub state: State,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, EnumString)]
#[serde(rename_all = "lowercase")]
pub enum State {
Creating { recreate_count: usize },
Expand Down
19 changes: 19 additions & 0 deletions gateway/src/api/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,24 @@ async fn get_projects_list(
Ok(AxumJson(projects))
}

async fn get_projects_list_with_filter(
State(RouterState { service, .. }): State<RouterState>,
User { name, .. }: User,
Path(project_status): Path<String>,
) -> Result<AxumJson<Vec<project::Response>>, Error> {
let projects = service
.iter_user_projects_detailed_filtered(name.clone(), project_status)
.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 @@ -455,6 +473,7 @@ impl ApiBuilder {
.router
.route("/", get(get_status))
.route("/projects", get(get_projects_list))
.route("/projects/:state", get(get_projects_list_with_filter))
.route(
"/projects/:project_name",
get(get_project).delete(delete_project).post(post_project),
Expand Down
38 changes: 38 additions & 0 deletions gateway/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,27 @@ impl GatewayService {
Ok(iter)
}

pub async fn iter_user_projects_detailed_filtered(
&self,
account_name: AccountName,
filter: String,
) -> Result<impl Iterator<Item = (ProjectName, Project)>, Error> {
let iter =
query("SELECT project_name, project_state FROM projects WHERE account_name = ?1 AND project_state = ?2")
.bind(account_name)
.bind(filter)
.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 @@ -690,6 +711,23 @@ pub mod tests {
vec![matrix.clone()]
);

assert_eq!(
svc.iter_user_projects_detailed_filtered(neo.clone(), "ready".to_string())
.await
.unwrap()
.next()
.expect("to get one project with its user and a valid Ready status"),
(matrix.clone(), project)
);

assert_eq!(
svc.iter_user_projects_detailed_filtered(neo.clone(), "destroyed".to_string())
.await
.unwrap()
.next(),
None
);

let mut work = svc
.new_task()
.project(matrix.clone())
Expand Down

0 comments on commit 94a0708

Please sign in to comment.