Files
DoGaMa-serv/internal/web/static/app.js
T
2026-08-26 22:35:42 +02:00

146 lines
7.4 KiB
JavaScript

document.querySelectorAll(".menu-button:not([data-menu-bound])").forEach((button) => {
button.dataset.menuBound = "true";
button.addEventListener("click", () => {
const open = document.body.classList.toggle("nav-open");
button.setAttribute("aria-expanded", String(open));
});
});
const instanceSearch = document.querySelector("#instance-search");
if (instanceSearch) {
const cards = [...document.querySelectorAll("[data-instance-card]")];
const empty = document.querySelector("[data-search-empty]");
const filter = () => {
const query = instanceSearch.value.trim().toLocaleLowerCase();
let visible = 0;
cards.forEach((card) => {
const matches = card.dataset.search.toLocaleLowerCase().includes(query);
card.hidden = !matches;
visible += Number(matches);
});
empty.hidden = visible !== 0;
};
instanceSearch.addEventListener("input", filter);
document.addEventListener("keydown", (event) => {
if (event.key === "/" && document.activeElement !== instanceSearch) {
event.preventDefault();
instanceSearch.focus();
}
});
}
const deploymentForm = document.querySelector("[data-deployment-form]");
if (deploymentForm) {
deploymentForm.addEventListener("submit", (event) => {
event.preventDefault();
const submit = deploymentForm.querySelector("[data-deployment-submit]");
if (submit) {
submit.disabled = true;
submit.textContent = "…";
}
let progress = document.querySelector("[data-deployment-progress]");
if (!progress) {
progress = document.createElement("section");
progress.className = "panel deployment-progress";
progress.dataset.deploymentProgress = "";
deploymentForm.after(progress);
}
progress.replaceChildren();
const heading = document.createElement("h2");
heading.textContent = "Deployment progress";
const status = document.createElement("p");
status.dataset.operationStatus = "";
const list = document.createElement("ol");
list.dataset.operationSteps = "";
const error = document.createElement("p");
error.className = "error";
error.hidden = true;
const link = document.createElement("a");
link.hidden = true;
progress.append(heading, status, list, error, link);
fetch(deploymentForm.action || window.location.href, { method: "POST", body: new FormData(deploymentForm), credentials: "same-origin", headers: { Accept: "application/json" } }).then((response) => response.ok ? response.json() : Promise.reject(new Error("Deployment could not be queued."))).then(({ operation_id, instance_id }) => {
progress.hidden = false; deploymentForm.hidden = true;
const poll = () => fetch(`/api/v1/operations/${encodeURIComponent(operation_id)}`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : Promise.reject(new Error("Progress unavailable."))).then((operation) => {
status.textContent = operation.global_status; list.replaceChildren(...operation.steps.map((step) => { const item = document.createElement("li"); item.textContent = `${step.status === "success" ? "✓" : step.status === "failed" ? "✗" : step.status === "running" ? "▶" : "○"} ${step.id}`; return item; }));
if (operation.global_status === "failed") { error.hidden = false; error.textContent = `Deployment failed${operation.error_code ? `. Code: ${operation.error_code}` : "."}`; return; }
if (operation.global_status === "success") { link.hidden = false; link.href = `/instances/${encodeURIComponent(instance_id)}`; link.textContent = "Open instance"; return; }
window.setTimeout(poll, 800);
}).catch((reason) => { error.hidden = false; error.textContent = reason.message; }); poll();
}).catch((reason) => { if (submit) { submit.disabled = false; submit.textContent = "Go"; } alert(reason.message); });
});
}
const adminTabs = document.querySelectorAll('.app-main > nav.tabs a[href^="#"]');
if (adminTabs.length) {
const panels = [...document.querySelectorAll('.settings-section[id]')];
const selectAdminTab = (requested, updateHash) => {
const valid = panels.some((panel) => panel.id === requested);
const selected = valid ? requested : (panels[0]?.id || "");
panels.forEach((panel) => { panel.hidden = panel.id !== selected; });
adminTabs.forEach((tab) => {
const active = tab.getAttribute("href") === `#${selected}`;
tab.setAttribute("aria-selected", String(active));
tab.tabIndex = active ? 0 : -1;
});
if (updateHash && selected && window.location.hash !== `#${selected}`) history.replaceState(null, "", `#${selected}`);
};
adminTabs.forEach((tab, index) => {
tab.setAttribute("role", "tab");
tab.addEventListener("click", (event) => { event.preventDefault(); selectAdminTab(tab.hash.slice(1), true); });
tab.addEventListener("keydown", (event) => {
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
event.preventDefault();
const next = (index + (event.key === "ArrowRight" ? 1 : -1) + adminTabs.length) % adminTabs.length;
adminTabs[next].focus(); selectAdminTab(adminTabs[next].hash.slice(1), true);
});
});
selectAdminTab(window.location.hash.slice(1), false);
window.addEventListener("hashchange", () => selectAdminTab(window.location.hash.slice(1), false));
}
// The backend, not a timer, is the source for this history. A 403 simply
// means the signed-in user is not an administrator and leaves no technical
// data in the DOM.
const detailMatch = window.location.pathname.match(/^\/instances\/([^/]+)$/);
if (detailMatch) {
fetch(`/api/v1/instances/${encodeURIComponent(detailMatch[1])}/diagnostics`, { credentials: "same-origin" })
.then((response) => response.ok ? response.json() : null)
.then((data) => {
if (!data || !data.operations || !data.operations.length) return;
const section = document.createElement("section");
section.className = "panel";
section.innerHTML = "<h2>Operation diagnostics</h2><p class=muted>Administrator-only technical history.</p>";
data.operations.forEach((operation) => {
const item = document.createElement("details");
const summary = document.createElement("summary");
summary.textContent = `${operation.created_at} · ${operation.kind} · ${operation.state}${operation.error_code ? ` · ${operation.error_code}` : ""} · ${operation.operation_id}`;
item.append(summary);
if (operation.diagnostic) { const detail = document.createElement("pre"); detail.textContent = operation.diagnostic.details; item.append(detail); }
section.append(item);
});
document.querySelector(".instance-detail-lower")?.append(section);
}).catch(() => {});
}
const catalogSearch = document.querySelector("#catalog-search");
if (catalogSearch) {
const cards = [...document.querySelectorAll("[data-catalog-card]")];
const empty = document.querySelector("[data-catalog-search-empty]");
const filter = () => {
const query = catalogSearch.value.trim().toLocaleLowerCase();
let visible = 0;
cards.forEach((card) => {
const matches = card.dataset.search.toLocaleLowerCase().includes(query);
card.hidden = !matches;
visible += Number(matches);
});
empty.hidden = visible !== 0;
};
catalogSearch.addEventListener("input", filter);
document.addEventListener("keydown", (event) => {
if (event.key === "/" && document.activeElement !== catalogSearch) {
event.preventDefault(); catalogSearch.focus();
}
});
}