feat(admin): add dashboard overview charts and log pagination
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m40s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m40s
Enhance the admin panel by introducing visual overview charts for upload and storage trends, along with status bars for system metrics. Additionally, implement pagination for the admin logs view, allowing users to navigate through log entries with configurable page sizes. Corresponding CSS styles have been added for the new charts, metrics grid, and pagination controls.
This commit is contained in:
@@ -62,7 +62,8 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-edit-metrics {
|
||||
.user-edit-metrics,
|
||||
.metric-grid-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -138,6 +139,146 @@
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pagination-bar .pagination {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.per-page-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.per-page-control select {
|
||||
width: auto;
|
||||
min-width: 4.5rem;
|
||||
min-height: 2rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.button.is-disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* Overview charts */
|
||||
.admin-charts {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chart-card h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.chart-card .muted-copy {
|
||||
margin: 0.3rem 0 0;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.4rem;
|
||||
height: 180px;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.bar-chart-col {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.35rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bar-chart-bar {
|
||||
width: 100%;
|
||||
max-width: 2.2rem;
|
||||
min-height: 2px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: linear-gradient(180deg, var(--primary, #8b5cf6), color-mix(in srgb, var(--primary, #8b5cf6) 55%, transparent));
|
||||
}
|
||||
|
||||
.bar-chart-value {
|
||||
color: var(--foreground);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.bar-chart-label {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.66rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stat-bars {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.stat-bar span {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.stat-bar span strong {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.stat-bar-track {
|
||||
margin-top: 0.35rem;
|
||||
height: 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-bar-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--primary, #8b5cf6);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.admin-charts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.metric-grid-4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
43
backend/static/js/35-pagination.js
Normal file
43
backend/static/js/35-pagination.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// Per-page selector: remembers the chosen page size in localStorage and keeps
|
||||
// the URL's `per` query param in sync. CSP-safe (external file, no inline JS).
|
||||
(function () {
|
||||
const select = document.querySelector("[data-per-page]");
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = "warpbox-perpage-" + select.dataset.perPage;
|
||||
const url = new URL(window.location.href);
|
||||
const current = url.searchParams.get("per");
|
||||
let stored = null;
|
||||
try {
|
||||
stored = window.localStorage.getItem(key);
|
||||
} catch (err) {
|
||||
stored = null;
|
||||
}
|
||||
|
||||
// No explicit choice in the URL but a remembered preference exists: apply it.
|
||||
if (!current && stored && stored !== select.value) {
|
||||
const valid = Array.prototype.some.call(select.options, function (opt) {
|
||||
return opt.value === stored;
|
||||
});
|
||||
if (valid) {
|
||||
url.searchParams.set("per", stored);
|
||||
url.searchParams.delete("page");
|
||||
window.location.replace(url.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
select.addEventListener("change", function () {
|
||||
try {
|
||||
window.localStorage.setItem(key, select.value);
|
||||
} catch (err) {
|
||||
/* ignore storage failures (private mode, etc.) */
|
||||
}
|
||||
const next = new URL(window.location.href);
|
||||
next.searchParams.set("per", select.value);
|
||||
next.searchParams.delete("page");
|
||||
window.location.assign(next.toString());
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user