Show 'most downloaded' on homepage
Diff
chartered-db/src/crates.rs | 40 ++++++++++++++++++++++++++++++++++++++++
chartered-db/src/schema.rs | 1 +
migrations/2021-08-31-214501_create_crates_table/up.sql | 1 +
chartered-frontend/src/pages/Dashboard.tsx | 43 +++++++++++++++++++++++++++++++++++++------
chartered-web/src/endpoints/cargo_api/download.rs | 8 ++++++++
chartered-web/src/endpoints/web_api/crates/mod.rs | 2 ++
chartered-web/src/endpoints/web_api/crates/most_downloaded.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 132 insertions(+), 14 deletions(-)
@@ -53,6 +53,7 @@
pub repository: Option<String>,
pub homepage: Option<String>,
pub documentation: Option<String>,
pub downloads: i32,
}
macro_rules! crate_with_permissions {
@@ -163,6 +164,31 @@
.await?
}
pub async fn list_most_downloaded(
conn: ConnectionPool,
requesting_user_id: i32,
) -> Result<Vec<(Crate, Organisation)>> {
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
let crates = crate_with_permissions!(requesting_user_id)
.filter(
select_permissions!()
.bitwise_and(UserPermission::VISIBLE.bits())
.eq(UserPermission::VISIBLE.bits()),
)
.inner_join(organisations::table)
.inner_join(crate_versions::table)
.select((crates::all_columns, organisations::all_columns))
.limit(10)
.order_by(crate::schema::crates::dsl::downloads.desc())
.load(&conn)?;
Ok(crates)
})
.await?
}
pub async fn list_recently_updated(
conn: ConnectionPool,
requesting_user_id: i32,
@@ -326,6 +352,20 @@
Ok(CrateVersion::belonging_to(&self.crate_)
.inner_join(users::table)
.load::<(CrateVersion, User)>(&conn)?)
})
.await?
}
pub async fn increment_download_count(self: Arc<Self>, conn: ConnectionPool) -> Result<()> {
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;
let _ = diesel::update(crates::table)
.set(crates::downloads.eq(crates::downloads + 1))
.filter(crates::id.eq(self.crate_.id))
.execute(&conn)?;
Ok(())
})
.await?
}
@@ -25,6 +25,7 @@
repository -> Nullable<Text>,
homepage -> Nullable<Text>,
documentation -> Nullable<Text>,
downloads -> Integer,
}
}
@@ -30,6 +30,7 @@
repository VARCHAR(255),
homepage VARCHAR(255),
documentation VARCHAR(255),
downloads INTEGER NOT NULL DEFAULT 0,
UNIQUE (name, organisation_id),
FOREIGN KEY (organisation_id) REFERENCES organisations (id)
);
@@ -1,9 +1,9 @@
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 {ChevronRight, Download} from "react-bootstrap-icons";
import { useAuthenticatedRequest } from "../util";
interface RecentlyUpdatedResponse {
@@ -16,15 +16,31 @@
organisation: string;
}
interface MostDownloadedResponse {
crates: MostDownloadedResponseCrate[];
}
interface MostDownloadedResponseCrate {
name: string;
downloads: number;
organisation: string;
}
export default function Dashboard() {
const auth = useAuth();
const { response: recentlyUpdated, error } =
const { response: recentlyUpdated, error: recentlyUpdatedError } =
useAuthenticatedRequest<RecentlyUpdatedResponse>({
auth,
endpoint: "crates/recently-updated",
});
const { response: mostDownloaded, error: mostDownloadedError } =
useAuthenticatedRequest<MostDownloadedResponse>({
auth,
endpoint: "crates/most-downloaded",
});
return (
<div className="text-white">
<Nav />
@@ -54,39 +70,38 @@
<div className="col-12 col-md-4">
<h4>Recently Updated</h4>
{(recentlyUpdated?.versions || []).map((v) => (
<CrateCard key={v.name} crate={v} />
<CrateCard key={v.name} organisation={v.organisation} name={v.name}>v{v.version}</CrateCard>
))}
</div>
<div className="col-12 col-md-4">
<h4>Most Downloaded</h4>
{(mostDownloaded?.crates || []).map((v) => (
<CrateCard key={v.name} organisation={v.organisation} name={v.name}>
<Download /> {v.downloads.toLocaleString()}
</CrateCard>
))}
</div>
</div>
</div>
</div>
);
}
interface Crate {
name: string;
version: string;
organisation: string;
}
function CrateCard({ crate }: { crate: Crate }) {
function CrateCard({ name, organisation, children }: React.PropsWithChildren<{ name: string, organisation: string }>) {
return (
<Link
to={`/crates/${crate.organisation}/${crate.name}`}
to={`/crates/${organisation}/${name}`}
className="text-decoration-none"
>
<div className="card border-0 mb-2 shadow-sm">
<div className="card-body text-black d-flex flex-row">
<div className="flex-grow-1 align-self-center">
<h6 className="text-primary my-0">
<span className="text-secondary">{crate.organisation}/</span>
{crate.name}
<span className="text-secondary">{organisation}/</span>
{name}
</h6>
<small className="text-secondary">v{crate.version}</small>
<small className="text-secondary">{children}</small>
</div>
<ChevronRight size={16} className="align-self-center" />
@@ -41,6 +41,14 @@
let crate_with_permissions =
Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?);
tokio::spawn(
crate_with_permissions
.clone()
.increment_download_count(db.clone()),
);
let version = crate_with_permissions
.version(db, version)
.await?
@@ -1,5 +1,6 @@
mod info;
mod members;
mod most_downloaded;
mod recently_updated;
mod search;
@@ -28,5 +29,6 @@
.route("/:org/:crate/members", put(members::handle_put))
.route("/:org/:crate/members", delete(members::handle_delete))
.route("/recently-updated", get(recently_updated::handle))
.route("/most-downloaded", get(most_downloaded::handle))
.route("/search", get(search::handle)))
}
@@ -1,0 +1,51 @@
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("{0}")]
Database(#[from] chartered_db::Error),
}
impl Error {
pub fn status_code(&self) -> axum::http::StatusCode {
match self {
Self::Database(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_most_downloaded(db, user.id).await?;
Ok(Json(Response {
crates: crates_with_versions
.into_iter()
.map(|(crate_, organisation)| ResponseCrate {
name: crate_.name,
downloads: crate_.downloads,
organisation: organisation.name,
})
.collect(),
}))
}
#[derive(Serialize)]
pub struct Response {
crates: Vec<ResponseCrate>,
}
#[derive(Serialize)]
pub struct ResponseCrate {
name: String,
downloads: i32,
organisation: String,
}