feature (admin): activity graph

This commit is contained in:
MickaelK 2025-10-07 15:52:16 +11:00
parent dce68b81a3
commit 79aaf52dbd
4 changed files with 83 additions and 3 deletions

View file

@ -3,6 +3,7 @@ import { createElement, createRender } from "../../lib/skeleton/index.js";
import { initConfig } from "./model_config.js"; import { initConfig } from "./model_config.js";
import componentLogForm from "./ctrl_activity_form.js"; import componentLogForm from "./ctrl_activity_form.js";
import componentLogViewer from "./ctrl_activity_viewer.js"; import componentLogViewer from "./ctrl_activity_viewer.js";
import componentLogGraph from "./ctrl_activity_graph.js";
import componentAuditor from "./ctrl_activity_audit.js"; import componentAuditor from "./ctrl_activity_audit.js";
import transition from "./animate.js"; import transition from "./animate.js";
import AdminHOC from "./decorator.js"; import AdminHOC from "./decorator.js";
@ -10,11 +11,12 @@ import AdminHOC from "./decorator.js";
export default AdminHOC(async function(render) { export default AdminHOC(async function(render) {
const $page = createElement(` const $page = createElement(`
<div class="component_logpage sticky"> <div class="component_logpage sticky">
<h2>Events</h2> <h2>System Logs</h2>
<div class="component_logviewer"></div> <div class="component_logviewer"></div>
<div class="component_stats"></div>
<div class="component_logger"></div> <div class="component_logger"></div>
<h2>Activity Report</h2> <h2>Audit Report</h2>
<div class="component_audit"></div> <div class="component_audit"></div>
<div> <div>
`); `);
@ -23,5 +25,6 @@ export default AdminHOC(async function(render) {
componentLogViewer(createRender($page.querySelector(".component_logviewer"))); componentLogViewer(createRender($page.querySelector(".component_logviewer")));
componentLogForm(createRender($page.querySelector(".component_logger"))); componentLogForm(createRender($page.querySelector(".component_logger")));
componentLogGraph(createRender($page.querySelector(".component_stats")));
componentAuditor(createRender($page.querySelector(".component_audit"))); componentAuditor(createRender($page.querySelector(".component_audit")));
}); });

View file

@ -0,0 +1,29 @@
.component_stats {
clear: both;
}
.component_stats .chart {
display: flex;
align-items: flex-end;
height: 100px;
}
.component_stats .chart .bar {
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);
border: 2px solid var(--border);
}
.component_stats .chart .bar[title="0"], .component_stats .chart .bar[title="1"], .component_stats .chart .bar[title="2"] {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.component_stats .legend {
display: flex;
justify-content: space-between;
color: var(--light);
font-size: 0.8rem;
}

View file

@ -0,0 +1,48 @@
import { createElement } from "../../lib/skeleton/index.js";
import rxjs, { effect } from "../../lib/rx.js";
import { get as getLogs } from "./model_log.js";
import { loadCSS } from "../../helpers/loader.js";
export default async function(render) {
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;
}
return {
bars,
start: new Date(start).toLocaleTimeString(),
end: new Date(end).toLocaleTimeString(),
};
}),
rxjs.tap(({ bars, start, end }) => {
const max = Math.max(1, ...bars);
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 + "%";
$chart.appendChild($bar);
}
$root.appendChild($chart);
$root.appendChild(createElement(`
<div class="legend">
<span>${start}</span>
<span>${end}</span>
</div>
`));
render($root);
}),
rxjs.catchError(() => rxjs.EMPTY),
));
}

View file

@ -13,7 +13,7 @@ export default async function(render) {
<a href="${getLogUrl()}" download="${logname()}"> <a href="${getLogUrl()}" download="${logname()}">
<button class="component_button primary">Download</button> <button class="component_button primary">Download</button>
</a> </a>
<br/><br/> <br><br>
</div> </div>
`); `);
const $log = qs($page, "pre"); const $log = qs($page, "pre");