diff --git a/.eslintrc.json b/.eslintrc.json index a328d17..aa730af 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,14 +1,14 @@ { - "env": { - "browser": true, - "es2021": true - }, - "extends": "standard", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "rules": { + "env": { + "browser": true, + "es2021": true + }, + "extends": "standard", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { "no-tabs": [ "error", { @@ -38,5 +38,5 @@ "allowSingleLine": false } ] - } + } } diff --git a/account.html b/account.html index 0f8bf77..0125233 100644 --- a/account.html +++ b/account.html @@ -5,13 +5,12 @@ proxmox - dashboard - + - + +
+
+ +
+
+
+ `; + this.responsiveStyle = this.shadowRoot.querySelector("#responsive-style"); + this.canvas = this.shadowRoot.querySelector("canvas"); + this.caption = this.shadowRoot.querySelector("figcaption"); + } + + set data (data) { + for (const line of data.title) { + this.caption.innerHTML += `${line}`; + } + + this.canvas.role = "img"; + this.canvas.ariaLabel = data.ariaLabel; + + const chartData = { + type: "pie", + data: data.data, + options: { + plugins: { + title: { + display: false + }, + legend: { + display: false + }, + tooltip: { + enabled: true + } + }, + interaction: { + mode: "nearest" + }, + onHover: function (e, activeElements) { + if (window.innerWidth <= data.breakpoint) { + updateTooltipShow(e.chart, false); + } + else { + updateTooltipShow(e.chart, true); + } + } + } + }; + + this.chart = new window.Chart(this.canvas, chartData); + + if (data.breakpoint) { + this.responsiveStyle.media = `screen and (width <= ${data.breakpoint}px)`; + } + else { + this.responsiveStyle.media = "not all"; + } + } + + get data () { + return null; + } +} + +// this is a really bad way to do this, but chartjs api does not expose many ways to dynamically set hover and tooltip options +function updateTooltipShow (chart, enabled) { + chart.options.plugins.tooltip.enabled = enabled; + chart.options.interaction.mode = enabled ? "nearest" : null; + chart.update(); +} + +customElements.define("resource-chart", ResourceChart); + window.addEventListener("DOMContentLoaded", init); const prefixes = { diff --git a/scripts/chart.js b/scripts/chart.js deleted file mode 100644 index cea5144..0000000 --- a/scripts/chart.js +++ /dev/null @@ -1,120 +0,0 @@ -class ResourceChart extends HTMLElement { - constructor () { - super(); - this.attachShadow({ mode: "open" }); - this.shadowRoot.innerHTML = ` - - -
-
- -
-
-
- `; - this.responsiveStyle = this.shadowRoot.querySelector("#responsive-style"); - this.canvas = this.shadowRoot.querySelector("canvas"); - this.caption = this.shadowRoot.querySelector("figcaption"); - } - - set data (data) { - for (const line of data.title) { - this.caption.innerHTML += `${line}`; - } - - this.canvas.role = "img"; - this.canvas.ariaLabel = data.ariaLabel; - - const chartData = { - type: "pie", - data: data.data, - options: { - plugins: { - title: { - display: false - }, - legend: { - display: false - }, - tooltip: { - enabled: true - } - }, - interaction: { - mode: "nearest" - }, - onHover: function (e, activeElements) { - if (window.innerWidth <= data.breakpoint) { - updateTooltipShow(e.chart, false); - } - else { - updateTooltipShow(e.chart, true); - } - } - } - }; - - this.chart = createChart(this.canvas, chartData); - - if (data.breakpoint) { - this.responsiveStyle.media = `screen and (width <= ${data.breakpoint}px)`; - } - else { - this.responsiveStyle.media = "not all"; - } - } - - get data () { - return null; - } -} - -function createChart (ctx, data) { - return new window.Chart(ctx, data); -} - -// this is a really bad way to do this, but chartjs api does not expose many ways to dynamically set hover and tooltip options -function updateTooltipShow (chart, enabled) { - chart.options.plugins.tooltip.enabled = enabled; - chart.options.interaction.mode = enabled ? "nearest" : null; - chart.update(); -} - -customElements.define("resource-chart", ResourceChart); diff --git a/scripts/index.js b/scripts/index.js index 6eac691..6a79bfd 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,7 +1,245 @@ -import { requestPVE, requestAPI, goToPage, setTitleAndHeader, setAppearance, getSearchSettings } from "./utils.js"; +import { requestPVE, requestAPI, goToPage, setTitleAndHeader, setAppearance, getSearchSettings, goToURL, instancesConfig, nodesConfig, setSVGSrc, setSVGAlt } from "./utils.js"; import { alert, dialog } from "./dialog.js"; import { setupClientSync } from "./clientsync.js"; import wfAlign from "../modules/wfa.js"; +import { PVE } from "../vars.js"; + +class InstanceCard extends HTMLElement { + constructor () { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.innerHTML = ` + + + + +
+
+

