Further progress on registry UI.
Some checks failed
continuous-integration/drone/push Build is failing

Add package README view support and style adjustments
This commit is contained in:
Fabian Stamm 2020-07-31 20:16:12 +02:00
parent 0bee324519
commit 7fcdf2c383
13 changed files with 336 additions and 76 deletions

View File

@ -4,12 +4,27 @@ await FS.ensureDir("./data");
export interface IPackage { export interface IPackage {
name: string; name: string;
author: string;
description: string;
versions: string[]; versions: string[];
} }
const db = new Datastore<IPackage>({ export interface IApiKey {
filename: "data/db.json", user: string;
key: string;
createdAt: Date;
lastAccess?: Date;
lastIP?: string;
}
const db = {
package: new Datastore<IPackage>({
filename: "data/packages.json",
autoload: true, autoload: true,
}); }),
api_key: new Datastore<IApiKey>({
filename: "data/api_keys.json",
}),
};
export default db; export default db;

View File

@ -13,6 +13,8 @@ export * as FS from "https://deno.land/std@0.62.0/fs/mod.ts";
export * as Compress from "https://git.stamm.me/Deno/DenReg/raw/branch/master/tar/mod.ts"; export * as Compress from "https://git.stamm.me/Deno/DenReg/raw/branch/master/tar/mod.ts";
export { Marked } from "https://deno.land/x/markdown/mod.ts";
import DS from "https://raw.githubusercontent.com/hibas123/dndb/master/mod.ts"; import DS from "https://raw.githubusercontent.com/hibas123/dndb/master/mod.ts";
/// <reference path="./types/jsx.d.ts" /> /// <reference path="./types/jsx.d.ts" />
@ -20,6 +22,6 @@ export {
React, React,
jsx, jsx,
Fragment, Fragment,
} from "https://deno.hibas123.de/raw/@denreg-jsx/mod.ts"; } from "https://raw.githubusercontent.com/apiel/jsx-html/master/mod.ts";
export const Datastore = DS; export const Datastore = DS;

View File

