feature (admin): migration of the frontend app for admin console

This commit is contained in:
Mickael Kerjean 2023-08-18 20:51:17 +10:00
parent 0d3e86db5a
commit f14a07e13b
31 changed files with 322 additions and 110 deletions

View 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%; }
}

View file

@ -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
View 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);
}

View 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({}, "");
}

View 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__");
});
});

View file

@ -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({}, "");
}

View 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);
});
});

View file

@ -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>

View file

@ -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>`;

View 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;
}

View file

@ -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")}"*/`;

View file

@ -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>

View file

@ -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;

View file

@ -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();
}

View file

@ -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")
));
}));
});

View file

@ -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");

View file

@ -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(

View file

@ -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>

View file

@ -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(),
));
}));
});

View 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);
}
}
}

View file

@ -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">

View file

@ -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$;

View file

@ -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));

View file

@ -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);

View file

@ -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);

View file

@ -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>

View file

@ -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(

View file

@ -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);

View file

@ -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);
}),

View file

@ -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;
}