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
-
+