fix bug in ssr dialog with multiple event listeners, add ssr dialog for instance delete

This commit is contained in:
2025-05-29 18:33:01 +00:00
parent 478ca20451
commit acd6eba520
3 changed files with 93 additions and 61 deletions

View File

@@ -23,15 +23,6 @@ class ModalDialog extends HTMLElement {
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 () {
@@ -46,6 +37,7 @@ class ModalDialog extends HTMLElement {
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");

View File

@@ -1,5 +1,5 @@
import { requestPVE, requestAPI, setAppearance, getSearchSettings, requestDash, setSVGSrc, setSVGAlt } from "./utils.js";
import { alert, dialog } from "./dialog.js";
import { alert } from "./dialog.js";
import { setupClientSync } from "./clientsync.js";
import wfaInit from "../modules/wfa.js";
@@ -11,7 +11,8 @@ async function init () {
wfaInit("modules/wfa.wasm");
initInstances();
document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd);
initInstanceAddForm();
document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton);
document.querySelector("#vm-search").addEventListener("input", sortInstances);
setupClientSync(refreshInstances);
@@ -119,6 +120,7 @@ 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);
@@ -131,6 +133,7 @@ class InstanceCard extends HTMLElement {
};
}
this.initDeleteForm();
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
if (deleteButton.classList.contains("clickable")) {
deleteButton.onclick = this.handleDeleteButton.bind(this);
@@ -153,63 +156,70 @@ 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.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();
}
});
dialog.showModal();
}
}
initDeleteForm () {
const dialog = this.shadowRoot.querySelector("#delete-dialog");
dialog.setOnClose(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();
}
});
}
handleDeleteButton () {
if (!this.actionLock && this.status === "stopped") {
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") {
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();
}
});
const dialog = this.shadowRoot.querySelector("#delete-dialog");
dialog.showModal();
}
}
}
@@ -312,9 +322,8 @@ function sortInstances () {
}
}
async function handleInstanceAdd () {
async function initInstanceAddForm () {
const d = document.querySelector("#create-instance-dialog");
d.setOnClose(async (result, form) => {
if (result === "confirm") {
const body = {
@@ -343,6 +352,10 @@ async function handleInstanceAdd () {
}
}
});
}
async function handleInstanceAddButton () {
const d = document.querySelector("#create-instance-dialog");
const templates = await requestAPI("/user/ct-templates", "GET");
@@ -375,6 +388,7 @@ async function handleInstanceAdd () {
const userCluster = await requestAPI("/user/config/cluster", "GET");
const nodeSelect = d.querySelector("#node");
nodeSelect.innerHTML = "";
const clusterNodes = await requestPVE("/nodes", "GET");
const allowedNodes = Object.keys(userCluster.nodes);
clusterNodes.data.forEach((element) => {
@@ -415,6 +429,7 @@ async function handleInstanceAdd () {
// add user pools to selector
const poolSelect = d.querySelector("#pool");
poolSelect.innerHTML = "";
const userPools = Object.keys(userCluster.pools);
userPools.forEach((element) => {
poolSelect.add(new Option(element));

View File

@@ -61,6 +61,7 @@
{{end}}
</div>
</div>
<modal-dialog id="power-dialog">
<template shadowrootmode="open">
<link rel="stylesheet" href="modules/w3.css">
@@ -94,6 +95,30 @@
</dialog>
</template>
</modal-dialog>
<modal-dialog id="delete-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;">
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>
</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}}