Web UI: Server and account menus keep the top bar on one line
Server options, ACLs, TSIG keys, Exposure, Certificates, Encrypted DNS and the audit log are in a Server menu, Account, Users and Log out under the user's name; the right-hand group stays together when it wraps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1cec6cc393
commit
5fcf4082c9
3 files changed
+92
-15
No files matched your search
@@ -45,21 +45,25 @@ const routes = [
|
||||
{ pattern: /^encrypted-dns$/, page: encdns, nav: "encdns" },
|
||||
];
|
||||
|
||||
// The top bar: the pages used most, then a menu for the server's
|
||||
// configuration and security. ["key", "#/href", "Label", "admin"?]
|
||||
const navItems = [
|
||||
["dashboard", "#/", "Dashboard"],
|
||||
["stats", "#/stats", "Statistics"],
|
||||
["zones", "#/zones", "Zones"],
|
||||
["blocking", "#/blocking", "Blocking"],
|
||||
["views", "#/views", "Views"],
|
||||
["exposure", "#/exposure", "Exposure"],
|
||||
["certificates", "#/certificates", "Certificates", "admin"],
|
||||
["encdns", "#/encrypted-dns", "Encrypted DNS", "admin"],
|
||||
["acls", "#/acls", "ACLs"],
|
||||
["keys", "#/keys", "TSIG keys"],
|
||||
["options", "#/options", "Server options"],
|
||||
["blocking", "#/blocking", "Blocking"],
|
||||
["changes", "#/changes", "Changes"],
|
||||
["history", "#/history", "History"],
|
||||
["audit", "#/audit", "Audit log"],
|
||||
{ menu: "Server", items: [
|
||||
["options", "#/options", "Server options"],
|
||||
["acls", "#/acls", "ACLs"],
|
||||
["keys", "#/keys", "TSIG keys"],
|
||||
["exposure", "#/exposure", "Exposure"],
|
||||
["certificates", "#/certificates", "Certificates", "admin"],
|
||||
["encdns", "#/encrypted-dns", "Encrypted DNS", "admin"],
|
||||
["audit", "#/audit", "Audit log"],
|
||||
] },
|
||||
];
|
||||
|
||||
const app = document.getElementById("app");
|
||||
@@ -168,12 +172,16 @@ function layout() {
|
||||
logo(30),
|
||||
wordmark({ rule: false, height: 19, cls: "brand-name" })),
|
||||
navEl,
|
||||
h("div", { class: "spacer" }),
|
||||
modeEl = h("span"),
|
||||
h("form", { class: "search", onsubmit: (e) => { e.preventDefault(); if (q.value.trim()) location.hash = "#/search?q=" + encodeURIComponent(q.value.trim()); } }, q),
|
||||
h("a", { href: "#/account", class: "user", title: (me.permissions || []).join("\n") }, me.user.username),
|
||||
me.admin ? h("a", { href: "#/users" }, "Users") : null,
|
||||
h("button", { class: "small", onclick: logout }, "Log out"));
|
||||
// The right-hand group stays together, on the right, also when it
|
||||
// has to wrap below the navigation.
|
||||
h("div", { class: "header-right" },
|
||||
modeEl = h("span"),
|
||||
h("form", { class: "search", onsubmit: (e) => { e.preventDefault(); if (q.value.trim()) location.hash = "#/search?q=" + encodeURIComponent(q.value.trim()); } }, q),
|
||||
menu(h("span", { class: "user", title: (me.permissions || []).join("\n") }, me.user.username), [
|
||||
h("a", { href: "#/account" }, "Account"),
|
||||
me.admin ? h("a", { href: "#/users" }, "Users") : null,
|
||||
h("button", { type: "button", class: "menu-item", onclick: logout }, "Log out"),
|
||||
], { right: true, active: ["account", "users"].includes(current.nav), label: "Account menu" })));
|
||||
mount(app, h("div", { class: "shell" }, header, barEl, mainEl, footer(me.build)));
|
||||
modeHolder = modeEl;
|
||||
renderNav();
|
||||
@@ -196,9 +204,33 @@ function footer(b) {
|
||||
}
|
||||
|
||||
function renderNav() {
|
||||
mount(navEl, navItems.filter((n) => n[3] !== "admin" || state.me.admin).map(([key, href, label]) => h("a", { href, class: current.nav === key ? "active" : "" }, label)));
|
||||
const allowed = (n) => n[3] !== "admin" || state.me.admin;
|
||||
const link = ([key, href, label]) => h("a", { href, class: current.nav === key ? "active" : "" }, label);
|
||||
mount(navEl, navItems.map((n) => {
|
||||
if (!n.menu) return allowed(n) ? link(n) : null;
|
||||
const items = n.items.filter(allowed);
|
||||
return menu(n.menu, items.map(link), { active: items.some((i) => i[0] === current.nav), label: n.menu + " menu" });
|
||||
}));
|
||||
}
|
||||
|
||||
// menu is a drop-down: a button and its links. It closes when one is
|
||||
// picked, on a click elsewhere and on Escape.
|
||||
function menu(label, items, opts = {}) {
|
||||
const d = h("details", { class: "menu" + (opts.right ? " right" : "") },
|
||||
h("summary", { class: opts.active ? "active" : "", "aria-label": opts.label || null }, label, h("span", { class: "caret", "aria-hidden": "true" }, "▾")),
|
||||
h("div", { class: "menu-list", role: "menu" }, items.filter(Boolean)));
|
||||
d.addEventListener("click", (e) => { if (e.target.closest(".menu-list a, .menu-list button")) d.open = false; });
|
||||
return d;
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
for (const d of document.querySelectorAll("details.menu[open]")) if (!d.contains(e.target)) d.open = false;
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Escape") return;
|
||||
for (const d of document.querySelectorAll("details.menu[open]")) { d.open = false; d.querySelector("summary").focus(); }
|
||||
});
|
||||
|
||||
// renderBar shows the pending-changes bar (only while there are pending
|
||||
// changes) and the small review/live switch in the header.
|
||||
function renderBar() {
|
||||
|
||||
Reference in new issue
Block a user