fix draggable list responsiveness,

add event handlers for adding/removing boot options
This commit is contained in:
Arthur Lu 2023-08-16 23:14:42 +00:00
parent 2b75732e92
commit 8c328dd22c
2 changed files with 136 additions and 129 deletions

View File

@ -12,6 +12,7 @@ let node;
let type; let type;
let vmid; let vmid;
let config; let config;
const bootEntries = {};
async function init () { async function init () {
setTitleAndHeader(); setTitleAndHeader();
@ -230,21 +231,19 @@ function addDiskLine (fieldset, busPrefix, busName, device, diskDetails) {
} }
async function handleDiskDetach () { async function handleDiskDetach () {
const header = `Detach ${this.dataset.disk}`; const disk = this.dataset.disk;
const body = `<p>Are you sure you want to detach disk</p><p>${this.dataset.disk}</p>`; const header = `Detach ${disk}`;
const body = `<p>Are you sure you want to detach disk</p><p>${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="${this.dataset.disk}"]`).src = "images/status/loading.svg"; document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${this.dataset.disk}/detach`, "POST"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST");
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDisk();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDisk(); populateDisk();
} deleteBootLine(`boot-${disk}`);
} }
}); });
} }
@ -260,17 +259,15 @@ async function handleDiskAttach () {
const body = { const body = {
source: this.dataset.disk.replace("unused", "") source: this.dataset.disk.replace("unused", "")
}; };
const disk = `${type === "qemu" ? "sata" : "mp"}${device}`; const prefix = type === "qemu" ? "sata" : "mp";
const disk = `${prefix}${device}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/attach`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/attach`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDisk();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDisk(); populateDisk();
} addBootLine("disabled", { id: disk, prefix, value: config.data[disk] });
} }
}); });
} }
@ -281,20 +278,19 @@ async function handleDiskResize () {
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; const disk = this.dataset.disk;
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const body = { const body = {
size: form.get("size-increment") size: form.get("size-increment")
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${this.dataset.disk}/resize`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/resize`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDisk();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDisk(); populateDisk();
} const prefix = bootMetaData.eligiblePrefixes.find((pref) => disk.startsWith(pref));
updateBootLine(`boot-${disk}`, { id: disk, prefix, value: config.data[disk] });
} }
}); });
} }
@ -320,42 +316,38 @@ async function handleDiskMove () {
dialog(header, body, async (result, form) => { dialog(header, body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; const disk = this.dataset.disk;
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const body = { const body = {
storage: form.get("storage-select"), storage: form.get("storage-select"),
delete: form.get("delete-check") === "on" ? "1" : "0" delete: form.get("delete-check") === "on" ? "1" : "0"
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${this.dataset.disk}/move`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/move`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDisk();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDisk(); populateDisk();
} const prefix = bootMetaData.eligiblePrefixes.find((pref) => disk.startsWith(pref));
updateBootLine(`boot-${disk}`, { id: disk, prefix, value: config.data[disk] });
} }
}); });
} }
async function handleDiskDelete () { async function handleDiskDelete () {
const header = `Delete ${this.dataset.disk}`; const disk = this.dataset.disk;
const body = `<p>Are you sure you want to <strong>delete</strong> disk</p><p>${this.dataset.disk}</p>`; const header = `Delete ${disk}`;
const body = `<p>Are you sure you want to <strong>delete</strong> disk</p><p>${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="${this.dataset.disk}"]`).src = "images/status/loading.svg"; document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${this.dataset.disk}/delete`, "DELETE"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE");
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDisk();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDisk(); populateDisk();
} deleteBootLine(`boot-${disk}`);
} }
}); });
} }
@ -386,17 +378,16 @@ async function handleDiskAdd () {
storage: form.get("storage-select"), storage: form.get("storage-select"),
size: form.get("size") size: form.get("size")
}; };
const disk = `${type === "qemu" ? "sata" : "mp"}${form.get("device")}`; const id = form.get("device");
const prefix = type === "qemu" ? "sata" : "mp";
const disk = `${prefix}${id}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDisk();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDisk(); populateDisk();
} addBootLine("disabled", { id: disk, prefix, value: config.data[disk] });
} }
}); });
} }
@ -428,15 +419,12 @@ async function handleCDAdd () {
}; };
const disk = `ide${form.get("device")}`; const disk = `ide${form.get("device")}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDisk();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDisk(); populateDisk();
} addBootLine("disabled", { id: disk, prefix: "ide", value: config.data[disk] });
} }
}); });
@ -530,15 +518,12 @@ async function handleNetworkConfig () {
rate: form.get("rate") rate: form.get("rate")
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/modify`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/modify`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateNetworks();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateNetworks(); populateNetworks();
} updateBootLine(`boot-net${netID}`, { id: `net${netID}`, prefix: "net", value: config.data[`net${netID}`] });
} }
}); });
@ -554,15 +539,12 @@ async function handleNetworkDelete () {
if (result === "confirm") { if (result === "confirm") {
document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg"; document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/delete`, "DELETE"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/delete`, "DELETE");
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateNetworks();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateNetworks(); populateNetworks();
} deleteBootLine(`boot-net${netID}`);
} }
}); });
} }
@ -584,15 +566,12 @@ async function handleNetworkAdd () {
} }
const netID = form.get("netid"); const netID = form.get("netid");
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/create`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateNetworks();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateNetworks(); populateNetworks();
} addBootLine("disabled", { id: `net${netID}`, prefix: "net", value: config.data[`net${netID}`] });
} }
}); });
} }
@ -679,16 +658,12 @@ async function handleDeviceConfig () {
pcie: form.get("pcie") ? 1 : 0 pcie: form.get("pcie") ? 1 : 0
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/modify`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/modify`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDevices();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDevices(); populateDevices();
} }
}
}); });
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET"); const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
@ -708,16 +683,12 @@ async function handleDeviceDelete () {
if (result === "confirm") { if (result === "confirm") {
document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg"; document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/delete`, "DELETE"); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/delete`, "DELETE");
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDevices();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDevices(); populateDevices();
} }
}
}); });
} }
@ -732,16 +703,12 @@ async function handleDeviceAdd () {
pcie: form.get("pcie") ? 1 : 0 pcie: form.get("pcie") ? 1 : 0
}; };
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/create`, "POST", body); const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/create`, "POST", body);
if (result.status === 200) { if (result.status !== 200) {
await getConfig();
populateDevices();
}
else {
alert(result.error); alert(result.error);
}
await getConfig(); await getConfig();
populateDevices(); populateDevices();
} }
}
}); });
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET"); const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
@ -756,17 +723,23 @@ async function populateBoot () {
document.querySelector("#boot-card").classList.remove("none"); document.querySelector("#boot-card").classList.remove("none");
document.querySelector("#enabled").title = "Enabled"; document.querySelector("#enabled").title = "Enabled";
document.querySelector("#disabled").title = "Disabled"; document.querySelector("#disabled").title = "Disabled";
const order = config.data.boot.replace("order=", "").split(";"); let order = [];
if (config.data.boot.startsWith("order=")) {
order = config.data.boot.replace("order=", "").split(";");
}
const bootable = { disabled: [] }; const bootable = { disabled: [] };
const eligible = bootMetaData.eligiblePrefixes; const eligible = bootMetaData.eligiblePrefixes;
for (let i = 0; i < order.length; i++) { for (let i = 0; i < order.length; i++) {
const element = order[i];
const prefix = eligible.find((pref) => order[i].startsWith(pref)); const prefix = eligible.find((pref) => order[i].startsWith(pref));
bootable[i] = { id: order[i], prefix }; const value = config.data[element];
bootable[i] = { id: element, prefix, value };
} }
Object.keys(config.data).forEach((element) => { Object.keys(config.data).forEach((element) => {
const prefix = eligible.find((pref) => element.startsWith(pref)); const prefix = eligible.find((pref) => element.startsWith(pref));
const value = config.data[element];
if (prefix && !order.includes(element)) { if (prefix && !order.includes(element)) {
bootable.disabled.push({ id: element, prefix }); bootable.disabled.push({ id: element, prefix, value });
} }
}); });
Object.keys(bootable).sort(); Object.keys(bootable).sort();
@ -783,20 +756,42 @@ async function populateBoot () {
} }
} }
function addBootLine (fieldset, bootable, before = null) { function addBootLine (container, data, before = null) {
const item = document.createElement("draggable-item"); const item = document.createElement("draggable-item");
item.bootable = bootable; item.data = data;
item.innerHTML = ` item.innerHTML = `
<img src="${bootMetaData[bootable.prefix].icon}"> <img src="${bootMetaData[data.prefix].icon}">
<p style="margin: 0px;">${bootable.id}</p> <p style="margin: 0px;">${data.id}</p>
<p style="margin: 0px; overflow-x: hidden; white-space: nowrap;">${data.value}</p>
`; `;
item.draggable = true; item.draggable = true;
item.classList.add("drop-target"); item.classList.add("drop-target");
item.id = `boot-${data.id}`;
if (before) { if (before) {
document.querySelector(`#${fieldset}`).insertBefore(item, before); document.querySelector(`#${container}`).insertBefore(item, before);
} }
else { else {
document.querySelector(`#${fieldset}`).append(item); document.querySelector(`#${container}`).append(item);
}
item.container = container;
bootEntries[item.id] = item;
}
function deleteBootLine (id) {
bootEntries[id].parentElement.removeChild(bootEntries[id]);
}
function updateBootLine (id, newData) {
const element = bootEntries[id];
if (element) {
const container = element.container;
const before = element.nextSibling;
deleteBootLine(id);
addBootLine(container, newData, before);
return true;
}
else {
return false;
} }
} }

