implement ssr modal dialog form construction for create instances

This commit is contained in:
2025-05-28 20:08:52 +00:00
parent 28c60aecc9
commit 478ca20451
5 changed files with 167 additions and 52 deletions

View File

@@ -77,6 +77,5 @@ input[type="radio"] {
dialog {
max-width: calc(min(50%, 80ch));
background-color: var(--main-bg-color);
color: var(--main-text-color);
}

View File

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

View File

@@ -1,3 +1,64 @@
/**
* Custom modal dialog with form support. Assumes the following structure:
* <modal-dialog><template shadowrootmode="open">
* <p id="prompt"></p>
* <div id="body">
* <form id="form"> ... </form>
* </div>
* <div id="controls">
* <button value="..." form=""
* </div>
* </modal-dialog></template>
* 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");
// add dialog handler to each control button with the return value corresponding to their value attribute
const controls = this.shadowRoot.querySelector("#controls");
for (const button of controls.childNodes) {
button.addEventListener("click", async (e) => {
e.preventDefault();
this.dialog.close(e.target.value);
});
}
this.setOnClose(); // default behavior to just close the dialog, should call setOnClose to override this behavior
}
showModal () {
this.dialog.showModal();
}
querySelector (query) {
return this.shadowRoot.querySelector(query);
}
querySelectorAll (query) {
return this.shadowRoot.querySelectorAll(query);
}
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 = `

View File

@@ -123,12 +123,12 @@ class InstanceCard extends HTMLElement {
if (powerButton.classList.contains("clickable")) {
powerButton.onclick = this.handlePowerButton.bind(this);
powerButton.onkeydown = (event) => {
console.log(event.key, event.key === "Enter")
console.log(event.key, event.key === "Enter");
if (event.key === "Enter") {
event.preventDefault()
this.handlePowerButton()
}
event.preventDefault();
this.handlePowerButton();
}
};
}
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
@@ -136,10 +136,10 @@ class InstanceCard extends HTMLElement {
deleteButton.onclick = this.handleDeleteButton.bind(this);
deleteButton.onkeydown = (event) => {
if (event.key === "Enter") {
event.preventDefault()
this.handleDeleteButton()
}
event.preventDefault();
this.handleDeleteButton();
}
};
}
}
@@ -155,9 +155,8 @@ class InstanceCard extends HTMLElement {
async handlePowerButton () {
if (!this.actionLock) {
const header = `${this.status === "running" ? "Stop" : "Start"} VM ${this.vmid}`;
const body = `<p>Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM ${this.vmid}</p>`;
dialog(header, body, async (result, form) => {
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";
@@ -185,13 +184,14 @@ class InstanceCard extends HTMLElement {
refreshInstances();
}
});
dialog.showModal();
}
}
handleDeleteButton () {
if (!this.actionLock && this.status === "stopped") {
const header = `Delete VM ${this.vmid}`;
const body = `<p>Are you sure you want to <strong>delete</strong> VM ${this.vmid}</p>`;
const header = `Delete ${this.vmid}`;
const body = `<p>Are you sure you want to <strong>delete</strong> ${this.vmid}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
@@ -313,46 +313,9 @@ function sortInstances () {
}
async function handleInstanceAdd () {
const header = "Create New Instance";
const d = document.querySelector("#create-instance-dialog");
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="type">Instance Type</label>
<select class="w3-select w3-border" name="type" id="type" required>
<option value="lxc">Container</option>
<option value="qemu">Virtual Machine</option>
</select>
<label for="node">Node</label>
<select class="w3-select w3-border" name="node" id="node" required></select>
<label for="name">Name</label>
<input class="w3-input w3-border" name="name" id="name" required>
<label for="vmid">ID</label>
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required>
<label for="pool">Pool</label>
<select class="w3-select w3-border" name="pool" id="pool" required></select>
<label for="cores">Cores (Threads)</label>
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required>
<label for="memory">Memory (MiB)</label>
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16", step="1" required>
<p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
<label class="container-specific none" for="swap">Swap (MiB)</label>
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled>
<label class="container-specific none" for="template-image">Template Image</label>
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label>
<select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select>
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
<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="password">Password</label>
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled>
<label class="container-specific none" for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" required disabled>
</form>
`;
const templates = await requestAPI("/user/ct-templates", "GET");
const d = dialog(header, body, async (result, form) => {
d.setOnClose(async (result, form) => {
if (result === "confirm") {
const body = {
name: form.get("name"),
@@ -381,6 +344,8 @@ async function handleInstanceAdd () {
}
});
const templates = await requestAPI("/user/ct-templates", "GET");
const typeSelect = d.querySelector("#type");
typeSelect.selectedIndex = -1;
typeSelect.addEventListener("change", () => {
@@ -397,6 +362,10 @@ async function handleInstanceAdd () {
});
}
});
d.querySelectorAll(".container-specific").forEach((element) => {
element.classList.add("none");
element.disabled = true;
});
const rootfsContent = "rootdir";
const rootfsStorage = d.querySelector("#rootfs-storage");
@@ -468,4 +437,6 @@ async function handleInstanceAdd () {
password.addEventListener("change", validatePassword);
confirmPassword.addEventListener("keyup", validatePassword);
d.showModal();
}

View File

@@ -61,6 +61,39 @@
{{end}}
</div>
</div>
<modal-dialog id="power-dialog">
<template shadowrootmode="open">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/form.css">
<dialog class="w3-container w3-card w3-border-0">
<p class="w3-large" id="prompt" style="text-align: center;">
{{if eq .Status "running"}}
Stop {{.VMID}}
{{else if eq .Status "stopped"}}
Start {{.VMID}}
{{else}}
{{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>
</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>
</template>
</instance-card>
{{end}}