Compare commits

8 Commits

15 changed files with 328 additions and 288 deletions

View File

@@ -6,81 +6,11 @@ import (
"net/http" "net/http"
"proxmoxaas-dashboard/app/common" "proxmoxaas-dashboard/app/common"
"github.com/gerow/go-color"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
func HandleGETAccount(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
account, err := GetUserAccount(auth)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
for k, v := range account.Resources {
switch t := v.(type) {
case NumericResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
}
case StorageResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
}
case ListResource:
l := struct {
Type string
Display bool
Resources []ResourceChart
}{
Type: t.Type,
Display: t.Display,
Resources: []ResourceChart{},
}
for _, r := range t.Total {
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: float64(r.Avail), // usually an int
Unit: "",
})
}
account.Resources[k] = l
}
}
c.HTML(http.StatusOK, "html/account.html", gin.H{
"global": common.Global,
"page": "account",
"account": account,
})
} else {
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
}
}
type Account struct { type Account struct {
Username string Username string
Pools map[string]bool Pools map[string]bool
@@ -151,6 +81,93 @@ type ResourceChart struct {
Avail float64 Avail float64
Prefix string Prefix string
Unit string Unit string
ColorHex string
}
var Red = color.RGB{
R: 1,
G: 0,
B: 0,
}
var Green = color.RGB{
R: 0,
G: 1,
B: 0,
}
func HandleGETAccount(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
account, err := GetUserAccount(auth)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
for k, v := range account.Resources {
switch t := v.(type) {
case NumericResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
}
case StorageResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
}
case ListResource:
l := struct {
Type string
Display bool
Resources []ResourceChart
}{
Type: t.Type,
Display: t.Display,
Resources: []ResourceChart{},
}
for _, r := range t.Total {
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: float64(r.Avail), // usually an int
Unit: "",
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
})
}
account.Resources[k] = l
}
}
c.HTML(http.StatusOK, "html/account.html", gin.H{
"global": common.Global,
"page": "account",
"account": account,
})
} else {
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
}
} }
func GetUserAccount(auth common.Auth) (Account, error) { func GetUserAccount(auth common.Auth) (Account, error) {
@@ -261,3 +278,15 @@ func FormatNumber(val int64, base int64) (float64, string) {
return 0, "" return 0, ""
} }
} }
// interpolate between min and max by normalized (0 - 1) val
func InterpolateColorHSV(min color.RGB, max color.RGB, val float64) color.RGB {
minhsl := min.ToHSL()
maxhsl := max.ToHSL()
interphsl := color.HSL{
H: (1-val)*minhsl.H + (val)*maxhsl.H,
S: (1-val)*minhsl.S + (val)*maxhsl.S,
L: (1-val)*minhsl.L + (val)*maxhsl.L,
}
return interphsl.ToRGB()
}

View File

