diff --git a/web/html/index.html b/web/html/index.html index 2915157..e66ff0c 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -64,55 +64,50 @@ - - - + \ No newline at end of file diff --git a/web/scripts/account.js b/web/scripts/account.js index a0cfb1c..638e4f9 100644 --- a/web/scripts/account.js +++ b/web/scripts/account.js @@ -1,18 +1,17 @@ import { requestAPI, setAppearance } from "./utils.js"; -import "./dialog.js"; +import { dialog } from "./dialog.js"; window.addEventListener("DOMContentLoaded", init); async function init () { setAppearance(); - initPasswordChangeForm(); document.querySelector("#change-password").addEventListener("click", handlePasswordChangeButton); } -function initPasswordChangeForm () { - const d = document.querySelector("#change-password-dialog"); - d.setOnClose(async (result, form) => { +function handlePasswordChangeButton () { + const template = document.querySelector("#change-password-dialog"); + const d = dialog(template, async (result, form) => { if (result === "confirm") { const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") }); if (result.status !== 200) { @@ -23,16 +22,9 @@ function initPasswordChangeForm () { const password = d.querySelector("#new-password"); const confirmPassword = d.querySelector("#confirm-password"); - function validatePassword () { confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : ""); } - password.addEventListener("change", validatePassword); confirmPassword.addEventListener("keyup", validatePassword); } - -function handlePasswordChangeButton () { - const d = document.querySelector("#change-password-dialog"); - d.showModal(); -} diff --git a/web/scripts/dialog.js b/web/scripts/dialog.js index 4296997..291e83f 100644 --- a/web/scripts/dialog.js +++ b/web/scripts/dialog.js @@ -1,84 +1,40 @@ /** - * Custom modal dialog with form support. Assumes the following structure: - * * Where prompt is the modal dialog's prompt or header, * body contains an optional form or other information, * and controls contains a series of buttons which controls the form */ -class ModalDialog extends HTMLElement { - shadowRoot = null; - dialog = null; - - constructor () { - super(); - // setup shadowDOM - const internals = this.attachInternals(); - this.shadowRoot = internals.shadowRoot; - this.dialog = this.shadowRoot.querySelector("dialog"); - } - - showModal () { - this.dialog.showModal(); - } - - querySelector (query) { - return this.shadowRoot.querySelector(query); - } - - querySelectorAll (query) { - return this.shadowRoot.querySelectorAll(query); - } - - // it is usually not safe to call this on each dialog invocation - setOnClose (callback = (result, form) => {}) { - this.dialog.addEventListener("close", () => { - const formElem = this.dialog.querySelector("form"); - const formData = formElem ? new FormData(formElem) : null; - callback(this.dialog.returnValue, formData); - formElem.reset(); - this.dialog.close(); - }); - } -} - -customElements.define("modal-dialog", ModalDialog); - -export function dialog (header, body, onclose = async (result, form) => { }) { - const dialog = document.createElement("dialog"); - dialog.innerHTML = ` -

-
-
- - -
- `; - dialog.className = "w3-container w3-card w3-border-0"; - dialog.querySelector("#prompt").innerText = header; - dialog.querySelector("#body").innerHTML = body; +export function dialog (template, onclose = async (result, form) => { }) { + const dialog = template.content.querySelector("dialog").cloneNode(true); + document.body.append(dialog); dialog.addEventListener("close", async () => { const formElem = dialog.querySelector("form"); const formData = formElem ? new FormData(formElem) : null; await onclose(dialog.returnValue, formData); + formElem.reset(); + dialog.close(); dialog.parentElement.removeChild(dialog); }); if (!dialog.querySelector("form")) { - dialog.querySelector("#confirm").addEventListener("click", async (e) => { - e.preventDefault(); - dialog.close(e.target.value); - }); - dialog.querySelector("#cancel").addEventListener("click", async (e) => { - e.preventDefault(); - dialog.close(e.target.value); - }); + for (const control of dialog.querySelector("#controls").childNodes) { + control.addEventListener("click", async (e) => { + e.preventDefault(); + dialog.close(e.target.value); + }); + } } document.body.append(dialog); dialog.showModal(); diff --git a/web/scripts/index.js b/web/scripts/index.js index ab44f04..7154050 100644 --- a/web/scripts/index.js +++ b/web/scripts/index.js @@ -1,5 +1,5 @@ import { requestPVE, requestAPI, setAppearance, getSearchSettings, requestDash, setSVGSrc, setSVGAlt } from "./utils.js"; -import { alert } from "./dialog.js"; +import { alert, dialog } from "./dialog.js"; import { setupClientSync } from "./clientsync.js"; import wfaInit from "../modules/wfa.js"; @@ -11,7 +11,6 @@ async function init () { wfaInit("modules/wfa.wasm"); initInstances(); - initInstanceAddForm(); document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton); document.querySelector("#vm-search").addEventListener("input", sortInstances); @@ -120,7 +119,6 @@ class InstanceCard extends HTMLElement { nameParagraph.innerHTML = this.name ? this.name : " "; } - this.initPowerForm(); const powerButton = this.shadowRoot.querySelector("#power-btn"); if (powerButton.classList.contains("clickable")) { powerButton.onclick = this.handlePowerButton.bind(this); @@ -133,7 +131,6 @@ class InstanceCard extends HTMLElement { }; } - this.initDeleteForm(); const deleteButton = this.shadowRoot.querySelector("#delete-btn"); if (deleteButton.classList.contains("clickable")) { deleteButton.onclick = this.handleDeleteButton.bind(this); @@ -156,70 +153,60 @@ class InstanceCard extends HTMLElement { setSVGAlt(powerbtn, ""); } - async initPowerForm () { - const dialog = this.shadowRoot.querySelector("#power-dialog"); - dialog.setOnClose(async (result, form) => { - if (result === "confirm") { - this.actionLock = true; - const targetAction = this.status === "running" ? "stop" : "start"; - - const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid }); - this.setStatusLoading(); - - const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); - - while (true) { - const taskStatus = await requestPVE(`/nodes/${this.node.name}/tasks/${result.data}/status`, "GET"); - if (taskStatus.data.status === "stopped" && taskStatus.data.exitstatus === "OK") { // task stopped and was successful - break; - } - else if (taskStatus.data.status === "stopped") { // task stopped but was not successful - alert(`Attempted to ${targetAction} ${this.vmid} but got: ${taskStatus.data.exitstatus}`); - break; - } - else { // task has not stopped - await waitFor(1000); - } - } - - this.actionLock = false; - refreshInstances(); - } - }); - } - async handlePowerButton () { if (!this.actionLock) { - const dialog = this.shadowRoot.querySelector("#power-dialog"); - dialog.showModal(); - } - } + const template = this.shadowRoot.querySelector("#power-dialog"); + dialog(template, async (result, form) => { + if (result === "confirm") { + this.actionLock = true; + const targetAction = this.status === "running" ? "stop" : "start"; - initDeleteForm () { - const dialog = this.shadowRoot.querySelector("#delete-dialog"); - dialog.setOnClose(async (result, form) => { - if (result === "confirm") { - this.actionLock = true; + const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid }); + this.setStatusLoading(); - const action = {}; - action.purge = 1; - action["destroy-unreferenced-disks"] = 1; + const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); - const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); - if (result.status !== 200) { - alert(`Attempted to delete ${this.vmid} but got: ${result.error}`); + while (true) { + const taskStatus = await requestPVE(`/nodes/${this.node.name}/tasks/${result.data}/status`, "GET"); + if (taskStatus.data.status === "stopped" && taskStatus.data.exitstatus === "OK") { // task stopped and was successful + break; + } + else if (taskStatus.data.status === "stopped") { // task stopped but was not successful + alert(`Attempted to ${targetAction} ${this.vmid} but got: ${taskStatus.data.exitstatus}`); + break; + } + else { // task has not stopped + await waitFor(1000); + } + } + + this.actionLock = false; + refreshInstances(); } - - this.actionLock = false; - refreshInstances(); - } - }); + }); + } } handleDeleteButton () { if (!this.actionLock && this.status === "stopped") { - const dialog = this.shadowRoot.querySelector("#delete-dialog"); - dialog.showModal(); + const template = this.shadowRoot.querySelector("#delete-dialog"); + dialog(template, async (result, form) => { + if (result === "confirm") { + this.actionLock = true; + + const action = {}; + action.purge = 1; + action["destroy-unreferenced-disks"] = 1; + + const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); + if (result.status !== 200) { + alert(`Attempted to delete ${this.vmid} but got: ${result.error}`); + } + + this.actionLock = false; + refreshInstances(); + } + }); } } } @@ -322,10 +309,9 @@ function sortInstances () { } } -async function initInstanceAddForm () { - // form submit logic - const d = document.querySelector("#create-instance-dialog"); - d.setOnClose(async (result, form) => { +async function handleInstanceAddButton () { + const template = document.querySelector("#create-instance-dialog"); + const d = dialog(template, async (result, form) => { if (result === "confirm") { const body = { name: form.get("name"), @@ -354,19 +340,6 @@ async function initInstanceAddForm () { } }); - // custom password validation checker - const password = d.querySelector("#password"); - const confirmPassword = d.querySelector("#confirm-password"); - function validatePassword () { - confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : ""); - } - password.addEventListener("change", validatePassword); - confirmPassword.addEventListener("keyup", validatePassword); -} - -async function handleInstanceAddButton () { - const d = document.querySelector("#create-instance-dialog"); - const templates = await requestAPI("/user/ct-templates", "GET"); const typeSelect = d.querySelector("#type"); @@ -454,5 +427,14 @@ async function handleInstanceAddButton () { } templateImage.selectedIndex = -1; + // setup custom password checker for containers + const password = d.querySelector("#password"); + const confirmPassword = d.querySelector("#confirm-password"); + function validatePassword () { + confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : ""); + } + password.addEventListener("change", validatePassword); + confirmPassword.addEventListener("keyup", validatePassword); + d.showModal(); } diff --git a/web/templates/instance-card.go.tmpl b/web/templates/instance-card.go.tmpl index 44ff41b..824e9c3 100644 --- a/web/templates/instance-card.go.tmpl +++ b/web/templates/instance-card.go.tmpl @@ -61,64 +61,60 @@ {{end}} - - - - - {{end}} \ No newline at end of file