Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pagination to app list queries #4454

Merged
merged 2 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 76 additions & 59 deletions lib/backend-api/src/query.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use std::{collections::HashSet, time::Duration};
use std::{collections::HashSet, pin::Pin, time::Duration};

use anyhow::{bail, Context};
use cynic::{MutationBuilder, QueryBuilder};
use edge_schema::schema::{NetworkTokenV1, WebcIdent};
use futures::StreamExt;
use futures::{Stream, StreamExt};
use time::OffsetDateTime;
use tracing::Instrument;
use url::Url;

use crate::{
types::{
self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion,
DeployAppVersionConnection, GetDeployAppAndVersion, GetDeployAppVersionsVars,
GetNamespaceAppsVars, Log, PackageVersionConnection, PublishDeployAppVars,
DeployAppVersionConnection, GetCurrentUserWithAppsVars, GetDeployAppAndVersion,
GetDeployAppVersionsVars, GetNamespaceAppsVars, Log, PackageVersionConnection,
PublishDeployAppVars,
},
GraphQLApiFailure, WasmerClient,
};
Expand Down Expand Up @@ -380,38 +381,45 @@ pub async fn get_app_version_by_id_with_app(
/// List all apps that are accessible by the current user.
///
/// NOTE: this will only include the first pages and does not provide pagination.
pub async fn user_apps(client: &WasmerClient) -> Result<Vec<types::DeployApp>, anyhow::Error> {
let user = client
.run_graphql(types::GetCurrentUserWithApps::build(()))
.await?
.viewer
.context("not logged in")?;

let apps = user
.apps
.edges
.into_iter()
.flatten()
.filter_map(|x| x.node)
.collect();

Ok(apps)
pub async fn user_apps(
client: &WasmerClient,
) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
futures::stream::try_unfold(None, move |cursor| async move {
let user = client
.run_graphql(types::GetCurrentUserWithApps::build(
GetCurrentUserWithAppsVars { after: cursor },
))
.await?
.viewer
.context("not logged in")?;

let apps: Vec<_> = user
.apps
.edges
.into_iter()
.flatten()
.filter_map(|x| x.node)
.collect();

let cursor = user.apps.page_info.end_cursor;

if apps.is_empty() {
Ok(None)
} else {
Ok(Some((apps, cursor)))
}
})
}

/// List all apps that are accessible by the current user.
///
/// NOTE: this does not currently do full pagination properly.
// TODO(theduke): fix pagination
pub async fn user_accessible_apps(
client: &WasmerClient,
) -> Result<Vec<types::DeployApp>, anyhow::Error> {
let mut apps = Vec::new();

// Get user apps.

let user_apps = user_apps(client).await?;

apps.extend(user_apps);
) -> Result<
impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_,
anyhow::Error,
> {
let apps: Pin<Box<dyn Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + Send + Sync>> =
Box::pin(user_apps(client).await);

// Get all aps in user-accessible namespaces.
let namespace_res = client
Expand All @@ -429,18 +437,16 @@ pub async fn user_accessible_apps(
.map(|node| node.name.clone())
.collect::<Vec<_>>();

for namespace in namespace_names {
let out = client
.run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
name: namespace.to_string(),
}))
.await?;
let mut all_apps = vec![apps];
for ns in namespace_names {
let apps: Pin<Box<dyn Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + Send + Sync>> =
Box::pin(namespace_apps(client, ns).await);

if let Some(ns) = out.get_namespace {
let ns_apps = ns.apps.edges.into_iter().flatten().filter_map(|x| x.node);
apps.extend(ns_apps);
}
all_apps.push(apps);
}

let apps = futures::stream::select_all(all_apps);

Ok(apps)
}

Expand All @@ -449,27 +455,38 @@ pub async fn user_accessible_apps(
/// NOTE: only retrieves the first page and does not do pagination.
pub async fn namespace_apps(
client: &WasmerClient,
namespace: &str,
) -> Result<Vec<types::DeployApp>, anyhow::Error> {
let res = client
.run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
name: namespace.to_string(),
}))
.await?;
namespace: String,
) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
let namespace = namespace.clone();

let ns = res
.get_namespace
.with_context(|| format!("failed to get namespace '{}'", namespace))?;
futures::stream::try_unfold((None, namespace), move |(cursor, namespace)| async move {
let res = client
.run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
name: namespace.to_string(),
after: cursor,
}))
.await?;

