From e66d3ceb33b36ef04610076ecb9c4134658f379d Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Thu, 29 Jun 2023 22:20:15 +0000 Subject: [PATCH] add js linting, fix js linting issues --- .eslintrc.json | 42 ++ .gitignore | 4 +- package.json | 16 + scripts/account.js | 51 +- scripts/config.js | 1654 ++++++++++++++++++++++---------------------- scripts/dialog.js | 12 +- scripts/index.js | 840 +++++++++++----------- scripts/login.js | 86 +-- scripts/utils.js | 506 +++++++------- 9 files changed, 1643 insertions(+), 1568 deletions(-) create mode 100644 .eslintrc.json create mode 100644 package.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a328d17 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,42 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": "standard", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "no-tabs": [ + "error", + { + "allowIndentationTabs": true + } + ], + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "brace-style": [ + "error", + "stroustrup", + { + "allowSingleLine": false + } + ] + } +} diff --git a/.gitignore b/.gitignore index 55a914d..8c9746f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -vars.js \ No newline at end of file +vars.js +**/package-lock.json +**/node_modules \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3725374 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "proxmoxaas-client", + "version": "0.0.1", + "description": "Front-end for ProxmoxAAS", + "type": "module", + "scripts": { + "lint": "eslint --fix ." + }, + "devDependencies": { + "eslint": "^8.43.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^16.0.1", + "eslint-plugin-promise": "^6.1.1" + } +} diff --git a/scripts/account.js b/scripts/account.js index 131b845..de64aa5 100644 --- a/scripts/account.js +++ b/scripts/account.js @@ -2,7 +2,7 @@ import { requestAPI, goToPage, getCookie, setTitleAndHeader } from "./utils.js"; window.addEventListener("DOMContentLoaded", init); -let prefixes = { +const prefixes = { 1024: [ "", "Ki", @@ -17,17 +17,17 @@ let prefixes = { "G", "T" ] -} +}; -async function init() { +async function init () { setTitleAndHeader(); - let cookie = document.cookie; + const cookie = document.cookie; if (cookie === "") { goToPage("login.html"); } - let resources = await requestAPI("/user/resources"); - let instances = await requestAPI("/user/config/cluster"); - let nodes = await requestAPI("/user/config/nodes"); + const resources = await requestAPI("/user/resources"); + const instances = await requestAPI("/user/config/cluster"); + const nodes = await requestAPI("/user/config/nodes"); document.querySelector("#username").innerText = `Username: ${getCookie("username")}`; document.querySelector("#pool").innerText = `Pool: ${instances.pool}`; document.querySelector("#vmid").innerText = `VMID Range: ${instances.vmid.min} - ${instances.vmid.max}`; @@ -35,24 +35,25 @@ async function init() { buildResourceTable(resources, "#resource-table"); } -function buildResourceTable(resources, tableid) { +function buildResourceTable (resources, tableid) { if (resources instanceof Object) { - let table = document.querySelector(tableid); - let tbody = table.querySelector("tbody"); + const table = document.querySelector(tableid); + const tbody = table.querySelector("tbody"); Object.keys(resources.resources).forEach((element) => { if (resources.resources[element].display) { if (resources.resources[element].type === "list") { - + // TODO + console.error("Unimplemented"); } else { - let row = tbody.insertRow(); - let key = row.insertCell(); + const row = tbody.insertRow(); + const key = row.insertCell(); key.innerText = `${element}`; - let used = row.insertCell(); + const used = row.insertCell(); used.innerText = `${parseNumber(resources.used[element], resources.resources[element])}`; - let val = row.insertCell(); + const val = row.insertCell(); val.innerText = `${parseNumber(resources.avail[element], resources.resources[element])}`; - let total = row.insertCell(); + const total = row.insertCell(); total.innerText = `${parseNumber(resources.max[element], resources.resources[element])}`; } } @@ -60,22 +61,22 @@ function buildResourceTable(resources, tableid) { } } -function parseNumber(value, unitData) { - let compact = unitData.compact; - let multiplier = unitData.multiplier; - let base = unitData.base; - let unit = unitData.unit; +function parseNumber (value, unitData) { + const compact = unitData.compact; + const multiplier = unitData.multiplier; + const base = unitData.base; + const unit = unitData.unit; value = multiplier * value; if (value <= 0) { return `0 ${unit}`; } else if (compact) { - let exponent = Math.floor(Math.log(value) / Math.log(base)); + const exponent = Math.floor(Math.log(value) / Math.log(base)); value = value / base ** exponent; - let unitPrefix = prefixes[base][exponent]; - return `${value} ${unitPrefix}${unit}` + const unitPrefix = prefixes[base][exponent]; + return `${value} ${unitPrefix}${unit}`; } else { return `${value} ${unit}`; } -} \ No newline at end of file +} diff --git a/scripts/config.js b/scripts/config.js index b4303e7..d4bebc4 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -1,824 +1,830 @@ -import { requestPVE, requestAPI, goToPage, getURIData, resources_config, setTitleAndHeader } from "./utils.js"; -import { alert, dialog } from "./dialog.js"; - -window.addEventListener("DOMContentLoaded", init); // do the dumb thing where the disk config refreshes every second - -let diskMetaData = resources_config.disk; -let networkMetaData = resources_config.network; -let pcieMetaData = resources_config.pcie; - -let node; -let type; -let vmid; -let config; - -async function init() { - setTitleAndHeader(); - let cookie = document.cookie; - if (cookie === "") { - goToPage("login.html"); - } - - let uriData = getURIData(); - node = uriData.node; - type = uriData.type; - vmid = uriData.vmid; - - await getConfig(); - - populateResources(); - populateDisk(); - populateNetworks(); - populateDevices(); - - document.querySelector("#exit").addEventListener("click", handleFormExit); -} - -function getOrdered(keys) { - let ordered_keys = Object.keys(keys).sort((a, b) => { parseInt(a) - parseInt(b) }); // ordered integer list - return ordered_keys; -} - -async function getConfig() { - config = await requestPVE(`/nodes/${node}/${type}/${vmid}/config`, "GET"); -} - -async function populateResources() { - let name = type === "qemu" ? "name" : "hostname"; - document.querySelector("#name").innerHTML = document.querySelector("#name").innerHTML.replace("%{vmname}", config.data[name]); - if (type === "qemu") { - let global = await requestAPI("/global/config/resources"); - let user = await requestAPI("/user/config/resources"); - let options = []; - if (global.cpu.whitelist) { - options = user.max.cpu.sort((a, b) => { return a.localeCompare(b) }); - } - else { - let supported = await requestPVE(`/nodes/${node}/capabilities/qemu/cpu`); - supported.data.forEach((element) => { - if (!user.max.cpu.includes(element.name)) { - options.push(element.name); - } - }); - options = options.sort((a, b) => { return a.localeCompare(b) }) - console.log(options); - console.log("blacklist not yet supported") - } - addResourceLine("resources", "images/resources/cpu.svg", "select", "CPU Type", "proctype", { value: config.data.cpu, options: options }); - } - addResourceLine("resources", "images/resources/cpu.svg", "input", "CPU Amount", "cores", { type: "number", value: config.data.cores, min: 1, max: 8192 }, "Cores"); - addResourceLine("resources", "images/resources/ram.svg", "input", "Memory", "ram", { type: "number", value: config.data.memory, min: 16, step: 1 }, "MiB"); - if (type === "lxc") { - addResourceLine("resources", "images/resources/swap.svg", "input", "Swap", "swap", { type: "number", value: config.data.swap, min: 0, step: 1 }, "MiB"); - } -} - -function addResourceLine(fieldset, iconHref, type, labelText, id, attributes, unitText = null) { - let field = document.querySelector(`#${fieldset}`); - - let icon = document.createElement("img"); - icon.src = iconHref; - icon.alt = labelText; - field.append(icon); - - let label = document.createElement("label"); - label.innerText = labelText; - label.htmlFor = labelText; - field.append(label); - - if (type === "input") { - let input = document.createElement("input"); - for (let k in attributes) { - input.setAttribute(k, attributes[k]) - } - input.id = id; - input.name = id; - input.required = true; - input.classList.add("w3-input"); - input.classList.add("w3-border"); - field.append(input); - } - else if (type === "select") { - let select = document.createElement("select"); - for (let option of attributes.options) { - select.append(new Option(option)); - } - select.value = attributes.value; - select.id = id; - select.name = id; - select.required = true; - select.classList.add("w3-select"); - select.classList.add("w3-border"); - field.append(select); - } - - if (unitText) { - let unit = document.createElement("p"); - unit.innerText = unitText; - field.append(unit); - } - else { - let unit = document.createElement("div"); - unit.classList.add("hidden"); - field.append(unit); - } -} - -async function populateDisk() { - document.querySelector("#disks").innerHTML = ""; - for (let i = 0; i < diskMetaData[type].prefixOrder.length; i++) { - let prefix = diskMetaData[type].prefixOrder[i]; - let busName = diskMetaData[type][prefix].name; - let disks = {}; - Object.keys(config.data).forEach((element) => { - if (element.startsWith(prefix)) { - disks[element.replace(prefix, "")] = config.data[element]; - } - }); - let ordered_keys = getOrdered(disks); - ordered_keys.forEach((element) => { - let disk = disks[element]; - addDiskLine("disks", prefix, busName, element, disk); - }); - } - document.querySelector("#disk-add").addEventListener("click", handleDiskAdd); - - if (type === "qemu") { - document.querySelector("#cd-add").classList.remove("none"); - document.querySelector("#cd-add").addEventListener("click", handleCDAdd); - } -} - -function addDiskLine(fieldset, busPrefix, busName, device, diskDetails) { - let field = document.querySelector(`#${fieldset}`); - - let diskName = `${busName} ${device}`; - let diskID = `${busPrefix}${device}`; - - // Set the disk icon, either drive.svg or disk.svg - let icon = document.createElement("img"); - icon.src = diskMetaData[type][busPrefix].icon; - icon.alt = diskName; - icon.dataset.disk = diskID; - field.append(icon); - - // Add a label for the disk bus and device number - let diskLabel = document.createElement("label"); - diskLabel.innerText = diskName; - diskLabel.dataset.disk = diskID; - field.append(diskLabel); - - // Add text of the disk configuration - let diskDesc = document.createElement("p"); - diskDesc.innerText = diskDetails; - diskDesc.dataset.disk = diskID; - diskDesc.style.overflowX = "hidden"; - diskDesc.style.whiteSpace = "nowrap"; - field.append(diskDesc); - - let actionDiv = document.createElement("div"); - diskMetaData.actionBarOrder.forEach((element) => { - let action = document.createElement("img"); - if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("attach")) { // attach - action.src = "images/actions/disk/attach.svg"; - action.title = "Attach Disk"; - action.addEventListener("click", handleDiskAttach); - action.classList.add("clickable"); - } - else if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("detach")) { // detach - action.src = "images/actions/disk/detach.svg"; - action.title = "Detach Disk"; - action.addEventListener("click", handleDiskDetach); - action.classList.add("clickable"); - } - else if (element === "delete") { - let active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize - action.src = `images/actions/delete-${active}.svg`; - action.title = "Delete Disk"; - if (active === "active") { - action.addEventListener("click", handleDiskDelete); - action.classList.add("clickable"); - } - } - else { - let active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize - action.src = `images/actions/disk/${element}-${active}.svg`; - if (active === "active") { - action.title = `${element.charAt(0).toUpperCase()}${element.slice(1)} Disk`; - if (element === "move") { - action.addEventListener("click", handleDiskMove); - } - else if (element === "resize") { - action.addEventListener("click", handleDiskResize); - } - action.classList.add("clickable"); - } - } - action.dataset.disk = diskID; - action.alt = action.title; - actionDiv.append(action); - }); - field.append(actionDiv); -} - -async function handleDiskDetach() { - let header = `Detach ${this.dataset.disk}`; - let body = `