View File

@ -3,7 +3,6 @@ class DraggableContainer extends HTMLElement {
super(); super();
this.attachShadow({ mode: "open" }); this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="css/style.css">
<label id="title"></label> <label id="title"></label>
<div id="wrapper"> <div id="wrapper">
<draggable-item id="bottom" class="drop-target"></draggable-item> <draggable-item id="bottom" class="drop-target"></draggable-item>
@ -29,20 +28,40 @@ class DraggableContainer extends HTMLElement {
insertBefore (newNode, referenceNode) { insertBefore (newNode, referenceNode) {
this.content.insertBefore(newNode, referenceNode); this.content.insertBefore(newNode, referenceNode);
} }
deleteItemByID (nodeid) {
const node = this.content.querySelector(`#${nodeid}`);
if (node) {
this.content.removeChild(node);
return true;
}
else {
return false;
}
}
} }
class DraggableItem extends HTMLElement { class DraggableItem extends HTMLElement {
constructor () { constructor () {
super(); super();
this.attachShadow({ mode: "open" }); this.attachShadow({ mode: "open" });
// for whatever reason, only grid layout seems to respect the parent's content bounds
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="css/style.css">
<style> <style>
#wrapper { #wrapper {
min-height: 1.5em; grid-template-columns: auto 8ch 1fr;
display: grid;
column-gap: 10px;
align-items: center;
}
img {
height: 1em;
width: 1em;
} }
</style> </style>
<div id="wrapper" class="flex row"></div> <div id="wrapper">
<div style="min-height: 1.5em;"></div>
</div>
`; `;
this.content = this.shadowRoot.querySelector("#wrapper"); this.content = this.shadowRoot.querySelector("#wrapper");
// add drag and drop listeners // add drag and drop listeners
@ -83,13 +102,14 @@ class DraggableItem extends HTMLElement {
event.target.borderTop = false; event.target.borderTop = false;
} }
if (event.target.classList.contains("drop-target")) { if (event.target.classList.contains("drop-target")) {
const data = event.dataTransfer.getData("application/json"); const data = JSON.parse(event.dataTransfer.getData("application/json"));
const content = event.dataTransfer.getData("text/html"); const content = event.dataTransfer.getData("text/html");
const item = document.createElement("draggable-item"); const item = document.createElement("draggable-item");
item.bootable = data; item.data = data;
item.innerHTML = content; item.innerHTML = content;
item.draggable = true; item.draggable = true;
item.classList.add("drop-target"); item.classList.add("drop-target");
item.id = `boot-${data.id}`;
event.target.parentElement.insertBefore(item, event.target); event.target.parentElement.insertBefore(item, event.target);
} }
this.content.attributeStyleMap.clear(); this.content.attributeStyleMap.clear();
@ -105,14 +125,6 @@ class DraggableItem extends HTMLElement {
this.content.innerHTML = innerHTML; this.content.innerHTML = innerHTML;
} }
get bootable () {
return this.data;
}
set bootable (bootable) {
this.data = bootable;
}
get borderTop () { get borderTop () {
return this.content.style.borderTop === ""; return this.content.style.borderTop === "";
} }