feat(admin): implement full admin dashboard structure
This commit is contained in:
201
static/js/admin/dashboard.js
Normal file
201
static/js/admin/dashboard.js
Normal file
@@ -0,0 +1,201 @@
|
||||
(() => {
|
||||
const menuController = window.WarpBoxUI?.bindMenuBar?.() || {
|
||||
close() {
|
||||
document.querySelectorAll(".menu-item.is-open").forEach((item) => {
|
||||
item.classList.remove("is-open");
|
||||
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
}
|
||||
};
|
||||
const toast = document.getElementById("toast");
|
||||
const statusText = document.getElementById("statusText");
|
||||
const modal = document.querySelector("[data-alert-modal]");
|
||||
const backdrop = document.querySelector("[data-modal-backdrop]");
|
||||
const modalTitle = document.getElementById("modalTitle");
|
||||
const modalMeta = document.getElementById("modalMeta");
|
||||
const alertCountValue = document.getElementById("alertCountValue");
|
||||
const alertStatNote = document.getElementById("alertStatNote");
|
||||
const alertsCard = document.getElementById("alertsCard");
|
||||
const topAlertChip = document.getElementById("topAlertChip");
|
||||
const topTaskbar = document.querySelector(".admin-taskbar");
|
||||
|
||||
if (!statusText || !alertsCard || !topAlertChip) return;
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
if (window.WarpBoxUI) {
|
||||
window.WarpBoxUI.toast(message, type, { target: toast });
|
||||
return;
|
||||
}
|
||||
if (!toast) return;
|
||||
toast.textContent = message;
|
||||
toast.classList.add("is-visible");
|
||||
window.clearTimeout(showToast.timer);
|
||||
showToast.timer = window.setTimeout(() => toast.classList.remove("is-visible"), 2600);
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
statusText.textContent = message;
|
||||
}
|
||||
|
||||
function openModal(title, meta) {
|
||||
if (!modal || !backdrop || !modalTitle || !modalMeta) return;
|
||||
modalTitle.textContent = title;
|
||||
modalMeta.textContent = meta;
|
||||
modal.classList.add("is-visible");
|
||||
modal.setAttribute("aria-hidden", "false");
|
||||
backdrop.classList.add("is-visible");
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal?.classList.remove("is-visible");
|
||||
modal?.setAttribute("aria-hidden", "true");
|
||||
backdrop?.classList.remove("is-visible");
|
||||
}
|
||||
|
||||
function visibleAlertRows() {
|
||||
return Array.from(document.querySelectorAll(".alert-row")).filter((row) => !row.classList.contains("is-dismissed"));
|
||||
}
|
||||
|
||||
function updateStickyHeader() {
|
||||
topTaskbar?.classList.toggle("is-scrolled", window.scrollY > 4);
|
||||
}
|
||||
|
||||
function updateAlertSummary() {
|
||||
const rows = visibleAlertRows();
|
||||
const counts = rows.reduce((acc, row) => {
|
||||
const severity = row.dataset.severity || "low";
|
||||
acc[severity] = (acc[severity] || 0) + 1;
|
||||
return acc;
|
||||
}, { high: 0, medium: 0, low: 0 });
|
||||
const score = counts.high * 5 + counts.medium * 2 + counts.low;
|
||||
const total = rows.length;
|
||||
const stateClass = counts.high > 0 || score >= 12 ? "is-danger" : counts.medium >= 2 || score >= 5 ? "is-warning" : total > 0 ? "is-info" : "is-ok";
|
||||
|
||||
alertsCard.classList.remove("is-ok", "is-info", "is-warning", "is-danger");
|
||||
alertsCard.classList.add(stateClass);
|
||||
topAlertChip.classList.remove("is-ok", "is-info", "is-warning", "is-danger");
|
||||
topAlertChip.classList.add(stateClass);
|
||||
if (alertCountValue) alertCountValue.textContent = String(total);
|
||||
topAlertChip.textContent = total === 0 ? "OK no alerts" : `! ${total} alerts`;
|
||||
if (alertStatNote) {
|
||||
alertStatNote.innerHTML = total === 0
|
||||
? '<span class="stat-note-pill">all clear</span>'
|
||||
: `<span class="stat-note-pill">${counts.high} high</span><span class="stat-note-pill">${counts.medium} medium</span><span class="stat-note-pill">${counts.low} low</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToSection(id) {
|
||||
const target = document.getElementById(id);
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
setStatus(`Focused ${id.replace("-", " ")}`);
|
||||
}
|
||||
|
||||
const commandMessages = {
|
||||
refresh: "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard refresh would re-fetch dashboard data.",
|
||||
"dashboard-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard snapshot export would start here.",
|
||||
logout: "CURRENTLY_MOCKED_LEAVE_AS_IS: logout would submit to the account logout route.",
|
||||
"compact-mode": "Toggled compact density.",
|
||||
"show-all-boxes": "TO-DO: navigate to /account/boxes.",
|
||||
"show-all-alerts": "TO-DO: navigate to /account/alerts.",
|
||||
"export-boxes": "CURRENTLY_MOCKED_LEAVE_AS_IS: boxes CSV export would be requested.",
|
||||
"export-alerts": "CURRENTLY_MOCKED_LEAVE_AS_IS: alerts JSON export would be requested.",
|
||||
"cleanup-dry-run": "CURRENTLY_MOCKED_LEAVE_AS_IS: cleanup dry run would calculate affected boxes without deleting.",
|
||||
"dismiss-low-alerts": "Closed visible low-severity alerts in this mock.",
|
||||
"config-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: config snapshot would summarize runtime settings and sources.",
|
||||
"support-summary": "CURRENTLY_MOCKED_LEAVE_AS_IS: support summary would collect safe diagnostic information.",
|
||||
"thumbnail-rebuild": "CURRENTLY_MOCKED_LEAVE_AS_IS: thumbnail rebuild would enqueue preview regeneration.",
|
||||
"open-users": "TO-DO: navigate to /account/users for admins.",
|
||||
"open-settings": "TO-DO: navigate to /account/settings for admins.",
|
||||
"alerts-help": "Alerts use title, description, severity, metadata JSON, trace identifier, and unique numeric code.",
|
||||
shortcuts: "Shortcuts: F5 refresh, Alt+A alerts, Alt+B boxes, Alt+R activity, Esc close menus/modal.",
|
||||
about: "WarpBox dashboard mock v5, single-window Win98 account dashboard."
|
||||
};
|
||||
|
||||
function runCommand(command) {
|
||||
if (command === "compact-mode") document.body.classList.toggle("is-compact");
|
||||
if (command === "dismiss-low-alerts") {
|
||||
document.querySelectorAll('.alert-row[data-severity="low"]').forEach((row) => row.classList.add("is-dismissed"));
|
||||
updateAlertSummary();
|
||||
}
|
||||
if (command === "show-all-boxes") window.location.hash = "recent-boxes";
|
||||
if (command === "show-all-alerts") window.location.hash = "alerts";
|
||||
|
||||
const message = commandMessages[command] || `Command: ${command}`;
|
||||
showToast(message);
|
||||
setStatus(message);
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-command]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
menuController.close();
|
||||
runCommand(button.dataset.command);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-scroll-to]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
menuController.close();
|
||||
scrollToSection(button.dataset.scrollTo);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-view-meta]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const row = button.closest(".alert-row");
|
||||
const title = row?.dataset.alertTitle || "Alert Metadata";
|
||||
let meta = row?.dataset.alertMeta || "{}";
|
||||
try {
|
||||
meta = JSON.stringify(JSON.parse(meta), null, 2);
|
||||
} catch (_) {
|
||||
meta = row?.dataset.alertMeta || "{}";
|
||||
}
|
||||
openModal(`${title} (${row?.dataset.alertCode || "mock"})`, meta);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-dismiss-alert]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const row = button.closest(".alert-row");
|
||||
row?.classList.add("is-dismissed");
|
||||
updateAlertSummary();
|
||||
showToast(`Closed alert ${row?.dataset.alertCode || "mock"}.`);
|
||||
setStatus(`Closed alert ${row?.dataset.alertCode || "mock"}`);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector("[data-close-modal]")?.addEventListener("click", closeModal);
|
||||
backdrop?.addEventListener("click", closeModal);
|
||||
topAlertChip.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
scrollToSection("alerts");
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", updateStickyHeader, { passive: true });
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
menuController.close();
|
||||
closeModal();
|
||||
}
|
||||
if (event.key === "F5") {
|
||||
event.preventDefault();
|
||||
runCommand("refresh");
|
||||
}
|
||||
if (event.altKey && event.key.toLowerCase() === "a") {
|
||||
event.preventDefault();
|
||||
scrollToSection("alerts");
|
||||
}
|
||||
if (event.altKey && event.key.toLowerCase() === "b") {
|
||||
event.preventDefault();
|
||||
scrollToSection("recent-boxes");
|
||||
}
|
||||
if (event.altKey && event.key.toLowerCase() === "r") {
|
||||
event.preventDefault();
|
||||
scrollToSection("recent-activity");
|
||||
}
|
||||
});
|
||||
|
||||
updateAlertSummary();
|
||||
updateStickyHeader();
|
||||
})();
|
||||
@@ -53,5 +53,46 @@ function renderTemplate(template, data = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
return { toast, openPopup, closePopup, htmlEscape, renderTemplate };
|
||||
function bindMenuBar(options = {}) {
|
||||
const root = options.root || document;
|
||||
const itemSelector = options.itemSelector || ".menu-item";
|
||||
const buttonSelector = options.buttonSelector || ".menu-button";
|
||||
const items = Array.from(root.querySelectorAll(itemSelector));
|
||||
|
||||
function close() {
|
||||
items.forEach((item) => {
|
||||
item.classList.remove("is-open");
|
||||
item.querySelector(buttonSelector)?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
}
|
||||
|
||||
function open(item) {
|
||||
close();
|
||||
item.classList.add("is-open");
|
||||
item.querySelector(buttonSelector)?.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const button = item.querySelector(buttonSelector);
|
||||
button?.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
const wasOpen = item.classList.contains("is-open");
|
||||
close();
|
||||
if (!wasOpen) open(item);
|
||||
});
|
||||
|
||||
item.addEventListener("mouseenter", () => {
|
||||
if (!root.querySelector(`${itemSelector}.is-open`)) return;
|
||||
open(item);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target.closest(itemSelector)) close();
|
||||
});
|
||||
|
||||
return { close, open };
|
||||
}
|
||||
|
||||
return { toast, openPopup, closePopup, htmlEscape, renderTemplate, bindMenuBar };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user