Are you sure you want to detach disk

${this.dataset.disk}

`; - dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - disk: this.dataset.disk - }; - let result = await requestAPI("/instance/disk/detach", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - } - else { - alert(result.error); - await getConfig(); - populateDisk(); - } - } - }); -} - -async function handleDiskAttach() { - let header = `Attach ${this.dataset.disk}`; - let body = ``; - - dialog(header, body, async (result, form) => { - if (result === "confirm") { - let device = form.get("device"); - document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - disk: `${type === "qemu" ? "sata" : "mp"}${device}`, - source: this.dataset.disk.replace("unused", "") - } - let result = await requestAPI("/instance/disk/attach", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - } - else { - alert(result.error); - await getConfig(); - populateDisk(); - } - } - }); -} - -async function handleDiskResize() { - let header = `Resize ${this.dataset.disk}`; - let body = ``; - - dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - disk: this.dataset.disk, - size: form.get("size-increment") - } - let result = await requestAPI("/instance/disk/resize", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - } - else { - alert(result.error); - await getConfig(); - populateDisk(); - } - } - }); -} - -async function handleDiskMove() { - let content = type === "qemu" ? "images" : "rootdir"; - let storage = await requestPVE(`/nodes/${node}/storage`, "GET"); - - let header = `Move ${this.dataset.disk}`; - - let options = ""; - storage.data.forEach((element) => { - if (element.content.includes(content)) { - options += `"`; - } - }); - let select = ``; - - let body = ` - ${select} - - `; - - dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - disk: this.dataset.disk, - storage: form.get("storage-select"), - delete: form.get("delete-check") === "on" ? "1" : "0" - } - let result = await requestAPI("/instance/disk/move", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - } - else { - alert(result.error); - await getConfig(); - populateDisk(); - } - } - }); -} - -async function handleDiskDelete() { - let header = `Delete ${this.dataset.disk}`; - let body = `

Are you sure you want to delete disk

${this.dataset.disk}

`; - - dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - disk: this.dataset.disk - }; - let result = await requestAPI("/instance/disk/delete", "DELETE", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - } - else { - alert(result.error); - await getConfig(); - populateDisk(); - } - } - }); -} - -async function handleDiskAdd() { - let content = type === "qemu" ? "images" : "rootdir"; - let storage = await requestPVE(`/nodes/${node}/storage`, "GET"); - - let header = "Create New Disk"; - - let options = ""; - storage.data.forEach((element) => { - if (element.content.includes(content)) { - options += `"`; - } - }); - let select = ``; - - let body = ` - - ${select} - - `; - - dialog(header, body, async (result, form) => { - if (result === "confirm") { - let body = { - node: node, - type: type, - vmid: vmid, - disk: `${type === "qemu" ? "sata" : "mp"}${form.get("device")}`, - storage: form.get("storage-select"), - size: form.get("size") - }; - let result = await requestAPI("/instance/disk/create", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - } - else { - alert(result.error); - await getConfig(); - populateDisk(); - } - } - }); -} - -async function handleCDAdd() { - let content = "iso"; - let storage = await requestPVE(`/nodes/${node}/storage`, "GET"); - - let header = `Add a CDROM`; - - let storageOptions = ""; - storage.data.forEach((element) => { - if (element.content.includes(content)) { - storageOptions += `"`; - } - }); - let storageSelect = ``; - - let body = ` - - ${storageSelect} - - `; - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - let body = { - node: node, - type: type, - vmid: vmid, - disk: `ide${form.get("device")}`, - iso: form.get("iso-select") - }; - let result = await requestAPI("/instance/disk/create", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - } - else { - alert(result.error); - await getConfig(); - populateDisk(); - } - } - }); - - d.querySelector("#storage-select").addEventListener("change", async () => { - let storage = document.querySelector("#storage-select").value; - let ISOSelect = document.querySelector("#iso-select"); - ISOSelect.innerHTML = ``; - let isos = await requestPVE(`/nodes/${node}/storage/${storage}/content`, "GET", { content: content }); - isos.data.forEach((element) => { - if (element.content.includes(content)) { - ISOSelect.append(new Option(element.volid.replace(`${storage}:${content}/`, ""), element.volid)); - } - }); - }); -} - -async function populateNetworks() { - document.querySelector("#networks").innerHTML = ""; - let networks = {}; - let prefix = networkMetaData.prefix; - Object.keys(config.data).forEach((element) => { - if (element.startsWith(prefix)) { - networks[element.replace(prefix, "")] = config.data[element]; - } - }); - let ordered_keys = getOrdered(networks); - ordered_keys.forEach((element) => { - addNetworkLine("networks", prefix, element, networks[element]); - }); - - document.querySelector("#network-add").addEventListener("click", handleNetworkAdd) -} - -function addNetworkLine(fieldset, prefix, netID, netDetails) { - let field = document.querySelector(`#${fieldset}`); - - let icon = document.createElement("img"); - icon.src = "images/resources/network.svg"; - icon.alt = `${prefix}${netID}`; - icon.dataset.network = netID; - icon.dataset.values = netDetails; - field.appendChild(icon); - - let netLabel = document.createElement("label"); - netLabel.innerText = `${prefix}${netID}`; - netLabel.dataset.network = netID; - netLabel.dataset.values = netDetails; - field.append(netLabel); - - let netDesc = document.createElement("p"); - netDesc.innerText = netDetails; - netDesc.dataset.network = netID; - netDesc.dataset.values = netDetails; - netDesc.style.overflowX = "hidden"; - netDesc.style.whiteSpace = "nowrap"; - field.append(netDesc); - - let actionDiv = document.createElement("div"); - - let configBtn = document.createElement("img"); - configBtn.classList.add("clickable"); - configBtn.src = `images/actions/network/config.svg`; - configBtn.title = "Config Interface"; - configBtn.addEventListener("click", handleNetworkConfig); - configBtn.dataset.network = netID; - configBtn.dataset.values = netDetails; - actionDiv.appendChild(configBtn); - - let deleteBtn = document.createElement("img"); - deleteBtn.classList.add("clickable"); - deleteBtn.src = `images/actions/delete-active.svg`; - deleteBtn.title = "Delete Interface"; - deleteBtn.addEventListener("click", handleNetworkDelete); - deleteBtn.dataset.network = netID; - deleteBtn.dataset.values = netDetails; - actionDiv.appendChild(deleteBtn); - - field.append(actionDiv); -} - -async function handleNetworkConfig() { - let netID = this.dataset.network; - let netDetails = this.dataset.values; - let header = `Edit net${netID}`; - let body = ``; - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - netid: netID, - rate: form.get("rate") - } - let result = await requestAPI("/instance/network/modify", "POST", body); - if (result.status === 200) { - await getConfig(); - populateNetworks(); - } - else { - alert(result.error); - await getConfig(); - populateNetworks(); - } - } - }); - - d.querySelector("#rate").value = netDetails.split("rate=")[1].split(",")[0]; -} - -async function handleNetworkDelete() { - let netID = this.dataset.network; - let header = `Delete net${netID}`; - let body = ``; - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - netid: netID - } - let result = await requestAPI("/instance/network/delete", "DELETE", body); - if (result.status === 200) { - await getConfig(); - populateNetworks(); - } - else { - alert(result.error); - await getConfig(); - populateNetworks(); - } - } - }); -} - -async function handleNetworkAdd() { - let header = `Create Network Interface`; - let body = ``; - if (type === "lxc") { - body += ``; - } - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - let body = { - node: node, - type: type, - vmid: vmid, - netid: form.get("netid"), - rate: form.get("rate") - } - if (type === "lxc") { - body.name = form.get("name") - } - let result = await requestAPI("/instance/network/create", "POST", body); - if (result.status === 200) { - await getConfig(); - populateNetworks(); - } - else { - alert(result.error); - await getConfig(); - populateNetworks(); - } - } - }); -} - -async function populateDevices() { - if (type === "qemu") { - document.querySelector("#devices-card").classList.remove("none"); - document.querySelector("#devices").innerHTML = ""; - let devices = {}; - let prefix = pcieMetaData.prefix; - Object.keys(config.data).forEach((element) => { - if (element.startsWith(prefix)) { - devices[element.replace(prefix, "")] = config.data[element]; - } - }); - let ordered_keys = getOrdered(devices); - ordered_keys.forEach(async (element) => { - let deviceData = await requestAPI(`/instance/pci?node=${node}&type=${type}&vmid=${vmid}&hostpci=${element}`, "GET"); - addDeviceLine("devices", prefix, element, devices[element], deviceData.device_name); - }); - - document.querySelector("#device-add").addEventListener("click", handleDeviceAdd) - } -} - -function addDeviceLine(fieldset, prefix, deviceID, deviceDetails, deviceName) { - let field = document.querySelector(`#${fieldset}`); - - let icon = document.createElement("img"); - icon.src = "images/resources/device.svg"; - icon.alt = `${prefix}${deviceID}`; - icon.dataset.device = deviceID; - icon.dataset.values = deviceDetails; - icon.dataset.name = deviceName; - field.appendChild(icon); - - let deviceLabel = document.createElement("p"); - - deviceLabel.innerText = deviceName; - deviceLabel.dataset.device = deviceID; - deviceLabel.dataset.values = deviceDetails; - deviceLabel.dataset.name = deviceName; - deviceLabel.style.overflowX = "hidden"; - deviceLabel.style.whiteSpace = "nowrap"; - field.append(deviceLabel); - - let actionDiv = document.createElement("div"); - - let configBtn = document.createElement("img"); - configBtn.classList.add("clickable"); - configBtn.src = `images/actions/device/config.svg`; - configBtn.title = "Config Device"; - configBtn.addEventListener("click", handleDeviceConfig); - configBtn.dataset.device = deviceID; - configBtn.dataset.values = deviceDetails; - configBtn.dataset.name = deviceName; - actionDiv.appendChild(configBtn); - - let deleteBtn = document.createElement("img"); - deleteBtn.classList.add("clickable"); - deleteBtn.src = `images/actions/delete-active.svg`; - deleteBtn.title = "Delete Device"; - deleteBtn.addEventListener("click", handleDeviceDelete); - deleteBtn.dataset.device = deviceID; - deleteBtn.dataset.values = deviceDetails; - deleteBtn.dataset.name = deviceName; - actionDiv.appendChild(deleteBtn); - - field.append(actionDiv); -} - -async function handleDeviceConfig() { - let deviceID = this.dataset.device; - let deviceDetails = this.dataset.values; - let deviceName = this.dataset.name; - let header = `Edit Expansion Card ${deviceID}`; - let body = ``; - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - hostpci: deviceID, - device: form.get("device"), - pcie: form.get("pcie") ? 1 : 0 - } - let result = await requestAPI("/instance/pci/modify", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDevices(); - } - else { - alert(result.error); - await getConfig(); - populateDevices(); - } - } - }); - - let availDevices = await requestAPI(`/nodes/pci?node=${node}`, "GET"); - d.querySelector("#device").append(new Option(deviceName, deviceDetails.split(",")[0])); - for (let availDevice of availDevices) { - d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id)); - } - d.querySelector("#pcie").checked = deviceDetails.includes("pcie=1"); -} - -async function handleDeviceDelete() { - let deviceID = this.dataset.device; - let header = `Remove Expansion Card ${deviceID}`; - let body = ``; - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg"; - let body = { - node: node, - type: type, - vmid: vmid, - hostpci: deviceID - } - let result = await requestAPI("/instance/pci/delete", "DELETE", body); - if (result.status === 200) { - await getConfig(); - populateDevices(); - } - else { - alert(result.error); - await getConfig(); - populateDevices(); - } - } - }); -} - -async function handleDeviceAdd() { - let header = `Add Expansion Card`; - let body = ``; - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - let body = { - node: node, - type: type, - vmid: vmid, - device: form.get("device"), - pcie: form.get("pcie") ? 1 : 0 - } - let result = await requestAPI("/instance/pci/create", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDevices(); - } - else { - alert(result.error); - await getConfig(); - populateDevices(); - } - } - }); - - let availDevices = await requestAPI(`/nodes/pci?node=${node}`, "GET"); - for (let availDevice of availDevices) { - d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id)); - } - d.querySelector("#pcie").checked = true; -} - -async function handleFormExit() { - let body = { - node: node, - type: type, - vmid: vmid, - cores: document.querySelector("#cores").value, - memory: document.querySelector("#ram").value - } - if (type === "lxc") { - body.swap = document.querySelector("#swap").value; - } - else if (type === "qemu") { - body.proctype = document.querySelector("#proctype").value; - } - let result = await requestAPI("/instance/resources", "POST", body); - if (result.status === 200) { - await getConfig(); - populateDisk(); - goToPage("index.html"); - } - else { - alert(result.error); - } -} \ No newline at end of file +import { requestPVE, requestAPI, goToPage, getURIData, resourcesConfig, setTitleAndHeader } from "./utils.js"; +import { alert, dialog } from "./dialog.js"; + +window.addEventListener("DOMContentLoaded", init); // do the dumb thing where the disk config refreshes every second + +const diskMetaData = resourcesConfig.disk; +const networkMetaData = resourcesConfig.network; +const pcieMetaData = resourcesConfig.pcie; + +let node; +let type; +let vmid; +let config; + +async function init () { + setTitleAndHeader(); + const cookie = document.cookie; + if (cookie === "") { + goToPage("login.html"); + } + + const uriData = getURIData(); + node = uriData.node; + type = uriData.type; + vmid = uriData.vmid; + + await getConfig(); + + populateResources(); + populateDisk(); + populateNetworks(); + populateDevices(); + + document.querySelector("#exit").addEventListener("click", handleFormExit); +} + +function getOrdered (keys) { + const orderedKeys = Object.keys(keys).sort((a, b) => { + return parseInt(a) - parseInt(b); + }); // ordered integer list + return orderedKeys; +} + +async function getConfig () { + config = await requestPVE(`/nodes/${node}/${type}/${vmid}/config`, "GET"); +} + +async function populateResources () { + const name = type === "qemu" ? "name" : "hostname"; + document.querySelector("#name").innerHTML = document.querySelector("#name").innerHTML.replace("%{vmname}", config.data[name]); + if (type === "qemu") { + const global = await requestAPI("/global/config/resources"); + const user = await requestAPI("/user/config/resources"); + let options = []; + if (global.cpu.whitelist) { + options = user.max.cpu.sort((a, b) => { + return a.localeCompare(b); + }); + } + else { + const supported = await requestPVE(`/nodes/${node}/capabilities/qemu/cpu`); + supported.data.forEach((element) => { + if (!user.max.cpu.includes(element.name)) { + options.push(element.name); + } + }); + options = options.sort((a, b) => { + return a.localeCompare(b); + }); + console.log(options); + console.log("blacklist not yet supported"); + } + addResourceLine("resources", "images/resources/cpu.svg", "select", "CPU Type", "proctype", { value: config.data.cpu, options }); + } + addResourceLine("resources", "images/resources/cpu.svg", "input", "CPU Amount", "cores", { type: "number", value: config.data.cores, min: 1, max: 8192 }, "Cores"); + addResourceLine("resources", "images/resources/ram.svg", "input", "Memory", "ram", { type: "number", value: config.data.memory, min: 16, step: 1 }, "MiB"); + if (type === "lxc") { + addResourceLine("resources", "images/resources/swap.svg", "input", "Swap", "swap", { type: "number", value: config.data.swap, min: 0, step: 1 }, "MiB"); + } +} + +function addResourceLine (fieldset, iconHref, type, labelText, id, attributes, unitText = null) { + const field = document.querySelector(`#${fieldset}`); + + const icon = document.createElement("img"); + icon.src = iconHref; + icon.alt = labelText; + field.append(icon); + + const label = document.createElement("label"); + label.innerText = labelText; + label.htmlFor = labelText; + field.append(label); + + if (type === "input") { + const input = document.createElement("input"); + for (const k in attributes) { + input.setAttribute(k, attributes[k]); + } + input.id = id; + input.name = id; + input.required = true; + input.classList.add("w3-input"); + input.classList.add("w3-border"); + field.append(input); + } + else if (type === "select") { + const select = document.createElement("select"); + for (const option of attributes.options) { + select.append(new Option(option)); + } + select.value = attributes.value; + select.id = id; + select.name = id; + select.required = true; + select.classList.add("w3-select"); + select.classList.add("w3-border"); + field.append(select); + } + + if (unitText) { + const unit = document.createElement("p"); + unit.innerText = unitText; + field.append(unit); + } + else { + const unit = document.createElement("div"); + unit.classList.add("hidden"); + field.append(unit); + } +} + +async function populateDisk () { + document.querySelector("#disks").innerHTML = ""; + for (let i = 0; i < diskMetaData[type].prefixOrder.length; i++) { + const prefix = diskMetaData[type].prefixOrder[i]; + const busName = diskMetaData[type][prefix].name; + const disks = {}; + Object.keys(config.data).forEach((element) => { + if (element.startsWith(prefix)) { + disks[element.replace(prefix, "")] = config.data[element]; + } + }); + const orderedKeys = getOrdered(disks); + orderedKeys.forEach((element) => { + const disk = disks[element]; + addDiskLine("disks", prefix, busName, element, disk); + }); + } + document.querySelector("#disk-add").addEventListener("click", handleDiskAdd); + + if (type === "qemu") { + document.querySelector("#cd-add").classList.remove("none"); + document.querySelector("#cd-add").addEventListener("click", handleCDAdd); + } +} + +function addDiskLine (fieldset, busPrefix, busName, device, diskDetails) { + const field = document.querySelector(`#${fieldset}`); + + const diskName = `${busName} ${device}`; + const diskID = `${busPrefix}${device}`; + + // Set the disk icon, either drive.svg or disk.svg + const icon = document.createElement("img"); + icon.src = diskMetaData[type][busPrefix].icon; + icon.alt = diskName; + icon.dataset.disk = diskID; + field.append(icon); + + // Add a label for the disk bus and device number + const diskLabel = document.createElement("label"); + diskLabel.innerText = diskName; + diskLabel.dataset.disk = diskID; + field.append(diskLabel); + + // Add text of the disk configuration + const diskDesc = document.createElement("p"); + diskDesc.innerText = diskDetails; + diskDesc.dataset.disk = diskID; + diskDesc.style.overflowX = "hidden"; + diskDesc.style.whiteSpace = "nowrap"; + field.append(diskDesc); + + const actionDiv = document.createElement("div"); + diskMetaData.actionBarOrder.forEach((element) => { + const action = document.createElement("img"); + if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("attach")) { // attach + action.src = "images/actions/disk/attach.svg"; + action.title = "Attach Disk"; + action.addEventListener("click", handleDiskAttach); + action.classList.add("clickable"); + } + else if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("detach")) { // detach + action.src = "images/actions/disk/detach.svg"; + action.title = "Detach Disk"; + action.addEventListener("click", handleDiskDetach); + action.classList.add("clickable"); + } + else if (element === "delete") { + const active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize + action.src = `images/actions/delete-${active}.svg`; + action.title = "Delete Disk"; + if (active === "active") { + action.addEventListener("click", handleDiskDelete); + action.classList.add("clickable"); + } + } + else { + const active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize + action.src = `images/actions/disk/${element}-${active}.svg`; + if (active === "active") { + action.title = `${element.charAt(0).toUpperCase()}${element.slice(1)} Disk`; + if (element === "move") { + action.addEventListener("click", handleDiskMove); + } + else if (element === "resize") { + action.addEventListener("click", handleDiskResize); + } + action.classList.add("clickable"); + } + } + action.dataset.disk = diskID; + action.alt = action.title; + actionDiv.append(action); + }); + field.append(actionDiv); +} + +async function handleDiskDetach () { + const header = `Detach ${this.dataset.disk}`; + const body = `