+

+

+
+ +

+
+

+
+ +

+
+
+ + + + +
+
+ `; + this.actionLock = false; + } + + get data () { + return { + type: this.type, + status: this.status, + vmid: this.status, + name: this.name, + node: this.node, + searchQuery: this.searchQuery + }; + } + + 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.searchQuery = data.searchQuery; + this.update(); + } + + update () { + const vmidParagraph = this.shadowRoot.querySelector("#instance-id"); + vmidParagraph.innerText = this.vmid; + + const nameParagraph = this.shadowRoot.querySelector("#instance-name"); + if (this.searchQuery) { + const regExpEscape = v => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const escapedQuery = regExpEscape(this.searchQuery); + const searchRegExp = new RegExp(`(${escapedQuery})`, "gi"); + const nameParts = this.name.split(searchRegExp); + for (let i = 0; i < nameParts.length; i++) { + const part = document.createElement("span"); + part.innerText = nameParts[i]; + if (nameParts[i].toLowerCase() === this.searchQuery.toLowerCase()) { + part.style = "color: var(--lightbg-text-color); background-color: var(--highlight-color);"; + } + nameParagraph.append(part); + } + } + else { + nameParagraph.innerHTML = this.name ? this.name : " "; + } + + const typeParagraph = this.shadowRoot.querySelector("#instance-type"); + typeParagraph.innerText = this.type; + + const statusParagraph = this.shadowRoot.querySelector("#instance-status"); + statusParagraph.innerText = this.status; + + const statusIcon = this.shadowRoot.querySelector("#instance-status-icon"); + setSVGSrc(statusIcon, instancesConfig[this.status].status.src); + setSVGAlt(statusIcon, instancesConfig[this.status].status.alt); + + const nodeNameParagraph = this.shadowRoot.querySelector("#node-name"); + nodeNameParagraph.innerText = this.node.name; + + const nodeStatusParagraph = this.shadowRoot.querySelector("#node-status"); + nodeStatusParagraph.innerText = this.node.status; + + const nodeStatusIcon = this.shadowRoot.querySelector("#node-status-icon"); + setSVGSrc(nodeStatusIcon, nodesConfig[this.node.status].status.src); + setSVGAlt(nodeStatusIcon, nodesConfig[this.node.status].status.alt); + + const powerButton = this.shadowRoot.querySelector("#power-btn"); + setSVGSrc(powerButton, instancesConfig[this.status].power.src); + setSVGAlt(powerButton, instancesConfig[this.status].power.alt); + if (instancesConfig[this.status].power.clickable) { + powerButton.classList.add("clickable"); + powerButton.onclick = this.handlePowerButton.bind(this); + } + + const configButton = this.shadowRoot.querySelector("#configure-btn"); + setSVGSrc(configButton, instancesConfig[this.status].config.src); + setSVGAlt(configButton, instancesConfig[this.status].config.alt); + if (instancesConfig[this.status].config.clickable) { + configButton.classList.add("clickable"); + configButton.onclick = this.handleConfigButton.bind(this); + } + + const consoleButton = this.shadowRoot.querySelector("#console-btn"); + setSVGSrc(consoleButton, instancesConfig[this.status].console.src); + setSVGAlt(consoleButton, instancesConfig[this.status].console.alt); + if (instancesConfig[this.status].console.clickable) { + consoleButton.classList.add("clickable"); + consoleButton.onclick = this.handleConsoleButton.bind(this); + } + + const deleteButton = this.shadowRoot.querySelector("#delete-btn"); + setSVGSrc(deleteButton, instancesConfig[this.status].delete.src); + setSVGAlt(deleteButton, 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 result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); + if (result.status === 200) { + if (this.parentElement) { + this.parentElement.removeChild(this); + } + } + else { + alert(result.error); + this.status = this.prevStatus; + this.update(); + this.actionLock = false; + } + } + }); + } + } +} + +customElements.define("instance-card", InstanceCard); window.addEventListener("DOMContentLoaded", init); diff --git a/scripts/instance.js b/scripts/instance.js deleted file mode 100644 index bb2f452..0000000 --- a/scripts/instance.js +++ /dev/null @@ -1,248 +0,0 @@ -import { requestPVE, requestAPI, goToPage, goToURL, instancesConfig, nodesConfig, setSVGSrc, setSVGAlt } from "./utils.js"; -import { PVE } from "../vars.js"; -import { dialog } from "./dialog.js"; - -class InstanceCard extends HTMLElement { - constructor () { - super(); - this.attachShadow({ mode: "open" }); - this.shadowRoot.innerHTML = ` - - - - -
-
-
-

