🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-09-20 0:05:00.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-09-20 0:05:00.0 +01:00:00
commit
8cbac7a5b715f8ae4c7a6a345cce5f9294fe8731 [patch]
tree
d21a94be1cd2f679f571930c6aa9af44943b1108
parent
f72a27b3a5f7e4d0b54549b57d6111308ca557b9
download
8cbac7a5b715f8ae4c7a6a345cce5f9294fe8731.tar.gz

Implement versions page on crates view



Diff

 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<Self>,
        conn: ConnectionPool,
    ) -> Result<Vec<CrateVersion<'static>>> {
    ) -> Result<Vec<(CrateVersion<'static>, User)>> {
        tokio::task::spawn_blocking(move || {
            let conn = conn.get()?;

            Ok(CrateVersion::belonging_to(&*self).load::<CrateVersion>(&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<Self>,
        conn: ConnectionPool,
        user: Arc<User>,
        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<String>,
    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<Text>,
        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 dotted
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 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" }}
                  >
                    <Box />
                    <BoxSeam />
                  </div>
                  <h1 className="text-primary d-inline px-2">{crate}</h1>
                  <h2 className="text-secondary m-0">{crateVersion.vers}</h2>
@@ -149,7 +166,11 @@

              <div className="card-body">
                {currentTab == "readme" ? <ReadMe crate={crateInfo} /> : <></>}
                {currentTab == "versions" ? <>Versions</> : <></>}
                {currentTab == "versions" ? (
                  <Versions crate={crateInfo} />
                ) : (
                  <></>
                )}
                {currentTab == "members" ? <Members crate={crate} /> : <></>}
              </div>
            </div>
@@ -187,6 +208,104 @@
          </div>
        </div>
      </div>
    </div>
  );
}

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 (
    <div>
      {[...props.crate.versions].reverse().map((version, index) => (
        <div
          key={index}
          className={`card text-white bg-gradient ${
            index == 0 ? "bg-primary" : "bg-dark mt-2"
          }`}
        >
          <div className="card-body d-flex align-items-center">
            <h5 className="m-0">{version.vers}</h5>

            <div className="text-uppercase ms-4" style={{ fontSize: ".75rem" }}>
              <div>
                <div className="d-inline-block">
                  By
                  <img
                    src="http://placekitten.com/22/22"
                    className="rounded-circle ms-1 me-1"
                  />
                  {version.uploader}
                </div>

                <div className="ms-3 d-inline-block">
                  <OverlayTrigger
                    overlay={
                      <Tooltip
                        id={`tooltip-${props.crate.name}-version-${version.vers}-date`}
                      >
                        {new Date(version.created_at).toLocaleString()}
                      </Tooltip>
                    }
                  >
                    <span>
                      <Calendar3 />{" "}
                      <HumanTime
                        time={new Date(version.created_at).getTime()}
                      />
                    </span>
                  </OverlayTrigger>
                </div>
              </div>

              <div>
                <div className="d-inline-block">
                  <Hdd /> {humanFileSize(version.size)}
                </div>

                <div className="ms-3 d-inline-block">
                  <OverlayTrigger
                    overlay={
                      <Tooltip
                        id={`tooltip-${props.crate.name}-version-${version.vers}-feature-${index}`}
                      >
                        <div className="text-start m-2">
                          {Object.keys(version.features).map(
                            (feature, index) => (
                              <div key={index}>
                                {version.features["default"].includes(
                                  feature
                                ) ? (
                                  <CheckSquare className="me-2" />
                                ) : (
                                  <Square className="me-2" />
                                )}
                                {feature}
                              </div>
                            )
                          )}
                        </div>
                      </Tooltip>
                    }
                  >
                    <span>
                      <Check2Square /> {Object.keys(version.features).length}{" "}
                      Features
                    </span>
                  </OverlayTrigger>
                </div>
              </div>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}
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 @@
                      <h6 className="m-0 lh-sm">{key.name}</h6>
                      <pre className="m-0">{key.fingerprint}</pre>
                      <div className="lh-sm" style={{ fontSize: ".75rem" }}>
                        <span className="text-muted">
                          Added <HumanTime time={key.created_at} />
                        </span>
                        <span className="mx-2"></span>
                        <div className="text-muted d-inline-block me-3">
                          Added{" "}
                          <OverlayTrigger
                            overlay={
                              <Tooltip id={`${key.uuid}-created-at`}>
                                {new Date(key.created_at).toLocaleString()}
                              </Tooltip>
                            }
                          >
                            <span className="text-decoration-underline-dotted">
                              <HumanTime
                                time={new Date(key.created_at).getTime()}
                              />
                            </span>
                          </OverlayTrigger>
                        </div>
                        <span
                          className={`text-${
                            key.last_used_at
@@ -123,7 +135,19 @@
                        >
                          Last used{" "}
                          {key.last_used_at ? (
                            <HumanTime time={key.last_used_at} />
                            <OverlayTrigger
                              overlay={
                                <Tooltip id={`${key.uuid}-last-used`}>
                                  {new Date(key.last_used_at).toLocaleString()}
                                </Tooltip>
                              }
                            >
                              <span className="text-decoration-underline-dotted">
                                <HumanTime
                                  time={new Date(key.last_used_at).getTime()}
                                />
                              </span>
                            </OverlayTrigger>
                          ) : (
                            <>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<NaiveDateTime>,
    created_at: DateTime<Utc>,
    last_used_at: Option<DateTime<Utc>>,
}

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<Arc<User>>,
) -> Result<axum::http::Response<Full<Bytes>>, 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<CrateVersion<'a>>,
    versions: Vec<ResponseVersion<'a>>,
}

#[derive(Serialize)]
pub struct ResponseVersion<'a> {
    #[serde(flatten)]
    inner: CrateVersion<'a>,
    size: i32,
    created_at: chrono::DateTime<chrono::Utc>,
    uploader: String,
}

#[derive(Serialize)]