@ -9,13 +9,15 @@ app.use(LoggerMW.logger({}));
app.use(CorsMW.cors({})); app.use(CorsMW.cors({}));
import api from "./http/api.ts"; import api from "./http/api.ts";
api(app.group("api")); api(app.group("/api"));
import raw from "./http/raw.ts"; import raw from "./http/raw.ts";
raw(app.group("raw")); raw(app.group("/raw"));
import view from "./http/views.ts"; import view from "./http/views.ts";
view(app.group("/")); view(app);
console.log(app.router.trees.GET);
import render from "./renderer.tsx"; import render from "./renderer.tsx";
app.renderer = { app.renderer = {

View File

@ -12,19 +12,40 @@ export default function api(g: ABC.Group) {
return { version: "1" }; return { version: "1" };
}); });
// g.post("/getapikey", getApiKey, basicauth("api"));
g.post("/package/:name", uploadPackage, basicauth("api")); g.post("/package/:name", uploadPackage, basicauth("api"));
} }
// async function getApiKey(ctx: ABC.Context) {
// const key = v4.generate();
// await db.api_key.insert({
// user: ctx.customContext.user,
// key,
// createdAt: new Date(),
// lastAccess: undefined,
// lastIP: undefined,
// });
// return {
// key,
// };
// }
async function uploadPackage(ctx: ABC.Context) { async function uploadPackage(ctx: ABC.Context) {
const reqId = v4.generate(); const reqId = v4.generate();
const filename = "./tmp/" + reqId + ".tar"; const filename = "./tmp/" + reqId + ".tar";
const folder = "./tmp/" + reqId; const folder = "./tmp/" + reqId;
try { try {
const packageName = ctx.params.name; const packageName = ctx.params.name.toLowerCase();
if (!isValidPackageName(packageName)) if (!isValidPackageName(packageName)) {
throw new Error("Invalid package name"); return {
success: false,
message: "Invalid package name",
};
}
console.log("Writing body to tmp file:", filename); console.log("Writing body to tmp file:", filename);
const file = await Deno.open(filename, { const file = await Deno.open(filename, {
@ -53,7 +74,10 @@ async function uploadPackage(ctx: ABC.Context) {
console.log("Checking meta.json"); console.log("Checking meta.json");
if (!meta?.version) { if (!meta?.version) {
throw new Error("No version available in meta.json"); return {
success: false,
message: "No version available in meta.json",
};
} }
const packageVersion = meta.version; const packageVersion = meta.version;
@ -61,28 +85,42 @@ async function uploadPackage(ctx: ABC.Context) {
console.log("Checking correct version"); console.log("Checking correct version");
if (!isValidFullVersion(packageVersion)) { if (!isValidFullVersion(packageVersion)) {
throw new Error("Invalid version. Version must be in format: 0.0.0"); return {
success: false,
message: "Invalid version. Version must be in format: 0.0.0",
};
return;
} }
console.log("Checking for previous uploads"); console.log("Checking for previous uploads");
let packageMeta = await db.findOne({ name: packageName }); let packageMeta = await db.package.findOne({ name: packageName });
console.log(meta, packageMeta);
if (!packageMeta) { if (!packageMeta) {
packageMeta = { packageMeta = {
name: packageName, name: packageName,
author: meta.author,
description: meta.description,
versions: [], versions: [],
}; };
await db.insert(packageMeta); await db.package.insert(packageMeta);
} }
console.log("Check if version was uploaded before"); console.log("Check if version was uploaded before");
if (packageMeta.versions.find((e) => e === meta.version)) { if (packageMeta.versions.find((e) => e === meta.version)) {
throw new Error("Version was already uploaded!"); return {
success: false,
message: "Version was already uploaded!",
};
} }
packageMeta.author = meta.author;
packageMeta.description = meta.description;
const bucketBase = "packages/" + packageName + "/" + packageVersion + "/"; const bucketBase = "packages/" + packageName + "/" + packageVersion + "/";
console.log("Uploading files to S3"); console.log("Uploading files to S3");
@ -107,7 +145,7 @@ async function uploadPackage(ctx: ABC.Context) {
console.log("Setting new live version"); console.log("Setting new live version");
//TODO: Better option, since this could error whith multiple upload to the same package //TODO: Better option, since this could error whith multiple upload to the same package
await db.update( await db.package.update(
{ name: packageName }, { name: packageName },
{ {
$set: { versions: [...packageMeta.versions, packageVersion] }, $set: { versions: [...packageMeta.versions, packageVersion] },
@ -115,6 +153,9 @@ async function uploadPackage(ctx: ABC.Context) {
); );
console.log("Finished successfully"); console.log("Finished successfully");
return {
success: true,
};
} catch (err) { } catch (err) {
console.error("Error while processing newly uploaded package"); console.error("Error while processing newly uploaded package");
console.error(err); console.error(err);
@ -127,7 +168,4 @@ async function uploadPackage(ctx: ABC.Context) {
await Deno.remove(filename).catch(console.error); await Deno.remove(filename).catch(console.error);
await Deno.remove(folder, { recursive: true }).catch(console.error); await Deno.remove(folder, { recursive: true }).catch(console.error);
} }
return {
success: true,
};
} }

View File

@ -1,10 +1,5 @@
import { ABC } from "../deps.ts"; import { ABC } from "../deps.ts";
import { extractPackagePath, getFile } from "../utils.ts";
import { sortVersions, extractPackagePath } from "../utils.ts";
import db, { IPackage } from "../db.ts";
import bucket from "../s3.ts";
export default function raw(g: ABC.Group) { export default function raw(g: ABC.Group) {
g.get("/:package/*path", async (ctx) => { g.get("/:package/*path", async (ctx) => {
@ -13,41 +8,17 @@ export default function raw(g: ABC.Group) {
ctx.params.package ctx.params.package
); );
const meta = await db.findOne({ name: packageName });
console.log(packageName, await db.findOne({ name: packageName }));
const E404 = () => { const E404 = () => {
ctx.response.status = 404; ctx.response.status = 404;
ctx.response.body = "Not found!"; ctx.response.body = "// Not found!";
throw new Error("Not found!");
}; };
if (!meta || meta.versions.length < 1) return E404(); const result = await getFile(
packageName,
const versions = meta.versions.sort(sortVersions).reverse(); packageVersion,
if (!packageVersion) {
packageVersion = versions[0];
} else {
const v = versions.filter((e) =>
e.startsWith(packageVersion as string)
);
if (v.length < 1) return E404();
packageVersion = v[0];
}
const bucketPath = (
"packages/" +
packageName +
"/" +
packageVersion +
"/" +
ctx.params.path ctx.params.path
).replace(/@/g, "§"); );
if (!result) return E404();
console.log("Getting file from:", bucketPath); return result;
return (await bucket.getObject(bucketPath))?.body;
}); });
} }

View File

@ -1,11 +1,13 @@
import { ABC } from "../deps.ts"; import { ABC } from "../deps.ts";
import { basicauth } from "../utils.ts"; import { basicauth, extractPackagePath } from "../utils.ts";
export default function views(g: ABC.Group) { export default function views(g: ABC.Application) {
g.get( g.get(
"/", "/",
async (ctx) => { async (ctx) => {
return ctx.render("index"); return ctx.render("index", {
search: ctx.queryParams["q"],
});
// const render = await IndexView(); // const render = await IndexView();
// console.log(render); // console.log(render);
// ctx.response.body = render; // ctx.response.body = render;
@ -13,4 +15,15 @@ export default function views(g: ABC.Group) {
}, },
basicauth("views") basicauth("views")
); );
g.get(
"/package/:package",
async (ctx) => {
let [packageName, packageVersion] = extractPackagePath(
ctx.params.package
);
return ctx.render("package", { packageName });
},
basicauth("views")
);
} }

View File

@ -1,6 +1,8 @@
/// <reference path="./types/jsx.d.ts" /> /// <reference path="./types/jsx.d.ts" />
import { React, jsx } from "./deps.ts"; import { React, jsx } from "./deps.ts";
import { v4 } from "https://deno.land/std/uuid/mod.ts";
class StringReader implements Deno.Reader { class StringReader implements Deno.Reader {
private data: Uint8Array; private data: Uint8Array;
private offset = 0; private offset = 0;
@ -27,11 +29,12 @@ export default async function render(
name: string, name: string,
data: any data: any
): Promise<Deno.Reader> { ): Promise<Deno.Reader> {
const id = v4.generate();
const component: { const component: {
default: () => JSX.IntrinsicElements | Promise<JSX.IntrinsicElements>; default: () => JSX.IntrinsicElements | Promise<JSX.IntrinsicElements>;
} = await import(`./views/${name}.tsx`); } = await import(`./views/${name}.tsx?id=${id}.tsx`);
const res = await (<component.default {...data} />).render(); const res = await (<component.default {...data} />).render();
console.log(res);
return new StringReader(res as string); return new StringReader(res as string);
} }

View File

@ -47,6 +47,8 @@ export const basicauth = (realm: string) => (next: ABC.HandlerFunc) => (
if (config?.user[username]?.password === passwd) { if (config?.user[username]?.password === passwd) {
console.log("User authenticated!"); console.log("User authenticated!");
if (!ctx.customContext) ctx.customContext = {};
ctx.customContext.user = username;
return next(ctx); return next(ctx);
} }
} }
@ -62,6 +64,7 @@ export const basicauth = (realm: string) => (next: ABC.HandlerFunc) => (
export function extractPackagePath(path: string): [string, string | undefined] { export function extractPackagePath(path: string): [string, string | undefined] {
let packageName = ""; let packageName = "";
path = path.toLowerCase();
if (path.startsWith("@")) { if (path.startsWith("@")) {
packageName = "@"; packageName = "@";
path = path.slice(1); path = path.slice(1);
@ -84,3 +87,48 @@ export function extractPackagePath(path: string): [string, string | undefined] {
return [packageName, packageVersion]; return [packageName, packageVersion];
} }
import db from "./db.ts";
import bucket from "./s3.ts";
import { S3 } from "./deps.ts";
export async function getFile(
pkgName: string,
version: string | null | undefined,
file: string
): Promise<Uint8Array | null> {
const meta = await db.package.findOne({ name: pkgName });
if (!meta || meta.versions.length < 1) return null;
const versions = meta.versions.sort(sortVersions).reverse();
if (!version) {
version = versions[0];
} else {
const v = versions.filter((e) => e.startsWith(version as string));
if (v.length < 1) return null;
version = v[0];
}
const bucketPath = (
"packages/" +
pkgName +
"/" +
version +
"/" +
file
).replace(/@/g, "§");
console.log("Getting file from:", bucketPath);
try {
const data = (await bucket.getObject(bucketPath))?.body;
return data;
} catch (err) {
const msg = err.message as string;
if (msg.indexOf("404") >= 0) return null;
throw err;
}
}

View File

@ -1,16 +1,25 @@
/// <reference path="../types/jsx.d.ts" /> /// <reference path="../types/jsx.d.ts" />
import { React } from "../deps.ts"; import { React } from "../deps.ts";
const styles = new TextDecoder().decode(
Deno.readFileSync("src/views/styles.css")
);
export default function Base(d: any, children: any[]) { export default function Base(d: any, children: any[]) {
return ( return (
<html> <html>
<head> <head>
<link {/* <link
rel="stylesheet" rel="stylesheet"
href="https://deno.hibas123.de/raw/@hibas123-theme@2.0.2/out/base.css" href="https://deno.hibas123.de/raw/@hibas123-theme@2.0.2/out/base.css"
/> */}
<link
rel="stylesheet"
href="https://unpkg.com/papercss@1.6.1/dist/paper.min.css"
/> />
<style innerHTML={styles}></style>
</head> </head>
<body class="light-theme">{children}</body> <body class="site">{children}</body>
</html> </html>
); );
} }

View File

@ -0,0 +1,30 @@
/// <reference path="../types/jsx.d.ts" />
import { React, Fragment } from "../deps.ts";
export function Main(a: any, children: any) {
return (
<div style="grid-area: main">
<div class="paper">{children}</div>
</div>
);
}
export function Menu({}: any, children: any) {
return (
<div style="grid-area: menu">
<div class="paper">
<div class="row flex-right">
<button class="sm-4">Login</button>
<button class="sm-4">SignUp</button>
</div>
<h3 class="sidebar-title" style="text-align:center">
<a href="/" style="all:inherit;">
DenReg
</a>
</h3>
{children}
</div>
</div>
);
}

View File

@ -2,32 +2,74 @@
import { React, Fragment } from "../deps.ts"; import { React, Fragment } from "../deps.ts";
import Base from "./_base.tsx"; import Base from "./_base.tsx";
import DB, { IPackage } from "../db.ts"; import DB, { IPackage } from "../db.ts";
import { sortVersions } from "../utils.ts";
function Package({ pkg }: { pkg: IPackage }) { function Package({ pkg }: { pkg: IPackage }) {
const { name, versions } = pkg; const { name, versions, author, description } = pkg;
const sorted = versions.sort(sortVersions).reverse();
return ( return (
<div class="card elv-4"> <div
<div style="font-weight: bold">{name}</div> class="card margin"
<ul class="list"> onClick={"window.location.href = '/package/" + name + "'"}
{versions.map((version) => ( >
<div class="card-body">
<h4 class="card-title">
{name} <span class="badge">{sorted[0]}</span>
</h4>
<h5 class="card-subtitle">By {author}</h5>
<div class="card-text">
{/* {versions.map((version) => (
<li>{version}</li> <li>{version}</li>
))} ))} */}
</ul> {description}
</div>
</div>
</div> </div>
); );
} }
export default async function index() { import { Main, Menu } from "./_default.tsx";
const packages = await DB.find({});
export default async function index({ search }: any) {
let packages: IPackage[] = [];
if (search && search !== "") {
packages = await DB.package.find({
name: RegExp(`${search}`),
});
} else {
packages = await DB.package.find({});
}
return ( return (
<Base> <Base>
<div class="container"> <Main>
<form method="GET" action="./">
<div class="form-group margin">
{/* <label for="searchInput">Search</label> */}
<div style="display:flex">
<input
placeholder="Search..."
class="input-block"
type="text"
id="searchInput"
name="q"
value={search}
/>
<button>Submit</button>
</div>
</div>
</form>
{packages.map((pkg) => ( {packages.map((pkg) => (
<Package pkg={pkg} /> <Package pkg={pkg} />
))} ))}
</div> </Main>
<Menu>
<ul>
<li>Item</li>
</ul>
</Menu>
</Base> </Base>
); );
} }

View File

@ -0,0 +1,70 @@
/// <reference path="../types/jsx.d.ts" />
import { React, Fragment, Marked } from "../deps.ts";
import Base from "./_base.tsx";
import DB, { IPackage } from "../db.ts";
import { sortVersions, getFile } from "../utils.ts";
// function Package({ pkg }: { pkg: IPackage }) {
// const { name, versions, author } = pkg;
// const sorted = versions.sort(sortVersions);
// return (
// <div
// class="card margin"
// onClick={"window.location.href = '/package/" + name + "'"}
// >
// <div class="card-body">
// <h4 class="card-title">{name}</h4>
// <ul class="card-text">
// {versions.map((version) => (
// <li>{version}</li>
// ))}
// {author} {sorted[0]}
// </ul>
// </div>
// </div>
// );
// }
import { Main, Menu } from "./_default.tsx";
export default async function index({ packageName }: any) {
const pkg = await DB.package.findOne({ name: packageName });
if (!pkg)
return (
<Base>
<h1>Not found</h1>
</Base>
);
const readmeContent = await getFile(
packageName,
undefined,
"README.md"
).then((res) => {
if (res)
return Marked.parse(new TextDecoder().decode(res)).content as string;
else return undefined;
});
return (
<Base>
<Main>
<h2 style="margin-bottom: 0">Package: {pkg.name}</h2>
<h4 class="text-muted" style="margin-top: 0; margin-left: .5rem">
By {pkg.author}
</h4>
{readmeContent !== undefined ? (
<div innerHTML={readmeContent} />
) : (
<div class="alert alert-warning">No README.md found!</div>
)}
</Main>
<Menu></Menu>
</Base>
);
}

View File

@ -0,0 +1,17 @@
.site {
display: grid;
grid-template-areas: "main menu";
grid-template-columns: 2fr 1fr;
gap: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
@media only screen and (max-width: 64rem) {
.site {
grid-template-columns: 1fr;
grid-template-areas:
"menu"
"main";
}
}