🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-10-11 19:18:31.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-10-11 19:21:41.0 +01:00:00
commit
dae6a84de4fed35acab26e7ed6a84dc3e356708a [patch]
tree
cbcca23bfc32d2837af96e30a50e5c471b8ca03f
parent
abb9bf8ec403242b74542e7b1fe3eaa0f8675866
download
dae6a84de4fed35acab26e7ed6a84dc3e356708a.tar.gz

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(-)

diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs
index b5678c4..671fe62 100644
--- a/chartered-db/src/crates.rs
+++ a/chartered-db/src/crates.rs
@@ -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?
    }
diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs
index 6010788..74790d4 100644
--- a/chartered-db/src/schema.rs
+++ a/chartered-db/src/schema.rs
@@ -25,6 +25,7 @@
        repository -> Nullable<Text>,
        homepage -> Nullable<Text>,
        documentation -> Nullable<Text>,
        downloads -> Integer,
    }
}

diff --git a/migrations/2021-08-31-214501_create_crates_table/up.sql b/migrations/2021-08-31-214501_create_crates_table/up.sql
index 3c136d1..4d9cd99 100644
--- a/migrations/2021-08-31-214501_create_crates_table/up.sql
+++ a/migrations/2021-08-31-214501_create_crates_table/up.sql
@@ -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)
);
diff --git a/chartered-frontend/src/pages/Dashboard.tsx b/chartered-frontend/src/pages/Dashboard.tsx
index 0db2797..a7b606e 100644
--- a/chartered-frontend/src/pages/Dashboard.tsx
+++ a/chartered-frontend/src/pages/Dashboard.tsx
@@ -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" />
diff --git a/chartered-web/src/endpoints/cargo_api/download.rs b/chartered-web/src/endpoints/cargo_api/download.rs
index d9138e1..b26c2d7 100644
--- a/chartered-web/src/endpoints/cargo_api/download.rs
+++ a/chartered-web/src/endpoints/cargo_api/download.rs
@@ -41,6 +41,14 @@
    let crate_with_permissions =
        Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?);

    // we shouldn't really hold back this request from progressing whilst waiting
    // on the downloads increment to complete so we'll just tokio::spawn it
    tokio::spawn(
        crate_with_permissions
            .clone()
            .increment_download_count(db.clone()),
    );

    let version = crate_with_permissions
        .version(db, version)
        .await?
diff --git a/chartered-web/src/endpoints/web_api/crates/mod.rs b/chartered-web/src/endpoints/web_api/crates/mod.rs
index 28f6bd4..e2e3d49 100644
--- a/chartered-web/src/endpoints/web_api/crates/mod.rs
+++ a/chartered-web/src/endpoints/web_api/crates/mod.rs
@@ -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)))
}
diff --git a/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs b/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs
new file mode 100644
index 0000000..cabad18 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs
@@ -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,
}