Recently updated crates endpoint & UI
Diff
chartered-db/src/crates.rs | 29 +++++++++++++++++++++++++----
chartered-web/src/main.rs | 4 ++++
chartered-frontend/src/pages/Dashboard.tsx | 37 +++++++++++++++++++++----------------
chartered-web/src/endpoints/web_api/ssh_key.rs | 6 ++++--
chartered-web/src/endpoints/web_api/crates/info.rs | 6 ++----
chartered-web/src/endpoints/web_api/crates/mod.rs | 2 ++
chartered-web/src/endpoints/web_api/crates/recently_updated.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 104 insertions(+), 34 deletions(-)
@@ -1,4 +1,4 @@
use crate::users::{UserCratePermission, User};
use crate::users::{User, UserCratePermission};
use super::{
schema::{crate_versions, crates, users},
@@ -21,14 +21,23 @@
}
impl Crate {
pub async fn all_with_versions(
pub async fn all_visible_with_versions(
conn: ConnectionPool,
given_user_id: i32,
) -> Result<HashMap<Crate, Vec<CrateVersion<'static>>>> {
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
let crate_versions = crates::table
.inner_join(crate::schema::user_crate_permissions::table)
.filter(
crate::schema::user_crate_permissions::permissions
.bitwise_and(crate::users::UserCratePermissionValue::VISIBLE.bits())
.ne(0),
)
.filter(crate::schema::user_crate_permissions::dsl::user_id.eq(given_user_id))
.inner_join(crate_versions::table)
.select((crates::all_columns, crate_versions::all_columns))
.load(&conn)?;
Ok(crate_versions.into_iter().into_grouping_map().collect())
@@ -36,14 +45,14 @@
.await?
}
pub async fn all_visible_with_versions(
pub async fn list_recently_updated(
conn: ConnectionPool,
given_user_id: i32,
) -> Result<HashMap<Crate, Vec<CrateVersion<'static>>>> {
) -> Result<Vec<(Crate, CrateVersion<'static>)>> {
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
let crate_versions = crates::table
let crates = crates::table
.inner_join(crate::schema::user_crate_permissions::table)
.filter(
crate::schema::user_crate_permissions::permissions
@@ -52,10 +61,12 @@
)
.filter(crate::schema::user_crate_permissions::dsl::user_id.eq(given_user_id))
.inner_join(crate_versions::table)
.order_by(crate::schema::crate_versions::dsl::id.desc())
.select((crates::all_columns, crate_versions::all_columns))
.limit(10)
.load(&conn)?;
Ok(crate_versions.into_iter().into_grouping_map().collect())
Ok(crates)
})
.await?
}
@@ -81,7 +92,9 @@
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
Ok(CrateVersion::belonging_to(&*self).inner_join(users::table).load::<(CrateVersion, User)>(&conn)?)
Ok(CrateVersion::belonging_to(&*self)
.inner_join(users::table)
.load::<(CrateVersion, User)>(&conn)?)
})
.await?
}
@@ -225,7 +238,7 @@
) -> Result<()> {
use crate::schema::crate_versions::dsl::{
checksum, crate_id, crate_versions, dependencies, features, filesystem_object, links,
version, size, user_id,
size, user_id, version,
};
use crate::schema::crates::dsl::{
crates, description, documentation, homepage, id, readme, repository,
@@ -87,6 +87,10 @@
"/crates/:crate/members",
delete(endpoints::web_api::crates::delete_member)
)
.route(
"/crates/recently-updated",
get(endpoints::web_api::crates::list_recently_updated)
)
.route("/users/search", get(endpoints::web_api::search_users))
.route("/ssh-key", get(endpoints::web_api::get_ssh_keys))
.route("/ssh-key", put(endpoints::web_api::add_ssh_key))
@@ -1,23 +1,26 @@
import React = require("react");
import { Link } from "react-router-dom";
import { useAuth } from "../useAuth";
import Nav from "../sections/Nav";
import { ChevronRight } from "react-bootstrap-icons";
import { useAuthenticatedRequest } from "../util";
interface RecentlyUpdatedResponse {
versions: RecentlyUpdatedResponseVersions,
}
interface RecentlyUpdatedResponseVersions {
[i: number]: { name: string, version: string, },
}
export default function Dashboard() {
const auth = useAuth();
const recentlyUpdated = [
{
name: "hello-world-rs",
version: "0.0.1",
},
{
name: "cool-beans-rs",
version: "0.0.1",
},
];
const { response: recentlyUpdated, error } = useAuthenticatedRequest<RecentlyUpdatedResponse>({
auth,
endpoint: "crates/recently-updated",
});
return (
<div className="text-white">
@@ -41,25 +44,19 @@
<hr />
<div className="row">
<div className="col-md-4">
<div className="col-12 col-md-4">
<h4>Your Crates</h4>
{recentlyUpdated.map((v) => (
<CrateCard key={v.name} crate={v} />
))}
</div>
<div className="col-md-4">
<div className="col-12 col-md-4">
<h4>Recently Updated</h4>
{recentlyUpdated.map((v) => (
{(recentlyUpdated?.versions || []).map((v) => (
<CrateCard key={v.name} crate={v} />
))}
</div>
<div className="col-md-4">
<div className="col-12 col-md-4">
<h4>Most Downloaded</h4>
{recentlyUpdated.map((v) => (
<CrateCard key={v.name} crate={v} />
))}
</div>
</div>
</div>
@@ -1,8 +1,8 @@
use chartered_db::{users::User, ConnectionPool};
use axum::{extract, Json};
use chartered_db::uuid::Uuid;
use chrono::{DateTime, Utc, TimeZone};
use chrono::{DateTime, TimeZone, Utc};
use log::warn;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
@@ -40,7 +40,9 @@
}),
name: key.name,
created_at: Utc.from_local_datetime(&key.created_at).unwrap(),
last_used_at: key.last_used_at.and_then(|v| Utc.from_local_datetime(&v).single()),
last_used_at: key
.last_used_at
.and_then(|v| Utc.from_local_datetime(&v).single()),
})
.collect();
@@ -6,8 +6,8 @@
users::{User, UserCratePermissionValue as Permission},
ConnectionPool,
};
use chrono::TimeZone;
use chartered_types::cargo::CrateVersion;
use chrono::TimeZone;
use serde::Serialize;
use std::sync::Arc;
use thiserror::Error;
@@ -16,8 +16,6 @@
pub enum Error {
#[error("Failed to query database")]
Database(#[from] chartered_db::Error),
#[error("Failed to fetch crate file")]
File(#[from] std::io::Error),
#[error("{0}")]
CrateFetch(#[from] crate::models::crates::CrateFetchError),
}
@@ -27,7 +25,7 @@
use axum::http::StatusCode;
match self {
Self::Database(_) | Self::File(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::CrateFetch(e) => e.status_code(),
}
}
@@ -1,8 +1,10 @@
mod info;
mod members;
mod recently_updated;
pub use info::handle as info;
pub use members::{
handle_delete as delete_member, handle_get as get_members, handle_patch as update_member,
handle_put as insert_member,
};
pub use recently_updated::handle as list_recently_updated;
@@ -1,0 +1,54 @@
use axum::{extract, Json};
use chartered_db::{crates::Crate, users::User, ConnectionPool};
use serde::Serialize;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Failed to query database")]
Database(#[from] chartered_db::Error),
#[error("{0}")]
CrateFetch(#[from] crate::models::crates::CrateFetchError),
}
impl Error {
pub fn status_code(&self) -> axum::http::StatusCode {
use axum::http::StatusCode;
match self {
Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::CrateFetch(e) => e.status_code(),
}
}
}
define_error_response!(Error);
pub async fn handle(
extract::Extension(db): extract::Extension<ConnectionPool>,
extract::Extension(user): extract::Extension<Arc<User>>,
) -> Result<Json<Response>, Error> {
let crates_with_versions = Crate::list_recently_updated(db, user.id).await?;
Ok(Json(Response {
versions: crates_with_versions
.into_iter()
.map(|(crate_, version)| ResponseVersion {
name: crate_.name,
version: version.version,
})
.collect(),
}))
}
#[derive(Serialize)]
pub struct Response {
versions: Vec<ResponseVersion>,
}
#[derive(Serialize)]
pub struct ResponseVersion {
name: String,
version: String,
}