@@ -13,6 +13,46 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
type VMPath struct {
Node string
Type string
VMID string
}
// imported types from fabric
type InstanceConfig struct {
Type fabric.InstanceType `json:"type"`
Name string `json:"name"`
Proctype string `json:"cpu"`
Cores uint64 `json:"cores"`
Memory uint64 `json:"memory"`
Swap uint64 `json:"swap"`
Volumes map[string]*fabric.Volume `json:"volumes"`
Nets map[string]*fabric.Net `json:"nets"`
Devices map[string]*fabric.Device `json:"devices"`
Boot fabric.BootOrder `json:"boot"`
// overrides
ProctypeSelect common.Select
}
type GlobalConfig struct {
CPU struct {
Whitelist bool
}
}
type UserConfig struct {
CPU struct {
Global []CPUConfig
Nodes map[string][]CPUConfig
}
}
type CPUConfig struct {
Name string
}
func HandleGETConfig(c *gin.Context) { func HandleGETConfig(c *gin.Context) {
auth, err := common.GetAuth(c) auth, err := common.GetAuth(c)
if err == nil { if err == nil {
@@ -155,29 +195,6 @@ func ExtractVMPath(c *gin.Context) (VMPath, error) {
return vm_path, nil return vm_path, nil
} }
type VMPath struct {
Node string
Type string
VMID string
}
// imported types from fabric
type InstanceConfig struct {
Type fabric.InstanceType `json:"type"`
Name string `json:"name"`
Proctype string `json:"cpu"`
Cores uint64 `json:"cores"`
Memory uint64 `json:"memory"`
Swap uint64 `json:"swap"`
Volumes map[string]*fabric.Volume `json:"volumes"`
Nets map[string]*fabric.Net `json:"nets"`
Devices map[string]*fabric.Device `json:"devices"`
Boot fabric.BootOrder `json:"boot"`
// overrides
ProctypeSelect common.Select
}
func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) { func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
config := InstanceConfig{} config := InstanceConfig{}
path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID) path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID)
@@ -208,23 +225,6 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
return config, nil return config, nil
} }
type GlobalConfig struct {
CPU struct {
Whitelist bool
}
}
type UserConfig struct {
CPU struct {
Global []CPUConfig
Nodes map[string][]CPUConfig
}
}
type CPUConfig struct {
Name string
}
func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) { func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
cputypes := common.Select{ cputypes := common.Select{
ID: "proctype", ID: "proctype",

View File

@@ -10,6 +10,39 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
// used in constructing instance cards in index
type Node struct {
Node string `json:"node"`
Status string `json:"status"`
}
// used in constructing instance cards in index
type InstanceCard struct {
VMID uint
Name string
Type string
Status string
Node string
NodeStatus string
ConfigPath string
ConsolePath string
}
// used in retriving cluster tasks
type Task struct {
Type string
Node string
User string
ID string
VMID uint
Status string
EndTime uint
}
type InstanceStatus struct {
Status string
}
func HandleGETIndex(c *gin.Context) { func HandleGETIndex(c *gin.Context) {
auth, err := common.GetAuth(c) auth, err := common.GetAuth(c)
if err == nil { // user should be authed, try to return index with population if err == nil { // user should be authed, try to return index with population
@@ -45,36 +78,6 @@ func HandleGETInstancesFragment(c *gin.Context) {
} }
// used in constructing instance cards in index
type Node struct {
Node string `json:"node"`
Status string `json:"status"`
}
// used in constructing instance cards in index
type InstanceCard struct {
VMID uint
Name string
Type string
Status string
Node string
NodeStatus string
}
// used in retriving cluster tasks
type Task struct {
Type string
Node string
User string
ID string
VMID uint
Status string
}
type InstanceStatus struct {
Status string
}
func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) { func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) {
ctx := common.RequestContext{ ctx := common.RequestContext{
Cookies: map[string]string{ Cookies: map[string]string{
@@ -116,6 +119,12 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
for vmid, instance := range instances { for vmid, instance := range instances {
nodestatus := nodes[instance.Node].Status nodestatus := nodes[instance.Node].Status
instance.NodeStatus = nodestatus instance.NodeStatus = nodestatus
instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
if instance.Type == "qemu" {
instance.ConsolePath = fmt.Sprintf("%s/?console=kvm&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&novnc=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
} else if instance.Type == "lxc" {
instance.ConsolePath = fmt.Sprintf("%s/?console=lxc&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&xtermjs=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
}
instances[vmid] = instance instances[vmid] = instance
} }
@@ -128,6 +137,9 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
return nil, nil, fmt.Errorf("request to /cluster/tasks resulted in %+v", res) return nil, nil, fmt.Errorf("request to /cluster/tasks resulted in %+v", res)
} }
most_recent_task := map[uint]uint{}
expected_state := map[uint]string{}
for _, v := range ctx.Body["data"].([]any) { for _, v := range ctx.Body["data"].([]any) {
task := Task{} task := Task{}
err := mapstructure.Decode(v, &task) err := mapstructure.Decode(v, &task)
@@ -151,8 +163,21 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
} else if !(task.Status == "running" || task.Status == "OK") { // task is not running or finished with status OK } else if !(task.Status == "running" || task.Status == "OK") { // task is not running or finished with status OK
continue continue
} else { // recent task is a start or stop task for user instance which is running or "OK" } else { // recent task is a start or stop task for user instance which is running or "OK"
if task.EndTime > most_recent_task[task.VMID] { // if the task's end time is later than the most recent one encountered
most_recent_task[task.VMID] = task.EndTime // update the most recent task
if task.Type == "qmstart" || task.Type == "vzstart" { // if the task was a start task, update the expected state to running
expected_state[task.VMID] = "running"
} else if task.Type == "qmstop" || task.Type == "vzstop" { // if the task was a stop task, update the expected state to stopped
expected_state[task.VMID] = "stopped"
}
}
}
}
for vmid, expected_state := range expected_state { // for the expected states from recent tasks
if instances[vmid].Status != expected_state { // if the current node's state from /cluster/resources differs from expected state
// get /status/current which is updated faster than /cluster/resources // get /status/current which is updated faster than /cluster/resources
instance := instances[task.VMID] instance := instances[vmid]
path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID) path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID)
ctx.Body = map[string]any{} ctx.Body = map[string]any{}
res, code, err := common.RequestGetAPI(path, ctx) res, code, err := common.RequestGetAPI(path, ctx)
@@ -167,7 +192,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
mapstructure.Decode(ctx.Body["data"], &status) mapstructure.Decode(ctx.Body["data"], &status)
instance.Status = status.Status instance.Status = status.Status
instances[task.VMID] = instance instances[vmid] = instance
} }
} }

View File

@@ -9,6 +9,18 @@ import (
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
) )
// used when requesting GET /access/domains
type GetRealmsBody struct {
Data []Realm `json:"data"`
}
// stores each realm's data
type Realm struct {
Default int `json:"default"`
Realm string `json:"realm"`
Comment string `json:"comment"`
}
func GetLoginRealms() ([]Realm, error) { func GetLoginRealms() ([]Realm, error) {
realms := []Realm{} realms := []Realm{}
@@ -37,18 +49,6 @@ func GetLoginRealms() ([]Realm, error) {
return realms, nil return realms, nil
} }
// used when requesting GET /access/domains
type GetRealmsBody struct {
Data []Realm `json:"data"`
}
// stores each realm's data
type Realm struct {
Default int `json:"default"`
Realm string `json:"realm"`
Comment string `json:"comment"`
}
func HandleGETLogin(c *gin.Context) { func HandleGETLogin(c *gin.Context) {
realms, err := GetLoginRealms() realms, err := GetLoginRealms()
if err != nil { if err != nil {

26
go.mod
View File

@@ -3,9 +3,11 @@ module proxmoxaas-dashboard
go 1.24 go 1.24
require ( require (
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/go-viper/mapstructure/v2 v2.2.1 github.com/go-viper/mapstructure/v2 v2.2.1
github.com/tdewolff/minify v2.3.6+incompatible github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/minify/v2 v2.23.5
proxmoxaas-fabric v0.0.0 proxmoxaas-fabric v0.0.0
) )
@@ -16,17 +18,20 @@ require (
github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/diskfs/go-diskfs v1.5.2 // indirect github.com/diskfs/go-diskfs v1.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect github.com/djherbis/times v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/elliotwutingfeng/asciiset v0.0.0-20240214025120-24af97c84155 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/luthermonson/go-proxmox v0.2.2 // indirect github.com/luthermonson/go-proxmox v0.2.2 // indirect
@@ -35,17 +40,20 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/tdewolff/minify/v2 v2.23.1 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/xattr v0.4.10 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/tdewolff/parse v2.3.4+incompatible // indirect github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/parse/v2 v2.7.23 // indirect github.com/tdewolff/parse/v2 v2.8.0 // indirect
github.com/tdewolff/test v1.0.11 // indirect github.com/tdewolff/test v1.0.11 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.16.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/arch v0.17.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/net v0.40.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -26,7 +26,7 @@
<main> <main>
<section> <section>
<h2><a href="index">Instances</a> / {{.config.Name}}</h2> <h2><a href="index">Instances</a> / {{.config.Name}}</h2>
<form> <form id="config-form">
<fieldset class="w3-card w3-padding"> <fieldset class="w3-card w3-padding">
<legend>Resources</legend> <legend>Resources</legend>
<div class="input-grid" id="resources" style="grid-template-columns: auto auto auto 1fr;"> <div class="input-grid" id="resources" style="grid-template-columns: auto auto auto 1fr;">
@@ -91,7 +91,7 @@
</fieldset> </fieldset>
{{end}} {{end}}
<div class="w3-container w3-center" id="form-actions"> <div class="w3-container w3-center" id="form-actions">
<button class="w3-button w3-margin" id="exit" type="button">EXIT</button> <button class="w3-button w3-margin" id="exit" type="submit">EXIT</button>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -35,7 +35,7 @@
<h2>Instances</h2> <h2>Instances</h2>
<div class="w3-card w3-padding"> <div class="w3-card w3-padding">
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;"> <div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
<form id="vm-search" role="search" class="flex row nowrap"> <form id="vm-search" role="search" class="flex row nowrap" tabindex="0">
<svg role="img" aria-label="Search Instances"><use href="images/common/search.svg#symb"></use></svg> <svg role="img" aria-label="Search Instances"><use href="images/common/search.svg#symb"></use></svg>
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name"> <input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
</form> </form>

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -19,7 +19,7 @@ async function init () {
initNetworks(); initNetworks();
initDevices(); initDevices();
document.querySelector("#exit").addEventListener("click", handleFormExit); document.querySelector("#config-form").addEventListener("submit", handleFormExit);
} }
class VolumeAction extends HTMLElement { class VolumeAction extends HTMLElement {
@@ -530,7 +530,8 @@ async function refreshBoot () {
} }
} }
async function handleFormExit () { async function handleFormExit (event) {
event.preventDefault();
const body = { const body = {
cores: document.querySelector("#cores").value, cores: document.querySelector("#cores").value,
memory: document.querySelector("#ram").value memory: document.querySelector("#ram").value

View File

@@ -1,4 +1,4 @@
import { requestPVE, requestAPI, goToPage, setAppearance, getSearchSettings, goToURL, requestDash, setSVGSrc, setSVGAlt } from "./utils.js"; import { requestPVE, requestAPI, setAppearance, getSearchSettings, requestDash, setSVGSrc, setSVGAlt } from "./utils.js";
import { alert, dialog } from "./dialog.js"; import { alert, dialog } from "./dialog.js";
import { setupClientSync } from "./clientsync.js"; import { setupClientSync } from "./clientsync.js";
import wfaInit from "../modules/wfa.js"; import wfaInit from "../modules/wfa.js";
@@ -122,33 +122,35 @@ class InstanceCard extends HTMLElement {
const powerButton = this.shadowRoot.querySelector("#power-btn"); const powerButton = this.shadowRoot.querySelector("#power-btn");
if (powerButton.classList.contains("clickable")) { if (powerButton.classList.contains("clickable")) {
powerButton.onclick = this.handlePowerButton.bind(this); powerButton.onclick = this.handlePowerButton.bind(this);
powerButton.onkeydown = (event) => {
console.log(event.key, event.key === "Enter")
if (event.key === "Enter") {
event.preventDefault()
this.handlePowerButton()
} }
const configButton = this.shadowRoot.querySelector("#configure-btn");
if (configButton.classList.contains("clickable")) {
configButton.onclick = this.handleConfigButton.bind(this);
} }
const consoleButton = this.shadowRoot.querySelector("#console-btn");
if (consoleButton.classList.contains("clickable")) {
consoleButton.classList.add("clickable");
consoleButton.onclick = this.handleConsoleButton.bind(this);
} }
const deleteButton = this.shadowRoot.querySelector("#delete-btn"); const deleteButton = this.shadowRoot.querySelector("#delete-btn");
if (deleteButton.classList.contains("clickable")) { if (deleteButton.classList.contains("clickable")) {
deleteButton.onclick = this.handleDeleteButton.bind(this); deleteButton.onclick = this.handleDeleteButton.bind(this);
deleteButton.onkeydown = (event) => {
if (event.key === "Enter") {
event.preventDefault()
this.handleDeleteButton()
}
}
} }
} }
setStatusLoading () { setStatusLoading () {
this.status = "loading" this.status = "loading";
let statusicon = this.shadowRoot.querySelector("#status") const statusicon = this.shadowRoot.querySelector("#status");
let powerbtn = this.shadowRoot.querySelector("#power-btn") const powerbtn = this.shadowRoot.querySelector("#power-btn");
setSVGSrc(statusicon, "images/status/loading.svg") setSVGSrc(statusicon, "images/status/loading.svg");
setSVGAlt(statusicon, "instance is loading") setSVGAlt(statusicon, "instance is loading");
setSVGSrc(powerbtn, "images/status/loading.svg") setSVGSrc(powerbtn, "images/status/loading.svg");
setSVGAlt(powerbtn, "") setSVGAlt(powerbtn, "");
} }
async handlePowerButton () { async handlePowerButton () {
@@ -161,7 +163,7 @@ class InstanceCard extends HTMLElement {
const targetAction = this.status === "running" ? "stop" : "start"; const targetAction = this.status === "running" ? "stop" : "start";
const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid }); const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid });
this.setStatusLoading() this.setStatusLoading();
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
@@ -186,20 +188,6 @@ class InstanceCard extends HTMLElement {
} }
} }
handleConfigButton () {
if (!this.actionLock && this.status === "stopped") { // if the action lock is false, and the node is stopped, then navigate to the config page with the node info in the search query
goToPage("config", { 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(window.PVE, data, true);
}
}
handleDeleteButton () { handleDeleteButton () {
if (!this.actionLock && this.status === "stopped") { if (!this.actionLock && this.status === "stopped") {
const header = `Delete VM ${this.vmid}`; const header = `Delete VM ${this.vmid}`;

View File

@@ -114,20 +114,6 @@ export function goToPage (page, data = null) {
window.location.href = `${page}${data ? "?" : ""}${params}`; window.location.href = `${page}${data ? "?" : ""}${params}`;
} }
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, document.title, "height=480,width=848");
}
else {
window.location.assign(url.toString());
}
}
export function getURIData () { export function getURIData () {
const url = new URL(window.location.href); const url = new URL(window.location.href);
return Object.fromEntries(url.searchParams); return Object.fromEntries(url.searchParams);

View File

@@ -237,33 +237,18 @@
{{end}} {{end}}
{{define "boot"}} {{define "boot"}}
<draggable-container id="enabled" data-group="boot"> {{template "boot-container" Map "ID" "enabled" "Name" "Enabled" "Targets" .Enabled}}
<template shadowrootmode="open">
{{template "boot-style"}}
<label>Enabled</label>
<div id="wrapper" style="padding-bottom: 1em;">
{{range .Enabled}}
{{template "boot-target" .}}
{{end}}
</div>
</template>
</draggable-container>
<hr style="padding: 0; margin: 0;"> <hr style="padding: 0; margin: 0;">
<draggable-container id="disabled" data-group="boot"> {{template "boot-container" Map "ID" "disabled" "Name" "Disabled" "Targets" .Disabled}}
<template shadowrootmode="open">
{{template "boot-style"}}
<label>Disabled</label>
<div id="wrapper" style="padding-bottom: 1em;">
{{range .Disabled}}
{{template "boot-target" .}}
{{end}}
</div>
</template>
</draggable-container>
{{end}} {{end}}
{{define "boot-style"}} {{define "boot-container"}}
<draggable-container id="{{.ID}}" data-group="boot">
<template shadowrootmode="open">
<style> <style>
* {
box-sizing: border-box;
}
div.draggable-item.ghost { div.draggable-item.ghost {
border: 1px dashed var(--main-text-color); border: 1px dashed var(--main-text-color);
border-radius: 5px; border-radius: 5px;
@@ -276,12 +261,18 @@
height: 1em; height: 1em;
width: 1em; width: 1em;
} }
* { #wrapper {
-webkit-box-sizing: border-box; padding-bottom: 1em;
-moz-box-sizing: border-box;
box-sizing: border-box;
} }
</style> </style>
<label>{{.Name}}</label>
<div id="wrapper">
{{range .Targets}}
{{template "boot-target" .}}
{{end}}
</div>
</template>
</draggable-container>
{{end}} {{end}}
{{define "boot-target"}} {{define "boot-target"}}

View File

@@ -39,20 +39,24 @@
</div> </div>
<div class="w3-col l2 m2 s6 flex row nowrap" style="height: 1lh;"> <div class="w3-col l2 m2 s6 flex row nowrap" style="height: 1lh;">
{{if and (eq .NodeStatus "online") (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="power-btn" class="clickable" aria-label="shutdown instance" role="button" tabindex=0><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="configure-btn" aria-disabled="true" role="none"><use href="images/actions/instance/config-inactive.svg#symb"></svg>
<a href="{{.ConsolePath}}" target="_blank">
<svg id="console-btn" class="clickable" aria-label="open console"><use href="images/actions/instance/console-active.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> </a>
<svg id="delete-btn" aria-disabled="true" role="none"><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
{{else if and (eq .NodeStatus "online") (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="power-btn" class="clickable" aria-label="start instance" role="button" tabindex=0><use href="images/actions/instance/start.svg#symb"></svg>
<a href="{{.ConfigPath}}">
<svg id="configure-btn" class="clickable" aria-label="change configuration"><use href="images/actions/instance/config-active.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> </a>
<svg id="delete-btn" class="clickable" aria-label="delete instance"><use href="images/actions/instance/delete-active.svg#symb"></svg> <svg id="console-btn" aria-disabled="true" role="none"><use href="images/actions/instance/console-inactive.svg#symb"></svg>
<svg id="delete-btn" class="clickable" aria-label="delete instance" role="button" tabindex=0><use href="images/actions/instance/delete-active.svg#symb"></svg>
{{else if and (eq .NodeStatus "online") (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="power-btn" aria-disabled="true" role="none"><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="configure-btn" aria-disabled="true" role="none"><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> <svg id="console-btn" aria-disabled="true" role="none"><use href="images/actions/instance/console-inactive.svg#symb"></svg>
<svg id="delete-btn" aria-label=""><use href="images/actions/instance/delete-inactive.svg#symb"></svg> <svg id="delete-btn" aria-disabled="true" role="none"><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
{{else}} {{else}}
{{end}} {{end}}
</div> </div>

View File

@@ -13,15 +13,13 @@
margin: 0; margin: 0;
width: 100%; width: 100%;
height: fit-content; height: fit-content;
padding: 10px 10px 10px 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
} }
progress { progress {
width: 100%; width: 100%;
border: 0; border: 0;
height: 1em; height: 1em;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; appearance: none;
} }
#caption { #caption {
@@ -30,14 +28,23 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
progress::-moz-progress-bar {
background: #{{.ColorHex}};
}
progress::-webkit-progress-bar {
background: var(--main-text-color);
}
progress::-webkit-progress-value {
background: #{{.ColorHex}};
}
</style> </style>
<div id="container"> <div id="container">
<progress value="{{.Used}}" max="{{.Max}}"></progress> <progress value="{{.Used}}" max="{{.Max}}" id="resource"></progress>
<p id="caption"> <label id="caption" for="resource">
<span>{{.Name}}</span> <span>{{.Name}}</span>
<span>{{printf "%g" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span> <span>{{printf "%g" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
</p> </label>
</div> </div>
</template> </template>
</resource-chart> </resource-chart>
{{end}}- {{end}}