cleanup various unused code,
simplify instance action hiding when node is not online
This commit is contained in:
@@ -10,7 +10,7 @@ import (
|
||||
"proxmoxaas-dashboard/app/routes"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tdewolff/minify"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
)
|
||||
|
||||
func Run() {
|
||||
|
@@ -3,17 +3,19 @@ package common
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/tdewolff/minify"
|
||||
"github.com/tdewolff/minify/css"
|
||||
"github.com/tdewolff/minify/html"
|
||||
"github.com/tdewolff/minify/js"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/css"
|
||||
"github.com/tdewolff/minify/v2/html"
|
||||
"github.com/tdewolff/minify/v2/js"
|
||||
)
|
||||
|
||||
// defines mime type and associated minifier
|
||||
type MimeType struct {
|
||||
Type string
|
||||
Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error
|
||||
}
|
||||
|
||||
// map file extension to mime types
|
||||
var MimeTypes = map[string]MimeType{
|
||||
"css": {
|
||||
Type: "text/css",
|
||||
|
@@ -16,7 +16,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tdewolff/minify"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
)
|
||||
|
||||
var TMPL *template.Template
|
||||
|
2
go.mod
2
go.mod
@@ -35,7 +35,9 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.23.1 // indirect
|
||||
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.23 // indirect
|
||||
github.com/tdewolff/test v1.0.11 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
|
@@ -77,4 +77,6 @@ input[type="radio"] {
|
||||
|
||||
dialog {
|
||||
max-width: calc(min(50%, 80ch));
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-text-color);
|
||||
}
|
@@ -61,13 +61,10 @@ body {
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
main, dialog {
|
||||
main {
|
||||
max-width: 100vw;
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,7 @@
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2 id="name"><a href="index">Instances</a> / {{.config.Name}}</h2>
|
||||
<h2><a href="index">Instances</a> / {{.config.Name}}</h2>
|
||||
<form>
|
||||
<fieldset class="w3-card w3-padding">
|
||||
<legend>Resources</legend>
|
||||
|
@@ -12,10 +12,10 @@ async function init () {
|
||||
function handlePasswordChangeForm () {
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="new-password">New Password</label>
|
||||
<input class="w3-input w3-border" id="new-password" name="new-password" type="password"required>
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" required>
|
||||
<label for="new-password">New Password</label>
|
||||
<input class="w3-input w3-border" id="new-password" name="new-password" type="password"required>
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" required>
|
||||
</form>
|
||||
`;
|
||||
const d = dialog("Change Password", body, async (result, form) => {
|
||||
|
@@ -6,7 +6,6 @@ window.addEventListener("DOMContentLoaded", init);
|
||||
let node;
|
||||
let type;
|
||||
let vmid;
|
||||
let config;
|
||||
|
||||
async function init () {
|
||||
setAppearance();
|
||||
@@ -16,11 +15,6 @@ async function init () {
|
||||
type = uriData.type;
|
||||
vmid = uriData.vmid;
|
||||
|
||||
await getConfig();
|
||||
|
||||
const name = type === "qemu" ? "name" : "hostname";
|
||||
document.querySelector("#name").innerHTML = document.querySelector("#name").innerHTML.replace("%{vmname}", config.data[name]);
|
||||
|
||||
initVolumes();
|
||||
initNetworks();
|
||||
initDevices();
|
||||
@@ -28,10 +22,6 @@ async function init () {
|
||||
document.querySelector("#exit").addEventListener("click", handleFormExit);
|
||||
}
|
||||
|
||||
async function getConfig () {
|
||||
config = await requestPVE(`/nodes/${node}/${type}/${vmid}/config`, "GET");
|
||||
}
|
||||
|
||||
class VolumeAction extends HTMLElement {
|
||||
shadowRoot = null;
|
||||
|
||||
@@ -83,7 +73,7 @@ class VolumeAction extends HTMLElement {
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">${type === "qemu" ? "SCSI" : "MP"}</label>
|
||||
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" required>
|
||||
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "30" : "255"}" required>
|
||||
</form>
|
||||
`;
|
||||
|
||||
@@ -113,7 +103,7 @@ class VolumeAction extends HTMLElement {
|
||||
<label for="size-increment">Size Increment (GiB)</label>
|
||||
<input class="w3-input w3-border" name="size-increment" id="size-increment" type="number" min="0" max="131072">
|
||||
</form>
|
||||
`;
|
||||
`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@@ -135,9 +125,7 @@ class VolumeAction extends HTMLElement {
|
||||
async handleDiskMove () {
|
||||
const content = type === "qemu" ? "images" : "rootdir";
|
||||
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||
|
||||
const header = `Move ${this.dataset.volume}`;
|
||||
|
||||
let options = "";
|
||||
storage.data.forEach((element) => {
|
||||
if (element.content.includes(content)) {
|
||||
@@ -215,9 +203,7 @@ async function refreshVolumes () {
|
||||
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)) {
|
||||
@@ -228,7 +214,7 @@ async function handleDiskAdd () {
|
||||
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">${type === "qemu" ? "SCSI" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" value="0" required>
|
||||
<label for="device">${type === "qemu" ? "SCSI" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "30" : "255"}" value="0" required>
|
||||
${select}
|
||||
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required>
|
||||
</form>
|
||||
@@ -255,9 +241,7 @@ async function handleDiskAdd () {
|
||||
|
||||
async function handleCDAdd () {
|
||||
const isos = await requestAPI("/user/vm-isos", "GET");
|
||||
|
||||
const header = "Mount a CDROM";
|
||||
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required>
|
||||
@@ -341,7 +325,6 @@ class NetworkAction extends HTMLElement {
|
||||
const netID = this.dataset.network;
|
||||
const header = `Delete ${netID}`;
|
||||
const body = "";
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
setSVGSrc(document.querySelector(`svg[data-network="${netID}"]`), "images/status/loading.svg");
|
||||
@@ -388,7 +371,6 @@ async function handleNetworkAdd () {
|
||||
body += "<label for=\"name\">Interface Name</label><input type=\"text\" id=\"name\" name=\"name\" class=\"w3-input w3-border\">";
|
||||
}
|
||||
body += "</form>";
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
const body = {
|
||||
@@ -468,7 +450,6 @@ class DeviceAction extends HTMLElement {
|
||||
const deviceID = this.dataset.device;
|
||||
const header = `Remove Expansion Card ${deviceID}`;
|
||||
const body = "";
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
this.setStatusLoading();
|
||||
@@ -514,7 +495,6 @@ async function handleDeviceAdd () {
|
||||
<label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
|
||||
</form>
|
||||
`;
|
||||
|
||||
const d = dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
const hostpci = form.get("hostpci");
|
||||
|
@@ -3,6 +3,20 @@ import { alert, dialog } from "./dialog.js";
|
||||
import { setupClientSync } from "./clientsync.js";
|
||||
import wfaInit from "../modules/wfa.js";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
async function init () {
|
||||
setAppearance();
|
||||
|
||||
wfaInit("modules/wfa.wasm");
|
||||
initInstances();
|
||||
|
||||
document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd);
|
||||
document.querySelector("#vm-search").addEventListener("input", sortInstances);
|
||||
|
||||
setupClientSync(refreshInstances);
|
||||
}
|
||||
|
||||
class InstanceCard extends HTMLElement {
|
||||
actionLock = false;
|
||||
shadowRoot = null;
|
||||
@@ -125,20 +139,12 @@ class InstanceCard extends HTMLElement {
|
||||
if (deleteButton.classList.contains("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 = `<p>Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM ${this.vmid}</p>`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
this.actionLock = true;
|
||||
@@ -226,22 +232,8 @@ class InstanceCard extends HTMLElement {
|
||||
|
||||
customElements.define("instance-card", InstanceCard);
|
||||
|
||||
window.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
async function init () {
|
||||
setAppearance();
|
||||
|
||||
wfaInit("modules/wfa.wasm");
|
||||
initInstances();
|
||||
|
||||
document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd);
|
||||
document.querySelector("#vm-search").addEventListener("input", sortInstances);
|
||||
|
||||
setupClientSync(refreshInstances);
|
||||
}
|
||||
|
||||
async function getInstancesFragment () {
|
||||
return await requestDash("/index/instances", "GET")
|
||||
return await requestDash("/index/instances", "GET");
|
||||
}
|
||||
|
||||
async function refreshInstances () {
|
||||
|
@@ -1,91 +1,3 @@
|
||||
export const resourcesConfig = {
|
||||
cpu: {
|
||||
name: "CPU Type",
|
||||
icon: "images/resources/cpu.svg",
|
||||
id: "proctype",
|
||||
unitText: null
|
||||
},
|
||||
cores: {
|
||||
name: "CPU Amount",
|
||||
icon: "images/resources/cpu.svg",
|
||||
id: "cores",
|
||||
unitText: "Cores"
|
||||
},
|
||||
memory: {
|
||||
name: "Memory",
|
||||
icon: "images/resources/ram.svg",
|
||||
id: "ram",
|
||||
unitText: "MiB"
|
||||
},
|
||||
swap: {
|
||||
name: "Swap",
|
||||
icon: "images/resources/swap.svg",
|
||||
id: "swap",
|
||||
unitText: "MiB"
|
||||
},
|
||||
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", "scsi", "unused"],
|
||||
ide: { name: "IDE", icon: "images/resources/disk.svg", actions: ["delete"] },
|
||||
sata: { name: "SATA", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] },
|
||||
scsi: { name: "SCSI", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] },
|
||||
unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] }
|
||||
},
|
||||
actions: {
|
||||
attach: {
|
||||
src: "images/actions/disk/attach.svg",
|
||||
title: "Attach Disk"
|
||||
},
|
||||
detach: {
|
||||
src: "images/actions/disk/detach.svg",
|
||||
title: "Detach Disk"
|
||||
},
|
||||
delete: null
|
||||
}
|
||||
},
|
||||
network: {
|
||||
name: "Network",
|
||||
icon: "images/resources/network.svg",
|
||||
id: "network",
|
||||
unitText: "MB/s",
|
||||
prefix: "net"
|
||||
},
|
||||
pci: {
|
||||
name: "Devices",
|
||||
icon: "images/resources/device.svg",
|
||||
id: "devices",
|
||||
unitText: null,
|
||||
prefix: "hostpci"
|
||||
}
|
||||
};
|
||||
|
||||
export const bootConfig = {
|
||||
eligiblePrefixes: ["ide", "sata", "scsi", "net"],
|
||||
ide: {
|
||||
icon: "images/resources/disk.svg",
|
||||
alt: "IDE Bootable Icon"
|
||||
},
|
||||
sata: {
|
||||
icon: "images/resources/drive.svg",
|
||||
alt: "SATA Bootable Icon"
|
||||
},
|
||||
scsi: {
|
||||
icon: "images/resources/drive.svg",
|
||||
alt: "SCSI Bootable Icon"
|
||||
},
|
||||
net: {
|
||||
icon: "images/resources/network.svg",
|
||||
alt: "NET Bootable Icon"
|
||||
}
|
||||
};
|
||||
|
||||
export function setCookie (cname, cval) {
|
||||
document.cookie = `${cname}=${cval}`;
|
||||
}
|
||||
@@ -301,135 +213,3 @@ export function setSVGSrc (svgElem, href) {
|
||||
export function setSVGAlt (svgElem, alt) {
|
||||
svgElem.setAttribute("aria-label", alt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple object check.
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isObject (item) {
|
||||
return (item && typeof item === "object" && !Array.isArray(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects.
|
||||
* @param target
|
||||
* @param ...sources
|
||||
*/
|
||||
export function mergeDeep (target, ...sources) {
|
||||
if (!sources.length) return target;
|
||||
const source = sources.shift();
|
||||
|
||||
if (isObject(target) && isObject(source)) {
|
||||
for (const key in source) {
|
||||
if (isObject(source[key])) {
|
||||
if (!target[key]) Object.assign(target, { [key]: {} });
|
||||
mergeDeep(target[key], source[key]);
|
||||
}
|
||||
else {
|
||||
Object.assign(target, { [key]: source[key] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergeDeep(target, ...sources);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if object or array is empty
|
||||
* @param {*} obj
|
||||
* @returns
|
||||
*/
|
||||
export function isEmpty (obj) {
|
||||
if (obj instanceof Array) {
|
||||
return obj.length === 0;
|
||||
}
|
||||
else {
|
||||
for (const prop in obj) {
|
||||
if (Object.hasOwn(obj, prop)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export function addResourceLine (resourceConfig, field, attributesOverride, labelPrefix = null) {
|
||||
const iconHref = resourceConfig.icon;
|
||||
const elementType = resourceConfig.element;
|
||||
const labelText = labelPrefix ? `${labelPrefix} ${resourceConfig.name}` : resourceConfig.name;
|
||||
const id = resourceConfig.id;
|
||||
const unitText = resourceConfig.unitText;
|
||||
const attributes = { ...(resourceConfig.attributes), ...(attributesOverride) };
|
||||
|
||||
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
setSVGSrc(icon, iconHref);
|
||||
setSVGAlt(icon, labelText);
|
||||
field.append(icon);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.innerText = labelText;
|
||||
label.htmlFor = id;
|
||||
field.append(label);
|
||||
|
||||
let element;
|
||||
|
||||
if (elementType === "input") {
|
||||
element = document.createElement("input");
|
||||
for (const k in attributes) {
|
||||
element.setAttribute(k, attributes[k]);
|
||||
}
|
||||
element.id = id;
|
||||
element.name = id;
|
||||
element.required = true;
|
||||
element.classList.add("w3-input");
|
||||
element.classList.add("w3-border");
|
||||
field.append(element);
|
||||
}
|
||||
else if (elementType === "select" || elementType === "multi-select") {
|
||||
element = document.createElement("select");
|
||||
for (const option of attributes.options) {
|
||||
element.append(new Option(option));
|
||||
}
|
||||
element.value = attributes.value;
|
||||
element.id = id;
|
||||
element.name = id;
|
||||
element.required = true;
|
||||
element.classList.add("w3-select");
|
||||
element.classList.add("w3-border");
|
||||
if (elementType === "multi-select") {
|
||||
element.setAttribute("multiple", true);
|
||||
}
|
||||
field.append(element);
|
||||
}
|
||||
else if (customElements.get(elementType)) {
|
||||
element = document.createElement(elementType);
|
||||
if (attributes.options) {
|
||||
for (const option of attributes.options) {
|
||||
element.append(new Option(option));
|
||||
}
|
||||
}
|
||||
element.value = attributes.value;
|
||||
element.id = id;
|
||||
element.name = id;
|
||||
element.required = true;
|
||||
field.append(element);
|
||||
}
|
||||
|
||||
let unit;
|
||||
|
||||
if (unitText) {
|
||||
unit = document.createElement("p");
|
||||
unit.innerText = unitText;
|
||||
field.append(unit);
|
||||
}
|
||||
else {
|
||||
unit = document.createElement("div");
|
||||
unit.classList.add("hidden");
|
||||
field.append(unit);
|
||||
}
|
||||
|
||||
return { icon, label, element, unit };
|
||||
}
|
||||
*/
|
||||
|
@@ -38,17 +38,17 @@
|
||||
<p>{{.NodeStatus}}</p>
|
||||
</div>
|
||||
<div class="w3-col l2 m2 s6 flex row nowrap" style="height: 1lh;">
|
||||
{{if eq .Status "running"}}
|
||||
{{if and (eq .NodeStatus "online") (eq .Status "running")}}
|
||||
<svg id="power-btn" class="clickable" aria-label="shutdown instance"><use href="images/actions/instance/stop.svg#symb"></svg>
|
||||
<svg id="configure-btn" aria-label=""><use href="images/actions/instance/config-inactive.svg#symb"></svg>
|
||||
<svg id="console-btn" class="clickable" aria-label="open console"><use href="images/actions/instance/console-active.svg#symb"></svg>
|
||||
<svg id="delete-btn" aria-label=""><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
|
||||
{{else if eq .Status "stopped"}}
|
||||
{{else if and (eq .NodeStatus "online") (eq .Status "stopped")}}
|
||||
<svg id="power-btn" class="clickable" aria-label="start instance"><use href="images/actions/instance/start.svg#symb"></svg>
|
||||
<svg id="configure-btn" class="clickable" aria-label="change configuration"><use href="images/actions/instance/config-active.svg#symb"></svg>
|
||||
<svg id="console-btn" aria-label=""><use href="images/actions/instance/console-inactive.svg#symb"></svg>
|
||||
<svg id="delete-btn" class="clickable" aria-label="delete instance"><use href="images/actions/instance/delete-active.svg#symb"></svg>
|
||||
{{else if eq .Status "loading"}}
|
||||
{{else if and (eq .NodeStatus "online") (eq .Status "loading")}}
|
||||
<svg id="power-btn" aria-label=""><use href="images/actions/instance/loading.svg#symb"></svg>
|
||||
<svg id="configure-btn" aria-label=""><use href="images/actions/instance/config-inactive.svg#symb"></svg>
|
||||
<svg id="console-btn" aria-label=""><use href="images/actions/instance/console-inactive.svg#symb"></svg>
|
||||
|
Reference in New Issue
Block a user