fix bugs in network config change,

generalize dialog layout,
add form validation callback,
add password change form
This commit is contained in:
Arthur Lu 2023-11-14 00:09:41 +00:00
parent 46773d290d
commit bce774d48c
5 changed files with 151 additions and 66 deletions

View File

@ -64,17 +64,10 @@
<p id="nodes">Nodes:</p> <p id="nodes">Nodes:</p>
</div> </div>
<div class="w3-card w3-padding"> <div class="w3-card w3-padding">
<h3>Password</h3> <div class="flex row nowrap">
<form id="password-form"> <h3>Password</h3>
<div class="input-grid" style="grid-template-columns: auto auto 1fr;"> <button class="w3-button w3-margin" id="change-password">Change Password</button>
<label for="new-password">New Password</label> </div>
<input class="w3-input w3-border" type="password" id="new-password">
<span></span>
<label for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border" type="password" id="confirm-password">
<span></span>
</div>
</form>
</div> </div>
<div class="w3-card w3-padding"> <div class="w3-card w3-padding">
<h3>Cluster Resources</h3> <h3>Cluster Resources</h3>

View File

@ -1,3 +1,4 @@
import { dialog } from "./dialog.js";
import { requestAPI, goToPage, getCookie, setTitleAndHeader } from "./utils.js"; import { requestAPI, goToPage, getCookie, setTitleAndHeader } from "./utils.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
@ -42,6 +43,8 @@ async function init () {
document.querySelector("#nodes").innerText = `Nodes: ${nodes.toString()}`; document.querySelector("#nodes").innerText = `Nodes: ${nodes.toString()}`;
populateResources("#resource-container", meta, resources); populateResources("#resource-container", meta, resources);
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeForm);
} }
function populateResources (containerID, meta, resources) { function populateResources (containerID, meta, resources) {
@ -117,3 +120,33 @@ function parseNumber (value, unitData) {
return `${value} ${unit}`; return `${value} ${unit}`;
} }
} }
function handlePasswordChangeForm () {
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="new-password">New Password</label>
<input class="w3-input w3-border" type="password" id="new-password" name="new-password">
<label for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border" type="password" id="confirm-password" name="confirm-password">
</form>
<p class="w3-large" id="error-message" style="text-align: center; color: var(--negative-color); margin-top: 0.5em; margin-bottom: 0;"></p>
`;
dialog("Change Password", body, async (result, form) => {
if (result === "confirm") {
const result = await requestAPI("/auth/password", "POST", {password: form.get("new-password")});
if (result.status !== 200) {
alert(result.error);
}
}
}, (dialog, form) => {
const pass = form.get("new-password");
const conf = form.get("confirm-password");
if (pass !== conf) {
dialog.querySelector("#error-message").innerText = "Passwords must match";
return false;
}
else {
return true;
}
});
}

View File

@ -233,7 +233,7 @@ function addDiskLine (fieldset, busPrefix, busName, device, diskDetails) {
async function handleDiskDetach () { async function handleDiskDetach () {
const disk = this.dataset.disk; const disk = this.dataset.disk;
const header = `Detach ${disk}`; const header = `Detach ${disk}`;
const body = `<p>Are you sure you want to detach disk</p><p>${disk}</p>`; const body = `<p>Are you sure you want to detach disk ${disk}</p>`;
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg"; document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
@ -250,7 +250,12 @@ async function handleDiskDetach () {
async function handleDiskAttach () { async function handleDiskAttach () {
const header = `Attach ${this.dataset.disk}`; const header = `Attach ${this.dataset.disk}`;
const body = `<label for="device">${type === "qemu" ? "SATA" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" required></input>`; const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">${type === "qemu" ? "SATA" : "MP"}</label>
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" required></input>
</form>
`;
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
@ -274,7 +279,12 @@ async function handleDiskAttach () {
async function handleDiskResize () { async function handleDiskResize () {
const header = `Resize ${this.dataset.disk}`; const header = `Resize ${this.dataset.disk}`;
const body = "<label for=\"size-increment\">Size Increment (GiB)</label><input class=\"w3-input w3-border\" name=\"size-increment\" id=\"size-increment\" type=\"number\" min=\"0\" max=\"131072\"></input>"; const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="size-increment">Size Increment (GiB)</label>
<input class="w3-input w3-border" name="size-increment" id="size-increment" type="number" min="0" max="131072"></input>
</form>
`;
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
@ -310,8 +320,10 @@ async function handleDiskMove () {
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select"><option hidden disabled selected value></option>${options}</select>`; const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select"><option hidden disabled selected value></option>${options}</select>`;
const body = ` const body = `
${select} <form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="delete-check">Delete Source</label><input class="w3-input w3-border" name="delete-check" id="delete-check" type="checkbox" checked required> ${select}
<label for="delete-check">Delete Source</label><input class="w3-input w3-border" name="delete-check" id="delete-check" type="checkbox" checked required>
</form>
`; `;
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
@ -337,7 +349,7 @@ async function handleDiskMove () {
async function handleDiskDelete () { async function handleDiskDelete () {
const disk = this.dataset.disk; const disk = this.dataset.disk;
const header = `Delete ${disk}`; const header = `Delete ${disk}`;
const body = `<p>Are you sure you want to <strong>delete</strong> disk</p><p>${disk}</p>`; const body = `<p>Are you sure you want to <strong>delete</strong> disk${disk}</p>`;
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg"; document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
@ -367,9 +379,11 @@ async function handleDiskAdd () {
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${options}</select>`; const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${options}</select>`;
const body = ` const body = `
<label for="device">${type === "qemu" ? "SATA" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" value="0" required></input> <form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
${select} <label for="device">${type === "qemu" ? "SATA" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" value="0" required></input>
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required></input> ${select}
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required></input>
</form>
`; `;
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
@ -407,9 +421,11 @@ async function handleCDAdd () {
const storageSelect = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${storageOptions}</select>`; const storageSelect = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${storageOptions}</select>`;
const body = ` const body = `
<label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required></input> <form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
${storageSelect} <label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required></input>
<label for="iso-select">Image</label><select class="w3-select w3-border" name="iso-select" id="iso-select" required></select> ${storageSelect}
<label for="iso-select">Image</label><select class="w3-select w3-border" name="iso-select" id="iso-select" required></select>
</form>
`; `;
const d = dialog(header, body, async (result, form) => { const d = dialog(header, body, async (result, form) => {
@ -509,7 +525,11 @@ async function handleNetworkConfig () {
const netID = this.dataset.network; const netID = this.dataset.network;
const netDetails = this.dataset.values; const netDetails = this.dataset.values;
const header = `Edit net${netID}`; const header = `Edit net${netID}`;
const body = "<label for=\"rate\">Rate Limit (MB/s)</label><input type=\"number\" id=\"rate\" name=\"rate\" class=\"w3-input w3-border\">"; const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
</form>
`;
const d = dialog(header, body, async (result, form) => { const d = dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
@ -523,7 +543,8 @@ async function handleNetworkConfig () {
} }
await getConfig(); await getConfig();
populateNetworks(); populateNetworks();
updateBootLine(`boot-net${netID}`, { id: `net${netID}`, prefix: "net", value: config.data[`net${netID}`] }); const id = `net${netID}`;
updateBootLine(`boot-net${netID}`, { id, prefix: "net", value: id, detail: config.data[`net${netID}`] });
} }
}); });
@ -551,10 +572,15 @@ async function handleNetworkDelete () {
async function handleNetworkAdd () { async function handleNetworkAdd () {
const header = "Create Network Interface"; const header = "Create Network Interface";
let body = "<label for=\"netid\">Interface ID</label><input type=\"number\" id=\"netid\" name=\"netid\" class=\"w3-input w3-border\"><label for=\"rate\">Rate Limit (MB/s)</label><input type=\"number\" id=\"rate\" name=\"rate\" class=\"w3-input w3-border\">"; let body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="netid">Interface ID</label><input type="number" id="netid" name="netid" class="w3-input w3-border">
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
`;
if (type === "lxc") { if (type === "lxc") {
body += "<label for=\"name\">Interface Name</label><input type=\"text\" id=\"name\" name=\"name\" class=\"w3-input w3-border\"></input>"; body += "<label for=\"name\">Interface Name</label><input type=\"text\" id=\"name\" name=\"name\" class=\"w3-input w3-border\"></input>";
} }
body += "</form>";
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
@ -648,7 +674,11 @@ async function handleDeviceConfig () {
const deviceDetails = this.dataset.values; const deviceDetails = this.dataset.values;
const deviceName = this.dataset.name; const deviceName = this.dataset.name;
const header = `Edit Expansion Card ${deviceID}`; const header = `Edit Expansion Card ${deviceID}`;
const body = "<label for=\"device\">Device</label><select id=\"device\" name=\"device\" required></select><label for=\"pcie\">PCI-Express</label><input type=\"checkbox\" id=\"pcie\" name=\"pcie\" class=\"w3-input w3-border\">"; const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
</form>
`;
const d = dialog(header, body, async (result, form) => { const d = dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
@ -694,7 +724,11 @@ async function handleDeviceDelete () {
async function handleDeviceAdd () { async function handleDeviceAdd () {
const header = "Add Expansion Card"; const header = "Add Expansion Card";
const body = "<label for=\"device\">Device</label><select id=\"device\" name=\"device\" required></select><label for=\"pcie\">PCI-Express</label><input type=\"checkbox\" id=\"pcie\" name=\"pcie\" class=\"w3-input w3-border\">"; const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
</form>
`;
const d = dialog(header, body, async (result, form) => { const d = dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
@ -798,11 +832,11 @@ function updateBootLine (id, newData) {
const enabled = document.querySelector("#enabled"); const enabled = document.querySelector("#enabled");
const disabled = document.querySelector("#disabled"); const disabled = document.querySelector("#disabled");
let element = null; let element = null;
if (enabled.getItemByID(id)) { if (enabled.querySelector(`#${id}`)) {
element = enabled.getItemByID(id); element = enabled.querySelector(`#${id}`);
} }
if (disabled.getItemByID(id)) { if (disabled.querySelector(`#${id}`)) {
element = disabled.getItemByID(id); element = disabled.querySelector(`#${id}`);
} }
if (element) { if (element) {
const container = element.container; const container = element.container;

View File

@ -1,20 +1,43 @@
export function dialog (header, body, callback = async (result, form) => { }) { export function dialog (header, body, onclose = async (result, form) => { }, validate = async (dialog, form) => {
return true;
}) {
const dialog = document.createElement("dialog"); const dialog = document.createElement("dialog");
dialog.innerHTML = ` dialog.innerHTML = `
<p class="w3-large" id="prompt" style="text-align: center;"></p> <p class="w3-large" id="prompt" style="text-align: center;"></p>
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form"></form> <div id="body"></div>
<div class="w3-center w3-container"> <div class="w3-center w3-container">
<button 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="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 value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</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> </div>
`; `;
dialog.className = "w3-container w3-card w3-border-0"; dialog.className = "w3-container w3-card w3-border-0";
dialog.querySelector("#prompt").innerText = header; dialog.querySelector("#prompt").innerText = header;
dialog.querySelector("form").innerHTML = body; dialog.querySelector("#body").innerHTML = body;
dialog.addEventListener("close", async () => { dialog.addEventListener("close", async () => {
await callback(dialog.returnValue, new FormData(dialog.querySelector("form"))); const formElem = dialog.querySelector("form");
let formData = null;
if (formElem) {
formData = new FormData(formElem);
}
await onclose(dialog.returnValue, formData);
dialog.parentElement.removeChild(dialog); dialog.parentElement.removeChild(dialog);
}); });
dialog.querySelector("#confirm").addEventListener("click", async (e) => {
e.preventDefault();
let valid = true;
const formElem = dialog.querySelector("form");
if (formElem) {
const form = new FormData(formElem);
valid = await validate(dialog, form);
}
if (valid) {
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();
return dialog; return dialog;

View File

@ -78,34 +78,36 @@ async function handleInstanceAdd () {
const header = "Create New Instance"; const header = "Create New Instance";
const body = ` const body = `
<label for="type">Instance Type</label> <form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<select class="w3-select w3-border" name="type" id="type" required> <label for="type">Instance Type</label>
<option value="lxc">Container</option> <select class="w3-select w3-border" name="type" id="type" required>
<option value="qemu">Virtual Machine</option> <option value="lxc">Container</option>
</select> <option value="qemu">Virtual Machine</option>
<label for="node">Node</label> </select>
<select class="w3-select w3-border" name="node" id="node" required></select> <label for="node">Node</label>
<label for="name">Name</label> <select class="w3-select w3-border" name="node" id="node" required></select>
<input class="w3-input w3-border" name="name" id="name" required></input> <label for="name">Name</label>
<label for="vmid">ID</label> <input class="w3-input w3-border" name="name" id="name" required></input>
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required></input> <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>
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required></input> <label for="cores">Cores (Threads)</label>
<label for="memory">Memory (MiB)</label> <input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required></input>
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16", step="1" required></input> <label for="memory">Memory (MiB)</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="memory" id="memory" type="number" min="16", step="1" required></input>
<label class="container-specific none" for="swap">Swap (MiB)</label> <p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled></input> <label class="container-specific none" for="swap">Swap (MiB)</label>
<label class="container-specific none" for="template-storage">Template Storage</label> <input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled></input>
<select class="w3-select w3-border container-specific none" name="template-storage" id="template-storage" required disabled></select> <label class="container-specific none" for="template-storage">Template Storage</label>
<label class="container-specific none" for="template-image">Template Image</label> <select class="w3-select w3-border container-specific none" name="template-storage" id="template-storage" required disabled></select>
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select> <label class="container-specific none" for="template-image">Template Image</label>
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label> <select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
<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-storage">ROOTFS Storage</label>
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</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="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled></input> <label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
<label class="container-specific none" for="password">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>
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled></input> <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></input>
</form>
`; `;
const d = dialog(header, body, async (result, form) => { const d = dialog(header, body, async (result, form) => {