mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
feature (admin): revamp admin ux
This commit is contained in:
parent
587793e376
commit
5a4571fb0e
7 changed files with 118 additions and 57 deletions
|
|
@ -1,23 +1,29 @@
|
|||
.component_stats {
|
||||
.component_page_admin .component_stats {
|
||||
clear: both;
|
||||
height: 100px;
|
||||
z-index: 1;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.component_page_admin .component_logviewer {
|
||||
z-index: 0;
|
||||
}
|
||||
.component_stats .chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100px;
|
||||
justify-content: end;
|
||||
height: 100%;
|
||||
}
|
||||
.component_stats .chart .bar {
|
||||
max-width: 45px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom: 1px solid var(--light);
|
||||
|
||||
background: var(--border);
|
||||
background: #ebebec;
|
||||
border: 2px solid var(--border);
|
||||
}
|
||||
.component_stats .chart .bar[title="0"], .component_stats .chart .bar[title="1"], .component_stats .chart .bar[title="2"] {
|
||||
.component_stats .chart .bar[title="0"], .component_stats .chart .bar[title="1"] {
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
|
@ -27,3 +33,12 @@
|
|||
color: var(--light);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.component_stats .legend .title {
|
||||
font-style: italic;
|
||||
}
|
||||
.component_stats .legend.invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
.component_stats .component_skeleton {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,83 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { effect } from "../../lib/rx.js";
|
||||
import { get as getLogs } from "./model_log.js";
|
||||
import { generateSkeleton } from "../../components/skeleton.js";
|
||||
import { loadCSS } from "../../helpers/loader.js";
|
||||
|
||||
import { get as getLogs } from "./model_log.js";
|
||||
|
||||
const NUMBER_BUCKETS = 30;
|
||||
const MIN_TIME_WIDTH = 5000;
|
||||
|
||||
export default async function(render) {
|
||||
render(createElement(`<div>${generateSkeleton(3)}</div>`));
|
||||
await loadCSS(import.meta.url, "./ctrl_activity_graph.css");
|
||||
|
||||
effect(getLogs().pipe(
|
||||
rxjs.map((log) => {
|
||||
const times = log.trim().split("\n").map((line) => new Date(line.substring(0, 19)).getTime());
|
||||
const start = times[0];
|
||||
const end = times[times.length - 1];
|
||||
|
||||
const size = 30
|
||||
const bars = Array(size).fill(0);
|
||||
const width = (end - start) / size;
|
||||
for (const t of times) {
|
||||
const idx = Math.min(size - 1, Math.max(0, Math.floor((t - start) / width)));
|
||||
bars[idx] += 1;
|
||||
effect(getLogs(300).pipe(
|
||||
rxjs.first(),
|
||||
rxjs.repeat({ delay: 2500 }),
|
||||
rxjs.scan(({ start, end, width, max, init = true, buckets = Array(NUMBER_BUCKETS).fill(0) }, logfile) => {
|
||||
const times = logfile.trim().split("\n").map((line) => new Date(line.substring(0, 19)).getTime());
|
||||
if (init === true) {
|
||||
start = times[0];
|
||||
end = times[times.length - 1];
|
||||
width = Math.max((end - start) / NUMBER_BUCKETS, MIN_TIME_WIDTH);
|
||||
for (let i=times.length-1; i>=0; i--) {
|
||||
let idx = Math.floor((times[i] - start) / width);
|
||||
if (idx === NUMBER_BUCKETS) idx -= 1;
|
||||
buckets[idx] += 1;
|
||||
}
|
||||
for (let i=buckets.length-1; i>=0; i--) {
|
||||
if (buckets[i] === 0) buckets[i] = -1;
|
||||
else break;
|
||||
}
|
||||
max = Math.max(1, ...buckets);
|
||||
init = false;
|
||||
} else {
|
||||
for (let i=times.length-1; i>=0; i--) {
|
||||
const current = times[i];
|
||||
// start end current
|
||||
// | | |
|
||||
// |=============|<-- new -->
|
||||
if (current <= end) {
|
||||
break;
|
||||
}
|
||||
const idx = Math.floor((times[i] - start) / width);
|
||||
if (!buckets[idx]) buckets[idx] = 0;
|
||||
buckets[idx] += 1;
|
||||
}
|
||||
const shift = buckets.length - NUMBER_BUCKETS;
|
||||
for (let i=0; i<shift; i++) {
|
||||
buckets.shift();
|
||||
start += width;
|
||||
}
|
||||
end = times[times.length - 1];
|
||||
}
|
||||
return {
|
||||
bars,
|
||||
start: new Date(start).toLocaleTimeString(),
|
||||
end: new Date(end).toLocaleTimeString(),
|
||||
};
|
||||
}),
|
||||
rxjs.tap(({ bars, start, end }) => {
|
||||
const max = Math.max(1, ...bars);
|
||||
return { start, end, width, buckets, max, init };
|
||||
}, {}),
|
||||
rxjs.tap(({ buckets, start, end, max }) => {
|
||||
const $root = document.createDocumentFragment();
|
||||
const $chart = createElement(`<div class="chart"></div>`);
|
||||
for (let i = 0; i < bars.length; i++) {
|
||||
const $bar = createElement(`<div class="bar" title="${bars[i]}"></div>`)
|
||||
$bar.style.height = Math.sqrt(bars[i]) / Math.sqrt(max) * 100 + "%";
|
||||
let display = true;
|
||||
for (let i = 0; i < buckets.length; i++) {
|
||||
if (buckets[i] < 0) {
|
||||
display = false;
|
||||
continue;
|
||||
}
|
||||
const $bar = createElement(`<div class="bar" title="${buckets[i]}"></div>`);
|
||||
const height = Math.sqrt(buckets[i]) / Math.sqrt(max) * 100;
|
||||
$bar.style.height = Math.min(height, 120) + "%";
|
||||
$chart.appendChild($bar);
|
||||
}
|
||||
$root.appendChild($chart);
|
||||
$root.appendChild(createElement(`
|
||||
<div class="legend">
|
||||
<span>${start}</span>
|
||||
<span>${end}</span>
|
||||
<span>${new Date(start).toLocaleTimeString()}</span>
|
||||
<span class="title">Log Events</span>
|
||||
<span>${new Date(end).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
`));
|
||||
render($root);
|
||||
if (display) render($root);
|
||||
}),
|
||||
rxjs.catchError(() => rxjs.EMPTY),
|
||||
rxjs.catchError((err) => rxjs.EMPTY),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ export default async function(render) {
|
|||
const $log = qs($page, "pre");
|
||||
render($page);
|
||||
|
||||
effect(getLogs().pipe(
|
||||
effect(rxjs.of(null).pipe(
|
||||
rxjs.mergeMap(() => $log.matches(":hover") ? rxjs.EMPTY : getLogs()),
|
||||
rxjs.map((logData) => logData + "\n\n\n\n\n"),
|
||||
stateMutation($log, "textContent"),
|
||||
rxjs.tap(() => {
|
||||
if ($log?.scrollTop !== 0) return;
|
||||
$log.scrollTop = $log.scrollHeight;
|
||||
}),
|
||||
rxjs.repeat({ delay: 2500 }),
|
||||
rxjs.catchError(() => rxjs.EMPTY),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ function setupHOC(ctrlWrapped) {
|
|||
function componentStep1(render) {
|
||||
const $page = createElement(`
|
||||
<div id="step1">
|
||||
<h4>Welcome Aboard, Captain!</h4>
|
||||
<h4>Welcome Aboard!</h4>
|
||||
<div>
|
||||
<p>First thing first, setup your password: </p>
|
||||
<form>
|
||||
|
|
|
|||
|
|
@ -104,8 +104,17 @@
|
|||
/* list page */
|
||||
.component_page_workflow h3.empty {
|
||||
padding: 30px;
|
||||
border: 2px dashed var(--light);
|
||||
color: var(--light);
|
||||
background: white;
|
||||
border: 2px dashed var(--color);
|
||||
text-shadow: 0px 0px 1px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 5px;
|
||||
font-weight: 600;
|
||||
color: var(--color);
|
||||
text-transform: uppercase;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: -0.5px;
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Workflow creation modal */
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ export default async function(render, { workflows, triggers }) {
|
|||
`);
|
||||
render(transition($page));
|
||||
const $workflows = qs($page, `[data-bind="workflows"]`);
|
||||
workflows.forEach((workflow) => $workflows.appendChild(createWorkflow(workflow)));
|
||||
if (workflows.length === 0) $workflows.appendChild(createEmptyWorkflow());
|
||||
workflows.forEach((workflow) => $workflows.appendChild(createWorkflow({ workflow })));
|
||||
if (workflows.length === 0) $workflows.appendChild(createEmptyWorkflow({ triggers }));
|
||||
|
||||
effect(onClick(qs($page, "h2 > a")).pipe(
|
||||
rxjs.tap(($a) => animate($a, {
|
||||
|
|
@ -32,14 +32,14 @@ export default async function(render, { workflows, triggers }) {
|
|||
));
|
||||
}
|
||||
|
||||
function createWorkflow(specs) {
|
||||
const { name, published, actions, trigger, updated_at } = specs;
|
||||
function createWorkflow({ workflow }) {
|
||||
const { name, published, actions, trigger, updated_at } = workflow;
|
||||
const summaryHTML = {
|
||||
trigger: `<button class="light">${safe(trigger.name)}</button>`,
|
||||
actions: (actions || []).map(({ name }) => `<button class="light">${safe(name.split("/")[0])}</button>`).join(""),
|
||||
};
|
||||
const $workflow = createElement(`
|
||||
<a href="./admin/workflow?specs=${encodeURIComponent(btoa(JSON.stringify(specs)))}" class="box ${published ? "" : "disabled"}" data-link>
|
||||
<a href="./admin/workflow?specs=${encodeURIComponent(btoa(JSON.stringify(workflow)))}" class="box ${published ? "" : "disabled"}" data-link>
|
||||
<h3 class="ellipsis">
|
||||
${safe(name)}
|
||||
<span>(${Intl.DateTimeFormat(navigator.language).format(new Date(safe(updated_at)))})</span>
|
||||
|
|
@ -52,10 +52,14 @@ function createWorkflow(specs) {
|
|||
return $workflow;
|
||||
}
|
||||
|
||||
function createEmptyWorkflow() {
|
||||
return createElement(`
|
||||
<h3 class="center empty no-select">Add a new workflow to get started</h3>
|
||||
function createEmptyWorkflow({ triggers }) {
|
||||
const $empty = createElement(`
|
||||
<h3 class="center empty no-select">Create your first workflow</h3>
|
||||
`);
|
||||
effect(onClick($empty).pipe(
|
||||
rxjs.tap(() => ctrlModal(createModal(), { triggers })),
|
||||
));
|
||||
return $empty;
|
||||
}
|
||||
|
||||
function ctrlModal(render, { triggers }) {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
import rxjs from "../../lib/rx.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
|
||||
const log$ = ajax({
|
||||
url: url(1024 * 100), // fetch the last 100kb by default
|
||||
responseType: "text",
|
||||
}).pipe(
|
||||
rxjs.map(({ response }) => response),
|
||||
);
|
||||
|
||||
export function url(logSize = 0) {
|
||||
return "admin/api/logs" + (logSize ? `?maxSize=${logSize}` : "");
|
||||
}
|
||||
|
||||
export function get() {
|
||||
return log$.pipe(
|
||||
rxjs.repeat({ delay: 10000 }),
|
||||
export function get(t = 100) {
|
||||
return ajax({
|
||||
url: url(1024 * t), // fetch the last 100KB by default
|
||||
responseType: "text",
|
||||
}).pipe(
|
||||
rxjs.map(({ response }) => response),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue