revert ssr dialog interface

This commit is contained in:
2025-06-18 21:21:00 +00:00
parent 308d133e6e
commit e41c8d2a07
5 changed files with 176 additions and 255 deletions

View File

@@ -64,55 +64,50 @@
</section> </section>
</main> </main>
<modal-dialog id="create-instance-dialog"> <template id="create-instance-dialog">
<template shadowrootmode="open"> <dialog class="w3-container w3-card w3-border-0">
<link rel="stylesheet" href="modules/w3.css"> <p class="w3-large" id="prompt" style="text-align: center;">
<link rel="stylesheet" href="css/style.css"> Create New Instance
<link rel="stylesheet" href="css/form.css"> </p>
<dialog class="w3-container w3-card w3-border-0"> <div id="body">
<p class="w3-large" id="prompt" style="text-align: center;"> <form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
Create New Instance <label for="type">Instance Type</label>
</p> <select class="w3-select w3-border" name="type" id="type" selected-index="-1" required>
<div id="body"> <option value="lxc">Container</option>
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form"> <option value="qemu">Virtual Machine</option>
<label for="type">Instance Type</label> </select>
<select class="w3-select w3-border" name="type" id="type" selected-index="-1" required> <label for="node">Node</label>
<option value="lxc">Container</option> <select class="w3-select w3-border" name="node" id="node" required></select>
<option value="qemu">Virtual Machine</option> <label for="name">Name</label>
</select> <input class="w3-input w3-border" name="name" id="name" type="text" required>
<label for="node">Node</label> <label for="vmid">ID</label>
<select class="w3-select w3-border" name="node" id="node" required></select> <input class="w3-input w3-border" name="vmid" id="vmid" type="number" required>
<label for="name">Name</label> <label for="pool">Pool</label>
<input class="w3-input w3-border" name="name" id="name" type="text" required> <select class="w3-select w3-border" name="pool" id="pool" required></select>
<label for="vmid">ID</label> <label for="cores">Cores (Threads)</label>
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required> <input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required>
<label for="pool">Pool</label> <label for="memory">Memory (MiB)</label>
<select class="w3-select w3-border" name="pool" id="pool" required></select> <input class="w3-input w3-border" name="memory" id="memory" type="number" min="16" step="1" required>
<label for="cores">Cores (Threads)</label> <p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required> <label class="container-specific none" for="swap">Swap (MiB)</label>
<label for="memory">Memory (MiB)</label> <input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled>
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16" step="1" required> <label class="container-specific none" for="template-image">Template Image</label>
<p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p> <select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
<label class="container-specific none" for="swap">Swap (MiB)</label> <label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label>
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled> <select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select>
<label class="container-specific none" for="template-image">Template Image</label> <label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select> <input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled>
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label> <label class="container-specific none" for="password">Password</label>
<select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select> <input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled>
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label> <label class="container-specific none" for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled> <input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" required disabled>
<label class="container-specific none" for="password">Password</label> </form>
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled> </div>
<label class="container-specific none" for="confirm-password">Confirm Password</label> <div id="controls" class="w3-center w3-container">
<input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" required disabled> <button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
</form> <button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
</div> </div>
<div id="controls" class="w3-center w3-container"> </dialog>
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button> </template>
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
</div>
</dialog>
</template>
</modal-dialog>
</body> </body>
</html> </html>

View File

@@ -1,18 +1,17 @@
import { requestAPI, setAppearance } from "./utils.js"; import { requestAPI, setAppearance } from "./utils.js";
import "./dialog.js"; import { dialog } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
async function init () { async function init () {
setAppearance(); setAppearance();
initPasswordChangeForm();
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeButton); document.querySelector("#change-password").addEventListener("click", handlePasswordChangeButton);
} }
function initPasswordChangeForm () { function handlePasswordChangeButton () {
const d = document.querySelector("#change-password-dialog"); const template = document.querySelector("#change-password-dialog");
d.setOnClose(async (result, form) => { const d = dialog(template, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") }); const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") });
if (result.status !== 200) { if (result.status !== 200) {
@@ -23,16 +22,9 @@ function initPasswordChangeForm () {
const password = d.querySelector("#new-password"); const password = d.querySelector("#new-password");
const confirmPassword = d.querySelector("#confirm-password"); const confirmPassword = d.querySelector("#confirm-password");
function validatePassword () { function validatePassword () {
confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : ""); confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : "");
} }
password.addEventListener("change", validatePassword); password.addEventListener("change", validatePassword);
confirmPassword.addEventListener("keyup", validatePassword); confirmPassword.addEventListener("keyup", validatePassword);
} }
function handlePasswordChangeButton () {
const d = document.querySelector("#change-password-dialog");
d.showModal();
}

View File

@@ -1,84 +1,40 @@
/** /**
* Custom modal dialog with form support. Assumes the following structure: * Spawn modal dialog from template node. Assumes the following structure:
* <modal-dialog><template shadowrootmode="open"> * <template>
* <dialog>
* <p id="prompt"></p> * <p id="prompt"></p>
* <div id="body"> * <div id="body">
* <form id="form"> ... </form> * <form id="form"> ... </form>
* </div> * </div>
* <div id="controls"> * <div id="controls">
* <button value="..." form="" * <button value="..." form="form"
* <button value="..." form="form"
* ...
* </div> * </div>
* </modal-dialog></template> * </dialog>
* </template>
* Where prompt is the modal dialog's prompt or header, * Where prompt is the modal dialog's prompt or header,
* body contains an optional form or other information, * body contains an optional form or other information,
* and controls contains a series of buttons which controls the form * and controls contains a series of buttons which controls the form
*/ */
class ModalDialog extends HTMLElement { export function dialog (template, onclose = async (result, form) => { }) {
shadowRoot = null; const dialog = template.content.querySelector("dialog").cloneNode(true);
dialog = null; document.body.append(dialog);
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 = `
<p class="w3-large" id="prompt" style="text-align: center;"></p>
<div id="body"></div>
<div class="w3-center w3-container">
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
</div>
`;
dialog.className = "w3-container w3-card w3-border-0";
dialog.querySelector("#prompt").innerText = header;
dialog.querySelector("#body").innerHTML = body;
dialog.addEventListener("close", async () => { dialog.addEventListener("close", async () => {
const formElem = dialog.querySelector("form"); const formElem = dialog.querySelector("form");
const formData = formElem ? new FormData(formElem) : null; const formData = formElem ? new FormData(formElem) : null;
await onclose(dialog.returnValue, formData); await onclose(dialog.returnValue, formData);
formElem.reset();
dialog.close();
dialog.parentElement.removeChild(dialog); dialog.parentElement.removeChild(dialog);
}); });
if (!dialog.querySelector("form")) { if (!dialog.querySelector("form")) {
dialog.querySelector("#confirm").addEventListener("click", async (e) => { for (const control of dialog.querySelector("#controls").childNodes) {
e.preventDefault(); control.addEventListener("click", async (e) => {
dialog.close(e.target.value); e.preventDefault();
}); dialog.close(e.target.value);
dialog.querySelector("#cancel").addEventListener("click", async (e) => { });
e.preventDefault(); }
dialog.close(e.target.value);
});
} }
document.body.append(dialog); document.body.append(dialog);
dialog.showModal(); dialog.showModal();

View File

@@ -1,5 +1,5 @@
import { requestPVE, requestAPI, setAppearance, getSearchSettings, requestDash, setSVGSrc, setSVGAlt } from "./utils.js"; 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 { setupClientSync } from "./clientsync.js";
import wfaInit from "../modules/wfa.js"; import wfaInit from "../modules/wfa.js";
@@ -11,7 +11,6 @@ async function init () {
wfaInit("modules/wfa.wasm"); wfaInit("modules/wfa.wasm");
initInstances(); initInstances();
initInstanceAddForm();
document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton); document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton);
document.querySelector("#vm-search").addEventListener("input", sortInstances); document.querySelector("#vm-search").addEventListener("input", sortInstances);
@@ -120,7 +119,6 @@ class InstanceCard extends HTMLElement {
nameParagraph.innerHTML = this.name ? this.name : "&nbsp;"; nameParagraph.innerHTML = this.name ? this.name : "&nbsp;";
} }
this.initPowerForm();
const powerButton = this.shadowRoot.querySelector("#power-btn"); const powerButton = this.shadowRoot.querySelector("#power-btn");
if (powerButton.classList.contains("clickable")) { if (powerButton.classList.contains("clickable")) {
powerButton.onclick = this.handlePowerButton.bind(this); powerButton.onclick = this.handlePowerButton.bind(this);
@@ -133,7 +131,6 @@ class InstanceCard extends HTMLElement {
}; };
} }
this.initDeleteForm();
const deleteButton = this.shadowRoot.querySelector("#delete-btn"); const deleteButton = this.shadowRoot.querySelector("#delete-btn");
if (deleteButton.classList.contains("clickable")) { if (deleteButton.classList.contains("clickable")) {
deleteButton.onclick = this.handleDeleteButton.bind(this); deleteButton.onclick = this.handleDeleteButton.bind(this);
@@ -156,70 +153,60 @@ class InstanceCard extends HTMLElement {
setSVGAlt(powerbtn, ""); 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 () { async handlePowerButton () {
if (!this.actionLock) { if (!this.actionLock) {
const dialog = this.shadowRoot.querySelector("#power-dialog"); const template = this.shadowRoot.querySelector("#power-dialog");
dialog.showModal(); dialog(template, async (result, form) => {
} if (result === "confirm") {
} this.actionLock = true;
const targetAction = this.status === "running" ? "stop" : "start";
initDeleteForm () { const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid });
const dialog = this.shadowRoot.querySelector("#delete-dialog"); this.setStatusLoading();
dialog.setOnClose(async (result, form) => {
if (result === "confirm") {
this.actionLock = true;
const action = {}; const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
action.purge = 1;
action["destroy-unreferenced-disks"] = 1;
const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); while (true) {
if (result.status !== 200) { const taskStatus = await requestPVE(`/nodes/${this.node.name}/tasks/${result.data}/status`, "GET");
alert(`Attempted to delete ${this.vmid} but got: ${result.error}`); 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 () { handleDeleteButton () {
if (!this.actionLock && this.status === "stopped") { if (!this.actionLock && this.status === "stopped") {
const dialog = this.shadowRoot.querySelector("#delete-dialog"); const template = this.shadowRoot.querySelector("#delete-dialog");
dialog.showModal(); 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 () { async function handleInstanceAddButton () {
// form submit logic const template = document.querySelector("#create-instance-dialog");
const d = document.querySelector("#create-instance-dialog"); const d = dialog(template, async (result, form) => {
d.setOnClose(async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
const body = { const body = {
name: form.get("name"), 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 templates = await requestAPI("/user/ct-templates", "GET");
const typeSelect = d.querySelector("#type"); const typeSelect = d.querySelector("#type");
@@ -454,5 +427,14 @@ async function handleInstanceAddButton () {
} }
templateImage.selectedIndex = -1; 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(); d.showModal();
} }

View File

@@ -61,64 +61,60 @@
{{end}} {{end}}
</div> </div>
</div> </div>
<modal-dialog id="power-dialog"> <template id="power-dialog">
<template shadowrootmode="open"> <link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="modules/w3.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/form.css">
<link rel="stylesheet" href="css/form.css"> <dialog class="w3-container w3-card w3-border-0">
<dialog class="w3-container w3-card w3-border-0"> <p class="w3-large" id="prompt" style="text-align: center;">
<p class="w3-large" id="prompt" style="text-align: center;"> {{if eq .Status "running"}}
{{if eq .Status "running"}} Stop {{.VMID}}
Stop {{.VMID}} {{else if eq .Status "stopped"}}
{{else if eq .Status "stopped"}} Start {{.VMID}}
Start {{.VMID}} {{else}}
{{else}} {{end}}
{{end}} </p>
<div id="body">
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<p>
{{if eq .Status "running"}}
Are you sure you want to <strong>stop</strong> {{.VMID}}?
{{else if eq .Status "stopped"}}
Are you sure you want to <strong>start</strong> {{.VMID}}?
{{else}}
{{end}}
</p> </p>
<div id="body"> </form>
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form"> </div>
<p> <div id="controls" class="w3-center w3-container">
{{if eq .Status "running"}} <button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
Are you sure you want to <strong>stop</strong> {{.VMID}}? <button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
{{else if eq .Status "stopped"}} </div>
Are you sure you want to <strong>start</strong> {{.VMID}}? </dialog>
{{else}} </template>
{{end}}
</p>
</form>
</div>
<div id="controls" class="w3-center w3-container">
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
</div>
</dialog>
</template>
</modal-dialog>
<modal-dialog id="delete-dialog"> <template id="delete-dialog">
<template shadowrootmode="open"> <link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="modules/w3.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/form.css">
<link rel="stylesheet" href="css/form.css"> <dialog class="w3-container w3-card w3-border-0">
<dialog class="w3-container w3-card w3-border-0"> <p class="w3-large" id="prompt" style="text-align: center;">
<p class="w3-large" id="prompt" style="text-align: center;"> Delete {{.VMID}}
Delete {{.VMID}} </p>
<div id="body">
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<p>
Are you sure you want to <strong>delete</strong> {{.VMID}}
</p> </p>
<div id="body"> </form>
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form"> </div>
<p> <div id="controls" class="w3-center w3-container">
Are you sure you want to <strong>delete</strong> {{.VMID}} <button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
</p> <button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
</form> </div>
</div> </dialog>
<div id="controls" class="w3-center w3-container"> </template>
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
</div>
</dialog>
</template>
</modal-dialog>
</template> </template>
</instance-card> </instance-card>
{{end}} {{end}}