From 8cbac7a5b715f8ae4c7a6a345cce5f9294fe8731 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 20 Sep 2021 00:05:00 +0100 Subject: [PATCH] Implement versions page on crates view --- chartered-db/src/crates.rs | 20 +++++++++++++++----- chartered-db/src/schema.rs | 4 ++++ chartered-frontend/src/index.sass | 3 +++ migrations/2021-08-31-214501_create_crates_table/up.sql | 4 ++++ chartered-frontend/src/pages/crate/CrateView.tsx | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 36 ++++++++++++++++++++++++++++++++++-- chartered-web/src/endpoints/cargo_api/publish.rs | 4 +++- chartered-web/src/endpoints/web_api/ssh_key.rs | 10 +++++----- chartered-web/src/endpoints/web_api/crates/info.rs | 21 +++++++++++++++++++-- 9 files changed, 203 insertions(+), 24 deletions(-) diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs index c7c172a..3f21e96 100644 --- a/chartered-db/src/crates.rs +++ a/chartered-db/src/crates.rs @@ -1,7 +1,7 @@ -use crate::users::UserCratePermission; +use crate::users::{UserCratePermission, User}; use super::{ - schema::{crate_versions, crates}, + schema::{crate_versions, crates, users}, BitwiseExpressionMethods, ConnectionPool, Result, }; use diesel::{insert_into, prelude::*, Associations, Identifiable, Queryable}; @@ -74,14 +74,14 @@ .await? } - pub async fn versions( + pub async fn versions_with_uploader( self: Arc, conn: ConnectionPool, - ) -> Result>> { + ) -> Result, User)>> { tokio::task::spawn_blocking(move || { let conn = conn.get()?; - Ok(CrateVersion::belonging_to(&*self).load::(&conn)?) + Ok(CrateVersion::belonging_to(&*self).inner_join(users::table).load::<(CrateVersion, User)>(&conn)?) }) .await? } @@ -216,14 +216,16 @@ pub async fn publish_version( self: Arc, conn: ConnectionPool, + user: Arc, file_identifier: chartered_fs::FileReference, file_checksum: String, + file_size: i32, given: chartered_types::cargo::CrateVersion<'static>, metadata: chartered_types::cargo::CrateVersionMetadata, ) -> Result<()> { use crate::schema::crate_versions::dsl::{ checksum, crate_id, crate_versions, dependencies, features, filesystem_object, links, - version, + version, size, user_id, }; use crate::schema::crates::dsl::{ crates, description, documentation, homepage, id, readme, repository, @@ -247,11 +249,13 @@ .values(( crate_id.eq(self.id), filesystem_object.eq(file_identifier.to_string()), + size.eq(file_size), checksum.eq(file_checksum), version.eq(given.vers), dependencies.eq(CrateDependencies(given.deps)), features.eq(CrateFeatures(given.features)), links.eq(given.links), + user_id.eq(user.id), )) .execute(&conn)?; @@ -290,16 +294,20 @@ #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[belongs_to(Crate)] +#[belongs_to(User)] pub struct CrateVersion<'a> { pub id: i32, pub crate_id: i32, pub version: String, pub filesystem_object: String, + pub size: i32, pub yanked: bool, pub checksum: String, pub dependencies: CrateDependencies<'a>, pub features: CrateFeatures, pub links: Option, + pub user_id: i32, + pub created_at: chrono::NaiveDateTime, } impl<'a> CrateVersion<'a> { diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs index 384d4e2..1b3e311 100644 --- a/chartered-db/src/schema.rs +++ a/chartered-db/src/schema.rs @@ -1,14 +1,17 @@ table! { crate_versions (id) { id -> Integer, crate_id -> Integer, version -> Text, filesystem_object -> Text, + size -> Integer, yanked -> Bool, checksum -> Text, dependencies -> Binary, features -> Binary, links -> Nullable, + user_id -> Integer, + created_at -> Timestamp, } } @@ -66,6 +69,7 @@ } joinable!(crate_versions -> crates (crate_id)); +joinable!(crate_versions -> users (user_id)); joinable!(user_crate_permissions -> crates (crate_id)); joinable!(user_crate_permissions -> users (user_id)); joinable!(user_sessions -> user_ssh_keys (user_ssh_key_id)); diff --git a/chartered-frontend/src/index.sass b/chartered-frontend/src/index.sass index 30c06c3..db918e5 100644 --- a/chartered-frontend/src/index.sass +++ a/chartered-frontend/src/index.sass @@ -19,3 +19,6 @@ td.fit, th.fit width: 0.1% white-space: nowrap + +.text-decoration-underline-dotted + text-decoration: underline dotteddiff --git a/migrations/2021-08-31-214501_create_crates_table/up.sql b/migrations/2021-08-31-214501_create_crates_table/up.sql index 21d1db1..5277879 100644 --- a/migrations/2021-08-31-214501_create_crates_table/up.sql +++ a/migrations/2021-08-31-214501_create_crates_table/up.sql @@ -13,12 +13,16 @@ crate_id INTEGER NOT NULL, version VARCHAR(255) NOT NULL, filesystem_object VARCHAR(255) NOT NULL, + size INTEGER NOT NULL, yanked BOOLEAN NOT NULL DEFAULT FALSE, checksum VARCHAR(255) NOT NULL, dependencies BLOB NOT NULL, features BLOB NOT NULL, links VARCHAR(255), + user_id INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (crate_id, version), + FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (crate_id) REFERENCES crates (id) ); diff --git a/chartered-frontend/src/pages/crate/CrateView.tsx b/chartered-frontend/src/pages/crate/CrateView.tsx index 3c7deb2..8a23aaf 100644 --- a/chartered-frontend/src/pages/crate/CrateView.tsx +++ a/chartered-frontend/src/pages/crate/CrateView.tsx @@ -6,7 +6,18 @@ import Nav from "../../sections/Nav"; import Loading from "../Loading"; import ErrorPage from "../ErrorPage"; -import { Box, HouseDoor, Book, Building } from "react-bootstrap-icons"; +import { + BoxSeam, + HouseDoor, + Book, + Building, + Calendar3, + Check2Square, + Hdd, + CheckSquare, + Check, + Square, +} from "react-bootstrap-icons"; import { useParams } from "react-router-dom"; import { useAuthenticatedRequest } from "../../util"; @@ -14,6 +25,8 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import Members from "./Members"; +import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import HumanTime from "react-human-time"; type Tab = "readme" | "versions" | "members"; @@ -30,6 +43,10 @@ export interface CrateInfoVersion { vers: string; deps: CrateInfoVersionDependency[]; + features: any[]; + size: number; + uploader: string; + created_at: string; } export interface CrateInfoVersionDependency { @@ -70,7 +87,7 @@ className="text-white circle bg-primary bg-gradient d-inline rounded-circle d-inline-flex justify-content-center align-items-center" style={{ width: "2rem", height: "2rem" }} > - +

{crate}

{crateVersion.vers}

@@ -149,7 +166,11 @@
{currentTab == "readme" ? : <>} - {currentTab == "versions" ? <>Versions : <>} + {currentTab == "versions" ? ( + + ) : ( + <> + )} {currentTab == "members" ? : <>}
@@ -187,6 +208,104 @@ + + ); +} + +function Versions(props: { crate: CrateInfo }) { + const humanFileSize = (size) => { + var i = Math.floor(Math.log(size) / Math.log(1024)); + return ( + Number((size / Math.pow(1024, i)).toFixed(2)) + + " " + + ["B", "kB", "MB", "GB", "TB"][i] + ); + }; + + return ( +
+ {[...props.crate.versions].reverse().map((version, index) => ( +
+
+
{version.vers}
+ +
+
+
+ By + + {version.uploader} +
+ +
+ + {new Date(version.created_at).toLocaleString()} + + } + > + + {" "} + + + +
+
+ +
+
+ {humanFileSize(version.size)} +
+ +
+ +
+ {Object.keys(version.features).map( + (feature, index) => ( +
+ {version.features["default"].includes( + feature + ) ? ( + + ) : ( + + )} + {feature} +
+ ) + )} +
+ + } + > + + {Object.keys(version.features).length}{" "} + Features + +
+
+
+
+
+
+ ))}
); } diff --git a/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx b/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx index e9f54dd..5b72de9 100644 --- a/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx +++ a/chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx @@ -7,7 +7,7 @@ import { useAuthenticatedRequest, authenticatedEndpoint } from "../../util"; import { Plus, Trash } from "react-bootstrap-icons"; -import { Button, Modal } from "react-bootstrap"; +import { Button, Modal, OverlayTrigger, Tooltip } from "react-bootstrap"; import HumanTime from "react-human-time"; import ErrorPage from "../ErrorPage"; import Loading from "../Loading"; @@ -108,10 +108,22 @@
{key.name}
{key.fingerprint}
- - Added - - +
+ Added{" "} + + {new Date(key.created_at).toLocaleString()} + + } + > + + + + +
Last used{" "} {key.last_used_at ? ( - + + {new Date(key.last_used_at).toLocaleString()} + + } + > + + + + ) : ( <>never )} diff --git a/chartered-web/src/endpoints/cargo_api/publish.rs b/chartered-web/src/endpoints/cargo_api/publish.rs index 52fdb3e..8adc936 100644 --- a/chartered-web/src/endpoints/cargo_api/publish.rs +++ a/chartered-web/src/endpoints/cargo_api/publish.rs @@ -60,7 +60,7 @@ let crate_ = get_crate_with_permissions( db.clone(), - user, + user.clone(), metadata.inner.name.to_string(), &[Permission::VISIBLE, Permission::PUBLISH_VERSION], ) @@ -71,8 +71,10 @@ crate_ .publish_version( db, + user, file_ref, hex::encode(Sha256::digest(crate_bytes)), + metadata_bytes.len().try_into().unwrap(), metadata.inner.into_owned(), metadata.meta, ) diff --git a/chartered-web/src/endpoints/web_api/ssh_key.rs b/chartered-web/src/endpoints/web_api/ssh_key.rs index 923ca05..83bdcd6 100644 --- a/chartered-web/src/endpoints/web_api/ssh_key.rs +++ a/chartered-web/src/endpoints/web_api/ssh_key.rs @@ -1,8 +1,8 @@ use chartered_db::{users::User, ConnectionPool}; use axum::{extract, Json}; use chartered_db::uuid::Uuid; -use chrono::NaiveDateTime; +use chrono::{DateTime, Utc, TimeZone}; use log::warn; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -20,8 +20,8 @@ uuid: Uuid, name: String, fingerprint: String, - created_at: NaiveDateTime, - last_used_at: Option, + created_at: DateTime, + last_used_at: Option>, } pub async fn handle_get( @@ -39,8 +39,8 @@ "INVALID".to_string() }), name: key.name, - created_at: key.created_at, - last_used_at: key.last_used_at, + 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()), }) .collect(); diff --git a/chartered-web/src/endpoints/web_api/crates/info.rs b/chartered-web/src/endpoints/web_api/crates/info.rs index bac066c..c1a8fff 100644 --- a/chartered-web/src/endpoints/web_api/crates/info.rs +++ a/chartered-web/src/endpoints/web_api/crates/info.rs @@ -6,6 +6,7 @@ users::{User, UserCratePermissionValue as Permission}, ConnectionPool, }; +use chrono::TimeZone; use chartered_types::cargo::CrateVersion; use serde::Serialize; use std::sync::Arc; @@ -40,7 +41,7 @@ extract::Extension(user): extract::Extension>, ) -> Result>, Error> { let crate_ = get_crate_with_permissions(db.clone(), user, name, &[Permission::VISIBLE]).await?; - let versions = crate_.clone().versions(db).await?; + let versions = crate_.clone().versions_with_uploader(db).await?; // returning a Response instead of Json here so we don't have to close // every Crate/CrateVersion etc, would be easier if we just had an owned @@ -51,7 +52,12 @@ info: crate_.as_ref().into(), versions: versions .into_iter() - .map(|v| v.into_cargo_format(&crate_)) + .map(|(v, user)| ResponseVersion { + size: v.size, + created_at: chrono::Utc.from_local_datetime(&v.created_at).unwrap(), + inner: v.into_cargo_format(&crate_), + uploader: user.username, + }) .collect(), }) .into_response()) @@ -61,7 +67,16 @@ pub struct Response<'a> { #[serde(flatten)] info: ResponseInfo<'a>, - versions: Vec>, + versions: Vec>, +} + +#[derive(Serialize)] +pub struct ResponseVersion<'a> { + #[serde(flatten)] + inner: CrateVersion<'a>, + size: i32, + created_at: chrono::DateTime, + uploader: String, } #[derive(Serialize)] -- rgit 0.1.3