Are you sure you want to detach disk

${this.dataset.disk}

`; + dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + disk: this.dataset.disk + }; + const result = await requestAPI("/instance/disk/detach", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + } + else { + alert(result.error); + await getConfig(); + populateDisk(); + } + } + }); +} + +async function handleDiskAttach () { + const header = `Attach ${this.dataset.disk}`; + const body = ``; + + dialog(header, body, async (result, form) => { + if (result === "confirm") { + const device = form.get("device"); + document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + disk: `${type === "qemu" ? "sata" : "mp"}${device}`, + source: this.dataset.disk.replace("unused", "") + }; + const result = await requestAPI("/instance/disk/attach", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + } + else { + alert(result.error); + await getConfig(); + populateDisk(); + } + } + }); +} + +async function handleDiskResize () { + const header = `Resize ${this.dataset.disk}`; + const body = ""; + + dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + disk: this.dataset.disk, + size: form.get("size-increment") + }; + const result = await requestAPI("/instance/disk/resize", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + } + else { + alert(result.error); + await getConfig(); + populateDisk(); + } + } + }); +} + +async function handleDiskMove () { + const content = type === "qemu" ? "images" : "rootdir"; + const storage = await requestPVE(`/nodes/${node}/storage`, "GET"); + + const header = `Move ${this.dataset.disk}`; + + let options = ""; + storage.data.forEach((element) => { + if (element.content.includes(content)) { + options += `"`; + } + }); + const select = ``; + + const body = ` + ${select} + + `; + + dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + disk: this.dataset.disk, + storage: form.get("storage-select"), + delete: form.get("delete-check") === "on" ? "1" : "0" + }; + const result = await requestAPI("/instance/disk/move", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + } + else { + alert(result.error); + await getConfig(); + populateDisk(); + } + } + }); +} + +async function handleDiskDelete () { + const header = `Delete ${this.dataset.disk}`; + const body = `

Are you sure you want to delete disk

${this.dataset.disk}

`; + + dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + disk: this.dataset.disk + }; + const result = await requestAPI("/instance/disk/delete", "DELETE", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + } + else { + alert(result.error); + await getConfig(); + populateDisk(); + } + } + }); +} + +async function handleDiskAdd () { + const content = type === "qemu" ? "images" : "rootdir"; + const storage = await requestPVE(`/nodes/${node}/storage`, "GET"); + + const header = "Create New Disk"; + + let options = ""; + storage.data.forEach((element) => { + if (element.content.includes(content)) { + options += `"`; + } + }); + const select = ``; + + const body = ` + + ${select} + + `; + + dialog(header, body, async (result, form) => { + if (result === "confirm") { + const body = { + node, + type, + vmid, + disk: `${type === "qemu" ? "sata" : "mp"}${form.get("device")}`, + storage: form.get("storage-select"), + size: form.get("size") + }; + const result = await requestAPI("/instance/disk/create", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + } + else { + alert(result.error); + await getConfig(); + populateDisk(); + } + } + }); +} + +async function handleCDAdd () { + const content = "iso"; + const storage = await requestPVE(`/nodes/${node}/storage`, "GET"); + + const header = "Add a CDROM"; + + let storageOptions = ""; + storage.data.forEach((element) => { + if (element.content.includes(content)) { + storageOptions += `"`; + } + }); + const storageSelect = ``; + + const body = ` + + ${storageSelect} + + `; + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + const body = { + node, + type, + vmid, + disk: `ide${form.get("device")}`, + iso: form.get("iso-select") + }; + const result = await requestAPI("/instance/disk/create", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + } + else { + alert(result.error); + await getConfig(); + populateDisk(); + } + } + }); + + d.querySelector("#storage-select").addEventListener("change", async () => { + const storage = document.querySelector("#storage-select").value; + const ISOSelect = document.querySelector("#iso-select"); + ISOSelect.innerHTML = ""; + const isos = await requestPVE(`/nodes/${node}/storage/${storage}/content`, "GET", { content }); + isos.data.forEach((element) => { + if (element.content.includes(content)) { + ISOSelect.append(new Option(element.volid.replace(`${storage}:${content}/`, ""), element.volid)); + } + }); + }); +} + +async function populateNetworks () { + document.querySelector("#networks").innerHTML = ""; + const networks = {}; + const prefix = networkMetaData.prefix; + Object.keys(config.data).forEach((element) => { + if (element.startsWith(prefix)) { + networks[element.replace(prefix, "")] = config.data[element]; + } + }); + const orderedKeys = getOrdered(networks); + orderedKeys.forEach((element) => { + addNetworkLine("networks", prefix, element, networks[element]); + }); + + document.querySelector("#network-add").addEventListener("click", handleNetworkAdd); +} + +function addNetworkLine (fieldset, prefix, netID, netDetails) { + const field = document.querySelector(`#${fieldset}`); + + const icon = document.createElement("img"); + icon.src = "images/resources/network.svg"; + icon.alt = `${prefix}${netID}`; + icon.dataset.network = netID; + icon.dataset.values = netDetails; + field.appendChild(icon); + + const netLabel = document.createElement("label"); + netLabel.innerText = `${prefix}${netID}`; + netLabel.dataset.network = netID; + netLabel.dataset.values = netDetails; + field.append(netLabel); + + const netDesc = document.createElement("p"); + netDesc.innerText = netDetails; + netDesc.dataset.network = netID; + netDesc.dataset.values = netDetails; + netDesc.style.overflowX = "hidden"; + netDesc.style.whiteSpace = "nowrap"; + field.append(netDesc); + + const actionDiv = document.createElement("div"); + + const configBtn = document.createElement("img"); + configBtn.classList.add("clickable"); + configBtn.src = "images/actions/network/config.svg"; + configBtn.title = "Config Interface"; + configBtn.addEventListener("click", handleNetworkConfig); + configBtn.dataset.network = netID; + configBtn.dataset.values = netDetails; + actionDiv.appendChild(configBtn); + + const deleteBtn = document.createElement("img"); + deleteBtn.classList.add("clickable"); + deleteBtn.src = "images/actions/delete-active.svg"; + deleteBtn.title = "Delete Interface"; + deleteBtn.addEventListener("click", handleNetworkDelete); + deleteBtn.dataset.network = netID; + deleteBtn.dataset.values = netDetails; + actionDiv.appendChild(deleteBtn); + + field.append(actionDiv); +} + +async function handleNetworkConfig () { + const netID = this.dataset.network; + const netDetails = this.dataset.values; + const header = `Edit net${netID}`; + const body = ""; + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + netid: netID, + rate: form.get("rate") + }; + const result = await requestAPI("/instance/network/modify", "POST", body); + if (result.status === 200) { + await getConfig(); + populateNetworks(); + } + else { + alert(result.error); + await getConfig(); + populateNetworks(); + } + } + }); + + d.querySelector("#rate").value = netDetails.split("rate=")[1].split(",")[0]; +} + +async function handleNetworkDelete () { + const netID = this.dataset.network; + const header = `Delete net${netID}`; + const body = ""; + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + netid: netID + }; + const result = await requestAPI("/instance/network/delete", "DELETE", body); + if (result.status === 200) { + await getConfig(); + populateNetworks(); + } + else { + alert(result.error); + await getConfig(); + populateNetworks(); + } + } + }); +} + +async function handleNetworkAdd () { + const header = "Create Network Interface"; + let body = ""; + if (type === "lxc") { + body += ""; + } + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + const body = { + node, + type, + vmid, + netid: form.get("netid"), + rate: form.get("rate") + }; + if (type === "lxc") { + body.name = form.get("name"); + } + const result = await requestAPI("/instance/network/create", "POST", body); + if (result.status === 200) { + await getConfig(); + populateNetworks(); + } + else { + alert(result.error); + await getConfig(); + populateNetworks(); + } + } + }); +} + +async function populateDevices () { + if (type === "qemu") { + document.querySelector("#devices-card").classList.remove("none"); + document.querySelector("#devices").innerHTML = ""; + const devices = {}; + const prefix = pcieMetaData.prefix; + Object.keys(config.data).forEach((element) => { + if (element.startsWith(prefix)) { + devices[element.replace(prefix, "")] = config.data[element]; + } + }); + const orderedKeys = getOrdered(devices); + orderedKeys.forEach(async (element) => { + const deviceData = await requestAPI(`/instance/pci?node=${node}&type=${type}&vmid=${vmid}&hostpci=${element}`, "GET"); + addDeviceLine("devices", prefix, element, devices[element], deviceData.device_name); + }); + + document.querySelector("#device-add").addEventListener("click", handleDeviceAdd); + } +} + +function addDeviceLine (fieldset, prefix, deviceID, deviceDetails, deviceName) { + const field = document.querySelector(`#${fieldset}`); + + const icon = document.createElement("img"); + icon.src = "images/resources/device.svg"; + icon.alt = `${prefix}${deviceID}`; + icon.dataset.device = deviceID; + icon.dataset.values = deviceDetails; + icon.dataset.name = deviceName; + field.appendChild(icon); + + const deviceLabel = document.createElement("p"); + + deviceLabel.innerText = deviceName; + deviceLabel.dataset.device = deviceID; + deviceLabel.dataset.values = deviceDetails; + deviceLabel.dataset.name = deviceName; + deviceLabel.style.overflowX = "hidden"; + deviceLabel.style.whiteSpace = "nowrap"; + field.append(deviceLabel); + + const actionDiv = document.createElement("div"); + + const configBtn = document.createElement("img"); + configBtn.classList.add("clickable"); + configBtn.src = "images/actions/device/config.svg"; + configBtn.title = "Config Device"; + configBtn.addEventListener("click", handleDeviceConfig); + configBtn.dataset.device = deviceID; + configBtn.dataset.values = deviceDetails; + configBtn.dataset.name = deviceName; + actionDiv.appendChild(configBtn); + + const deleteBtn = document.createElement("img"); + deleteBtn.classList.add("clickable"); + deleteBtn.src = "images/actions/delete-active.svg"; + deleteBtn.title = "Delete Device"; + deleteBtn.addEventListener("click", handleDeviceDelete); + deleteBtn.dataset.device = deviceID; + deleteBtn.dataset.values = deviceDetails; + deleteBtn.dataset.name = deviceName; + actionDiv.appendChild(deleteBtn); + + field.append(actionDiv); +} + +async function handleDeviceConfig () { + const deviceID = this.dataset.device; + const deviceDetails = this.dataset.values; + const deviceName = this.dataset.name; + const header = `Edit Expansion Card ${deviceID}`; + const body = ""; + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + hostpci: deviceID, + device: form.get("device"), + pcie: form.get("pcie") ? 1 : 0 + }; + const result = await requestAPI("/instance/pci/modify", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDevices(); + } + else { + alert(result.error); + await getConfig(); + populateDevices(); + } + } + }); + + const availDevices = await requestAPI(`/nodes/pci?node=${node}`, "GET"); + d.querySelector("#device").append(new Option(deviceName, deviceDetails.split(",")[0])); + for (const availDevice of availDevices) { + d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id)); + } + d.querySelector("#pcie").checked = deviceDetails.includes("pcie=1"); +} + +async function handleDeviceDelete () { + const deviceID = this.dataset.device; + const header = `Remove Expansion Card ${deviceID}`; + const body = ""; + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg"; + const body = { + node, + type, + vmid, + hostpci: deviceID + }; + const result = await requestAPI("/instance/pci/delete", "DELETE", body); + if (result.status === 200) { + await getConfig(); + populateDevices(); + } + else { + alert(result.error); + await getConfig(); + populateDevices(); + } + } + }); +} + +async function handleDeviceAdd () { + const header = "Add Expansion Card"; + const body = ""; + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + const body = { + node, + type, + vmid, + device: form.get("device"), + pcie: form.get("pcie") ? 1 : 0 + }; + const result = await requestAPI("/instance/pci/create", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDevices(); + } + else { + alert(result.error); + await getConfig(); + populateDevices(); + } + } + }); + + const availDevices = await requestAPI(`/nodes/pci?node=${node}`, "GET"); + for (const availDevice of availDevices) { + d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id)); + } + d.querySelector("#pcie").checked = true; +} + +async function handleFormExit () { + const body = { + node, + type, + vmid, + cores: document.querySelector("#cores").value, + memory: document.querySelector("#ram").value + }; + if (type === "lxc") { + body.swap = document.querySelector("#swap").value; + } + else if (type === "qemu") { + body.proctype = document.querySelector("#proctype").value; + } + const result = await requestAPI("/instance/resources", "POST", body); + if (result.status === 200) { + await getConfig(); + populateDisk(); + goToPage("index.html"); + } + else { + alert(result.error); + } +} diff --git a/scripts/dialog.js b/scripts/dialog.js index 7d8e21e..9abb2c3 100644 --- a/scripts/dialog.js +++ b/scripts/dialog.js @@ -1,5 +1,5 @@ -export function dialog(header, body, callback = async (result, form) => { }) { - let dialog = document.createElement("dialog"); +export function dialog (header, body, callback = async (result, form) => { }) { + const dialog = document.createElement("dialog"); dialog.innerHTML = `