-
-
-

-
-
-

-
-
- -

-
-
-

-
-
- -

-
-
- - - - -
-
- `; - this.actionLock = false; - } - - get data () { - return { - type: this.type, - status: this.status, - vmid: this.status, - name: this.name, - node: this.node, - searchQuery: this.searchQuery - }; - } - - 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.searchQuery = data.searchQuery; - this.update(); - } - - update () { - const vmidParagraph = this.shadowRoot.querySelector("#instance-id"); - vmidParagraph.innerText = this.vmid; - - const nameParagraph = this.shadowRoot.querySelector("#instance-name"); - if (this.searchQuery) { - const regExpEscape = v => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const escapedQuery = regExpEscape(this.searchQuery); - const searchRegExp = new RegExp(`(${escapedQuery})`, "gi"); - const nameParts = this.name.split(searchRegExp); - for (let i = 0; i < nameParts.length; i++) { - const part = document.createElement("span"); - part.innerText = nameParts[i]; - if (nameParts[i].toLowerCase() === this.searchQuery.toLowerCase()) { - part.style = "color: var(--lightbg-text-color); background-color: var(--highlight-color);"; - } - nameParagraph.append(part); - } - } - else { - nameParagraph.innerHTML = this.name ? this.name : " "; - } - - const typeParagraph = this.shadowRoot.querySelector("#instance-type"); - typeParagraph.innerText = this.type; - - const statusParagraph = this.shadowRoot.querySelector("#instance-status"); - statusParagraph.innerText = this.status; - - const statusIcon = this.shadowRoot.querySelector("#instance-status-icon"); - setSVGSrc(statusIcon, instancesConfig[this.status].status.src); - setSVGAlt(statusIcon, instancesConfig[this.status].status.alt); - - const nodeNameParagraph = this.shadowRoot.querySelector("#node-name"); - nodeNameParagraph.innerText = this.node.name; - - const nodeStatusParagraph = this.shadowRoot.querySelector("#node-status"); - nodeStatusParagraph.innerText = this.node.status; - - const nodeStatusIcon = this.shadowRoot.querySelector("#node-status-icon"); - setSVGSrc(nodeStatusIcon, nodesConfig[this.node.status].status.src); - setSVGAlt(nodeStatusIcon, nodesConfig[this.node.status].status.alt); - - const powerButton = this.shadowRoot.querySelector("#power-btn"); - setSVGSrc(powerButton, instancesConfig[this.status].power.src); - setSVGAlt(powerButton, instancesConfig[this.status].power.alt); - if (instancesConfig[this.status].power.clickable) { - powerButton.classList.add("clickable"); - powerButton.onclick = this.handlePowerButton.bind(this); - } - - const configButton = this.shadowRoot.querySelector("#configure-btn"); - setSVGSrc(configButton, instancesConfig[this.status].config.src); - setSVGAlt(configButton, instancesConfig[this.status].config.alt); - if (instancesConfig[this.status].config.clickable) { - configButton.classList.add("clickable"); - configButton.onclick = this.handleConfigButton.bind(this); - } - - const consoleButton = this.shadowRoot.querySelector("#console-btn"); - setSVGSrc(consoleButton, instancesConfig[this.status].console.src); - setSVGAlt(consoleButton, instancesConfig[this.status].console.alt); - if (instancesConfig[this.status].console.clickable) { - consoleButton.classList.add("clickable"); - consoleButton.onclick = this.handleConsoleButton.bind(this); - } - - const deleteButton = this.shadowRoot.querySelector("#delete-btn"); - setSVGSrc(deleteButton, instancesConfig[this.status].delete.src); - setSVGAlt(deleteButton, 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 result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); - if (result.status === 200) { - if (this.parentElement) { - this.parentElement.removeChild(this); - } - } - else { - alert(result.error); - this.status = this.prevStatus; - this.update(); - this.actionLock = false; - } - } - }); - } - } -} - -customElements.define("instance-card", InstanceCard); diff --git a/settings.html b/settings.html index a4f5a42..d7b8d85 100644 --- a/settings.html +++ b/settings.html @@ -5,7 +5,7 @@ proxmox - dashboard - +