forked from security/hakurei
cmd/pkgserver: minimum viable frontend
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ go.work.sum
|
||||
|
||||
# go generate
|
||||
/cmd/hakurei/LICENSE
|
||||
/cmd/pkgserver/.sass-cache
|
||||
/internal/pkg/testdata/testtool
|
||||
/internal/rosa/hakurei_current.tar.gz
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ func WritePayload(w http.ResponseWriter, payload any) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(payload)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc -p ui/static"
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("pkgserver: ")
|
||||
@@ -36,6 +37,7 @@ func main() {
|
||||
return err
|
||||
}
|
||||
cache, err := pkg.Open(ctx, msg, 0, baseDir)
|
||||
defer cache.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts"
|
||||
//go:embed ui/*
|
||||
var content embed.FS
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<title>Hakurei PkgServer</title>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||
<script src="static/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hakurei PkgServer</h1>
|
||||
|
||||
<table id="pkg-list">
|
||||
<tr><th>Status</th><th>Name</th><th>Version</th></tr>
|
||||
<tr><td>Loading...</td></tr>
|
||||
</table>
|
||||
<p>Showing entries <span id="entry-counter"></span>.</p>
|
||||
<span class="bottom-nav"><a href="javascript:prevPage()">« Previous</a> <span id="page-number">1</span> <a href="javascript:nextPage()">Next »</a></span>
|
||||
@@ -22,5 +21,7 @@
|
||||
<option value="100">100</option>
|
||||
</select></span>
|
||||
</body>
|
||||
<footer>© <a href="https://hakurei.app/">Hakurei</a>. Licensed under the MIT license.</footer>
|
||||
<footer>
|
||||
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -1,67 +1,136 @@
|
||||
"use strict";
|
||||
var PackageEntry = /** @class */ (function () {
|
||||
function PackageEntry() {
|
||||
}
|
||||
return PackageEntry;
|
||||
}());
|
||||
var State = /** @class */ (function () {
|
||||
function State() {
|
||||
this.entriesPerPage = 10;
|
||||
this.currentPage = 1;
|
||||
this.entryIndex = 0;
|
||||
this.loadedEntries = [];
|
||||
}
|
||||
State.prototype.getEntriesPerPage = function () {
|
||||
class PackageIndexEntry {
|
||||
name;
|
||||
description;
|
||||
website;
|
||||
version;
|
||||
report;
|
||||
}
|
||||
function toHTML(entry) {
|
||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : "";
|
||||
let d = entry.description != null ? `<p>${escapeHtml(entry.description)}</p>` : "";
|
||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : "";
|
||||
let r = entry.report != null ? `<a href="${encodeURI(entry.report)}">Log</a>` : "";
|
||||
let row = (document.createElement('tr'));
|
||||
row.innerHTML = `<td>
|
||||
<h2>${escapeHtml(entry.name)} ${v}</h2>
|
||||
${d}
|
||||
${w}
|
||||
${r}
|
||||
</td>`;
|
||||
return row;
|
||||
}
|
||||
const API_VERSION = 1;
|
||||
const ENDPOINT = `/api/v${API_VERSION}`;
|
||||
class InfoPayload {
|
||||
count;
|
||||
hakurei_version;
|
||||
}
|
||||
async function infoRequest() {
|
||||
const res = await fetch(`${ENDPOINT}/info`);
|
||||
const res_1 = await res.json();
|
||||
return res_1;
|
||||
}
|
||||
class GetPayload {
|
||||
count;
|
||||
values;
|
||||
}
|
||||
var SortOrders;
|
||||
(function (SortOrders) {
|
||||
SortOrders[SortOrders["DeclarationAscending"] = 0] = "DeclarationAscending";
|
||||
SortOrders[SortOrders["DeclarationDescending"] = 1] = "DeclarationDescending";
|
||||
SortOrders[SortOrders["NameAscending"] = 2] = "NameAscending";
|
||||
SortOrders[SortOrders["NameDescending"] = 3] = "NameDescending";
|
||||
})(SortOrders || (SortOrders = {}));
|
||||
async function getRequest(limit, index, sort) {
|
||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`);
|
||||
const res_1 = await res.json();
|
||||
return res_1;
|
||||
}
|
||||
class State {
|
||||
entriesPerPage = 10;
|
||||
currentPage = 1;
|
||||
entryIndex = 0;
|
||||
maxEntries = 100;
|
||||
getEntriesPerPage() {
|
||||
return this.entriesPerPage;
|
||||
};
|
||||
State.prototype.setEntriesPerPage = function (entriesPerPage) {
|
||||
}
|
||||
setEntriesPerPage(entriesPerPage) {
|
||||
this.entriesPerPage = entriesPerPage;
|
||||
this.updateRange();
|
||||
};
|
||||
State.prototype.getCurrentPage = function () {
|
||||
if (this.currentPage > this.getMaxPage()) {
|
||||
this.setCurrentPage(this.getMaxPage());
|
||||
}
|
||||
}
|
||||
getCurrentPage() {
|
||||
return this.currentPage;
|
||||
};
|
||||
State.prototype.setCurrentPage = function (page) {
|
||||
}
|
||||
setCurrentPage(page) {
|
||||
this.currentPage = page;
|
||||
document.getElementById("page-number").innerText = String(this.currentPage);
|
||||
this.updateRange();
|
||||
};
|
||||
State.prototype.getEntryIndex = function () {
|
||||
this.setEntryIndex((this.getCurrentPage() - 1) * this.getEntriesPerPage());
|
||||
document.getElementById("page-number").innerText = String(this.getCurrentPage());
|
||||
}
|
||||
getEntryIndex() {
|
||||
return this.entryIndex;
|
||||
};
|
||||
State.prototype.setEntryIndex = function (entryIndex) {
|
||||
}
|
||||
setEntryIndex(entryIndex) {
|
||||
this.entryIndex = entryIndex;
|
||||
this.updateRange();
|
||||
};
|
||||
State.prototype.getLoadedEntries = function () {
|
||||
return this.loadedEntries;
|
||||
};
|
||||
State.prototype.getMaxPage = function () {
|
||||
return this.loadedEntries.length / this.entriesPerPage;
|
||||
};
|
||||
State.prototype.updateRange = function () {
|
||||
var max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length);
|
||||
document.getElementById("entry-counter").innerText = "".concat(this.entryIndex, "-").concat(max, " of ").concat(this.loadedEntries.length);
|
||||
};
|
||||
return State;
|
||||
}());
|
||||
var STATE;
|
||||
this.updateListings();
|
||||
}
|
||||
getMaxEntries() {
|
||||
return this.maxEntries;
|
||||
}
|
||||
setMaxEntries(max) {
|
||||
this.maxEntries = max;
|
||||
}
|
||||
getMaxPage() {
|
||||
return Math.ceil(this.getMaxEntries() / this.getEntriesPerPage());
|
||||
}
|
||||
updateRange() {
|
||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxEntries());
|
||||
document.getElementById("entry-counter").innerText = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxEntries()}`;
|
||||
}
|
||||
updateListings() {
|
||||
getRequest(this.getEntriesPerPage(), this.entryIndex, 0)
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list");
|
||||
table.innerHTML = '';
|
||||
for (let i = 0; i < res.count; i++) {
|
||||
table.appendChild(toHTML(res.values[i]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let STATE;
|
||||
function prevPage() {
|
||||
var current = STATE.getCurrentPage();
|
||||
let current = STATE.getCurrentPage();
|
||||
if (current > 1) {
|
||||
STATE.setCurrentPage(STATE.getCurrentPage() - 1);
|
||||
}
|
||||
}
|
||||
function nextPage() {
|
||||
var current = STATE.getCurrentPage();
|
||||
let current = STATE.getCurrentPage();
|
||||
if (current < STATE.getMaxPage()) {
|
||||
STATE.setCurrentPage(STATE.getCurrentPage() + 1);
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function escapeHtml(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
STATE = new State();
|
||||
infoRequest()
|
||||
.then(res => {
|
||||
STATE.setMaxEntries(res.count);
|
||||
document.getElementById("hakurei-version").innerText = res.hakurei_version;
|
||||
STATE.updateRange();
|
||||
document.getElementById("count").addEventListener("change", function (event) {
|
||||
STATE.updateListings();
|
||||
});
|
||||
document.getElementById("count").addEventListener("change", (event) => {
|
||||
STATE.setEntriesPerPage(parseInt(event.target.value));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,74 @@
|
||||
"use strict"
|
||||
class PackageIndexEntry {
|
||||
name: string
|
||||
description: string | null
|
||||
website: string | null
|
||||
version: string | null
|
||||
report: string | null
|
||||
}
|
||||
function toHTML(entry: PackageIndexEntry): HTMLTableRowElement {
|
||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
|
||||
let d = entry.description != null ? `<p>${escapeHtml(entry.description)}</p>` : ""
|
||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
|
||||
let r = entry.report != null ? `<a href="${encodeURI(entry.report)}">Log</a>` : ""
|
||||
let row = <HTMLTableRowElement>(document.createElement('tr'))
|
||||
row.innerHTML = `<td>
|
||||
<h2>${escapeHtml(entry.name)} ${v}</h2>
|
||||
${d}
|
||||
${w}
|
||||
${r}
|
||||
</td>`
|
||||
return row
|
||||
}
|
||||
|
||||
class PackageEntry {
|
||||
const API_VERSION = 1
|
||||
const ENDPOINT = `/api/v${API_VERSION}`
|
||||
class InfoPayload {
|
||||
count: number
|
||||
hakurei_version: string
|
||||
}
|
||||
|
||||
async function infoRequest(): Promise<InfoPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/info`)
|
||||
const res_1 = await res.json()
|
||||
return res_1 as InfoPayload
|
||||
}
|
||||
class GetPayload {
|
||||
count: number
|
||||
values: PackageIndexEntry[]
|
||||
}
|
||||
|
||||
enum SortOrders {
|
||||
DeclarationAscending = 0,
|
||||
DeclarationDescending,
|
||||
NameAscending,
|
||||
NameDescending
|
||||
}
|
||||
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
|
||||
const res_1 = await res.json()
|
||||
return res_1 as GetPayload
|
||||
}
|
||||
class State {
|
||||
entriesPerPage: number = 10
|
||||
currentPage: number = 1
|
||||
entryIndex: number = 0
|
||||
loadedEntries: PackageEntry[] = []
|
||||
maxEntries: number = 0
|
||||
getEntriesPerPage(): number {
|
||||
return this.entriesPerPage
|
||||
}
|
||||
setEntriesPerPage(entriesPerPage: number) {
|
||||
this.entriesPerPage = entriesPerPage
|
||||
this.updateRange()
|
||||
if (this.currentPage > this.getMaxPage()) {
|
||||
this.setCurrentPage(this.getMaxPage())
|
||||
}
|
||||
}
|
||||
getCurrentPage(): number {
|
||||
return this.currentPage
|
||||
}
|
||||
setCurrentPage(page: number) {
|
||||
this.currentPage = page
|
||||
document.getElementById("page-number").innerText = String(this.currentPage)
|
||||
this.updateRange()
|
||||
this.setEntryIndex((this.getCurrentPage() - 1) * this.getEntriesPerPage())
|
||||
document.getElementById("page-number").innerText = String(this.getCurrentPage())
|
||||
}
|
||||
getEntryIndex(): number {
|
||||
return this.entryIndex
|
||||
@@ -29,17 +76,32 @@ class State {
|
||||
setEntryIndex(entryIndex: number) {
|
||||
this.entryIndex = entryIndex
|
||||
this.updateRange()
|
||||
this.updateListings()
|
||||
}
|
||||
getLoadedEntries(): PackageEntry[] {
|
||||
return this.loadedEntries
|
||||
getMaxEntries(): number {
|
||||
return this.maxEntries
|
||||
}
|
||||
setMaxEntries(max: number) {
|
||||
this.maxEntries = max
|
||||
}
|
||||
getMaxPage(): number {
|
||||
return this.loadedEntries.length / this.entriesPerPage
|
||||
return Math.ceil(this.getMaxEntries() / this.getEntriesPerPage())
|
||||
}
|
||||
updateRange() {
|
||||
let max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length)
|
||||
document.getElementById("entry-counter").innerText = `${this.entryIndex}-${max} of ${this.loadedEntries.length}`
|
||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxEntries())
|
||||
document.getElementById("entry-counter").innerText = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxEntries()}`
|
||||
}
|
||||
updateListings() {
|
||||
getRequest(this.getEntriesPerPage(), this.entryIndex, 0)
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list")
|
||||
table.innerHTML = ''
|
||||
for(let i = 0; i < res.count; i++) {
|
||||
table.appendChild(toHTML(res.values[i]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let STATE: State
|
||||
@@ -57,9 +119,25 @@ function nextPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
STATE = new State()
|
||||
infoRequest()
|
||||
.then(res => {
|
||||
STATE.setMaxEntries(res.count)
|
||||
document.getElementById("hakurei-version").innerText = res.hakurei_version
|
||||
STATE.updateRange()
|
||||
STATE.updateListings()
|
||||
})
|
||||
|
||||
document.getElementById("count").addEventListener("change", (event) => {
|
||||
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
|
||||
})
|
||||
|
||||
5
cmd/pkgserver/ui/static/tsconfig.json
Normal file
5
cmd/pkgserver/ui/static/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user