@@ -23,8 +23,8 @@ export function dialog(header, body, callback = async (result, form) => { }) { return dialog; } -export function alert(message) { - let dialog = document.createElement("dialog"); +export function alert (message) { + const dialog = document.createElement("dialog"); dialog.innerHTML = `

${message}

@@ -40,7 +40,7 @@ export function alert(message) { dialog.addEventListener("close", () => { dialog.parentElement.removeChild(dialog); - }) + }); return dialog; -} \ No newline at end of file +} diff --git a/scripts/index.js b/scripts/index.js index cb0410d..d6657f0 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,416 +1,424 @@ -import { requestPVE, requestAPI, goToPage, goToURL, instances_config, nodes_config, setTitleAndHeader } from "./utils.js"; -import { alert, dialog } from "./dialog.js"; -import { PVE } from "../vars.js" - -window.addEventListener("DOMContentLoaded", init); - -async function init() { - setTitleAndHeader(); - let cookie = document.cookie; - if (cookie === "") { - goToPage("login.html"); - } - - await populateInstances(); - - let addInstanceBtn = document.querySelector("#instance-add"); - addInstanceBtn.addEventListener("click", handleInstanceAdd); -} - -async function populateInstances() { - let resources = await requestPVE("/cluster/resources", "GET"); - let instanceContainer = document.getElementById("instance-container"); - let instances = []; - - resources.data.forEach((element) => { - if (element.type === "lxc" || element.type === "qemu") { - let nodeName = element.node; - let nodeStatus = resources.data.find(item => item.node === nodeName && item.type === "node").status; - element.node = { name: nodeName, status: nodeStatus }; - instances.push(element); - } - }); - - instances.sort((a, b) => (a.vmid > b.vmid) ? 1 : -1); - - instanceContainer.innerHTML = ` -
-
-

VM ID

-
-
-

VM Name

-
-
-

VM Type

-
-
-

VM Status

-
-
-

Host Name

-
-
-

Host Status

-
-
-

Actions

-
-
- `; - for (let i = 0; i < instances.length; i++) { - let newInstance = new Instance(); - newInstance.data = instances[i]; - instanceContainer.append(newInstance.shadowElement); - } -} - -async function handleInstanceAdd() { - let header = "Create New Instance"; - - let body = ` - - - - - - - - - - - - -

Container Options

- - - - - - - - - - - - - `; - - let d = dialog(header, body, async (result, form) => { - if (result === "confirm") { - let body = { - node: form.get("node"), - type: form.get("type"), - name: form.get("name"), - vmid: form.get("vmid"), - cores: form.get("cores"), - memory: form.get("memory") - }; - if (form.get("type") === "lxc") { - body.swap = form.get("swap"); - body.password = form.get("password"); - body.ostemplate = form.get("template-image"); - body.rootfslocation = form.get("rootfs-storage"); - body.rootfssize = form.get("rootfs-size"); - } - let result = await requestAPI("/instance", "POST", body); - if (result.status === 200) { - populateInstances(); - } - else { - alert(result.error); - populateInstances(); - } - } - }); - - let typeSelect = d.querySelector("#type"); - typeSelect.selectedIndex = -1; - typeSelect.addEventListener("change", () => { - if (typeSelect.value === "qemu") { - d.querySelectorAll(".container-specific").forEach((element) => { - element.classList.add("none"); - element.disabled = true; - }); - } - else { - d.querySelectorAll(".container-specific").forEach((element) => { - element.classList.remove("none"); - element.disabled = false; - }); - } - }); - - let templateContent = "iso"; - let templateStorage = d.querySelector("#template-storage"); - templateStorage.selectedIndex = -1; - - let rootfsContent = "rootdir"; - let rootfsStorage = d.querySelector("#rootfs-storage"); - rootfsStorage.selectedIndex = -1; - - let nodeSelect = d.querySelector("#node"); - let clusterNodes = await requestPVE("/nodes", "GET"); - let allowedNodes = await requestAPI("/user/config/nodes", "GET"); - clusterNodes.data.forEach((element) => { - if (element.status === "online" && allowedNodes.includes(element.node)) { - nodeSelect.add(new Option(element.node)); - } - }); - nodeSelect.selectedIndex = -1; - nodeSelect.addEventListener("change", async () => { // change template and rootfs storage based on node - let node = nodeSelect.value; - let storage = await requestPVE(`/nodes/${node}/storage`, "GET"); - storage.data.forEach((element) => { - if (element.content.includes(templateContent)) { - templateStorage.add(new Option(element.storage)); - } - if (element.content.includes(rootfsContent)) { - rootfsStorage.add(new Option(element.storage)); - } - }); - templateStorage.selectedIndex = -1; - rootfsStorage.selectedIndex = -1; - }); - - let templateImage = d.querySelector("#template-image"); // populate templateImage depending on selected image storage - templateStorage.addEventListener("change", async () => { - templateImage.innerHTML = ``; - let content = "vztmpl"; - let images = await requestPVE(`/nodes/${nodeSelect.value}/storage/${templateStorage.value}/content`, "GET"); - images.data.forEach((element) => { - if (element.content.includes(content)) { - templateImage.append(new Option(element.volid.replace(`${templateStorage.value}:${content}/`, ""), element.volid)); - } - }); - templateImage.selectedIndex = -1; - }); - - let userResources = await requestAPI("/user/resources", "GET"); - let userInstances = await requestAPI("/user/config/instances", "GET"); - d.querySelector("#cores").max = userResources.avail.cores; - d.querySelector("#memory").max = userResources.avail.memory; - d.querySelector("#vmid").min = userInstances.vmid.min; - d.querySelector("#vmid").max = userInstances.vmid.max; -} - -class Instance { - constructor() { - let shadowRoot = document.createElement("div"); - shadowRoot.classList.add("w3-row"); - - shadowRoot.innerHTML = ` -
-

-
-
-

-
-
-

-
-
- -

-
-
-

-
-
- -

-
-
- - - - -
- `; - - this.shadowElement = shadowRoot; - this.actionLock = false; - } - - set data(data) { - if (data.status === "unknown") { - data.status = "stopped"; - } - this.type = data.type; - this.status = data.status; - this.vmid = data.vmid; - this.name = data.name; - this.node = data.node; - this.update(); - } - - update() { - let vmidParagraph = this.shadowElement.querySelector("#instance-id"); - vmidParagraph.innerText = this.vmid; - - let nameParagraph = this.shadowElement.querySelector("#instance-name"); - nameParagraph.innerText = this.name ? this.name : ""; - - let typeParagraph = this.shadowElement.querySelector("#instance-type"); - typeParagraph.innerText = this.type; - - let statusParagraph = this.shadowElement.querySelector("#instance-status"); - statusParagraph.innerText = this.status; - - let statusIcon = this.shadowElement.querySelector("#instance-status-icon"); - statusIcon.src = instances_config[this.status].status.src; - statusIcon.alt = instances_config[this.status].status.alt; - - let nodeNameParagraph = this.shadowElement.querySelector("#node-name"); - nodeNameParagraph.innerText = this.node.name; - - let nodeStatusParagraph = this.shadowElement.querySelector("#node-status"); - nodeStatusParagraph.innerText = this.node.status; - - let nodeStatusIcon = this.shadowElement.querySelector("#node-status-icon"); - nodeStatusIcon.src = nodes_config[this.node.status].status.src; - nodeStatusIcon.alt = nodes_config[this.node.status].status.src; - - let powerButton = this.shadowElement.querySelector("#power-btn"); - powerButton.src = instances_config[this.status].power.src; - powerButton.alt = instances_config[this.status].power.alt; - powerButton.title = instances_config[this.status].power.alt; - if (instances_config[this.status].power.clickable) { - powerButton.classList.add("clickable"); - powerButton.onclick = this.handlePowerButton.bind(this) - } - - let configButton = this.shadowElement.querySelector("#configure-btn"); - configButton.src = instances_config[this.status].config.src; - configButton.alt = instances_config[this.status].config.alt; - configButton.title = instances_config[this.status].config.alt; - if (instances_config[this.status].config.clickable) { - configButton.classList.add("clickable"); - configButton.onclick = this.handleConfigButton.bind(this); - } - - let consoleButton = this.shadowElement.querySelector("#console-btn"); - consoleButton.src = instances_config[this.status].console.src; - consoleButton.alt = instances_config[this.status].console.alt; - consoleButton.title = instances_config[this.status].console.alt; - if (instances_config[this.status].console.clickable) { - consoleButton.classList.add("clickable"); - consoleButton.onclick = this.handleConsoleButton.bind(this); - } - - let deleteButton = this.shadowElement.querySelector("#delete-btn"); - deleteButton.src = instances_config[this.status].delete.src; - deleteButton.alt = instances_config[this.status].delete.alt; - deleteButton.title = instances_config[this.status].delete.alt; - if (instances_config[this.status].delete.clickable) { - deleteButton.classList.add("clickable"); - deleteButton.onclick = this.handleDeleteButton.bind(this); - } - - if (this.node.status !== "online") { - powerButton.classList.add("hidden"); - configButton.classList.add("hidden"); - consoleButton.classList.add("hidden"); - deleteButton.classList.add("hidden"); - } - } - - async handlePowerButton() { - if (!this.actionLock) { - let header = `${this.status === "running" ? "Stop" : "Start"} VM ${this.vmid}`; - let body = `

Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM

${this.vmid}

` - - dialog(header, body, async (result, form) => { - if (result === "confirm") { - this.actionLock = true; - let targetAction = this.status === "running" ? "stop" : "start"; - let targetStatus = this.status === "running" ? "stopped" : "running"; - let prevStatus = this.status; - this.status = "loading"; - - this.update(); - - let result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid }); - - const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); - - while (true) { - let 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 - this.status = targetStatus; - this.update(); - this.actionLock = false; - break; - } - else if (taskStatus.data.status === "stopped") { // task stopped but was not successful - this.status = prevStatus; - alert(`attempted to ${targetAction} ${this.vmid} but process returned stopped:${result.data.exitstatus}`); - this.update(); - this.actionLock = false; - break; - } - else { // task has not stopped - await waitFor(1000); - } - } - } - }); - } - } - - handleConfigButton() { - if (!this.actionLock && this.status === "stopped") { // if the action lock is false, and the node is stopped, then navigate to the conig page with the node infor in the search query - goToPage("config.html", { node: this.node.name, type: this.type, vmid: this.vmid }); - } - } - - handleConsoleButton() { - if (!this.actionLock && this.status === "running") { - let data = { console: `${this.type === "qemu" ? "kvm" : "lxc"}`, vmid: this.vmid, vmname: this.name, node: this.node.name, resize: "off", cmd: "" }; - data[`${this.type === "qemu" ? "novnc" : "xtermjs"}`] = 1; - goToURL(PVE, data, true); - } - } - - handleDeleteButton() { - if (!this.actionLock && this.status === "stopped") { - - let header = `Delete VM ${this.vmid}`; - let body = `

Are you sure you want to delete VM

${this.vmid}

` - - dialog(header, body, async (result, form) => { - if (result === "confirm") { - this.actionLock = true; - let prevStatus = this.status; - this.status = "loading"; - this.update(); - - let action = {}; - action.purge = 1; - action["destroy-unreferenced-disks"] = 1; - - let body = { - node: this.node.name, - type: this.type, - vmid: this.vmid, - action: JSON.stringify(action) - }; - - let result = await requestAPI("/instance", "DELETE", body); - if (result.status === 200) { - this.shadowElement.parentElement.removeChild(this.shadowElement); - } - else { - alert(result.error); - this.status = this.prevStatus; - this.update(); - this.actionLock = false; - } - } - }); - } - } -} \ No newline at end of file +import { requestPVE, requestAPI, goToPage, goToURL, instancesConfig, nodesConfig, setTitleAndHeader } from "./utils.js"; +import { alert, dialog } from "./dialog.js"; +import { PVE } from "../vars.js"; + +window.addEventListener("DOMContentLoaded", init); + +async function init () { + setTitleAndHeader(); + const cookie = document.cookie; + if (cookie === "") { + goToPage("login.html"); + } + + await populateInstances(); + + const addInstanceBtn = document.querySelector("#instance-add"); + addInstanceBtn.addEventListener("click", handleInstanceAdd); +} + +async function populateInstances () { + const resources = await requestPVE("/cluster/resources", "GET"); + const instanceContainer = document.getElementById("instance-container"); + const instances = []; + + resources.data.forEach((element) => { + if (element.type === "lxc" || element.type === "qemu") { + const nodeName = element.node; + const nodeStatus = resources.data.find(item => item.node === nodeName && item.type === "node").status; + element.node = { name: nodeName, status: nodeStatus }; + instances.push(element); + } + }); + + instances.sort((a, b) => (a.vmid > b.vmid) ? 1 : -1); + + instanceContainer.innerHTML = ` +
+
+

VM ID

+
+
+

VM Name

+
+
+

VM Type

+
+
+

VM Status

+
+
+

Host Name

+
+
+

Host Status

+
+
+

Actions

+
+
+ `; + for (let i = 0; i < instances.length; i++) { + const newInstance = new Instance(); + newInstance.data = instances[i]; + instanceContainer.append(newInstance.shadowElement); + } +} + +async function handleInstanceAdd () { + const header = "Create New Instance"; + + const body = ` + + + + + + + + + + + + +

Container Options

+ + + + + + + + + + + + + `; + + const d = dialog(header, body, async (result, form) => { + if (result === "confirm") { + const body = { + node: form.get("node"), + type: form.get("type"), + name: form.get("name"), + vmid: form.get("vmid"), + cores: form.get("cores"), + memory: form.get("memory") + }; + if (form.get("type") === "lxc") { + body.swap = form.get("swap"); + body.password = form.get("password"); + body.ostemplate = form.get("template-image"); + body.rootfslocation = form.get("rootfs-storage"); + body.rootfssize = form.get("rootfs-size"); + } + const result = await requestAPI("/instance", "POST", body); + if (result.status === 200) { + populateInstances(); + } + else { + alert(result.error); + populateInstances(); + } + } + }); + + const typeSelect = d.querySelector("#type"); + typeSelect.selectedIndex = -1; + typeSelect.addEventListener("change", () => { + if (typeSelect.value === "qemu") { + d.querySelectorAll(".container-specific").forEach((element) => { + element.classList.add("none"); + element.disabled = true; + }); + } + else { + d.querySelectorAll(".container-specific").forEach((element) => { + element.classList.remove("none"); + element.disabled = false; + }); + } + }); + + const templateContent = "iso"; + const templateStorage = d.querySelector("#template-storage"); + templateStorage.selectedIndex = -1; + + const rootfsContent = "rootdir"; + const rootfsStorage = d.querySelector("#rootfs-storage"); + rootfsStorage.selectedIndex = -1; + + const nodeSelect = d.querySelector("#node"); + const clusterNodes = await requestPVE("/nodes", "GET"); + const allowedNodes = await requestAPI("/user/config/nodes", "GET"); + clusterNodes.data.forEach((element) => { + if (element.status === "online" && allowedNodes.includes(element.node)) { + nodeSelect.add(new Option(element.node)); + } + }); + nodeSelect.selectedIndex = -1; + nodeSelect.addEventListener("change", async () => { // change template and rootfs storage based on node + const node = nodeSelect.value; + const storage = await requestPVE(`/nodes/${node}/storage`, "GET"); + storage.data.forEach((element) => { + if (element.content.includes(templateContent)) { + templateStorage.add(new Option(element.storage)); + } + if (element.content.includes(rootfsContent)) { + rootfsStorage.add(new Option(element.storage)); + } + }); + templateStorage.selectedIndex = -1; + rootfsStorage.selectedIndex = -1; + }); + + const templateImage = d.querySelector("#template-image"); // populate templateImage depending on selected image storage + templateStorage.addEventListener("change", async () => { + templateImage.innerHTML = ""; + const content = "vztmpl"; + const images = await requestPVE(`/nodes/${nodeSelect.value}/storage/${templateStorage.value}/content`, "GET"); + images.data.forEach((element) => { + if (element.content.includes(content)) { + templateImage.append(new Option(element.volid.replace(`${templateStorage.value}:${content}/`, ""), element.volid)); + } + }); + templateImage.selectedIndex = -1; + }); + + const userResources = await requestAPI("/user/resources", "GET"); + const userInstances = await requestAPI("/user/config/instances", "GET"); + d.querySelector("#cores").max = userResources.avail.cores; + d.querySelector("#memory").max = userResources.avail.memory; + d.querySelector("#vmid").min = userInstances.vmid.min; + d.querySelector("#vmid").max = userInstances.vmid.max; +} + +class Instance { + constructor () { + const shadowRoot = document.createElement("div"); + shadowRoot.classList.add("w3-row"); + + shadowRoot.innerHTML = ` +
+

+
+
+

+
+
+

+
+
+ +

+
+
+

+
+
+ +

+
+
+ + + + +
+ `; + + this.shadowElement = shadowRoot; + this.actionLock = false; + } + + get data () { + return { + type: this.type, + status: this.status, + vmid: this.status, + name: this.name, + node: this.node + }; + } + + set data (data) { + if (data.status === "unknown") { + data.status = "stopped"; + } + this.type = data.type; + this.status = data.status; + this.vmid = data.vmid; + this.name = data.name; + this.node = data.node; + this.update(); + } + + update () { + const vmidParagraph = this.shadowElement.querySelector("#instance-id"); + vmidParagraph.innerText = this.vmid; + + const nameParagraph = this.shadowElement.querySelector("#instance-name"); + nameParagraph.innerText = this.name ? this.name : ""; + + const typeParagraph = this.shadowElement.querySelector("#instance-type"); + typeParagraph.innerText = this.type; + + const statusParagraph = this.shadowElement.querySelector("#instance-status"); + statusParagraph.innerText = this.status; + + const statusIcon = this.shadowElement.querySelector("#instance-status-icon"); + statusIcon.src = instancesConfig[this.status].status.src; + statusIcon.alt = instancesConfig[this.status].status.alt; + + const nodeNameParagraph = this.shadowElement.querySelector("#node-name"); + nodeNameParagraph.innerText = this.node.name; + + const nodeStatusParagraph = this.shadowElement.querySelector("#node-status"); + nodeStatusParagraph.innerText = this.node.status; + + const nodeStatusIcon = this.shadowElement.querySelector("#node-status-icon"); + nodeStatusIcon.src = nodesConfig[this.node.status].status.src; + nodeStatusIcon.alt = nodesConfig[this.node.status].status.src; + + const powerButton = this.shadowElement.querySelector("#power-btn"); + powerButton.src = instancesConfig[this.status].power.src; + powerButton.alt = instancesConfig[this.status].power.alt; + powerButton.title = instancesConfig[this.status].power.alt; + if (instancesConfig[this.status].power.clickable) { + powerButton.classList.add("clickable"); + powerButton.onclick = this.handlePowerButton.bind(this); + } + + const configButton = this.shadowElement.querySelector("#configure-btn"); + configButton.src = instancesConfig[this.status].config.src; + configButton.alt = instancesConfig[this.status].config.alt; + configButton.title = instancesConfig[this.status].config.alt; + if (instancesConfig[this.status].config.clickable) { + configButton.classList.add("clickable"); + configButton.onclick = this.handleConfigButton.bind(this); + } + + const consoleButton = this.shadowElement.querySelector("#console-btn"); + consoleButton.src = instancesConfig[this.status].console.src; + consoleButton.alt = instancesConfig[this.status].console.alt; + consoleButton.title = instancesConfig[this.status].console.alt; + if (instancesConfig[this.status].console.clickable) { + consoleButton.classList.add("clickable"); + consoleButton.onclick = this.handleConsoleButton.bind(this); + } + + const deleteButton = this.shadowElement.querySelector("#delete-btn"); + deleteButton.src = instancesConfig[this.status].delete.src; + deleteButton.alt = instancesConfig[this.status].delete.alt; + deleteButton.title = instancesConfig[this.status].delete.alt; + if (instancesConfig[this.status].delete.clickable) { + deleteButton.classList.add("clickable"); + deleteButton.onclick = this.handleDeleteButton.bind(this); + } + + if (this.node.status !== "online") { + powerButton.classList.add("hidden"); + configButton.classList.add("hidden"); + consoleButton.classList.add("hidden"); + deleteButton.classList.add("hidden"); + } + } + + async handlePowerButton () { + if (!this.actionLock) { + const header = `${this.status === "running" ? "Stop" : "Start"} VM ${this.vmid}`; + const body = `

Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM

${this.vmid}

`; + + dialog(header, body, async (result, form) => { + if (result === "confirm") { + this.actionLock = true; + const targetAction = this.status === "running" ? "stop" : "start"; + const targetStatus = this.status === "running" ? "stopped" : "running"; + const prevStatus = this.status; + this.status = "loading"; + + this.update(); + + const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid }); + + 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 + this.status = targetStatus; + this.update(); + this.actionLock = false; + break; + } + else if (taskStatus.data.status === "stopped") { // task stopped but was not successful + this.status = prevStatus; + alert(`attempted to ${targetAction} ${this.vmid} but process returned stopped:${result.data.exitstatus}`); + this.update(); + this.actionLock = false; + break; + } + else { // task has not stopped + await waitFor(1000); + } + } + } + }); + } + } + + handleConfigButton () { + if (!this.actionLock && this.status === "stopped") { // if the action lock is false, and the node is stopped, then navigate to the conig page with the node infor in the search query + goToPage("config.html", { node: this.node.name, type: this.type, vmid: this.vmid }); + } + } + + handleConsoleButton () { + if (!this.actionLock && this.status === "running") { + const data = { console: `${this.type === "qemu" ? "kvm" : "lxc"}`, vmid: this.vmid, vmname: this.name, node: this.node.name, resize: "off", cmd: "" }; + data[`${this.type === "qemu" ? "novnc" : "xtermjs"}`] = 1; + goToURL(PVE, data, true); + } + } + + handleDeleteButton () { + if (!this.actionLock && this.status === "stopped") { + const header = `Delete VM ${this.vmid}`; + const body = `

Are you sure you want to delete VM

${this.vmid}

`; + + dialog(header, body, async (result, form) => { + if (result === "confirm") { + this.actionLock = true; + this.status = "loading"; + this.update(); + + const action = {}; + action.purge = 1; + action["destroy-unreferenced-disks"] = 1; + + const body = { + node: this.node.name, + type: this.type, + vmid: this.vmid, + action: JSON.stringify(action) + }; + + const result = await requestAPI("/instance", "DELETE", body); + if (result.status === 200) { + this.shadowElement.parentElement.removeChild(this.shadowElement); + } + else { + alert(result.error); + this.status = this.prevStatus; + this.update(); + this.actionLock = false; + } + } + }); + } + } +} diff --git a/scripts/login.js b/scripts/login.js index a803c81..514df03 100644 --- a/scripts/login.js +++ b/scripts/login.js @@ -1,43 +1,43 @@ -import { requestTicket, goToPage, deleteAllCookies, requestPVE, setTitleAndHeader } from "./utils.js"; -import { alert } from "./dialog.js"; - -window.addEventListener("DOMContentLoaded", init); - -async function init() { - setTitleAndHeader(); - await deleteAllCookies(); - let formSubmitButton = document.querySelector("#submit"); - let realms = await requestPVE("/access/domains", "GET"); - let realmSelect = document.querySelector("#realm"); - realms.data.forEach((element) => { - realmSelect.add(new Option(element.comment, element.realm)); - if ("default" in element && element.default === 1) { - realmSelect.value = element.realm; - } - }); - formSubmitButton.addEventListener("click", async (e) => { - e.preventDefault(); - let form = document.querySelector("form"); - let formData = new FormData(form); - - formSubmitButton.innerText = "Authenticating..."; - let ticket = await requestTicket(formData.get("username"), formData.get("password"), formData.get("realm")); - if (ticket.status === 200) { - formSubmitButton.innerText = "LOGIN"; - goToPage("index.html"); - } - else if (ticket.status === 401) { - alert("Authenticaton failed."); - formSubmitButton.innerText = "LOGIN"; - } - else if (ticket.status === 408) { - alert("Network error."); - formSubmitButton.innerText = "LOGIN"; - } - else { - alert("An error occured."); - formSubmitButton.innerText = "LOGIN"; - console.error(ticket.error); - } - }); -} \ No newline at end of file +import { requestTicket, goToPage, deleteAllCookies, requestPVE, setTitleAndHeader } from "./utils.js"; +import { alert } from "./dialog.js"; + +window.addEventListener("DOMContentLoaded", init); + +async function init () { + setTitleAndHeader(); + await deleteAllCookies(); + const formSubmitButton = document.querySelector("#submit"); + const realms = await requestPVE("/access/domains", "GET"); + const realmSelect = document.querySelector("#realm"); + realms.data.forEach((element) => { + realmSelect.add(new Option(element.comment, element.realm)); + if ("default" in element && element.default === 1) { + realmSelect.value = element.realm; + } + }); + formSubmitButton.addEventListener("click", async (e) => { + e.preventDefault(); + const form = document.querySelector("form"); + const formData = new FormData(form); + + formSubmitButton.innerText = "Authenticating..."; + const ticket = await requestTicket(formData.get("username"), formData.get("password"), formData.get("realm")); + if (ticket.status === 200) { + formSubmitButton.innerText = "LOGIN"; + goToPage("index.html"); + } + else if (ticket.status === 401) { + alert("Authenticaton failed."); + formSubmitButton.innerText = "LOGIN"; + } + else if (ticket.status === 408) { + alert("Network error."); + formSubmitButton.innerText = "LOGIN"; + } + else { + alert("An error occured."); + formSubmitButton.innerText = "LOGIN"; + console.error(ticket.error); + } + }); +} diff --git a/scripts/utils.js b/scripts/utils.js index 456de7e..7d7feb0 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,253 +1,253 @@ -import { API, organization } from "/vars.js"; - -export const resources_config = { - disk: { - actionBarOrder: ["move", "resize", "detach_attach", "delete"], - lxc: { - prefixOrder: ["rootfs", "mp", "unused"], - rootfs: { name: "ROOTFS", icon: "images/resources/drive.svg", actions: ["move", "resize"] }, - mp: { name: "MP", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] }, - unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] } - }, - qemu: { - prefixOrder: ["ide", "sata", "unused"], - ide: { name: "IDE", icon: "images/resources/disk.svg", actions: ["delete"] }, - sata: { name: "SATA", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] }, - unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] } - } - }, - network: { - prefix: "net" - }, - pcie: { - prefix: "hostpci" - } -} - -export const instances_config = { - running: { - status: { - src: "images/status/active.svg", - alt: "Instance is running", - clickable: false - }, - power: { - src: "images/actions/instance/stop.svg", - alt: "Shutdown Instance", - clickable: true, - }, - config: { - src: "images/actions/instance/config-inactive.svg", - alt: "Change Configuration (Inactive)", - clickable: false, - }, - console: { - src: "images/actions/instance/console-active.svg", - alt: "Open Console", - clickable: true, - }, - delete: { - src: "images/actions/delete-inactive.svg", - alt: "Delete Instance (Inactive)", - clickable: false, - } - }, - stopped: { - status: { - src: "images/status/inactive.svg", - alt: "Instance is stopped", - clickable: false - }, - power: { - src: "images/actions/instance/start.svg", - alt: "Start Instance", - clickable: true, - }, - config: { - src: "images/actions/instance/config-active.svg", - alt: "Change Configuration", - clickable: true, - }, - console: { - src: "images/actions/instance/console-inactive.svg", - alt: "Open Console (Inactive)", - clickable: false, - }, - delete: { - src: "images/actions/delete-active.svg", - alt: "Delete Instance", - clickable: true, - } - }, - loading: { - status: { - src: "images/status/loading.svg", - alt: "Instance is loading", - clickable: false - }, - power: { - src: "images/status/loading.svg", - alt: "Loading Instance", - clickable: false, - }, - config: { - src: "images/actions/instance/config-inactive.svg", - alt: "Change Configuration (Inactive)", - clickable: false, - }, - console: { - src: "images/actions/instance/console-inactive.svg", - alt: "Open Console (Inactive)", - clickable: false, - }, - delete: { - src: "images/actions/delete-inactive.svg", - alt: "Delete Instance (Inactive)", - clickable: false, - } - } -} - -export const nodes_config = { - online: { - status: { - src: "images/status/active.svg", - alt: "Node is online" - } - }, - offline: { - status: { - src: "images/status/inactive.svg", - alt: "Node is offline" - } - }, - uknown: { - status: { - src: "images/status/inactive.svg", - alt: "Node is offline" - } - } -} - -export function getCookie(cname) { - let name = cname + "="; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(";"); - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) === " ") { - c = c.substring(1); - } - if (c.indexOf(name) === 0) { - return c.substring(name.length, c.length); - } - } - return ""; -} - -export async function requestTicket(username, password, realm) { - let response = await requestAPI("/ticket", "POST", { username: `${username}@${realm}`, password: password }, false); - return response; -} - -export async function requestPVE(path, method, body = null) { - let prms = new URLSearchParams(body); - let content = { - method: method, - mode: "cors", - credentials: "include", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - } - if (method === "POST") { - content.body = prms.toString(); - content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken"); - } - - let response = await request(`${API}/proxmox${path}`, content); - return response; -} - -export async function requestAPI(path, method, body = null) { - let prms = new URLSearchParams(body); - let content = { - method: method, - mode: "cors", - credentials: "include", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - } - if (method === "POST" || method === "DELETE") { - content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken"); - } - if (body) { - content.body = prms.toString(); - } - - let response = await request(`${API}${path}`, content); - return response; -} - -async function request(url, content) { - let response = await fetch(url, content); - let data = null; - try { - data = await response.json(); - data.status = response.status; - } - catch { - data = null; - } - - if (!response.ok) { - return { status: response.status, error: data ? data.error : response.status }; - } - else { - data.status = response.status; - return data ? data : response; - } -} - -export function goToPage(page, data = {}, newwindow = false) { - let url = new URL(`https://${window.location.host}/${page}`); - for (let k in data) { - url.searchParams.append(k, data[k]); - } - - if (newwindow) { - window.open(url, `${organization} - client`, "height=480,width=848"); - } - else { - window.location.assign(url.toString()); - } -} - -export function goToURL(href, data = {}, newwindow = false) { - let url = new URL(href); - for (let k in data) { - url.searchParams.append(k, data[k]); - } - - if (newwindow) { - window.open(url, `${organization} - client`, "height=480,width=848"); - } - else { - window.location.assign(url.toString()); - } -} - -export function getURIData() { - let url = new URL(window.location.href); - return Object.fromEntries(url.searchParams); -} - -export async function deleteAllCookies() { - await requestAPI("/ticket", "DELETE"); -} - -export function setTitleAndHeader() { - document.title = `${organization} - client`; - document.querySelector("h1").innerText = organization; -} \ No newline at end of file +import { API, organization } from "../../../../../vars.js"; + +export const resourcesConfig = { + disk: { + actionBarOrder: ["move", "resize", "detach_attach", "delete"], + lxc: { + prefixOrder: ["rootfs", "mp", "unused"], + rootfs: { name: "ROOTFS", icon: "images/resources/drive.svg", actions: ["move", "resize"] }, + mp: { name: "MP", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] }, + unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] } + }, + qemu: { + prefixOrder: ["ide", "sata", "unused"], + ide: { name: "IDE", icon: "images/resources/disk.svg", actions: ["delete"] }, + sata: { name: "SATA", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] }, + unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] } + } + }, + network: { + prefix: "net" + }, + pcie: { + prefix: "hostpci" + } +}; + +export const instancesConfig = { + running: { + status: { + src: "images/status/active.svg", + alt: "Instance is running", + clickable: false + }, + power: { + src: "images/actions/instance/stop.svg", + alt: "Shutdown Instance", + clickable: true + }, + config: { + src: "images/actions/instance/config-inactive.svg", + alt: "Change Configuration (Inactive)", + clickable: false + }, + console: { + src: "images/actions/instance/console-active.svg", + alt: "Open Console", + clickable: true + }, + delete: { + src: "images/actions/delete-inactive.svg", + alt: "Delete Instance (Inactive)", + clickable: false + } + }, + stopped: { + status: { + src: "images/status/inactive.svg", + alt: "Instance is stopped", + clickable: false + }, + power: { + src: "images/actions/instance/start.svg", + alt: "Start Instance", + clickable: true + }, + config: { + src: "images/actions/instance/config-active.svg", + alt: "Change Configuration", + clickable: true + }, + console: { + src: "images/actions/instance/console-inactive.svg", + alt: "Open Console (Inactive)", + clickable: false + }, + delete: { + src: "images/actions/delete-active.svg", + alt: "Delete Instance", + clickable: true + } + }, + loading: { + status: { + src: "images/status/loading.svg", + alt: "Instance is loading", + clickable: false + }, + power: { + src: "images/status/loading.svg", + alt: "Loading Instance", + clickable: false + }, + config: { + src: "images/actions/instance/config-inactive.svg", + alt: "Change Configuration (Inactive)", + clickable: false + }, + console: { + src: "images/actions/instance/console-inactive.svg", + alt: "Open Console (Inactive)", + clickable: false + }, + delete: { + src: "images/actions/delete-inactive.svg", + alt: "Delete Instance (Inactive)", + clickable: false + } + } +}; + +export const nodesConfig = { + online: { + status: { + src: "images/status/active.svg", + alt: "Node is online" + } + }, + offline: { + status: { + src: "images/status/inactive.svg", + alt: "Node is offline" + } + }, + uknown: { + status: { + src: "images/status/inactive.svg", + alt: "Node is offline" + } + } +}; + +export function getCookie (cname) { + const name = cname + "="; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === " ") { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ""; +} + +export async function requestTicket (username, password, realm) { + const response = await requestAPI("/ticket", "POST", { username: `${username}@${realm}`, password }, false); + return response; +} + +export async function requestPVE (path, method, body = null) { + const prms = new URLSearchParams(body); + const content = { + method, + mode: "cors", + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }; + if (method === "POST") { + content.body = prms.toString(); + content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken"); + } + + const response = await request(`${API}/proxmox${path}`, content); + return response; +} + +export async function requestAPI (path, method, body = null) { + const prms = new URLSearchParams(body); + const content = { + method, + mode: "cors", + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }; + if (method === "POST" || method === "DELETE") { + content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken"); + } + if (body) { + content.body = prms.toString(); + } + + const response = await request(`${API}${path}`, content); + return response; +} + +async function request (url, content) { + const response = await fetch(url, content); + let data = null; + try { + data = await response.json(); + data.status = response.status; + } + catch { + data = null; + } + + if (!response.ok) { + return { status: response.status, error: data ? data.error : response.status }; + } + else { + data.status = response.status; + return data || response; + } +} + +export function goToPage (page, data = {}, newwindow = false) { + const url = new URL(`https://${window.location.host}/${page}`); + for (const k in data) { + url.searchParams.append(k, data[k]); + } + + if (newwindow) { + window.open(url, `${organization} - client`, "height=480,width=848"); + } + else { + window.location.assign(url.toString()); + } +} + +export function goToURL (href, data = {}, newwindow = false) { + const url = new URL(href); + for (const k in data) { + url.searchParams.append(k, data[k]); + } + + if (newwindow) { + window.open(url, `${organization} - client`, "height=480,width=848"); + } + else { + window.location.assign(url.toString()); + } +} + +export function getURIData () { + const url = new URL(window.location.href); + return Object.fromEntries(url.searchParams); +} + +export async function deleteAllCookies () { + await requestAPI("/ticket", "DELETE"); +} + +export function setTitleAndHeader () { + document.title = `${organization} - client`; + document.querySelector("h1").innerText = organization; +}