Show 'newly created' on homepage
Diff
chartered-db/src/crates.rs | 26 ++++++++++++++++++++++++++
chartered-db/src/schema.rs | 1 +
migrations/2021-08-31-214501_create_crates_table/up.sql | 1 +
chartered-frontend/src/pages/Dashboard.tsx | 42 ++++++++++++++++++++++++++++++++++++++++++
chartered-web/src/endpoints/web_api/crates/mod.rs | 2 ++
chartered-web/src/endpoints/web_api/crates/most_downloaded.rs | 4 ++--
chartered-web/src/endpoints/web_api/crates/recently_created.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 124 insertions(+), 4 deletions(-)
@@ -54,6 +54,7 @@
pub homepage: Option<String>,
pub documentation: Option<String>,
pub downloads: i32,
pub created_at: chrono::NaiveDateTime,
}
macro_rules! crate_with_permissions {
@@ -160,6 +161,31 @@
.load(&conn)?;
Ok(crate_versions.into_iter().into_grouping_map().collect())
})
.await?
}
pub async fn list_recently_created(
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::created_at.desc())
.load(&conn)?;
Ok(crates)
})
.await?
}
@@ -26,6 +26,7 @@
homepage -> Nullable<Text>,
documentation -> Nullable<Text>,
downloads -> Integer,
created_at -> Timestamp,
}
}
@@ -31,6 +31,7 @@
homepage VARCHAR(255),
documentation VARCHAR(255),
downloads INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (name, organisation_id),
FOREIGN KEY (organisation_id) REFERENCES organisations (id)
);
@@ -1,10 +1,22 @@
import React = require("react");
import { Link } from "react-router-dom";
import { useAuth } from "../useAuth";
import Nav from "../sections/Nav";
import {ChevronRight, Download} from "react-bootstrap-icons";
import {Calendar3, ChevronRight, Download} from "react-bootstrap-icons";
import { useAuthenticatedRequest } from "../util";
import HumanTime from "react-human-time";
import {OverlayTrigger, Tooltip} from "react-bootstrap";
interface RecentlyCreatedResponse {
crates: RecentlyCreatedResponseVersion[];
}
interface RecentlyCreatedResponseVersion {
name: string;
created_at: string;
organisation: string;
}
interface RecentlyUpdatedResponse {
versions: RecentlyUpdatedResponseVersion[];
@@ -28,6 +40,12 @@
export default function Dashboard() {
const auth = useAuth();
const { response: recentlyCreated, error: recentlyCreatedError } =
useAuthenticatedRequest<RecentlyCreatedResponse>({
auth,
endpoint: "crates/recently-created",
});
const { response: recentlyUpdated, error: recentlyUpdatedError } =
useAuthenticatedRequest<RecentlyUpdatedResponse>({
@@ -64,7 +82,27 @@
<div className="row">
<div className="col-12 col-md-4">
<h4>Your Crates</h4>
<h4>Newly Created</h4>
{(recentlyCreated?.crates || []).map((v) => (
<CrateCard key={v.name} organisation={v.organisation} name={v.name}>
<OverlayTrigger
overlay={
<Tooltip
id={`tooltip-${v.name}-date`}
>
{new Date(v.created_at).toLocaleString()}
</Tooltip>
}
>
<span>
<Calendar3 />{" "}
<HumanTime
time={new Date(v.created_at).getTime()}
/>
</span>
</OverlayTrigger>
</CrateCard>
))}
</div>
<div className="col-12 col-md-4">
@@ -1,8 +1,9 @@
mod info;
mod members;
mod most_downloaded;
mod recently_updated;
mod search;
mod recently_created;
use axum::{
body::{Body, BoxBody},
@@ -29,6 +30,7 @@
.route("/:org/:crate/members", put(members::handle_put))
.route("/:org/:crate/members", delete(members::handle_delete))
.route("/recently-updated", get(recently_updated::handle))
.route("/recently-created", get(recently_created::handle))
.route("/most-downloaded", get(most_downloaded::handle))
.route("/search", get(search::handle)))
}
@@ -24,10 +24,10 @@
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?;
let crates = Crate::list_most_downloaded(db, user.id).await?;
Ok(Json(Response {
crates: crates_with_versions
crates: crates
.into_iter()
.map(|(crate_, organisation)| ResponseCrate {
name: crate_.name,
@@ -1,0 +1,52 @@
use axum::{extract, Json};
use chartered_db::{crates::Crate, users::User, ConnectionPool};
use serde::Serialize;
use std::sync::Arc;
use chrono::{DateTime, TimeZone, Utc};
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 = Crate::list_recently_created(db, user.id).await?;
Ok(Json(Response {
crates: crates
.into_iter()
.map(|(crate_, organisation)| ResponseCrate {
name: crate_.name,
created_at: chrono::Utc.from_local_datetime(&crate_.created_at).unwrap(),
organisation: organisation.name,
})
.collect(),
}))
}
#[derive(Serialize)]
pub struct Response {
crates: Vec<ResponseCrate>,
}
#[derive(Serialize)]
pub struct ResponseCrate {
name: String,
created_at: DateTime<Utc>,
organisation: String,
}