From 0f8a85d22a6261f2c8e5d7981cb31245c7471078 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 15 Sep 2023 22:13:21 +0000 Subject: [PATCH] finish renaming, improve search bar and add instance responsiveness --- account.html | 2 +- config.html | 2 +- images/actions/{network => instance}/add.svg | 0 index.html | 38 ++- login.html | 2 +- package.json | 2 +- scripts/index.js | 232 +------------------ scripts/instance.js | 231 ++++++++++++++++++ scripts/utils.js | 6 +- settings.html | 2 +- template.vars.js | 2 +- 11 files changed, 278 insertions(+), 241 deletions(-) rename images/actions/{network => instance}/add.svg (100%) create mode 100644 scripts/instance.js diff --git a/account.html b/account.html index 5cc2f52..4a84b80 100644 --- a/account.html +++ b/account.html @@ -3,7 +3,7 @@ - proxmox - client + proxmox - dashboard diff --git a/config.html b/config.html index 23d5ba5..e8f0463 100644 --- a/config.html +++ b/config.html @@ -3,7 +3,7 @@ - proxmox - client + proxmox - dashboard diff --git a/images/actions/network/add.svg b/images/actions/instance/add.svg similarity index 100% rename from images/actions/network/add.svg rename to images/actions/instance/add.svg diff --git a/index.html b/index.html index 4c1c864..6f6fe68 100644 --- a/index.html +++ b/index.html @@ -3,13 +3,14 @@ - proxmox - client + proxmox - dashboard + @@ -35,12 +58,15 @@

Instances

-
-
diff --git a/login.html b/login.html index 4bd9f5c..68dfee8 100644 --- a/login.html +++ b/login.html @@ -3,7 +3,7 @@ - proxmox - client + proxmox - dashboard diff --git a/package.json b/package.json index 1f2d4a7..21fb0ce 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "proxmoxaas-client", + "name": "proxmoxaas-dashboard", "version": "0.0.1", "description": "Front-end for ProxmoxAAS", "type": "module", diff --git a/scripts/index.js b/scripts/index.js index 6903822..df539ae 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,6 +1,5 @@ -import { requestPVE, requestAPI, goToPage, goToURL, instancesConfig, nodesConfig, setTitleAndHeader } from "./utils.js"; +import { requestPVE, requestAPI, goToPage, setTitleAndHeader } from "./utils.js"; import { alert, dialog } from "./dialog.js"; -import { PVE } from "../vars.js"; import { setupClientSync } from "./clientsync.js"; window.addEventListener("DOMContentLoaded", init); @@ -12,10 +11,13 @@ async function init () { goToPage("login.html"); } - const addInstanceBtn = document.querySelector("#instance-add"); - addInstanceBtn.addEventListener("click", handleInstanceAdd); + document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd); setupClientSync(populateInstances); + + document.querySelector("#vm-search").addEventListener("input", () => { + + }); } async function populateInstances () { @@ -197,225 +199,3 @@ async function handleInstanceAdd () { d.querySelector("#vmid").min = userCluster.vmid.min; d.querySelector("#vmid").max = userCluster.vmid.max; } - -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 - }; - } - - 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.shadowRoot.querySelector("#instance-id"); - vmidParagraph.innerText = this.vmid; - - const nameParagraph = this.shadowRoot.querySelector("#instance-name"); - nameParagraph.innerText = 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"); - statusIcon.src = instancesConfig[this.status].status.src; - statusIcon.alt = 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"); - nodeStatusIcon.src = nodesConfig[this.node.status].status.src; - nodeStatusIcon.alt = nodesConfig[this.node.status].status.src; - - const powerButton = this.shadowRoot.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.shadowRoot.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.shadowRoot.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.shadowRoot.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 result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); - if (result.status === 200) { - 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/scripts/instance.js b/scripts/instance.js new file mode 100644 index 0000000..b7b1127 --- /dev/null +++ b/scripts/instance.js @@ -0,0 +1,231 @@ +import { requestPVE, requestAPI, goToPage, goToURL, instancesConfig, nodesConfig } 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 + }; + } + + 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.shadowRoot.querySelector("#instance-id"); + vmidParagraph.innerText = this.vmid; + + const nameParagraph = this.shadowRoot.querySelector("#instance-name"); + nameParagraph.innerText = 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"); + statusIcon.src = instancesConfig[this.status].status.src; + statusIcon.alt = 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"); + nodeStatusIcon.src = nodesConfig[this.node.status].status.src; + nodeStatusIcon.alt = nodesConfig[this.node.status].status.src; + + const powerButton = this.shadowRoot.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.shadowRoot.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.shadowRoot.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.shadowRoot.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 result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE"); + if (result.status === 200) { + 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/scripts/utils.js b/scripts/utils.js index 0e360aa..ca4ddab 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -235,7 +235,7 @@ export function goToPage (page, data = {}, newwindow = false) { } if (newwindow) { - window.open(url, `${organization} - client`, "height=480,width=848"); + window.open(url, `${organization} - dashboard`, "height=480,width=848"); } else { window.location.assign(url.toString()); @@ -249,7 +249,7 @@ export function goToURL (href, data = {}, newwindow = false) { } if (newwindow) { - window.open(url, `${organization} - client`, "height=480,width=848"); + window.open(url, `${organization} - dashboard`, "height=480,width=848"); } else { window.location.assign(url.toString()); @@ -266,6 +266,6 @@ export async function deleteAllCookies () { } export function setTitleAndHeader () { - document.title = `${organization} - client`; + document.title = `${organization} - dashboard`; document.querySelector("h1").innerText = organization; } diff --git a/settings.html b/settings.html index 5518ac3..40d7459 100644 --- a/settings.html +++ b/settings.html @@ -3,7 +3,7 @@ - proxmox - client + proxmox - dashboard diff --git a/template.vars.js b/template.vars.js index 70df2df..747f239 100644 --- a/template.vars.js +++ b/template.vars.js @@ -1,3 +1,3 @@ -export const API = "https://client.mydomain.example/api"; // the proxmox-aas api +export const API = "https://dashboard.mydomain.example/api"; // the proxmox-aas api export const PVE = "https://pve.mydomain.example"; // the proxmox api export const organization = "mydomain"; // org name used in page title and nav bar