let apps = ns
.apps
.edges
.into_iter()
.flatten()
.filter_map(|x| x.node)
.collect();
let ns = res
.get_namespace
.with_context(|| format!("failed to get namespace '{}'", namespace))?;

Ok(apps)
let apps: Vec<_> = ns
.apps
.edges
.into_iter()
.flatten()
.filter_map(|x| x.node)
.collect();

let cursor = ns.apps.page_info.end_cursor;

if apps.is_empty() {
Ok(None)
} else {
Ok(Some((apps, (cursor, namespace))))
}
})
}

/// Publish a new app (version).
Expand Down
12 changes: 11 additions & 1 deletion lib/backend-api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,24 @@ mod queries {
pub get_deploy_app: Option<DeployApp>,
}

#[derive(cynic::QueryVariables, Debug)]
pub struct GetCurrentUserWithAppsVars {
pub after: Option<String>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query")]
#[cynic(graphql_type = "Query", variables = "GetCurrentUserWithAppsVars")]
pub struct GetCurrentUserWithApps {
pub viewer: Option<UserWithApps>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "User")]
#[cynic(variables = "GetCurrentUserWithAppsVars")]
pub struct UserWithApps {
pub id: cynic::Id,
pub username: String,
#[arguments(after: $after)]
pub apps: DeployAppConnection,
}

Expand Down Expand Up @@ -537,6 +544,7 @@ mod queries {
#[derive(cynic::QueryVariables, Debug)]
pub struct GetNamespaceAppsVars {
pub name: String,
pub after: Option<String>,
}

#[derive(cynic::QueryFragment, Debug)]
Expand All @@ -548,9 +556,11 @@ mod queries {

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Namespace")]
#[cynic(variables = "GetNamespaceAppsVars")]
pub struct NamespaceWithApps {
pub id: cynic::Id,
pub name: String,
#[arguments(after: $after)]
pub apps: DeployAppConnection,
}

Expand Down
67 changes: 62 additions & 5 deletions lib/cli/src/commands/app/list.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
//! List Edge apps.

use std::pin::Pin;

use futures::{Stream, StreamExt};
use wasmer_api::types::DeployApp;

use crate::{
commands::AsyncCliCommand,
opts::{ApiOpts, ListFormatOpts},
Expand All @@ -23,6 +28,14 @@ pub struct CmdAppList {
/// directly owned by the user and apps in namespaces the user can access.
#[clap(short = 'a', long)]
all: bool,

/// Maximum number of apps to display
#[clap(long, default_value = "1000")]
max: usize,

/// Asks whether to display the next page or not
#[clap(long, default_value = "false")]
paging_mode: bool,
}

#[async_trait::async_trait]
Expand All @@ -32,15 +45,59 @@ impl AsyncCliCommand for CmdAppList {
async fn run_async(self) -> Result<(), anyhow::Error> {
let client = self.api.client()?;

let apps = if let Some(ns) = self.namespace {
wasmer_api::query::namespace_apps(&client, &ns).await?
let apps_stream: Pin<
Box<dyn Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + Send + Sync>,
> = if let Some(ns) = self.namespace.clone() {
Box::pin(wasmer_api::query::namespace_apps(&client, ns).await)
} else if self.all {
wasmer_api::query::user_accessible_apps(&client).await?
Box::pin(wasmer_api::query::user_accessible_apps(&client).await?)
} else {
wasmer_api::query::user_apps(&client).await?
Box::pin(wasmer_api::query::user_apps(&client).await)
};

println!("{}", self.fmt.format.render(&apps));
let mut apps_stream = std::pin::pin!(apps_stream);

let mut rem = self.max;

let mut display_apps = vec![];

'list: while let Some(apps) = apps_stream.next().await {
let mut apps = apps?;

let limit = std::cmp::min(apps.len(), rem);

if limit == 0 {
break;
}

rem -= limit;

if self.paging_mode {
println!("{}", self.fmt.format.render(&apps));

loop {
println!("next page? [y, n]");

let mut rsp = String::new();
std::io::stdin().read_line(&mut rsp)?;

if rsp.trim() == "y" {
continue 'list;
}
if rsp.trim() == "n" {
break 'list;
}

println!("uknown response: {rsp}");
}
}

display_apps.extend(apps.drain(..limit));
}

if !display_apps.is_empty() {
println!("{}", self.fmt.format.render(&display_apps));
}

Ok(())
}
Expand Down
Loading