mirror of
https://github.com/mickael-kerjean/filestash
synced 2026-01-04 06:43:18 +01:00
feature (admin): migration of the frontend app for admin console
This commit is contained in:
parent
0d3e86db5a
commit
f14a07e13b
31 changed files with 322 additions and 110 deletions
12
public/assets/css/designsystem_skeleton.css
Normal file
12
public/assets/css/designsystem_skeleton.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.component_skeleton {
|
||||
height: 30px;
|
||||
background: linear-gradient(110deg, rgba(0,0,0,0.02) 8%, rgba(0,0,0,0.04) 18%, rgba(0,0,0,0.02) 33%);
|
||||
border-radius: 5px;
|
||||
background-size: 200% 100%;
|
||||
animation: 3s skeleton_shine linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes skeleton_shine {
|
||||
to { background-position-x: -200%; }
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
@import url("./designsystem_container.css");
|
||||
@import url("./designsystem_box.css");
|
||||
@import url("./designsystem_darkmode.css");
|
||||
@import url("./designsystem_skeleton.css");
|
||||
@import url("./designsystem_utils.css");
|
||||
|
||||
/* latin-ext */
|
||||
|
|
|
|||
17
public/boot/common.js
Normal file
17
public/boot/common.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export function $error(msg) {
|
||||
const $code = document.createElement("code");
|
||||
$code.style.display = "block";
|
||||
$code.style.margin = "20px 0";
|
||||
$code.style.fontSize = "1.3rem";
|
||||
$code.style.padding = "0 10% 0 10%";
|
||||
$code.textContent = msg;
|
||||
|
||||
const $img = document.createElement("img");
|
||||
$img.setAttribute("src", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABQAQMAAADcLOLWAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAFlJREFUeF69zrERgCAQBdElMqQEOtHSuNIohRIMjfjO6DDmB7jZy5YgySQVYDIakIHD1kBPC9Bra5G2Ans0N7iAcOLF+EHvXySpjSBWCDI/3nIdBDihr8m4AcKdbn96jpAHAAAAAElFTkSuQmCC");
|
||||
$img.style.display = "block";
|
||||
$img.style.padding = "20vh 10% 0 10%";
|
||||
|
||||
document.body.innerHTML = "";
|
||||
document.body.appendChild($img);
|
||||
document.body.appendChild($code);
|
||||
}
|
||||
44
public/boot/ctrl_boot_backoffice.js
Normal file
44
public/boot/ctrl_boot_backoffice.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import rxjs, { ajax } from "../lib/rx.js";
|
||||
import { loadScript } from "../helpers/loader.js";
|
||||
import { report } from "../helpers/log.js";
|
||||
import { $error } from "./common.js";
|
||||
|
||||
export default async function main() {
|
||||
try {
|
||||
await Promise.all([
|
||||
setup_device(),
|
||||
setup_blue_death_screen(),
|
||||
setup_history(),
|
||||
]);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const msg = window.navigator.onLine === false ? "OFFLINE" : (err.message || "CAN'T LOAD");
|
||||
report(msg + " - " + (err && err.message), location.href);
|
||||
$error(msg);
|
||||
}
|
||||
}
|
||||
main();
|
||||
|
||||
async function setup_device() {
|
||||
const className = "ontouchstart" in window ? "touch-yes" : "touch-no";
|
||||
document.body.classList.add(className);
|
||||
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
}
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function(e) {
|
||||
e.matches ? document.body.classList.add("dark-mode") : document.body.classList.remove("dark-mode");
|
||||
});
|
||||
}
|
||||
|
||||
async function setup_blue_death_screen() {
|
||||
window.onerror = function(msg, url, lineNo, colNo, error) {
|
||||
report(msg, url, lineNo, colNo, error);
|
||||
$error(msg);
|
||||
};
|
||||
}
|
||||
|
||||
async function setup_history() {
|
||||
window.history.replaceState({}, "");
|
||||
}
|
||||
26
public/boot/ctrl_boot_backoffice.test.js
Normal file
26
public/boot/ctrl_boot_backoffice.test.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import exec from "./ctrl_boot_backoffice.js";
|
||||
|
||||
describe("ctrl::boot", () => {
|
||||
it("runs", async() => {
|
||||
const start = new Date();
|
||||
await exec();
|
||||
expect(new Date() - start).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it("reset the history", () => {
|
||||
expect(window.history.state).toEqual({});
|
||||
});
|
||||
|
||||
it("setup the dom", () => {
|
||||
expect(
|
||||
document.body.classList.contains("touch-no") ||
|
||||
document.body.classList.contains("touch-yes")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("setup the error screen", () => {
|
||||
expect(typeof window.onerror).toBe("function");
|
||||
window.onerror("__MESSAGE__");
|
||||
expect(document.body.outerHTML).toContain("__MESSAGE__");
|
||||
});
|
||||
});
|
||||
|
|
@ -11,7 +11,8 @@ export default async function main() {
|
|||
// setup_cache(), // TODO: dependency on session
|
||||
setup_device(),
|
||||
// setup_sw(), // TODO
|
||||
setup_blue_death_screen()
|
||||
setup_blue_death_screen(),
|
||||
setup_history(),
|
||||
]);
|
||||
// await Config.refresh()
|
||||
|
||||
|
|
@ -137,3 +138,7 @@ async function setup_blue_death_screen() {
|
|||
// }
|
||||
// return window.Chromecast.init();
|
||||
// }
|
||||
|
||||
async function setup_history() {
|
||||
window.history.replaceState({}, "");
|
||||
}
|
||||
9
public/boot/ctrl_boot_frontoffice.test.js
Normal file
9
public/boot/ctrl_boot_frontoffice.test.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import exec from "./ctrl_boot_backoffice.js";
|
||||
|
||||
describe("ctrl::boot", () => {
|
||||
it("runs", async() => {
|
||||
const start = new Date();
|
||||
await exec();
|
||||
expect(new Date() - start).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,43 +1,46 @@
|
|||
import { CSS } from "../helpers/loader.js";
|
||||
|
||||
const isRunningFromAnIframe = window.self !== window.top;
|
||||
const css = await CSS(import.meta.url, "breadcrumb.css");
|
||||
|
||||
class ComponentBreadcrumb extends HTMLDivElement {
|
||||
constructor() {
|
||||
super();
|
||||
if (new window.URL(location.href).searchParams.get("nav") === "false") return null;
|
||||
|
||||
const htmlLogout = isRunningFromAnIframe
|
||||
? ""
|
||||
: `
|
||||
const htmlLogout = isRunningFromAnIframe ? "" : `
|
||||
<a href="/logout" data-link>
|
||||
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODkuODg4IDQ4OS44ODgiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDQ4OS44ODggNDg5Ljg4ODsiPgogIDxwYXRoIGZpbGw9IiM2ZjZmNmYiIGQ9Ik0yNS4zODMsMjkwLjVjLTcuMi03Ny41LDI1LjktMTQ3LjcsODAuOC0xOTIuM2MyMS40LTE3LjQsNTMuNC0yLjUsNTMuNCwyNWwwLDBjMCwxMC4xLTQuOCwxOS40LTEyLjYsMjUuNyAgICBjLTM4LjksMzEuNy02Mi4zLDgxLjctNTYuNiwxMzYuOWM3LjQsNzEuOSw2NSwxMzAuMSwxMzYuOCwxMzguMWM5My43LDEwLjUsMTczLjMtNjIuOSwxNzMuMy0xNTQuNWMwLTQ4LjYtMjIuNS05Mi4xLTU3LjYtMTIwLjYgICAgYy03LjgtNi4zLTEyLjUtMTUuNi0xMi41LTI1LjZsMCwwYzAtMjcuMiwzMS41LTQyLjYsNTIuNy0yNS42YzUwLjIsNDAuNSw4Mi40LDEwMi40LDgyLjQsMTcxLjhjMCwxMjYuOS0xMDcuOCwyMjkuMi0yMzYuNywyMTkuOSAgICBDMTIyLjE4Myw0ODEuOCwzNS4yODMsMzk2LjksMjUuMzgzLDI5MC41eiBNMjQ0Ljg4MywwYy0xOCwwLTMyLjUsMTQuNi0zMi41LDMyLjV2MTQ5LjdjMCwxOCwxNC42LDMyLjUsMzIuNSwzMi41ICAgIHMzMi41LTE0LjYsMzIuNS0zMi41VjMyLjVDMjc3LjM4MywxNC42LDI2Mi44ODMsMCwyNDQuODgzLDB6IiAvPgo8L3N2Zz4K" alt="power">
|
||||
</a>`;
|
||||
const paths = (this.getAttribute("path") || "").split("/");
|
||||
const htmlPathChunks = paths.slice(0, -1).map((chunk, idx) => {
|
||||
</a>
|
||||
`;
|
||||
const paths = (this.getAttribute("path") || "").replace(new RegExp("/$"), "").split("/");
|
||||
console.log(paths);
|
||||
const htmlPathChunks = paths.map((chunk, idx) => {
|
||||
const label = idx === 0 ? "Filestash" : chunk;
|
||||
const link = paths.slice(0, idx).join("/") + "/";
|
||||
// const minify = () => {
|
||||
// if (idx === 0) return false;
|
||||
// else if (paths.length <= (document.body.clientWidth > 800 ? 5 : 4)) return false;
|
||||
// else if (idx > paths.length - (document.body.clientWidth > 1000 ? 4 : 3)) return false;
|
||||
// return true;
|
||||
// };
|
||||
const limitSize = (word) => { // TODO
|
||||
const link = paths.slice(0, idx + 1).join("/") + "/";
|
||||
const minify = function() {
|
||||
if (idx === 0) return false;
|
||||
else if (paths.length <= (document.body.clientWidth > 800 ? 5 : 4)) return false;
|
||||
else if (idx > paths.length - (document.body.clientWidth > 1000 ? 4 : 3)) return false;
|
||||
return true;
|
||||
}();
|
||||
const limitSize = (word, highlight = false) => {
|
||||
if (highlight === true && word.length > 30) return word.substring(0, 12).trim() + "..." +
|
||||
word.substring(word.length - 10, word.length).trim();
|
||||
else if (word.length > 27) return word.substring(0, 20).trim() + "...";
|
||||
return word;
|
||||
|
||||
};
|
||||
const isLast = idx === paths.length - 1;
|
||||
if (isLast) {
|
||||
return `
|
||||
if (isLast) return `
|
||||
<div class="component_path-element n${idx}">
|
||||
<div class="li component_path-element-wrapper">
|
||||
<div class="label">
|
||||
<div>${label}</div>
|
||||
<div>${limitSize(label)}</div>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
return `
|
||||
<div class="component_path-element n${idx}">
|
||||
<div class="li component_path-element-wrapper">
|
||||
|
|
@ -56,7 +59,6 @@ class ComponentBreadcrumb extends HTMLDivElement {
|
|||
}
|
||||
|
||||
async render({ htmlLogout, htmlPathChunks }) {
|
||||
const css = await CSS(import.meta.url, "breadcrumb.css");
|
||||
this.innerHTML = `
|
||||
<div class="component_breadcrumb" role="navigation">
|
||||
<style>${css}</style>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ class Loader extends window.HTMLElement {
|
|||
constructor() {
|
||||
super();
|
||||
this.timeout = window.setTimeout(() => {
|
||||
this.innerHTML = this.render();
|
||||
this.innerHTML = this.render({
|
||||
inline: this.hasAttribute("inlined"),
|
||||
});
|
||||
}, parseInt(this.getAttribute("delay")) || 0);
|
||||
}
|
||||
|
||||
|
|
@ -13,7 +15,12 @@ class Loader extends window.HTMLElement {
|
|||
window.clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
render() {
|
||||
render({ inline }) {
|
||||
const fixedCss = `
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(50% - 200px);`
|
||||
return `
|
||||
<div class="component_loader">
|
||||
<svg width="120px" height="120px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
|
|
@ -27,11 +34,8 @@ class Loader extends window.HTMLElement {
|
|||
<style>
|
||||
.component_loader{
|
||||
text-align: center;
|
||||
margin: 50px auto 0 auto;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(50% - 200px);
|
||||
margin: 100px auto 0 auto;
|
||||
${inline ? "" : fixedCss}
|
||||
}
|
||||
</style>
|
||||
</div>`;
|
||||
|
|
|
|||
8
public/components/skeleton.js
Normal file
8
public/components/skeleton.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export function generateSkeleton(n) {
|
||||
let tmpl = `<div class="component_skeleton"></div>`;
|
||||
let html = "";
|
||||
for (let i=0; i<n; i++) {
|
||||
html += tmpl;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
|
@ -14,8 +14,8 @@ export async function CSS(baseURL, ...arrayOfFilenames) {
|
|||
}
|
||||
|
||||
async function loadSingleCSS(baseURL, filename) {
|
||||
const res = await fetch(baseURL.replace(/(.*)\/[^\/]+$/, "$1/") + filename, {
|
||||
cache: "default"
|
||||
const res = await fetch(baseURL.replace(/(.*)\/[^\/]+$/, "$1/") + filename + "?version=" + "__", {
|
||||
cache: "force-cache"
|
||||
});
|
||||
if (res.status !== 200) return `/* ERROR: ${res.status} */`;
|
||||
else if (!res.headers.get("Content-Type").startsWith("text/css")) return `/* ERROR: wrong type, got "${res.headers.get("Content-Type")}"*/`;
|
||||
|
|
|
|||
|
|
@ -6,37 +6,27 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/assets/css/reset.css">
|
||||
<script type="module" src="/components/loader.js"></script>
|
||||
<title></title>
|
||||
<title>Admin Console</title>
|
||||
</head>
|
||||
<body>
|
||||
<div role="main" id="app">
|
||||
<component-loader delay="500"></component-loader>
|
||||
</div>
|
||||
<script type="module">
|
||||
import main from "/lib/skeleton/index.js";
|
||||
const routes = {
|
||||
"/login": "/pages/ctrl_connectpage.js",
|
||||
"/logout": "/pages/ctrl_logout.js",
|
||||
|
||||
"/": "/pages/ctrl_homepage.js",
|
||||
"/files/.*": "/pages/ctrl_filespage.js",
|
||||
"/view/.*": "/pages/ctrl_viewerpage.js",
|
||||
// /tags/.* -> "pages/ctrl_tags.js",
|
||||
// /s/.* -> "/pages/ctrl_share.js",
|
||||
|
||||
"/admin/backend": "/pages/adminpage/ctrl_backend.js",
|
||||
"/admin/settings": "/pages/adminpage/ctrl_settings.js",
|
||||
"/admin/logs": "/pages/adminpage/ctrl_logger.js",
|
||||
"/admin/about": "/pages/adminpage/ctrl_about.js",
|
||||
"/admin/setup": "/pages/adminpage/ctrl_setup.js",
|
||||
"/admin/": "/pages/ctrl_adminpage.js",
|
||||
|
||||
"": "/pages/ctrl_notfound.js",
|
||||
};
|
||||
main(document.getElementById("app"), routes, {
|
||||
spinner: `<component-loader></component-loader>`,
|
||||
beforeStart: import("/pages/ctrl_boot.js"),
|
||||
});
|
||||
import main from "/lib/skeleton/index.js";
|
||||
const routes = {
|
||||
"/admin/backend": "/pages/adminpage/ctrl_backend.js",
|
||||
"/admin/settings": "/pages/adminpage/ctrl_settings.js",
|
||||
"/admin/logs": "/pages/adminpage/ctrl_logger.js",
|
||||
"/admin/about": "/pages/adminpage/ctrl_about.js",
|
||||
"/admin/setup": "/pages/adminpage/ctrl_setup.js",
|
||||
"/admin/": "/pages/ctrl_adminpage.js",
|
||||
"": "/pages/ctrl_notfound.js",
|
||||
};
|
||||
main(document.getElementById("app"), routes, {
|
||||
spinner: `<component-loader></component-loader>`,
|
||||
beforeStart: import("/boot/ctrl_boot_backoffice.js"),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="module" src="/components/modal.js"></script>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export { onDestroy } from "./lifecycle.js";
|
|||
let pageLoader;
|
||||
|
||||
export default async function($root, routes, opts = {}) {
|
||||
window.addEventListener("pagechange", async() => {
|
||||
window.addEventListener("pagechange", async () => {
|
||||
try {
|
||||
const route = currentRoute(routes, "");
|
||||
const [ctrl] = await Promise.all([
|
||||
|
|
@ -17,6 +17,7 @@ export default async function($root, routes, opts = {}) {
|
|||
if (typeof ctrl !== "function") throw new Error(`Unknown route for ${route}`);
|
||||
pageLoader = ctrl(createRender($root));
|
||||
} catch (err) {
|
||||
console.error("skeleton::index.js", err);
|
||||
window.onerror && window.onerror(err.message);
|
||||
}
|
||||
});
|
||||
|
|
@ -33,16 +34,17 @@ async function load(route, opts) {
|
|||
ctrl = route;
|
||||
} else if (typeof route === "string") {
|
||||
let spinnerID;
|
||||
if (pageLoader && typeof pageLoader.then === "function") {
|
||||
const pageLoaderCallback = await pageLoader;
|
||||
if (typeof pageLoaderCallback !== "function") throw new Error("expected a function as returned value");
|
||||
spinnerID = setTimeout(() => pageLoaderCallback(route), spinnerTime);
|
||||
if (pageLoader && typeof pageLoader === "function") {
|
||||
spinnerID = setTimeout(() => pageLoader(createRender($root)), spinnerTime);
|
||||
} else if (typeof spinner === "string") {
|
||||
spinnerID = setTimeout(() => $root.innerHTML = spinner, spinnerTime);
|
||||
}
|
||||
const module = await import("../.." + route);
|
||||
clearTimeout(spinnerID);
|
||||
if (typeof module.default !== "function") throw new Error(`missing default export on ${route}`);
|
||||
if (typeof module.default !== "function") {
|
||||
console.error(module, module.default);
|
||||
throw new Error(`missing default export on ${route}`);
|
||||
}
|
||||
ctrl = module.default;
|
||||
}
|
||||
return ctrl;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ export async function init($root) {
|
|||
}
|
||||
|
||||
export function navigate(href) {
|
||||
window.history.pushState("", "", href);
|
||||
window.history.pushState({
|
||||
...window.history,
|
||||
previous: window.location.pathname,
|
||||
}, "", href);
|
||||
triggerPageChange();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import { CSS } from "../../helpers/loader.js";
|
|||
import transition from "./animate.js";
|
||||
|
||||
import { get as getRelease } from "./model_release.js";
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithShell from "./decorator_sidemenu.js";
|
||||
import AdminHOC from "./decorator.js";
|
||||
|
||||
export default AdminOnly(WithShell(async function(render) {
|
||||
export default AdminHOC(async function(render) {
|
||||
const css = await CSS(import.meta.url, "ctrl_about.css");
|
||||
const $page = createElement(`
|
||||
<div class="component_page_about">
|
||||
|
|
@ -22,4 +21,4 @@ export default AdminOnly(WithShell(async function(render) {
|
|||
rxjs.map(({ html }) => html),
|
||||
stateMutation(qs($page, "[data-bind=\"about\"]"), "innerHTML")
|
||||
));
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,24 +6,24 @@ import { CSS } from "../../helpers/loader.js";
|
|||
import backend$ from "../connectpage/model_backend.js";
|
||||
|
||||
import transition from "./animate.js";
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithShell from "./decorator_sidemenu.js";
|
||||
import AdminHOC from "./decorator.js";
|
||||
|
||||
export default AdminOnly(WithShell(function(render) {
|
||||
export default AdminHOC(function(render) {
|
||||
const $page = createElement(`
|
||||
<div class="component_dashboard sticky">
|
||||
<div data-bind="backend"></div>
|
||||
|
||||
<h2>Authentication Middleware</h2>
|
||||
<div data-bind="authentation_middleware"></div>
|
||||
<div data-bind="authentication_middleware"></div>
|
||||
|
||||
<style>${css}</style>
|
||||
</div>
|
||||
`);
|
||||
render(transition($page));
|
||||
|
||||
componentStorageBackend(createRender(qs($page, "[data-bind=\"backend\"]")));
|
||||
}));
|
||||
componentStorageBackend(createRender(qs($page, `[data-bind="backend"]`)));
|
||||
componentAuthenticationMiddleware(createRender(qs($page, `[data-bind="authentication_middleware"]`)));
|
||||
});
|
||||
|
||||
function componentStorageBackend(render) {
|
||||
const $page = createElement(`
|
||||
|
|
@ -51,4 +51,50 @@ function componentStorageBackend(render) {
|
|||
));
|
||||
}
|
||||
|
||||
function componentAuthenticationMiddleware(render) {
|
||||
const $page = createElement(`
|
||||
<div class="box-container">
|
||||
<div class="box-item pointer no-select">
|
||||
<div>admin <span class="no-select">
|
||||
<span class="icon">+</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-item pointer no-select">
|
||||
<div>htpasswd <span class="no-select">
|
||||
<span class="icon">+</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-item pointer no-select">
|
||||
<div>ldap <span class="no-select">
|
||||
<span class="icon">+</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-item pointer no-select active">
|
||||
<div>openid <span class="no-select">
|
||||
<span class="icon">
|
||||
<img class="component_icon" draggable="false" src="/assets/icons/delete.svg" alt="delete">
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-item pointer no-select">
|
||||
<div>passthrough <span class="no-select">
|
||||
<span class="icon">+</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-item pointer no-select">
|
||||
<div>saml <span class="no-select">
|
||||
<span class="icon">+</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
}
|
||||
|
||||
const css = await CSS(import.meta.url, "ctrl_backend.css");
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import rxjs, { effect, stateMutation, applyMutation } from "../../lib/rx.js";
|
|||
import { qs } from "../../lib/dom.js";
|
||||
import { createForm } from "../../lib/form.js";
|
||||
import { formTmpl } from "../../components/form.js";
|
||||
import { generateSkeleton } from "../../components/skeleton.js";
|
||||
|
||||
import transition from "./animate.js";
|
||||
import { renderLeaf } from "./helper_form.js";
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithShell from "./decorator_sidemenu.js";
|
||||
import AdminHOC from "./decorator.js";
|
||||
import Log from "./model_log.js";
|
||||
import Audit from "./model_audit.js";
|
||||
import { get as getConfig } from "./model_config.js";
|
||||
|
|
@ -16,8 +16,8 @@ function Page(render) {
|
|||
const $page = createElement(`
|
||||
<div class="component_logpage sticky">
|
||||
<h2>Logging</h2>
|
||||
<div class="component_logger"></div>
|
||||
<div class="component_logviewer"></div>
|
||||
<div class="component_logger"></div>
|
||||
|
||||
<h2>Activity Report</h2>
|
||||
<div class="component_reporter"></div>
|
||||
|
|
@ -30,10 +30,14 @@ function Page(render) {
|
|||
componentAuditor(createRender($page.querySelector(".component_reporter")));
|
||||
}
|
||||
|
||||
export default AdminOnly(WithShell(Page));
|
||||
export default AdminHOC(Page);
|
||||
|
||||
function componentLogForm(render) {
|
||||
const $form = createElement("<form></form>");
|
||||
const $form = createElement(`
|
||||
<form style="min-height: 240px; margin-top:20px;">
|
||||
${generateSkeleton(4)}
|
||||
</form>
|
||||
`);
|
||||
|
||||
render($form);
|
||||
|
||||
|
|
@ -43,14 +47,14 @@ function componentLogForm(render) {
|
|||
rxjs.map((formSpec) => createForm(formSpec, formTmpl({ renderLeaf }))),
|
||||
rxjs.mergeMap((promise) => rxjs.from(promise)),
|
||||
rxjs.map(($form) => [$form]),
|
||||
applyMutation($form, "appendChild")
|
||||
applyMutation($form, "replaceChildren")
|
||||
));
|
||||
|
||||
// TODO feature2: response to form change
|
||||
}
|
||||
|
||||
function componentLogViewer(render) {
|
||||
const $page = createElement("<pre>t</pre>");
|
||||
const $page = createElement(`<pre style="height:350px; max-height: 350px">…</pre>`);
|
||||
render($page);
|
||||
|
||||
effect(Log.get().pipe(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default async function(render) {
|
|||
const css = await CSS(import.meta.url, "ctrl_login.css");
|
||||
const $form = createElement(`
|
||||
<div class="component_container component_page_adminlogin">
|
||||
<style>.component_page_adminlogin{ visibility: hidden; } </style>
|
||||
<style>${css}</style>
|
||||
<form>
|
||||
<div class="input_group">
|
||||
<input type="password" name="password" placeholder="Password" class="component_input" autocomplete>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@ import rxjs, { effect, applyMutation } from "../../lib/rx.js";
|
|||
import { qs, qsa } from "../../lib/dom.js";
|
||||
import { createForm, mutateForm } from "../../lib/form.js";
|
||||
import { formTmpl } from "../../components/form.js";
|
||||
import { generateSkeleton } from "../../components/skeleton.js";
|
||||
|
||||
import transition from "./animate.js";
|
||||
import { renderLeaf } from "./helper_form.js";
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithShell from "./decorator_sidemenu.js";
|
||||
import AdminHOC from "./decorator.js";
|
||||
import { get as getConfig, save as saveConfig } from "./model_config.js";
|
||||
|
||||
export default AdminOnly(WithShell(function(render) {
|
||||
export default AdminHOC(function(render) {
|
||||
const $container = createElement(`
|
||||
<div class="component_settingspage sticky">
|
||||
<form data-bind="form" class="formbuilder"></form>
|
||||
<form data-bind="form" class="formbuilder">
|
||||
<h2>…</h2>
|
||||
${generateSkeleton(10)}
|
||||
</form>
|
||||
</div>
|
||||
`);
|
||||
render(transition($container));
|
||||
|
|
@ -36,16 +39,16 @@ export default AdminOnly(WithShell(function(render) {
|
|||
</div>
|
||||
`);
|
||||
},
|
||||
renderLeaf
|
||||
renderLeaf, autocomplete: false,
|
||||
});
|
||||
|
||||
// feature: setup the form
|
||||
const setup$ = config$.pipe(
|
||||
rxjs.mergeMap((formSpec) => createForm(formSpec, tmpl)),
|
||||
rxjs.map(($form) => [$form]),
|
||||
applyMutation(qs($container, "[data-bind=\"form\"]"), "appendChild"),
|
||||
applyMutation(qs($container, "[data-bind=\"form\"]"), "replaceChildren"),
|
||||
rxjs.share(),
|
||||
)
|
||||
);
|
||||
effect(setup$);
|
||||
|
||||
// feature: handle form change
|
||||
|
|
@ -65,4 +68,4 @@ export default AdminOnly(WithShell(function(render) {
|
|||
rxjs.map(([formState, formSpec]) => mutateForm(formSpec, formState)),
|
||||
saveConfig(),
|
||||
));
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
19
public/pages/adminpage/decorator.js
Normal file
19
public/pages/adminpage/decorator.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithShell from "./decorator_sidemenu.js";
|
||||
|
||||
import "../../components/loader.js";
|
||||
|
||||
export default function HOC(ctrlPage) {
|
||||
const ctrlLoading = (render) => {
|
||||
render(createElement(`<component-loader inlined></component-loader>`));
|
||||
};
|
||||
|
||||
return (render) => {
|
||||
AdminOnly(WithShell(ctrlPage))(render);
|
||||
|
||||
return (render) => {
|
||||
WithShell(ctrlLoading)(render);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,11 @@ import { isSaving } from "./model_config.js";
|
|||
import "../../components/icon.js";
|
||||
|
||||
export default function(ctrl) {
|
||||
return async (render) => {
|
||||
return async function(render) {
|
||||
const css = await CSS(import.meta.url, "decorator_sidemenu.css", "index.css");
|
||||
const $page = createElement(`
|
||||
<div class="component_page_admin">
|
||||
<style id="adminpage::decorator_sidemenu">${css}</style>
|
||||
<style>${css}</style>
|
||||
<div class="component_menu_sidebar no-select">
|
||||
<a class="header" href="/">
|
||||
<svg class="arrow_left" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import rxjs from "../../lib/rx.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
|
||||
const sessionSubject$ = new rxjs.Subject();
|
||||
const sessionSubject$ = new rxjs.Subject(1);
|
||||
|
||||
const adminSession$ = rxjs.merge(
|
||||
sessionSubject$,
|
||||
|
|
@ -10,9 +10,8 @@ const adminSession$ = rxjs.merge(
|
|||
rxjs.mergeMap(() => ajax({ url: "/admin/api/session", responseType: "json" })),
|
||||
rxjs.map(({ responseJSON }) => responseJSON.result),
|
||||
rxjs.distinctUntilChanged(),
|
||||
rxjs.shareReplay(1)
|
||||
)
|
||||
);
|
||||
).pipe(rxjs.shareReplay(1));
|
||||
|
||||
export function isAdmin$() {
|
||||
return adminSession$;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import rxjs from "../../lib/rx.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
|
||||
let release$ = ajax({
|
||||
const release$ = ajax({
|
||||
url: "/about",
|
||||
responseType: "text"
|
||||
}).pipe(rxjs.shareReplay(1));
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
overflow-x: auto;
|
||||
}
|
||||
.component_page_connection_form div.buttons button {
|
||||
width: 100%;
|
||||
min-width: 110px;
|
||||
padding: 8px 5px;
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import { createElement, navigate } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { effect, applyMutation, preventDefault } from "../../lib/rx.js";
|
||||
import { qs, qsa, safe } from "../../lib/dom.js";
|
||||
import { animate, slideYIn } from "../../lib/animate.js";
|
||||
|
|
@ -98,7 +98,9 @@ export default async function(render) {
|
|||
}
|
||||
return json;
|
||||
}),
|
||||
rxjs.mergeMap((creds) => createSession(creds))
|
||||
rxjs.mergeMap((creds) => createSession(creds)),
|
||||
rxjs.tap(() => navigate("/")),
|
||||
// TODO: error with notification
|
||||
));
|
||||
|
||||
render($page);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import componentFilesystem from "./filespage/filesystem.js";
|
|||
import "../components/breadcrumb.js";
|
||||
|
||||
export default function(render) {
|
||||
const currentPath = location.pathname.replace(new RegExp("/files"), "");
|
||||
const currentPath = decodeURIComponent(location.pathname).replace(new RegExp("/files"), "");
|
||||
const $page = createElement(`
|
||||
<div class="component_page_filespage">
|
||||
<div is="component-breadcrumb" path="${currentPath}"></div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { deleteSession } from "../model/session.js";
|
|||
import ctrlError from "./ctrl_error.js";
|
||||
import $loader from "../components/loader.js";
|
||||
|
||||
export default async function(render) {
|
||||
export default function(render) {
|
||||
render($loader);
|
||||
|
||||
effect(deleteSession().pipe(
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import { createElement } from "../lib/skeleton/index.js";
|
|||
import "../components/breadcrumb.js";
|
||||
|
||||
export default function(render) {
|
||||
const currentPath = decodeURIComponent(location.pathname).replace(new RegExp("/view"), "");
|
||||
const $page = createElement(`
|
||||
<div class="component_page_filespage">
|
||||
<div is="component-breadcrumb"></div>
|
||||
<div is="component-breadcrumb" path="${currentPath}" animateOut="test"></div>
|
||||
<div class="page_container">
|
||||
viewerpage - TODO
|
||||
</div>
|
||||
|
|
@ -13,3 +14,13 @@ export default function(render) {
|
|||
`);
|
||||
render($page);
|
||||
}
|
||||
|
||||
const tmp = () => {
|
||||
const { previous } = history.state;
|
||||
if (!previous) return;
|
||||
|
||||
const path = previous.split("/").slice(2)
|
||||
};
|
||||
|
||||
|
||||
const cleanupPath = (path = "") => path.split("/").slice(2);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { qs } from "../../lib/dom.js";
|
|||
|
||||
import { toggle as toggleLoader } from "../../components/loader.js";
|
||||
|
||||
import { createThing, css as cssThing } from "./thing.js";
|
||||
import { createThing, css } from "./thing.js";
|
||||
import { handleError } from "./state.js";
|
||||
import { ls } from "./model_files.js";
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ export default async function(render) {
|
|||
const $page = createElement(`
|
||||
<div class="component_container">
|
||||
<div class="list"></div>
|
||||
<style>${await cssThing}</style>
|
||||
<style>${await css}</style>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
|
|
@ -26,10 +26,14 @@ export default async function(render) {
|
|||
rxjs.tap(({ files }) => {
|
||||
const $fs = document.createDocumentFragment();
|
||||
for (let i = 0; i < files.length && i < 100; i++) {
|
||||
// $node.querySelector(".component_filename .file-details > span").textContent = files[i]["name"];
|
||||
// if (files[i]["type"] === "file") $node.querySelector("a").setAttribute("href", "/view" + path + files[i]["name"]);
|
||||
// else $node.querySelector("a").setAttribute("href", "/files" + path + files[i]["name"] + "/");
|
||||
$fs.appendChild(createThing({ label: files[i].name, link: "/test/" }));
|
||||
if (!files[i]) continue;
|
||||
$fs.appendChild(createThing({
|
||||
name: files[i].name,
|
||||
type: files[i].type,
|
||||
size: files[i].size,
|
||||
time: files[i].time,
|
||||
link: (files[i].type === "file"? "/view" : "/files") + path + files[i].name + (files[i].type === "file" ? "" : "/"),
|
||||
}));
|
||||
}
|
||||
qs($page, ".list").appendChild($fs);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { CSS } from "../../helpers/loader.js";
|
|||
|
||||
const $tmpl = createElement(`
|
||||
<div class="component_thing view-grid not-selected" draggable="true">
|
||||
<a href="/files/Videos/" data-link>
|
||||
<a href="/view/README.org" data-link>
|
||||
<div class="box">
|
||||
<div class="component_checkbox"><input type="checkbox"><span class="indicator"></span></div>
|
||||
<span>
|
||||
|
|
@ -32,17 +32,18 @@ const $tmpl = createElement(`
|
|||
// can toggle links, potentially includes a thumbnail, can be used as a source and target for
|
||||
// drag and drop on other folders and many other non obvious stuff
|
||||
export function createThing({
|
||||
link = null,
|
||||
label = "N/A",
|
||||
name = null,
|
||||
type = "N/A",
|
||||
size = 0,
|
||||
time = null,
|
||||
link = "",
|
||||
permissions = {}
|
||||
}) {
|
||||
const $thing = $tmpl.cloneNode(true);
|
||||
if ($thing instanceof HTMLElement) {
|
||||
const $label = $thing.querySelector(".component_filename .file-details > span");
|
||||
if ($label instanceof HTMLElement) $label.textContent = label;
|
||||
// if (files[i]["type"] === "file") $node.querySelector("a").setAttribute("href", "/view" + path + files[i]["name"]);
|
||||
// else $node.querySelector("a").setAttribute("href", "/files" + path + files[i]["name"] + "/");
|
||||
if ($label instanceof HTMLElement) $label.textContent = name;
|
||||
$thing.querySelector("a").setAttribute("href", link);
|
||||
}
|
||||
return $thing;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue